iosm-cli 0.1.2 → 0.2.0

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 (103) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +37 -3
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +4 -2
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/agent-profiles.d.ts.map +1 -1
  7. package/dist/core/agent-profiles.js +1 -0
  8. package/dist/core/agent-profiles.js.map +1 -1
  9. package/dist/core/agent-session.d.ts.map +1 -1
  10. package/dist/core/agent-session.js +3 -0
  11. package/dist/core/agent-session.js.map +1 -1
  12. package/dist/core/blast.d.ts +62 -0
  13. package/dist/core/blast.d.ts.map +1 -0
  14. package/dist/core/blast.js +448 -0
  15. package/dist/core/blast.js.map +1 -0
  16. package/dist/core/contract.d.ts +54 -0
  17. package/dist/core/contract.d.ts.map +1 -0
  18. package/dist/core/contract.js +300 -0
  19. package/dist/core/contract.js.map +1 -0
  20. package/dist/core/sdk.d.ts +3 -3
  21. package/dist/core/sdk.d.ts.map +1 -1
  22. package/dist/core/sdk.js +11 -19
  23. package/dist/core/sdk.js.map +1 -1
  24. package/dist/core/semantic/chunking.d.ts +10 -0
  25. package/dist/core/semantic/chunking.d.ts.map +1 -0
  26. package/dist/core/semantic/chunking.js +82 -0
  27. package/dist/core/semantic/chunking.js.map +1 -0
  28. package/dist/core/semantic/cli.d.ts +23 -0
  29. package/dist/core/semantic/cli.d.ts.map +1 -0
  30. package/dist/core/semantic/cli.js +86 -0
  31. package/dist/core/semantic/cli.js.map +1 -0
  32. package/dist/core/semantic/config.d.ts +8 -0
  33. package/dist/core/semantic/config.d.ts.map +1 -0
  34. package/dist/core/semantic/config.js +266 -0
  35. package/dist/core/semantic/config.js.map +1 -0
  36. package/dist/core/semantic/index-store.d.ts +21 -0
  37. package/dist/core/semantic/index-store.d.ts.map +1 -0
  38. package/dist/core/semantic/index-store.js +73 -0
  39. package/dist/core/semantic/index-store.js.map +1 -0
  40. package/dist/core/semantic/index.d.ts +8 -0
  41. package/dist/core/semantic/index.d.ts.map +1 -0
  42. package/dist/core/semantic/index.js +7 -0
  43. package/dist/core/semantic/index.js.map +1 -0
  44. package/dist/core/semantic/providers.d.ts +22 -0
  45. package/dist/core/semantic/providers.d.ts.map +1 -0
  46. package/dist/core/semantic/providers.js +317 -0
  47. package/dist/core/semantic/providers.js.map +1 -0
  48. package/dist/core/semantic/runtime.d.ts +32 -0
  49. package/dist/core/semantic/runtime.d.ts.map +1 -0
  50. package/dist/core/semantic/runtime.js +510 -0
  51. package/dist/core/semantic/runtime.js.map +1 -0
  52. package/dist/core/semantic/types.d.ts +157 -0
  53. package/dist/core/semantic/types.d.ts.map +1 -0
  54. package/dist/core/semantic/types.js +23 -0
  55. package/dist/core/semantic/types.js.map +1 -0
  56. package/dist/core/shadow-guard.d.ts +30 -0
  57. package/dist/core/shadow-guard.d.ts.map +1 -0
  58. package/dist/core/shadow-guard.js +81 -0
  59. package/dist/core/shadow-guard.js.map +1 -0
  60. package/dist/core/singular.d.ts +73 -0
  61. package/dist/core/singular.d.ts.map +1 -0
  62. package/dist/core/singular.js +413 -0
  63. package/dist/core/singular.js.map +1 -0
  64. package/dist/core/slash-commands.d.ts.map +1 -1
  65. package/dist/core/slash-commands.js +12 -0
  66. package/dist/core/slash-commands.js.map +1 -1
  67. package/dist/core/system-prompt.d.ts.map +1 -1
  68. package/dist/core/system-prompt.js +21 -3
  69. package/dist/core/system-prompt.js.map +1 -1
  70. package/dist/core/tools/ast-grep.js +1 -1
  71. package/dist/core/tools/ast-grep.js.map +1 -1
  72. package/dist/core/tools/comby.js +1 -1
  73. package/dist/core/tools/comby.js.map +1 -1
  74. package/dist/core/tools/index.d.ts +9 -0
  75. package/dist/core/tools/index.d.ts.map +1 -1
  76. package/dist/core/tools/index.js +6 -0
  77. package/dist/core/tools/index.js.map +1 -1
  78. package/dist/core/tools/rg.js +1 -1
  79. package/dist/core/tools/rg.js.map +1 -1
  80. package/dist/core/tools/semantic-search.d.ts +21 -0
  81. package/dist/core/tools/semantic-search.d.ts.map +1 -0
  82. package/dist/core/tools/semantic-search.js +123 -0
  83. package/dist/core/tools/semantic-search.js.map +1 -0
  84. package/dist/index.d.ts +4 -2
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +3 -2
  87. package/dist/index.js.map +1 -1
  88. package/dist/main.d.ts.map +1 -1
  89. package/dist/main.js +124 -0
  90. package/dist/main.js.map +1 -1
  91. package/dist/modes/interactive/components/custom-editor.d.ts +8 -0
  92. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  93. package/dist/modes/interactive/components/custom-editor.js +70 -1
  94. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  95. package/dist/modes/interactive/interactive-mode.d.ts +58 -0
  96. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  97. package/dist/modes/interactive/interactive-mode.js +2067 -104
  98. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  99. package/docs/cli-reference.md +36 -1
  100. package/docs/configuration.md +79 -2
  101. package/docs/getting-started.md +1 -0
  102. package/docs/interactive-mode.md +135 -1
  103. package/package.json +1 -1
@@ -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,6 +19,9 @@ 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 { ContractService, normalizeEngineeringContract, } from "../../core/contract.js";
23
+ import { SingularService, } from "../../core/singular.js";
24
+ import { getDefaultSemanticSearchConfig, getSemanticConfigPath, getSemanticIndexDir, isLikelyEmbeddingModelId, listOllamaLocalModels, listOpenRouterEmbeddingModels, loadMergedSemanticConfig, readScopedSemanticConfig, SemanticConfigMissingError, SemanticIndexRequiredError, SemanticRebuildRequiredError, SemanticSearchRuntime, upsertScopedSemanticSearchConfig, } from "../../core/semantic/index.js";
22
25
  import { DefaultResourceLoader } from "../../core/resource-loader.js";
23
26
  import { createAgentSession } from "../../core/sdk.js";
24
27
  import { createTeamRun, getTeamRun, listTeamRuns } from "../../core/agent-teams.js";
@@ -70,6 +73,94 @@ function isExpandable(obj) {
70
73
  }
71
74
  const IOSM_PROFILE_ONLY_COMMANDS = new Set(["iosm", "cycle-list", "cycle-plan", "cycle-status", "cycle-report"]);
72
75
  const CHECKPOINT_LABEL_PREFIX = "checkpoint:";
