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.
Files changed (132) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +88 -46
  3. package/dist/core/agent-teams.d.ts.map +1 -1
  4. package/dist/core/agent-teams.js +38 -11
  5. package/dist/core/agent-teams.js.map +1 -1
  6. package/dist/core/blast.d.ts +62 -0
  7. package/dist/core/blast.d.ts.map +1 -0
  8. package/dist/core/blast.js +448 -0
  9. package/dist/core/blast.js.map +1 -0
  10. package/dist/core/contract.d.ts +54 -0
  11. package/dist/core/contract.d.ts.map +1 -0
  12. package/dist/core/contract.js +300 -0
  13. package/dist/core/contract.js.map +1 -0
  14. package/dist/core/failure-retrospective.d.ts +12 -0
  15. package/dist/core/failure-retrospective.d.ts.map +1 -0
  16. package/dist/core/failure-retrospective.js +115 -0
  17. package/dist/core/failure-retrospective.js.map +1 -0
  18. package/dist/core/project-index/index.d.ts +17 -0
  19. package/dist/core/project-index/index.d.ts.map +1 -0
  20. package/dist/core/project-index/index.js +323 -0
  21. package/dist/core/project-index/index.js.map +1 -0
  22. package/dist/core/project-index/types.d.ts +34 -0
  23. package/dist/core/project-index/types.d.ts.map +1 -0
  24. package/dist/core/project-index/types.js +2 -0
  25. package/dist/core/project-index/types.js.map +1 -0
  26. package/dist/core/sdk.d.ts.map +1 -1
  27. package/dist/core/sdk.js +8 -0
  28. package/dist/core/sdk.js.map +1 -1
  29. package/dist/core/semantic/config.d.ts.map +1 -1
  30. package/dist/core/semantic/config.js +5 -0
  31. package/dist/core/semantic/config.js.map +1 -1
  32. package/dist/core/semantic/index.d.ts +1 -1
  33. package/dist/core/semantic/index.d.ts.map +1 -1
  34. package/dist/core/semantic/index.js +1 -1
  35. package/dist/core/semantic/index.js.map +1 -1
  36. package/dist/core/semantic/runtime.d.ts.map +1 -1
  37. package/dist/core/semantic/runtime.js +12 -1
  38. package/dist/core/semantic/runtime.js.map +1 -1
  39. package/dist/core/semantic/types.d.ts +6 -0
  40. package/dist/core/semantic/types.d.ts.map +1 -1
  41. package/dist/core/semantic/types.js +6 -0
  42. package/dist/core/semantic/types.js.map +1 -1
  43. package/dist/core/shadow-guard.d.ts +30 -0
  44. package/dist/core/shadow-guard.d.ts.map +1 -0
  45. package/dist/core/shadow-guard.js +81 -0
  46. package/dist/core/shadow-guard.js.map +1 -0
  47. package/dist/core/shared-memory.d.ts +46 -0
  48. package/dist/core/shared-memory.d.ts.map +1 -0
  49. package/dist/core/shared-memory.js +253 -0
  50. package/dist/core/shared-memory.js.map +1 -0
  51. package/dist/core/singular.d.ts +73 -0
  52. package/dist/core/singular.d.ts.map +1 -0
  53. package/dist/core/singular.js +413 -0
  54. package/dist/core/singular.js.map +1 -0
  55. package/dist/core/slash-commands.d.ts.map +1 -1
  56. package/dist/core/slash-commands.js +14 -2
  57. package/dist/core/slash-commands.js.map +1 -1
  58. package/dist/core/subagents.js +1 -1
  59. package/dist/core/subagents.js.map +1 -1
  60. package/dist/core/swarm/gates.d.ts +9 -0
  61. package/dist/core/swarm/gates.d.ts.map +1 -0
  62. package/dist/core/swarm/gates.js +65 -0
  63. package/dist/core/swarm/gates.js.map +1 -0
  64. package/dist/core/swarm/index.d.ts +9 -0
  65. package/dist/core/swarm/index.d.ts.map +1 -0
  66. package/dist/core/swarm/index.js +9 -0
  67. package/dist/core/swarm/index.js.map +1 -0
  68. package/dist/core/swarm/locks.d.ts +21 -0
  69. package/dist/core/swarm/locks.d.ts.map +1 -0
  70. package/dist/core/swarm/locks.js +93 -0
  71. package/dist/core/swarm/locks.js.map +1 -0
  72. package/dist/core/swarm/planner.d.ts +16 -0
  73. package/dist/core/swarm/planner.d.ts.map +1 -0
  74. package/dist/core/swarm/planner.js +137 -0
  75. package/dist/core/swarm/planner.js.map +1 -0
  76. package/dist/core/swarm/retry.d.ts +16 -0
  77. package/dist/core/swarm/retry.d.ts.map +1 -0
  78. package/dist/core/swarm/retry.js +32 -0
  79. package/dist/core/swarm/retry.js.map +1 -0
  80. package/dist/core/swarm/scheduler.d.ts +48 -0
  81. package/dist/core/swarm/scheduler.d.ts.map +1 -0
  82. package/dist/core/swarm/scheduler.js +554 -0
  83. package/dist/core/swarm/scheduler.js.map +1 -0
  84. package/dist/core/swarm/spawn.d.ts +16 -0
  85. package/dist/core/swarm/spawn.d.ts.map +1 -0
  86. package/dist/core/swarm/spawn.js +42 -0
  87. package/dist/core/swarm/spawn.js.map +1 -0
  88. package/dist/core/swarm/state-store.d.ts +35 -0
  89. package/dist/core/swarm/state-store.d.ts.map +1 -0
  90. package/dist/core/swarm/state-store.js +106 -0
  91. package/dist/core/swarm/state-store.js.map +1 -0
  92. package/dist/core/swarm/types.d.ts +116 -0
  93. package/dist/core/swarm/types.d.ts.map +1 -0
  94. package/dist/core/swarm/types.js +2 -0
  95. package/dist/core/swarm/types.js.map +1 -0
  96. package/dist/core/system-prompt.d.ts.map +1 -1
  97. package/dist/core/system-prompt.js +6 -3
  98. package/dist/core/system-prompt.js.map +1 -1
  99. package/dist/core/tools/semantic-search.d.ts.map +1 -1
  100. package/dist/core/tools/semantic-search.js +1 -0
  101. package/dist/core/tools/semantic-search.js.map +1 -1
  102. package/dist/core/tools/shared-memory.d.ts +23 -0
  103. package/dist/core/tools/shared-memory.d.ts.map +1 -0
  104. package/dist/core/tools/shared-memory.js +134 -0
  105. package/dist/core/tools/shared-memory.js.map +1 -0
  106. package/dist/core/tools/task.d.ts +8 -1
  107. package/dist/core/tools/task.d.ts.map +1 -1
  108. package/dist/core/tools/task.js +664 -123
  109. package/dist/core/tools/task.js.map +1 -1
  110. package/dist/main.d.ts.map +1 -1
  111. package/dist/main.js +8 -1
  112. package/dist/main.js.map +1 -1
  113. package/dist/modes/interactive/components/custom-editor.d.ts +8 -0
  114. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  115. package/dist/modes/interactive/components/custom-editor.js +70 -1
  116. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  117. package/dist/modes/interactive/components/login-dialog.d.ts +1 -0
  118. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  119. package/dist/modes/interactive/components/login-dialog.js +27 -4
  120. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  121. package/dist/modes/interactive/components/subagent-message.d.ts.map +1 -1
  122. package/dist/modes/interactive/components/subagent-message.js +14 -0
  123. package/dist/modes/interactive/components/subagent-message.js.map +1 -1
  124. package/dist/modes/interactive/interactive-mode.d.ts +81 -0
  125. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  126. package/dist/modes/interactive/interactive-mode.js +3481 -870
  127. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  128. package/docs/cli-reference.md +29 -1
  129. package/docs/configuration.md +5 -0
  130. package/docs/interactive-mode.md +171 -2
  131. package/docs/orchestration-and-subagents.md +96 -169
  132. 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 { getDefaultSemanticSearchConfig, getSemanticConfigPath, getSemanticIndexDir, isLikelyEmbeddingModelId, listOllamaLocalModels, listOpenRouterEmbeddingModels, loadMergedSemanticConfig, SemanticConfigMissingError, SemanticRebuildRequiredError, SemanticSearchRuntime, upsertScopedSemanticSearchConfig, } from "../../core/semantic/index.js";
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
- this.activeProfileName = getAgentProfile(options.profile).name;
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", "mcp", "semantic", "memory", "doctor", "new"]
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(lines.join("\n"), 0, 0));
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
- subagent.component.update({
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 availableTools = new Set(this.session.getAllTools().map((tool) => tool.name));
3566
- const nextActiveTools = [...profile.tools];
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.session.setSystemPromptSuffix(profile.systemPromptAppend || undefined);
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
- : "Stopping current run...");
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
- await Promise.allSettled(abortPromises);
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: "Usage: /semantic setup --scope <user|project>" };
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
- parseCheckpointNameFromLabel(label) {
6137
- if (!label)
6138
- return undefined;
6139
- if (!label.startsWith(CHECKPOINT_LABEL_PREFIX))
6140
- return undefined;
6141
- const name = label.slice(CHECKPOINT_LABEL_PREFIX.length).trim();
6142
- return name.length > 0 ? name : undefined;
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
- buildCheckpointLabel(name) {
6145
- return `${CHECKPOINT_LABEL_PREFIX}${name}`;
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
- normalizeCheckpointName(raw) {
6148
- const normalized = raw.replace(/\s+/g, " ").trim();
6149
- if (!normalized)
6150
- return undefined;
6151
- if (normalized.length > 80)
6152
- return undefined;
6153
- return normalized;
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
- getSessionCheckpoints() {
6156
- const active = new Map();
6157
- for (const entry of this.sessionManager.getEntries()) {
6158
- if (entry.type !== "label")
6159
- continue;
6160
- const name = this.parseCheckpointNameFromLabel(entry.label);
6161
- if (!name) {
6162
- active.delete(entry.targetId);
6163
- continue;
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
- active.set(entry.targetId, {
6166
- name,
6167
- targetId: entry.targetId,
6168
- labelEntryId: entry.id,
6169
- timestamp: entry.timestamp,
6170
- });
6581
+ else {
6582
+ nextPayload[field.key] = normalized;
6583
+ }
6584
+ return normalizeEngineeringContract(nextPayload);
6171
6585
  }
6172
- return [...active.values()].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
6173
- }
6174
- buildDefaultCheckpointName(checkpoints) {
6175
- const used = new Set(checkpoints.map((checkpoint) => checkpoint.name.toLowerCase()));
6176
- let index = 1;
6177
- while (used.has(`cp-${index}`)) {
6178
- index += 1;
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
- return `cp-${index}`;
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
- const newestFirst = [...checkpoints].reverse();
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
- formatCheckpointOption(index, checkpoint) {
6197
- const target = this.sessionManager.getEntry(checkpoint.targetId);
6198
- const type = target?.type ?? "missing";
6199
- return `${index}. ${checkpoint.name} -> ${checkpoint.targetId} (${type}) @ ${checkpoint.timestamp}`;
6200
- }
6201
- handleCheckpointCommand(text) {
6202
- const args = this.parseSlashArgs(text).slice(1);
6203
- const subcommand = args[0]?.toLowerCase();
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 handleRollbackCommand(text) {
6228
- const args = this.parseSlashArgs(text).slice(1);
6229
- const subcommand = args[0]?.toLowerCase();
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
- const newestFirst = [...checkpoints].reverse();
6254
- const selector = args.join(" ").trim();
6255
- let target = newestFirst[0];
6256
- if (selector) {
6257
- const numeric = Number.parseInt(selector, 10);
6258
- if (Number.isFinite(numeric) && `${numeric}` === selector) {
6259
- target = newestFirst[numeric - 1];
6260
- if (!target) {
6261
- this.showWarning(`Checkpoint index ${numeric} is out of range.`);
6262
- return;
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
- else {
6266
- target = newestFirst.find((checkpoint) => checkpoint.name === selector);
6267
- if (!target) {
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
- else {
6274
- const canShowInteractiveSelector = !!this.ui && !!this.editorContainer;
6275
- if (canShowInteractiveSelector) {
6276
- const options = newestFirst.map((checkpoint, index) => this.formatCheckpointOption(index + 1, checkpoint));
6277
- const picked = await this.showExtensionSelector("/rollback: choose checkpoint", options);
6278
- if (!picked) {
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
- if (!target) {
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
- this.chatContainer.clear();
6299
- this.renderInitialMessages();
6300
- if (result.editorText && !this.editor.getText().trim()) {
6301
- this.editor.setText(result.editorText);
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 runDoctorInteractiveFixes(checks) {
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 options = [];
6321
- if (hasModelIssues) {
6322
- options.push("Open model selector");
6323
- options.push("Login provider");
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 (this.mcpRuntime) {
6326
- if (hasMcpIssues) {
6327
- options.push("Open MCP manager");
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
- options.push("Refresh MCP runtime");
6717
+ catch (error) {
6718
+ this.showWarning(error instanceof Error ? error.message : String(error));
6719
+ }
6720
+ continue;
6330
6721
  }
6331
- if (hasSemanticIssues) {
6332
- options.push("Open semantic manager");
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 (this.permissionMode === "yolo") {
6335
- options.push("Set permissions mode to ask");
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 (hasResourceIssues) {
6338
- options.push("Reload resources");
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
- options.push("Show auth/models paths");
6341
- options.push("Close");
6342
- const selected = await this.showExtensionSelector("/doctor fixes", options);
6343
- if (!selected || selected === "Close") {
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
- if (selected === "Open model selector") {
6347
- await this.showModelProviderSelector();
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 (selected === "Login provider") {
6351
- await this.showOAuthSelector("login");
6788
+ if (parsed.rest.length > 0) {
6789
+ this.showWarning(`Unexpected arguments for /contract edit: ${parsed.rest.join(" ")}`);
6352
6790
  return;
6353
6791
  }
6354
- if (selected === "Open MCP manager") {
6355
- await this.refreshMcpRuntimeAndSession();
6356
- this.showMcpSelector();
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 (selected === "Refresh MCP runtime") {
6360
- await this.refreshMcpRuntimeAndSession();
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 (selected === "Set permissions mode to ask") {
6369
- this.permissionMode = "ask";
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
- if (selected === "Show auth/models paths") {
6379
- const cwd = this.sessionManager.getCwd();
6380
- const agentDir = getAgentDir();
6381
- this.showCommandTextBlock("Runtime Paths", [
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
- async handleDoctorCommand(text) {
6393
- const args = this.parseSlashArgs(text).slice(1);
6394
- const outputJson = args.includes("--json");
6395
- const hooks = this.getHookPolicySummary();
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
- catch (error) {
6420
- semanticStatusError = error instanceof Error ? error.message : String(error);
6421
- }
6422
- const checks = [];
6423
- const addCheck = (level, label, detail, fix) => checks.push({ level, label, detail, fix });
6424
- if (!model) {
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
- if (availableModels.length === 0) {
6434
- addCheck("fail", "Available models", "No models currently have valid auth", "Run /login or configure API keys.");
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
- else {
6437
- addCheck("ok", "Available models", `${availableModels.length}/${allModels.length} ready`);
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 (modelLoadError) {
6440
- addCheck("warn", "models.json", modelLoadError.split("\n")[0] ?? modelLoadError, `Inspect ${getModelsPath()}.`);
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
- else {
6443
- addCheck("ok", "models.json", "Loaded without schema/runtime errors");
6870
+ return lines;
6871
+ }
6872
+ normalizeSingularImpactAnalysis(value, fallback) {
6873
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
6874
+ return fallback;
6444
6875
  }
6445
- if (!fs.existsSync(getAuthPath())) {
6446
- addCheck("warn", "auth.json", `Missing ${getAuthPath()}`, "Run /login to create credentials.");
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
- else {
6449
- addCheck("ok", "auth.json", `${authProviders.length} provider credential(s) stored`);
6897
+ if ((contract.scope_exclude ?? []).length > 0) {
6898
+ lines.push(`- scope_exclude: ${(contract.scope_exclude ?? []).slice(0, 8).join("; ")}`);
6450
6899
  }
6451
- if (!this.mcpRuntime) {
6452
- addCheck("warn", "MCP runtime", "Unavailable in this session");
6900
+ if ((contract.constraints ?? []).length > 0) {
6901
+ lines.push(`- constraints: ${(contract.constraints ?? []).slice(0, 10).join("; ")}`);
6453
6902
  }
6454
- else if (mcpStatuses.length === 0) {
6455
- addCheck("warn", "MCP servers", "No MCP servers configured", "Use /mcp add or iosm mcp add ...");
6903
+ if ((contract.quality_gates ?? []).length > 0) {
6904
+ lines.push(`- quality_gates: ${(contract.quality_gates ?? []).slice(0, 10).join("; ")}`);
6456
6905
  }
6457
- else if (mcpErrored > 0 || mcpConfigErrors.length > 0) {
6458
- addCheck("warn", "MCP servers", `${mcpConnected} connected, ${mcpErrored} error, ${mcpDisabled} disabled`, "Open /mcp to reconnect or inspect server errors.");
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
- else {
6461
- addCheck("ok", "MCP servers", `${mcpConnected} connected, ${mcpDisabled} disabled`);
6909
+ if ((contract.non_goals ?? []).length > 0) {
6910
+ lines.push(`- non_goals: ${(contract.non_goals ?? []).slice(0, 8).join("; ")}`);
6462
6911
  }
6463
- if (semanticStatusError) {
6464
- addCheck("fail", "Semantic index", semanticStatusError, "Run /semantic setup and retry /semantic status.");
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
- else if (!semanticStatus) {
6467
- addCheck("warn", "Semantic index", "Status unavailable", "Run /semantic setup.");
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
- else if (!semanticStatus.configured) {
6470
- addCheck("warn", "Semantic index", "Not configured", `Run /semantic setup (user: ${semanticStatus.configPathUser} or project: ${semanticStatus.configPathProject}).`);
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
- else if (!semanticStatus.enabled) {
6473
- addCheck("warn", "Semantic index", "Configured but disabled", "Enable semanticSearch.enabled or rerun /semantic setup.");
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
- else if (!semanticStatus.indexed) {
6476
- addCheck("warn", "Semantic index", "Configured but index is missing", "Run /semantic index.");
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
- else if (semanticStatus.stale) {
6479
- const requiresRebuild = semanticStatus.staleReason === "provider_changed" ||
6480
- semanticStatus.staleReason === "chunking_changed" ||
6481
- semanticStatus.staleReason === "index_filters_changed" ||
6482
- semanticStatus.staleReason === "dimension_mismatch";
6483
- addCheck("warn", "Semantic index", `Indexed but stale${semanticStatus.staleReason ? ` (${semanticStatus.staleReason})` : ""}`, requiresRebuild ? "Run /semantic rebuild." : "Run /semantic index.");
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
- else {
6486
- addCheck("ok", "Semantic index", `${semanticStatus.provider}/${semanticStatus.model} · files=${semanticStatus.files} chunks=${semanticStatus.chunks}`);
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
- if (missingCliTools.length > 0) {
6489
- addCheck("warn", "CLI toolchain", `${cliToolStatuses.length - missingCliTools.length}/${cliToolStatuses.length} available (missing: ${missingCliTools.join(", ")})`, `Install missing CLI tools: ${missingCliTools.join(", ")}.`);
6996
+ try {
6997
+ const status = await this.createSemanticRuntime().status();
6998
+ return this.buildSingularSemanticGuidanceFromStatus(status);
6490
6999
  }
6491
- else {
6492
- addCheck("ok", "CLI toolchain", `${cliToolStatuses.length}/${cliToolStatuses.length} available`);
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
- if (extensionErrors > 0 || skillDiagnostics > 0 || promptDiagnostics > 0 || themeDiagnostics > 0) {
6495
- addCheck("warn", "Resources", `extensions:${extensionErrors} skills:${skillDiagnostics} prompts:${promptDiagnostics} themes:${themeDiagnostics}`, "Run /reload and inspect warnings shown in the chat.");
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
- else {
6498
- addCheck("ok", "Resources", "No extension/skill/prompt/theme diagnostics");
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
- 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);
6501
- 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);
6502
- addCheck("ok", "Hooks", hooks
6503
- ? `U${hooks.userPromptSubmit}/P${hooks.preToolUse}/T${hooks.postToolUse}/S${hooks.stop}`
6504
- : "No hooks loaded");
6505
- const recommendations = [...new Set(checks.map((check) => check.fix).filter((fix) => !!fix))];
6506
- if (outputJson) {
6507
- this.showCommandJsonBlock("Doctor Report", {
6508
- timestamp: new Date().toISOString(),
6509
- cwd: this.sessionManager.getCwd(),
6510
- sessionFile: this.sessionManager.getSessionFile() ?? null,
6511
- activeProfile: this.activeProfileName,
6512
- externalCliTools: cliToolStatuses,
6513
- checks,
6514
- recommendations,
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
- lines.push("");
6548
- lines.push("Tip: /doctor --json");
6549
- this.showCommandTextBlock("Doctor Report", lines.join("\n"));
6550
- const wantsInteractiveFixes = !args.includes("--no-fix");
6551
- if (wantsInteractiveFixes) {
6552
- await this.runDoctorInteractiveFixes(checks);
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
- showCommandTextBlock(title, body) {
6556
- this.chatContainer.addChild(new Spacer(1));
6557
- this.chatContainer.addChild(new DynamicBorder());
6558
- this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", title)), 1, 0));
6559
- this.chatContainer.addChild(new Spacer(1));
6560
- this.chatContainer.addChild(new Text(body, 1, 0));
6561
- this.chatContainer.addChild(new DynamicBorder());
6562
- this.ui.requestRender();
6563
- }
6564
- showCommandJsonBlock(title, value) {
6565
- const json = JSON.stringify(value, null, 2);
6566
- this.chatContainer.addChild(new Spacer(1));
6567
- this.chatContainer.addChild(new DynamicBorder());
6568
- this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", title)), 1, 0));
6569
- this.chatContainer.addChild(new Spacer(1));
6570
- this.chatContainer.addChild(new Markdown(`\`\`\`json\n${json}\n\`\`\``, 1, 1, this.getMarkdownThemeWithSettings()));
6571
- this.chatContainer.addChild(new DynamicBorder());
6572
- this.ui.requestRender();
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 runIosmInitAgentVerification(result, targetDir, onEvent) {
6575
- if (!result.cycle) {
6576
- return {
6577
- completed: false,
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 cycleId = result.cycle.cycleId;
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(targetDir, agentDir);
6586
- const settingsErrors = settingsManager.drainErrors();
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: targetDir,
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, modelFallbackMessage } = await createAgentSession({
6600
- cwd: targetDir,
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
- onEvent?.(event);
6623
- if (event.type === "turn_start") {
6624
- pushActivity("agent turn started");
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 !== "tool_execution_start") {
6629
- return;
6630
- }
6631
- toolExecutions += 1;
6632
- if (event.toolName === "bash") {
6633
- const commandRaw = event.args && typeof event.args === "object" && "command" in event.args
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.iosmVerificationSession = session;
7254
+ this.singularAnalysisSession = session;
6647
7255
  try {
6648
- if (!session.model) {
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
- completed: false,
6651
- skippedReason: modelFallbackMessage ??
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
- const timeoutMs = 180_000;
6657
- const startedAt = Date.now();
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
- finally {
6689
- if (timeoutHandle) {
6690
- clearTimeout(timeoutHandle);
6691
- }
6692
- if (heartbeatHandle) {
6693
- clearInterval(heartbeatHandle);
6694
- }
7287
+ return parsed.result;
7288
+ }
7289
+ finally {
7290
+ if (session.isStreaming) {
7291
+ await session.abort().catch(() => {
7292
+ // best effort
7293
+ });
6695
7294
  }
6696
- const messages = session.state.messages;
6697
- let lastAssistant;
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
- if (lastAssistant &&
6706
- (lastAssistant.stopReason === "error" || lastAssistant.stopReason === "aborted")) {
6707
- pushActivity(`agent finished with ${lastAssistant.stopReason}`);
6708
- if (lastAssistant.stopReason === "aborted") {
6709
- return {
6710
- completed: false,
6711
- cancelled: true,
6712
- skippedReason: "Cancelled by user.",
6713
- tracePath: session.sessionTracePath ??
6714
- (isSessionTraceEnabled() ? getSessionTracePath(session.sessionManager.getSessionId()) : undefined),
6715
- activityLog,
6716
- };
6717
- }
6718
- return {
6719
- completed: false,
6720
- error: lastAssistant.errorMessage ?? `Verifier finished with ${lastAssistant.stopReason}.`,
6721
- tracePath: session.sessionTracePath ??
6722
- (isSessionTraceEnabled() ? getSessionTracePath(session.sessionManager.getSessionId()) : undefined),
6723
- activityLog,
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
- let authoredGuide;
6727
- try {
6728
- pushActivity("authoring IOSM.md from repository evidence...");
6729
- setVerifyLiveStatus("Authoring IOSM.md...");
6730
- await session.prompt(buildIosmGuideAuthoringPrompt(result), {
6731
- expandPromptTemplates: false,
6732
- skipIosmAutopilot: true,
6733
- source: "interactive",
6734
- });
6735
- const guideAssistant = (() => {
6736
- const messages = session.state.messages;
6737
- for (let index = messages.length - 1; index >= 0; index--) {
6738
- const message = messages[index];
6739
- if (message.role === "assistant") {
6740
- return message;
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
- return undefined;
6744
- })();
6745
- const guideText = extractAssistantText(guideAssistant);
6746
- const normalizedGuide = normalizeIosmGuideMarkdown(guideText);
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
- catch {
6753
- pushActivity("agent IOSM.md authoring failed; using structured fallback");
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
- let current;
6756
- let guidePath;
6757
- try {
6758
- const report = readIosmCycleReport(result.rootDir, cycleId);
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
- catch {
6781
- current = undefined;
6782
- guidePath = undefined;
8795
+ else if (selected === "Open manual /contract editor") {
8796
+ await this.runContractInteractiveMenu();
6783
8797
  }
6784
- let historyPath;
6785
- try {
6786
- const history = recordIosmCycleHistory(result.rootDir, cycleId);
6787
- historyPath = history.historyPath;
8798
+ const state = this.getContractStateSafe();
8799
+ if (!state)
8800
+ return undefined;
8801
+ if (this.isEffectiveContractReady(state.effective)) {
8802
+ return state.effective;
6788
8803
  }
6789
- catch {
6790
- historyPath = undefined;
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
- pushActivity(`completed (${toolExecutions} tool calls)`, true);
6793
- return {
6794
- completed: true,
6795
- current,
6796
- historyPath,
6797
- guidePath,
6798
- toolCalls: toolExecutions,
6799
- tracePath: session.sessionTracePath ??
6800
- (isSessionTraceEnabled() ? getSessionTracePath(session.sessionManager.getSessionId()) : undefined),
6801
- activityLog,
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
- if (isAbortLikeMessage(message)) {
6807
- pushActivity("cancelled by user", true);
6808
- return {
6809
- completed: false,
6810
- cancelled: true,
6811
- skippedReason: "Cancelled by user.",
6812
- activityLog,
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
- pushActivity(`failed (${message})`);
6816
- this.showWarning(`IOSM init verify: failed (${message})`);
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
- completed: false,
6819
- error: message,
6820
- activityLog,
8868
+ subcommand: "watch",
8869
+ runId: rest[0],
6821
8870
  };
6822
8871
  }
6823
- finally {
6824
- try {
6825
- if (session.isStreaming) {
6826
- await session.abort();
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
- catch {
6830
- // Best-effort shutdown to avoid a stuck verification loader.
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 (this.iosmVerificationSession === session) {
6833
- this.iosmVerificationSession = undefined;
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
- unsubscribe();
6836
- session.dispose();
6837
- if (this.loadingAnimation) {
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
- this.statusContainer.clear();
6842
- this.pendingWorkingMessage = undefined;
6843
- this.ui.requestRender();
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
- return !(message.stopReason === "aborted" && message.content.length === 0);
6855
- });
6856
- }
6857
- sanitizeAssistantDisplayMessage(message) {
6858
- const hideAllTextForOrchestration = this.activeAssistantOrchestrationContext;
6859
- let changed = false;
6860
- const nextContent = message.content.map((content) => {
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 content;
6870
- });
6871
- return changed ? { ...message, content: nextContent } : message;
6872
- }
6873
- resolveMentionedAgent(text) {
6874
- const cwd = this.sessionManager.getCwd();
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
- isCapabilityQuery(text) {
6889
- const normalized = text.toLowerCase().trim();
6890
- if (!normalized)
6891
- return true;
6892
- return (normalized === "?" ||
6893
- normalized === "help" ||
6894
- normalized === "--help" ||
6895
- normalized === "/help" ||
6896
- normalized === "/capabilities" ||
6897
- normalized === "capabilities");
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
- async promptWithTaskFallback(userInput) {
6900
- const mentionedAgent = this.resolveMentionedAgent(userInput);
6901
- if (mentionedAgent) {
6902
- const cleaned = userInput.replace(/(?:^|\s)@[^\s]+/g, " ").trim();
6903
- if (this.isCapabilityQuery(cleaned)) {
6904
- const cwd = this.sessionManager.getCwd();
6905
- const loaded = loadCustomSubagents({ cwd, agentDir: getAgentDir() });
6906
- const agent = loaded.agents.find((item) => item.name === mentionedAgent);
6907
- if (agent) {
6908
- const capabilityPrompt = [
6909
- "<agent_capability_query>",
6910
- `agent_name: ${agent.name}`,
6911
- `description: ${agent.description}`,
6912
- `profile: ${agent.profile ?? "full"}`,
6913
- `model: ${agent.model ?? "default"}`,
6914
- `background: ${agent.background ? "true" : "false"}`,
6915
- `tools: ${agent.tools?.join(", ") ?? "default profile tools"}`,
6916
- `disallowed_tools: ${agent.disallowedTools?.join(", ") ?? "none"}`,
6917
- "",
6918
- "agent_instructions:",
6919
- agent.instructions,
6920
- "",
6921
- `user_question: ${cleaned || "what can you do?"}`,
6922
- "Answer normally and concisely in plain language. Do not run task tool for this query.",
6923
- "</agent_capability_query>",
6924
- ].join("\n");
6925
- await this.session.prompt(capabilityPrompt, {
6926
- expandPromptTemplates: false,
6927
- source: "interactive",
6928
- });
6929
- return;
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
- const mentionPrompt = [
6933
- "<orchestrate mode=\"sequential\" agents=\"1\">",
6934
- `- agent 1: profile=${this.activeProfileName} cwd=${this.sessionManager.getCwd()} agent=${mentionedAgent}`,
6935
- `task: ${cleaned.length > 0 ? cleaned : userInput}`,
6936
- "constraints:",
6937
- "- user selected a concrete custom agent via @mention",
6938
- `- MUST call task tool with agent="${mentionedAgent}"`,
6939
- "</orchestrate>",
6940
- ].join("\n");
6941
- await this.session.prompt(mentionPrompt, {
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
- await this.session.prompt(userInput);
6948
- }
6949
- createIosmVerificationEventBridge(options) {
6950
- const loaderMessage = options?.loaderMessage ?? `Verifying workspace... (${appKey(this.keybindings, "interrupt")} to interrupt)`;
6951
- let verifyStreamingComponent;
6952
- let verifyStreamingMessage;
6953
- const verifyPendingTools = new Map();
6954
- return (event) => {
6955
- switch (event.type) {
6956
- case "agent_start":
6957
- if (this.loadingAnimation) {
6958
- this.loadingAnimation.stop();
6959
- }
6960
- this.statusContainer.clear();
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
- this.ui.requestRender();
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
- resolveIosmSnapshot(result, verification) {
7030
- let snapshot = verification?.current;
7031
- if (!snapshot && result.cycle) {
7032
- try {
7033
- snapshot = createMetricSnapshot(readIosmCycleReport(result.rootDir, result.cycle.cycleId));
7034
- }
7035
- catch {
7036
- snapshot = undefined;
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
- return (snapshot ?? {
7040
- metrics: result.analysis.metrics,
7041
- iosm_index: null,
7042
- decision_confidence: null,
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 runIosmRefreshPass(options) {
7046
- const initResult = await initIosmWorkspace({ cwd: options.cwd, force: options.force });
7047
- let verification;
7048
- if (options.agentVerify) {
7049
- verification = await this.runIosmInitAgentVerification(initResult, options.cwd, this.createIosmVerificationEventBridge());
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
- let cycleDecision;
7052
- if (initResult.cycle) {
7053
- try {
7054
- cycleDecision = inspectIosmCycle(initResult.rootDir, initResult.cycle.cycleId).decision;
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
- return {
7061
- initResult,
7062
- verification,
7063
- snapshot: this.resolveIosmSnapshot(initResult, verification),
7064
- guidePath: verification?.guidePath ?? getIosmGuidePath(initResult.rootDir),
7065
- cycleDecision,
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
- parseIosmAutomationSlashCommand(text) {
7069
- const args = this.parseSlashArgs(text).slice(1);
7070
- let targetIndex;
7071
- let maxIterations;
7072
- let forceInit = false;
7073
- for (let index = 0; index < args.length; index++) {
7074
- const arg = args[index];
7075
- if (arg === "--force-init" || arg === "--force") {
7076
- forceInit = true;
7077
- continue;
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
- if (arg === "--max-iterations") {
7080
- const nextValue = args[index + 1];
7081
- const parsed = nextValue ? Number.parseInt(nextValue, 10) : Number.NaN;
7082
- if (!Number.isInteger(parsed) || parsed < 1) {
7083
- this.showWarning('Usage: /iosm [target-index] [--max-iterations N] [--force-init]');
7084
- return undefined;
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
- maxIterations = parsed;
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 parsed = Number.parseFloat(arg);
7101
- if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
7102
- this.showWarning(`Invalid target index for /iosm: ${arg}`);
7103
- this.showWarning("Target index must be a number in the range 0.0 to 1.0.");
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
- targetIndex = parsed;
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;