iosm-cli 0.1.3 → 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 +27 -0
- package/README.md +33 -2
- 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/semantic/config.d.ts.map +1 -1
- package/dist/core/semantic/config.js +5 -0
- package/dist/core/semantic/config.js.map +1 -1
- package/dist/core/semantic/index.d.ts +1 -1
- package/dist/core/semantic/index.d.ts.map +1 -1
- package/dist/core/semantic/index.js +1 -1
- package/dist/core/semantic/index.js.map +1 -1
- package/dist/core/semantic/runtime.d.ts.map +1 -1
- package/dist/core/semantic/runtime.js +12 -1
- package/dist/core/semantic/runtime.js.map +1 -1
- package/dist/core/semantic/types.d.ts +6 -0
- package/dist/core/semantic/types.d.ts.map +1 -1
- package/dist/core/semantic/types.js +6 -0
- package/dist/core/semantic/types.js.map +1 -1
- package/dist/core/shadow-guard.d.ts +30 -0
- package/dist/core/shadow-guard.d.ts.map +1 -0
- package/dist/core/shadow-guard.js +81 -0
- package/dist/core/shadow-guard.js.map +1 -0
- package/dist/core/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 +9 -1
- 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 +3 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/semantic-search.d.ts.map +1 -1
- package/dist/core/tools/semantic-search.js +1 -0
- package/dist/core/tools/semantic-search.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +8 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +8 -0
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +70 -1
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +43 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +1304 -24
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/cli-reference.md +19 -1
- package/docs/configuration.md +5 -0
- package/docs/interactive-mode.md +131 -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,7 +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 {
|
|
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";
|
|
23
25
|
import { DefaultResourceLoader } from "../../core/resource-loader.js";
|
|
24
26
|
import { createAgentSession } from "../../core/sdk.js";
|
|
25
27
|
import { createTeamRun, getTeamRun, listTeamRuns } from "../../core/agent-teams.js";
|
|
@@ -71,6 +73,94 @@ function isExpandable(obj) {
|
|
|
71
73
|
}
|
|
72
74
|
const IOSM_PROFILE_ONLY_COMMANDS = new Set(["iosm", "cycle-list", "cycle-plan", "cycle-status", "cycle-report"]);
|
|
73
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
|
+
];
|
|
74
164
|
const DOCTOR_CLI_TOOL_SPECS = [
|
|
75
165
|
{
|
|
76
166
|
tool: "rg",
|
|
@@ -265,11 +355,13 @@ export class InteractiveMode {
|
|
|
265
355
|
this.pendingWorkingMessage = undefined;
|
|
266
356
|
this.defaultWorkingMessage = "Working...";
|
|
267
357
|
this.activeProfileName = "full";
|
|
358
|
+
this.profilePromptSuffix = undefined;
|
|
268
359
|
this.permissionMode = "ask";
|
|
269
360
|
this.permissionAllowRules = [];
|
|
270
361
|
this.permissionDenyRules = [];
|
|
271
362
|
this.permissionPromptLock = Promise.resolve();
|
|
272
363
|
this.sessionAllowedToolSignatures = new Set();
|
|
364
|
+
this.singularLastEffectiveContract = {};
|
|
273
365
|
this.lastSigintTime = 0;
|
|
274
366
|
this.lastEscapeTime = 0;
|
|
275
367
|
this.changelogMarkdown = undefined;
|
|
@@ -310,6 +402,7 @@ export class InteractiveMode {
|
|
|
310
402
|
// IOSM automation state
|
|
311
403
|
this.iosmAutomationRun = undefined;
|
|
312
404
|
this.iosmVerificationSession = undefined;
|
|
405
|
+
this.singularAnalysisSession = undefined;
|
|
313
406
|
// Extension UI state
|
|
314
407
|
this.extensionSelector = undefined;
|
|
315
408
|
this.extensionInput = undefined;
|
|
@@ -354,14 +447,19 @@ export class InteractiveMode {
|
|
|
354
447
|
this.footerDataProvider = new FooterDataProvider();
|
|
355
448
|
this.footer = new FooterComponent(session, this.footerDataProvider);
|
|
356
449
|
this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
|
|
357
|
-
|
|
450
|
+
const profile = getAgentProfile(options.profile);
|
|
451
|
+
this.activeProfileName = profile.name;
|
|
452
|
+
this.profilePromptSuffix = profile.systemPromptAppend || undefined;
|
|
358
453
|
this.mcpRuntime = options.mcpRuntime;
|
|
454
|
+
this.contractService = new ContractService({ cwd: this.sessionManager.getCwd() });
|
|
455
|
+
this.singularService = new SingularService({ cwd: this.sessionManager.getCwd() });
|
|
359
456
|
// Apply plan mode and profile badges immediately if set
|
|
360
457
|
if (options.planMode || this.activeProfileName === "plan") {
|
|
361
458
|
this.footer.setPlanMode(true);
|
|
362
459
|
}
|
|
363
460
|
this.footer.setActiveProfile(this.activeProfileName);
|
|
364
461
|
this.session.setIosmAutopilotEnabled(this.activeProfileName === "iosm");
|
|
462
|
+
this.syncRuntimePromptSuffix();
|
|
365
463
|
this.permissionMode = this.settingsManager.getPermissionMode();
|
|
366
464
|
this.permissionAllowRules = this.settingsManager.getPermissionAllowRules();
|
|
367
465
|
this.permissionDenyRules = this.settingsManager.getPermissionDenyRules();
|
|
@@ -373,6 +471,44 @@ export class InteractiveMode {
|
|
|
373
471
|
setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
|
|
374
472
|
initTheme(this.settingsManager.getTheme(), true);
|
|
375
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
|
+
}
|
|
376
512
|
getToolPermissionSignature(request) {
|
|
377
513
|
const summary = request.summary.trim().replace(/\s+/g, " ");
|
|
378
514
|
return `${request.toolName}:${summary}`;
|
|
@@ -834,6 +970,46 @@ export class InteractiveMode {
|
|
|
834
970
|
}
|
|
835
971
|
return null;
|
|
836
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
|
+
}
|
|
837
1013
|
setupAutocomplete(fdPath) {
|
|
838
1014
|
// Define commands for autocomplete
|
|
839
1015
|
const builtinCommands = BUILTIN_SLASH_COMMANDS.filter((command) => this.activeProfileName === "iosm" || !IOSM_PROFILE_ONLY_COMMANDS.has(command.name));
|
|
@@ -879,6 +1055,14 @@ export class InteractiveMode {
|
|
|
879
1055
|
if (semanticCommand) {
|
|
880
1056
|
semanticCommand.getArgumentCompletions = (prefix) => this.getSemanticArgumentCompletions(prefix);
|
|
881
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
|
+
}
|
|
882
1066
|
// Convert prompt templates to SlashCommand format for autocomplete
|
|
883
1067
|
const templateCommands = this.session.promptTemplates.map((cmd) => ({
|
|
884
1068
|
name: cmd.name,
|
|
@@ -1132,7 +1316,7 @@ export class InteractiveMode {
|
|
|
1132
1316
|
}
|
|
1133
1317
|
// Commands and keys
|
|
1134
1318
|
const commandsLine = `${pad}${theme.fg("dim", "cmds")} ` +
|
|
1135
|
-
["model", "login", "
|
|
1319
|
+
["model", "login", "contract", "singular", "semantic", "memory", "new"]
|
|
1136
1320
|
.map((c) => theme.fg("accent", `/${c}`))
|
|
1137
1321
|
.join(theme.fg("dim", " "));
|
|
1138
1322
|
const keysLine = `${pad}${theme.fg("dim", "keys")} ` +
|
|
@@ -1562,8 +1746,10 @@ export class InteractiveMode {
|
|
|
1562
1746
|
const names = extensionPaths.map((p) => theme.fg("dim", this.formatDisplayPath(p)));
|
|
1563
1747
|
lines.push(` ${theme.fg("muted", "ext")} ${names.join(theme.fg("dim", ", "))}`);
|
|
1564
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);
|
|
1565
1751
|
this.chatContainer.addChild(new Spacer(1));
|
|
1566
|
-
this.chatContainer.addChild(new Text(
|
|
1752
|
+
this.chatContainer.addChild(new Text(safeLines.join("\n"), 0, 0));
|
|
1567
1753
|
}
|
|
1568
1754
|
}
|
|
1569
1755
|
if (showDiagnostics) {
|
|
@@ -2287,6 +2473,7 @@ export class InteractiveMode {
|
|
|
2287
2473
|
this.session.isRetrying ||
|
|
2288
2474
|
this.iosmAutomationRun !== undefined ||
|
|
2289
2475
|
this.iosmVerificationSession !== undefined ||
|
|
2476
|
+
this.singularAnalysisSession !== undefined ||
|
|
2290
2477
|
queuedMessages.steering.length > 0 ||
|
|
2291
2478
|
queuedMessages.followUp.length > 0 ||
|
|
2292
2479
|
queuedMeta.length > 0;
|
|
@@ -2454,6 +2641,16 @@ export class InteractiveMode {
|
|
|
2454
2641
|
await this.handleSemanticCommand(text);
|
|
2455
2642
|
return;
|
|
2456
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
|
+
}
|
|
2457
2654
|
if (text === "/settings") {
|
|
2458
2655
|
this.showSettingsSelector();
|
|
2459
2656
|
this.editor.setText("");
|
|
@@ -3562,19 +3759,11 @@ export class InteractiveMode {
|
|
|
3562
3759
|
}
|
|
3563
3760
|
applyProfile(profileName) {
|
|
3564
3761
|
const profile = getAgentProfile(profileName);
|
|
3565
|
-
const
|
|
3566
|
-
|
|
3567
|
-
if (availableTools.has("task"))
|
|
3568
|
-
nextActiveTools.push("task");
|
|
3569
|
-
if (availableTools.has("todo_write"))
|
|
3570
|
-
nextActiveTools.push("todo_write");
|
|
3571
|
-
if (availableTools.has("todo_read"))
|
|
3572
|
-
nextActiveTools.push("todo_read");
|
|
3573
|
-
if (availableTools.has("ask_user"))
|
|
3574
|
-
nextActiveTools.push("ask_user");
|
|
3575
|
-
this.session.setActiveToolsByName([...new Set(nextActiveTools)]);
|
|
3762
|
+
const nextActiveTools = this.getProfileToolNames(profile.name);
|
|
3763
|
+
this.session.setActiveToolsByName(nextActiveTools);
|
|
3576
3764
|
this.session.setThinkingLevel(profile.thinkingLevel);
|
|
3577
|
-
this.
|
|
3765
|
+
this.profilePromptSuffix = profile.systemPromptAppend || undefined;
|
|
3766
|
+
this.syncRuntimePromptSuffix();
|
|
3578
3767
|
this.session.setIosmAutopilotEnabled(profile.name === "iosm");
|
|
3579
3768
|
this.activeProfileName = profile.name;
|
|
3580
3769
|
this.footer.setActiveProfile(profile.name);
|
|
@@ -3819,6 +4008,8 @@ export class InteractiveMode {
|
|
|
3819
4008
|
const hasAutomationWork = this.iosmAutomationRun !== undefined;
|
|
3820
4009
|
const verificationSession = this.iosmVerificationSession;
|
|
3821
4010
|
const hasVerificationWork = verificationSession !== undefined;
|
|
4011
|
+
const singularSession = this.singularAnalysisSession;
|
|
4012
|
+
const hasSingularWork = singularSession !== undefined;
|
|
3822
4013
|
const hasMainStreaming = this.session.isStreaming;
|
|
3823
4014
|
const hasRetryWork = this.session.isRetrying;
|
|
3824
4015
|
const hasCompactionWork = this.session.isCompacting;
|
|
@@ -3826,6 +4017,7 @@ export class InteractiveMode {
|
|
|
3826
4017
|
if (!hasPendingQueuedMessages &&
|
|
3827
4018
|
!hasAutomationWork &&
|
|
3828
4019
|
!hasVerificationWork &&
|
|
4020
|
+
!hasSingularWork &&
|
|
3829
4021
|
!hasMainStreaming &&
|
|
3830
4022
|
!hasRetryWork &&
|
|
3831
4023
|
!hasCompactionWork &&
|
|
@@ -3858,7 +4050,9 @@ export class InteractiveMode {
|
|
|
3858
4050
|
? "Stopping IOSM automation..."
|
|
3859
4051
|
: hasVerificationWork
|
|
3860
4052
|
? "Stopping IOSM verification..."
|
|
3861
|
-
:
|
|
4053
|
+
: hasSingularWork
|
|
4054
|
+
? "Stopping /singular analysis..."
|
|
4055
|
+
: "Stopping current run...");
|
|
3862
4056
|
const abortPromises = [];
|
|
3863
4057
|
if (hasMainStreaming) {
|
|
3864
4058
|
abortPromises.push(this.session.abort());
|
|
@@ -3866,8 +4060,30 @@ export class InteractiveMode {
|
|
|
3866
4060
|
if (verificationSession) {
|
|
3867
4061
|
abortPromises.push(verificationSession.abort());
|
|
3868
4062
|
}
|
|
4063
|
+
if (singularSession) {
|
|
4064
|
+
abortPromises.push(singularSession.abort());
|
|
4065
|
+
}
|
|
3869
4066
|
if (abortPromises.length > 0) {
|
|
3870
|
-
|
|
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
|
+
}
|
|
3871
4087
|
}
|
|
3872
4088
|
return true;
|
|
3873
4089
|
}
|
|
@@ -5018,6 +5234,7 @@ export class InteractiveMode {
|
|
|
5018
5234
|
});
|
|
5019
5235
|
}
|
|
5020
5236
|
async handleResumeSession(sessionPath) {
|
|
5237
|
+
this.contractService.clear("session");
|
|
5021
5238
|
// Stop loading animation
|
|
5022
5239
|
if (this.loadingAnimation) {
|
|
5023
5240
|
this.loadingAnimation.stop();
|
|
@@ -5035,6 +5252,7 @@ export class InteractiveMode {
|
|
|
5035
5252
|
// Clear and re-render the chat
|
|
5036
5253
|
this.chatContainer.clear();
|
|
5037
5254
|
this.renderInitialMessages();
|
|
5255
|
+
this.syncRuntimePromptSuffix();
|
|
5038
5256
|
this.refreshBuiltInHeader();
|
|
5039
5257
|
this.showStatus("Resumed session");
|
|
5040
5258
|
}
|
|
@@ -5579,7 +5797,7 @@ export class InteractiveMode {
|
|
|
5579
5797
|
authStorage: this.session.modelRegistry.authStorage,
|
|
5580
5798
|
});
|
|
5581
5799
|
}
|
|
5582
|
-
parseSemanticScopeOptions(args) {
|
|
5800
|
+
parseSemanticScopeOptions(args, usage = "Usage: /semantic setup --scope <user|project>") {
|
|
5583
5801
|
let scope;
|
|
5584
5802
|
const rest = [];
|
|
5585
5803
|
for (let index = 0; index < args.length; index++) {
|
|
@@ -5588,7 +5806,7 @@ export class InteractiveMode {
|
|
|
5588
5806
|
if (normalized === "--scope") {
|
|
5589
5807
|
const value = (args[index + 1] ?? "").toLowerCase();
|
|
5590
5808
|
if (!value) {
|
|
5591
|
-
return { scope, rest, error:
|
|
5809
|
+
return { scope, rest, error: usage };
|
|
5592
5810
|
}
|
|
5593
5811
|
if (value !== "user" && value !== "project") {
|
|
5594
5812
|
return { scope, rest, error: `Invalid semantic scope "${value}". Use user or project.` };
|
|
@@ -5628,6 +5846,7 @@ export class InteractiveMode {
|
|
|
5628
5846
|
const lines = [
|
|
5629
5847
|
`configured: ${status.configured ? "yes" : "no"}`,
|
|
5630
5848
|
`enabled: ${status.enabled ? "yes" : "no"}`,
|
|
5849
|
+
`auto_index: ${status.autoIndex ? "on" : "off"}`,
|
|
5631
5850
|
`indexed: ${status.indexed ? "yes" : "no"}`,
|
|
5632
5851
|
`stale: ${status.stale ? `yes${status.staleReason ? ` (${status.staleReason})` : ""}` : "no"}`,
|
|
5633
5852
|
];
|
|
@@ -5912,10 +6131,42 @@ export class InteractiveMode {
|
|
|
5912
6131
|
`scope: ${scope}`,
|
|
5913
6132
|
`provider: ${this.getSemanticSetupProviderLabel(providerType)}`,
|
|
5914
6133
|
`model: ${nextProvider.model}`,
|
|
6134
|
+
`auto_index: ${nextConfig.autoIndex ? "on" : "off"}`,
|
|
5915
6135
|
`config: ${savedPath}`,
|
|
5916
6136
|
`index_dir: ${getSemanticIndexDir(cwd, agentDir)}`,
|
|
5917
6137
|
].join("\n"));
|
|
5918
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
|
+
}
|
|
5919
6170
|
async runSemanticInteractiveMenu() {
|
|
5920
6171
|
while (true) {
|
|
5921
6172
|
let status;
|
|
@@ -5926,7 +6177,7 @@ export class InteractiveMode {
|
|
|
5926
6177
|
this.reportSemanticError(error, "status");
|
|
5927
6178
|
}
|
|
5928
6179
|
const summary = status
|
|
5929
|
-
? `configured=${status.configured ? "yes" : "no"} indexed=${status.indexed ? "yes" : "no"} stale=${status.stale ? "yes" : "no"}`
|
|
6180
|
+
? `configured=${status.configured ? "yes" : "no"} auto_index=${status.autoIndex ? "on" : "off"} indexed=${status.indexed ? "yes" : "no"} stale=${status.stale ? "yes" : "no"}`
|
|
5930
6181
|
: "status unavailable";
|
|
5931
6182
|
const selected = await this.showExtensionSelector(`/semantic manager\n${summary}`, [
|
|
5932
6183
|
"Configure provider/model",
|
|
@@ -5934,6 +6185,7 @@ export class InteractiveMode {
|
|
|
5934
6185
|
"Index now",
|
|
5935
6186
|
"Rebuild index",
|
|
5936
6187
|
"Query index",
|
|
6188
|
+
`Automatic indexing: ${status?.autoIndex ? "on" : "off"}`,
|
|
5937
6189
|
"Show config/index paths",
|
|
5938
6190
|
"Close",
|
|
5939
6191
|
]);
|
|
@@ -6007,6 +6259,10 @@ export class InteractiveMode {
|
|
|
6007
6259
|
}
|
|
6008
6260
|
continue;
|
|
6009
6261
|
}
|
|
6262
|
+
if (selected.startsWith("Automatic indexing:")) {
|
|
6263
|
+
await this.updateSemanticAutoIndexSetting();
|
|
6264
|
+
continue;
|
|
6265
|
+
}
|
|
6010
6266
|
if (selected === "Show config/index paths") {
|
|
6011
6267
|
const runtime = this.createSemanticRuntime();
|
|
6012
6268
|
const cwd = this.sessionManager.getCwd();
|
|
@@ -6036,6 +6292,11 @@ export class InteractiveMode {
|
|
|
6036
6292
|
].join("\n"));
|
|
6037
6293
|
return;
|
|
6038
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
|
+
}
|
|
6039
6300
|
if (error instanceof SemanticRebuildRequiredError) {
|
|
6040
6301
|
this.showWarning(error.message);
|
|
6041
6302
|
this.showWarning("Run /semantic rebuild.");
|
|
@@ -6058,6 +6319,7 @@ export class InteractiveMode {
|
|
|
6058
6319
|
" /semantic",
|
|
6059
6320
|
" /semantic ui",
|
|
6060
6321
|
" /semantic setup [--scope user|project]",
|
|
6322
|
+
" /semantic auto-index [on|off] [--scope user|project]",
|
|
6061
6323
|
" /semantic status",
|
|
6062
6324
|
" /semantic index",
|
|
6063
6325
|
" /semantic rebuild",
|
|
@@ -6079,6 +6341,34 @@ export class InteractiveMode {
|
|
|
6079
6341
|
await this.runSemanticSetupWizard(parsedScope.scope);
|
|
6080
6342
|
return;
|
|
6081
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
|
+
}
|
|
6082
6372
|
if (subcommand === "status") {
|
|
6083
6373
|
try {
|
|
6084
6374
|
const result = await this.runWithExtensionLoader("Checking semantic index status...", async () => this.createSemanticRuntime().status());
|
|
@@ -6133,6 +6423,962 @@ export class InteractiveMode {
|
|
|
6133
6423
|
}
|
|
6134
6424
|
this.showWarning(`Unknown /semantic subcommand "${subcommand}". Use /semantic help.`);
|
|
6135
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
|
+
}
|
|
6136
7382
|
parseCheckpointNameFromLabel(label) {
|
|
6137
7383
|
if (!label)
|
|
6138
7384
|
return undefined;
|
|
@@ -6315,6 +7561,7 @@ export class InteractiveMode {
|
|
|
6315
7561
|
check.level === "fail");
|
|
6316
7562
|
const hasMcpIssues = checks.some((check) => check.label === "MCP servers" && check.level !== "ok");
|
|
6317
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");
|
|
6318
7565
|
const hasResourceIssues = checks.some((check) => check.label === "Resources" && check.level !== "ok");
|
|
6319
7566
|
while (true) {
|
|
6320
7567
|
const options = [];
|
|
@@ -6331,6 +7578,9 @@ export class InteractiveMode {
|
|
|
6331
7578
|
if (hasSemanticIssues) {
|
|
6332
7579
|
options.push("Open semantic manager");
|
|
6333
7580
|
}
|
|
7581
|
+
if (hasContractIssues) {
|
|
7582
|
+
options.push("Open contract manager");
|
|
7583
|
+
}
|
|
6334
7584
|
if (this.permissionMode === "yolo") {
|
|
6335
7585
|
options.push("Set permissions mode to ask");
|
|
6336
7586
|
}
|
|
@@ -6365,6 +7615,10 @@ export class InteractiveMode {
|
|
|
6365
7615
|
await this.handleSemanticCommand("/semantic");
|
|
6366
7616
|
return;
|
|
6367
7617
|
}
|
|
7618
|
+
if (selected === "Open contract manager") {
|
|
7619
|
+
await this.handleContractCommand("/contract");
|
|
7620
|
+
return;
|
|
7621
|
+
}
|
|
6368
7622
|
if (selected === "Set permissions mode to ask") {
|
|
6369
7623
|
this.permissionMode = "ask";
|
|
6370
7624
|
this.settingsManager.setPermissionMode("ask");
|
|
@@ -6381,6 +7635,8 @@ export class InteractiveMode {
|
|
|
6381
7635
|
this.showCommandTextBlock("Runtime Paths", [
|
|
6382
7636
|
`auth.json: ${getAuthPath()}`,
|
|
6383
7637
|
`models.json: ${getModelsPath()}`,
|
|
7638
|
+
`contract(project): ${this.contractService.getProjectPath()}`,
|
|
7639
|
+
`singular(analyses): ${this.singularService.getAnalysesRoot()}`,
|
|
6384
7640
|
`semantic(user): ${getSemanticConfigPath("user", cwd, agentDir)}`,
|
|
6385
7641
|
`semantic(project): ${getSemanticConfigPath("project", cwd, agentDir)}`,
|
|
6386
7642
|
`semantic(index): ${getSemanticIndexDir(cwd, agentDir)}`,
|
|
@@ -6419,6 +7675,14 @@ export class InteractiveMode {
|
|
|
6419
7675
|
catch (error) {
|
|
6420
7676
|
semanticStatusError = error instanceof Error ? error.message : String(error);
|
|
6421
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
|
+
}
|
|
6422
7686
|
const checks = [];
|
|
6423
7687
|
const addCheck = (level, label, detail, fix) => checks.push({ level, label, detail, fix });
|
|
6424
7688
|
if (!model) {
|
|
@@ -6483,8 +7747,21 @@ export class InteractiveMode {
|
|
|
6483
7747
|
addCheck("warn", "Semantic index", `Indexed but stale${semanticStatus.staleReason ? ` (${semanticStatus.staleReason})` : ""}`, requiresRebuild ? "Run /semantic rebuild." : "Run /semantic index.");
|
|
6484
7748
|
}
|
|
6485
7749
|
else {
|
|
6486
|
-
addCheck("ok", "Semantic index", `${semanticStatus.provider}/${semanticStatus.model} · files=${semanticStatus.files} chunks=${semanticStatus.chunks}`);
|
|
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}`);
|
|
6487
7763
|
}
|
|
7764
|
+
addCheck("ok", "Singular analyzer", "Available via /singular <request>");
|
|
6488
7765
|
if (missingCliTools.length > 0) {
|
|
6489
7766
|
addCheck("warn", "CLI toolchain", `${cliToolStatuses.length - missingCliTools.length}/${cliToolStatuses.length} available (missing: ${missingCliTools.join(", ")})`, `Install missing CLI tools: ${missingCliTools.join(", ")}.`);
|
|
6490
7767
|
}
|
|
@@ -6948,6 +8225,7 @@ export class InteractiveMode {
|
|
|
6948
8225
|
}
|
|
6949
8226
|
createIosmVerificationEventBridge(options) {
|
|
6950
8227
|
const loaderMessage = options?.loaderMessage ?? `Verifying workspace... (${appKey(this.keybindings, "interrupt")} to interrupt)`;
|
|
8228
|
+
const hideAssistantText = options?.hideAssistantText === true;
|
|
6951
8229
|
let verifyStreamingComponent;
|
|
6952
8230
|
let verifyStreamingMessage;
|
|
6953
8231
|
const verifyPendingTools = new Map();
|
|
@@ -6962,7 +8240,7 @@ export class InteractiveMode {
|
|
|
6962
8240
|
this.statusContainer.addChild(this.loadingAnimation);
|
|
6963
8241
|
break;
|
|
6964
8242
|
case "message_start":
|
|
6965
|
-
if (event.message.role === "assistant") {
|
|
8243
|
+
if (event.message.role === "assistant" && !hideAssistantText) {
|
|
6966
8244
|
verifyStreamingComponent = new AssistantMessageComponent(undefined, false, this.getMarkdownThemeWithSettings());
|
|
6967
8245
|
verifyStreamingMessage = event.message;
|
|
6968
8246
|
this.chatContainer.addChild(verifyStreamingComponent);
|
|
@@ -9281,6 +10559,7 @@ The agent will automatically receive IOSM context on every turn.`;
|
|
|
9281
10559
|
this.ui.requestRender();
|
|
9282
10560
|
}
|
|
9283
10561
|
async handleClearCommand() {
|
|
10562
|
+
this.contractService.clear("session");
|
|
9284
10563
|
// Stop loading animation
|
|
9285
10564
|
if (this.loadingAnimation) {
|
|
9286
10565
|
this.loadingAnimation.stop();
|
|
@@ -9296,6 +10575,7 @@ The agent will automatically receive IOSM context on every turn.`;
|
|
|
9296
10575
|
this.streamingComponent = undefined;
|
|
9297
10576
|
this.streamingMessage = undefined;
|
|
9298
10577
|
this.pendingTools.clear();
|
|
10578
|
+
this.syncRuntimePromptSuffix();
|
|
9299
10579
|
this.chatContainer.addChild(new Spacer(1));
|
|
9300
10580
|
this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
|
|
9301
10581
|
this.refreshBuiltInHeader();
|