76
+ const INTERRUPT_ABORT_TIMEOUT_MS = 8_000;
77
+ const CONTRACT_FIELD_DEFINITIONS = [
78
+ { key: "goal", kind: "text", placeholder: "Ship X with measurable impact", help: "Single primary objective." },
79
+ {
80
+ key: "scope_include",
81
+ kind: "list",
82
+ placeholder: "auth/*",
83
+ help: "What is explicitly in scope.",
84
+ },
85
+ {
86
+ key: "scope_exclude",
87
+ kind: "list",
88
+ placeholder: "billing/*",
89
+ help: "What must remain untouched for this change.",
90
+ },
91
+ {
92
+ key: "constraints",
93
+ kind: "list",
94
+ placeholder: "no breaking API changes",
95
+ help: "Hard constraints that cannot be violated.",
96
+ },
97
+ {
98
+ key: "quality_gates",
99
+ kind: "list",
100
+ placeholder: "tests pass",
101
+ help: "Objective quality checks before merge.",
102
+ },
103
+ {
104
+ key: "definition_of_done",
105
+ kind: "list",
106
+ placeholder: "docs updated",
107
+ help: "Completion criteria.",
108
+ },
109
+ {
110
+ key: "assumptions",
111
+ kind: "list",
112
+ placeholder: "API v2 stays backward compatible",
113
+ help: "Assumptions the plan depends on.",
114
+ },
115
+ {
116
+ key: "non_goals",
117
+ kind: "list",
118
+ placeholder: "no redesign in this cycle",
119
+ help: "Intentional exclusions.",
120
+ },
121
+ {
122
+ key: "risks",
123
+ kind: "list",
124
+ placeholder: "migration may affect legacy clients",
125
+ help: "Known delivery risks.",
126
+ },
127
+ {
128
+ key: "deliverables",
129
+ kind: "list",
130
+ placeholder: "CLI command + tests + docs",
131
+ help: "Expected artifacts.",
132
+ },
133
+ {
134
+ key: "success_metrics",
135
+ kind: "list",
136
+ placeholder: "p95 latency < 250ms",
137
+ help: "Measurable target outcomes.",
138
+ },
139
+ {
140
+ key: "stakeholders",
141
+ kind: "list",
142
+ placeholder: "backend team, QA",
143
+ help: "Who should review/accept the result.",
144
+ },
145
+ {
146
+ key: "owner",
147
+ kind: "text",
148
+ placeholder: "team/platform",
149
+ help: "Owner accountable for delivery.",
150
+ },
151
+ {
152
+ key: "timebox",
153
+ kind: "text",
154
+ placeholder: "this sprint",
155
+ help: "Deadline or delivery window.",
156
+ },
157
+ {
158
+ key: "notes",
159
+ kind: "text",
160
+ placeholder: "additional context",
161
+ help: "Free-form context for this contract.",
162
+ },
163
+ ];
73
164
  const DOCTOR_CLI_TOOL_SPECS = [
74
165
  {
75
166
  tool: "rg",
@@ -264,11 +355,13 @@ export class InteractiveMode {
264
355
  this.pendingWorkingMessage = undefined;
265
356
  this.defaultWorkingMessage = "Working...";
266
357
  this.activeProfileName = "full";
358
+ this.profilePromptSuffix = undefined;
267
359
  this.permissionMode = "ask";
268
360
  this.permissionAllowRules = [];
269
361
  this.permissionDenyRules = [];
270
362
  this.permissionPromptLock = Promise.resolve();
271
363
  this.sessionAllowedToolSignatures = new Set();
364
+ this.singularLastEffectiveContract = {};
272
365
  this.lastSigintTime = 0;
273
366
  this.lastEscapeTime = 0;
274
367
  this.changelogMarkdown = undefined;
@@ -309,6 +402,7 @@ export class InteractiveMode {
309
402
  // IOSM automation state
310
403
  this.iosmAutomationRun = undefined;
311
404
  this.iosmVerificationSession = undefined;
405
+ this.singularAnalysisSession = undefined;
312
406
  // Extension UI state
313
407
  this.extensionSelector = undefined;
314
408
  this.extensionInput = undefined;
@@ -353,14 +447,19 @@ export class InteractiveMode {
353
447
  this.footerDataProvider = new FooterDataProvider();
354
448
  this.footer = new FooterComponent(session, this.footerDataProvider);
355
449
  this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
356
- this.activeProfileName = getAgentProfile(options.profile).name;
450
+ const profile = getAgentProfile(options.profile);
451
+ this.activeProfileName = profile.name;
452
+ this.profilePromptSuffix = profile.systemPromptAppend || undefined;
357
453
  this.mcpRuntime = options.mcpRuntime;
454
+ this.contractService = new ContractService({ cwd: this.sessionManager.getCwd() });
455
+ this.singularService = new SingularService({ cwd: this.sessionManager.getCwd() });
358
456
  // Apply plan mode and profile badges immediately if set
359
457
  if (options.planMode || this.activeProfileName === "plan") {
360
458
  this.footer.setPlanMode(true);
361
459
  }
362
460
  this.footer.setActiveProfile(this.activeProfileName);
363
461
  this.session.setIosmAutopilotEnabled(this.activeProfileName === "iosm");
462
+ this.syncRuntimePromptSuffix();
364
463
  this.permissionMode = this.settingsManager.getPermissionMode();
365
464
  this.permissionAllowRules = this.settingsManager.getPermissionAllowRules();
366
465
  this.permissionDenyRules = this.settingsManager.getPermissionDenyRules();
@@ -372,6 +471,44 @@ export class InteractiveMode {
372
471
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
373
472
  initTheme(this.settingsManager.getTheme(), true);
374
473
  }
474
+ syncRuntimePromptSuffix() {
475
+ let contractSuffix;
476
+ try {
477
+ contractSuffix = this.contractService.buildPromptContext();
478
+ }
479
+ catch {
480
+ contractSuffix = undefined;
481
+ }
482
+ const suffix = [this.profilePromptSuffix, contractSuffix]
483
+ .map((entry) => entry?.trim())
484
+ .filter((entry) => !!entry && entry.length > 0)
485
+ .join("\n\n");
486
+ this.session.setSystemPromptSuffix(suffix || undefined);
487
+ }
488
+ getContractStateSafe() {
489
+ try {
490
+ return this.contractService.getState();
491
+ }
492
+ catch (error) {
493
+ const message = error instanceof Error ? error.message : String(error);
494
+ this.showWarning(`Contract load failed: ${message}`);
495
+ return undefined;
496
+ }
497
+ }
498
+ getProfileToolNames(profileName) {
499
+ const profile = getAgentProfile(profileName);
500
+ const availableTools = new Set(this.session.getAllTools().map((tool) => tool.name));
501
+ const nextActiveTools = [...profile.tools];
502
+ if (availableTools.has("task"))
503
+ nextActiveTools.push("task");
504
+ if (availableTools.has("todo_write"))
505
+ nextActiveTools.push("todo_write");
506
+ if (availableTools.has("todo_read"))
507
+ nextActiveTools.push("todo_read");
508
+ if (availableTools.has("ask_user"))
509
+ nextActiveTools.push("ask_user");
510
+ return [...new Set(nextActiveTools)];
511
+ }
375
512
  getToolPermissionSignature(request) {
376
513
  const summary = request.summary.trim().replace(/\s+/g, " ");
377
514
  return `${request.toolName}:${summary}`;
@@ -797,6 +934,82 @@ export class InteractiveMode {
797
934
  }
798
935
  return null;
799
936
  }
937
+ getSemanticArgumentCompletions(prefix) {
938
+ const subcommands = ["ui", "setup", "status", "index", "rebuild", "query", "help"];
939
+ const queryFlags = ["--top-k"];
940
+ const topKValues = ["5", "8", "10", "20"];
941
+ const hasTrailingSpace = /\\s$/.test(prefix);
942
+ const tokens = this.parseSlashArgs(prefix);
943
+ const first = tokens[0]?.toLowerCase();
944
+ if (!first || (tokens.length === 1 && !hasTrailingSpace)) {
945
+ const query = first ?? "";
946
+ const candidates = subcommands.filter((item) => item.includes(query));
947
+ return candidates.map((item) => ({ value: item, label: item }));
948
+ }
949
+ if (first !== "query") {
950
+ return null;
951
+ }
952
+ const topKIndex = tokens.findIndex((token) => token === "--top-k");
953
+ if (topKIndex >= 0) {
954
+ const currentValue = tokens[topKIndex + 1];
955
+ if (!currentValue) {
956
+ return topKValues.map((value) => ({ value, label: value }));
957
+ }
958
+ return topKValues
959
+ .filter((value) => value.startsWith(currentValue))
960
+ .map((value) => ({ value, label: value }));
961
+ }
962
+ const query = hasTrailingSpace ? "" : (tokens[tokens.length - 1] ?? "");
963
+ if (query.startsWith("--")) {
964
+ return queryFlags
965
+ .filter((flag) => flag.includes(query))
966
+ .map((flag) => ({ value: flag, label: flag }));
967
+ }
968
+ if (hasTrailingSpace) {
969
+ return queryFlags.map((flag) => ({ value: flag, label: flag }));
970
+ }
971
+ return null;
972
+ }
973
+ getContractArgumentCompletions(prefix) {
974
+ const subcommands = ["ui", "show", "edit", "clear", "help"];
975
+ const scopeFlags = ["--scope"];
976
+ const scopeValues = ["project", "session"];
977
+ const hasTrailingSpace = /\\s$/.test(prefix);
978
+ const tokens = this.parseSlashArgs(prefix);
979
+ const first = tokens[0]?.toLowerCase();
980
+ if (!first || (tokens.length === 1 && !hasTrailingSpace)) {
981
+ const query = first ?? "";
982
+ return [...subcommands, ...scopeFlags]
983
+ .filter((item) => item.includes(query))
984
+ .map((item) => ({ value: item, label: item }));
985
+ }
986
+ const scopeIndex = tokens.findIndex((token) => token === "--scope");
987
+ if (scopeIndex >= 0) {
988
+ const currentValue = tokens[scopeIndex + 1];
989
+ if (!currentValue) {
990
+ return scopeValues.map((value) => ({ value, label: value }));
991
+ }
992
+ return scopeValues
993
+ .filter((value) => value.startsWith(currentValue))
994
+ .map((value) => ({ value, label: value }));
995
+ }
996
+ const query = hasTrailingSpace ? "" : (tokens[tokens.length - 1] ?? "");
997
+ if (query.startsWith("--")) {
998
+ return scopeFlags.filter((flag) => flag.includes(query)).map((flag) => ({ value: flag, label: flag }));
999
+ }
1000
+ return null;
1001
+ }
1002
+ getSingularArgumentCompletions(prefix) {
1003
+ const subcommands = ["help", "last"];
1004
+ const hasTrailingSpace = /\\s$/.test(prefix);
1005
+ const tokens = this.parseSlashArgs(prefix);
1006
+ const first = tokens[0]?.toLowerCase();
1007
+ if (!first || (tokens.length === 1 && !hasTrailingSpace)) {
1008
+ const query = first ?? "";
1009
+ return subcommands.filter((item) => item.includes(query)).map((item) => ({ value: item, label: item }));
1010
+ }
1011
+ return null;
1012
+ }
800
1013
  setupAutocomplete(fdPath) {
801
1014
  // Define commands for autocomplete
802
1015
  const builtinCommands = BUILTIN_SLASH_COMMANDS.filter((command) => this.activeProfileName === "iosm" || !IOSM_PROFILE_ONLY_COMMANDS.has(command.name));
@@ -838,6 +1051,18 @@ export class InteractiveMode {
838
1051
  if (memoryCommand) {
839
1052
  memoryCommand.getArgumentCompletions = (prefix) => this.getMemoryArgumentCompletions(prefix);
840
1053
  }
1054
+ const semanticCommand = slashCommands.find((command) => command.name === "semantic");
1055
+ if (semanticCommand) {
1056
+ semanticCommand.getArgumentCompletions = (prefix) => this.getSemanticArgumentCompletions(prefix);
1057
+ }
1058
+ const contractCommand = slashCommands.find((command) => command.name === "contract");
1059
+ if (contractCommand) {
1060
+ contractCommand.getArgumentCompletions = (prefix) => this.getContractArgumentCompletions(prefix);
1061
+ }
1062
+ const singularCommand = slashCommands.find((command) => command.name === "singular");
1063
+ if (singularCommand) {
1064
+ singularCommand.getArgumentCompletions = (prefix) => this.getSingularArgumentCompletions(prefix);
1065
+ }
841
1066
  // Convert prompt templates to SlashCommand format for autocomplete
842
1067
  const templateCommands = this.session.promptTemplates.map((cmd) => ({
843
1068
  name: cmd.name,
@@ -1091,7 +1316,9 @@ export class InteractiveMode {
1091
1316
  }
1092
1317
  // Commands and keys
1093
1318
  const commandsLine = `${pad}${theme.fg("dim", "cmds")} ` +
1094
- ["model", "login", "mcp", "memory", "doctor", "new"].map((c) => theme.fg("accent", `/${c}`)).join(theme.fg("dim", " "));
1319
+ ["model", "login", "contract", "singular", "semantic", "memory", "new"]
1320
+ .map((c) => theme.fg("accent", `/${c}`))
1321
+ .join(theme.fg("dim", " "));
1095
1322
  const keysLine = `${pad}${theme.fg("dim", "keys")} ` +
1096
1323
  appKeyHint(kb, "interrupt", "stop") +
1097
1324
  theme.fg("dim", " ") +
@@ -1519,8 +1746,10 @@ export class InteractiveMode {
1519
1746
  const names = extensionPaths.map((p) => theme.fg("dim", this.formatDisplayPath(p)));
1520
1747
  lines.push(` ${theme.fg("muted", "ext")} ${names.join(theme.fg("dim", ", "))}`);
1521
1748
  }
1749
+ const resourceWidth = Math.max(24, this.ui?.terminal?.columns ?? 120);
1750
+ const safeLines = lines.map((line) => visibleWidth(line) > resourceWidth ? truncateToWidth(line, resourceWidth, "") : line);
1522
1751
  this.chatContainer.addChild(new Spacer(1));
1523
- this.chatContainer.addChild(new Text(lines.join("\n"), 0, 0));
1752
+ this.chatContainer.addChild(new Text(safeLines.join("\n"), 0, 0));
1524
1753
  }
1525
1754
  }
1526
1755
  if (showDiagnostics) {
@@ -1985,6 +2214,28 @@ export class InteractiveMode {
1985
2214
  const result = await this.showExtensionSelector(`${title}\n${message}`, ["Yes", "No"], opts);
1986
2215
  return result === "Yes";
1987
2216
  }
2217
+ async runWithExtensionLoader(message, task) {
2218
+ const loader = new BorderedLoader(this.ui, theme, message, {
2219
+ cancellable: false,
2220
+ });
2221
+ this.editorContainer.clear();
2222
+ this.editorContainer.addChild(loader);
2223
+ this.ui.setFocus(loader);
2224
+ this.ui.requestRender();
2225
+ const restoreEditor = () => {
2226
+ loader.dispose();
2227
+ this.editorContainer.clear();
2228
+ this.editorContainer.addChild(this.editor);
2229
+ this.ui.setFocus(this.editor);
2230
+ this.ui.requestRender();
2231
+ };
2232
+ try {
2233
+ return await task();
2234
+ }
2235
+ finally {
2236
+ restoreEditor();
2237
+ }
2238
+ }
1988
2239
  /**
1989
2240
  * Show a text input for extensions.
1990
2241
  */
@@ -2222,6 +2473,7 @@ export class InteractiveMode {
2222
2473
  this.session.isRetrying ||
2223
2474
  this.iosmAutomationRun !== undefined ||
2224
2475
  this.iosmVerificationSession !== undefined ||
2476
+ this.singularAnalysisSession !== undefined ||
2225
2477
  queuedMessages.steering.length > 0 ||
2226
2478
  queuedMessages.followUp.length > 0 ||
2227
2479
  queuedMeta.length > 0;
@@ -2384,6 +2636,21 @@ export class InteractiveMode {
2384
2636
  await this.handleMemoryCommand(text);
2385
2637
  return;
2386
2638
  }
2639
+ if (text === "/semantic" || text.startsWith("/semantic ")) {
2640
+ this.editor.setText("");
2641
+ await this.handleSemanticCommand(text);
2642
+ return;
2643
+ }
2644
+ if (text === "/contract" || text.startsWith("/contract ")) {
2645
+ this.editor.setText("");
2646
+ await this.handleContractCommand(text);
2647
+ return;
2648
+ }
2649
+ if (text === "/singular" || text.startsWith("/singular ")) {
2650
+ this.editor.setText("");
2651
+ await this.handleSingularCommand(text);
2652
+ return;
2653
+ }
2387
2654
  if (text === "/settings") {
2388
2655
  this.showSettingsSelector();
2389
2656
  this.editor.setText("");
@@ -3492,19 +3759,11 @@ export class InteractiveMode {
3492
3759
  }
3493
3760
  applyProfile(profileName) {
3494
3761
  const profile = getAgentProfile(profileName);
3495
- const availableTools = new Set(this.session.getAllTools().map((tool) => tool.name));
3496
- const nextActiveTools = [...profile.tools];
3497
- if (availableTools.has("task"))
3498
- nextActiveTools.push("task");
3499
- if (availableTools.has("todo_write"))
3500
- nextActiveTools.push("todo_write");
3501
- if (availableTools.has("todo_read"))
3502
- nextActiveTools.push("todo_read");
3503
- if (availableTools.has("ask_user"))
3504
- nextActiveTools.push("ask_user");
3505
- this.session.setActiveToolsByName([...new Set(nextActiveTools)]);
3762
+ const nextActiveTools = this.getProfileToolNames(profile.name);
3763
+ this.session.setActiveToolsByName(nextActiveTools);
3506
3764
  this.session.setThinkingLevel(profile.thinkingLevel);
3507
- this.session.setSystemPromptSuffix(profile.systemPromptAppend || undefined);
3765
+ this.profilePromptSuffix = profile.systemPromptAppend || undefined;
3766
+ this.syncRuntimePromptSuffix();
3508
3767
  this.session.setIosmAutopilotEnabled(profile.name === "iosm");
3509
3768
  this.activeProfileName = profile.name;
3510
3769
  this.footer.setActiveProfile(profile.name);
@@ -3749,6 +4008,8 @@ export class InteractiveMode {
3749
4008
  const hasAutomationWork = this.iosmAutomationRun !== undefined;
3750
4009
  const verificationSession = this.iosmVerificationSession;
3751
4010
  const hasVerificationWork = verificationSession !== undefined;
4011
+ const singularSession = this.singularAnalysisSession;
4012
+ const hasSingularWork = singularSession !== undefined;
3752
4013
  const hasMainStreaming = this.session.isStreaming;
3753
4014
  const hasRetryWork = this.session.isRetrying;
3754
4015
  const hasCompactionWork = this.session.isCompacting;
@@ -3756,6 +4017,7 @@ export class InteractiveMode {
3756
4017
  if (!hasPendingQueuedMessages &&
3757
4018
  !hasAutomationWork &&
3758
4019
  !hasVerificationWork &&
4020
+ !hasSingularWork &&
3759
4021
  !hasMainStreaming &&
3760
4022
  !hasRetryWork &&
3761
4023
  !hasCompactionWork &&
@@ -3788,7 +4050,9 @@ export class InteractiveMode {
3788
4050
  ? "Stopping IOSM automation..."
3789
4051
  : hasVerificationWork
3790
4052
  ? "Stopping IOSM verification..."
3791
- : "Stopping current run...");
4053
+ : hasSingularWork
4054
+ ? "Stopping /singular analysis..."
4055
+ : "Stopping current run...");
3792
4056
  const abortPromises = [];
3793
4057
  if (hasMainStreaming) {
3794
4058
  abortPromises.push(this.session.abort());
@@ -3796,8 +4060,30 @@ export class InteractiveMode {
3796
4060
  if (verificationSession) {
3797
4061
  abortPromises.push(verificationSession.abort());
3798
4062
  }
4063
+ if (singularSession) {
4064
+ abortPromises.push(singularSession.abort());
4065
+ }
3799
4066
  if (abortPromises.length > 0) {
3800
- await Promise.allSettled(abortPromises);
4067
+ const settleWithTimeout = (promise) => new Promise((resolve) => {
4068
+ let finished = false;
4069
+ const timeout = setTimeout(() => {
4070
+ if (finished)
4071
+ return;
4072
+ finished = true;
4073
+ resolve("timeout");
4074
+ }, INTERRUPT_ABORT_TIMEOUT_MS);
4075
+ promise.finally(() => {
4076
+ if (finished)
4077
+ return;
4078
+ finished = true;
4079
+ clearTimeout(timeout);
4080
+ resolve("done");
4081
+ });
4082
+ });
4083
+ const settled = await Promise.all(abortPromises.map((promise) => settleWithTimeout(promise)));
4084
+ if (settled.includes("timeout")) {
4085
+ this.showWarning(`Abort is taking longer than ${Math.round(INTERRUPT_ABORT_TIMEOUT_MS / 1000)}s. Try interrupt again if the run is still active.`);
4086
+ }
3801
4087
  }
3802
4088
  return true;
3803
4089
  }
@@ -4948,6 +5234,7 @@ export class InteractiveMode {
4948
5234
  });
4949
5235
  }
4950
5236
  async handleResumeSession(sessionPath) {
5237
+ this.contractService.clear("session");
4951
5238
  // Stop loading animation
4952
5239
  if (this.loadingAnimation) {
4953
5240
  this.loadingAnimation.stop();
@@ -4965,6 +5252,7 @@ export class InteractiveMode {
4965
5252
  // Clear and re-render the chat
4966
5253
  this.chatContainer.clear();
4967
5254
  this.renderInitialMessages();
5255
+ this.syncRuntimePromptSuffix();
4968
5256
  this.refreshBuiltInHeader();
4969
5257
  this.showStatus("Resumed session");
4970
5258
  }
@@ -5018,7 +5306,8 @@ export class InteractiveMode {
5018
5306
  const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId);
5019
5307
  return providerInfo?.name || providerId;
5020
5308
  }
5021
- async handleOpenRouterApiKeyLogin() {
5309
+ async handleOpenRouterApiKeyLogin(options) {
5310
+ const openModelSelector = options?.openModelSelector ?? true;
5022
5311
  const providerName = this.getProviderDisplayName(OPENROUTER_PROVIDER_ID);
5023
5312
  const existingCredential = this.session.modelRegistry.authStorage.get(OPENROUTER_PROVIDER_ID);
5024
5313
  if (existingCredential) {
@@ -5041,7 +5330,9 @@ export class InteractiveMode {
5041
5330
  this.session.modelRegistry.authStorage.set(OPENROUTER_PROVIDER_ID, { type: "api_key", key: apiKey });
5042
5331
  await this.updateAvailableProviderCount();
5043
5332
  this.showStatus(`${providerName} API key saved to ${getAuthPath()}`);
5044
- await this.showModelProviderSelector(OPENROUTER_PROVIDER_ID);
5333
+ if (openModelSelector) {
5334
+ await this.showModelProviderSelector(OPENROUTER_PROVIDER_ID);
5335
+ }
5045
5336
  }
5046
5337
  async showLoginDialog(providerId) {
5047
5338
  const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId);
@@ -5499,104 +5790,1693 @@ export class InteractiveMode {
5499
5790
  this.showError(error instanceof Error ? error.message : String(error));
5500
5791
  }
5501
5792
  }
5502
- parseCheckpointNameFromLabel(label) {
5503
- if (!label)
5504
- return undefined;
5505
- if (!label.startsWith(CHECKPOINT_LABEL_PREFIX))
5506
- return undefined;
5507
- const name = label.slice(CHECKPOINT_LABEL_PREFIX.length).trim();
5508
- return name.length > 0 ? name : undefined;
5509
- }
5510
- buildCheckpointLabel(name) {
5511
- return `${CHECKPOINT_LABEL_PREFIX}${name}`;
5512
- }
5513
- normalizeCheckpointName(raw) {
5514
- const normalized = raw.replace(/\s+/g, " ").trim();
5515
- if (!normalized)
5516
- return undefined;
5517
- if (normalized.length > 80)
5518
- return undefined;
5519
- return normalized;
5793
+ createSemanticRuntime() {
5794
+ return new SemanticSearchRuntime({
5795
+ cwd: this.sessionManager.getCwd(),
5796
+ agentDir: getAgentDir(),
5797
+ authStorage: this.session.modelRegistry.authStorage,
5798
+ });
5520
5799
  }
5521
- getSessionCheckpoints() {
5522
- const active = new Map();
5523
- for (const entry of this.sessionManager.getEntries()) {
5524
- if (entry.type !== "label")
5525
- continue;
5526
- const name = this.parseCheckpointNameFromLabel(entry.label);
5527
- if (!name) {
5528
- active.delete(entry.targetId);
5800
+ parseSemanticScopeOptions(args, usage = "Usage: /semantic setup --scope <user|project>") {
5801
+ let scope;
5802
+ const rest = [];
5803
+ for (let index = 0; index < args.length; index++) {
5804
+ const token = args[index] ?? "";
5805
+ const normalized = token.toLowerCase();
5806
+ if (normalized === "--scope") {
5807
+ const value = (args[index + 1] ?? "").toLowerCase();
5808
+ if (!value) {
5809
+ return { scope, rest, error: usage };
5810
+ }
5811
+ if (value !== "user" && value !== "project") {
5812
+ return { scope, rest, error: `Invalid semantic scope "${value}". Use user or project.` };
5813
+ }
5814
+ scope = value;
5815
+ index += 1;
5529
5816
  continue;
5530
5817
  }
5531
- active.set(entry.targetId, {
5532
- name,
5533
- targetId: entry.targetId,
5534
- labelEntryId: entry.id,
5535
- timestamp: entry.timestamp,
5536
- });
5818
+ rest.push(token);
5537
5819
  }
5538
- return [...active.values()].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
5820
+ return { scope, rest };
5539
5821
  }
5540
- buildDefaultCheckpointName(checkpoints) {
5541
- const used = new Set(checkpoints.map((checkpoint) => checkpoint.name.toLowerCase()));
5542
- let index = 1;
5543
- while (used.has(`cp-${index}`)) {
5544
- index += 1;
5822
+ parseSemanticTopKOptions(args) {
5823
+ let topK;
5824
+ const rest = [];
5825
+ for (let index = 0; index < args.length; index++) {
5826
+ const token = args[index] ?? "";
5827
+ const normalized = token.toLowerCase();
5828
+ if (normalized === "--top-k" || normalized === "--topk") {
5829
+ const raw = (args[index + 1] ?? "").trim();
5830
+ if (!raw) {
5831
+ return { topK, rest, error: "Usage: /semantic query <text> [--top-k 1..20]" };
5832
+ }
5833
+ const parsed = Number.parseInt(raw, 10);
5834
+ if (!Number.isFinite(parsed) || `${parsed}` !== raw || parsed < 1 || parsed > 20) {
5835
+ return { topK, rest, error: "--top-k must be an integer between 1 and 20." };
5836
+ }
5837
+ topK = parsed;
5838
+ index += 1;
5839
+ continue;
5840
+ }
5841
+ rest.push(token);
5545
5842
  }
5546
- return `cp-${index}`;
5843
+ return { topK, rest };
5547
5844
  }
5548
- formatCheckpointList(checkpoints) {
5549
- if (checkpoints.length === 0) {
5550
- return "No checkpoints yet.\nCreate one with: /checkpoint [name]";
5845
+ formatSemanticStatusReport(status) {
5846
+ const lines = [
5847
+ `configured: ${status.configured ? "yes" : "no"}`,
5848
+ `enabled: ${status.enabled ? "yes" : "no"}`,
5849
+ `auto_index: ${status.autoIndex ? "on" : "off"}`,
5850
+ `indexed: ${status.indexed ? "yes" : "no"}`,
5851
+ `stale: ${status.stale ? `yes${status.staleReason ? ` (${status.staleReason})` : ""}` : "no"}`,
5852
+ ];
5853
+ if (status.provider)
5854
+ lines.push(`provider: ${status.provider}`);
5855
+ if (status.model)
5856
+ lines.push(`model: ${status.model}`);
5857
+ lines.push(`files: ${status.files}`);
5858
+ lines.push(`chunks: ${status.chunks}`);
5859
+ if (status.dimension !== undefined)
5860
+ lines.push(`dimension: ${status.dimension}`);
5861
+ if (status.ageSeconds !== undefined)
5862
+ lines.push(`age_seconds: ${status.ageSeconds}`);
5863
+ lines.push(`index_path: ${status.indexPath}`);
5864
+ lines.push(`config_user: ${status.configPathUser}`);
5865
+ lines.push(`config_project: ${status.configPathProject}`);
5866
+ if (!status.configured) {
5867
+ lines.push("hint: run /semantic setup");
5551
5868
  }
5552
- const newestFirst = [...checkpoints].reverse();
5553
- const lines = newestFirst.map((checkpoint, index) => {
5554
- const target = this.sessionManager.getEntry(checkpoint.targetId);
5555
- const type = target?.type ?? "missing";
5556
- return `${index + 1}. ${checkpoint.name} -> ${checkpoint.targetId} (${type}) @ ${checkpoint.timestamp}`;
5557
- });
5558
- lines.push("");
5559
- lines.push("Usage: /rollback [name|index]");
5560
5869
  return lines.join("\n");
5561
5870
  }
5562
- formatCheckpointOption(index, checkpoint) {
5563
- const target = this.sessionManager.getEntry(checkpoint.targetId);
5564
- const type = target?.type ?? "missing";
5565
- return `${index}. ${checkpoint.name} -> ${checkpoint.targetId} (${type}) @ ${checkpoint.timestamp}`;
5871
+ formatSemanticIndexReport(result) {
5872
+ return [
5873
+ `action: ${result.action}`,
5874
+ `processed_files: ${result.processedFiles}`,
5875
+ `reused_files: ${result.reusedFiles}`,
5876
+ `new_files: ${result.newFiles}`,
5877
+ `changed_files: ${result.changedFiles}`,
5878
+ `removed_files: ${result.removedFiles}`,
5879
+ `chunks: ${result.chunks}`,
5880
+ `dimension: ${result.dimension}`,
5881
+ `duration_ms: ${result.durationMs}`,
5882
+ `built_at: ${result.builtAt}`,
5883
+ `index_path: ${result.indexPath}`,
5884
+ ].join("\n");
5566
5885
  }
5567
- handleCheckpointCommand(text) {
5568
- const args = this.parseSlashArgs(text).slice(1);
5569
- const subcommand = args[0]?.toLowerCase();
5570
- const checkpoints = this.getSessionCheckpoints();
5571
- if (subcommand === "list" || subcommand === "ls") {
5572
- this.showCommandTextBlock("Checkpoints", this.formatCheckpointList(checkpoints));
5573
- return;
5886
+ formatSemanticQueryReport(result) {
5887
+ const lines = [
5888
+ `query: ${result.query}`,
5889
+ `top_k: ${result.topK}`,
5890
+ `auto_refreshed: ${result.autoRefreshed ? "yes" : "no"}`,
5891
+ ];
5892
+ if (result.hits.length === 0) {
5893
+ lines.push("hits: none");
5894
+ return lines.join("\n");
5574
5895
  }
5575
- if (subcommand === "help" || subcommand === "-h" || subcommand === "--help") {
5576
- this.showCommandTextBlock("Checkpoint Help", ["Usage:", " /checkpoint [name]", " /checkpoint list", "", "Examples:", " /checkpoint", " /checkpoint before-refactor"].join("\n"));
5577
- return;
5896
+ lines.push("hits:");
5897
+ for (let index = 0; index < result.hits.length; index++) {
5898
+ const hit = result.hits[index];
5899
+ lines.push(`${index + 1}. score=${hit.score.toFixed(4)} ${hit.path}:${hit.lineStart}-${hit.lineEnd}`);
5900
+ lines.push(` ${hit.snippet}`);
5578
5901
  }
5579
- const leafId = this.sessionManager.getLeafId();
5580
- if (!leafId) {
5581
- this.showWarning("Cannot create checkpoint yet (session has no entries).");
5902
+ return lines.join("\n");
5903
+ }
5904
+ getSemanticSetupProviderLabel(type) {
5905
+ if (type === "openrouter")
5906
+ return "openrouter";
5907
+ if (type === "ollama")
5908
+ return "ollama";
5909
+ return "custom_openai";
5910
+ }
5911
+ async ensureOpenRouterSemanticCredentials() {
5912
+ const existing = await this.session.modelRegistry.authStorage.getApiKey(OPENROUTER_PROVIDER_ID);
5913
+ if (existing)
5582
5914
  return;
5583
- }
5584
- const requestedName = args.join(" ");
5585
- const name = requestedName ? this.normalizeCheckpointName(requestedName) : this.buildDefaultCheckpointName(checkpoints);
5586
- if (!name) {
5587
- this.showWarning("Invalid checkpoint name. Use 1-80 visible characters.");
5915
+ const shouldLogin = await this.showExtensionConfirm("Semantic setup: OpenRouter key missing", "No OpenRouter API key found. Open login flow now?");
5916
+ if (!shouldLogin) {
5917
+ this.showWarning("OpenRouter key is missing. semantic index/query will fail until credentials are added.");
5588
5918
  return;
5589
5919
  }
5590
- this.sessionManager.appendLabelChange(leafId, this.buildCheckpointLabel(name));
5591
- this.showStatus(`Checkpoint saved: ${name} (${leafId})`);
5592
- }
5593
- async handleRollbackCommand(text) {
5594
- const args = this.parseSlashArgs(text).slice(1);
5595
- const subcommand = args[0]?.toLowerCase();
5596
- const checkpoints = this.getSessionCheckpoints();
5597
- if (subcommand === "list" || subcommand === "ls") {
5598
- this.showCommandTextBlock("Checkpoints", this.formatCheckpointList(checkpoints));
5599
- return;
5920
+ await this.handleOpenRouterApiKeyLogin({ openModelSelector: false });
5921
+ const afterLogin = await this.session.modelRegistry.authStorage.getApiKey(OPENROUTER_PROVIDER_ID);
5922
+ if (!afterLogin) {
5923
+ this.showWarning("OpenRouter key is still missing. You can run /login later.");
5924
+ }
5925
+ }
5926
+ async selectSemanticModelFromCatalog(title, catalogModels, currentModel, options) {
5927
+ const normalizedCurrent = currentModel.trim();
5928
+ const uniqueModels = [];
5929
+ const seen = new Set();
5930
+ for (const model of catalogModels) {
5931
+ const normalized = model.trim();
5932
+ if (!normalized || seen.has(normalized))
5933
+ continue;
5934
+ seen.add(normalized);
5935
+ uniqueModels.push(normalized);
5936
+ }
5937
+ const optionToModel = new Map();
5938
+ const selectorOptions = [];
5939
+ const addOption = (label, modelId) => {
5940
+ let uniqueLabel = label;
5941
+ let suffix = 2;
5942
+ while (optionToModel.has(uniqueLabel)) {
5943
+ uniqueLabel = `${label} (${suffix})`;
5944
+ suffix += 1;
5945
+ }
5946
+ optionToModel.set(uniqueLabel, modelId);
5947
+ selectorOptions.push(uniqueLabel);
5948
+ };
5949
+ if (normalizedCurrent) {
5950
+ addOption(`Current: ${normalizedCurrent}`, normalizedCurrent);
5951
+ }
5952
+ for (const modelId of uniqueModels) {
5953
+ const marker = options?.highlightLikelyEmbedding && isLikelyEmbeddingModelId(modelId) ? " [embedding]" : "";
5954
+ addOption(`${modelId}${marker}`, modelId);
5955
+ }
5956
+ const manualOption = "Enter model manually";
5957
+ selectorOptions.push(manualOption);
5958
+ const selected = await this.showExtensionSelector(title, selectorOptions);
5959
+ if (!selected)
5960
+ return undefined;
5961
+ if (selected === manualOption) {
5962
+ const modelInput = await this.showExtensionInput(`${title}: model`, normalizedCurrent || "model-id");
5963
+ if (modelInput === undefined)
5964
+ return undefined;
5965
+ const model = modelInput.trim();
5966
+ if (!model) {
5967
+ this.showWarning("Model cannot be empty.");
5968
+ return undefined;
5969
+ }
5970
+ return model;
5971
+ }
5972
+ return optionToModel.get(selected);
5973
+ }
5974
+ async runSemanticSetupWizard(initialScope) {
5975
+ const cwd = this.sessionManager.getCwd();
5976
+ const agentDir = getAgentDir();
5977
+ const merged = loadMergedSemanticConfig(cwd, agentDir);
5978
+ const config = merged.config ?? getDefaultSemanticSearchConfig();
5979
+ let scope = initialScope;
5980
+ if (!scope) {
5981
+ const pickedScope = await this.showExtensionSelector("/semantic setup: scope", [
5982
+ "user (Recommended)",
5983
+ "project",
5984
+ ]);
5985
+ if (!pickedScope) {
5986
+ this.showStatus("Semantic setup cancelled");
5987
+ return;
5988
+ }
5989
+ scope = pickedScope.startsWith("project") ? "project" : "user";
5990
+ }
5991
+ const providerOption = await this.showExtensionSelector("/semantic setup: provider", [
5992
+ "openrouter (Recommended)",
5993
+ "ollama",
5994
+ "custom_openai",
5995
+ ]);
5996
+ if (!providerOption) {
5997
+ this.showStatus("Semantic setup cancelled");
5998
+ return;
5999
+ }
6000
+ const providerType = providerOption.startsWith("ollama")
6001
+ ? "ollama"
6002
+ : providerOption.startsWith("custom_openai")
6003
+ ? "custom_openai"
6004
+ : "openrouter";
6005
+ const providerDefaults = {
6006
+ openrouter: "openai/text-embedding-3-small",
6007
+ ollama: "nomic-embed-text",
6008
+ custom_openai: "text-embedding-3-small",
6009
+ };
6010
+ const currentModel = (config.provider.type === providerType ? config.provider.model : providerDefaults[providerType]).trim();
6011
+ let model = currentModel;
6012
+ const nextProvider = {
6013
+ ...config.provider,
6014
+ type: providerType,
6015
+ model: model || providerDefaults[providerType],
6016
+ };
6017
+ if (providerType === "ollama") {
6018
+ const defaultBase = (config.provider.type === "ollama" ? config.provider.baseUrl : undefined) ?? "http://127.0.0.1:11434";
6019
+ const baseUrlInput = await this.showExtensionInput("/semantic setup: ollama base URL", defaultBase);
6020
+ if (baseUrlInput === undefined) {
6021
+ this.showStatus("Semantic setup cancelled");
6022
+ return;
6023
+ }
6024
+ const baseUrl = baseUrlInput.trim();
6025
+ nextProvider.baseUrl = baseUrl || undefined;
6026
+ nextProvider.apiKeyEnv = undefined;
6027
+ let ollamaModels = [];
6028
+ try {
6029
+ ollamaModels = await listOllamaLocalModels({
6030
+ baseUrl: nextProvider.baseUrl,
6031
+ headers: config.provider.type === "ollama" ? config.provider.headers : undefined,
6032
+ timeoutMs: nextProvider.timeoutMs,
6033
+ });
6034
+ if (ollamaModels.length === 0) {
6035
+ this.showWarning("No local Ollama models found at /api/tags. You can enter model manually.");
6036
+ }
6037
+ }
6038
+ catch (error) {
6039
+ const errorMsg = error instanceof Error ? error.message : String(error);
6040
+ this.showWarning(`Failed to load Ollama models automatically: ${errorMsg}`);
6041
+ }
6042
+ const selectedModel = await this.selectSemanticModelFromCatalog("/semantic setup: ollama model", ollamaModels, currentModel || providerDefaults.ollama, { highlightLikelyEmbedding: true });
6043
+ if (!selectedModel) {
6044
+ this.showStatus("Semantic setup cancelled");
6045
+ return;
6046
+ }
6047
+ model = selectedModel;
6048
+ }
6049
+ else if (providerType === "custom_openai") {
6050
+ const defaultBase = (config.provider.type === "custom_openai" ? config.provider.baseUrl : undefined) ?? "http://127.0.0.1:8000/v1";
6051
+ const baseUrlInput = await this.showExtensionInput("/semantic setup: custom base URL", defaultBase);
6052
+ if (baseUrlInput === undefined) {
6053
+ this.showStatus("Semantic setup cancelled");
6054
+ return;
6055
+ }
6056
+ const baseUrl = baseUrlInput.trim();
6057
+ if (!baseUrl) {
6058
+ this.showWarning("Custom provider base URL cannot be empty.");
6059
+ return;
6060
+ }
6061
+ nextProvider.baseUrl = baseUrl;
6062
+ const defaultApiKeyEnv = (config.provider.type === "custom_openai" ? config.provider.apiKeyEnv : undefined) ?? "OPENAI_API_KEY";
6063
+ const apiKeyEnvInput = await this.showExtensionInput("/semantic setup: custom API key env (optional)", defaultApiKeyEnv);
6064
+ if (apiKeyEnvInput === undefined) {
6065
+ this.showStatus("Semantic setup cancelled");
6066
+ return;
6067
+ }
6068
+ nextProvider.apiKeyEnv = apiKeyEnvInput.trim() || undefined;
6069
+ const modelInput = await this.showExtensionInput("/semantic setup: custom model", currentModel);
6070
+ if (modelInput === undefined) {
6071
+ this.showStatus("Semantic setup cancelled");
6072
+ return;
6073
+ }
6074
+ const normalizedModel = modelInput.trim();
6075
+ if (!normalizedModel) {
6076
+ this.showWarning("Model cannot be empty.");
6077
+ return;
6078
+ }
6079
+ model = normalizedModel;
6080
+ }
6081
+ else {
6082
+ nextProvider.baseUrl = undefined;
6083
+ nextProvider.apiKeyEnv = undefined;
6084
+ let openRouterModels = [];
6085
+ try {
6086
+ openRouterModels = await listOpenRouterEmbeddingModels({
6087
+ timeoutMs: nextProvider.timeoutMs,
6088
+ authStorage: this.session.modelRegistry.authStorage,
6089
+ });
6090
+ if (openRouterModels.length === 0) {
6091
+ this.showWarning("OpenRouter embeddings catalog is empty. You can enter model manually.");
6092
+ }
6093
+ }
6094
+ catch (error) {
6095
+ const errorMsg = error instanceof Error ? error.message : String(error);
6096
+ this.showWarning(`Failed to load OpenRouter embedding models automatically: ${errorMsg}`);
6097
+ }
6098
+ const selectedModel = await this.selectSemanticModelFromCatalog("/semantic setup: openrouter model", openRouterModels, currentModel || providerDefaults.openrouter);
6099
+ if (!selectedModel) {
6100
+ this.showStatus("Semantic setup cancelled");
6101
+ return;
6102
+ }
6103
+ model = selectedModel;
6104
+ }
6105
+ nextProvider.model = model;
6106
+ while (true) {
6107
+ const headersInput = await this.showExtensionInput("/semantic setup: headers (optional KEY=VALUE,CSV; press Enter to skip)", "");
6108
+ if (headersInput === undefined) {
6109
+ this.showStatus("Semantic setup cancelled");
6110
+ return;
6111
+ }
6112
+ const parsedHeaders = this.parseMcpKeyValueMapInput(headersInput);
6113
+ if (parsedHeaders.error) {
6114
+ this.showWarning(parsedHeaders.error);
6115
+ continue;
6116
+ }
6117
+ nextProvider.headers = parsedHeaders.value;
6118
+ break;
6119
+ }
6120
+ const nextConfig = {
6121
+ ...config,
6122
+ enabled: true,
6123
+ provider: nextProvider,
6124
+ };
6125
+ const savedPath = upsertScopedSemanticSearchConfig(scope, nextConfig, cwd, agentDir);
6126
+ if (providerType === "openrouter") {
6127
+ await this.ensureOpenRouterSemanticCredentials();
6128
+ }
6129
+ this.showStatus(`Semantic setup saved (${scope})`);
6130
+ this.showCommandTextBlock("Semantic Setup", [
6131
+ `scope: ${scope}`,
6132
+ `provider: ${this.getSemanticSetupProviderLabel(providerType)}`,
6133
+ `model: ${nextProvider.model}`,
6134
+ `auto_index: ${nextConfig.autoIndex ? "on" : "off"}`,
6135
+ `config: ${savedPath}`,
6136
+ `index_dir: ${getSemanticIndexDir(cwd, agentDir)}`,
6137
+ ].join("\n"));
6138
+ }
6139
+ resolveSemanticAutoIndexWriteScope(cwd, agentDir) {
6140
+ const project = readScopedSemanticConfig("project", cwd, agentDir);
6141
+ if (!project.error && project.file.semanticSearch) {
6142
+ return "project";
6143
+ }
6144
+ return "user";
6145
+ }
6146
+ async updateSemanticAutoIndexSetting(options) {
6147
+ const cwd = this.sessionManager.getCwd();
6148
+ const agentDir = getAgentDir();
6149
+ const merged = loadMergedSemanticConfig(cwd, agentDir);
6150
+ if (!merged.config) {
6151
+ this.showWarning("Semantic search is not configured. Run /semantic setup first.");
6152
+ return;
6153
+ }
6154
+ const scope = options?.scope ?? this.resolveSemanticAutoIndexWriteScope(cwd, agentDir);
6155
+ const value = options?.value ?? !merged.config.autoIndex;
6156
+ const nextConfig = {
6157
+ ...merged.config,
6158
+ autoIndex: value,
6159
+ };
6160
+ const savedPath = upsertScopedSemanticSearchConfig(scope, nextConfig, cwd, agentDir);
6161
+ const effective = await this.createSemanticRuntime().status().catch(() => undefined);
6162
+ this.showStatus(`Semantic auto-index ${value ? "enabled" : "disabled"} (${scope}).`);
6163
+ this.showCommandTextBlock("Semantic Auto-Index", [
6164
+ `scope: ${scope}`,
6165
+ `saved: ${value ? "on" : "off"}`,
6166
+ `effective: ${effective ? (effective.autoIndex ? "on" : "off") : "unknown"}`,
6167
+ `config: ${savedPath}`,
6168
+ ].join("\n"));
6169
+ }
6170
+ async runSemanticInteractiveMenu() {
6171
+ while (true) {
6172
+ let status;
6173
+ try {
6174
+ status = await this.createSemanticRuntime().status();
6175
+ }
6176
+ catch (error) {
6177
+ this.reportSemanticError(error, "status");
6178
+ }
6179
+ const summary = status
6180
+ ? `configured=${status.configured ? "yes" : "no"} auto_index=${status.autoIndex ? "on" : "off"} indexed=${status.indexed ? "yes" : "no"} stale=${status.stale ? "yes" : "no"}`
6181
+ : "status unavailable";
6182
+ const selected = await this.showExtensionSelector(`/semantic manager\n${summary}`, [
6183
+ "Configure provider/model",
6184
+ "Show status",
6185
+ "Index now",
6186
+ "Rebuild index",
6187
+ "Query index",
6188
+ `Automatic indexing: ${status?.autoIndex ? "on" : "off"}`,
6189
+ "Show config/index paths",
6190
+ "Close",
6191
+ ]);
6192
+ if (!selected || selected === "Close") {
6193
+ return;
6194
+ }
6195
+ if (selected === "Configure provider/model") {
6196
+ await this.runSemanticSetupWizard();
6197
+ continue;
6198
+ }
6199
+ if (selected === "Show status") {
6200
+ try {
6201
+ const result = await this.runWithExtensionLoader("Checking semantic index status...", async () => this.createSemanticRuntime().status());
6202
+ this.showCommandTextBlock("Semantic Status", this.formatSemanticStatusReport(result));
6203
+ }
6204
+ catch (error) {
6205
+ this.reportSemanticError(error, "status");
6206
+ }
6207
+ continue;
6208
+ }
6209
+ if (selected === "Index now") {
6210
+ try {
6211
+ const result = await this.runWithExtensionLoader("Indexing semantic embeddings...", async () => this.createSemanticRuntime().index());
6212
+ this.showStatus(`Semantic index updated (${result.processedFiles} files).`);
6213
+ this.showCommandTextBlock("Semantic Index", this.formatSemanticIndexReport(result));
6214
+ }
6215
+ catch (error) {
6216
+ this.reportSemanticError(error, "index");
6217
+ }
6218
+ continue;
6219
+ }
6220
+ if (selected === "Rebuild index") {
6221
+ try {
6222
+ const result = await this.runWithExtensionLoader("Rebuilding semantic index...", async () => this.createSemanticRuntime().rebuild());
6223
+ this.showStatus(`Semantic index rebuilt (${result.processedFiles} files).`);
6224
+ this.showCommandTextBlock("Semantic Rebuild", this.formatSemanticIndexReport(result));
6225
+ }
6226
+ catch (error) {
6227
+ this.reportSemanticError(error, "rebuild");
6228
+ }
6229
+ continue;
6230
+ }
6231
+ if (selected === "Query index") {
6232
+ const queryInput = await this.showExtensionInput("/semantic query", "where auth token is validated");
6233
+ if (queryInput === undefined)
6234
+ continue;
6235
+ const query = queryInput.trim();
6236
+ if (!query) {
6237
+ this.showWarning("Semantic query cannot be empty.");
6238
+ continue;
6239
+ }
6240
+ const topKInput = await this.showExtensionInput("/semantic query: top-k (optional, 1..20)", "8");
6241
+ if (topKInput === undefined)
6242
+ continue;
6243
+ const topKRaw = topKInput.trim();
6244
+ let topK = undefined;
6245
+ if (topKRaw) {
6246
+ const parsed = Number.parseInt(topKRaw, 10);
6247
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 20) {
6248
+ this.showWarning("top-k must be an integer between 1 and 20.");
6249
+ continue;
6250
+ }
6251
+ topK = parsed;
6252
+ }
6253
+ try {
6254
+ const result = await this.runWithExtensionLoader("Querying semantic index...", async () => this.createSemanticRuntime().query(query, topK));
6255
+ this.showCommandTextBlock("Semantic Query", this.formatSemanticQueryReport(result));
6256
+ }
6257
+ catch (error) {
6258
+ this.reportSemanticError(error, "query");
6259
+ }
6260
+ continue;
6261
+ }
6262
+ if (selected.startsWith("Automatic indexing:")) {
6263
+ await this.updateSemanticAutoIndexSetting();
6264
+ continue;
6265
+ }
6266
+ if (selected === "Show config/index paths") {
6267
+ const runtime = this.createSemanticRuntime();
6268
+ const cwd = this.sessionManager.getCwd();
6269
+ const agentDir = getAgentDir();
6270
+ try {
6271
+ const semanticStatus = await this.runWithExtensionLoader("Loading semantic paths...", async () => runtime.status());
6272
+ this.showCommandTextBlock("Semantic Paths", [
6273
+ `user_config: ${getSemanticConfigPath("user", cwd, agentDir)}`,
6274
+ `project_config: ${getSemanticConfigPath("project", cwd, agentDir)}`,
6275
+ `index_dir: ${semanticStatus.indexPath}`,
6276
+ ].join("\n"));
6277
+ }
6278
+ catch (error) {
6279
+ this.reportSemanticError(error, "status");
6280
+ }
6281
+ continue;
6282
+ }
6283
+ }
6284
+ }
6285
+ reportSemanticError(error, context) {
6286
+ if (error instanceof SemanticConfigMissingError) {
6287
+ this.showWarning("Semantic search is not configured. Run /semantic setup.");
6288
+ this.showCommandTextBlock("Semantic Config", [
6289
+ `user_config: ${error.userConfigPath}`,
6290
+ `project_config: ${error.projectConfigPath}`,
6291
+ "next: /semantic setup",
6292
+ ].join("\n"));
6293
+ return;
6294
+ }
6295
+ if (error instanceof SemanticIndexRequiredError) {
6296
+ this.showWarning(error.message);
6297
+ this.showWarning("Run /semantic index (or enable automatic indexing in /semantic).");
6298
+ return;
6299
+ }
6300
+ if (error instanceof SemanticRebuildRequiredError) {
6301
+ this.showWarning(error.message);
6302
+ this.showWarning("Run /semantic rebuild.");
6303
+ return;
6304
+ }
6305
+ const message = error instanceof Error ? error.message : String(error);
6306
+ this.showError(`Semantic ${context} failed: ${message}`);
6307
+ }
6308
+ async handleSemanticCommand(text) {
6309
+ const args = this.parseSlashArgs(text).slice(1);
6310
+ if (args.length === 0 || (args[0]?.toLowerCase() ?? "") === "ui") {
6311
+ await this.runSemanticInteractiveMenu();
6312
+ return;
6313
+ }
6314
+ const subcommand = (args[0] ?? "").toLowerCase();
6315
+ const rest = args.slice(1);
6316
+ if (subcommand === "help" || subcommand === "-h" || subcommand === "--help") {
6317
+ this.showCommandTextBlock("Semantic Help", [
6318
+ "Usage:",
6319
+ " /semantic",
6320
+ " /semantic ui",
6321
+ " /semantic setup [--scope user|project]",
6322
+ " /semantic auto-index [on|off] [--scope user|project]",
6323
+ " /semantic status",
6324
+ " /semantic index",
6325
+ " /semantic rebuild",
6326
+ " /semantic query <text> [--top-k N]",
6327
+ " /semantic help",
6328
+ ].join("\n"));
6329
+ return;
6330
+ }
6331
+ if (subcommand === "setup") {
6332
+ const parsedScope = this.parseSemanticScopeOptions(rest);
6333
+ if (parsedScope.error) {
6334
+ this.showWarning(parsedScope.error);
6335
+ return;
6336
+ }
6337
+ if (parsedScope.rest.length > 0) {
6338
+ this.showWarning(`Unexpected arguments for /semantic setup: ${parsedScope.rest.join(" ")}`);
6339
+ return;
6340
+ }
6341
+ await this.runSemanticSetupWizard(parsedScope.scope);
6342
+ return;
6343
+ }
6344
+ if (subcommand === "auto-index" || subcommand === "autoindex") {
6345
+ const parsedScope = this.parseSemanticScopeOptions(rest, "Usage: /semantic auto-index [on|off] [--scope user|project]");
6346
+ if (parsedScope.error) {
6347
+ this.showWarning(parsedScope.error);
6348
+ return;
6349
+ }
6350
+ let value;
6351
+ if (parsedScope.rest.length > 1) {
6352
+ this.showWarning("Usage: /semantic auto-index [on|off] [--scope user|project]");
6353
+ return;
6354
+ }
6355
+ if (parsedScope.rest.length === 1) {
6356
+ const mode = parsedScope.rest[0]?.toLowerCase();
6357
+ if (mode === "on" || mode === "enable" || mode === "enabled")
6358
+ value = true;
6359
+ else if (mode === "off" || mode === "disable" || mode === "disabled")
6360
+ value = false;
6361
+ else {
6362
+ this.showWarning(`Unknown auto-index mode "${parsedScope.rest[0]}". Use on|off.`);
6363
+ return;
6364
+ }
6365
+ }
6366
+ await this.updateSemanticAutoIndexSetting({
6367
+ scope: parsedScope.scope,
6368
+ value,
6369
+ });
6370
+ return;
6371
+ }
6372
+ if (subcommand === "status") {
6373
+ try {
6374
+ const result = await this.runWithExtensionLoader("Checking semantic index status...", async () => this.createSemanticRuntime().status());
6375
+ this.showCommandTextBlock("Semantic Status", this.formatSemanticStatusReport(result));
6376
+ }
6377
+ catch (error) {
6378
+ this.reportSemanticError(error, "status");
6379
+ }
6380
+ return;
6381
+ }
6382
+ if (subcommand === "index") {
6383
+ try {
6384
+ const result = await this.runWithExtensionLoader("Indexing semantic embeddings...", async () => this.createSemanticRuntime().index());
6385
+ this.showStatus(`Semantic index updated (${result.processedFiles} files).`);
6386
+ this.showCommandTextBlock("Semantic Index", this.formatSemanticIndexReport(result));
6387
+ }
6388
+ catch (error) {
6389
+ this.reportSemanticError(error, "index");
6390
+ }
6391
+ return;
6392
+ }
6393
+ if (subcommand === "rebuild") {
6394
+ try {
6395
+ const result = await this.runWithExtensionLoader("Rebuilding semantic index...", async () => this.createSemanticRuntime().rebuild());
6396
+ this.showStatus(`Semantic index rebuilt (${result.processedFiles} files).`);
6397
+ this.showCommandTextBlock("Semantic Rebuild", this.formatSemanticIndexReport(result));
6398
+ }
6399
+ catch (error) {
6400
+ this.reportSemanticError(error, "rebuild");
6401
+ }
6402
+ return;
6403
+ }
6404
+ if (subcommand === "query") {
6405
+ const parsed = this.parseSemanticTopKOptions(rest);
6406
+ if (parsed.error) {
6407
+ this.showWarning(parsed.error);
6408
+ return;
6409
+ }
6410
+ const query = parsed.rest.join(" ").trim();
6411
+ if (!query) {
6412
+ this.showWarning("Usage: /semantic query <text> [--top-k N]");
6413
+ return;
6414
+ }
6415
+ try {
6416
+ const result = await this.runWithExtensionLoader("Querying semantic index...", async () => this.createSemanticRuntime().query(query, parsed.topK));
6417
+ this.showCommandTextBlock("Semantic Query", this.formatSemanticQueryReport(result));
6418
+ }
6419
+ catch (error) {
6420
+ this.reportSemanticError(error, "query");
6421
+ }
6422
+ return;
6423
+ }
6424
+ this.showWarning(`Unknown /semantic subcommand "${subcommand}". Use /semantic help.`);
6425
+ }
6426
+ parseContractScopeOptions(args, usage = "Usage: /contract <edit|clear> --scope <project|session>") {
6427
+ let scope;
6428
+ const rest = [];
6429
+ for (let index = 0; index < args.length; index++) {
6430
+ const token = args[index] ?? "";
6431
+ const normalized = token.toLowerCase();
6432
+ if (normalized === "--scope") {
6433
+ const value = (args[index + 1] ?? "").toLowerCase().trim();
6434
+ if (!value) {
6435
+ return { scope, rest, error: usage };
6436
+ }
6437
+ if (value !== "project" && value !== "session") {
6438
+ return { scope, rest, error: `Invalid contract scope "${value}". Use project or session.` };
6439
+ }
6440
+ scope = value;
6441
+ index += 1;
6442
+ continue;
6443
+ }
6444
+ rest.push(token);
6445
+ }
6446
+ return { scope, rest };
6447
+ }
6448
+ cloneContract(contract) {
6449
+ return normalizeEngineeringContract({
6450
+ ...(contract.goal ? { goal: contract.goal } : {}),
6451
+ ...(contract.scope_include ? { scope_include: [...contract.scope_include] } : {}),
6452
+ ...(contract.scope_exclude ? { scope_exclude: [...contract.scope_exclude] } : {}),
6453
+ ...(contract.constraints ? { constraints: [...contract.constraints] } : {}),
6454
+ ...(contract.quality_gates ? { quality_gates: [...contract.quality_gates] } : {}),
6455
+ ...(contract.definition_of_done ? { definition_of_done: [...contract.definition_of_done] } : {}),
6456
+ ...(contract.assumptions ? { assumptions: [...contract.assumptions] } : {}),
6457
+ ...(contract.non_goals ? { non_goals: [...contract.non_goals] } : {}),
6458
+ ...(contract.risks ? { risks: [...contract.risks] } : {}),
6459
+ ...(contract.deliverables ? { deliverables: [...contract.deliverables] } : {}),
6460
+ ...(contract.success_metrics ? { success_metrics: [...contract.success_metrics] } : {}),
6461
+ ...(contract.stakeholders ? { stakeholders: [...contract.stakeholders] } : {}),
6462
+ ...(contract.owner ? { owner: contract.owner } : {}),
6463
+ ...(contract.timebox ? { timebox: contract.timebox } : {}),
6464
+ ...(contract.notes ? { notes: contract.notes } : {}),
6465
+ });
6466
+ }
6467
+ formatContractSection(title, contract) {
6468
+ const payload = Object.keys(contract).length > 0 ? contract : {};
6469
+ return `${title}:\n${JSON.stringify(payload, null, 2)}`;
6470
+ }
6471
+ formatContractFieldPreview(field, value) {
6472
+ if (field.kind === "text") {
6473
+ if (typeof value !== "string" || value.trim().length === 0)
6474
+ return "(empty)";
6475
+ return value.trim();
6476
+ }
6477
+ if (!Array.isArray(value) || value.length === 0)
6478
+ return "(empty)";
6479
+ const values = value.filter((item) => typeof item === "string" && item.trim().length > 0);
6480
+ if (values.length === 0)
6481
+ return "(empty)";
6482
+ const preview = values.slice(0, 2).join("; ");
6483
+ return values.length > 2 ? `${preview} (+${values.length - 2})` : preview;
6484
+ }
6485
+ showContractState(state) {
6486
+ this.showCommandTextBlock("Contract", [
6487
+ `project_path: ${state.projectPath}${state.hasProjectFile ? "" : " (missing)"}`,
6488
+ "",
6489
+ this.formatContractSection("Project", state.project),
6490
+ "",
6491
+ this.formatContractSection("Session overlay", state.sessionOverlay),
6492
+ "",
6493
+ this.formatContractSection("Effective", state.effective),
6494
+ ].join("\n"));
6495
+ }
6496
+ async editContractFieldValue(scope, field, draft) {
6497
+ const payload = draft;
6498
+ const current = payload[field.key];
6499
+ if (field.kind === "text") {
6500
+ 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);
6501
+ if (entered === undefined)
6502
+ return undefined;
6503
+ const nextPayload = { ...payload };
6504
+ const normalized = entered.trim();
6505
+ if (normalized.length === 0) {
6506
+ delete nextPayload[field.key];
6507
+ }
6508
+ else {
6509
+ nextPayload[field.key] = normalized;
6510
+ }
6511
+ return normalizeEngineeringContract(nextPayload);
6512
+ }
6513
+ const prefill = Array.isArray(current)
6514
+ ? current.filter((item) => typeof item === "string").join("\n")
6515
+ : "";
6516
+ const edited = await this.showExtensionEditor(`/contract ${scope}: ${field.key}\n${field.help}\nOne item per line. Empty value clears the field.`, prefill);
6517
+ if (edited === undefined)
6518
+ return undefined;
6519
+ const values = edited
6520
+ .split(/\r?\n/)
6521
+ .map((line) => line.trim())
6522
+ .filter((line) => line.length > 0);
6523
+ const nextPayload = { ...payload };
6524
+ if (values.length === 0) {
6525
+ delete nextPayload[field.key];
6526
+ }
6527
+ else {
6528
+ nextPayload[field.key] = values;
6529
+ }
6530
+ return normalizeEngineeringContract(nextPayload);
6531
+ }
6532
+ formatContractEditorSummary(scope, draft) {
6533
+ const setCount = CONTRACT_FIELD_DEFINITIONS.filter((field) => {
6534
+ const value = draft[field.key];
6535
+ if (field.kind === "text")
6536
+ return typeof value === "string" && value.trim().length > 0;
6537
+ return Array.isArray(value) && value.length > 0;
6538
+ }).length;
6539
+ return `/contract editor (${scope})\nfilled=${setCount}/${CONTRACT_FIELD_DEFINITIONS.length}`;
6540
+ }
6541
+ async editContractScope(scope) {
6542
+ const state = this.getContractStateSafe();
6543
+ if (!state)
6544
+ return;
6545
+ let draft = this.cloneContract(scope === "project" ? state.project : state.sessionOverlay);
6546
+ while (true) {
6547
+ const fieldOptions = CONTRACT_FIELD_DEFINITIONS.map((field) => {
6548
+ const value = draft[field.key];
6549
+ return {
6550
+ field,
6551
+ label: `Edit ${field.key}: ${this.formatContractFieldPreview(field, value)}`,
6552
+ };
6553
+ });
6554
+ 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.`, [
6555
+ ...fieldOptions.map((entry) => entry.label),
6556
+ "Open JSON preview",
6557
+ "Delete scope contract",
6558
+ "Cancel",
6559
+ ]);
6560
+ if (!selected || selected.startsWith("Cancel")) {
6561
+ this.showStatus("Contract edit cancelled.");
6562
+ return;
6563
+ }
6564
+ if (selected.startsWith("Open JSON preview")) {
6565
+ this.showCommandTextBlock(`Contract Draft (${scope})`, JSON.stringify(Object.keys(draft).length > 0 ? draft : {}, null, 2));
6566
+ continue;
6567
+ }
6568
+ if (selected.startsWith("Delete scope contract")) {
6569
+ if (scope === "project") {
6570
+ const confirm = await this.showExtensionConfirm("Delete project contract?", `${state.projectPath}\nThis removes .iosm/contract.json`);
6571
+ if (!confirm) {
6572
+ this.showStatus("Project contract delete cancelled.");
6573
+ continue;
6574
+ }
6575
+ }
6576
+ this.contractService.clear(scope);
6577
+ this.syncRuntimePromptSuffix();
6578
+ this.showStatus(`Contract cleared (${scope}).`);
6579
+ return;
6580
+ }
6581
+ const fieldEntry = fieldOptions.find((entry) => entry.label === selected);
6582
+ const field = fieldEntry?.field;
6583
+ if (!field) {
6584
+ this.showWarning("Unknown contract field selection.");
6585
+ continue;
6586
+ }
6587
+ const updated = await this.editContractFieldValue(scope, field, draft);
6588
+ if (!updated) {
6589
+ this.showStatus(`Field edit cancelled (${field.key}).`);
6590
+ continue;
6591
+ }
6592
+ draft = updated;
6593
+ try {
6594
+ this.contractService.save(scope, draft);
6595
+ this.syncRuntimePromptSuffix();
6596
+ this.showStatus(`Saved ${field.key} (${scope}).`);
6597
+ }
6598
+ catch (error) {
6599
+ this.showWarning(error instanceof Error ? error.message : String(error));
6600
+ }
6601
+ }
6602
+ }
6603
+ async runContractInteractiveMenu() {
6604
+ while (true) {
6605
+ const state = this.getContractStateSafe();
6606
+ if (!state)
6607
+ return;
6608
+ const selected = await this.showExtensionSelector([
6609
+ `/contract manager`,
6610
+ `project=${state.hasProjectFile ? "yes" : "no"} session_keys=${Object.keys(state.sessionOverlay).length} effective_keys=${Object.keys(state.effective).length}`,
6611
+ `How to use: open effective to inspect merged JSON, edit session for temporary changes, edit project for persistent changes.`,
6612
+ `Field edits are auto-saved right after Enter.`,
6613
+ ].join("\n"), [
6614
+ "Open effective contract",
6615
+ "Edit session contract",
6616
+ "Edit project contract",
6617
+ "Copy effective -> session",
6618
+ "Copy effective -> project",
6619
+ "Delete session contract",
6620
+ "Delete project contract",
6621
+ "Close",
6622
+ ]);
6623
+ if (!selected || selected.startsWith("Close")) {
6624
+ return;
6625
+ }
6626
+ if (selected === "Open effective contract") {
6627
+ this.showContractState(state);
6628
+ continue;
6629
+ }
6630
+ if (selected === "Edit session contract") {
6631
+ await this.editContractScope("session");
6632
+ continue;
6633
+ }
6634
+ if (selected === "Edit project contract") {
6635
+ await this.editContractScope("project");
6636
+ continue;
6637
+ }
6638
+ if (selected === "Copy effective -> session") {
6639
+ try {
6640
+ this.contractService.save("session", state.effective);
6641
+ this.syncRuntimePromptSuffix();
6642
+ this.showStatus("Effective contract copied to session overlay.");
6643
+ }
6644
+ catch (error) {
6645
+ this.showWarning(error instanceof Error ? error.message : String(error));
6646
+ }
6647
+ continue;
6648
+ }
6649
+ if (selected === "Copy effective -> project") {
6650
+ try {
6651
+ this.contractService.save("project", state.effective);
6652
+ this.syncRuntimePromptSuffix();
6653
+ this.showStatus("Effective contract copied to project.");
6654
+ }
6655
+ catch (error) {
6656
+ this.showWarning(error instanceof Error ? error.message : String(error));
6657
+ }
6658
+ continue;
6659
+ }
6660
+ if (selected === "Delete session contract") {
6661
+ this.contractService.clear("session");
6662
+ this.syncRuntimePromptSuffix();
6663
+ this.showStatus("Session contract deleted.");
6664
+ continue;
6665
+ }
6666
+ if (selected === "Delete project contract") {
6667
+ const confirm = await this.showExtensionConfirm("Delete project contract?", `${state.projectPath}\nThis removes .iosm/contract.json`);
6668
+ if (!confirm) {
6669
+ this.showStatus("Project contract delete cancelled.");
6670
+ continue;
6671
+ }
6672
+ this.contractService.clear("project");
6673
+ this.syncRuntimePromptSuffix();
6674
+ this.showStatus("Project contract deleted.");
6675
+ }
6676
+ }
6677
+ }
6678
+ async handleContractCommand(text) {
6679
+ const args = this.parseSlashArgs(text).slice(1);
6680
+ if (args.length === 0 || (args[0]?.toLowerCase() ?? "") === "ui") {
6681
+ await this.runContractInteractiveMenu();
6682
+ return;
6683
+ }
6684
+ const subcommand = (args[0] ?? "").toLowerCase();
6685
+ const rest = args.slice(1);
6686
+ if (subcommand === "help" || subcommand === "-h" || subcommand === "--help") {
6687
+ this.showCommandTextBlock("Contract Help", [
6688
+ "Usage:",
6689
+ " /contract",
6690
+ " /contract ui",
6691
+ " /contract show",
6692
+ " /contract edit --scope <project|session>",
6693
+ " /contract clear --scope <project|session>",
6694
+ " /contract help",
6695
+ "",
6696
+ "Editor model:",
6697
+ " - Fill fields interactively (goal, scope, constraints, quality gates, DoD, risks, etc.)",
6698
+ " - Each field is saved immediately after Enter (no extra Save step)",
6699
+ ].join("\n"));
6700
+ return;
6701
+ }
6702
+ if (subcommand === "show" || subcommand === "status" || subcommand === "open") {
6703
+ const state = this.getContractStateSafe();
6704
+ if (!state)
6705
+ return;
6706
+ this.showContractState(state);
6707
+ return;
6708
+ }
6709
+ if (subcommand === "edit") {
6710
+ const parsed = this.parseContractScopeOptions(rest, "Usage: /contract edit --scope <project|session>");
6711
+ if (parsed.error) {
6712
+ this.showWarning(parsed.error);
6713
+ return;
6714
+ }
6715
+ if (parsed.rest.length > 0) {
6716
+ this.showWarning(`Unexpected arguments for /contract edit: ${parsed.rest.join(" ")}`);
6717
+ return;
6718
+ }
6719
+ await this.editContractScope(parsed.scope ?? "session");
6720
+ return;
6721
+ }
6722
+ if (subcommand === "clear" || subcommand === "rm" || subcommand === "remove" || subcommand === "delete") {
6723
+ const parsed = this.parseContractScopeOptions(rest, "Usage: /contract clear --scope <project|session>");
6724
+ if (parsed.error) {
6725
+ this.showWarning(parsed.error);
6726
+ return;
6727
+ }
6728
+ if (!parsed.scope) {
6729
+ this.showWarning("Usage: /contract clear --scope <project|session>");
6730
+ return;
6731
+ }
6732
+ if (parsed.rest.length > 0) {
6733
+ this.showWarning(`Unexpected arguments for /contract clear: ${parsed.rest.join(" ")}`);
6734
+ return;
6735
+ }
6736
+ this.contractService.clear(parsed.scope);
6737
+ this.syncRuntimePromptSuffix();
6738
+ this.showStatus(`Contract cleared (${parsed.scope}).`);
6739
+ return;
6740
+ }
6741
+ this.showWarning(`Unknown /contract subcommand "${subcommand}". Use /contract help.`);
6742
+ }
6743
+ normalizeSingularComplexity(value, fallback) {
6744
+ const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
6745
+ if (normalized === "low" || normalized === "medium" || normalized === "high") {
6746
+ return normalized;
6747
+ }
6748
+ return fallback;
6749
+ }
6750
+ normalizeSingularBlastRadius(value, fallback) {
6751
+ const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
6752
+ if (normalized === "low" || normalized === "medium" || normalized === "high") {
6753
+ return normalized;
6754
+ }
6755
+ return fallback;
6756
+ }
6757
+ normalizeSingularRecommendation(value, fallback) {
6758
+ const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
6759
+ if (normalized === "implement_now" || normalized === "implement_incrementally" || normalized === "defer") {
6760
+ return normalized;
6761
+ }
6762
+ return fallback;
6763
+ }
6764
+ normalizeSingularStageFit(value) {
6765
+ const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
6766
+ if (normalized === "needed_now" || normalized === "optional_now" || normalized === "later") {
6767
+ return normalized;
6768
+ }
6769
+ if (normalized === "now")
6770
+ return "needed_now";
6771
+ if (normalized === "optional")
6772
+ return "optional_now";
6773
+ return undefined;
6774
+ }
6775
+ toTrimmedString(value, maxLength, fallback) {
6776
+ if (typeof value !== "string")
6777
+ return fallback;
6778
+ const compact = value.replace(/\s+/g, " ").trim();
6779
+ if (!compact)
6780
+ return fallback;
6781
+ if (compact.length <= maxLength)
6782
+ return compact;
6783
+ return compact.slice(0, maxLength).trim();
6784
+ }
6785
+ toTrimmedStringList(value, maxItems, maxLength = 220) {
6786
+ if (!Array.isArray(value))
6787
+ return [];
6788
+ const lines = [];
6789
+ for (const item of value) {
6790
+ const normalized = this.toTrimmedString(item, maxLength);
6791
+ if (!normalized)
6792
+ continue;
6793
+ lines.push(normalized);
6794
+ if (lines.length >= maxItems)
6795
+ break;
6796
+ }
6797
+ return lines;
6798
+ }
6799
+ normalizeSingularImpactAnalysis(value, fallback) {
6800
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
6801
+ return fallback;
6802
+ }
6803
+ const payload = value;
6804
+ const codebase = this.toTrimmedString(payload.codebase, 260, fallback?.codebase ?? "Unknown.");
6805
+ const delivery = this.toTrimmedString(payload.delivery, 260, fallback?.delivery ?? "Unknown.");
6806
+ const risks = this.toTrimmedString(payload.risks, 260, fallback?.risks ?? "Unknown.");
6807
+ const operations = this.toTrimmedString(payload.operations, 260, fallback?.operations ?? "Unknown.");
6808
+ if (!codebase || !delivery || !risks || !operations)
6809
+ return fallback;
6810
+ return {
6811
+ codebase,
6812
+ delivery,
6813
+ risks,
6814
+ operations,
6815
+ };
6816
+ }
6817
+ buildSingularContractPromptSection(contract) {
6818
+ const lines = [];
6819
+ if (contract.goal)
6820
+ lines.push(`- goal: ${contract.goal}`);
6821
+ if ((contract.scope_include ?? []).length > 0) {
6822
+ lines.push(`- scope_include: ${(contract.scope_include ?? []).slice(0, 8).join("; ")}`);
6823
+ }
6824
+ if ((contract.scope_exclude ?? []).length > 0) {
6825
+ lines.push(`- scope_exclude: ${(contract.scope_exclude ?? []).slice(0, 8).join("; ")}`);
6826
+ }
6827
+ if ((contract.constraints ?? []).length > 0) {
6828
+ lines.push(`- constraints: ${(contract.constraints ?? []).slice(0, 10).join("; ")}`);
6829
+ }
6830
+ if ((contract.quality_gates ?? []).length > 0) {
6831
+ lines.push(`- quality_gates: ${(contract.quality_gates ?? []).slice(0, 10).join("; ")}`);
6832
+ }
6833
+ if ((contract.definition_of_done ?? []).length > 0) {
6834
+ lines.push(`- definition_of_done: ${(contract.definition_of_done ?? []).slice(0, 10).join("; ")}`);
6835
+ }
6836
+ if ((contract.non_goals ?? []).length > 0) {
6837
+ lines.push(`- non_goals: ${(contract.non_goals ?? []).slice(0, 8).join("; ")}`);
6838
+ }
6839
+ return lines.length > 0 ? lines.join("\n") : "- none";
6840
+ }
6841
+ buildSingularAgentPrompt(request, baseline, contract) {
6842
+ const filesHint = baseline.matchedFiles.length > 0
6843
+ ? baseline.matchedFiles.slice(0, 12).map((item) => `- ${item}`).join("\n")
6844
+ : "- no direct file matches found in heuristic pass";
6845
+ return [
6846
+ "You are running a feasibility pass for `/singular`.",
6847
+ "Task: analyze the codebase for this request and decide whether to implement now, incrementally, or defer.",
6848
+ "",
6849
+ "Hard requirements:",
6850
+ "- Inspect repository files with tools before final output (at least one tool call).",
6851
+ "- Return a human-readable markdown report (no JSON).",
6852
+ "- Include exactly three options (Option 1, Option 2, Option 3).",
6853
+ "- Each option must contain concrete file paths when possible.",
6854
+ "",
6855
+ "Use this exact template:",
6856
+ "# Singular Feasibility",
6857
+ "Recommendation: implement_now|implement_incrementally|defer",
6858
+ "Reason: <one concise reason>",
6859
+ "Complexity: low|medium|high",
6860
+ "Blast Radius: low|medium|high",
6861
+ "Stage Fit: needed_now|optional_now|later",
6862
+ "Stage Fit Reason: <why this stage fit>",
6863
+ "Impact - Codebase: <impact>",
6864
+ "Impact - Delivery: <impact>",
6865
+ "Impact - Risks: <impact>",
6866
+ "Impact - Operations: <impact>",
6867
+ "",
6868
+ "## Option 1: <title>",
6869
+ "Summary: <summary>",
6870
+ "Complexity: low|medium|high",
6871
+ "Blast Radius: low|medium|high",
6872
+ "When to choose: <guidance>",
6873
+ "Suggested files:",
6874
+ "- <path>",
6875
+ "Plan:",
6876
+ "1. <step>",
6877
+ "Pros:",
6878
+ "- <pro>",
6879
+ "Cons:",
6880
+ "- <con>",
6881
+ "",
6882
+ "## Option 2: <title>",
6883
+ "... same fields ...",
6884
+ "",
6885
+ "## Option 3: <title>",
6886
+ "... same fields ...",
6887
+ "",
6888
+ `Feature request: ${request}`,
6889
+ "",
6890
+ "Baseline scan summary (heuristic pass):",
6891
+ `- scanned_files: ${baseline.scannedFiles}`,
6892
+ `- source_files: ${baseline.sourceFiles}`,
6893
+ `- test_files: ${baseline.testFiles}`,
6894
+ `- baseline_complexity: ${baseline.baselineComplexity}`,
6895
+ `- baseline_blast_radius: ${baseline.baselineBlastRadius}`,
6896
+ `- baseline_recommendation: ${baseline.recommendation}`,
6897
+ "",
6898
+ "Matched file hints from baseline (verify, do not assume blindly):",
6899
+ filesHint,
6900
+ "",
6901
+ "Active engineering contract:",
6902
+ this.buildSingularContractPromptSection(contract),
6903
+ ].join("\n");
6904
+ }
6905
+ extractLabeledValue(text, labels, maxLength = 320) {
6906
+ for (const label of labels) {
6907
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6908
+ const match = text.match(new RegExp(`^\\s*${escaped}\\s*:\\s*(.+?)\\s*$`, "im"));
6909
+ if (!match?.[1])
6910
+ continue;
6911
+ const normalized = this.toTrimmedString(match[1], maxLength);
6912
+ if (normalized)
6913
+ return normalized;
6914
+ }
6915
+ return undefined;
6916
+ }
6917
+ parseSingularListSection(block, heading, maxItems) {
6918
+ const headings = [
6919
+ "Summary",
6920
+ "Complexity",
6921
+ "Blast Radius",
6922
+ "When to choose",
6923
+ "Suggested files",
6924
+ "Plan",
6925
+ "Pros",
6926
+ "Cons",
6927
+ ];
6928
+ const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6929
+ const nextHeadings = headings.filter((item) => item !== heading).map((item) => item.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
6930
+ const sectionRegex = new RegExp(`(?:^|\\n)\\s*${escapedHeading}\\s*:\\s*([\\s\\S]*?)(?=\\n\\s*(?:${nextHeadings.join("|")})\\s*:|\\n\\s*##\\s*Option\\s*[123]\\s*:|$)`, "i");
6931
+ const sectionMatch = block.match(sectionRegex);
6932
+ if (!sectionMatch?.[1])
6933
+ return [];
6934
+ const result = [];
6935
+ const lines = sectionMatch[1].split(/\r?\n/);
6936
+ for (const rawLine of lines) {
6937
+ let line = rawLine.trim();
6938
+ if (!line)
6939
+ continue;
6940
+ line = line.replace(/^[-*]\s+/, "").replace(/^\d+[.)]\s+/, "").trim();
6941
+ if (!line)
6942
+ continue;
6943
+ result.push(line);
6944
+ if (result.length >= maxItems)
6945
+ break;
6946
+ }
6947
+ return result;
6948
+ }
6949
+ parseSingularOptionsFromText(text, baseline) {
6950
+ const optionRegex = /^##\s*Option\s*([123])\s*:\s*(.+?)\s*$/gim;
6951
+ const matches = [...text.matchAll(optionRegex)];
6952
+ const parsed = [];
6953
+ for (let index = 0; index < matches.length; index += 1) {
6954
+ const current = matches[index];
6955
+ const optionIndexRaw = Number.parseInt(current[1] ?? "", 10);
6956
+ const optionIndex = Number.isInteger(optionIndexRaw) ? Math.max(1, Math.min(3, optionIndexRaw)) - 1 : index;
6957
+ const fallback = baseline.options[optionIndex] ?? baseline.options[Math.min(index, baseline.options.length - 1)];
6958
+ if (!fallback)
6959
+ continue;
6960
+ const bodyStart = (current.index ?? 0) + current[0].length;
6961
+ const bodyEnd = index + 1 < matches.length ? (matches[index + 1].index ?? text.length) : text.length;
6962
+ const body = text.slice(bodyStart, bodyEnd);
6963
+ const title = this.toTrimmedString(current[2], 120, fallback.title) ?? fallback.title;
6964
+ const summary = this.extractLabeledValue(body, ["Summary"], 320) ?? fallback.summary;
6965
+ const complexity = this.normalizeSingularComplexity(this.extractLabeledValue(body, ["Complexity"], 24), fallback.complexity);
6966
+ const blastRadius = this.normalizeSingularBlastRadius(this.extractLabeledValue(body, ["Blast Radius", "Blast"], 24), fallback.blast_radius);
6967
+ const whenToChoose = this.extractLabeledValue(body, ["When to choose"], 220) ?? fallback.when_to_choose;
6968
+ const suggestedFiles = this.parseSingularListSection(body, "Suggested files", 8);
6969
+ const plan = this.parseSingularListSection(body, "Plan", 8);
6970
+ const pros = this.parseSingularListSection(body, "Pros", 6);
6971
+ const cons = this.parseSingularListSection(body, "Cons", 6);
6972
+ parsed.push({
6973
+ id: String(parsed.length + 1),
6974
+ title,
6975
+ summary,
6976
+ complexity,
6977
+ blast_radius: blastRadius,
6978
+ suggested_files: suggestedFiles.length > 0 ? suggestedFiles : fallback.suggested_files,
6979
+ plan: plan.length > 0 ? plan : fallback.plan,
6980
+ pros: pros.length > 0 ? pros : fallback.pros,
6981
+ cons: cons.length > 0 ? cons : fallback.cons,
6982
+ when_to_choose: whenToChoose,
6983
+ });
6984
+ }
6985
+ while (parsed.length < 3 && parsed.length < baseline.options.length) {
6986
+ const fallback = baseline.options[parsed.length];
6987
+ parsed.push({
6988
+ ...fallback,
6989
+ id: String(parsed.length + 1),
6990
+ });
6991
+ }
6992
+ return {
6993
+ options: parsed.slice(0, 3),
6994
+ parsedCount: matches.length,
6995
+ };
6996
+ }
6997
+ parseSingularAgentAnalysisFromText(baseline, rawText) {
6998
+ const recommendationRaw = this.extractLabeledValue(rawText, ["Recommendation"], 64);
6999
+ const recommendation = this.normalizeSingularRecommendation(recommendationRaw, baseline.recommendation);
7000
+ const recommendationReason = this.extractLabeledValue(rawText, ["Reason", "Recommendation Reason"], 320) ?? baseline.recommendationReason;
7001
+ const baselineComplexity = this.normalizeSingularComplexity(this.extractLabeledValue(rawText, ["Complexity"], 24), baseline.baselineComplexity);
7002
+ const baselineBlastRadius = this.normalizeSingularBlastRadius(this.extractLabeledValue(rawText, ["Blast Radius", "Blast"], 24), baseline.baselineBlastRadius);
7003
+ const stageFit = this.normalizeSingularStageFit(this.extractLabeledValue(rawText, ["Stage Fit"], 40)) ?? baseline.stageFit;
7004
+ const stageFitReason = this.extractLabeledValue(rawText, ["Stage Fit Reason"], 280) ?? baseline.stageFitReason;
7005
+ const impactAnalysis = {
7006
+ codebase: this.extractLabeledValue(rawText, ["Impact - Codebase", "Codebase Impact"], 260) ?? "Unknown.",
7007
+ delivery: this.extractLabeledValue(rawText, ["Impact - Delivery", "Delivery Impact"], 260) ?? "Unknown.",
7008
+ risks: this.extractLabeledValue(rawText, ["Impact - Risks", "Risk Impact"], 260) ?? "Unknown.",
7009
+ operations: this.extractLabeledValue(rawText, ["Impact - Operations", "Operations Impact"], 260) ?? "Unknown.",
7010
+ };
7011
+ const { options, parsedCount } = this.parseSingularOptionsFromText(rawText, baseline);
7012
+ return {
7013
+ result: {
7014
+ ...baseline,
7015
+ recommendation,
7016
+ recommendationReason,
7017
+ baselineComplexity,
7018
+ baselineBlastRadius,
7019
+ stageFit,
7020
+ stageFitReason,
7021
+ impactAnalysis,
7022
+ options,
7023
+ },
7024
+ parsedOptions: parsedCount,
7025
+ hasRecommendation: recommendationRaw !== undefined,
7026
+ };
7027
+ }
7028
+ async runSingularAgentFeasibilityPass(request, baseline, contract) {
7029
+ const model = this.session.model;
7030
+ if (!model) {
7031
+ return undefined;
7032
+ }
7033
+ const cwd = this.sessionManager.getCwd();
7034
+ const agentDir = getAgentDir();
7035
+ const settingsManager = SettingsManager.create(cwd, agentDir);
7036
+ const authStorage = AuthStorage.create(getAuthPath());
7037
+ const modelRegistry = new ModelRegistry(authStorage, getModelsPath());
7038
+ const resourceLoader = new DefaultResourceLoader({
7039
+ cwd,
7040
+ agentDir,
7041
+ settingsManager,
7042
+ noExtensions: true,
7043
+ noSkills: true,
7044
+ noPromptTemplates: true,
7045
+ });
7046
+ await resourceLoader.reload();
7047
+ const { session } = await createAgentSession({
7048
+ cwd,
7049
+ sessionManager: SessionManager.inMemory(),
7050
+ settingsManager,
7051
+ authStorage,
7052
+ modelRegistry,
7053
+ resourceLoader,
7054
+ model,
7055
+ profile: "plan",
7056
+ enableTaskTool: false,
7057
+ });
7058
+ let toolCallsStarted = 0;
7059
+ const chunks = [];
7060
+ const eventBridge = this.createIosmVerificationEventBridge({
7061
+ loaderMessage: `Running /singular feasibility analysis... (${appKey(this.keybindings, "interrupt")} to interrupt)`,
7062
+ });
7063
+ const unsubscribe = session.subscribe((event) => {
7064
+ eventBridge(event);
7065
+ if (event.type === "tool_execution_start") {
7066
+ toolCallsStarted += 1;
7067
+ return;
7068
+ }
7069
+ if (event.type === "message_end" && event.message.role === "assistant") {
7070
+ for (const part of event.message.content) {
7071
+ if (part.type === "text" && part.text.trim()) {
7072
+ chunks.push(part.text.trim());
7073
+ }
7074
+ }
7075
+ }
7076
+ });
7077
+ this.singularAnalysisSession = session;
7078
+ try {
7079
+ const primaryPrompt = this.buildSingularAgentPrompt(request, baseline, contract);
7080
+ const strictRetryPrompt = [
7081
+ "Retry strict mode:",
7082
+ "- Inspect repository files using tools first.",
7083
+ "- Return markdown report using the exact template from previous prompt.",
7084
+ "- Include Recommendation and Option 1/2/3 sections.",
7085
+ "- Do not return JSON.",
7086
+ ].join("\n");
7087
+ const runAttempt = async (promptText) => {
7088
+ const chunkStart = chunks.length;
7089
+ const toolStart = toolCallsStarted;
7090
+ await session.prompt(promptText, {
7091
+ expandPromptTemplates: false,
7092
+ skipIosmAutopilot: true,
7093
+ skipOrchestrationDirective: true,
7094
+ source: "interactive",
7095
+ });
7096
+ return {
7097
+ text: chunks.slice(chunkStart).join("\n\n").trim(),
7098
+ toolCalls: Math.max(0, toolCallsStarted - toolStart),
7099
+ };
7100
+ };
7101
+ let attempt = await runAttempt(primaryPrompt);
7102
+ let parsed = this.parseSingularAgentAnalysisFromText(baseline, attempt.text);
7103
+ if ((parsed.parsedOptions < 3 || !parsed.hasRecommendation || attempt.toolCalls === 0) && !session.isStreaming) {
7104
+ attempt = await runAttempt(strictRetryPrompt);
7105
+ parsed = this.parseSingularAgentAnalysisFromText(baseline, attempt.text);
7106
+ }
7107
+ if ((parsed.parsedOptions < 3 || !parsed.hasRecommendation) || toolCallsStarted === 0) {
7108
+ return undefined;
7109
+ }
7110
+ return parsed.result;
7111
+ }
7112
+ finally {
7113
+ if (session.isStreaming) {
7114
+ await session.abort().catch(() => {
7115
+ // best effort
7116
+ });
7117
+ }
7118
+ if (this.singularAnalysisSession === session) {
7119
+ this.singularAnalysisSession = undefined;
7120
+ }
7121
+ unsubscribe();
7122
+ session.dispose();
7123
+ }
7124
+ }
7125
+ formatSingularRunReport(result) {
7126
+ const recommendedId = this.resolveRecommendedSingularOptionId(result);
7127
+ const coverageLine = `${result.scannedFiles} scanned · ${result.sourceFiles} source · ${result.testFiles} tests`;
7128
+ const lines = [
7129
+ `run_id: ${result.runId}`,
7130
+ `request: ${result.request}`,
7131
+ `generated_at: ${result.generatedAt}`,
7132
+ "",
7133
+ "overview:",
7134
+ ` recommendation: ${result.recommendation}`,
7135
+ ` reason: ${result.recommendationReason}`,
7136
+ ` complexity: ${result.baselineComplexity}`,
7137
+ ` blast_radius: ${result.baselineBlastRadius}`,
7138
+ ` repository_coverage: ${coverageLine}`,
7139
+ ];
7140
+ if (result.stageFit) {
7141
+ lines.push(` stage_fit: ${result.stageFit}`);
7142
+ }
7143
+ if (result.stageFitReason) {
7144
+ lines.push(` stage_fit_reason: ${result.stageFitReason}`);
7145
+ }
7146
+ if (result.impactAnalysis) {
7147
+ lines.push("");
7148
+ lines.push("impact_analysis:");
7149
+ lines.push(` codebase: ${result.impactAnalysis.codebase}`);
7150
+ lines.push(` delivery: ${result.impactAnalysis.delivery}`);
7151
+ lines.push(` risks: ${result.impactAnalysis.risks}`);
7152
+ lines.push(` operations: ${result.impactAnalysis.operations}`);
7153
+ }
7154
+ if (result.matchedFiles.length > 0) {
7155
+ lines.push("");
7156
+ lines.push(`matched_files: ${result.matchedFiles.slice(0, 8).join(", ")}`);
7157
+ }
7158
+ if (result.contractSignals.length > 0) {
7159
+ lines.push(`contract_signals: ${result.contractSignals.join(", ")}`);
7160
+ }
7161
+ lines.push("");
7162
+ lines.push("implementation_options:");
7163
+ for (const option of result.options) {
7164
+ const recommendedMark = option.id === recommendedId ? " [recommended]" : "";
7165
+ lines.push(`${option.id}. ${option.title}${recommendedMark} [complexity=${option.complexity}, blast=${option.blast_radius}]`);
7166
+ lines.push(` ${option.summary}`);
7167
+ if (option.when_to_choose) {
7168
+ lines.push(` when_to_choose: ${option.when_to_choose}`);
7169
+ }
7170
+ if (option.suggested_files.length > 0) {
7171
+ lines.push(` files: ${option.suggested_files.slice(0, 8).join(", ")}`);
7172
+ }
7173
+ if (option.plan.length > 0) {
7174
+ lines.push(` first_step: ${option.plan[0]}`);
7175
+ }
7176
+ }
7177
+ lines.push("");
7178
+ lines.push("next_action:");
7179
+ lines.push(" choose option 1/2/3 to generate a detailed execution draft in editor");
7180
+ return lines.join("\n");
7181
+ }
7182
+ buildSingularExecutionDraft(result, option, contract) {
7183
+ const effectiveContract = contract ?? this.singularLastEffectiveContract ?? {};
7184
+ const defaultQualityGates = [
7185
+ "Targeted tests for changed flows pass.",
7186
+ "No regressions in adjacent user paths.",
7187
+ "Logs/metrics updated for the new behavior.",
7188
+ ];
7189
+ const defaultDoD = [
7190
+ "Core behavior implemented and manually validated.",
7191
+ "Automated coverage added for critical path.",
7192
+ "Documentation/changelog updated for user-visible changes.",
7193
+ ];
7194
+ const qualityGates = (effectiveContract.quality_gates ?? []).length > 0
7195
+ ? (effectiveContract.quality_gates ?? []).slice(0, 10)
7196
+ : defaultQualityGates;
7197
+ const definitionOfDone = (effectiveContract.definition_of_done ?? []).length > 0
7198
+ ? (effectiveContract.definition_of_done ?? []).slice(0, 10)
7199
+ : defaultDoD;
7200
+ const constraints = (effectiveContract.constraints ?? []).slice(0, 10);
7201
+ const scopeInclude = (effectiveContract.scope_include ?? []).slice(0, 10);
7202
+ const scopeExclude = (effectiveContract.scope_exclude ?? []).slice(0, 10);
7203
+ const risksFromOption = option.cons.slice(0, 6);
7204
+ const files = option.suggested_files.slice(0, 14);
7205
+ const planSteps = option.plan.length > 0 ? option.plan : ["Implement minimal working path for selected option."];
7206
+ const lines = [
7207
+ "# Singular Execution Plan",
7208
+ "",
7209
+ `Request: ${result.request}`,
7210
+ `Selected option: ${option.id}. ${option.title}`,
7211
+ `Decision context: recommendation=${result.recommendation}, complexity=${option.complexity}, blast_radius=${option.blast_radius}`,
7212
+ ...(result.stageFit ? [`Stage fit: ${result.stageFit}`] : []),
7213
+ ...(result.stageFitReason ? [`Stage fit reason: ${result.stageFitReason}`] : []),
7214
+ ...(option.when_to_choose ? [`When to choose: ${option.when_to_choose}`] : []),
7215
+ "",
7216
+ "## 1) Scope and Boundaries",
7217
+ "In scope:",
7218
+ ...(scopeInclude.length > 0 ? scopeInclude.map((item) => `- ${item}`) : ["- Deliver selected option with minimal blast radius."]),
7219
+ "Out of scope:",
7220
+ ...(scopeExclude.length > 0 ? scopeExclude.map((item) => `- ${item}`) : ["- Broad refactors outside touched modules."]),
7221
+ "Hard constraints:",
7222
+ ...(constraints.length > 0 ? constraints.map((item) => `- ${item}`) : ["- Keep backward compatibility for existing behavior."]),
7223
+ "",
7224
+ "## 2) Implementation Phases",
7225
+ "Phase A - Preparation",
7226
+ "1. Confirm acceptance criteria and edge cases for the selected option.",
7227
+ "2. Lock touched modules and define rollback strategy before coding.",
7228
+ "Phase B - Implementation",
7229
+ ...planSteps.map((step, index) => `${index + 1}. ${step}`),
7230
+ "Phase C - Hardening",
7231
+ "1. Run targeted regression checks on impacted flows.",
7232
+ "2. Address review findings and update docs if behavior changed.",
7233
+ "",
7234
+ "## 3) Priority Files",
7235
+ ];
7236
+ lines.push(...(files.length > 0 ? files.map((filePath) => `- ${filePath}`) : ["- Determine target files during code scan."]));
7237
+ 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.");
7238
+ return lines.join("\n");
7239
+ }
7240
+ resolveRecommendedSingularOptionId(result) {
7241
+ if (result.recommendation === "defer") {
7242
+ return "3";
7243
+ }
7244
+ if (result.recommendation === "implement_incrementally") {
7245
+ const incremental = result.options.find((option) => /increment|mvp|phased/i.test(`${option.title} ${option.summary}`));
7246
+ return incremental?.id ?? "1";
7247
+ }
7248
+ const nonDefer = result.options.find((option) => !/defer|later|postpone/i.test(`${option.title} ${option.summary}`));
7249
+ return nonDefer?.id ?? "1";
7250
+ }
7251
+ async promptSingularDecision(result) {
7252
+ const recommendedId = this.resolveRecommendedSingularOptionId(result);
7253
+ const options = result.options.map((option) => {
7254
+ const recommendedSuffix = option.id === recommendedId ? " (Recommended)" : "";
7255
+ return `Option ${option.id}${recommendedSuffix}: ${option.title} [risk=${option.blast_radius}]`;
7256
+ });
7257
+ options.push("Close without decision");
7258
+ const selected = await this.showExtensionSelector("/singular: choose next step", options);
7259
+ if (!selected || selected === "Close without decision")
7260
+ return;
7261
+ const match = selected.match(/^Option\s+(\d+)/);
7262
+ if (!match)
7263
+ return;
7264
+ const picked = result.options.find((option) => option.id === match[1]);
7265
+ if (!picked)
7266
+ return;
7267
+ this.showCommandTextBlock("Singular Decision", [
7268
+ `selected: ${picked.id}. ${picked.title}`,
7269
+ `summary: ${picked.summary}`,
7270
+ `complexity: ${picked.complexity}`,
7271
+ `blast_radius: ${picked.blast_radius}`,
7272
+ ...(picked.when_to_choose ? [`when_to_choose: ${picked.when_to_choose}`] : []),
7273
+ "",
7274
+ "plan:",
7275
+ ...picked.plan.map((step, index) => `${index + 1}. ${step}`),
7276
+ "",
7277
+ "pros:",
7278
+ ...picked.pros.map((item) => `- ${item}`),
7279
+ "",
7280
+ "cons:",
7281
+ ...picked.cons.map((item) => `- ${item}`),
7282
+ ].join("\n"));
7283
+ if (picked.id === "3" || result.recommendation === "defer") {
7284
+ this.showStatus("Singular: defer option selected, implementation postponed.");
7285
+ return;
7286
+ }
7287
+ this.editor.setText(this.buildSingularExecutionDraft(result, picked, this.singularLastEffectiveContract));
7288
+ this.showStatus("Singular: detailed execution draft generated in editor.");
7289
+ }
7290
+ showSingularLastSummary() {
7291
+ const last = this.singularService.getLastRun();
7292
+ if (!last) {
7293
+ this.showWarning("No /singular analyses found yet.");
7294
+ return;
7295
+ }
7296
+ this.showCommandTextBlock("Singular Last Run", [
7297
+ `run_id: ${last.runId}`,
7298
+ `generated_at: ${last.generatedAt ?? "unknown"}`,
7299
+ `recommendation: ${last.recommendation ?? "unknown"}`,
7300
+ `request: ${last.request ?? "unknown"}`,
7301
+ `analysis_path: ${last.analysisPath}`,
7302
+ ...(last.metaPath ? [`meta_path: ${last.metaPath}`] : []),
7303
+ ].join("\n"));
7304
+ }
7305
+ async runSingularAnalysis(request) {
7306
+ let effectiveContract = {};
7307
+ try {
7308
+ effectiveContract = this.contractService.getState().effective;
7309
+ }
7310
+ catch (error) {
7311
+ const message = error instanceof Error ? error.message : String(error);
7312
+ this.showWarning(`Contract unavailable, continuing /singular without contract overlay: ${message}`);
7313
+ }
7314
+ this.singularLastEffectiveContract = effectiveContract;
7315
+ try {
7316
+ this.showStatus("Singular: preparing baseline scan...");
7317
+ const baseline = await this.singularService.analyze({
7318
+ request,
7319
+ autosave: false,
7320
+ contract: effectiveContract,
7321
+ });
7322
+ let result = baseline;
7323
+ if (!this.session.model) {
7324
+ this.showWarning("No model selected. /singular used heuristic analysis only. Use /model to enable agent feasibility pass.");
7325
+ }
7326
+ else {
7327
+ try {
7328
+ const enriched = await this.runSingularAgentFeasibilityPass(request, baseline, effectiveContract);
7329
+ if (enriched) {
7330
+ result = enriched;
7331
+ }
7332
+ else {
7333
+ this.showWarning("Agent feasibility pass returned incomplete output. Showing heuristic fallback.");
7334
+ }
7335
+ }
7336
+ catch (error) {
7337
+ const message = error instanceof Error ? error.message : String(error);
7338
+ this.showWarning(`Agent feasibility pass failed. Showing heuristic fallback: ${message}`);
7339
+ }
7340
+ }
7341
+ this.singularService.saveAnalysis(result);
7342
+ this.showStatus(`Singular analysis complete: ${result.runId}`);
7343
+ this.showCommandTextBlock("Singular Analysis", this.formatSingularRunReport(result));
7344
+ await this.promptSingularDecision(result);
7345
+ }
7346
+ catch (error) {
7347
+ const message = error instanceof Error ? error.message : String(error);
7348
+ this.showError(`Singular analysis failed: ${message}`);
7349
+ }
7350
+ }
7351
+ async handleSingularCommand(text) {
7352
+ const args = this.parseSlashArgs(text).slice(1);
7353
+ if (args.length === 0) {
7354
+ this.showWarning("Usage: /singular <feature request>");
7355
+ return;
7356
+ }
7357
+ const subcommand = (args[0] ?? "").toLowerCase();
7358
+ if (subcommand === "help" || subcommand === "-h" || subcommand === "--help") {
7359
+ this.showCommandTextBlock("Singular Help", [
7360
+ "Usage:",
7361
+ " /singular <feature request>",
7362
+ " /singular last",
7363
+ " /singular help",
7364
+ "",
7365
+ "Examples:",
7366
+ " /singular add account dashboard",
7367
+ " /singular introduce RBAC for API",
7368
+ ].join("\n"));
7369
+ return;
7370
+ }
7371
+ if (subcommand === "last" && args.length === 1) {
7372
+ this.showSingularLastSummary();
7373
+ return;
7374
+ }
7375
+ const request = args.join(" ").trim();
7376
+ if (!request) {
7377
+ this.showWarning("Usage: /singular <feature request>");
7378
+ return;
7379
+ }
7380
+ await this.runSingularAnalysis(request);
7381
+ }
7382
+ parseCheckpointNameFromLabel(label) {
7383
+ if (!label)
7384
+ return undefined;
7385
+ if (!label.startsWith(CHECKPOINT_LABEL_PREFIX))
7386
+ return undefined;
7387
+ const name = label.slice(CHECKPOINT_LABEL_PREFIX.length).trim();
7388
+ return name.length > 0 ? name : undefined;
7389
+ }
7390
+ buildCheckpointLabel(name) {
7391
+ return `${CHECKPOINT_LABEL_PREFIX}${name}`;
7392
+ }
7393
+ normalizeCheckpointName(raw) {
7394
+ const normalized = raw.replace(/\s+/g, " ").trim();
7395
+ if (!normalized)
7396
+ return undefined;
7397
+ if (normalized.length > 80)
7398
+ return undefined;
7399
+ return normalized;
7400
+ }
7401
+ getSessionCheckpoints() {
7402
+ const active = new Map();
7403
+ for (const entry of this.sessionManager.getEntries()) {
7404
+ if (entry.type !== "label")
7405
+ continue;
7406
+ const name = this.parseCheckpointNameFromLabel(entry.label);
7407
+ if (!name) {
7408
+ active.delete(entry.targetId);
7409
+ continue;
7410
+ }
7411
+ active.set(entry.targetId, {
7412
+ name,
7413
+ targetId: entry.targetId,
7414
+ labelEntryId: entry.id,
7415
+ timestamp: entry.timestamp,
7416
+ });
7417
+ }
7418
+ return [...active.values()].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
7419
+ }
7420
+ buildDefaultCheckpointName(checkpoints) {
7421
+ const used = new Set(checkpoints.map((checkpoint) => checkpoint.name.toLowerCase()));
7422
+ let index = 1;
7423
+ while (used.has(`cp-${index}`)) {
7424
+ index += 1;
7425
+ }
7426
+ return `cp-${index}`;
7427
+ }
7428
+ formatCheckpointList(checkpoints) {
7429
+ if (checkpoints.length === 0) {
7430
+ return "No checkpoints yet.\nCreate one with: /checkpoint [name]";
7431
+ }
7432
+ const newestFirst = [...checkpoints].reverse();
7433
+ const lines = newestFirst.map((checkpoint, index) => {
7434
+ const target = this.sessionManager.getEntry(checkpoint.targetId);
7435
+ const type = target?.type ?? "missing";
7436
+ return `${index + 1}. ${checkpoint.name} -> ${checkpoint.targetId} (${type}) @ ${checkpoint.timestamp}`;
7437
+ });
7438
+ lines.push("");
7439
+ lines.push("Usage: /rollback [name|index]");
7440
+ return lines.join("\n");
7441
+ }
7442
+ formatCheckpointOption(index, checkpoint) {
7443
+ const target = this.sessionManager.getEntry(checkpoint.targetId);
7444
+ const type = target?.type ?? "missing";
7445
+ return `${index}. ${checkpoint.name} -> ${checkpoint.targetId} (${type}) @ ${checkpoint.timestamp}`;
7446
+ }
7447
+ handleCheckpointCommand(text) {
7448
+ const args = this.parseSlashArgs(text).slice(1);
7449
+ const subcommand = args[0]?.toLowerCase();
7450
+ const checkpoints = this.getSessionCheckpoints();
7451
+ if (subcommand === "list" || subcommand === "ls") {
7452
+ this.showCommandTextBlock("Checkpoints", this.formatCheckpointList(checkpoints));
7453
+ return;
7454
+ }
7455
+ if (subcommand === "help" || subcommand === "-h" || subcommand === "--help") {
7456
+ this.showCommandTextBlock("Checkpoint Help", ["Usage:", " /checkpoint [name]", " /checkpoint list", "", "Examples:", " /checkpoint", " /checkpoint before-refactor"].join("\n"));
7457
+ return;
7458
+ }
7459
+ const leafId = this.sessionManager.getLeafId();
7460
+ if (!leafId) {
7461
+ this.showWarning("Cannot create checkpoint yet (session has no entries).");
7462
+ return;
7463
+ }
7464
+ const requestedName = args.join(" ");
7465
+ const name = requestedName ? this.normalizeCheckpointName(requestedName) : this.buildDefaultCheckpointName(checkpoints);
7466
+ if (!name) {
7467
+ this.showWarning("Invalid checkpoint name. Use 1-80 visible characters.");
7468
+ return;
7469
+ }
7470
+ this.sessionManager.appendLabelChange(leafId, this.buildCheckpointLabel(name));
7471
+ this.showStatus(`Checkpoint saved: ${name} (${leafId})`);
7472
+ }
7473
+ async handleRollbackCommand(text) {
7474
+ const args = this.parseSlashArgs(text).slice(1);
7475
+ const subcommand = args[0]?.toLowerCase();
7476
+ const checkpoints = this.getSessionCheckpoints();
7477
+ if (subcommand === "list" || subcommand === "ls") {
7478
+ this.showCommandTextBlock("Checkpoints", this.formatCheckpointList(checkpoints));
7479
+ return;
5600
7480
  }
5601
7481
  if (subcommand === "help" || subcommand === "-h" || subcommand === "--help") {
5602
7482
  this.showCommandTextBlock("Rollback Help", [
@@ -5680,6 +7560,8 @@ export class InteractiveMode {
5680
7560
  const hasModelIssues = checks.some((check) => (check.label === "Active model" || check.label === "Active model auth" || check.label === "Available models") &&
5681
7561
  check.level === "fail");
5682
7562
  const hasMcpIssues = checks.some((check) => check.label === "MCP servers" && check.level !== "ok");
7563
+ const hasSemanticIssues = checks.some((check) => check.label === "Semantic index" && check.level !== "ok");
7564
+ const hasContractIssues = checks.some((check) => check.label === "Contract state" && check.level !== "ok");
5683
7565
  const hasResourceIssues = checks.some((check) => check.label === "Resources" && check.level !== "ok");
5684
7566
  while (true) {
5685
7567
  const options = [];
@@ -5693,6 +7575,12 @@ export class InteractiveMode {
5693
7575
  }
5694
7576
  options.push("Refresh MCP runtime");
5695
7577
  }
7578
+ if (hasSemanticIssues) {
7579
+ options.push("Open semantic manager");
7580
+ }
7581
+ if (hasContractIssues) {
7582
+ options.push("Open contract manager");
7583
+ }
5696
7584
  if (this.permissionMode === "yolo") {
5697
7585
  options.push("Set permissions mode to ask");
5698
7586
  }
@@ -5723,6 +7611,14 @@ export class InteractiveMode {
5723
7611
  this.showStatus("MCP servers refreshed");
5724
7612
  continue;
5725
7613
  }
7614
+ if (selected === "Open semantic manager") {
7615
+ await this.handleSemanticCommand("/semantic");
7616
+ return;
7617
+ }
7618
+ if (selected === "Open contract manager") {
7619
+ await this.handleContractCommand("/contract");
7620
+ return;
7621
+ }
5726
7622
  if (selected === "Set permissions mode to ask") {
5727
7623
  this.permissionMode = "ask";
5728
7624
  this.settingsManager.setPermissionMode("ask");
@@ -5734,7 +7630,17 @@ export class InteractiveMode {
5734
7630
  return;
5735
7631
  }
5736
7632
  if (selected === "Show auth/models paths") {
5737
- this.showCommandTextBlock("Runtime Paths", [`auth.json: ${getAuthPath()}`, `models.json: ${getModelsPath()}`].join("\n"));
7633
+ const cwd = this.sessionManager.getCwd();
7634
+ const agentDir = getAgentDir();
7635
+ this.showCommandTextBlock("Runtime Paths", [
7636
+ `auth.json: ${getAuthPath()}`,
7637
+ `models.json: ${getModelsPath()}`,
7638
+ `contract(project): ${this.contractService.getProjectPath()}`,
7639
+ `singular(analyses): ${this.singularService.getAnalysesRoot()}`,
7640
+ `semantic(user): ${getSemanticConfigPath("user", cwd, agentDir)}`,
7641
+ `semantic(project): ${getSemanticConfigPath("project", cwd, agentDir)}`,
7642
+ `semantic(index): ${getSemanticIndexDir(cwd, agentDir)}`,
7643
+ ].join("\n"));
5738
7644
  continue;
5739
7645
  }
5740
7646
  }
@@ -5761,6 +7667,22 @@ export class InteractiveMode {
5761
7667
  const mcpDisabled = mcpStatuses.filter((status) => !status.enabled).length;
5762
7668
  const cliToolStatuses = resolveDoctorCliToolStatuses();
5763
7669
  const missingCliTools = cliToolStatuses.filter((status) => !status.available).map((status) => status.tool);
7670
+ let semanticStatus;
7671
+ let semanticStatusError;
7672
+ try {
7673
+ semanticStatus = await this.createSemanticRuntime().status();
7674
+ }
7675
+ catch (error) {
7676
+ semanticStatusError = error instanceof Error ? error.message : String(error);
7677
+ }
7678
+ let contractState;
7679
+ let contractStateError;
7680
+ try {
7681
+ contractState = this.contractService.getState();
7682
+ }
7683
+ catch (error) {
7684
+ contractStateError = error instanceof Error ? error.message : String(error);
7685
+ }
5764
7686
  const checks = [];
5765
7687
  const addCheck = (level, label, detail, fix) => checks.push({ level, label, detail, fix });
5766
7688
  if (!model) {
@@ -5802,6 +7724,44 @@ export class InteractiveMode {
5802
7724
  else {
5803
7725
  addCheck("ok", "MCP servers", `${mcpConnected} connected, ${mcpDisabled} disabled`);
5804
7726
  }
7727
+ if (semanticStatusError) {
7728
+ addCheck("fail", "Semantic index", semanticStatusError, "Run /semantic setup and retry /semantic status.");
7729
+ }
7730
+ else if (!semanticStatus) {
7731
+ addCheck("warn", "Semantic index", "Status unavailable", "Run /semantic setup.");
7732
+ }
7733
+ else if (!semanticStatus.configured) {
7734
+ addCheck("warn", "Semantic index", "Not configured", `Run /semantic setup (user: ${semanticStatus.configPathUser} or project: ${semanticStatus.configPathProject}).`);
7735
+ }
7736
+ else if (!semanticStatus.enabled) {
7737
+ addCheck("warn", "Semantic index", "Configured but disabled", "Enable semanticSearch.enabled or rerun /semantic setup.");
7738
+ }
7739
+ else if (!semanticStatus.indexed) {
7740
+ addCheck("warn", "Semantic index", "Configured but index is missing", "Run /semantic index.");
7741
+ }
7742
+ else if (semanticStatus.stale) {
7743
+ const requiresRebuild = semanticStatus.staleReason === "provider_changed" ||
7744
+ semanticStatus.staleReason === "chunking_changed" ||
7745
+ semanticStatus.staleReason === "index_filters_changed" ||
7746
+ semanticStatus.staleReason === "dimension_mismatch";
7747
+ addCheck("warn", "Semantic index", `Indexed but stale${semanticStatus.staleReason ? ` (${semanticStatus.staleReason})` : ""}`, requiresRebuild ? "Run /semantic rebuild." : "Run /semantic index.");
7748
+ }
7749
+ else {
7750
+ addCheck("ok", "Semantic index", `${semanticStatus.provider}/${semanticStatus.model} · auto_index=${semanticStatus.autoIndex ? "on" : "off"} · files=${semanticStatus.files} chunks=${semanticStatus.chunks}`);
7751
+ }
7752
+ if (contractStateError) {
7753
+ addCheck("fail", "Contract state", contractStateError, "Fix .iosm/contract.json or run /contract clear --scope project.");
7754
+ }
7755
+ else if (!contractState) {
7756
+ addCheck("warn", "Contract state", "Unavailable", "Run /contract show to inspect state.");
7757
+ }
7758
+ else if (!contractState.hasProjectFile && Object.keys(contractState.sessionOverlay).length === 0) {
7759
+ addCheck("warn", "Contract state", "No project/session contract active", "Run /contract to define constraints and quality gates.");
7760
+ }
7761
+ else {
7762
+ addCheck("ok", "Contract state", `project=${contractState.hasProjectFile ? "yes" : "no"} session_keys=${Object.keys(contractState.sessionOverlay).length} effective_keys=${Object.keys(contractState.effective).length}`);
7763
+ }
7764
+ addCheck("ok", "Singular analyzer", "Available via /singular <request>");
5805
7765
  if (missingCliTools.length > 0) {
5806
7766
  addCheck("warn", "CLI toolchain", `${cliToolStatuses.length - missingCliTools.length}/${cliToolStatuses.length} available (missing: ${missingCliTools.join(", ")})`, `Install missing CLI tools: ${missingCliTools.join(", ")}.`);
5807
7767
  }
@@ -6265,6 +8225,7 @@ export class InteractiveMode {
6265
8225
  }
6266
8226
  createIosmVerificationEventBridge(options) {
6267
8227
  const loaderMessage = options?.loaderMessage ?? `Verifying workspace... (${appKey(this.keybindings, "interrupt")} to interrupt)`;
8228
+ const hideAssistantText = options?.hideAssistantText === true;
6268
8229
  let verifyStreamingComponent;
6269
8230
  let verifyStreamingMessage;
6270
8231
  const verifyPendingTools = new Map();
@@ -6279,7 +8240,7 @@ export class InteractiveMode {
6279
8240
  this.statusContainer.addChild(this.loadingAnimation);
6280
8241
  break;
6281
8242
  case "message_start":
6282
- if (event.message.role === "assistant") {
8243
+ if (event.message.role === "assistant" && !hideAssistantText) {
6283
8244
  verifyStreamingComponent = new AssistantMessageComponent(undefined, false, this.getMarkdownThemeWithSettings());
6284
8245
  verifyStreamingMessage = event.message;
6285
8246
  this.chatContainer.addChild(verifyStreamingComponent);
@@ -8598,6 +10559,7 @@ The agent will automatically receive IOSM context on every turn.`;
8598
10559
  this.ui.requestRender();
8599
10560
  }
8600
10561
  async handleClearCommand() {
10562
+ this.contractService.clear("session");
8601
10563
  // Stop loading animation
8602
10564
  if (this.loadingAnimation) {
8603
10565
  this.loadingAnimation.stop();
@@ -8613,6 +10575,7 @@ The agent will automatically receive IOSM context on every turn.`;
8613
10575
  this.streamingComponent = undefined;
8614
10576
  this.streamingMessage = undefined;
8615
10577
  this.pendingTools.clear();
10578
+ this.syncRuntimePromptSuffix();
8616
10579
  this.chatContainer.addChild(new Spacer(1));
8617
10580
  this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
8618
10581
  this.refreshBuiltInHeader();