iosm-cli 0.1.3 → 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/CHANGELOG.md +27 -0
- package/README.md +88 -46
- 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/blast.d.ts +62 -0
- package/dist/core/blast.d.ts.map +1 -0
- package/dist/core/blast.js +448 -0
- package/dist/core/blast.js.map +1 -0
- package/dist/core/contract.d.ts +54 -0
- package/dist/core/contract.d.ts.map +1 -0
- package/dist/core/contract.js +300 -0
- package/dist/core/contract.js.map +1 -0
- 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/semantic/config.d.ts.map +1 -1
- package/dist/core/semantic/config.js +5 -0
- package/dist/core/semantic/config.js.map +1 -1
- package/dist/core/semantic/index.d.ts +1 -1
- package/dist/core/semantic/index.d.ts.map +1 -1
- package/dist/core/semantic/index.js +1 -1
- package/dist/core/semantic/index.js.map +1 -1
- package/dist/core/semantic/runtime.d.ts.map +1 -1
- package/dist/core/semantic/runtime.js +12 -1
- package/dist/core/semantic/runtime.js.map +1 -1
- package/dist/core/semantic/types.d.ts +6 -0
- package/dist/core/semantic/types.d.ts.map +1 -1
- package/dist/core/semantic/types.js +6 -0
- package/dist/core/semantic/types.js.map +1 -1
- package/dist/core/shadow-guard.d.ts +30 -0
- package/dist/core/shadow-guard.d.ts.map +1 -0
- package/dist/core/shadow-guard.js +81 -0
- package/dist/core/shadow-guard.js.map +1 -0
- 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/singular.d.ts +73 -0
- package/dist/core/singular.d.ts.map +1 -0
- package/dist/core/singular.js +413 -0
- package/dist/core/singular.js.map +1 -0
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +14 -2
- 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 +6 -3
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/semantic-search.d.ts.map +1 -1
- package/dist/core/tools/semantic-search.js +1 -0
- package/dist/core/tools/semantic-search.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/main.d.ts.map +1 -1
- package/dist/main.js +8 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +8 -0
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +70 -1
- package/dist/modes/interactive/components/custom-editor.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 +81 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +3481 -870
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/cli-reference.md +29 -1
- package/docs/configuration.md +5 -0
- package/docs/interactive-mode.md +171 -2
- package/docs/orchestration-and-subagents.md +96 -169
- package/package.json +4 -3
|
@@ -6,7 +6,7 @@ import * as crypto from "node:crypto";
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
|
|
9
|
+
import { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, Text, truncateToWidth, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
|
|
10
10
|
import { spawn, spawnSync } from "child_process";
|
|
11
11
|
import { APP_NAME, CHANGELOG_URL, ENV_SESSION_TRACE, ENV_OFFLINE, ENV_SKIP_VERSION_CHECK, getAgentDir, getAuthPath, getDebugLogPath, getModelsPath, getSessionTracePath, getShareViewerUrl, getUpdateInstruction, isSessionTraceEnabled, PACKAGE_NAME, VERSION, } from "../../config.js";
|
|
12
12
|
import { AuthStorage } from "../../core/auth-storage.js";
|
|
@@ -19,10 +19,14 @@ import { ModelRegistry } from "../../core/model-registry.js";
|
|
|
19
19
|
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
|
-
import {
|
|
22
|
+
import { ContractService, normalizeEngineeringContract, } from "../../core/contract.js";
|
|
23
|
+
import { buildProjectIndex, collectChangedFilesSince, ensureProjectIndex, loadProjectIndex, queryProjectIndex, saveProjectIndex, } from "../../core/project-index/index.js";
|
|
24
|
+
import { SingularService, } from "../../core/singular.js";
|
|
25
|
+
import { getDefaultSemanticSearchConfig, getSemanticConfigPath, getSemanticIndexDir, isLikelyEmbeddingModelId, listOllamaLocalModels, listOpenRouterEmbeddingModels, loadMergedSemanticConfig, readScopedSemanticConfig, SemanticConfigMissingError, SemanticIndexRequiredError, SemanticRebuildRequiredError, SemanticSearchRuntime, upsertScopedSemanticSearchConfig, } from "../../core/semantic/index.js";
|
|
23
26
|
import { DefaultResourceLoader } from "../../core/resource-loader.js";
|
|
24
27
|
import { createAgentSession } from "../../core/sdk.js";
|
|
25
28
|
import { createTeamRun, getTeamRun, listTeamRuns } from "../../core/agent-teams.js";
|
|
29
|
+
import { buildSwarmPlanFromSingular, buildSwarmPlanFromTask, runSwarmScheduler, SwarmStateStore, } from "../../core/swarm/index.js";
|
|
26
30
|
import { loadCustomSubagents, resolveCustomSubagentReference, } from "../../core/subagents.js";
|
|
27
31
|
import { getSubagentRun, listSubagentRuns } from "../../core/subagent-runs.js";
|
|
28
32
|
import { SessionManager } from "../../core/session-manager.js";
|
|
@@ -71,6 +75,94 @@ function isExpandable(obj) {
|
|
|
71
75
|
}
|
|
72
76
|
const IOSM_PROFILE_ONLY_COMMANDS = new Set(["iosm", "cycle-list", "cycle-plan", "cycle-status", "cycle-report"]);
|
|
73
77
|
const CHECKPOINT_LABEL_PREFIX = "checkpoint:";
|
|
78
|
+
const INTERRUPT_ABORT_TIMEOUT_MS = 8_000;
|
|
79
|
+
const CONTRACT_FIELD_DEFINITIONS = [
|
|
80
|
+
{ key: "goal", kind: "text", placeholder: "Ship X with measurable impact", help: "Single primary objective." },
|
|
81
|
+
{
|
|
82
|
+
key: "scope_include",
|
|
83
|
+
kind: "list",
|
|
84
|
+
placeholder: "auth/*",
|
|
85
|
+
help: "What is explicitly in scope.",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
key: "scope_exclude",
|
|
89
|
+
kind: "list",
|
|
90
|
+
placeholder: "billing/*",
|
|
91
|
+
help: "What must remain untouched for this change.",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
key: "constraints",
|
|
95
|
+
kind: "list",
|
|
96
|
+
placeholder: "no breaking API changes",
|
|
97
|
+
help: "Hard constraints that cannot be violated.",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
key: "quality_gates",
|
|
101
|
+
kind: "list",
|
|
102
|
+
placeholder: "tests pass",
|
|
103
|
+
help: "Objective quality checks before merge.",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
key: "definition_of_done",
|
|
107
|
+
kind: "list",
|
|
108
|
+
placeholder: "docs updated",
|
|
109
|
+
help: "Completion criteria.",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
key: "assumptions",
|
|
113
|
+
kind: "list",
|
|
114
|
+
placeholder: "API v2 stays backward compatible",
|
|
115
|
+
help: "Assumptions the plan depends on.",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
key: "non_goals",
|
|
119
|
+
kind: "list",
|
|
120
|
+
placeholder: "no redesign in this cycle",
|
|
121
|
+
help: "Intentional exclusions.",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
key: "risks",
|
|
125
|
+
kind: "list",
|
|
126
|
+
placeholder: "migration may affect legacy clients",
|
|
127
|
+
help: "Known delivery risks.",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
key: "deliverables",
|
|
131
|
+
kind: "list",
|
|
132
|
+
placeholder: "CLI command + tests + docs",
|
|
133
|
+
help: "Expected artifacts.",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
key: "success_metrics",
|
|
137
|
+
kind: "list",
|
|
138
|
+
placeholder: "p95 latency < 250ms",
|
|
139
|
+
help: "Measurable target outcomes.",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
key: "stakeholders",
|
|
143
|
+
kind: "list",
|
|
144
|
+
placeholder: "backend team, QA",
|
|
145
|
+
help: "Who should review/accept the result.",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
key: "owner",
|
|
149
|
+
kind: "text",
|
|
150
|
+
placeholder: "team/platform",
|
|
151
|
+
help: "Owner accountable for delivery.",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
key: "timebox",
|
|
155
|
+
kind: "text",
|
|
156
|
+
placeholder: "this sprint",
|
|
157
|
+
help: "Deadline or delivery window.",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
key: "notes",
|
|
161
|
+
kind: "text",
|
|
162
|
+
placeholder: "additional context",
|
|
163
|
+
help: "Free-form context for this contract.",
|
|
164
|
+
},
|
|
165
|
+
];
|
|
74
166
|
const DOCTOR_CLI_TOOL_SPECS = [
|
|
75
167
|
{
|
|
76
168
|
tool: "rg",
|
|
@@ -265,11 +357,14 @@ export class InteractiveMode {
|
|
|
265
357
|
this.pendingWorkingMessage = undefined;
|
|
266
358
|
this.defaultWorkingMessage = "Working...";
|
|
267
359
|
this.activeProfileName = "full";
|
|
360
|
+
this.profilePromptSuffix = undefined;
|
|
268
361
|
this.permissionMode = "ask";
|
|
269
362
|
this.permissionAllowRules = [];
|
|
270
363
|
this.permissionDenyRules = [];
|
|
271
364
|
this.permissionPromptLock = Promise.resolve();
|
|
272
365
|
this.sessionAllowedToolSignatures = new Set();
|
|
366
|
+
this.singularLastEffectiveContract = {};
|
|
367
|
+
this.swarmActiveRunId = undefined;
|
|
273
368
|
this.lastSigintTime = 0;
|
|
274
369
|
this.lastEscapeTime = 0;
|
|
275
370
|
this.changelogMarkdown = undefined;
|
|
@@ -283,6 +378,7 @@ export class InteractiveMode {
|
|
|
283
378
|
this.pendingTools = new Map();
|
|
284
379
|
// Subagent execution tracking with live progress/metadata for task tool calls.
|
|
285
380
|
this.subagentComponents = new Map();
|
|
381
|
+
this.subagentElapsedTimer = undefined;
|
|
286
382
|
// Internal UI metadata emitted by runtime for orchestration rendering.
|
|
287
383
|
this.pendingInternalUserDisplayAliases = [];
|
|
288
384
|
this.pendingAssistantOrchestrationContexts = 0;
|
|
@@ -310,6 +406,7 @@ export class InteractiveMode {
|
|
|
310
406
|
// IOSM automation state
|
|
311
407
|
this.iosmAutomationRun = undefined;
|
|
312
408
|
this.iosmVerificationSession = undefined;
|
|
409
|
+
this.singularAnalysisSession = undefined;
|
|
313
410
|
// Extension UI state
|
|
314
411
|
this.extensionSelector = undefined;
|
|
315
412
|
this.extensionInput = undefined;
|
|
@@ -354,14 +451,19 @@ export class InteractiveMode {
|
|
|
354
451
|
this.footerDataProvider = new FooterDataProvider();
|
|
355
452
|
this.footer = new FooterComponent(session, this.footerDataProvider);
|
|
356
453
|
this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
|
|
357
|
-
|
|
454
|
+
const profile = getAgentProfile(options.profile);
|
|
455
|
+
this.activeProfileName = profile.name;
|
|
456
|
+
this.profilePromptSuffix = profile.systemPromptAppend || undefined;
|
|
358
457
|
this.mcpRuntime = options.mcpRuntime;
|
|
458
|
+
this.contractService = new ContractService({ cwd: this.sessionManager.getCwd() });
|
|
459
|
+
this.singularService = new SingularService({ cwd: this.sessionManager.getCwd() });
|
|
359
460
|
// Apply plan mode and profile badges immediately if set
|
|
360
461
|
if (options.planMode || this.activeProfileName === "plan") {
|
|
361
462
|
this.footer.setPlanMode(true);
|
|
362
463
|
}
|
|
363
464
|
this.footer.setActiveProfile(this.activeProfileName);
|
|
364
465
|
this.session.setIosmAutopilotEnabled(this.activeProfileName === "iosm");
|
|
466
|
+
this.syncRuntimePromptSuffix();
|
|
365
467
|
this.permissionMode = this.settingsManager.getPermissionMode();
|
|
366
468
|
this.permissionAllowRules = this.settingsManager.getPermissionAllowRules();
|
|
367
469
|
this.permissionDenyRules = this.settingsManager.getPermissionDenyRules();
|
|
@@ -373,6 +475,44 @@ export class InteractiveMode {
|
|
|
373
475
|
setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
|
|
374
476
|
initTheme(this.settingsManager.getTheme(), true);
|
|
375
477
|
}
|
|
478
|
+
syncRuntimePromptSuffix() {
|
|
479
|
+
let contractSuffix;
|
|
480
|
+
try {
|
|
481
|
+
contractSuffix = this.contractService.buildPromptContext();
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
contractSuffix = undefined;
|
|
485
|
+
}
|
|
486
|
+
const suffix = [this.profilePromptSuffix, contractSuffix]
|
|
487
|
+
.map((entry) => entry?.trim())
|
|
488
|
+
.filter((entry) => !!entry && entry.length > 0)
|
|
489
|
+
.join("\n\n");
|
|
490
|
+
this.session.setSystemPromptSuffix(suffix || undefined);
|
|
491
|
+
}
|
|
492
|
+
getContractStateSafe() {
|
|
493
|
+
try {
|
|
494
|
+
return this.contractService.getState();
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
498
|
+
this.showWarning(`Contract load failed: ${message}`);
|
|
499
|
+
return undefined;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
getProfileToolNames(profileName) {
|
|
503
|
+
const profile = getAgentProfile(profileName);
|
|
504
|
+
const availableTools = new Set(this.session.getAllTools().map((tool) => tool.name));
|
|
505
|
+
const nextActiveTools = [...profile.tools];
|
|
506
|
+
if (availableTools.has("task"))
|
|
507
|
+
nextActiveTools.push("task");
|
|
508
|
+
if (availableTools.has("todo_write"))
|
|
509
|
+
nextActiveTools.push("todo_write");
|
|
510
|
+
if (availableTools.has("todo_read"))
|
|
511
|
+
nextActiveTools.push("todo_read");
|
|
512
|
+
if (availableTools.has("ask_user"))
|
|
513
|
+
nextActiveTools.push("ask_user");
|
|
514
|
+
return [...new Set(nextActiveTools)];
|
|
515
|
+
}
|
|
376
516
|
getToolPermissionSignature(request) {
|
|
377
517
|
const summary = request.summary.trim().replace(/\s+/g, " ");
|
|
378
518
|
return `${request.toolName}:${summary}`;
|
|
@@ -834,6 +974,72 @@ export class InteractiveMode {
|
|
|
834
974
|
}
|
|
835
975
|
return null;
|
|
836
976
|
}
|
|
977
|
+
getContractArgumentCompletions(prefix) {
|
|
978
|
+
const subcommands = ["ui", "show", "edit", "clear", "help"];
|
|
979
|
+
const scopeFlags = ["--scope"];
|
|
980
|
+
const scopeValues = ["project", "session"];
|
|
981
|
+
const hasTrailingSpace = /\\s$/.test(prefix);
|
|
982
|
+
const tokens = this.parseSlashArgs(prefix);
|
|
983
|
+
const first = tokens[0]?.toLowerCase();
|
|
984
|
+
if (!first || (tokens.length === 1 && !hasTrailingSpace)) {
|
|
985
|
+
const query = first ?? "";
|
|
986
|
+
return [...subcommands, ...scopeFlags]
|
|
987
|
+
.filter((item) => item.includes(query))
|
|
988
|
+
.map((item) => ({ value: item, label: item }));
|
|
989
|
+
}
|
|
990
|
+
const scopeIndex = tokens.findIndex((token) => token === "--scope");
|
|
991
|
+
if (scopeIndex >= 0) {
|
|
992
|
+
const currentValue = tokens[scopeIndex + 1];
|
|
993
|
+
if (!currentValue) {
|
|
994
|
+
return scopeValues.map((value) => ({ value, label: value }));
|
|
995
|
+
}
|
|
996
|
+
return scopeValues
|
|
997
|
+
.filter((value) => value.startsWith(currentValue))
|
|
998
|
+
.map((value) => ({ value, label: value }));
|
|
999
|
+
}
|
|
1000
|
+
const query = hasTrailingSpace ? "" : (tokens[tokens.length - 1] ?? "");
|
|
1001
|
+
if (query.startsWith("--")) {
|
|
1002
|
+
return scopeFlags.filter((flag) => flag.includes(query)).map((flag) => ({ value: flag, label: flag }));
|
|
1003
|
+
}
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
getSingularArgumentCompletions(prefix) {
|
|
1007
|
+
const subcommands = ["help", "last"];
|
|
1008
|
+
const hasTrailingSpace = /\\s$/.test(prefix);
|
|
1009
|
+
const tokens = this.parseSlashArgs(prefix);
|
|
1010
|
+
const first = tokens[0]?.toLowerCase();
|
|
1011
|
+
if (!first || (tokens.length === 1 && !hasTrailingSpace)) {
|
|
1012
|
+
const query = first ?? "";
|
|
1013
|
+
return subcommands.filter((item) => item.includes(query)).map((item) => ({ value: item, label: item }));
|
|
1014
|
+
}
|
|
1015
|
+
return null;
|
|
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
|
+
}
|
|
837
1043
|
setupAutocomplete(fdPath) {
|
|
838
1044
|
// Define commands for autocomplete
|
|
839
1045
|
const builtinCommands = BUILTIN_SLASH_COMMANDS.filter((command) => this.activeProfileName === "iosm" || !IOSM_PROFILE_ONLY_COMMANDS.has(command.name));
|
|
@@ -879,6 +1085,18 @@ export class InteractiveMode {
|
|
|
879
1085
|
if (semanticCommand) {
|
|
880
1086
|
semanticCommand.getArgumentCompletions = (prefix) => this.getSemanticArgumentCompletions(prefix);
|
|
881
1087
|
}
|
|
1088
|
+
const contractCommand = slashCommands.find((command) => command.name === "contract");
|
|
1089
|
+
if (contractCommand) {
|
|
1090
|
+
contractCommand.getArgumentCompletions = (prefix) => this.getContractArgumentCompletions(prefix);
|
|
1091
|
+
}
|
|
1092
|
+
const singularCommand = slashCommands.find((command) => command.name === "singular");
|
|
1093
|
+
if (singularCommand) {
|
|
1094
|
+
singularCommand.getArgumentCompletions = (prefix) => this.getSingularArgumentCompletions(prefix);
|
|
1095
|
+
}
|
|
1096
|
+
const swarmCommand = slashCommands.find((command) => command.name === "swarm");
|
|
1097
|
+
if (swarmCommand) {
|
|
1098
|
+
swarmCommand.getArgumentCompletions = (prefix) => this.getSwarmArgumentCompletions(prefix);
|
|
1099
|
+
}
|
|
882
1100
|
// Convert prompt templates to SlashCommand format for autocomplete
|
|
883
1101
|
const templateCommands = this.session.promptTemplates.map((cmd) => ({
|
|
884
1102
|
name: cmd.name,
|
|
@@ -1132,7 +1350,7 @@ export class InteractiveMode {
|
|
|
1132
1350
|
}
|
|
1133
1351
|
// Commands and keys
|
|
1134
1352
|
const commandsLine = `${pad}${theme.fg("dim", "cmds")} ` +
|
|
1135
|
-
["model", "login", "
|
|
1353
|
+
["model", "login", "contract", "singular", "semantic", "memory", "new"]
|
|
1136
1354
|
.map((c) => theme.fg("accent", `/${c}`))
|
|
1137
1355
|
.join(theme.fg("dim", " "));
|
|
1138
1356
|
const keysLine = `${pad}${theme.fg("dim", "keys")} ` +
|
|
@@ -1562,8 +1780,10 @@ export class InteractiveMode {
|
|
|
1562
1780
|
const names = extensionPaths.map((p) => theme.fg("dim", this.formatDisplayPath(p)));
|
|
1563
1781
|
lines.push(` ${theme.fg("muted", "ext")} ${names.join(theme.fg("dim", ", "))}`);
|
|
1564
1782
|
}
|
|
1783
|
+
const resourceWidth = Math.max(24, this.ui?.terminal?.columns ?? 120);
|
|
1784
|
+
const safeLines = lines.map((line) => visibleWidth(line) > resourceWidth ? truncateToWidth(line, resourceWidth, "") : line);
|
|
1565
1785
|
this.chatContainer.addChild(new Spacer(1));
|
|
1566
|
-
this.chatContainer.addChild(new Text(
|
|
1786
|
+
this.chatContainer.addChild(new Text(safeLines.join("\n"), 0, 0));
|
|
1567
1787
|
}
|
|
1568
1788
|
}
|
|
1569
1789
|
if (showDiagnostics) {
|
|
@@ -2287,6 +2507,7 @@ export class InteractiveMode {
|
|
|
2287
2507
|
this.session.isRetrying ||
|
|
2288
2508
|
this.iosmAutomationRun !== undefined ||
|
|
2289
2509
|
this.iosmVerificationSession !== undefined ||
|
|
2510
|
+
this.singularAnalysisSession !== undefined ||
|
|
2290
2511
|
queuedMessages.steering.length > 0 ||
|
|
2291
2512
|
queuedMessages.followUp.length > 0 ||
|
|
2292
2513
|
queuedMeta.length > 0;
|
|
@@ -2394,6 +2615,11 @@ export class InteractiveMode {
|
|
|
2394
2615
|
await this.handleOrchestrateSlashCommand(text);
|
|
2395
2616
|
return;
|
|
2396
2617
|
}
|
|
2618
|
+
if (text === "/swarm" || text.startsWith("/swarm ")) {
|
|
2619
|
+
this.editor.setText("");
|
|
2620
|
+
await this.handleSwarmCommand(text);
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2397
2623
|
if (text === "/agents" || text.startsWith("/agents ")) {
|
|
2398
2624
|
this.editor.setText("");
|
|
2399
2625
|
await this.handleAgentsSlashCommand(text);
|
|
@@ -2454,6 +2680,16 @@ export class InteractiveMode {
|
|
|
2454
2680
|
await this.handleSemanticCommand(text);
|
|
2455
2681
|
return;
|
|
2456
2682
|
}
|
|
2683
|
+
if (text === "/contract" || text.startsWith("/contract ")) {
|
|
2684
|
+
this.editor.setText("");
|
|
2685
|
+
await this.handleContractCommand(text);
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
if (text === "/singular" || text.startsWith("/singular ")) {
|
|
2689
|
+
this.editor.setText("");
|
|
2690
|
+
await this.handleSingularCommand(text);
|
|
2691
|
+
return;
|
|
2692
|
+
}
|
|
2457
2693
|
if (text === "/settings") {
|
|
2458
2694
|
this.showSettingsSelector();
|
|
2459
2695
|
this.editor.setText("");
|
|
@@ -2642,6 +2878,59 @@ export class InteractiveMode {
|
|
|
2642
2878
|
this.editor.addToHistory?.(text);
|
|
2643
2879
|
};
|
|
2644
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
|
+
}
|
|
2645
2934
|
subscribeToAgent() {
|
|
2646
2935
|
this.unsubscribe = this.session.subscribe(async (event) => {
|
|
2647
2936
|
await this.handleEvent(event);
|
|
@@ -2823,6 +3112,7 @@ export class InteractiveMode {
|
|
|
2823
3112
|
delegateProfile: info.delegateProfile,
|
|
2824
3113
|
delegateItems: info.delegateItems,
|
|
2825
3114
|
});
|
|
3115
|
+
this.ensureSubagentElapsedTimer();
|
|
2826
3116
|
this.ui.requestRender();
|
|
2827
3117
|
}
|
|
2828
3118
|
else if (event.toolName !== "task" && !this.pendingTools.has(event.toolCallId)) {
|
|
@@ -2908,29 +3198,7 @@ export class InteractiveMode {
|
|
|
2908
3198
|
subagent.phase = text.trim();
|
|
2909
3199
|
}
|
|
2910
3200
|
}
|
|
2911
|
-
|
|
2912
|
-
description: subagent.description,
|
|
2913
|
-
profile: subagent.profile,
|
|
2914
|
-
status: "running",
|
|
2915
|
-
phase: subagent.phase ?? "running",
|
|
2916
|
-
phaseState: subagent.phaseState,
|
|
2917
|
-
cwd: subagent.cwd,
|
|
2918
|
-
agent: subagent.agent,
|
|
2919
|
-
lockKey: subagent.lockKey,
|
|
2920
|
-
isolation: subagent.isolation,
|
|
2921
|
-
activeTool: subagent.activeTool,
|
|
2922
|
-
toolCallsStarted: subagent.toolCallsStarted,
|
|
2923
|
-
toolCallsCompleted: subagent.toolCallsCompleted,
|
|
2924
|
-
assistantMessages: subagent.assistantMessages,
|
|
2925
|
-
delegatedTasks: subagent.delegatedTasks,
|
|
2926
|
-
delegatedSucceeded: subagent.delegatedSucceeded,
|
|
2927
|
-
delegatedFailed: subagent.delegatedFailed,
|
|
2928
|
-
delegateIndex: subagent.delegateIndex,
|
|
2929
|
-
delegateTotal: subagent.delegateTotal,
|
|
2930
|
-
delegateDescription: subagent.delegateDescription,
|
|
2931
|
-
delegateProfile: subagent.delegateProfile,
|
|
2932
|
-
delegateItems: subagent.delegateItems,
|
|
2933
|
-
});
|
|
3201
|
+
this.updateRunningSubagentDisplay(subagent);
|
|
2934
3202
|
this.ui.requestRender();
|
|
2935
3203
|
break;
|
|
2936
3204
|
}
|
|
@@ -2994,6 +3262,7 @@ export class InteractiveMode {
|
|
|
2994
3262
|
this.pendingTools.delete(event.toolCallId);
|
|
2995
3263
|
}
|
|
2996
3264
|
this.subagentComponents.delete(event.toolCallId);
|
|
3265
|
+
this.stopSubagentElapsedTimerIfIdle();
|
|
2997
3266
|
this.ui.requestRender();
|
|
2998
3267
|
break;
|
|
2999
3268
|
}
|
|
@@ -3018,6 +3287,7 @@ export class InteractiveMode {
|
|
|
3018
3287
|
}
|
|
3019
3288
|
this.pendingTools.clear();
|
|
3020
3289
|
this.subagentComponents.clear();
|
|
3290
|
+
this.clearSubagentElapsedTimer();
|
|
3021
3291
|
await this.checkShutdownRequested();
|
|
3022
3292
|
this.ui.requestRender();
|
|
3023
3293
|
break;
|
|
@@ -3562,19 +3832,11 @@ export class InteractiveMode {
|
|
|
3562
3832
|
}
|
|
3563
3833
|
applyProfile(profileName) {
|
|
3564
3834
|
const profile = getAgentProfile(profileName);
|
|
3565
|
-
const
|
|
3566
|
-
|
|
3567
|
-
if (availableTools.has("task"))
|
|
3568
|
-
nextActiveTools.push("task");
|
|
3569
|
-
if (availableTools.has("todo_write"))
|
|
3570
|
-
nextActiveTools.push("todo_write");
|
|
3571
|
-
if (availableTools.has("todo_read"))
|
|
3572
|
-
nextActiveTools.push("todo_read");
|
|
3573
|
-
if (availableTools.has("ask_user"))
|
|
3574
|
-
nextActiveTools.push("ask_user");
|
|
3575
|
-
this.session.setActiveToolsByName([...new Set(nextActiveTools)]);
|
|
3835
|
+
const nextActiveTools = this.getProfileToolNames(profile.name);
|
|
3836
|
+
this.session.setActiveToolsByName(nextActiveTools);
|
|
3576
3837
|
this.session.setThinkingLevel(profile.thinkingLevel);
|
|
3577
|
-
this.
|
|
3838
|
+
this.profilePromptSuffix = profile.systemPromptAppend || undefined;
|
|
3839
|
+
this.syncRuntimePromptSuffix();
|
|
3578
3840
|
this.session.setIosmAutopilotEnabled(profile.name === "iosm");
|
|
3579
3841
|
this.activeProfileName = profile.name;
|
|
3580
3842
|
this.footer.setActiveProfile(profile.name);
|
|
@@ -3819,6 +4081,8 @@ export class InteractiveMode {
|
|
|
3819
4081
|
const hasAutomationWork = this.iosmAutomationRun !== undefined;
|
|
3820
4082
|
const verificationSession = this.iosmVerificationSession;
|
|
3821
4083
|
const hasVerificationWork = verificationSession !== undefined;
|
|
4084
|
+
const singularSession = this.singularAnalysisSession;
|
|
4085
|
+
const hasSingularWork = singularSession !== undefined;
|
|
3822
4086
|
const hasMainStreaming = this.session.isStreaming;
|
|
3823
4087
|
const hasRetryWork = this.session.isRetrying;
|
|
3824
4088
|
const hasCompactionWork = this.session.isCompacting;
|
|
@@ -3826,6 +4090,7 @@ export class InteractiveMode {
|
|
|
3826
4090
|
if (!hasPendingQueuedMessages &&
|
|
3827
4091
|
!hasAutomationWork &&
|
|
3828
4092
|
!hasVerificationWork &&
|
|
4093
|
+
!hasSingularWork &&
|
|
3829
4094
|
!hasMainStreaming &&
|
|
3830
4095
|
!hasRetryWork &&
|
|
3831
4096
|
!hasCompactionWork &&
|
|
@@ -3858,7 +4123,9 @@ export class InteractiveMode {
|
|
|
3858
4123
|
? "Stopping IOSM automation..."
|
|
3859
4124
|
: hasVerificationWork
|
|
3860
4125
|
? "Stopping IOSM verification..."
|
|
3861
|
-
:
|
|
4126
|
+
: hasSingularWork
|
|
4127
|
+
? "Stopping /singular analysis..."
|
|
4128
|
+
: "Stopping current run...");
|
|
3862
4129
|
const abortPromises = [];
|
|
3863
4130
|
if (hasMainStreaming) {
|
|
3864
4131
|
abortPromises.push(this.session.abort());
|
|
@@ -3866,8 +4133,30 @@ export class InteractiveMode {
|
|
|
3866
4133
|
if (verificationSession) {
|
|
3867
4134
|
abortPromises.push(verificationSession.abort());
|
|
3868
4135
|
}
|
|
4136
|
+
if (singularSession) {
|
|
4137
|
+
abortPromises.push(singularSession.abort());
|
|
4138
|
+
}
|
|
3869
4139
|
if (abortPromises.length > 0) {
|
|
3870
|
-
|
|
4140
|
+
const settleWithTimeout = (promise) => new Promise((resolve) => {
|
|
4141
|
+
let finished = false;
|
|
4142
|
+
const timeout = setTimeout(() => {
|
|
4143
|
+
if (finished)
|
|
4144
|
+
return;
|
|
4145
|
+
finished = true;
|
|
4146
|
+
resolve("timeout");
|
|
4147
|
+
}, INTERRUPT_ABORT_TIMEOUT_MS);
|
|
4148
|
+
promise.finally(() => {
|
|
4149
|
+
if (finished)
|
|
4150
|
+
return;
|
|
4151
|
+
finished = true;
|
|
4152
|
+
clearTimeout(timeout);
|
|
4153
|
+
resolve("done");
|
|
4154
|
+
});
|
|
4155
|
+
});
|
|
4156
|
+
const settled = await Promise.all(abortPromises.map((promise) => settleWithTimeout(promise)));
|
|
4157
|
+
if (settled.includes("timeout")) {
|
|
4158
|
+
this.showWarning(`Abort is taking longer than ${Math.round(INTERRUPT_ABORT_TIMEOUT_MS / 1000)}s. Try interrupt again if the run is still active.`);
|
|
4159
|
+
}
|
|
3871
4160
|
}
|
|
3872
4161
|
return true;
|
|
3873
4162
|
}
|
|
@@ -5018,6 +5307,7 @@ export class InteractiveMode {
|
|
|
5018
5307
|
});
|
|
5019
5308
|
}
|
|
5020
5309
|
async handleResumeSession(sessionPath) {
|
|
5310
|
+
this.contractService.clear("session");
|
|
5021
5311
|
// Stop loading animation
|
|
5022
5312
|
if (this.loadingAnimation) {
|
|
5023
5313
|
this.loadingAnimation.stop();
|
|
@@ -5035,6 +5325,7 @@ export class InteractiveMode {
|
|
|
5035
5325
|
// Clear and re-render the chat
|
|
5036
5326
|
this.chatContainer.clear();
|
|
5037
5327
|
this.renderInitialMessages();
|
|
5328
|
+
this.syncRuntimePromptSuffix();
|
|
5038
5329
|
this.refreshBuiltInHeader();
|
|
5039
5330
|
this.showStatus("Resumed session");
|
|
5040
5331
|
}
|
|
@@ -5579,7 +5870,7 @@ export class InteractiveMode {
|
|
|
5579
5870
|
authStorage: this.session.modelRegistry.authStorage,
|
|
5580
5871
|
});
|
|
5581
5872
|
}
|
|
5582
|
-
parseSemanticScopeOptions(args) {
|
|
5873
|
+
parseSemanticScopeOptions(args, usage = "Usage: /semantic setup --scope <user|project>") {
|
|
5583
5874
|
let scope;
|
|
5584
5875
|
const rest = [];
|
|
5585
5876
|
for (let index = 0; index < args.length; index++) {
|
|
@@ -5588,7 +5879,7 @@ export class InteractiveMode {
|
|
|
5588
5879
|
if (normalized === "--scope") {
|
|
5589
5880
|
const value = (args[index + 1] ?? "").toLowerCase();
|
|
5590
5881
|
if (!value) {
|
|
5591
|
-
return { scope, rest, error:
|
|
5882
|
+
return { scope, rest, error: usage };
|
|
5592
5883
|
}
|
|
5593
5884
|
if (value !== "user" && value !== "project") {
|
|
5594
5885
|
return { scope, rest, error: `Invalid semantic scope "${value}". Use user or project.` };
|
|
@@ -5628,6 +5919,7 @@ export class InteractiveMode {
|
|
|
5628
5919
|
const lines = [
|
|
5629
5920
|
`configured: ${status.configured ? "yes" : "no"}`,
|
|
5630
5921
|
`enabled: ${status.enabled ? "yes" : "no"}`,
|
|
5922
|
+
`auto_index: ${status.autoIndex ? "on" : "off"}`,
|
|
5631
5923
|
`indexed: ${status.indexed ? "yes" : "no"}`,
|
|
5632
5924
|
`stale: ${status.stale ? `yes${status.staleReason ? ` (${status.staleReason})` : ""}` : "no"}`,
|
|
5633
5925
|
];
|
|
@@ -5912,10 +6204,42 @@ export class InteractiveMode {
|
|
|
5912
6204
|
`scope: ${scope}`,
|
|
5913
6205
|
`provider: ${this.getSemanticSetupProviderLabel(providerType)}`,
|
|
5914
6206
|
`model: ${nextProvider.model}`,
|
|
6207
|
+
`auto_index: ${nextConfig.autoIndex ? "on" : "off"}`,
|
|
5915
6208
|
`config: ${savedPath}`,
|
|
5916
6209
|
`index_dir: ${getSemanticIndexDir(cwd, agentDir)}`,
|
|
5917
6210
|
].join("\n"));
|
|
5918
6211
|
}
|
|
6212
|
+
resolveSemanticAutoIndexWriteScope(cwd, agentDir) {
|
|
6213
|
+
const project = readScopedSemanticConfig("project", cwd, agentDir);
|
|
6214
|
+
if (!project.error && project.file.semanticSearch) {
|
|
6215
|
+
return "project";
|
|
6216
|
+
}
|
|
6217
|
+
return "user";
|
|
6218
|
+
}
|
|
6219
|
+
async updateSemanticAutoIndexSetting(options) {
|
|
6220
|
+
const cwd = this.sessionManager.getCwd();
|
|
6221
|
+
const agentDir = getAgentDir();
|
|
6222
|
+
const merged = loadMergedSemanticConfig(cwd, agentDir);
|
|
6223
|
+
if (!merged.config) {
|
|
6224
|
+
this.showWarning("Semantic search is not configured. Run /semantic setup first.");
|
|
6225
|
+
return;
|
|
6226
|
+
}
|
|
6227
|
+
const scope = options?.scope ?? this.resolveSemanticAutoIndexWriteScope(cwd, agentDir);
|
|
6228
|
+
const value = options?.value ?? !merged.config.autoIndex;
|
|
6229
|
+
const nextConfig = {
|
|
6230
|
+
...merged.config,
|
|
6231
|
+
autoIndex: value,
|
|
6232
|
+
};
|
|
6233
|
+
const savedPath = upsertScopedSemanticSearchConfig(scope, nextConfig, cwd, agentDir);
|
|
6234
|
+
const effective = await this.createSemanticRuntime().status().catch(() => undefined);
|
|
6235
|
+
this.showStatus(`Semantic auto-index ${value ? "enabled" : "disabled"} (${scope}).`);
|
|
6236
|
+
this.showCommandTextBlock("Semantic Auto-Index", [
|
|
6237
|
+
`scope: ${scope}`,
|
|
6238
|
+
`saved: ${value ? "on" : "off"}`,
|
|
6239
|
+
`effective: ${effective ? (effective.autoIndex ? "on" : "off") : "unknown"}`,
|
|
6240
|
+
`config: ${savedPath}`,
|
|
6241
|
+
].join("\n"));
|
|
6242
|
+
}
|
|
5919
6243
|
async runSemanticInteractiveMenu() {
|
|
5920
6244
|
while (true) {
|
|
5921
6245
|
let status;
|
|
@@ -5926,7 +6250,7 @@ export class InteractiveMode {
|
|
|
5926
6250
|
this.reportSemanticError(error, "status");
|
|
5927
6251
|
}
|
|
5928
6252
|
const summary = status
|
|
5929
|
-
? `configured=${status.configured ? "yes" : "no"} indexed=${status.indexed ? "yes" : "no"} stale=${status.stale ? "yes" : "no"}`
|
|
6253
|
+
? `configured=${status.configured ? "yes" : "no"} auto_index=${status.autoIndex ? "on" : "off"} indexed=${status.indexed ? "yes" : "no"} stale=${status.stale ? "yes" : "no"}`
|
|
5930
6254
|
: "status unavailable";
|
|
5931
6255
|
const selected = await this.showExtensionSelector(`/semantic manager\n${summary}`, [
|
|
5932
6256
|
"Configure provider/model",
|
|
@@ -5934,6 +6258,7 @@ export class InteractiveMode {
|
|
|
5934
6258
|
"Index now",
|
|
5935
6259
|
"Rebuild index",
|
|
5936
6260
|
"Query index",
|
|
6261
|
+
`Automatic indexing: ${status?.autoIndex ? "on" : "off"}`,
|
|
5937
6262
|
"Show config/index paths",
|
|
5938
6263
|
"Close",
|
|
5939
6264
|
]);
|
|
@@ -6007,6 +6332,10 @@ export class InteractiveMode {
|
|
|
6007
6332
|
}
|
|
6008
6333
|
continue;
|
|
6009
6334
|
}
|
|
6335
|
+
if (selected.startsWith("Automatic indexing:")) {
|
|
6336
|
+
await this.updateSemanticAutoIndexSetting();
|
|
6337
|
+
continue;
|
|
6338
|
+
}
|
|
6010
6339
|
if (selected === "Show config/index paths") {
|
|
6011
6340
|
const runtime = this.createSemanticRuntime();
|
|
6012
6341
|
const cwd = this.sessionManager.getCwd();
|
|
@@ -6036,6 +6365,11 @@ export class InteractiveMode {
|
|
|
6036
6365
|
].join("\n"));
|
|
6037
6366
|
return;
|
|
6038
6367
|
}
|
|
6368
|
+
if (error instanceof SemanticIndexRequiredError) {
|
|
6369
|
+
this.showWarning(error.message);
|
|
6370
|
+
this.showWarning("Run /semantic index (or enable automatic indexing in /semantic).");
|
|
6371
|
+
return;
|
|
6372
|
+
}
|
|
6039
6373
|
if (error instanceof SemanticRebuildRequiredError) {
|
|
6040
6374
|
this.showWarning(error.message);
|
|
6041
6375
|
this.showWarning("Run /semantic rebuild.");
|
|
@@ -6058,6 +6392,7 @@ export class InteractiveMode {
|
|
|
6058
6392
|
" /semantic",
|
|
6059
6393
|
" /semantic ui",
|
|
6060
6394
|
" /semantic setup [--scope user|project]",
|
|
6395
|
+
" /semantic auto-index [on|off] [--scope user|project]",
|
|
6061
6396
|
" /semantic status",
|
|
6062
6397
|
" /semantic index",
|
|
6063
6398
|
" /semantic rebuild",
|
|
@@ -6079,6 +6414,34 @@ export class InteractiveMode {
|
|
|
6079
6414
|
await this.runSemanticSetupWizard(parsedScope.scope);
|
|
6080
6415
|
return;
|
|
6081
6416
|
}
|
|
6417
|
+
if (subcommand === "auto-index" || subcommand === "autoindex") {
|
|
6418
|
+
const parsedScope = this.parseSemanticScopeOptions(rest, "Usage: /semantic auto-index [on|off] [--scope user|project]");
|
|
6419
|
+
if (parsedScope.error) {
|
|
6420
|
+
this.showWarning(parsedScope.error);
|
|
6421
|
+
return;
|
|
6422
|
+
}
|
|
6423
|
+
let value;
|
|
6424
|
+
if (parsedScope.rest.length > 1) {
|
|
6425
|
+
this.showWarning("Usage: /semantic auto-index [on|off] [--scope user|project]");
|
|
6426
|
+
return;
|
|
6427
|
+
}
|
|
6428
|
+
if (parsedScope.rest.length === 1) {
|
|
6429
|
+
const mode = parsedScope.rest[0]?.toLowerCase();
|
|
6430
|
+
if (mode === "on" || mode === "enable" || mode === "enabled")
|
|
6431
|
+
value = true;
|
|
6432
|
+
else if (mode === "off" || mode === "disable" || mode === "disabled")
|
|
6433
|
+
value = false;
|
|
6434
|
+
else {
|
|
6435
|
+
this.showWarning(`Unknown auto-index mode "${parsedScope.rest[0]}". Use on|off.`);
|
|
6436
|
+
return;
|
|
6437
|
+
}
|
|
6438
|
+
}
|
|
6439
|
+
await this.updateSemanticAutoIndexSetting({
|
|
6440
|
+
scope: parsedScope.scope,
|
|
6441
|
+
value,
|
|
6442
|
+
});
|
|
6443
|
+
return;
|
|
6444
|
+
}
|
|
6082
6445
|
if (subcommand === "status") {
|
|
6083
6446
|
try {
|
|
6084
6447
|
const result = await this.runWithExtensionLoader("Checking semantic index status...", async () => this.createSemanticRuntime().status());
|
|
@@ -6133,979 +6496,3213 @@ export class InteractiveMode {
|
|
|
6133
6496
|
}
|
|
6134
6497
|
this.showWarning(`Unknown /semantic subcommand "${subcommand}". Use /semantic help.`);
|
|
6135
6498
|
}
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6499
|
+
parseContractScopeOptions(args, usage = "Usage: /contract <edit|clear> --scope <project|session>") {
|
|
6500
|
+
let scope;
|
|
6501
|
+
const rest = [];
|
|
6502
|
+
for (let index = 0; index < args.length; index++) {
|
|
6503
|
+
const token = args[index] ?? "";
|
|
6504
|
+
const normalized = token.toLowerCase();
|
|
6505
|
+
if (normalized === "--scope") {
|
|
6506
|
+
const value = (args[index + 1] ?? "").toLowerCase().trim();
|
|
6507
|
+
if (!value) {
|
|
6508
|
+
return { scope, rest, error: usage };
|
|
6509
|
+
}
|
|
6510
|
+
if (value !== "project" && value !== "session") {
|
|
6511
|
+
return { scope, rest, error: `Invalid contract scope "${value}". Use project or session.` };
|
|
6512
|
+
}
|
|
6513
|
+
scope = value;
|
|
6514
|
+
index += 1;
|
|
6515
|
+
continue;
|
|
6516
|
+
}
|
|
6517
|
+
rest.push(token);
|
|
6518
|
+
}
|
|
6519
|
+
return { scope, rest };
|
|
6143
6520
|
}
|
|
6144
|
-
|
|
6145
|
-
return
|
|
6521
|
+
cloneContract(contract) {
|
|
6522
|
+
return normalizeEngineeringContract({
|
|
6523
|
+
...(contract.goal ? { goal: contract.goal } : {}),
|
|
6524
|
+
...(contract.scope_include ? { scope_include: [...contract.scope_include] } : {}),
|
|
6525
|
+
...(contract.scope_exclude ? { scope_exclude: [...contract.scope_exclude] } : {}),
|
|
6526
|
+
...(contract.constraints ? { constraints: [...contract.constraints] } : {}),
|
|
6527
|
+
...(contract.quality_gates ? { quality_gates: [...contract.quality_gates] } : {}),
|
|
6528
|
+
...(contract.definition_of_done ? { definition_of_done: [...contract.definition_of_done] } : {}),
|
|
6529
|
+
...(contract.assumptions ? { assumptions: [...contract.assumptions] } : {}),
|
|
6530
|
+
...(contract.non_goals ? { non_goals: [...contract.non_goals] } : {}),
|
|
6531
|
+
...(contract.risks ? { risks: [...contract.risks] } : {}),
|
|
6532
|
+
...(contract.deliverables ? { deliverables: [...contract.deliverables] } : {}),
|
|
6533
|
+
...(contract.success_metrics ? { success_metrics: [...contract.success_metrics] } : {}),
|
|
6534
|
+
...(contract.stakeholders ? { stakeholders: [...contract.stakeholders] } : {}),
|
|
6535
|
+
...(contract.owner ? { owner: contract.owner } : {}),
|
|
6536
|
+
...(contract.timebox ? { timebox: contract.timebox } : {}),
|
|
6537
|
+
...(contract.notes ? { notes: contract.notes } : {}),
|
|
6538
|
+
});
|
|
6146
6539
|
}
|
|
6147
|
-
|
|
6148
|
-
const
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6540
|
+
formatContractSection(title, contract) {
|
|
6541
|
+
const payload = Object.keys(contract).length > 0 ? contract : {};
|
|
6542
|
+
return `${title}:\n${JSON.stringify(payload, null, 2)}`;
|
|
6543
|
+
}
|
|
6544
|
+
formatContractFieldPreview(field, value) {
|
|
6545
|
+
if (field.kind === "text") {
|
|
6546
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
6547
|
+
return "(empty)";
|
|
6548
|
+
return value.trim();
|
|
6549
|
+
}
|
|
6550
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
6551
|
+
return "(empty)";
|
|
6552
|
+
const values = value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
6553
|
+
if (values.length === 0)
|
|
6554
|
+
return "(empty)";
|
|
6555
|
+
const preview = values.slice(0, 2).join("; ");
|
|
6556
|
+
return values.length > 2 ? `${preview} (+${values.length - 2})` : preview;
|
|
6557
|
+
}
|
|
6558
|
+
showContractState(state) {
|
|
6559
|
+
this.showCommandTextBlock("Contract", [
|
|
6560
|
+
`project_path: ${state.projectPath}${state.hasProjectFile ? "" : " (missing)"}`,
|
|
6561
|
+
"",
|
|
6562
|
+
this.formatContractSection("Project", state.project),
|
|
6563
|
+
"",
|
|
6564
|
+
this.formatContractSection("Session overlay", state.sessionOverlay),
|
|
6565
|
+
"",
|
|
6566
|
+
this.formatContractSection("Effective", state.effective),
|
|
6567
|
+
].join("\n"));
|
|
6154
6568
|
}
|
|
6155
|
-
|
|
6156
|
-
const
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6569
|
+
async editContractFieldValue(scope, field, draft) {
|
|
6570
|
+
const payload = draft;
|
|
6571
|
+
const current = payload[field.key];
|
|
6572
|
+
if (field.kind === "text") {
|
|
6573
|
+
const entered = await this.showExtensionInput(`/contract ${scope}: ${field.key}\n${field.help}\nEnter empty value to clear.`, typeof current === "string" && current.trim().length > 0 ? current : field.placeholder);
|
|
6574
|
+
if (entered === undefined)
|
|
6575
|
+
return undefined;
|
|
6576
|
+
const nextPayload = { ...payload };
|
|
6577
|
+
const normalized = entered.trim();
|
|
6578
|
+
if (normalized.length === 0) {
|
|
6579
|
+
delete nextPayload[field.key];
|
|
6164
6580
|
}
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
timestamp: entry.timestamp,
|
|
6170
|
-
});
|
|
6581
|
+
else {
|
|
6582
|
+
nextPayload[field.key] = normalized;
|
|
6583
|
+
}
|
|
6584
|
+
return normalizeEngineeringContract(nextPayload);
|
|
6171
6585
|
}
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
const
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6586
|
+
const prefill = Array.isArray(current)
|
|
6587
|
+
? current.filter((item) => typeof item === "string").join("\n")
|
|
6588
|
+
: "";
|
|
6589
|
+
const edited = await this.showExtensionEditor(`/contract ${scope}: ${field.key}\n${field.help}\nOne item per line. Empty value clears the field.`, prefill);
|
|
6590
|
+
if (edited === undefined)
|
|
6591
|
+
return undefined;
|
|
6592
|
+
const values = edited
|
|
6593
|
+
.split(/\r?\n/)
|
|
6594
|
+
.map((line) => line.trim())
|
|
6595
|
+
.filter((line) => line.length > 0);
|
|
6596
|
+
const nextPayload = { ...payload };
|
|
6597
|
+
if (values.length === 0) {
|
|
6598
|
+
delete nextPayload[field.key];
|
|
6179
6599
|
}
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
formatCheckpointList(checkpoints) {
|
|
6183
|
-
if (checkpoints.length === 0) {
|
|
6184
|
-
return "No checkpoints yet.\nCreate one with: /checkpoint [name]";
|
|
6600
|
+
else {
|
|
6601
|
+
nextPayload[field.key] = values;
|
|
6185
6602
|
}
|
|
6186
|
-
|
|
6187
|
-
const lines = newestFirst.map((checkpoint, index) => {
|
|
6188
|
-
const target = this.sessionManager.getEntry(checkpoint.targetId);
|
|
6189
|
-
const type = target?.type ?? "missing";
|
|
6190
|
-
return `${index + 1}. ${checkpoint.name} -> ${checkpoint.targetId} (${type}) @ ${checkpoint.timestamp}`;
|
|
6191
|
-
});
|
|
6192
|
-
lines.push("");
|
|
6193
|
-
lines.push("Usage: /rollback [name|index]");
|
|
6194
|
-
return lines.join("\n");
|
|
6603
|
+
return normalizeEngineeringContract(nextPayload);
|
|
6195
6604
|
}
|
|
6196
|
-
|
|
6197
|
-
const
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
const checkpoints = this.getSessionCheckpoints();
|
|
6205
|
-
if (subcommand === "list" || subcommand === "ls") {
|
|
6206
|
-
this.showCommandTextBlock("Checkpoints", this.formatCheckpointList(checkpoints));
|
|
6207
|
-
return;
|
|
6208
|
-
}
|
|
6209
|
-
if (subcommand === "help" || subcommand === "-h" || subcommand === "--help") {
|
|
6210
|
-
this.showCommandTextBlock("Checkpoint Help", ["Usage:", " /checkpoint [name]", " /checkpoint list", "", "Examples:", " /checkpoint", " /checkpoint before-refactor"].join("\n"));
|
|
6211
|
-
return;
|
|
6212
|
-
}
|
|
6213
|
-
const leafId = this.sessionManager.getLeafId();
|
|
6214
|
-
if (!leafId) {
|
|
6215
|
-
this.showWarning("Cannot create checkpoint yet (session has no entries).");
|
|
6216
|
-
return;
|
|
6217
|
-
}
|
|
6218
|
-
const requestedName = args.join(" ");
|
|
6219
|
-
const name = requestedName ? this.normalizeCheckpointName(requestedName) : this.buildDefaultCheckpointName(checkpoints);
|
|
6220
|
-
if (!name) {
|
|
6221
|
-
this.showWarning("Invalid checkpoint name. Use 1-80 visible characters.");
|
|
6222
|
-
return;
|
|
6223
|
-
}
|
|
6224
|
-
this.sessionManager.appendLabelChange(leafId, this.buildCheckpointLabel(name));
|
|
6225
|
-
this.showStatus(`Checkpoint saved: ${name} (${leafId})`);
|
|
6605
|
+
formatContractEditorSummary(scope, draft) {
|
|
6606
|
+
const setCount = CONTRACT_FIELD_DEFINITIONS.filter((field) => {
|
|
6607
|
+
const value = draft[field.key];
|
|
6608
|
+
if (field.kind === "text")
|
|
6609
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
6610
|
+
return Array.isArray(value) && value.length > 0;
|
|
6611
|
+
}).length;
|
|
6612
|
+
return `/contract editor (${scope})\nfilled=${setCount}/${CONTRACT_FIELD_DEFINITIONS.length}`;
|
|
6226
6613
|
}
|
|
6227
|
-
async
|
|
6228
|
-
const
|
|
6229
|
-
|
|
6230
|
-
const checkpoints = this.getSessionCheckpoints();
|
|
6231
|
-
if (subcommand === "list" || subcommand === "ls") {
|
|
6232
|
-
this.showCommandTextBlock("Checkpoints", this.formatCheckpointList(checkpoints));
|
|
6233
|
-
return;
|
|
6234
|
-
}
|
|
6235
|
-
if (subcommand === "help" || subcommand === "-h" || subcommand === "--help") {
|
|
6236
|
-
this.showCommandTextBlock("Rollback Help", [
|
|
6237
|
-
"Usage:",
|
|
6238
|
-
" /rollback",
|
|
6239
|
-
" /rollback <name>",
|
|
6240
|
-
" /rollback <index>",
|
|
6241
|
-
"",
|
|
6242
|
-
"Examples:",
|
|
6243
|
-
" /rollback",
|
|
6244
|
-
" /rollback before-refactor",
|
|
6245
|
-
" /rollback 2",
|
|
6246
|
-
].join("\n"));
|
|
6247
|
-
return;
|
|
6248
|
-
}
|
|
6249
|
-
if (checkpoints.length === 0) {
|
|
6250
|
-
this.showWarning("No checkpoints available. Create one with /checkpoint.");
|
|
6614
|
+
async editContractScope(scope) {
|
|
6615
|
+
const state = this.getContractStateSafe();
|
|
6616
|
+
if (!state)
|
|
6251
6617
|
return;
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6618
|
+
let draft = this.cloneContract(scope === "project" ? state.project : state.sessionOverlay);
|
|
6619
|
+
while (true) {
|
|
6620
|
+
const fieldOptions = CONTRACT_FIELD_DEFINITIONS.map((field) => {
|
|
6621
|
+
const value = draft[field.key];
|
|
6622
|
+
return {
|
|
6623
|
+
field,
|
|
6624
|
+
label: `Edit ${field.key}: ${this.formatContractFieldPreview(field, value)}`,
|
|
6625
|
+
};
|
|
6626
|
+
});
|
|
6627
|
+
const selected = await this.showExtensionSelector(`${this.formatContractEditorSummary(scope, draft)}\nHow to use: select a field and press Enter to edit. Changes are auto-saved immediately.`, [
|
|
6628
|
+
...fieldOptions.map((entry) => entry.label),
|
|
6629
|
+
"Open JSON preview",
|
|
6630
|
+
"Delete scope contract",
|
|
6631
|
+
"Cancel",
|
|
6632
|
+
]);
|
|
6633
|
+
if (!selected || selected.startsWith("Cancel")) {
|
|
6634
|
+
this.showStatus("Contract edit cancelled.");
|
|
6635
|
+
return;
|
|
6264
6636
|
}
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
this.showWarning(`Checkpoint "${selector}" not found.`);
|
|
6269
|
-
return;
|
|
6270
|
-
}
|
|
6637
|
+
if (selected.startsWith("Open JSON preview")) {
|
|
6638
|
+
this.showCommandTextBlock(`Contract Draft (${scope})`, JSON.stringify(Object.keys(draft).length > 0 ? draft : {}, null, 2));
|
|
6639
|
+
continue;
|
|
6271
6640
|
}
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
this.showStatus("Rollback cancelled");
|
|
6280
|
-
return;
|
|
6281
|
-
}
|
|
6282
|
-
const selectedIndex = options.indexOf(picked);
|
|
6283
|
-
if (selectedIndex >= 0) {
|
|
6284
|
-
target = newestFirst[selectedIndex];
|
|
6641
|
+
if (selected.startsWith("Delete scope contract")) {
|
|
6642
|
+
if (scope === "project") {
|
|
6643
|
+
const confirm = await this.showExtensionConfirm("Delete project contract?", `${state.projectPath}\nThis removes .iosm/contract.json`);
|
|
6644
|
+
if (!confirm) {
|
|
6645
|
+
this.showStatus("Project contract delete cancelled.");
|
|
6646
|
+
continue;
|
|
6647
|
+
}
|
|
6285
6648
|
}
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
this.showWarning("No rollback target selected.");
|
|
6290
|
-
return;
|
|
6291
|
-
}
|
|
6292
|
-
try {
|
|
6293
|
-
const result = await this.session.navigateTree(target.targetId, { summarize: false });
|
|
6294
|
-
if (result.cancelled || result.aborted) {
|
|
6295
|
-
this.showStatus("Rollback cancelled");
|
|
6649
|
+
this.contractService.clear(scope);
|
|
6650
|
+
this.syncRuntimePromptSuffix();
|
|
6651
|
+
this.showStatus(`Contract cleared (${scope}).`);
|
|
6296
6652
|
return;
|
|
6297
6653
|
}
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
if (
|
|
6301
|
-
this.
|
|
6654
|
+
const fieldEntry = fieldOptions.find((entry) => entry.label === selected);
|
|
6655
|
+
const field = fieldEntry?.field;
|
|
6656
|
+
if (!field) {
|
|
6657
|
+
this.showWarning("Unknown contract field selection.");
|
|
6658
|
+
continue;
|
|
6659
|
+
}
|
|
6660
|
+
const updated = await this.editContractFieldValue(scope, field, draft);
|
|
6661
|
+
if (!updated) {
|
|
6662
|
+
this.showStatus(`Field edit cancelled (${field.key}).`);
|
|
6663
|
+
continue;
|
|
6664
|
+
}
|
|
6665
|
+
draft = updated;
|
|
6666
|
+
try {
|
|
6667
|
+
this.contractService.save(scope, draft);
|
|
6668
|
+
this.syncRuntimePromptSuffix();
|
|
6669
|
+
this.showStatus(`Saved ${field.key} (${scope}).`);
|
|
6670
|
+
}
|
|
6671
|
+
catch (error) {
|
|
6672
|
+
this.showWarning(error instanceof Error ? error.message : String(error));
|
|
6302
6673
|
}
|
|
6303
|
-
this.showStatus(`Rolled back to checkpoint: ${target.name}`);
|
|
6304
|
-
}
|
|
6305
|
-
catch (error) {
|
|
6306
|
-
this.showError(error instanceof Error ? error.message : String(error));
|
|
6307
6674
|
}
|
|
6308
6675
|
}
|
|
6309
|
-
async
|
|
6310
|
-
const canShowInteractiveSelector = !!this.ui && !!this.editorContainer;
|
|
6311
|
-
if (!canShowInteractiveSelector) {
|
|
6312
|
-
return;
|
|
6313
|
-
}
|
|
6314
|
-
const hasModelIssues = checks.some((check) => (check.label === "Active model" || check.label === "Active model auth" || check.label === "Available models") &&
|
|
6315
|
-
check.level === "fail");
|
|
6316
|
-
const hasMcpIssues = checks.some((check) => check.label === "MCP servers" && check.level !== "ok");
|
|
6317
|
-
const hasSemanticIssues = checks.some((check) => check.label === "Semantic index" && check.level !== "ok");
|
|
6318
|
-
const hasResourceIssues = checks.some((check) => check.label === "Resources" && check.level !== "ok");
|
|
6676
|
+
async runContractInteractiveMenu() {
|
|
6319
6677
|
while (true) {
|
|
6320
|
-
const
|
|
6321
|
-
if (
|
|
6322
|
-
|
|
6323
|
-
|
|
6678
|
+
const state = this.getContractStateSafe();
|
|
6679
|
+
if (!state)
|
|
6680
|
+
return;
|
|
6681
|
+
const selected = await this.showExtensionSelector([
|
|
6682
|
+
`/contract manager`,
|
|
6683
|
+
`project=${state.hasProjectFile ? "yes" : "no"} session_keys=${Object.keys(state.sessionOverlay).length} effective_keys=${Object.keys(state.effective).length}`,
|
|
6684
|
+
`How to use: open effective to inspect merged JSON, edit session for temporary changes, edit project for persistent changes.`,
|
|
6685
|
+
`Field edits are auto-saved right after Enter.`,
|
|
6686
|
+
].join("\n"), [
|
|
6687
|
+
"Open effective contract",
|
|
6688
|
+
"Edit session contract",
|
|
6689
|
+
"Edit project contract",
|
|
6690
|
+
"Copy effective -> session",
|
|
6691
|
+
"Copy effective -> project",
|
|
6692
|
+
"Delete session contract",
|
|
6693
|
+
"Delete project contract",
|
|
6694
|
+
"Close",
|
|
6695
|
+
]);
|
|
6696
|
+
if (!selected || selected.startsWith("Close")) {
|
|
6697
|
+
return;
|
|
6324
6698
|
}
|
|
6325
|
-
if (
|
|
6326
|
-
|
|
6327
|
-
|
|
6699
|
+
if (selected === "Open effective contract") {
|
|
6700
|
+
this.showContractState(state);
|
|
6701
|
+
continue;
|
|
6702
|
+
}
|
|
6703
|
+
if (selected === "Edit session contract") {
|
|
6704
|
+
await this.editContractScope("session");
|
|
6705
|
+
continue;
|
|
6706
|
+
}
|
|
6707
|
+
if (selected === "Edit project contract") {
|
|
6708
|
+
await this.editContractScope("project");
|
|
6709
|
+
continue;
|
|
6710
|
+
}
|
|
6711
|
+
if (selected === "Copy effective -> session") {
|
|
6712
|
+
try {
|
|
6713
|
+
this.contractService.save("session", state.effective);
|
|
6714
|
+
this.syncRuntimePromptSuffix();
|
|
6715
|
+
this.showStatus("Effective contract copied to session overlay.");
|
|
6328
6716
|
}
|
|
6329
|
-
|
|
6717
|
+
catch (error) {
|
|
6718
|
+
this.showWarning(error instanceof Error ? error.message : String(error));
|
|
6719
|
+
}
|
|
6720
|
+
continue;
|
|
6330
6721
|
}
|
|
6331
|
-
if (
|
|
6332
|
-
|
|
6722
|
+
if (selected === "Copy effective -> project") {
|
|
6723
|
+
try {
|
|
6724
|
+
this.contractService.save("project", state.effective);
|
|
6725
|
+
this.syncRuntimePromptSuffix();
|
|
6726
|
+
this.showStatus("Effective contract copied to project.");
|
|
6727
|
+
}
|
|
6728
|
+
catch (error) {
|
|
6729
|
+
this.showWarning(error instanceof Error ? error.message : String(error));
|
|
6730
|
+
}
|
|
6731
|
+
continue;
|
|
6333
6732
|
}
|
|
6334
|
-
if (
|
|
6335
|
-
|
|
6733
|
+
if (selected === "Delete session contract") {
|
|
6734
|
+
this.contractService.clear("session");
|
|
6735
|
+
this.syncRuntimePromptSuffix();
|
|
6736
|
+
this.showStatus("Session contract deleted.");
|
|
6737
|
+
continue;
|
|
6336
6738
|
}
|
|
6337
|
-
if (
|
|
6338
|
-
|
|
6739
|
+
if (selected === "Delete project contract") {
|
|
6740
|
+
const confirm = await this.showExtensionConfirm("Delete project contract?", `${state.projectPath}\nThis removes .iosm/contract.json`);
|
|
6741
|
+
if (!confirm) {
|
|
6742
|
+
this.showStatus("Project contract delete cancelled.");
|
|
6743
|
+
continue;
|
|
6744
|
+
}
|
|
6745
|
+
this.contractService.clear("project");
|
|
6746
|
+
this.syncRuntimePromptSuffix();
|
|
6747
|
+
this.showStatus("Project contract deleted.");
|
|
6339
6748
|
}
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6749
|
+
}
|
|
6750
|
+
}
|
|
6751
|
+
async handleContractCommand(text) {
|
|
6752
|
+
const args = this.parseSlashArgs(text).slice(1);
|
|
6753
|
+
if (args.length === 0 || (args[0]?.toLowerCase() ?? "") === "ui") {
|
|
6754
|
+
await this.runContractInteractiveMenu();
|
|
6755
|
+
return;
|
|
6756
|
+
}
|
|
6757
|
+
const subcommand = (args[0] ?? "").toLowerCase();
|
|
6758
|
+
const rest = args.slice(1);
|
|
6759
|
+
if (subcommand === "help" || subcommand === "-h" || subcommand === "--help") {
|
|
6760
|
+
this.showCommandTextBlock("Contract Help", [
|
|
6761
|
+
"Usage:",
|
|
6762
|
+
" /contract",
|
|
6763
|
+
" /contract ui",
|
|
6764
|
+
" /contract show",
|
|
6765
|
+
" /contract edit --scope <project|session>",
|
|
6766
|
+
" /contract clear --scope <project|session>",
|
|
6767
|
+
" /contract help",
|
|
6768
|
+
"",
|
|
6769
|
+
"Editor model:",
|
|
6770
|
+
" - Fill fields interactively (goal, scope, constraints, quality gates, DoD, risks, etc.)",
|
|
6771
|
+
" - Each field is saved immediately after Enter (no extra Save step)",
|
|
6772
|
+
].join("\n"));
|
|
6773
|
+
return;
|
|
6774
|
+
}
|
|
6775
|
+
if (subcommand === "show" || subcommand === "status" || subcommand === "open") {
|
|
6776
|
+
const state = this.getContractStateSafe();
|
|
6777
|
+
if (!state)
|
|
6344
6778
|
return;
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6779
|
+
this.showContractState(state);
|
|
6780
|
+
return;
|
|
6781
|
+
}
|
|
6782
|
+
if (subcommand === "edit") {
|
|
6783
|
+
const parsed = this.parseContractScopeOptions(rest, "Usage: /contract edit --scope <project|session>");
|
|
6784
|
+
if (parsed.error) {
|
|
6785
|
+
this.showWarning(parsed.error);
|
|
6348
6786
|
return;
|
|
6349
6787
|
}
|
|
6350
|
-
if (
|
|
6351
|
-
|
|
6788
|
+
if (parsed.rest.length > 0) {
|
|
6789
|
+
this.showWarning(`Unexpected arguments for /contract edit: ${parsed.rest.join(" ")}`);
|
|
6352
6790
|
return;
|
|
6353
6791
|
}
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6792
|
+
await this.editContractScope(parsed.scope ?? "session");
|
|
6793
|
+
return;
|
|
6794
|
+
}
|
|
6795
|
+
if (subcommand === "clear" || subcommand === "rm" || subcommand === "remove" || subcommand === "delete") {
|
|
6796
|
+
const parsed = this.parseContractScopeOptions(rest, "Usage: /contract clear --scope <project|session>");
|
|
6797
|
+
if (parsed.error) {
|
|
6798
|
+
this.showWarning(parsed.error);
|
|
6357
6799
|
return;
|
|
6358
6800
|
}
|
|
6359
|
-
if (
|
|
6360
|
-
|
|
6361
|
-
this.showStatus("MCP servers refreshed");
|
|
6362
|
-
continue;
|
|
6363
|
-
}
|
|
6364
|
-
if (selected === "Open semantic manager") {
|
|
6365
|
-
await this.handleSemanticCommand("/semantic");
|
|
6801
|
+
if (!parsed.scope) {
|
|
6802
|
+
this.showWarning("Usage: /contract clear --scope <project|session>");
|
|
6366
6803
|
return;
|
|
6367
6804
|
}
|
|
6368
|
-
if (
|
|
6369
|
-
this.
|
|
6370
|
-
this.settingsManager.setPermissionMode("ask");
|
|
6371
|
-
this.showStatus("Permissions: ask");
|
|
6372
|
-
continue;
|
|
6373
|
-
}
|
|
6374
|
-
if (selected === "Reload resources") {
|
|
6375
|
-
await this.handleReloadCommand();
|
|
6805
|
+
if (parsed.rest.length > 0) {
|
|
6806
|
+
this.showWarning(`Unexpected arguments for /contract clear: ${parsed.rest.join(" ")}`);
|
|
6376
6807
|
return;
|
|
6377
6808
|
}
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
`auth.json: ${getAuthPath()}`,
|
|
6383
|
-
`models.json: ${getModelsPath()}`,
|
|
6384
|
-
`semantic(user): ${getSemanticConfigPath("user", cwd, agentDir)}`,
|
|
6385
|
-
`semantic(project): ${getSemanticConfigPath("project", cwd, agentDir)}`,
|
|
6386
|
-
`semantic(index): ${getSemanticIndexDir(cwd, agentDir)}`,
|
|
6387
|
-
].join("\n"));
|
|
6388
|
-
continue;
|
|
6389
|
-
}
|
|
6809
|
+
this.contractService.clear(parsed.scope);
|
|
6810
|
+
this.syncRuntimePromptSuffix();
|
|
6811
|
+
this.showStatus(`Contract cleared (${parsed.scope}).`);
|
|
6812
|
+
return;
|
|
6390
6813
|
}
|
|
6814
|
+
this.showWarning(`Unknown /contract subcommand "${subcommand}". Use /contract help.`);
|
|
6391
6815
|
}
|
|
6392
|
-
|
|
6393
|
-
const
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
const model = this.session.model;
|
|
6397
|
-
const modelRegistry = this.session.modelRegistry;
|
|
6398
|
-
const allModels = modelRegistry.getAll();
|
|
6399
|
-
const availableModels = modelRegistry.getAvailable();
|
|
6400
|
-
const modelLoadError = modelRegistry.getError();
|
|
6401
|
-
const authProviders = modelRegistry.authStorage.list();
|
|
6402
|
-
const hasModelAuth = model ? modelRegistry.authStorage.hasAuth(model.provider) : false;
|
|
6403
|
-
const extensionErrors = this.session.resourceLoader.getExtensions().errors.length;
|
|
6404
|
-
const skillDiagnostics = this.session.resourceLoader.getSkills().diagnostics.length;
|
|
6405
|
-
const promptDiagnostics = this.session.resourceLoader.getPrompts().diagnostics.length;
|
|
6406
|
-
const themeDiagnostics = this.session.resourceLoader.getThemes().diagnostics.length;
|
|
6407
|
-
const mcpStatuses = this.mcpRuntime?.getServers() ?? [];
|
|
6408
|
-
const mcpConfigErrors = this.mcpRuntime?.getErrors() ?? [];
|
|
6409
|
-
const mcpConnected = mcpStatuses.filter((status) => status.state === "connected").length;
|
|
6410
|
-
const mcpErrored = mcpStatuses.filter((status) => status.state === "error").length;
|
|
6411
|
-
const mcpDisabled = mcpStatuses.filter((status) => !status.enabled).length;
|
|
6412
|
-
const cliToolStatuses = resolveDoctorCliToolStatuses();
|
|
6413
|
-
const missingCliTools = cliToolStatuses.filter((status) => !status.available).map((status) => status.tool);
|
|
6414
|
-
let semanticStatus;
|
|
6415
|
-
let semanticStatusError;
|
|
6416
|
-
try {
|
|
6417
|
-
semanticStatus = await this.createSemanticRuntime().status();
|
|
6816
|
+
normalizeSingularComplexity(value, fallback) {
|
|
6817
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
6818
|
+
if (normalized === "low" || normalized === "medium" || normalized === "high") {
|
|
6819
|
+
return normalized;
|
|
6418
6820
|
}
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
const
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
addCheck("fail", "Active model", "No active model selected", "Run /model and pick a model.");
|
|
6426
|
-
}
|
|
6427
|
-
else if (!hasModelAuth) {
|
|
6428
|
-
addCheck("fail", "Active model auth", `Model ${model.provider}/${model.id} has no auth configured`, `Run /login ${model.provider} or set API key env vars.`);
|
|
6429
|
-
}
|
|
6430
|
-
else {
|
|
6431
|
-
addCheck("ok", "Active model", `${model.provider}/${model.id}`);
|
|
6821
|
+
return fallback;
|
|
6822
|
+
}
|
|
6823
|
+
normalizeSingularBlastRadius(value, fallback) {
|
|
6824
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
6825
|
+
if (normalized === "low" || normalized === "medium" || normalized === "high") {
|
|
6826
|
+
return normalized;
|
|
6432
6827
|
}
|
|
6433
|
-
|
|
6434
|
-
|
|
6828
|
+
return fallback;
|
|
6829
|
+
}
|
|
6830
|
+
normalizeSingularRecommendation(value, fallback) {
|
|
6831
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
6832
|
+
if (normalized === "implement_now" || normalized === "implement_incrementally" || normalized === "defer") {
|
|
6833
|
+
return normalized;
|
|
6435
6834
|
}
|
|
6436
|
-
|
|
6437
|
-
|
|
6835
|
+
return fallback;
|
|
6836
|
+
}
|
|
6837
|
+
normalizeSingularStageFit(value) {
|
|
6838
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
6839
|
+
if (normalized === "needed_now" || normalized === "optional_now" || normalized === "later") {
|
|
6840
|
+
return normalized;
|
|
6438
6841
|
}
|
|
6439
|
-
if (
|
|
6440
|
-
|
|
6842
|
+
if (normalized === "now")
|
|
6843
|
+
return "needed_now";
|
|
6844
|
+
if (normalized === "optional")
|
|
6845
|
+
return "optional_now";
|
|
6846
|
+
return undefined;
|
|
6847
|
+
}
|
|
6848
|
+
toTrimmedString(value, maxLength, fallback) {
|
|
6849
|
+
if (typeof value !== "string")
|
|
6850
|
+
return fallback;
|
|
6851
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
6852
|
+
if (!compact)
|
|
6853
|
+
return fallback;
|
|
6854
|
+
if (compact.length <= maxLength)
|
|
6855
|
+
return compact;
|
|
6856
|
+
return compact.slice(0, maxLength).trim();
|
|
6857
|
+
}
|
|
6858
|
+
toTrimmedStringList(value, maxItems, maxLength = 220) {
|
|
6859
|
+
if (!Array.isArray(value))
|
|
6860
|
+
return [];
|
|
6861
|
+
const lines = [];
|
|
6862
|
+
for (const item of value) {
|
|
6863
|
+
const normalized = this.toTrimmedString(item, maxLength);
|
|
6864
|
+
if (!normalized)
|
|
6865
|
+
continue;
|
|
6866
|
+
lines.push(normalized);
|
|
6867
|
+
if (lines.length >= maxItems)
|
|
6868
|
+
break;
|
|
6441
6869
|
}
|
|
6442
|
-
|
|
6443
|
-
|
|
6870
|
+
return lines;
|
|
6871
|
+
}
|
|
6872
|
+
normalizeSingularImpactAnalysis(value, fallback) {
|
|
6873
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
6874
|
+
return fallback;
|
|
6444
6875
|
}
|
|
6445
|
-
|
|
6446
|
-
|
|
6876
|
+
const payload = value;
|
|
6877
|
+
const codebase = this.toTrimmedString(payload.codebase, 260, fallback?.codebase ?? "Unknown.");
|
|
6878
|
+
const delivery = this.toTrimmedString(payload.delivery, 260, fallback?.delivery ?? "Unknown.");
|
|
6879
|
+
const risks = this.toTrimmedString(payload.risks, 260, fallback?.risks ?? "Unknown.");
|
|
6880
|
+
const operations = this.toTrimmedString(payload.operations, 260, fallback?.operations ?? "Unknown.");
|
|
6881
|
+
if (!codebase || !delivery || !risks || !operations)
|
|
6882
|
+
return fallback;
|
|
6883
|
+
return {
|
|
6884
|
+
codebase,
|
|
6885
|
+
delivery,
|
|
6886
|
+
risks,
|
|
6887
|
+
operations,
|
|
6888
|
+
};
|
|
6889
|
+
}
|
|
6890
|
+
buildSingularContractPromptSection(contract) {
|
|
6891
|
+
const lines = [];
|
|
6892
|
+
if (contract.goal)
|
|
6893
|
+
lines.push(`- goal: ${contract.goal}`);
|
|
6894
|
+
if ((contract.scope_include ?? []).length > 0) {
|
|
6895
|
+
lines.push(`- scope_include: ${(contract.scope_include ?? []).slice(0, 8).join("; ")}`);
|
|
6447
6896
|
}
|
|
6448
|
-
|
|
6449
|
-
|
|
6897
|
+
if ((contract.scope_exclude ?? []).length > 0) {
|
|
6898
|
+
lines.push(`- scope_exclude: ${(contract.scope_exclude ?? []).slice(0, 8).join("; ")}`);
|
|
6450
6899
|
}
|
|
6451
|
-
if (
|
|
6452
|
-
|
|
6900
|
+
if ((contract.constraints ?? []).length > 0) {
|
|
6901
|
+
lines.push(`- constraints: ${(contract.constraints ?? []).slice(0, 10).join("; ")}`);
|
|
6453
6902
|
}
|
|
6454
|
-
|
|
6455
|
-
|
|
6903
|
+
if ((contract.quality_gates ?? []).length > 0) {
|
|
6904
|
+
lines.push(`- quality_gates: ${(contract.quality_gates ?? []).slice(0, 10).join("; ")}`);
|
|
6456
6905
|
}
|
|
6457
|
-
|
|
6458
|
-
|
|
6906
|
+
if ((contract.definition_of_done ?? []).length > 0) {
|
|
6907
|
+
lines.push(`- definition_of_done: ${(contract.definition_of_done ?? []).slice(0, 10).join("; ")}`);
|
|
6459
6908
|
}
|
|
6460
|
-
|
|
6461
|
-
|
|
6909
|
+
if ((contract.non_goals ?? []).length > 0) {
|
|
6910
|
+
lines.push(`- non_goals: ${(contract.non_goals ?? []).slice(0, 8).join("; ")}`);
|
|
6462
6911
|
}
|
|
6463
|
-
|
|
6464
|
-
|
|
6912
|
+
return lines.length > 0 ? lines.join("\n") : "- none";
|
|
6913
|
+
}
|
|
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
|
+
};
|
|
6465
6920
|
}
|
|
6466
|
-
|
|
6467
|
-
|
|
6921
|
+
if (baseline.scannedFiles >= 2500 || baseline.sourceFiles >= 1200) {
|
|
6922
|
+
return {
|
|
6923
|
+
mode: "medium",
|
|
6924
|
+
reason: `scanned=${baseline.scannedFiles}, source=${baseline.sourceFiles}`,
|
|
6925
|
+
};
|
|
6468
6926
|
}
|
|
6469
|
-
|
|
6470
|
-
|
|
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
|
+
};
|
|
6471
6942
|
}
|
|
6472
|
-
|
|
6473
|
-
|
|
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
|
+
};
|
|
6474
6952
|
}
|
|
6475
|
-
|
|
6476
|
-
|
|
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
|
+
};
|
|
6477
6962
|
}
|
|
6478
|
-
|
|
6479
|
-
const requiresRebuild =
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
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
|
+
};
|
|
6484
6978
|
}
|
|
6485
|
-
|
|
6486
|
-
|
|
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
|
+
};
|
|
6487
6995
|
}
|
|
6488
|
-
|
|
6489
|
-
|
|
6996
|
+
try {
|
|
6997
|
+
const status = await this.createSemanticRuntime().status();
|
|
6998
|
+
return this.buildSingularSemanticGuidanceFromStatus(status);
|
|
6490
6999
|
}
|
|
6491
|
-
|
|
6492
|
-
|
|
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
|
+
};
|
|
6493
7010
|
}
|
|
6494
|
-
|
|
6495
|
-
|
|
7011
|
+
}
|
|
7012
|
+
buildSingularAgentPrompt(request, baseline, contract, runtimeGuidance) {
|
|
7013
|
+
const filesHint = baseline.matchedFiles.length > 0
|
|
7014
|
+
? baseline.matchedFiles.slice(0, 12).map((item) => `- ${item}`).join("\n")
|
|
7015
|
+
: "- no direct file matches found in heuristic pass";
|
|
7016
|
+
return [
|
|
7017
|
+
"You are running a feasibility pass for `/singular`.",
|
|
7018
|
+
"Task: analyze the codebase for this request and decide whether to implement now, incrementally, or defer.",
|
|
7019
|
+
"",
|
|
7020
|
+
"Hard requirements:",
|
|
7021
|
+
"- Inspect repository files with tools before final output (at least one tool call).",
|
|
7022
|
+
"- Return a human-readable markdown report (no JSON).",
|
|
7023
|
+
"- Include exactly three options (Option 1, Option 2, Option 3).",
|
|
7024
|
+
"- Each option must contain concrete file paths when possible.",
|
|
7025
|
+
"",
|
|
7026
|
+
"Use this exact template:",
|
|
7027
|
+
"# Singular Feasibility",
|
|
7028
|
+
"Recommendation: implement_now|implement_incrementally|defer",
|
|
7029
|
+
"Reason: <one concise reason>",
|
|
7030
|
+
"Complexity: low|medium|high",
|
|
7031
|
+
"Blast Radius: low|medium|high",
|
|
7032
|
+
"Stage Fit: needed_now|optional_now|later",
|
|
7033
|
+
"Stage Fit Reason: <why this stage fit>",
|
|
7034
|
+
"Impact - Codebase: <impact>",
|
|
7035
|
+
"Impact - Delivery: <impact>",
|
|
7036
|
+
"Impact - Risks: <impact>",
|
|
7037
|
+
"Impact - Operations: <impact>",
|
|
7038
|
+
"",
|
|
7039
|
+
"## Option 1: <title>",
|
|
7040
|
+
"Summary: <summary>",
|
|
7041
|
+
"Complexity: low|medium|high",
|
|
7042
|
+
"Blast Radius: low|medium|high",
|
|
7043
|
+
"When to choose: <guidance>",
|
|
7044
|
+
"Suggested files:",
|
|
7045
|
+
"- <path>",
|
|
7046
|
+
"Plan:",
|
|
7047
|
+
"1. <step>",
|
|
7048
|
+
"Pros:",
|
|
7049
|
+
"- <pro>",
|
|
7050
|
+
"Cons:",
|
|
7051
|
+
"- <con>",
|
|
7052
|
+
"",
|
|
7053
|
+
"## Option 2: <title>",
|
|
7054
|
+
"... same fields ...",
|
|
7055
|
+
"",
|
|
7056
|
+
"## Option 3: <title>",
|
|
7057
|
+
"... same fields ...",
|
|
7058
|
+
"",
|
|
7059
|
+
`Feature request: ${request}`,
|
|
7060
|
+
"",
|
|
7061
|
+
"Baseline scan summary (heuristic pass):",
|
|
7062
|
+
`- scanned_files: ${baseline.scannedFiles}`,
|
|
7063
|
+
`- source_files: ${baseline.sourceFiles}`,
|
|
7064
|
+
`- test_files: ${baseline.testFiles}`,
|
|
7065
|
+
`- baseline_complexity: ${baseline.baselineComplexity}`,
|
|
7066
|
+
`- baseline_blast_radius: ${baseline.baselineBlastRadius}`,
|
|
7067
|
+
`- baseline_recommendation: ${baseline.recommendation}`,
|
|
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
|
+
"",
|
|
7075
|
+
"Matched file hints from baseline (verify, do not assume blindly):",
|
|
7076
|
+
filesHint,
|
|
7077
|
+
"",
|
|
7078
|
+
"Active engineering contract:",
|
|
7079
|
+
this.buildSingularContractPromptSection(contract),
|
|
7080
|
+
].join("\n");
|
|
7081
|
+
}
|
|
7082
|
+
extractLabeledValue(text, labels, maxLength = 320) {
|
|
7083
|
+
for (const label of labels) {
|
|
7084
|
+
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7085
|
+
const match = text.match(new RegExp(`^\\s*${escaped}\\s*:\\s*(.+?)\\s*$`, "im"));
|
|
7086
|
+
if (!match?.[1])
|
|
7087
|
+
continue;
|
|
7088
|
+
const normalized = this.toTrimmedString(match[1], maxLength);
|
|
7089
|
+
if (normalized)
|
|
7090
|
+
return normalized;
|
|
6496
7091
|
}
|
|
6497
|
-
|
|
6498
|
-
|
|
7092
|
+
return undefined;
|
|
7093
|
+
}
|
|
7094
|
+
parseSingularListSection(block, heading, maxItems) {
|
|
7095
|
+
const headings = [
|
|
7096
|
+
"Summary",
|
|
7097
|
+
"Complexity",
|
|
7098
|
+
"Blast Radius",
|
|
7099
|
+
"When to choose",
|
|
7100
|
+
"Suggested files",
|
|
7101
|
+
"Plan",
|
|
7102
|
+
"Pros",
|
|
7103
|
+
"Cons",
|
|
7104
|
+
];
|
|
7105
|
+
const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7106
|
+
const nextHeadings = headings.filter((item) => item !== heading).map((item) => item.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
7107
|
+
const sectionRegex = new RegExp(`(?:^|\\n)\\s*${escapedHeading}\\s*:\\s*([\\s\\S]*?)(?=\\n\\s*(?:${nextHeadings.join("|")})\\s*:|\\n\\s*##\\s*Option\\s*[123]\\s*:|$)`, "i");
|
|
7108
|
+
const sectionMatch = block.match(sectionRegex);
|
|
7109
|
+
if (!sectionMatch?.[1])
|
|
7110
|
+
return [];
|
|
7111
|
+
const result = [];
|
|
7112
|
+
const lines = sectionMatch[1].split(/\r?\n/);
|
|
7113
|
+
for (const rawLine of lines) {
|
|
7114
|
+
let line = rawLine.trim();
|
|
7115
|
+
if (!line)
|
|
7116
|
+
continue;
|
|
7117
|
+
line = line.replace(/^[-*]\s+/, "").replace(/^\d+[.)]\s+/, "").trim();
|
|
7118
|
+
if (!line)
|
|
7119
|
+
continue;
|
|
7120
|
+
result.push(line);
|
|
7121
|
+
if (result.length >= maxItems)
|
|
7122
|
+
break;
|
|
6499
7123
|
}
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
const
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
7124
|
+
return result;
|
|
7125
|
+
}
|
|
7126
|
+
parseSingularOptionsFromText(text, baseline) {
|
|
7127
|
+
const optionRegex = /^##\s*Option\s*([123])\s*:\s*(.+?)\s*$/gim;
|
|
7128
|
+
const matches = [...text.matchAll(optionRegex)];
|
|
7129
|
+
const parsed = [];
|
|
7130
|
+
for (let index = 0; index < matches.length; index += 1) {
|
|
7131
|
+
const current = matches[index];
|
|
7132
|
+
const optionIndexRaw = Number.parseInt(current[1] ?? "", 10);
|
|
7133
|
+
const optionIndex = Number.isInteger(optionIndexRaw) ? Math.max(1, Math.min(3, optionIndexRaw)) - 1 : index;
|
|
7134
|
+
const fallback = baseline.options[optionIndex] ?? baseline.options[Math.min(index, baseline.options.length - 1)];
|
|
7135
|
+
if (!fallback)
|
|
7136
|
+
continue;
|
|
7137
|
+
const bodyStart = (current.index ?? 0) + current[0].length;
|
|
7138
|
+
const bodyEnd = index + 1 < matches.length ? (matches[index + 1].index ?? text.length) : text.length;
|
|
7139
|
+
const body = text.slice(bodyStart, bodyEnd);
|
|
7140
|
+
const title = this.toTrimmedString(current[2], 120, fallback.title) ?? fallback.title;
|
|
7141
|
+
const summary = this.extractLabeledValue(body, ["Summary"], 320) ?? fallback.summary;
|
|
7142
|
+
const complexity = this.normalizeSingularComplexity(this.extractLabeledValue(body, ["Complexity"], 24), fallback.complexity);
|
|
7143
|
+
const blastRadius = this.normalizeSingularBlastRadius(this.extractLabeledValue(body, ["Blast Radius", "Blast"], 24), fallback.blast_radius);
|
|
7144
|
+
const whenToChoose = this.extractLabeledValue(body, ["When to choose"], 220) ?? fallback.when_to_choose;
|
|
7145
|
+
const suggestedFiles = this.parseSingularListSection(body, "Suggested files", 8);
|
|
7146
|
+
const plan = this.parseSingularListSection(body, "Plan", 8);
|
|
7147
|
+
const pros = this.parseSingularListSection(body, "Pros", 6);
|
|
7148
|
+
const cons = this.parseSingularListSection(body, "Cons", 6);
|
|
7149
|
+
parsed.push({
|
|
7150
|
+
id: String(parsed.length + 1),
|
|
7151
|
+
title,
|
|
7152
|
+
summary,
|
|
7153
|
+
complexity,
|
|
7154
|
+
blast_radius: blastRadius,
|
|
7155
|
+
suggested_files: suggestedFiles.length > 0 ? suggestedFiles : fallback.suggested_files,
|
|
7156
|
+
plan: plan.length > 0 ? plan : fallback.plan,
|
|
7157
|
+
pros: pros.length > 0 ? pros : fallback.pros,
|
|
7158
|
+
cons: cons.length > 0 ? cons : fallback.cons,
|
|
7159
|
+
when_to_choose: whenToChoose,
|
|
6515
7160
|
});
|
|
6516
|
-
return;
|
|
6517
|
-
}
|
|
6518
|
-
const lines = [];
|
|
6519
|
-
lines.push(`Timestamp: ${new Date().toISOString()}`);
|
|
6520
|
-
lines.push(`CWD: ${this.sessionManager.getCwd()}`);
|
|
6521
|
-
lines.push(`Session: ${this.sessionManager.getSessionFile() ?? "in-memory"}`);
|
|
6522
|
-
lines.push(`Profile: ${this.activeProfileName}`);
|
|
6523
|
-
lines.push("");
|
|
6524
|
-
for (const check of checks) {
|
|
6525
|
-
const prefix = check.level === "ok" ? "[OK]" : check.level === "warn" ? "[WARN]" : "[FAIL]";
|
|
6526
|
-
lines.push(`${prefix} ${check.label}: ${check.detail}`);
|
|
6527
|
-
}
|
|
6528
|
-
lines.push("");
|
|
6529
|
-
lines.push("External CLI tools:");
|
|
6530
|
-
for (const status of cliToolStatuses) {
|
|
6531
|
-
const prefix = status.available ? "[OK]" : "[WARN]";
|
|
6532
|
-
const sourceLabel = status.source === "missing"
|
|
6533
|
-
? "missing"
|
|
6534
|
-
: `${status.source}${status.command ? ` (${status.command})` : ""}`;
|
|
6535
|
-
lines.push(`${prefix} ${status.tool}: ${sourceLabel}`);
|
|
6536
|
-
if (!status.available && status.hint) {
|
|
6537
|
-
lines.push(` fix: ${status.hint}`);
|
|
6538
|
-
}
|
|
6539
|
-
}
|
|
6540
|
-
if (recommendations.length > 0) {
|
|
6541
|
-
lines.push("");
|
|
6542
|
-
lines.push("Recommended actions:");
|
|
6543
|
-
for (const recommendation of recommendations) {
|
|
6544
|
-
lines.push(`- ${recommendation}`);
|
|
6545
|
-
}
|
|
6546
7161
|
}
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
7162
|
+
while (parsed.length < 3 && parsed.length < baseline.options.length) {
|
|
7163
|
+
const fallback = baseline.options[parsed.length];
|
|
7164
|
+
parsed.push({
|
|
7165
|
+
...fallback,
|
|
7166
|
+
id: String(parsed.length + 1),
|
|
7167
|
+
});
|
|
6553
7168
|
}
|
|
7169
|
+
return {
|
|
7170
|
+
options: parsed.slice(0, 3),
|
|
7171
|
+
parsedCount: matches.length,
|
|
7172
|
+
};
|
|
6554
7173
|
}
|
|
6555
|
-
|
|
6556
|
-
this.
|
|
6557
|
-
this.
|
|
6558
|
-
this.
|
|
6559
|
-
this.
|
|
6560
|
-
this.
|
|
6561
|
-
this.
|
|
6562
|
-
this.
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
this.
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
7174
|
+
parseSingularAgentAnalysisFromText(baseline, rawText) {
|
|
7175
|
+
const recommendationRaw = this.extractLabeledValue(rawText, ["Recommendation"], 64);
|
|
7176
|
+
const recommendation = this.normalizeSingularRecommendation(recommendationRaw, baseline.recommendation);
|
|
7177
|
+
const recommendationReason = this.extractLabeledValue(rawText, ["Reason", "Recommendation Reason"], 320) ?? baseline.recommendationReason;
|
|
7178
|
+
const baselineComplexity = this.normalizeSingularComplexity(this.extractLabeledValue(rawText, ["Complexity"], 24), baseline.baselineComplexity);
|
|
7179
|
+
const baselineBlastRadius = this.normalizeSingularBlastRadius(this.extractLabeledValue(rawText, ["Blast Radius", "Blast"], 24), baseline.baselineBlastRadius);
|
|
7180
|
+
const stageFit = this.normalizeSingularStageFit(this.extractLabeledValue(rawText, ["Stage Fit"], 40)) ?? baseline.stageFit;
|
|
7181
|
+
const stageFitReason = this.extractLabeledValue(rawText, ["Stage Fit Reason"], 280) ?? baseline.stageFitReason;
|
|
7182
|
+
const impactAnalysis = {
|
|
7183
|
+
codebase: this.extractLabeledValue(rawText, ["Impact - Codebase", "Codebase Impact"], 260) ?? "Unknown.",
|
|
7184
|
+
delivery: this.extractLabeledValue(rawText, ["Impact - Delivery", "Delivery Impact"], 260) ?? "Unknown.",
|
|
7185
|
+
risks: this.extractLabeledValue(rawText, ["Impact - Risks", "Risk Impact"], 260) ?? "Unknown.",
|
|
7186
|
+
operations: this.extractLabeledValue(rawText, ["Impact - Operations", "Operations Impact"], 260) ?? "Unknown.",
|
|
7187
|
+
};
|
|
7188
|
+
const { options, parsedCount } = this.parseSingularOptionsFromText(rawText, baseline);
|
|
7189
|
+
return {
|
|
7190
|
+
result: {
|
|
7191
|
+
...baseline,
|
|
7192
|
+
recommendation,
|
|
7193
|
+
recommendationReason,
|
|
7194
|
+
baselineComplexity,
|
|
7195
|
+
baselineBlastRadius,
|
|
7196
|
+
stageFit,
|
|
7197
|
+
stageFitReason,
|
|
7198
|
+
impactAnalysis,
|
|
7199
|
+
options,
|
|
7200
|
+
},
|
|
7201
|
+
parsedOptions: parsedCount,
|
|
7202
|
+
hasRecommendation: recommendationRaw !== undefined,
|
|
7203
|
+
};
|
|
6573
7204
|
}
|
|
6574
|
-
async
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
skippedReason: "No cycle scaffold was available for verification.",
|
|
6579
|
-
};
|
|
7205
|
+
async runSingularAgentFeasibilityPass(request, baseline, contract, runtimeGuidance) {
|
|
7206
|
+
const model = this.session.model;
|
|
7207
|
+
if (!model) {
|
|
7208
|
+
return undefined;
|
|
6580
7209
|
}
|
|
6581
|
-
const
|
|
6582
|
-
process.env[ENV_SESSION_TRACE] = "1";
|
|
6583
|
-
process.env.PI_SESSION_TRACE = "1";
|
|
7210
|
+
const cwd = this.sessionManager.getCwd();
|
|
6584
7211
|
const agentDir = getAgentDir();
|
|
6585
|
-
const settingsManager = SettingsManager.create(
|
|
6586
|
-
const
|
|
6587
|
-
for (const { scope, error } of settingsErrors) {
|
|
6588
|
-
this.showWarning(`Init verify warning (${scope} settings): ${error.message}`);
|
|
6589
|
-
}
|
|
6590
|
-
const authStorage = AuthStorage.create();
|
|
7212
|
+
const settingsManager = SettingsManager.create(cwd, agentDir);
|
|
7213
|
+
const authStorage = AuthStorage.create(getAuthPath());
|
|
6591
7214
|
const modelRegistry = new ModelRegistry(authStorage, getModelsPath());
|
|
6592
7215
|
const resourceLoader = new DefaultResourceLoader({
|
|
6593
|
-
cwd
|
|
7216
|
+
cwd,
|
|
6594
7217
|
agentDir,
|
|
6595
7218
|
settingsManager,
|
|
6596
7219
|
noExtensions: true,
|
|
7220
|
+
noSkills: true,
|
|
7221
|
+
noPromptTemplates: true,
|
|
6597
7222
|
});
|
|
6598
7223
|
await resourceLoader.reload();
|
|
6599
|
-
const { session
|
|
6600
|
-
cwd
|
|
7224
|
+
const { session } = await createAgentSession({
|
|
7225
|
+
cwd,
|
|
6601
7226
|
sessionManager: SessionManager.inMemory(),
|
|
6602
7227
|
settingsManager,
|
|
6603
7228
|
authStorage,
|
|
6604
7229
|
modelRegistry,
|
|
6605
7230
|
resourceLoader,
|
|
7231
|
+
model,
|
|
7232
|
+
profile: "plan",
|
|
7233
|
+
enableTaskTool: false,
|
|
7234
|
+
});
|
|
7235
|
+
let toolCallsStarted = 0;
|
|
7236
|
+
const chunks = [];
|
|
7237
|
+
const eventBridge = this.createIosmVerificationEventBridge({
|
|
7238
|
+
loaderMessage: `Running /singular feasibility analysis... (${appKey(this.keybindings, "interrupt")} to interrupt)`,
|
|
6606
7239
|
});
|
|
6607
|
-
let toolExecutions = 0;
|
|
6608
|
-
const activityLog = [];
|
|
6609
|
-
const pushActivity = (line, persist = false) => {
|
|
6610
|
-
activityLog.push(line);
|
|
6611
|
-
if (activityLog.length > 30) {
|
|
6612
|
-
activityLog.shift();
|
|
6613
|
-
}
|
|
6614
|
-
if (persist) {
|
|
6615
|
-
this.showProgressLine(`IOSM init verify: ${line}`);
|
|
6616
|
-
}
|
|
6617
|
-
};
|
|
6618
|
-
const setVerifyLiveStatus = (message) => {
|
|
6619
|
-
this.setWorkingMessage(`${message} (${appKey(this.keybindings, "interrupt")} to interrupt)`);
|
|
6620
|
-
};
|
|
6621
7240
|
const unsubscribe = session.subscribe((event) => {
|
|
6622
|
-
|
|
6623
|
-
if (event.type === "
|
|
6624
|
-
|
|
6625
|
-
setVerifyLiveStatus("Verifying workspace...");
|
|
7241
|
+
eventBridge(event);
|
|
7242
|
+
if (event.type === "tool_execution_start") {
|
|
7243
|
+
toolCallsStarted += 1;
|
|
6626
7244
|
return;
|
|
6627
7245
|
}
|
|
6628
|
-
if (event.type
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
? String(event.args.command ?? "")
|
|
6635
|
-
: "";
|
|
6636
|
-
const preview = commandRaw.replace(/\s+/g, " ").trim().slice(0, 68);
|
|
6637
|
-
const line = `bash #${toolExecutions}${preview ? ` ${preview}${commandRaw.length > 68 ? "..." : ""}` : ""}`;
|
|
6638
|
-
pushActivity(line);
|
|
6639
|
-
setVerifyLiveStatus(`Verifying workspace · ${line}`);
|
|
6640
|
-
return;
|
|
7246
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
7247
|
+
for (const part of event.message.content) {
|
|
7248
|
+
if (part.type === "text" && part.text.trim()) {
|
|
7249
|
+
chunks.push(part.text.trim());
|
|
7250
|
+
}
|
|
7251
|
+
}
|
|
6641
7252
|
}
|
|
6642
|
-
const line = `${event.toolName} #${toolExecutions}`;
|
|
6643
|
-
pushActivity(line);
|
|
6644
|
-
setVerifyLiveStatus(`Verifying workspace · ${line}`);
|
|
6645
7253
|
});
|
|
6646
|
-
this.
|
|
7254
|
+
this.singularAnalysisSession = session;
|
|
6647
7255
|
try {
|
|
6648
|
-
|
|
7256
|
+
const primaryPrompt = this.buildSingularAgentPrompt(request, baseline, contract, runtimeGuidance);
|
|
7257
|
+
const strictRetryPrompt = [
|
|
7258
|
+
"Retry strict mode:",
|
|
7259
|
+
"- Inspect repository files using tools first.",
|
|
7260
|
+
"- Return markdown report using the exact template from previous prompt.",
|
|
7261
|
+
"- Include Recommendation and Option 1/2/3 sections.",
|
|
7262
|
+
"- Do not return JSON.",
|
|
7263
|
+
].join("\n");
|
|
7264
|
+
const runAttempt = async (promptText) => {
|
|
7265
|
+
const chunkStart = chunks.length;
|
|
7266
|
+
const toolStart = toolCallsStarted;
|
|
7267
|
+
await session.prompt(promptText, {
|
|
7268
|
+
expandPromptTemplates: false,
|
|
7269
|
+
skipIosmAutopilot: true,
|
|
7270
|
+
skipOrchestrationDirective: true,
|
|
7271
|
+
source: "interactive",
|
|
7272
|
+
});
|
|
6649
7273
|
return {
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
"No model available for agent verification. Configure /login or an API key, then re-run /init.",
|
|
6653
|
-
activityLog,
|
|
7274
|
+
text: chunks.slice(chunkStart).join("\n\n").trim(),
|
|
7275
|
+
toolCalls: Math.max(0, toolCallsStarted - toolStart),
|
|
6654
7276
|
};
|
|
7277
|
+
};
|
|
7278
|
+
let attempt = await runAttempt(primaryPrompt);
|
|
7279
|
+
let parsed = this.parseSingularAgentAnalysisFromText(baseline, attempt.text);
|
|
7280
|
+
if ((parsed.parsedOptions < 3 || !parsed.hasRecommendation || attempt.toolCalls === 0) && !session.isStreaming) {
|
|
7281
|
+
attempt = await runAttempt(strictRetryPrompt);
|
|
7282
|
+
parsed = this.parseSingularAgentAnalysisFromText(baseline, attempt.text);
|
|
6655
7283
|
}
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
const heartbeatMs = 10_000;
|
|
6659
|
-
let nextPersistentHeartbeatSec = 30;
|
|
6660
|
-
let timeoutHandle;
|
|
6661
|
-
let heartbeatHandle;
|
|
6662
|
-
try {
|
|
6663
|
-
pushActivity("waiting for model response...", true);
|
|
6664
|
-
setVerifyLiveStatus("Waiting for model response...");
|
|
6665
|
-
heartbeatHandle = setInterval(() => {
|
|
6666
|
-
const elapsedSec = Math.round((Date.now() - startedAt) / 1000);
|
|
6667
|
-
if (toolExecutions === 0 && elapsedSec >= nextPersistentHeartbeatSec) {
|
|
6668
|
-
pushActivity(`still waiting for model response (${elapsedSec}s)`, true);
|
|
6669
|
-
nextPersistentHeartbeatSec += 30;
|
|
6670
|
-
}
|
|
6671
|
-
setVerifyLiveStatus(toolExecutions === 0
|
|
6672
|
-
? `Waiting for model response... ${elapsedSec}s`
|
|
6673
|
-
: `Verifying workspace... ${elapsedSec}s · tool calls=${toolExecutions}`);
|
|
6674
|
-
}, heartbeatMs);
|
|
6675
|
-
await Promise.race([
|
|
6676
|
-
session.prompt(buildIosmAgentVerificationPrompt(result), {
|
|
6677
|
-
expandPromptTemplates: false,
|
|
6678
|
-
skipIosmAutopilot: true,
|
|
6679
|
-
source: "interactive",
|
|
6680
|
-
}),
|
|
6681
|
-
new Promise((_resolve, reject) => {
|
|
6682
|
-
timeoutHandle = setTimeout(() => {
|
|
6683
|
-
reject(new Error(`Verifier timeout after ${Math.round(timeoutMs / 1000)}s.`));
|
|
6684
|
-
}, timeoutMs);
|
|
6685
|
-
}),
|
|
6686
|
-
]);
|
|
7284
|
+
if ((parsed.parsedOptions < 3 || !parsed.hasRecommendation) || toolCallsStarted === 0) {
|
|
7285
|
+
return undefined;
|
|
6687
7286
|
}
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
}
|
|
7287
|
+
return parsed.result;
|
|
7288
|
+
}
|
|
7289
|
+
finally {
|
|
7290
|
+
if (session.isStreaming) {
|
|
7291
|
+
await session.abort().catch(() => {
|
|
7292
|
+
// best effort
|
|
7293
|
+
});
|
|
6695
7294
|
}
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
for (let index = messages.length - 1; index >= 0; index--) {
|
|
6699
|
-
const message = messages[index];
|
|
6700
|
-
if (message.role === "assistant") {
|
|
6701
|
-
lastAssistant = message;
|
|
6702
|
-
break;
|
|
6703
|
-
}
|
|
7295
|
+
if (this.singularAnalysisSession === session) {
|
|
7296
|
+
this.singularAnalysisSession = undefined;
|
|
6704
7297
|
}
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
7298
|
+
unsubscribe();
|
|
7299
|
+
session.dispose();
|
|
7300
|
+
}
|
|
7301
|
+
}
|
|
7302
|
+
formatSingularRunReport(result) {
|
|
7303
|
+
const recommendedId = this.resolveRecommendedSingularOptionId(result);
|
|
7304
|
+
const coverageLine = `${result.scannedFiles} scanned · ${result.sourceFiles} source · ${result.testFiles} tests`;
|
|
7305
|
+
const lines = [
|
|
7306
|
+
`run_id: ${result.runId}`,
|
|
7307
|
+
`request: ${result.request}`,
|
|
7308
|
+
`generated_at: ${result.generatedAt}`,
|
|
7309
|
+
"",
|
|
7310
|
+
"overview:",
|
|
7311
|
+
` recommendation: ${result.recommendation}`,
|
|
7312
|
+
` reason: ${result.recommendationReason}`,
|
|
7313
|
+
` complexity: ${result.baselineComplexity}`,
|
|
7314
|
+
` blast_radius: ${result.baselineBlastRadius}`,
|
|
7315
|
+
` repository_coverage: ${coverageLine}`,
|
|
7316
|
+
];
|
|
7317
|
+
if (result.stageFit) {
|
|
7318
|
+
lines.push(` stage_fit: ${result.stageFit}`);
|
|
7319
|
+
}
|
|
7320
|
+
if (result.stageFitReason) {
|
|
7321
|
+
lines.push(` stage_fit_reason: ${result.stageFitReason}`);
|
|
7322
|
+
}
|
|
7323
|
+
if (result.impactAnalysis) {
|
|
7324
|
+
lines.push("");
|
|
7325
|
+
lines.push("impact_analysis:");
|
|
7326
|
+
lines.push(` codebase: ${result.impactAnalysis.codebase}`);
|
|
7327
|
+
lines.push(` delivery: ${result.impactAnalysis.delivery}`);
|
|
7328
|
+
lines.push(` risks: ${result.impactAnalysis.risks}`);
|
|
7329
|
+
lines.push(` operations: ${result.impactAnalysis.operations}`);
|
|
7330
|
+
}
|
|
7331
|
+
if (result.matchedFiles.length > 0) {
|
|
7332
|
+
lines.push("");
|
|
7333
|
+
lines.push(`matched_files: ${result.matchedFiles.slice(0, 8).join(", ")}`);
|
|
7334
|
+
}
|
|
7335
|
+
if (result.contractSignals.length > 0) {
|
|
7336
|
+
lines.push(`contract_signals: ${result.contractSignals.join(", ")}`);
|
|
7337
|
+
}
|
|
7338
|
+
lines.push("");
|
|
7339
|
+
lines.push("implementation_options:");
|
|
7340
|
+
for (const option of result.options) {
|
|
7341
|
+
const recommendedMark = option.id === recommendedId ? " [recommended]" : "";
|
|
7342
|
+
lines.push(`${option.id}. ${option.title}${recommendedMark} [complexity=${option.complexity}, blast=${option.blast_radius}]`);
|
|
7343
|
+
lines.push(` ${option.summary}`);
|
|
7344
|
+
if (option.when_to_choose) {
|
|
7345
|
+
lines.push(` when_to_choose: ${option.when_to_choose}`);
|
|
6725
7346
|
}
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
7347
|
+
if (option.suggested_files.length > 0) {
|
|
7348
|
+
lines.push(` files: ${option.suggested_files.slice(0, 8).join(", ")}`);
|
|
7349
|
+
}
|
|
7350
|
+
if (option.plan.length > 0) {
|
|
7351
|
+
lines.push(` first_step: ${option.plan[0]}`);
|
|
7352
|
+
}
|
|
7353
|
+
}
|
|
7354
|
+
lines.push("");
|
|
7355
|
+
lines.push("next_action:");
|
|
7356
|
+
lines.push(" choose option 1/2/3, then pick Start with Swarm or Continue without Swarm");
|
|
7357
|
+
return lines.join("\n");
|
|
7358
|
+
}
|
|
7359
|
+
buildSingularExecutionDraft(result, option, contract) {
|
|
7360
|
+
const effectiveContract = contract ?? this.singularLastEffectiveContract ?? {};
|
|
7361
|
+
const defaultQualityGates = [
|
|
7362
|
+
"Targeted tests for changed flows pass.",
|
|
7363
|
+
"No regressions in adjacent user paths.",
|
|
7364
|
+
"Logs/metrics updated for the new behavior.",
|
|
7365
|
+
];
|
|
7366
|
+
const defaultDoD = [
|
|
7367
|
+
"Core behavior implemented and manually validated.",
|
|
7368
|
+
"Automated coverage added for critical path.",
|
|
7369
|
+
"Documentation/changelog updated for user-visible changes.",
|
|
7370
|
+
];
|
|
7371
|
+
const qualityGates = (effectiveContract.quality_gates ?? []).length > 0
|
|
7372
|
+
? (effectiveContract.quality_gates ?? []).slice(0, 10)
|
|
7373
|
+
: defaultQualityGates;
|
|
7374
|
+
const definitionOfDone = (effectiveContract.definition_of_done ?? []).length > 0
|
|
7375
|
+
? (effectiveContract.definition_of_done ?? []).slice(0, 10)
|
|
7376
|
+
: defaultDoD;
|
|
7377
|
+
const constraints = (effectiveContract.constraints ?? []).slice(0, 10);
|
|
7378
|
+
const scopeInclude = (effectiveContract.scope_include ?? []).slice(0, 10);
|
|
7379
|
+
const scopeExclude = (effectiveContract.scope_exclude ?? []).slice(0, 10);
|
|
7380
|
+
const risksFromOption = option.cons.slice(0, 6);
|
|
7381
|
+
const files = option.suggested_files.slice(0, 14);
|
|
7382
|
+
const planSteps = option.plan.length > 0 ? option.plan : ["Implement minimal working path for selected option."];
|
|
7383
|
+
const lines = [
|
|
7384
|
+
"# Singular Execution Plan",
|
|
7385
|
+
"",
|
|
7386
|
+
`Request: ${result.request}`,
|
|
7387
|
+
`Selected option: ${option.id}. ${option.title}`,
|
|
7388
|
+
`Decision context: recommendation=${result.recommendation}, complexity=${option.complexity}, blast_radius=${option.blast_radius}`,
|
|
7389
|
+
...(result.stageFit ? [`Stage fit: ${result.stageFit}`] : []),
|
|
7390
|
+
...(result.stageFitReason ? [`Stage fit reason: ${result.stageFitReason}`] : []),
|
|
7391
|
+
...(option.when_to_choose ? [`When to choose: ${option.when_to_choose}`] : []),
|
|
7392
|
+
"",
|
|
7393
|
+
"## 1) Scope and Boundaries",
|
|
7394
|
+
"In scope:",
|
|
7395
|
+
...(scopeInclude.length > 0 ? scopeInclude.map((item) => `- ${item}`) : ["- Deliver selected option with minimal blast radius."]),
|
|
7396
|
+
"Out of scope:",
|
|
7397
|
+
...(scopeExclude.length > 0 ? scopeExclude.map((item) => `- ${item}`) : ["- Broad refactors outside touched modules."]),
|
|
7398
|
+
"Hard constraints:",
|
|
7399
|
+
...(constraints.length > 0 ? constraints.map((item) => `- ${item}`) : ["- Keep backward compatibility for existing behavior."]),
|
|
7400
|
+
"",
|
|
7401
|
+
"## 2) Implementation Phases",
|
|
7402
|
+
"Phase A - Preparation",
|
|
7403
|
+
"1. Confirm acceptance criteria and edge cases for the selected option.",
|
|
7404
|
+
"2. Lock touched modules and define rollback strategy before coding.",
|
|
7405
|
+
"Phase B - Implementation",
|
|
7406
|
+
...planSteps.map((step, index) => `${index + 1}. ${step}`),
|
|
7407
|
+
"Phase C - Hardening",
|
|
7408
|
+
"1. Run targeted regression checks on impacted flows.",
|
|
7409
|
+
"2. Address review findings and update docs if behavior changed.",
|
|
7410
|
+
"",
|
|
7411
|
+
"## 3) Priority Files",
|
|
7412
|
+
];
|
|
7413
|
+
lines.push(...(files.length > 0 ? files.map((filePath) => `- ${filePath}`) : ["- Determine target files during code scan."]));
|
|
7414
|
+
lines.push("", "## 4) Validation and Quality Gates", "Functional checks:", "- Validate main user flow end-to-end.", "- Validate failure/edge path handling.", "Quality gates:", ...qualityGates.map((gate) => `- [ ] ${gate}`), "", "## 5) Risk Controls and Rollout", ...(result.impactAnalysis?.risks ? [`- Risk focus: ${result.impactAnalysis.risks}`] : ["- Risk focus: maintain safe rollout with quick rollback."]), ...(risksFromOption.length > 0 ? risksFromOption.map((risk) => `- [ ] Mitigate: ${risk}`) : ["- [ ] Track and mitigate newly discovered risks during implementation."]), "- [ ] Prepare rollback checkpoint before merge.", "- [ ] Use incremental rollout/feature-flag if blast radius is medium or high.", "", "## 6) Definition of Done", ...definitionOfDone.map((item) => `- [ ] ${item}`), "", "## 7) Delivery Notes", "- Keep commits scoped by phase (prep -> impl -> hardening).", "- Include tests and docs in the same delivery stream.");
|
|
7415
|
+
return lines.join("\n");
|
|
7416
|
+
}
|
|
7417
|
+
resolveRecommendedSingularOptionId(result) {
|
|
7418
|
+
if (result.recommendation === "defer") {
|
|
7419
|
+
return "3";
|
|
7420
|
+
}
|
|
7421
|
+
if (result.recommendation === "implement_incrementally") {
|
|
7422
|
+
const incremental = result.options.find((option) => /increment|mvp|phased/i.test(`${option.title} ${option.summary}`));
|
|
7423
|
+
return incremental?.id ?? "1";
|
|
7424
|
+
}
|
|
7425
|
+
const nonDefer = result.options.find((option) => !/defer|later|postpone/i.test(`${option.title} ${option.summary}`));
|
|
7426
|
+
return nonDefer?.id ?? "1";
|
|
7427
|
+
}
|
|
7428
|
+
async promptSingularDecision(result) {
|
|
7429
|
+
const recommendedId = this.resolveRecommendedSingularOptionId(result);
|
|
7430
|
+
const options = result.options.map((option) => {
|
|
7431
|
+
const recommendedSuffix = option.id === recommendedId ? " (Recommended)" : "";
|
|
7432
|
+
return `Option ${option.id}${recommendedSuffix}: ${option.title} [risk=${option.blast_radius}]`;
|
|
7433
|
+
});
|
|
7434
|
+
options.push("Close without decision");
|
|
7435
|
+
const selected = await this.showExtensionSelector("/singular: choose next step", options);
|
|
7436
|
+
if (!selected || selected === "Close without decision")
|
|
7437
|
+
return;
|
|
7438
|
+
const match = selected.match(/^Option\s+(\d+)/);
|
|
7439
|
+
if (!match)
|
|
7440
|
+
return;
|
|
7441
|
+
const picked = result.options.find((option) => option.id === match[1]);
|
|
7442
|
+
if (!picked)
|
|
7443
|
+
return;
|
|
7444
|
+
this.showCommandTextBlock("Singular Decision", [
|
|
7445
|
+
`selected: ${picked.id}. ${picked.title}`,
|
|
7446
|
+
`summary: ${picked.summary}`,
|
|
7447
|
+
`complexity: ${picked.complexity}`,
|
|
7448
|
+
`blast_radius: ${picked.blast_radius}`,
|
|
7449
|
+
...(picked.when_to_choose ? [`when_to_choose: ${picked.when_to_choose}`] : []),
|
|
7450
|
+
"",
|
|
7451
|
+
"plan:",
|
|
7452
|
+
...picked.plan.map((step, index) => `${index + 1}. ${step}`),
|
|
7453
|
+
"",
|
|
7454
|
+
"pros:",
|
|
7455
|
+
...picked.pros.map((item) => `- ${item}`),
|
|
7456
|
+
"",
|
|
7457
|
+
"cons:",
|
|
7458
|
+
...picked.cons.map((item) => `- ${item}`),
|
|
7459
|
+
].join("\n"));
|
|
7460
|
+
if (picked.id === "3") {
|
|
7461
|
+
this.showStatus("Singular: defer option selected, implementation postponed.");
|
|
7462
|
+
return;
|
|
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
|
+
}
|
|
7476
|
+
this.editor.setText(this.buildSingularExecutionDraft(result, picked, this.singularLastEffectiveContract));
|
|
7477
|
+
this.showStatus("Singular: detailed execution draft generated in editor.");
|
|
7478
|
+
}
|
|
7479
|
+
showSingularLastSummary() {
|
|
7480
|
+
const last = this.singularService.getLastRun();
|
|
7481
|
+
if (!last) {
|
|
7482
|
+
this.showWarning("No /singular analyses found yet.");
|
|
7483
|
+
return;
|
|
7484
|
+
}
|
|
7485
|
+
this.showCommandTextBlock("Singular Last Run", [
|
|
7486
|
+
`run_id: ${last.runId}`,
|
|
7487
|
+
`generated_at: ${last.generatedAt ?? "unknown"}`,
|
|
7488
|
+
`recommendation: ${last.recommendation ?? "unknown"}`,
|
|
7489
|
+
`request: ${last.request ?? "unknown"}`,
|
|
7490
|
+
`analysis_path: ${last.analysisPath}`,
|
|
7491
|
+
...(last.metaPath ? [`meta_path: ${last.metaPath}`] : []),
|
|
7492
|
+
].join("\n"));
|
|
7493
|
+
}
|
|
7494
|
+
async runSingularAnalysis(request) {
|
|
7495
|
+
let effectiveContract = {};
|
|
7496
|
+
try {
|
|
7497
|
+
effectiveContract = this.contractService.getState().effective;
|
|
7498
|
+
}
|
|
7499
|
+
catch (error) {
|
|
7500
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7501
|
+
this.showWarning(`Contract unavailable, continuing /singular without contract overlay: ${message}`);
|
|
7502
|
+
}
|
|
7503
|
+
this.singularLastEffectiveContract = effectiveContract;
|
|
7504
|
+
try {
|
|
7505
|
+
this.showStatus("Singular: preparing baseline scan...");
|
|
7506
|
+
const baseline = await this.singularService.analyze({
|
|
7507
|
+
request,
|
|
7508
|
+
autosave: false,
|
|
7509
|
+
contract: effectiveContract,
|
|
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
|
+
};
|
|
7525
|
+
let result = baseline;
|
|
7526
|
+
if (!this.session.model) {
|
|
7527
|
+
this.showWarning("No model selected. /singular used heuristic analysis only. Use /model to enable agent feasibility pass.");
|
|
7528
|
+
}
|
|
7529
|
+
else {
|
|
7530
|
+
try {
|
|
7531
|
+
const enriched = await this.runSingularAgentFeasibilityPass(request, baseline, effectiveContract, runtimeGuidance);
|
|
7532
|
+
if (enriched) {
|
|
7533
|
+
result = enriched;
|
|
7534
|
+
}
|
|
7535
|
+
else {
|
|
7536
|
+
this.showWarning("Agent feasibility pass returned incomplete output. Showing heuristic fallback.");
|
|
6742
7537
|
}
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
if (normalizedGuide.trim().length > 0) {
|
|
6748
|
-
authoredGuide = normalizedGuide;
|
|
6749
|
-
pushActivity("IOSM.md authored by agent");
|
|
7538
|
+
}
|
|
7539
|
+
catch (error) {
|
|
7540
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7541
|
+
this.showWarning(`Agent feasibility pass failed. Showing heuristic fallback: ${message}`);
|
|
6750
7542
|
}
|
|
6751
7543
|
}
|
|
6752
|
-
|
|
6753
|
-
|
|
7544
|
+
this.singularService.saveAnalysis(result);
|
|
7545
|
+
this.showStatus(`Singular analysis complete: ${result.runId}`);
|
|
7546
|
+
this.showCommandTextBlock("Singular Analysis", this.formatSingularRunReport(result));
|
|
7547
|
+
await this.promptSingularDecision(result);
|
|
7548
|
+
}
|
|
7549
|
+
catch (error) {
|
|
7550
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7551
|
+
this.showError(`Singular analysis failed: ${message}`);
|
|
7552
|
+
}
|
|
7553
|
+
}
|
|
7554
|
+
async handleSingularCommand(text) {
|
|
7555
|
+
const args = this.parseSlashArgs(text).slice(1);
|
|
7556
|
+
if (args.length === 0) {
|
|
7557
|
+
this.showWarning("Usage: /singular <feature request>");
|
|
7558
|
+
return;
|
|
7559
|
+
}
|
|
7560
|
+
const subcommand = (args[0] ?? "").toLowerCase();
|
|
7561
|
+
if (subcommand === "help" || subcommand === "-h" || subcommand === "--help") {
|
|
7562
|
+
this.showCommandTextBlock("Singular Help", [
|
|
7563
|
+
"Usage:",
|
|
7564
|
+
" /singular <feature request>",
|
|
7565
|
+
" /singular last",
|
|
7566
|
+
" /singular help",
|
|
7567
|
+
"",
|
|
7568
|
+
"Flow:",
|
|
7569
|
+
" /singular -> choose option -> Start with Swarm or Continue without Swarm",
|
|
7570
|
+
"",
|
|
7571
|
+
"Examples:",
|
|
7572
|
+
" /singular add account dashboard",
|
|
7573
|
+
" /singular introduce RBAC for API",
|
|
7574
|
+
].join("\n"));
|
|
7575
|
+
return;
|
|
7576
|
+
}
|
|
7577
|
+
if (subcommand === "last" && args.length === 1) {
|
|
7578
|
+
this.showSingularLastSummary();
|
|
7579
|
+
return;
|
|
7580
|
+
}
|
|
7581
|
+
const request = args.join(" ").trim();
|
|
7582
|
+
if (!request) {
|
|
7583
|
+
this.showWarning("Usage: /singular <feature request>");
|
|
7584
|
+
return;
|
|
7585
|
+
}
|
|
7586
|
+
await this.runSingularAnalysis(request);
|
|
7587
|
+
}
|
|
7588
|
+
parseCheckpointNameFromLabel(label) {
|
|
7589
|
+
if (!label)
|
|
7590
|
+
return undefined;
|
|
7591
|
+
if (!label.startsWith(CHECKPOINT_LABEL_PREFIX))
|
|
7592
|
+
return undefined;
|
|
7593
|
+
const name = label.slice(CHECKPOINT_LABEL_PREFIX.length).trim();
|
|
7594
|
+
return name.length > 0 ? name : undefined;
|
|
7595
|
+
}
|
|
7596
|
+
buildCheckpointLabel(name) {
|
|
7597
|
+
return `${CHECKPOINT_LABEL_PREFIX}${name}`;
|
|
7598
|
+
}
|
|
7599
|
+
normalizeCheckpointName(raw) {
|
|
7600
|
+
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
7601
|
+
if (!normalized)
|
|
7602
|
+
return undefined;
|
|
7603
|
+
if (normalized.length > 80)
|
|
7604
|
+
return undefined;
|
|
7605
|
+
return normalized;
|
|
7606
|
+
}
|
|
7607
|
+
getSessionCheckpoints() {
|
|
7608
|
+
const active = new Map();
|
|
7609
|
+
for (const entry of this.sessionManager.getEntries()) {
|
|
7610
|
+
if (entry.type !== "label")
|
|
7611
|
+
continue;
|
|
7612
|
+
const name = this.parseCheckpointNameFromLabel(entry.label);
|
|
7613
|
+
if (!name) {
|
|
7614
|
+
active.delete(entry.targetId);
|
|
7615
|
+
continue;
|
|
6754
7616
|
}
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
7617
|
+
active.set(entry.targetId, {
|
|
7618
|
+
name,
|
|
7619
|
+
targetId: entry.targetId,
|
|
7620
|
+
labelEntryId: entry.id,
|
|
7621
|
+
timestamp: entry.timestamp,
|
|
7622
|
+
});
|
|
7623
|
+
}
|
|
7624
|
+
return [...active.values()].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
7625
|
+
}
|
|
7626
|
+
buildDefaultCheckpointName(checkpoints) {
|
|
7627
|
+
const used = new Set(checkpoints.map((checkpoint) => checkpoint.name.toLowerCase()));
|
|
7628
|
+
let index = 1;
|
|
7629
|
+
while (used.has(`cp-${index}`)) {
|
|
7630
|
+
index += 1;
|
|
7631
|
+
}
|
|
7632
|
+
return `cp-${index}`;
|
|
7633
|
+
}
|
|
7634
|
+
formatCheckpointList(checkpoints) {
|
|
7635
|
+
if (checkpoints.length === 0) {
|
|
7636
|
+
return "No checkpoints yet.\nCreate one with: /checkpoint [name]";
|
|
7637
|
+
}
|
|
7638
|
+
const newestFirst = [...checkpoints].reverse();
|
|
7639
|
+
const lines = newestFirst.map((checkpoint, index) => {
|
|
7640
|
+
const target = this.sessionManager.getEntry(checkpoint.targetId);
|
|
7641
|
+
const type = target?.type ?? "missing";
|
|
7642
|
+
return `${index + 1}. ${checkpoint.name} -> ${checkpoint.targetId} (${type}) @ ${checkpoint.timestamp}`;
|
|
7643
|
+
});
|
|
7644
|
+
lines.push("");
|
|
7645
|
+
lines.push("Usage: /rollback [name|index]");
|
|
7646
|
+
return lines.join("\n");
|
|
7647
|
+
}
|
|
7648
|
+
formatCheckpointOption(index, checkpoint) {
|
|
7649
|
+
const target = this.sessionManager.getEntry(checkpoint.targetId);
|
|
7650
|
+
const type = target?.type ?? "missing";
|
|
7651
|
+
return `${index}. ${checkpoint.name} -> ${checkpoint.targetId} (${type}) @ ${checkpoint.timestamp}`;
|
|
7652
|
+
}
|
|
7653
|
+
handleCheckpointCommand(text) {
|
|
7654
|
+
const args = this.parseSlashArgs(text).slice(1);
|
|
7655
|
+
const subcommand = args[0]?.toLowerCase();
|
|
7656
|
+
const checkpoints = this.getSessionCheckpoints();
|
|
7657
|
+
if (subcommand === "list" || subcommand === "ls") {
|
|
7658
|
+
this.showCommandTextBlock("Checkpoints", this.formatCheckpointList(checkpoints));
|
|
7659
|
+
return;
|
|
7660
|
+
}
|
|
7661
|
+
if (subcommand === "help" || subcommand === "-h" || subcommand === "--help") {
|
|
7662
|
+
this.showCommandTextBlock("Checkpoint Help", ["Usage:", " /checkpoint [name]", " /checkpoint list", "", "Examples:", " /checkpoint", " /checkpoint before-refactor"].join("\n"));
|
|
7663
|
+
return;
|
|
7664
|
+
}
|
|
7665
|
+
const leafId = this.sessionManager.getLeafId();
|
|
7666
|
+
if (!leafId) {
|
|
7667
|
+
this.showWarning("Cannot create checkpoint yet (session has no entries).");
|
|
7668
|
+
return;
|
|
7669
|
+
}
|
|
7670
|
+
const requestedName = args.join(" ");
|
|
7671
|
+
const name = requestedName ? this.normalizeCheckpointName(requestedName) : this.buildDefaultCheckpointName(checkpoints);
|
|
7672
|
+
if (!name) {
|
|
7673
|
+
this.showWarning("Invalid checkpoint name. Use 1-80 visible characters.");
|
|
7674
|
+
return;
|
|
7675
|
+
}
|
|
7676
|
+
this.sessionManager.appendLabelChange(leafId, this.buildCheckpointLabel(name));
|
|
7677
|
+
this.showStatus(`Checkpoint saved: ${name} (${leafId})`);
|
|
7678
|
+
}
|
|
7679
|
+
async handleRollbackCommand(text) {
|
|
7680
|
+
const args = this.parseSlashArgs(text).slice(1);
|
|
7681
|
+
const subcommand = args[0]?.toLowerCase();
|
|
7682
|
+
const checkpoints = this.getSessionCheckpoints();
|
|
7683
|
+
if (subcommand === "list" || subcommand === "ls") {
|
|
7684
|
+
this.showCommandTextBlock("Checkpoints", this.formatCheckpointList(checkpoints));
|
|
7685
|
+
return;
|
|
7686
|
+
}
|
|
7687
|
+
if (subcommand === "help" || subcommand === "-h" || subcommand === "--help") {
|
|
7688
|
+
this.showCommandTextBlock("Rollback Help", [
|
|
7689
|
+
"Usage:",
|
|
7690
|
+
" /rollback",
|
|
7691
|
+
" /rollback <name>",
|
|
7692
|
+
" /rollback <index>",
|
|
7693
|
+
"",
|
|
7694
|
+
"Examples:",
|
|
7695
|
+
" /rollback",
|
|
7696
|
+
" /rollback before-refactor",
|
|
7697
|
+
" /rollback 2",
|
|
7698
|
+
].join("\n"));
|
|
7699
|
+
return;
|
|
7700
|
+
}
|
|
7701
|
+
if (checkpoints.length === 0) {
|
|
7702
|
+
this.showWarning("No checkpoints available. Create one with /checkpoint.");
|
|
7703
|
+
return;
|
|
7704
|
+
}
|
|
7705
|
+
const newestFirst = [...checkpoints].reverse();
|
|
7706
|
+
const selector = args.join(" ").trim();
|
|
7707
|
+
let target = newestFirst[0];
|
|
7708
|
+
if (selector) {
|
|
7709
|
+
const numeric = Number.parseInt(selector, 10);
|
|
7710
|
+
if (Number.isFinite(numeric) && `${numeric}` === selector) {
|
|
7711
|
+
target = newestFirst[numeric - 1];
|
|
7712
|
+
if (!target) {
|
|
7713
|
+
this.showWarning(`Checkpoint index ${numeric} is out of range.`);
|
|
7714
|
+
return;
|
|
7715
|
+
}
|
|
7716
|
+
}
|
|
7717
|
+
else {
|
|
7718
|
+
target = newestFirst.find((checkpoint) => checkpoint.name === selector);
|
|
7719
|
+
if (!target) {
|
|
7720
|
+
this.showWarning(`Checkpoint "${selector}" not found.`);
|
|
7721
|
+
return;
|
|
7722
|
+
}
|
|
7723
|
+
}
|
|
7724
|
+
}
|
|
7725
|
+
else {
|
|
7726
|
+
const canShowInteractiveSelector = !!this.ui && !!this.editorContainer;
|
|
7727
|
+
if (canShowInteractiveSelector) {
|
|
7728
|
+
const options = newestFirst.map((checkpoint, index) => this.formatCheckpointOption(index + 1, checkpoint));
|
|
7729
|
+
const picked = await this.showExtensionSelector("/rollback: choose checkpoint", options);
|
|
7730
|
+
if (!picked) {
|
|
7731
|
+
this.showStatus("Rollback cancelled");
|
|
7732
|
+
return;
|
|
7733
|
+
}
|
|
7734
|
+
const selectedIndex = options.indexOf(picked);
|
|
7735
|
+
if (selectedIndex >= 0) {
|
|
7736
|
+
target = newestFirst[selectedIndex];
|
|
7737
|
+
}
|
|
7738
|
+
}
|
|
7739
|
+
}
|
|
7740
|
+
if (!target) {
|
|
7741
|
+
this.showWarning("No rollback target selected.");
|
|
7742
|
+
return;
|
|
7743
|
+
}
|
|
7744
|
+
try {
|
|
7745
|
+
const result = await this.session.navigateTree(target.targetId, { summarize: false });
|
|
7746
|
+
if (result.cancelled || result.aborted) {
|
|
7747
|
+
this.showStatus("Rollback cancelled");
|
|
7748
|
+
return;
|
|
7749
|
+
}
|
|
7750
|
+
this.chatContainer.clear();
|
|
7751
|
+
this.renderInitialMessages();
|
|
7752
|
+
if (result.editorText && !this.editor.getText().trim()) {
|
|
7753
|
+
this.editor.setText(result.editorText);
|
|
7754
|
+
}
|
|
7755
|
+
this.showStatus(`Rolled back to checkpoint: ${target.name}`);
|
|
7756
|
+
}
|
|
7757
|
+
catch (error) {
|
|
7758
|
+
this.showError(error instanceof Error ? error.message : String(error));
|
|
7759
|
+
}
|
|
7760
|
+
}
|
|
7761
|
+
async runDoctorInteractiveFixes(checks) {
|
|
7762
|
+
const canShowInteractiveSelector = !!this.ui && !!this.editorContainer;
|
|
7763
|
+
if (!canShowInteractiveSelector) {
|
|
7764
|
+
return;
|
|
7765
|
+
}
|
|
7766
|
+
const hasModelIssues = checks.some((check) => (check.label === "Active model" || check.label === "Active model auth" || check.label === "Available models") &&
|
|
7767
|
+
check.level === "fail");
|
|
7768
|
+
const hasMcpIssues = checks.some((check) => check.label === "MCP servers" && check.level !== "ok");
|
|
7769
|
+
const hasSemanticIssues = checks.some((check) => check.label === "Semantic index" && check.level !== "ok");
|
|
7770
|
+
const hasContractIssues = checks.some((check) => check.label === "Contract state" && check.level !== "ok");
|
|
7771
|
+
const hasResourceIssues = checks.some((check) => check.label === "Resources" && check.level !== "ok");
|
|
7772
|
+
while (true) {
|
|
7773
|
+
const options = [];
|
|
7774
|
+
if (hasModelIssues) {
|
|
7775
|
+
options.push("Open model selector");
|
|
7776
|
+
options.push("Login provider");
|
|
7777
|
+
}
|
|
7778
|
+
if (this.mcpRuntime) {
|
|
7779
|
+
if (hasMcpIssues) {
|
|
7780
|
+
options.push("Open MCP manager");
|
|
7781
|
+
}
|
|
7782
|
+
options.push("Refresh MCP runtime");
|
|
7783
|
+
}
|
|
7784
|
+
if (hasSemanticIssues) {
|
|
7785
|
+
options.push("Open semantic manager");
|
|
7786
|
+
}
|
|
7787
|
+
if (hasContractIssues) {
|
|
7788
|
+
options.push("Open contract manager");
|
|
7789
|
+
}
|
|
7790
|
+
if (this.permissionMode === "yolo") {
|
|
7791
|
+
options.push("Set permissions mode to ask");
|
|
7792
|
+
}
|
|
7793
|
+
if (hasResourceIssues) {
|
|
7794
|
+
options.push("Reload resources");
|
|
7795
|
+
}
|
|
7796
|
+
options.push("Show auth/models paths");
|
|
7797
|
+
options.push("Close");
|
|
7798
|
+
const selected = await this.showExtensionSelector("/doctor fixes", options);
|
|
7799
|
+
if (!selected || selected === "Close") {
|
|
7800
|
+
return;
|
|
7801
|
+
}
|
|
7802
|
+
if (selected === "Open model selector") {
|
|
7803
|
+
await this.showModelProviderSelector();
|
|
7804
|
+
return;
|
|
7805
|
+
}
|
|
7806
|
+
if (selected === "Login provider") {
|
|
7807
|
+
await this.showOAuthSelector("login");
|
|
7808
|
+
return;
|
|
7809
|
+
}
|
|
7810
|
+
if (selected === "Open MCP manager") {
|
|
7811
|
+
await this.refreshMcpRuntimeAndSession();
|
|
7812
|
+
this.showMcpSelector();
|
|
7813
|
+
return;
|
|
7814
|
+
}
|
|
7815
|
+
if (selected === "Refresh MCP runtime") {
|
|
7816
|
+
await this.refreshMcpRuntimeAndSession();
|
|
7817
|
+
this.showStatus("MCP servers refreshed");
|
|
7818
|
+
continue;
|
|
7819
|
+
}
|
|
7820
|
+
if (selected === "Open semantic manager") {
|
|
7821
|
+
await this.handleSemanticCommand("/semantic");
|
|
7822
|
+
return;
|
|
7823
|
+
}
|
|
7824
|
+
if (selected === "Open contract manager") {
|
|
7825
|
+
await this.handleContractCommand("/contract");
|
|
7826
|
+
return;
|
|
7827
|
+
}
|
|
7828
|
+
if (selected === "Set permissions mode to ask") {
|
|
7829
|
+
this.permissionMode = "ask";
|
|
7830
|
+
this.settingsManager.setPermissionMode("ask");
|
|
7831
|
+
this.showStatus("Permissions: ask");
|
|
7832
|
+
continue;
|
|
7833
|
+
}
|
|
7834
|
+
if (selected === "Reload resources") {
|
|
7835
|
+
await this.handleReloadCommand();
|
|
7836
|
+
return;
|
|
7837
|
+
}
|
|
7838
|
+
if (selected === "Show auth/models paths") {
|
|
7839
|
+
const cwd = this.sessionManager.getCwd();
|
|
7840
|
+
const agentDir = getAgentDir();
|
|
7841
|
+
this.showCommandTextBlock("Runtime Paths", [
|
|
7842
|
+
`auth.json: ${getAuthPath()}`,
|
|
7843
|
+
`models.json: ${getModelsPath()}`,
|
|
7844
|
+
`contract(project): ${this.contractService.getProjectPath()}`,
|
|
7845
|
+
`singular(analyses): ${this.singularService.getAnalysesRoot()}`,
|
|
7846
|
+
`semantic(user): ${getSemanticConfigPath("user", cwd, agentDir)}`,
|
|
7847
|
+
`semantic(project): ${getSemanticConfigPath("project", cwd, agentDir)}`,
|
|
7848
|
+
`semantic(index): ${getSemanticIndexDir(cwd, agentDir)}`,
|
|
7849
|
+
].join("\n"));
|
|
7850
|
+
continue;
|
|
7851
|
+
}
|
|
7852
|
+
}
|
|
7853
|
+
}
|
|
7854
|
+
async handleDoctorCommand(text) {
|
|
7855
|
+
const args = this.parseSlashArgs(text).slice(1);
|
|
7856
|
+
const outputJson = args.includes("--json");
|
|
7857
|
+
const hooks = this.getHookPolicySummary();
|
|
7858
|
+
const model = this.session.model;
|
|
7859
|
+
const modelRegistry = this.session.modelRegistry;
|
|
7860
|
+
const allModels = modelRegistry.getAll();
|
|
7861
|
+
const availableModels = modelRegistry.getAvailable();
|
|
7862
|
+
const modelLoadError = modelRegistry.getError();
|
|
7863
|
+
const authProviders = modelRegistry.authStorage.list();
|
|
7864
|
+
const hasModelAuth = model ? modelRegistry.authStorage.hasAuth(model.provider) : false;
|
|
7865
|
+
const extensionErrors = this.session.resourceLoader.getExtensions().errors.length;
|
|
7866
|
+
const skillDiagnostics = this.session.resourceLoader.getSkills().diagnostics.length;
|
|
7867
|
+
const promptDiagnostics = this.session.resourceLoader.getPrompts().diagnostics.length;
|
|
7868
|
+
const themeDiagnostics = this.session.resourceLoader.getThemes().diagnostics.length;
|
|
7869
|
+
const mcpStatuses = this.mcpRuntime?.getServers() ?? [];
|
|
7870
|
+
const mcpConfigErrors = this.mcpRuntime?.getErrors() ?? [];
|
|
7871
|
+
const mcpConnected = mcpStatuses.filter((status) => status.state === "connected").length;
|
|
7872
|
+
const mcpErrored = mcpStatuses.filter((status) => status.state === "error").length;
|
|
7873
|
+
const mcpDisabled = mcpStatuses.filter((status) => !status.enabled).length;
|
|
7874
|
+
const cliToolStatuses = resolveDoctorCliToolStatuses();
|
|
7875
|
+
const missingCliTools = cliToolStatuses.filter((status) => !status.available).map((status) => status.tool);
|
|
7876
|
+
let semanticStatus;
|
|
7877
|
+
let semanticStatusError;
|
|
7878
|
+
try {
|
|
7879
|
+
semanticStatus = await this.createSemanticRuntime().status();
|
|
7880
|
+
}
|
|
7881
|
+
catch (error) {
|
|
7882
|
+
semanticStatusError = error instanceof Error ? error.message : String(error);
|
|
7883
|
+
}
|
|
7884
|
+
let contractState;
|
|
7885
|
+
let contractStateError;
|
|
7886
|
+
try {
|
|
7887
|
+
contractState = this.contractService.getState();
|
|
7888
|
+
}
|
|
7889
|
+
catch (error) {
|
|
7890
|
+
contractStateError = error instanceof Error ? error.message : String(error);
|
|
7891
|
+
}
|
|
7892
|
+
const checks = [];
|
|
7893
|
+
const addCheck = (level, label, detail, fix) => checks.push({ level, label, detail, fix });
|
|
7894
|
+
if (!model) {
|
|
7895
|
+
addCheck("fail", "Active model", "No active model selected", "Run /model and pick a model.");
|
|
7896
|
+
}
|
|
7897
|
+
else if (!hasModelAuth) {
|
|
7898
|
+
addCheck("fail", "Active model auth", `Model ${model.provider}/${model.id} has no auth configured`, `Run /login ${model.provider} or set API key env vars.`);
|
|
7899
|
+
}
|
|
7900
|
+
else {
|
|
7901
|
+
addCheck("ok", "Active model", `${model.provider}/${model.id}`);
|
|
7902
|
+
}
|
|
7903
|
+
if (availableModels.length === 0) {
|
|
7904
|
+
addCheck("fail", "Available models", "No models currently have valid auth", "Run /login or configure API keys.");
|
|
7905
|
+
}
|
|
7906
|
+
else {
|
|
7907
|
+
addCheck("ok", "Available models", `${availableModels.length}/${allModels.length} ready`);
|
|
7908
|
+
}
|
|
7909
|
+
if (modelLoadError) {
|
|
7910
|
+
addCheck("warn", "models.json", modelLoadError.split("\n")[0] ?? modelLoadError, `Inspect ${getModelsPath()}.`);
|
|
7911
|
+
}
|
|
7912
|
+
else {
|
|
7913
|
+
addCheck("ok", "models.json", "Loaded without schema/runtime errors");
|
|
7914
|
+
}
|
|
7915
|
+
if (!fs.existsSync(getAuthPath())) {
|
|
7916
|
+
addCheck("warn", "auth.json", `Missing ${getAuthPath()}`, "Run /login to create credentials.");
|
|
7917
|
+
}
|
|
7918
|
+
else {
|
|
7919
|
+
addCheck("ok", "auth.json", `${authProviders.length} provider credential(s) stored`);
|
|
7920
|
+
}
|
|
7921
|
+
if (!this.mcpRuntime) {
|
|
7922
|
+
addCheck("warn", "MCP runtime", "Unavailable in this session");
|
|
7923
|
+
}
|
|
7924
|
+
else if (mcpStatuses.length === 0) {
|
|
7925
|
+
addCheck("warn", "MCP servers", "No MCP servers configured", "Use /mcp add or iosm mcp add ...");
|
|
7926
|
+
}
|
|
7927
|
+
else if (mcpErrored > 0 || mcpConfigErrors.length > 0) {
|
|
7928
|
+
addCheck("warn", "MCP servers", `${mcpConnected} connected, ${mcpErrored} error, ${mcpDisabled} disabled`, "Open /mcp to reconnect or inspect server errors.");
|
|
7929
|
+
}
|
|
7930
|
+
else {
|
|
7931
|
+
addCheck("ok", "MCP servers", `${mcpConnected} connected, ${mcpDisabled} disabled`);
|
|
7932
|
+
}
|
|
7933
|
+
if (semanticStatusError) {
|
|
7934
|
+
addCheck("fail", "Semantic index", semanticStatusError, "Run /semantic setup and retry /semantic status.");
|
|
7935
|
+
}
|
|
7936
|
+
else if (!semanticStatus) {
|
|
7937
|
+
addCheck("warn", "Semantic index", "Status unavailable", "Run /semantic setup.");
|
|
7938
|
+
}
|
|
7939
|
+
else if (!semanticStatus.configured) {
|
|
7940
|
+
addCheck("warn", "Semantic index", "Not configured", `Run /semantic setup (user: ${semanticStatus.configPathUser} or project: ${semanticStatus.configPathProject}).`);
|
|
7941
|
+
}
|
|
7942
|
+
else if (!semanticStatus.enabled) {
|
|
7943
|
+
addCheck("warn", "Semantic index", "Configured but disabled", "Enable semanticSearch.enabled or rerun /semantic setup.");
|
|
7944
|
+
}
|
|
7945
|
+
else if (!semanticStatus.indexed) {
|
|
7946
|
+
addCheck("warn", "Semantic index", "Configured but index is missing", "Run /semantic index.");
|
|
7947
|
+
}
|
|
7948
|
+
else if (semanticStatus.stale) {
|
|
7949
|
+
const requiresRebuild = semanticStatus.staleReason === "provider_changed" ||
|
|
7950
|
+
semanticStatus.staleReason === "chunking_changed" ||
|
|
7951
|
+
semanticStatus.staleReason === "index_filters_changed" ||
|
|
7952
|
+
semanticStatus.staleReason === "dimension_mismatch";
|
|
7953
|
+
addCheck("warn", "Semantic index", `Indexed but stale${semanticStatus.staleReason ? ` (${semanticStatus.staleReason})` : ""}`, requiresRebuild ? "Run /semantic rebuild." : "Run /semantic index.");
|
|
7954
|
+
}
|
|
7955
|
+
else {
|
|
7956
|
+
addCheck("ok", "Semantic index", `${semanticStatus.provider}/${semanticStatus.model} · auto_index=${semanticStatus.autoIndex ? "on" : "off"} · files=${semanticStatus.files} chunks=${semanticStatus.chunks}`);
|
|
7957
|
+
}
|
|
7958
|
+
if (contractStateError) {
|
|
7959
|
+
addCheck("fail", "Contract state", contractStateError, "Fix .iosm/contract.json or run /contract clear --scope project.");
|
|
7960
|
+
}
|
|
7961
|
+
else if (!contractState) {
|
|
7962
|
+
addCheck("warn", "Contract state", "Unavailable", "Run /contract show to inspect state.");
|
|
7963
|
+
}
|
|
7964
|
+
else if (!contractState.hasProjectFile && Object.keys(contractState.sessionOverlay).length === 0) {
|
|
7965
|
+
addCheck("warn", "Contract state", "No project/session contract active", "Run /contract to define constraints and quality gates.");
|
|
7966
|
+
}
|
|
7967
|
+
else {
|
|
7968
|
+
addCheck("ok", "Contract state", `project=${contractState.hasProjectFile ? "yes" : "no"} session_keys=${Object.keys(contractState.sessionOverlay).length} effective_keys=${Object.keys(contractState.effective).length}`);
|
|
7969
|
+
}
|
|
7970
|
+
addCheck("ok", "Singular analyzer", "Available via /singular <request>");
|
|
7971
|
+
if (missingCliTools.length > 0) {
|
|
7972
|
+
addCheck("warn", "CLI toolchain", `${cliToolStatuses.length - missingCliTools.length}/${cliToolStatuses.length} available (missing: ${missingCliTools.join(", ")})`, `Install missing CLI tools: ${missingCliTools.join(", ")}.`);
|
|
7973
|
+
}
|
|
7974
|
+
else {
|
|
7975
|
+
addCheck("ok", "CLI toolchain", `${cliToolStatuses.length}/${cliToolStatuses.length} available`);
|
|
7976
|
+
}
|
|
7977
|
+
if (extensionErrors > 0 || skillDiagnostics > 0 || promptDiagnostics > 0 || themeDiagnostics > 0) {
|
|
7978
|
+
addCheck("warn", "Resources", `extensions:${extensionErrors} skills:${skillDiagnostics} prompts:${promptDiagnostics} themes:${themeDiagnostics}`, "Run /reload and inspect warnings shown in the chat.");
|
|
7979
|
+
}
|
|
7980
|
+
else {
|
|
7981
|
+
addCheck("ok", "Resources", "No extension/skill/prompt/theme diagnostics");
|
|
7982
|
+
}
|
|
7983
|
+
addCheck(this.permissionMode === "yolo" ? "warn" : "ok", "Permissions", `mode=${this.permissionMode}, allowRules=${this.permissionAllowRules.length}, denyRules=${this.permissionDenyRules.length}`, this.permissionMode === "yolo" ? "Switch to /permissions ask for safer execution." : undefined);
|
|
7984
|
+
addCheck(process.env[ENV_OFFLINE] ? "warn" : "ok", "Environment", `offline=${process.env[ENV_OFFLINE] ? "on" : "off"}, sessionTrace=${isSessionTraceEnabled() ? "on" : "off"}`, process.env[ENV_OFFLINE] ? "Unset IOSM_OFFLINE/PI_OFFLINE when network access is required." : undefined);
|
|
7985
|
+
addCheck("ok", "Hooks", hooks
|
|
7986
|
+
? `U${hooks.userPromptSubmit}/P${hooks.preToolUse}/T${hooks.postToolUse}/S${hooks.stop}`
|
|
7987
|
+
: "No hooks loaded");
|
|
7988
|
+
const recommendations = [...new Set(checks.map((check) => check.fix).filter((fix) => !!fix))];
|
|
7989
|
+
if (outputJson) {
|
|
7990
|
+
this.showCommandJsonBlock("Doctor Report", {
|
|
7991
|
+
timestamp: new Date().toISOString(),
|
|
7992
|
+
cwd: this.sessionManager.getCwd(),
|
|
7993
|
+
sessionFile: this.sessionManager.getSessionFile() ?? null,
|
|
7994
|
+
activeProfile: this.activeProfileName,
|
|
7995
|
+
externalCliTools: cliToolStatuses,
|
|
7996
|
+
checks,
|
|
7997
|
+
recommendations,
|
|
7998
|
+
});
|
|
7999
|
+
return;
|
|
8000
|
+
}
|
|
8001
|
+
const lines = [];
|
|
8002
|
+
lines.push(`Timestamp: ${new Date().toISOString()}`);
|
|
8003
|
+
lines.push(`CWD: ${this.sessionManager.getCwd()}`);
|
|
8004
|
+
lines.push(`Session: ${this.sessionManager.getSessionFile() ?? "in-memory"}`);
|
|
8005
|
+
lines.push(`Profile: ${this.activeProfileName}`);
|
|
8006
|
+
lines.push("");
|
|
8007
|
+
for (const check of checks) {
|
|
8008
|
+
const prefix = check.level === "ok" ? "[OK]" : check.level === "warn" ? "[WARN]" : "[FAIL]";
|
|
8009
|
+
lines.push(`${prefix} ${check.label}: ${check.detail}`);
|
|
8010
|
+
}
|
|
8011
|
+
lines.push("");
|
|
8012
|
+
lines.push("External CLI tools:");
|
|
8013
|
+
for (const status of cliToolStatuses) {
|
|
8014
|
+
const prefix = status.available ? "[OK]" : "[WARN]";
|
|
8015
|
+
const sourceLabel = status.source === "missing"
|
|
8016
|
+
? "missing"
|
|
8017
|
+
: `${status.source}${status.command ? ` (${status.command})` : ""}`;
|
|
8018
|
+
lines.push(`${prefix} ${status.tool}: ${sourceLabel}`);
|
|
8019
|
+
if (!status.available && status.hint) {
|
|
8020
|
+
lines.push(` fix: ${status.hint}`);
|
|
8021
|
+
}
|
|
8022
|
+
}
|
|
8023
|
+
if (recommendations.length > 0) {
|
|
8024
|
+
lines.push("");
|
|
8025
|
+
lines.push("Recommended actions:");
|
|
8026
|
+
for (const recommendation of recommendations) {
|
|
8027
|
+
lines.push(`- ${recommendation}`);
|
|
8028
|
+
}
|
|
8029
|
+
}
|
|
8030
|
+
lines.push("");
|
|
8031
|
+
lines.push("Tip: /doctor --json");
|
|
8032
|
+
this.showCommandTextBlock("Doctor Report", lines.join("\n"));
|
|
8033
|
+
const wantsInteractiveFixes = !args.includes("--no-fix");
|
|
8034
|
+
if (wantsInteractiveFixes) {
|
|
8035
|
+
await this.runDoctorInteractiveFixes(checks);
|
|
8036
|
+
}
|
|
8037
|
+
}
|
|
8038
|
+
showCommandTextBlock(title, body) {
|
|
8039
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
8040
|
+
this.chatContainer.addChild(new DynamicBorder());
|
|
8041
|
+
this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", title)), 1, 0));
|
|
8042
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
8043
|
+
this.chatContainer.addChild(new Text(body, 1, 0));
|
|
8044
|
+
this.chatContainer.addChild(new DynamicBorder());
|
|
8045
|
+
this.ui.requestRender();
|
|
8046
|
+
}
|
|
8047
|
+
showCommandJsonBlock(title, value) {
|
|
8048
|
+
const json = JSON.stringify(value, null, 2);
|
|
8049
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
8050
|
+
this.chatContainer.addChild(new DynamicBorder());
|
|
8051
|
+
this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", title)), 1, 0));
|
|
8052
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
8053
|
+
this.chatContainer.addChild(new Markdown(`\`\`\`json\n${json}\n\`\`\``, 1, 1, this.getMarkdownThemeWithSettings()));
|
|
8054
|
+
this.chatContainer.addChild(new DynamicBorder());
|
|
8055
|
+
this.ui.requestRender();
|
|
8056
|
+
}
|
|
8057
|
+
async runIosmInitAgentVerification(result, targetDir, onEvent) {
|
|
8058
|
+
if (!result.cycle) {
|
|
8059
|
+
return {
|
|
8060
|
+
completed: false,
|
|
8061
|
+
skippedReason: "No cycle scaffold was available for verification.",
|
|
8062
|
+
};
|
|
8063
|
+
}
|
|
8064
|
+
const cycleId = result.cycle.cycleId;
|
|
8065
|
+
process.env[ENV_SESSION_TRACE] = "1";
|
|
8066
|
+
process.env.PI_SESSION_TRACE = "1";
|
|
8067
|
+
const agentDir = getAgentDir();
|
|
8068
|
+
const settingsManager = SettingsManager.create(targetDir, agentDir);
|
|
8069
|
+
const settingsErrors = settingsManager.drainErrors();
|
|
8070
|
+
for (const { scope, error } of settingsErrors) {
|
|
8071
|
+
this.showWarning(`Init verify warning (${scope} settings): ${error.message}`);
|
|
8072
|
+
}
|
|
8073
|
+
const authStorage = AuthStorage.create();
|
|
8074
|
+
const modelRegistry = new ModelRegistry(authStorage, getModelsPath());
|
|
8075
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
8076
|
+
cwd: targetDir,
|
|
8077
|
+
agentDir,
|
|
8078
|
+
settingsManager,
|
|
8079
|
+
noExtensions: true,
|
|
8080
|
+
});
|
|
8081
|
+
await resourceLoader.reload();
|
|
8082
|
+
const { session, modelFallbackMessage } = await createAgentSession({
|
|
8083
|
+
cwd: targetDir,
|
|
8084
|
+
sessionManager: SessionManager.inMemory(),
|
|
8085
|
+
settingsManager,
|
|
8086
|
+
authStorage,
|
|
8087
|
+
modelRegistry,
|
|
8088
|
+
resourceLoader,
|
|
8089
|
+
});
|
|
8090
|
+
let toolExecutions = 0;
|
|
8091
|
+
const activityLog = [];
|
|
8092
|
+
const pushActivity = (line, persist = false) => {
|
|
8093
|
+
activityLog.push(line);
|
|
8094
|
+
if (activityLog.length > 30) {
|
|
8095
|
+
activityLog.shift();
|
|
8096
|
+
}
|
|
8097
|
+
if (persist) {
|
|
8098
|
+
this.showProgressLine(`IOSM init verify: ${line}`);
|
|
8099
|
+
}
|
|
8100
|
+
};
|
|
8101
|
+
const setVerifyLiveStatus = (message) => {
|
|
8102
|
+
this.setWorkingMessage(`${message} (${appKey(this.keybindings, "interrupt")} to interrupt)`);
|
|
8103
|
+
};
|
|
8104
|
+
const unsubscribe = session.subscribe((event) => {
|
|
8105
|
+
onEvent?.(event);
|
|
8106
|
+
if (event.type === "turn_start") {
|
|
8107
|
+
pushActivity("agent turn started");
|
|
8108
|
+
setVerifyLiveStatus("Verifying workspace...");
|
|
8109
|
+
return;
|
|
8110
|
+
}
|
|
8111
|
+
if (event.type !== "tool_execution_start") {
|
|
8112
|
+
return;
|
|
8113
|
+
}
|
|
8114
|
+
toolExecutions += 1;
|
|
8115
|
+
if (event.toolName === "bash") {
|
|
8116
|
+
const commandRaw = event.args && typeof event.args === "object" && "command" in event.args
|
|
8117
|
+
? String(event.args.command ?? "")
|
|
8118
|
+
: "";
|
|
8119
|
+
const preview = commandRaw.replace(/\s+/g, " ").trim().slice(0, 68);
|
|
8120
|
+
const line = `bash #${toolExecutions}${preview ? ` ${preview}${commandRaw.length > 68 ? "..." : ""}` : ""}`;
|
|
8121
|
+
pushActivity(line);
|
|
8122
|
+
setVerifyLiveStatus(`Verifying workspace · ${line}`);
|
|
8123
|
+
return;
|
|
8124
|
+
}
|
|
8125
|
+
const line = `${event.toolName} #${toolExecutions}`;
|
|
8126
|
+
pushActivity(line);
|
|
8127
|
+
setVerifyLiveStatus(`Verifying workspace · ${line}`);
|
|
8128
|
+
});
|
|
8129
|
+
this.iosmVerificationSession = session;
|
|
8130
|
+
try {
|
|
8131
|
+
if (!session.model) {
|
|
8132
|
+
return {
|
|
8133
|
+
completed: false,
|
|
8134
|
+
skippedReason: modelFallbackMessage ??
|
|
8135
|
+
"No model available for agent verification. Configure /login or an API key, then re-run /init.",
|
|
8136
|
+
activityLog,
|
|
8137
|
+
};
|
|
8138
|
+
}
|
|
8139
|
+
const timeoutMs = 180_000;
|
|
8140
|
+
const startedAt = Date.now();
|
|
8141
|
+
const heartbeatMs = 10_000;
|
|
8142
|
+
let nextPersistentHeartbeatSec = 30;
|
|
8143
|
+
let timeoutHandle;
|
|
8144
|
+
let heartbeatHandle;
|
|
8145
|
+
try {
|
|
8146
|
+
pushActivity("waiting for model response...", true);
|
|
8147
|
+
setVerifyLiveStatus("Waiting for model response...");
|
|
8148
|
+
heartbeatHandle = setInterval(() => {
|
|
8149
|
+
const elapsedSec = Math.round((Date.now() - startedAt) / 1000);
|
|
8150
|
+
if (toolExecutions === 0 && elapsedSec >= nextPersistentHeartbeatSec) {
|
|
8151
|
+
pushActivity(`still waiting for model response (${elapsedSec}s)`, true);
|
|
8152
|
+
nextPersistentHeartbeatSec += 30;
|
|
8153
|
+
}
|
|
8154
|
+
setVerifyLiveStatus(toolExecutions === 0
|
|
8155
|
+
? `Waiting for model response... ${elapsedSec}s`
|
|
8156
|
+
: `Verifying workspace... ${elapsedSec}s · tool calls=${toolExecutions}`);
|
|
8157
|
+
}, heartbeatMs);
|
|
8158
|
+
await Promise.race([
|
|
8159
|
+
session.prompt(buildIosmAgentVerificationPrompt(result), {
|
|
8160
|
+
expandPromptTemplates: false,
|
|
8161
|
+
skipIosmAutopilot: true,
|
|
8162
|
+
source: "interactive",
|
|
8163
|
+
}),
|
|
8164
|
+
new Promise((_resolve, reject) => {
|
|
8165
|
+
timeoutHandle = setTimeout(() => {
|
|
8166
|
+
reject(new Error(`Verifier timeout after ${Math.round(timeoutMs / 1000)}s.`));
|
|
8167
|
+
}, timeoutMs);
|
|
8168
|
+
}),
|
|
8169
|
+
]);
|
|
8170
|
+
}
|
|
8171
|
+
finally {
|
|
8172
|
+
if (timeoutHandle) {
|
|
8173
|
+
clearTimeout(timeoutHandle);
|
|
8174
|
+
}
|
|
8175
|
+
if (heartbeatHandle) {
|
|
8176
|
+
clearInterval(heartbeatHandle);
|
|
8177
|
+
}
|
|
8178
|
+
}
|
|
8179
|
+
const messages = session.state.messages;
|
|
8180
|
+
let lastAssistant;
|
|
8181
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
8182
|
+
const message = messages[index];
|
|
8183
|
+
if (message.role === "assistant") {
|
|
8184
|
+
lastAssistant = message;
|
|
8185
|
+
break;
|
|
8186
|
+
}
|
|
8187
|
+
}
|
|
8188
|
+
if (lastAssistant &&
|
|
8189
|
+
(lastAssistant.stopReason === "error" || lastAssistant.stopReason === "aborted")) {
|
|
8190
|
+
pushActivity(`agent finished with ${lastAssistant.stopReason}`);
|
|
8191
|
+
if (lastAssistant.stopReason === "aborted") {
|
|
8192
|
+
return {
|
|
8193
|
+
completed: false,
|
|
8194
|
+
cancelled: true,
|
|
8195
|
+
skippedReason: "Cancelled by user.",
|
|
8196
|
+
tracePath: session.sessionTracePath ??
|
|
8197
|
+
(isSessionTraceEnabled() ? getSessionTracePath(session.sessionManager.getSessionId()) : undefined),
|
|
8198
|
+
activityLog,
|
|
8199
|
+
};
|
|
8200
|
+
}
|
|
8201
|
+
return {
|
|
8202
|
+
completed: false,
|
|
8203
|
+
error: lastAssistant.errorMessage ?? `Verifier finished with ${lastAssistant.stopReason}.`,
|
|
8204
|
+
tracePath: session.sessionTracePath ??
|
|
8205
|
+
(isSessionTraceEnabled() ? getSessionTracePath(session.sessionManager.getSessionId()) : undefined),
|
|
8206
|
+
activityLog,
|
|
8207
|
+
};
|
|
8208
|
+
}
|
|
8209
|
+
let authoredGuide;
|
|
8210
|
+
try {
|
|
8211
|
+
pushActivity("authoring IOSM.md from repository evidence...");
|
|
8212
|
+
setVerifyLiveStatus("Authoring IOSM.md...");
|
|
8213
|
+
await session.prompt(buildIosmGuideAuthoringPrompt(result), {
|
|
8214
|
+
expandPromptTemplates: false,
|
|
8215
|
+
skipIosmAutopilot: true,
|
|
8216
|
+
source: "interactive",
|
|
8217
|
+
});
|
|
8218
|
+
const guideAssistant = (() => {
|
|
8219
|
+
const messages = session.state.messages;
|
|
8220
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
8221
|
+
const message = messages[index];
|
|
8222
|
+
if (message.role === "assistant") {
|
|
8223
|
+
return message;
|
|
8224
|
+
}
|
|
8225
|
+
}
|
|
8226
|
+
return undefined;
|
|
8227
|
+
})();
|
|
8228
|
+
const guideText = extractAssistantText(guideAssistant);
|
|
8229
|
+
const normalizedGuide = normalizeIosmGuideMarkdown(guideText);
|
|
8230
|
+
if (normalizedGuide.trim().length > 0) {
|
|
8231
|
+
authoredGuide = normalizedGuide;
|
|
8232
|
+
pushActivity("IOSM.md authored by agent");
|
|
8233
|
+
}
|
|
8234
|
+
}
|
|
8235
|
+
catch {
|
|
8236
|
+
pushActivity("agent IOSM.md authoring failed; using structured fallback");
|
|
8237
|
+
}
|
|
8238
|
+
let current;
|
|
8239
|
+
let guidePath;
|
|
8240
|
+
try {
|
|
8241
|
+
const report = readIosmCycleReport(result.rootDir, cycleId);
|
|
6759
8242
|
current = createMetricSnapshot(report);
|
|
6760
8243
|
if (authoredGuide) {
|
|
6761
8244
|
guidePath = getIosmGuidePath(targetDir);
|
|
6762
8245
|
fs.writeFileSync(guidePath, authoredGuide, "utf8");
|
|
6763
8246
|
}
|
|
6764
8247
|
else {
|
|
6765
|
-
guidePath = writeIosmGuideDocument({
|
|
6766
|
-
rootDir: targetDir,
|
|
6767
|
-
cycleId,
|
|
6768
|
-
assessmentSource: "verified",
|
|
6769
|
-
metrics: report.metrics,
|
|
6770
|
-
iosmIndex: report.iosm_index,
|
|
6771
|
-
decisionConfidence: report.decision_confidence,
|
|
6772
|
-
goals: report.goals,
|
|
6773
|
-
filesAnalyzed: result.analysis.files_analyzed,
|
|
6774
|
-
sourceFileCount: result.analysis.source_file_count,
|
|
6775
|
-
testFileCount: result.analysis.test_file_count,
|
|
6776
|
-
docFileCount: result.analysis.doc_file_count,
|
|
6777
|
-
}, true).path;
|
|
8248
|
+
guidePath = writeIosmGuideDocument({
|
|
8249
|
+
rootDir: targetDir,
|
|
8250
|
+
cycleId,
|
|
8251
|
+
assessmentSource: "verified",
|
|
8252
|
+
metrics: report.metrics,
|
|
8253
|
+
iosmIndex: report.iosm_index,
|
|
8254
|
+
decisionConfidence: report.decision_confidence,
|
|
8255
|
+
goals: report.goals,
|
|
8256
|
+
filesAnalyzed: result.analysis.files_analyzed,
|
|
8257
|
+
sourceFileCount: result.analysis.source_file_count,
|
|
8258
|
+
testFileCount: result.analysis.test_file_count,
|
|
8259
|
+
docFileCount: result.analysis.doc_file_count,
|
|
8260
|
+
}, true).path;
|
|
8261
|
+
}
|
|
8262
|
+
}
|
|
8263
|
+
catch {
|
|
8264
|
+
current = undefined;
|
|
8265
|
+
guidePath = undefined;
|
|
8266
|
+
}
|
|
8267
|
+
let historyPath;
|
|
8268
|
+
try {
|
|
8269
|
+
const history = recordIosmCycleHistory(result.rootDir, cycleId);
|
|
8270
|
+
historyPath = history.historyPath;
|
|
8271
|
+
}
|
|
8272
|
+
catch {
|
|
8273
|
+
historyPath = undefined;
|
|
8274
|
+
}
|
|
8275
|
+
pushActivity(`completed (${toolExecutions} tool calls)`, true);
|
|
8276
|
+
return {
|
|
8277
|
+
completed: true,
|
|
8278
|
+
current,
|
|
8279
|
+
historyPath,
|
|
8280
|
+
guidePath,
|
|
8281
|
+
toolCalls: toolExecutions,
|
|
8282
|
+
tracePath: session.sessionTracePath ??
|
|
8283
|
+
(isSessionTraceEnabled() ? getSessionTracePath(session.sessionManager.getSessionId()) : undefined),
|
|
8284
|
+
activityLog,
|
|
8285
|
+
};
|
|
8286
|
+
}
|
|
8287
|
+
catch (error) {
|
|
8288
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8289
|
+
if (isAbortLikeMessage(message)) {
|
|
8290
|
+
pushActivity("cancelled by user", true);
|
|
8291
|
+
return {
|
|
8292
|
+
completed: false,
|
|
8293
|
+
cancelled: true,
|
|
8294
|
+
skippedReason: "Cancelled by user.",
|
|
8295
|
+
activityLog,
|
|
8296
|
+
};
|
|
8297
|
+
}
|
|
8298
|
+
pushActivity(`failed (${message})`);
|
|
8299
|
+
this.showWarning(`IOSM init verify: failed (${message})`);
|
|
8300
|
+
return {
|
|
8301
|
+
completed: false,
|
|
8302
|
+
error: message,
|
|
8303
|
+
activityLog,
|
|
8304
|
+
};
|
|
8305
|
+
}
|
|
8306
|
+
finally {
|
|
8307
|
+
try {
|
|
8308
|
+
if (session.isStreaming) {
|
|
8309
|
+
await session.abort();
|
|
8310
|
+
}
|
|
8311
|
+
}
|
|
8312
|
+
catch {
|
|
8313
|
+
// Best-effort shutdown to avoid a stuck verification loader.
|
|
8314
|
+
}
|
|
8315
|
+
if (this.iosmVerificationSession === session) {
|
|
8316
|
+
this.iosmVerificationSession = undefined;
|
|
8317
|
+
}
|
|
8318
|
+
unsubscribe();
|
|
8319
|
+
session.dispose();
|
|
8320
|
+
if (this.loadingAnimation) {
|
|
8321
|
+
this.loadingAnimation.stop();
|
|
8322
|
+
this.loadingAnimation = undefined;
|
|
8323
|
+
}
|
|
8324
|
+
this.statusContainer.clear();
|
|
8325
|
+
this.pendingWorkingMessage = undefined;
|
|
8326
|
+
this.ui.requestRender();
|
|
8327
|
+
}
|
|
8328
|
+
}
|
|
8329
|
+
getLastAssistantMessage() {
|
|
8330
|
+
return this.session.messages
|
|
8331
|
+
.slice()
|
|
8332
|
+
.reverse()
|
|
8333
|
+
.find((message) => {
|
|
8334
|
+
if (message.role !== "assistant") {
|
|
8335
|
+
return false;
|
|
8336
|
+
}
|
|
8337
|
+
return !(message.stopReason === "aborted" && message.content.length === 0);
|
|
8338
|
+
});
|
|
8339
|
+
}
|
|
8340
|
+
sanitizeAssistantDisplayMessage(message) {
|
|
8341
|
+
const hideAllTextForOrchestration = this.activeAssistantOrchestrationContext;
|
|
8342
|
+
let changed = false;
|
|
8343
|
+
const nextContent = message.content.map((content) => {
|
|
8344
|
+
if (content.type !== "text")
|
|
8345
|
+
return content;
|
|
8346
|
+
if (hideAllTextForOrchestration) {
|
|
8347
|
+
if (content.text !== "") {
|
|
8348
|
+
changed = true;
|
|
8349
|
+
}
|
|
8350
|
+
return { ...content, text: "" };
|
|
8351
|
+
}
|
|
8352
|
+
return content;
|
|
8353
|
+
});
|
|
8354
|
+
return changed ? { ...message, content: nextContent } : message;
|
|
8355
|
+
}
|
|
8356
|
+
resolveMentionedAgent(text) {
|
|
8357
|
+
const cwd = this.sessionManager.getCwd();
|
|
8358
|
+
const loaded = loadCustomSubagents({ cwd, agentDir: getAgentDir() });
|
|
8359
|
+
if (loaded.agents.length === 0)
|
|
8360
|
+
return undefined;
|
|
8361
|
+
const matches = text.matchAll(/(?:^|\s)@([^\s]+)/g);
|
|
8362
|
+
for (const match of matches) {
|
|
8363
|
+
const candidate = (match[1] ?? "").trim();
|
|
8364
|
+
const resolved = resolveCustomSubagentReference(candidate, loaded.agents);
|
|
8365
|
+
if (resolved) {
|
|
8366
|
+
return resolved;
|
|
8367
|
+
}
|
|
8368
|
+
}
|
|
8369
|
+
return undefined;
|
|
8370
|
+
}
|
|
8371
|
+
isCapabilityQuery(text) {
|
|
8372
|
+
const normalized = text.toLowerCase().trim();
|
|
8373
|
+
if (!normalized)
|
|
8374
|
+
return true;
|
|
8375
|
+
return (normalized === "?" ||
|
|
8376
|
+
normalized === "help" ||
|
|
8377
|
+
normalized === "--help" ||
|
|
8378
|
+
normalized === "/help" ||
|
|
8379
|
+
normalized === "/capabilities" ||
|
|
8380
|
+
normalized === "capabilities");
|
|
8381
|
+
}
|
|
8382
|
+
async promptWithTaskFallback(userInput) {
|
|
8383
|
+
const mentionedAgent = this.resolveMentionedAgent(userInput);
|
|
8384
|
+
if (mentionedAgent) {
|
|
8385
|
+
const cleaned = userInput.replace(/(?:^|\s)@[^\s]+/g, " ").trim();
|
|
8386
|
+
if (this.isCapabilityQuery(cleaned)) {
|
|
8387
|
+
const cwd = this.sessionManager.getCwd();
|
|
8388
|
+
const loaded = loadCustomSubagents({ cwd, agentDir: getAgentDir() });
|
|
8389
|
+
const agent = loaded.agents.find((item) => item.name === mentionedAgent);
|
|
8390
|
+
if (agent) {
|
|
8391
|
+
const capabilityPrompt = [
|
|
8392
|
+
"<agent_capability_query>",
|
|
8393
|
+
`agent_name: ${agent.name}`,
|
|
8394
|
+
`description: ${agent.description}`,
|
|
8395
|
+
`profile: ${agent.profile ?? "full"}`,
|
|
8396
|
+
`model: ${agent.model ?? "default"}`,
|
|
8397
|
+
`background: ${agent.background ? "true" : "false"}`,
|
|
8398
|
+
`tools: ${agent.tools?.join(", ") ?? "default profile tools"}`,
|
|
8399
|
+
`disallowed_tools: ${agent.disallowedTools?.join(", ") ?? "none"}`,
|
|
8400
|
+
"",
|
|
8401
|
+
"agent_instructions:",
|
|
8402
|
+
agent.instructions,
|
|
8403
|
+
"",
|
|
8404
|
+
`user_question: ${cleaned || "what can you do?"}`,
|
|
8405
|
+
"Answer normally and concisely in plain language. Do not run task tool for this query.",
|
|
8406
|
+
"</agent_capability_query>",
|
|
8407
|
+
].join("\n");
|
|
8408
|
+
await this.session.prompt(capabilityPrompt, {
|
|
8409
|
+
expandPromptTemplates: false,
|
|
8410
|
+
source: "interactive",
|
|
8411
|
+
});
|
|
8412
|
+
return;
|
|
8413
|
+
}
|
|
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;
|
|
8419
|
+
const mentionPrompt = [
|
|
8420
|
+
`<orchestrate mode="${mentionMode}" agents="1"${mentionMaxParallel ? ` max_parallel="${mentionMaxParallel}"` : ""}>`,
|
|
8421
|
+
`- agent 1: profile=${this.activeProfileName} cwd=${this.sessionManager.getCwd()} agent=${mentionedAgent}`,
|
|
8422
|
+
`task: ${mentionTask}`,
|
|
8423
|
+
"constraints:",
|
|
8424
|
+
"- user selected a concrete custom agent via @mention",
|
|
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
|
+
: []),
|
|
8437
|
+
"</orchestrate>",
|
|
8438
|
+
].join("\n");
|
|
8439
|
+
await this.session.prompt(mentionPrompt, {
|
|
8440
|
+
expandPromptTemplates: false,
|
|
8441
|
+
source: "interactive",
|
|
8442
|
+
});
|
|
8443
|
+
return;
|
|
8444
|
+
}
|
|
8445
|
+
await this.session.prompt(userInput);
|
|
8446
|
+
}
|
|
8447
|
+
createIosmVerificationEventBridge(options) {
|
|
8448
|
+
const loaderMessage = options?.loaderMessage ?? `Verifying workspace... (${appKey(this.keybindings, "interrupt")} to interrupt)`;
|
|
8449
|
+
const hideAssistantText = options?.hideAssistantText === true;
|
|
8450
|
+
let verifyStreamingComponent;
|
|
8451
|
+
let verifyStreamingMessage;
|
|
8452
|
+
const verifyPendingTools = new Map();
|
|
8453
|
+
return (event) => {
|
|
8454
|
+
switch (event.type) {
|
|
8455
|
+
case "agent_start":
|
|
8456
|
+
if (this.loadingAnimation) {
|
|
8457
|
+
this.loadingAnimation.stop();
|
|
8458
|
+
}
|
|
8459
|
+
this.statusContainer.clear();
|
|
8460
|
+
this.loadingAnimation = new DecryptLoader(this.ui, (s) => theme.fg("accent", s), (t) => theme.fg("muted", t), loaderMessage);
|
|
8461
|
+
this.statusContainer.addChild(this.loadingAnimation);
|
|
8462
|
+
break;
|
|
8463
|
+
case "message_start":
|
|
8464
|
+
if (event.message.role === "assistant" && !hideAssistantText) {
|
|
8465
|
+
verifyStreamingComponent = new AssistantMessageComponent(undefined, false, this.getMarkdownThemeWithSettings());
|
|
8466
|
+
verifyStreamingMessage = event.message;
|
|
8467
|
+
this.chatContainer.addChild(verifyStreamingComponent);
|
|
8468
|
+
verifyStreamingComponent.updateContent(verifyStreamingMessage);
|
|
8469
|
+
}
|
|
8470
|
+
break;
|
|
8471
|
+
case "message_update":
|
|
8472
|
+
if (verifyStreamingComponent && event.message.role === "assistant") {
|
|
8473
|
+
verifyStreamingMessage = event.message;
|
|
8474
|
+
verifyStreamingComponent.updateContent(verifyStreamingMessage);
|
|
8475
|
+
for (const content of verifyStreamingMessage.content) {
|
|
8476
|
+
if (content.type === "toolCall" && !verifyPendingTools.has(content.id)) {
|
|
8477
|
+
const component = new ToolExecutionComponent(content.name, content.arguments, { showImages: this.settingsManager.getShowImages() }, this.getRegisteredToolDefinition(content.name), this.ui);
|
|
8478
|
+
component.setExpanded(this.toolOutputExpanded);
|
|
8479
|
+
this.chatContainer.addChild(component);
|
|
8480
|
+
verifyPendingTools.set(content.id, component);
|
|
8481
|
+
}
|
|
8482
|
+
}
|
|
8483
|
+
}
|
|
8484
|
+
break;
|
|
8485
|
+
case "message_end":
|
|
8486
|
+
if (verifyStreamingComponent && event.message.role === "assistant") {
|
|
8487
|
+
verifyStreamingMessage = event.message;
|
|
8488
|
+
verifyStreamingComponent.updateContent(verifyStreamingMessage);
|
|
8489
|
+
verifyStreamingComponent = undefined;
|
|
8490
|
+
verifyStreamingMessage = undefined;
|
|
8491
|
+
}
|
|
8492
|
+
break;
|
|
8493
|
+
case "tool_execution_start":
|
|
8494
|
+
if (!verifyPendingTools.has(event.toolCallId)) {
|
|
8495
|
+
const component = new ToolExecutionComponent(event.toolName, event.args, { showImages: this.settingsManager.getShowImages() }, this.getRegisteredToolDefinition(event.toolName), this.ui);
|
|
8496
|
+
component.setExpanded(this.toolOutputExpanded);
|
|
8497
|
+
this.chatContainer.addChild(component);
|
|
8498
|
+
verifyPendingTools.set(event.toolCallId, component);
|
|
8499
|
+
}
|
|
8500
|
+
break;
|
|
8501
|
+
case "tool_execution_update": {
|
|
8502
|
+
const component = verifyPendingTools.get(event.toolCallId);
|
|
8503
|
+
if (component) {
|
|
8504
|
+
component.updateResult({ ...event.partialResult, isError: false }, true);
|
|
8505
|
+
}
|
|
8506
|
+
break;
|
|
8507
|
+
}
|
|
8508
|
+
case "tool_execution_end": {
|
|
8509
|
+
const component = verifyPendingTools.get(event.toolCallId);
|
|
8510
|
+
if (component) {
|
|
8511
|
+
component.updateResult({ ...event.result, isError: event.isError });
|
|
8512
|
+
verifyPendingTools.delete(event.toolCallId);
|
|
8513
|
+
}
|
|
8514
|
+
break;
|
|
8515
|
+
}
|
|
8516
|
+
case "agent_end":
|
|
8517
|
+
if (this.loadingAnimation) {
|
|
8518
|
+
this.loadingAnimation.stop();
|
|
8519
|
+
this.loadingAnimation = undefined;
|
|
8520
|
+
this.statusContainer.clear();
|
|
8521
|
+
}
|
|
8522
|
+
verifyPendingTools.clear();
|
|
8523
|
+
break;
|
|
8524
|
+
}
|
|
8525
|
+
this.ui.requestRender();
|
|
8526
|
+
};
|
|
8527
|
+
}
|
|
8528
|
+
resolveIosmSnapshot(result, verification) {
|
|
8529
|
+
let snapshot = verification?.current;
|
|
8530
|
+
if (!snapshot && result.cycle) {
|
|
8531
|
+
try {
|
|
8532
|
+
snapshot = createMetricSnapshot(readIosmCycleReport(result.rootDir, result.cycle.cycleId));
|
|
8533
|
+
}
|
|
8534
|
+
catch {
|
|
8535
|
+
snapshot = undefined;
|
|
8536
|
+
}
|
|
8537
|
+
}
|
|
8538
|
+
return (snapshot ?? {
|
|
8539
|
+
metrics: result.analysis.metrics,
|
|
8540
|
+
iosm_index: null,
|
|
8541
|
+
decision_confidence: null,
|
|
8542
|
+
});
|
|
8543
|
+
}
|
|
8544
|
+
async runIosmRefreshPass(options) {
|
|
8545
|
+
const initResult = await initIosmWorkspace({ cwd: options.cwd, force: options.force });
|
|
8546
|
+
let verification;
|
|
8547
|
+
if (options.agentVerify) {
|
|
8548
|
+
verification = await this.runIosmInitAgentVerification(initResult, options.cwd, this.createIosmVerificationEventBridge());
|
|
8549
|
+
}
|
|
8550
|
+
let cycleDecision;
|
|
8551
|
+
if (initResult.cycle) {
|
|
8552
|
+
try {
|
|
8553
|
+
cycleDecision = inspectIosmCycle(initResult.rootDir, initResult.cycle.cycleId).decision;
|
|
8554
|
+
}
|
|
8555
|
+
catch {
|
|
8556
|
+
cycleDecision = undefined;
|
|
8557
|
+
}
|
|
8558
|
+
}
|
|
8559
|
+
return {
|
|
8560
|
+
initResult,
|
|
8561
|
+
verification,
|
|
8562
|
+
snapshot: this.resolveIosmSnapshot(initResult, verification),
|
|
8563
|
+
guidePath: verification?.guidePath ?? getIosmGuidePath(initResult.rootDir),
|
|
8564
|
+
cycleDecision,
|
|
8565
|
+
};
|
|
8566
|
+
}
|
|
8567
|
+
parseIosmAutomationSlashCommand(text) {
|
|
8568
|
+
const args = this.parseSlashArgs(text).slice(1);
|
|
8569
|
+
let targetIndex;
|
|
8570
|
+
let maxIterations;
|
|
8571
|
+
let forceInit = false;
|
|
8572
|
+
for (let index = 0; index < args.length; index++) {
|
|
8573
|
+
const arg = args[index];
|
|
8574
|
+
if (arg === "--force-init" || arg === "--force") {
|
|
8575
|
+
forceInit = true;
|
|
8576
|
+
continue;
|
|
8577
|
+
}
|
|
8578
|
+
if (arg === "--max-iterations") {
|
|
8579
|
+
const nextValue = args[index + 1];
|
|
8580
|
+
const parsed = nextValue ? Number.parseInt(nextValue, 10) : Number.NaN;
|
|
8581
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
8582
|
+
this.showWarning('Usage: /iosm [target-index] [--max-iterations N] [--force-init]');
|
|
8583
|
+
return undefined;
|
|
8584
|
+
}
|
|
8585
|
+
maxIterations = parsed;
|
|
8586
|
+
index += 1;
|
|
8587
|
+
continue;
|
|
8588
|
+
}
|
|
8589
|
+
if (arg.startsWith("-")) {
|
|
8590
|
+
this.showWarning(`Unknown option for /iosm: ${arg}`);
|
|
8591
|
+
this.showWarning('Usage: /iosm [target-index] [--max-iterations N] [--force-init]');
|
|
8592
|
+
return undefined;
|
|
8593
|
+
}
|
|
8594
|
+
if (targetIndex !== undefined) {
|
|
8595
|
+
this.showWarning(`Unexpected argument for /iosm: ${arg}`);
|
|
8596
|
+
this.showWarning('Usage: /iosm [target-index] [--max-iterations N] [--force-init]');
|
|
8597
|
+
return undefined;
|
|
8598
|
+
}
|
|
8599
|
+
const parsed = Number.parseFloat(arg);
|
|
8600
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
|
|
8601
|
+
this.showWarning(`Invalid target index for /iosm: ${arg}`);
|
|
8602
|
+
this.showWarning("Target index must be a number in the range 0.0 to 1.0.");
|
|
8603
|
+
return undefined;
|
|
8604
|
+
}
|
|
8605
|
+
targetIndex = parsed;
|
|
8606
|
+
}
|
|
8607
|
+
return { targetIndex, maxIterations, forceInit };
|
|
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
|
+
}
|
|
6778
8793
|
}
|
|
6779
8794
|
}
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
guidePath = undefined;
|
|
8795
|
+
else if (selected === "Open manual /contract editor") {
|
|
8796
|
+
await this.runContractInteractiveMenu();
|
|
6783
8797
|
}
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
8798
|
+
const state = this.getContractStateSafe();
|
|
8799
|
+
if (!state)
|
|
8800
|
+
return undefined;
|
|
8801
|
+
if (this.isEffectiveContractReady(state.effective)) {
|
|
8802
|
+
return state.effective;
|
|
6788
8803
|
}
|
|
6789
|
-
|
|
6790
|
-
|
|
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";
|
|
6791
8816
|
}
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
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}`;
|
|
6803
8831
|
}
|
|
6804
8832
|
catch (error) {
|
|
6805
8833
|
const message = error instanceof Error ? error.message : String(error);
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
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 };
|
|
6814
8844
|
}
|
|
6815
|
-
|
|
6816
|
-
|
|
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") {
|
|
6817
8867
|
return {
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
activityLog,
|
|
8868
|
+
subcommand: "watch",
|
|
8869
|
+
runId: rest[0],
|
|
6821
8870
|
};
|
|
6822
8871
|
}
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
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;
|
|
6827
8903
|
}
|
|
8904
|
+
maxParallel = parsed;
|
|
8905
|
+
index += 1;
|
|
8906
|
+
continue;
|
|
6828
8907
|
}
|
|
6829
|
-
|
|
6830
|
-
|
|
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;
|
|
6831
8918
|
}
|
|
6832
|
-
if (
|
|
6833
|
-
|
|
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;
|
|
6834
8929
|
}
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
this.loadingAnimation.stop();
|
|
6839
|
-
this.loadingAnimation = undefined;
|
|
8930
|
+
if (token.startsWith("-")) {
|
|
8931
|
+
this.showWarning(`Unknown option for /swarm ${subcommand}: ${token}`);
|
|
8932
|
+
return undefined;
|
|
6840
8933
|
}
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
}
|
|
6845
|
-
}
|
|
6846
|
-
getLastAssistantMessage() {
|
|
6847
|
-
return this.session.messages
|
|
6848
|
-
.slice()
|
|
6849
|
-
.reverse()
|
|
6850
|
-
.find((message) => {
|
|
6851
|
-
if (message.role !== "assistant") {
|
|
6852
|
-
return false;
|
|
8934
|
+
if (subcommand === "from-singular" && !fromSingularRunId) {
|
|
8935
|
+
fromSingularRunId = token;
|
|
8936
|
+
continue;
|
|
6853
8937
|
}
|
|
6854
|
-
|
|
6855
|
-
}
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
if (content.type !== "text")
|
|
6862
|
-
return content;
|
|
6863
|
-
if (hideAllTextForOrchestration) {
|
|
6864
|
-
if (content.text !== "") {
|
|
6865
|
-
changed = true;
|
|
6866
|
-
}
|
|
6867
|
-
return { ...content, text: "" };
|
|
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;
|
|
6868
8945
|
}
|
|
6869
|
-
return
|
|
6870
|
-
}
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
const loaded = loadCustomSubagents({ cwd, agentDir: getAgentDir() });
|
|
6876
|
-
if (loaded.agents.length === 0)
|
|
6877
|
-
return undefined;
|
|
6878
|
-
const matches = text.matchAll(/(?:^|\s)@([^\s]+)/g);
|
|
6879
|
-
for (const match of matches) {
|
|
6880
|
-
const candidate = (match[1] ?? "").trim();
|
|
6881
|
-
const resolved = resolveCustomSubagentReference(candidate, loaded.agents);
|
|
6882
|
-
if (resolved) {
|
|
6883
|
-
return resolved;
|
|
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;
|
|
6884
8952
|
}
|
|
8953
|
+
return {
|
|
8954
|
+
subcommand,
|
|
8955
|
+
runId: fromSingularRunId,
|
|
8956
|
+
option: fromSingularOption,
|
|
8957
|
+
maxParallel,
|
|
8958
|
+
budgetUsd,
|
|
8959
|
+
};
|
|
6885
8960
|
}
|
|
8961
|
+
this.showWarning(`Unknown /swarm subcommand "${subcommand}". Use /swarm help.`);
|
|
6886
8962
|
return undefined;
|
|
6887
8963
|
}
|
|
6888
|
-
|
|
6889
|
-
const
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
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
|
+
};
|
|
6898
8997
|
}
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
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);
|
|
6930
9190
|
}
|
|
9191
|
+
accumulateFailureCauses(details?.failureCauses);
|
|
9192
|
+
if (event.isError) {
|
|
9193
|
+
taskErrors.push(result?.error ?? result?.output ?? "task tool failed");
|
|
9194
|
+
}
|
|
9195
|
+
return;
|
|
6931
9196
|
}
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
await
|
|
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, {
|
|
6942
9207
|
expandPromptTemplates: false,
|
|
9208
|
+
skipIosmAutopilot: true,
|
|
9209
|
+
skipOrchestrationDirective: true,
|
|
6943
9210
|
source: "interactive",
|
|
6944
9211
|
});
|
|
6945
|
-
return;
|
|
6946
9212
|
}
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
this.loadingAnimation = new DecryptLoader(this.ui, (s) => theme.fg("accent", s), (t) => theme.fg("muted", t), loaderMessage);
|
|
6962
|
-
this.statusContainer.addChild(this.loadingAnimation);
|
|
6963
|
-
break;
|
|
6964
|
-
case "message_start":
|
|
6965
|
-
if (event.message.role === "assistant") {
|
|
6966
|
-
verifyStreamingComponent = new AssistantMessageComponent(undefined, false, this.getMarkdownThemeWithSettings());
|
|
6967
|
-
verifyStreamingMessage = event.message;
|
|
6968
|
-
this.chatContainer.addChild(verifyStreamingComponent);
|
|
6969
|
-
verifyStreamingComponent.updateContent(verifyStreamingMessage);
|
|
6970
|
-
}
|
|
6971
|
-
break;
|
|
6972
|
-
case "message_update":
|
|
6973
|
-
if (verifyStreamingComponent && event.message.role === "assistant") {
|
|
6974
|
-
verifyStreamingMessage = event.message;
|
|
6975
|
-
verifyStreamingComponent.updateContent(verifyStreamingMessage);
|
|
6976
|
-
for (const content of verifyStreamingMessage.content) {
|
|
6977
|
-
if (content.type === "toolCall" && !verifyPendingTools.has(content.id)) {
|
|
6978
|
-
const component = new ToolExecutionComponent(content.name, content.arguments, { showImages: this.settingsManager.getShowImages() }, this.getRegisteredToolDefinition(content.name), this.ui);
|
|
6979
|
-
component.setExpanded(this.toolOutputExpanded);
|
|
6980
|
-
this.chatContainer.addChild(component);
|
|
6981
|
-
verifyPendingTools.set(content.id, component);
|
|
6982
|
-
}
|
|
6983
|
-
}
|
|
6984
|
-
}
|
|
6985
|
-
break;
|
|
6986
|
-
case "message_end":
|
|
6987
|
-
if (verifyStreamingComponent && event.message.role === "assistant") {
|
|
6988
|
-
verifyStreamingMessage = event.message;
|
|
6989
|
-
verifyStreamingComponent.updateContent(verifyStreamingMessage);
|
|
6990
|
-
verifyStreamingComponent = undefined;
|
|
6991
|
-
verifyStreamingMessage = undefined;
|
|
6992
|
-
}
|
|
6993
|
-
break;
|
|
6994
|
-
case "tool_execution_start":
|
|
6995
|
-
if (!verifyPendingTools.has(event.toolCallId)) {
|
|
6996
|
-
const component = new ToolExecutionComponent(event.toolName, event.args, { showImages: this.settingsManager.getShowImages() }, this.getRegisteredToolDefinition(event.toolName), this.ui);
|
|
6997
|
-
component.setExpanded(this.toolOutputExpanded);
|
|
6998
|
-
this.chatContainer.addChild(component);
|
|
6999
|
-
verifyPendingTools.set(event.toolCallId, component);
|
|
7000
|
-
}
|
|
7001
|
-
break;
|
|
7002
|
-
case "tool_execution_update": {
|
|
7003
|
-
const component = verifyPendingTools.get(event.toolCallId);
|
|
7004
|
-
if (component) {
|
|
7005
|
-
component.updateResult({ ...event.partialResult, isError: false }, true);
|
|
7006
|
-
}
|
|
7007
|
-
break;
|
|
7008
|
-
}
|
|
7009
|
-
case "tool_execution_end": {
|
|
7010
|
-
const component = verifyPendingTools.get(event.toolCallId);
|
|
7011
|
-
if (component) {
|
|
7012
|
-
component.updateResult({ ...event.result, isError: event.isError });
|
|
7013
|
-
verifyPendingTools.delete(event.toolCallId);
|
|
7014
|
-
}
|
|
7015
|
-
break;
|
|
7016
|
-
}
|
|
7017
|
-
case "agent_end":
|
|
7018
|
-
if (this.loadingAnimation) {
|
|
7019
|
-
this.loadingAnimation.stop();
|
|
7020
|
-
this.loadingAnimation = undefined;
|
|
7021
|
-
this.statusContainer.clear();
|
|
7022
|
-
}
|
|
7023
|
-
verifyPendingTools.clear();
|
|
7024
|
-
break;
|
|
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
|
+
});
|
|
7025
9227
|
}
|
|
7026
|
-
|
|
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),
|
|
7027
9277
|
};
|
|
7028
9278
|
}
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
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}).`);
|
|
7038
9410
|
}
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
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",
|
|
7043
9437
|
});
|
|
7044
9438
|
}
|
|
7045
|
-
async
|
|
7046
|
-
const
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
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;
|
|
7050
9444
|
}
|
|
7051
|
-
|
|
7052
|
-
if (
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
}
|
|
7056
|
-
catch {
|
|
7057
|
-
cycleDecision = undefined;
|
|
7058
|
-
}
|
|
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;
|
|
7059
9449
|
}
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
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
|
+
});
|
|
7067
9484
|
}
|
|
7068
|
-
|
|
7069
|
-
const
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
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(", ")}`);
|
|
7078
9573
|
}
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
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;
|
|
7085
9672
|
}
|
|
7086
|
-
|
|
7087
|
-
index += 1;
|
|
7088
|
-
continue;
|
|
7089
|
-
}
|
|
7090
|
-
if (arg.startsWith("-")) {
|
|
7091
|
-
this.showWarning(`Unknown option for /iosm: ${arg}`);
|
|
7092
|
-
this.showWarning('Usage: /iosm [target-index] [--max-iterations N] [--force-init]');
|
|
7093
|
-
return undefined;
|
|
7094
|
-
}
|
|
7095
|
-
if (targetIndex !== undefined) {
|
|
7096
|
-
this.showWarning(`Unexpected argument for /iosm: ${arg}`);
|
|
7097
|
-
this.showWarning('Usage: /iosm [target-index] [--max-iterations N] [--force-init]');
|
|
7098
|
-
return undefined;
|
|
9673
|
+
runId = runs[0].runId;
|
|
7099
9674
|
}
|
|
7100
|
-
const
|
|
7101
|
-
if (!
|
|
7102
|
-
this.showWarning(`
|
|
7103
|
-
|
|
7104
|
-
return undefined;
|
|
9675
|
+
const bundle = this.loadSwarmRunBundle(runId);
|
|
9676
|
+
if (!bundle) {
|
|
9677
|
+
this.showWarning(`Swarm run not found or incomplete: ${runId}`);
|
|
9678
|
+
return;
|
|
7105
9679
|
}
|
|
7106
|
-
|
|
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
|
+
});
|
|
7107
9705
|
}
|
|
7108
|
-
return { targetIndex, maxIterations, forceInit };
|
|
7109
9706
|
}
|
|
7110
9707
|
parseOrchestrateSlashCommand(text) {
|
|
7111
9708
|
const args = this.parseSlashArgs(text).slice(1);
|
|
@@ -7312,10 +9909,20 @@ export class InteractiveMode {
|
|
|
7312
9909
|
this.showWarning("Cannot run /orchestrate while compaction is running.");
|
|
7313
9910
|
return;
|
|
7314
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
|
+
}
|
|
7315
9918
|
const parsed = this.parseOrchestrateSlashCommand(text);
|
|
7316
9919
|
if (!parsed) {
|
|
7317
9920
|
return;
|
|
7318
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
|
+
}
|
|
7319
9926
|
const currentCwd = this.sessionManager.getCwd();
|
|
7320
9927
|
const assignments = [];
|
|
7321
9928
|
const assignmentRecords = [];
|
|
@@ -7359,6 +9966,7 @@ export class InteractiveMode {
|
|
|
7359
9966
|
"- use task tool for every agent assignment",
|
|
7360
9967
|
"- for parallel mode, emit all independent task calls in one assistant response",
|
|
7361
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",
|
|
7362
9970
|
"- keep required orchestration task calls in foreground; do not set background=true unless user explicitly requested detached async runs",
|
|
7363
9971
|
"- do not poll .iosm/subagents/background via bash/read during orchestration; wait for task results and then synthesize",
|
|
7364
9972
|
"- include run_id and task_id from each assignment in the task tool arguments",
|
|
@@ -9281,6 +11889,7 @@ The agent will automatically receive IOSM context on every turn.`;
|
|
|
9281
11889
|
this.ui.requestRender();
|
|
9282
11890
|
}
|
|
9283
11891
|
async handleClearCommand() {
|
|
11892
|
+
this.contractService.clear("session");
|
|
9284
11893
|
// Stop loading animation
|
|
9285
11894
|
if (this.loadingAnimation) {
|
|
9286
11895
|
this.loadingAnimation.stop();
|
|
@@ -9296,6 +11905,7 @@ The agent will automatically receive IOSM context on every turn.`;
|
|
|
9296
11905
|
this.streamingComponent = undefined;
|
|
9297
11906
|
this.streamingMessage = undefined;
|
|
9298
11907
|
this.pendingTools.clear();
|
|
11908
|
+
this.syncRuntimePromptSuffix();
|
|
9299
11909
|
this.chatContainer.addChild(new Spacer(1));
|
|
9300
11910
|
this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
|
|
9301
11911
|
this.refreshBuiltInHeader();
|
|
@@ -9466,6 +12076,7 @@ The agent will automatically receive IOSM context on every turn.`;
|
|
|
9466
12076
|
return result;
|
|
9467
12077
|
}
|
|
9468
12078
|
stop() {
|
|
12079
|
+
this.clearSubagentElapsedTimer();
|
|
9469
12080
|
if (this.loadingAnimation) {
|
|
9470
12081
|
this.loadingAnimation.stop();
|
|
9471
12082
|
this.loadingAnimation = undefined;
|