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.
- package/CHANGELOG.md +33 -0
- package/README.md +37 -3
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +4 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-profiles.d.ts.map +1 -1
- package/dist/core/agent-profiles.js +1 -0
- package/dist/core/agent-profiles.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +3 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/blast.d.ts +62 -0
- package/dist/core/blast.d.ts.map +1 -0
- package/dist/core/blast.js +448 -0
- package/dist/core/blast.js.map +1 -0
- package/dist/core/contract.d.ts +54 -0
- package/dist/core/contract.d.ts.map +1 -0
- package/dist/core/contract.js +300 -0
- package/dist/core/contract.js.map +1 -0
- package/dist/core/sdk.d.ts +3 -3
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +11 -19
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/semantic/chunking.d.ts +10 -0
- package/dist/core/semantic/chunking.d.ts.map +1 -0
- package/dist/core/semantic/chunking.js +82 -0
- package/dist/core/semantic/chunking.js.map +1 -0
- package/dist/core/semantic/cli.d.ts +23 -0
- package/dist/core/semantic/cli.d.ts.map +1 -0
- package/dist/core/semantic/cli.js +86 -0
- package/dist/core/semantic/cli.js.map +1 -0
- package/dist/core/semantic/config.d.ts +8 -0
- package/dist/core/semantic/config.d.ts.map +1 -0
- package/dist/core/semantic/config.js +266 -0
- package/dist/core/semantic/config.js.map +1 -0
- package/dist/core/semantic/index-store.d.ts +21 -0
- package/dist/core/semantic/index-store.d.ts.map +1 -0
- package/dist/core/semantic/index-store.js +73 -0
- package/dist/core/semantic/index-store.js.map +1 -0
- package/dist/core/semantic/index.d.ts +8 -0
- package/dist/core/semantic/index.d.ts.map +1 -0
- package/dist/core/semantic/index.js +7 -0
- package/dist/core/semantic/index.js.map +1 -0
- package/dist/core/semantic/providers.d.ts +22 -0
- package/dist/core/semantic/providers.d.ts.map +1 -0
- package/dist/core/semantic/providers.js +317 -0
- package/dist/core/semantic/providers.js.map +1 -0
- package/dist/core/semantic/runtime.d.ts +32 -0
- package/dist/core/semantic/runtime.d.ts.map +1 -0
- package/dist/core/semantic/runtime.js +510 -0
- package/dist/core/semantic/runtime.js.map +1 -0
- package/dist/core/semantic/types.d.ts +157 -0
- package/dist/core/semantic/types.d.ts.map +1 -0
- package/dist/core/semantic/types.js +23 -0
- package/dist/core/semantic/types.js.map +1 -0
- package/dist/core/shadow-guard.d.ts +30 -0
- package/dist/core/shadow-guard.d.ts.map +1 -0
- package/dist/core/shadow-guard.js +81 -0
- package/dist/core/shadow-guard.js.map +1 -0
- package/dist/core/singular.d.ts +73 -0
- package/dist/core/singular.d.ts.map +1 -0
- package/dist/core/singular.js +413 -0
- package/dist/core/singular.js.map +1 -0
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +12 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +21 -3
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/ast-grep.js +1 -1
- package/dist/core/tools/ast-grep.js.map +1 -1
- package/dist/core/tools/comby.js +1 -1
- package/dist/core/tools/comby.js.map +1 -1
- package/dist/core/tools/index.d.ts +9 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +6 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/rg.js +1 -1
- package/dist/core/tools/rg.js.map +1 -1
- package/dist/core/tools/semantic-search.d.ts +21 -0
- package/dist/core/tools/semantic-search.d.ts.map +1 -0
- package/dist/core/tools/semantic-search.js +123 -0
- package/dist/core/tools/semantic-search.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +124 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +8 -0
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +70 -1
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +58 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +2067 -104
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/cli-reference.md +36 -1
- package/docs/configuration.md +79 -2
- package/docs/getting-started.md +1 -0
- package/docs/interactive-mode.md +135 -1
- 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
|
-
|
|
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", "
|
|
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(
|
|
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
|
|
3496
|
-
|
|
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.
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
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
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
const
|
|
5527
|
-
if (
|
|
5528
|
-
|
|
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
|
-
|
|
5532
|
-
name,
|
|
5533
|
-
targetId: entry.targetId,
|
|
5534
|
-
labelEntryId: entry.id,
|
|
5535
|
-
timestamp: entry.timestamp,
|
|
5536
|
-
});
|
|
5818
|
+
rest.push(token);
|
|
5537
5819
|
}
|
|
5538
|
-
return
|
|
5820
|
+
return { scope, rest };
|
|
5539
5821
|
}
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
index
|
|
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
|
|
5843
|
+
return { topK, rest };
|
|
5547
5844
|
}
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
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
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
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
|
-
|
|
5568
|
-
const
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
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
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
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
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
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
|
-
|
|
5585
|
-
|
|
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.
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
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
|
-
|
|
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();
|