vigthoria-cli 1.10.1 → 1.10.37
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/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +99 -17
- package/dist/commands/chat.d.ts +40 -0
- package/dist/commands/chat.js +1332 -66
- package/dist/commands/config.js +11 -2
- package/dist/commands/legion.js +8 -2
- package/dist/commands/wallet.d.ts +25 -0
- package/dist/commands/wallet.js +191 -0
- package/dist/index.js +158 -2
- package/dist/utils/api.d.ts +90 -2
- package/dist/utils/api.js +1730 -266
- package/dist/utils/brain-hub-client.d.ts +32 -0
- package/dist/utils/brain-hub-client.js +48 -0
- package/dist/utils/codebase-indexer.d.ts +59 -0
- package/dist/utils/codebase-indexer.js +314 -0
- package/dist/utils/config.d.ts +11 -0
- package/dist/utils/config.js +52 -11
- package/dist/utils/persona.d.ts +4 -0
- package/dist/utils/persona.js +34 -0
- package/dist/utils/tools.d.ts +7 -0
- package/dist/utils/tools.js +67 -1
- package/dist/utils/workspace-brain-service.d.ts +43 -0
- package/dist/utils/workspace-brain-service.js +121 -0
- package/dist/utils/workspace-stream.js +56 -15
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/package.json +4 -2
- package/scripts/release/validate-no-go-gates.sh +1 -1
package/dist/commands/chat.js
CHANGED
|
@@ -4,13 +4,15 @@ import * as os from 'os';
|
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import * as readline from 'readline';
|
|
6
6
|
import { createSpinner } from '../utils/logger.js';
|
|
7
|
-
import { APIClient, CLIError, classifyError, formatCLIError, sanitizeUserFacingErrorText, sanitizeUserFacingPathText, propagateError } from '../utils/api.js';
|
|
7
|
+
import { APIClient, CLIError, classifyError, formatCLIError, sanitizeUserFacingErrorText, sanitizeUserFacingPathText, propagateError, VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE } from '../utils/api.js';
|
|
8
8
|
import { AgenticTools, robustifyStreamResponse } from '../utils/tools.js';
|
|
9
9
|
import { SessionManager } from '../utils/session.js';
|
|
10
10
|
import { BridgeClient, getBridgeClient } from '../utils/bridge-client.js';
|
|
11
|
-
import { WorkspaceWatcher } from '../utils/workspace-stream.js';
|
|
11
|
+
import { WorkspaceWatcher, WorkspaceWSClient } from '../utils/workspace-stream.js';
|
|
12
12
|
import { TaskDisplay } from '../utils/task-display.js';
|
|
13
13
|
import { ProjectMemoryService } from '../utils/project-memory.js';
|
|
14
|
+
import { WorkspaceBrainService } from '../utils/workspace-brain-service.js';
|
|
15
|
+
import { buildPersonaOverlay, normalizePersonaMode } from '../utils/persona.js';
|
|
14
16
|
const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
15
17
|
const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
|
|
16
18
|
if (!rawValue) {
|
|
@@ -22,18 +24,18 @@ const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
|
22
24
|
const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
|
|
23
25
|
const rawValue = process.env.VIGTHORIA_AGENT_IDLE_TIMEOUT_MS || process.env.V3_AGENT_IDLE_TIMEOUT_MS;
|
|
24
26
|
if (!rawValue) {
|
|
25
|
-
return
|
|
27
|
+
return 90000;
|
|
26
28
|
}
|
|
27
29
|
const parsed = Number.parseInt(rawValue, 10);
|
|
28
|
-
return Number.isFinite(parsed) && parsed
|
|
30
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 90000;
|
|
29
31
|
})();
|
|
30
32
|
const DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS = (() => {
|
|
31
33
|
const rawValue = process.env.VIGTHORIA_AGENT_SOFT_TIMEOUT_MS || process.env.V3_AGENT_SOFT_TIMEOUT_MS;
|
|
32
34
|
if (!rawValue) {
|
|
33
|
-
return
|
|
35
|
+
return 180000;
|
|
34
36
|
}
|
|
35
37
|
const parsed = Number.parseInt(rawValue, 10);
|
|
36
|
-
return Number.isFinite(parsed) && parsed
|
|
38
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 180000;
|
|
37
39
|
})();
|
|
38
40
|
export class ChatCommand {
|
|
39
41
|
config;
|
|
@@ -43,6 +45,7 @@ export class ChatCommand {
|
|
|
43
45
|
tools = null;
|
|
44
46
|
sessionManager;
|
|
45
47
|
projectMemory = null;
|
|
48
|
+
workspaceBrain = null;
|
|
46
49
|
currentSession = null;
|
|
47
50
|
agentMode = false;
|
|
48
51
|
currentProjectPath = process.cwd();
|
|
@@ -52,6 +55,7 @@ export class ChatCommand {
|
|
|
52
55
|
currentModel = 'code';
|
|
53
56
|
modelExplicitlySelected = false;
|
|
54
57
|
autoApprove = false;
|
|
58
|
+
personaOverride = null;
|
|
55
59
|
// Phase 5: Agent quality gate — track tool usage for evidence thresholds
|
|
56
60
|
agentToolEvidence = { discovery: 0, mutation: 0, searchFailed: 0 };
|
|
57
61
|
operatorMode = false;
|
|
@@ -112,17 +116,22 @@ export class ChatCommand {
|
|
|
112
116
|
toUserFacingApiError(error, context) {
|
|
113
117
|
const classified = classifyError(error);
|
|
114
118
|
const status = classified.statusCode || (this.isJwtExpirationError(error) ? 401 : 500);
|
|
115
|
-
if (this.isJwtExpirationError(error)) {
|
|
119
|
+
if (this.isJwtExpirationError(error) || classified.category === 'auth') {
|
|
116
120
|
return new CLIError('Your Vigthoria session has expired. Run `vigthoria login` to authenticate again.', 'auth', { statusCode: 401 });
|
|
117
121
|
}
|
|
118
|
-
|
|
122
|
+
// Preserve structured API classification first (auth/model/network/etc.)
|
|
123
|
+
// so upstream responses are not relabeled by message heuristics.
|
|
124
|
+
if (classified.category === 'timeout') {
|
|
119
125
|
return new CLIError(`${context} timed out. Check your connection and try again.`, 'timeout', { statusCode: status });
|
|
120
126
|
}
|
|
121
|
-
if (
|
|
127
|
+
if (classified.category === 'network') {
|
|
122
128
|
return new CLIError(`${context} could not reach the Vigthoria API. Check your network connection and try again.`, 'network', { statusCode: status });
|
|
123
129
|
}
|
|
130
|
+
if (classified.category === 'model_backend') {
|
|
131
|
+
return new CLIError(VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE, 'model_backend', { statusCode: status, endpoint: classified.endpoint });
|
|
132
|
+
}
|
|
124
133
|
const message = sanitizeUserFacingErrorText(classified.message || `${context} failed`);
|
|
125
|
-
return new CLIError(message,
|
|
134
|
+
return new CLIError(message, 'model_backend', { statusCode: status, endpoint: classified.endpoint });
|
|
126
135
|
}
|
|
127
136
|
handleApiError(error, context) {
|
|
128
137
|
const userFacingError = this.toUserFacingApiError(error, context);
|
|
@@ -151,10 +160,6 @@ export class ChatCommand {
|
|
|
151
160
|
return await operation();
|
|
152
161
|
}
|
|
153
162
|
catch (error) {
|
|
154
|
-
if (!this.jsonOutput) {
|
|
155
|
-
const transient = this.isTimeoutError(error) ? 'timeout' : this.isNetworkError(error) ? 'network error' : 'API error';
|
|
156
|
-
console.error(chalk.red(`${context} failed with ${transient}: ${this.toUserFacingApiError(error, context).message}`));
|
|
157
|
-
}
|
|
158
163
|
lastError = error;
|
|
159
164
|
if (this.isJwtExpirationError(error)) {
|
|
160
165
|
this.handleApiError(error, context);
|
|
@@ -163,7 +168,8 @@ export class ChatCommand {
|
|
|
163
168
|
this.handleApiError(error, context);
|
|
164
169
|
}
|
|
165
170
|
if (!this.jsonOutput) {
|
|
166
|
-
|
|
171
|
+
const transient = this.isTimeoutError(error) ? 'timeout' : this.isNetworkError(error) ? 'network error' : 'temporary service issue';
|
|
172
|
+
this.logger.warn(`${context} failed due to ${transient}; retrying (${attempt + 1}/${retries})...`);
|
|
167
173
|
}
|
|
168
174
|
await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
|
|
169
175
|
}
|
|
@@ -262,7 +268,7 @@ export class ChatCommand {
|
|
|
262
268
|
explicitModel: true,
|
|
263
269
|
heavyTask,
|
|
264
270
|
cloudEligible,
|
|
265
|
-
cloudSelected:
|
|
271
|
+
cloudSelected: ['cloud', 'cloud-reason', 'ultra', 'cloud-fast', 'cloud-balanced', 'cloud-code', 'cloud-power', 'cloud-maximum'].includes(this.currentModel),
|
|
266
272
|
routeReason: 'explicit-model-selection',
|
|
267
273
|
};
|
|
268
274
|
}
|
|
@@ -295,8 +301,19 @@ export class ChatCommand {
|
|
|
295
301
|
routeReason: 'default-v3-agent',
|
|
296
302
|
};
|
|
297
303
|
}
|
|
298
|
-
getMessagesForModel() {
|
|
304
|
+
getMessagesForModel(options) {
|
|
299
305
|
const messages = [...this.messages];
|
|
306
|
+
const personaOverlay = this.buildActivePersonaOverlay();
|
|
307
|
+
if (personaOverlay && !messages.some((message) => message.role === 'system' && message.content.includes('Optional persona overlay: Wiener Grantler mode.'))) {
|
|
308
|
+
const insertionIndex = messages.findIndex((message) => message.role !== 'system');
|
|
309
|
+
const personaMessage = { role: 'system', content: personaOverlay };
|
|
310
|
+
if (insertionIndex === -1) {
|
|
311
|
+
messages.push(personaMessage);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
messages.splice(insertionIndex, 0, personaMessage);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
300
317
|
const memoryContexts = [
|
|
301
318
|
this.sessionManager.buildMemoryContext(this.currentSession),
|
|
302
319
|
this.projectMemory?.buildContextForPrompt(this.getLastUserPrompt()) || '',
|
|
@@ -318,8 +335,148 @@ export class ChatCommand {
|
|
|
318
335
|
messages.splice(insertionIndex, 0, memoryMessage);
|
|
319
336
|
}
|
|
320
337
|
}
|
|
338
|
+
const codebaseContext = this.workspaceBrain?.buildCodebaseContext(this.getLastUserPrompt()) || '';
|
|
339
|
+
if (codebaseContext && !messages.some((message) => message.role === 'system' && message.content.includes('Vigthoria codebase index context.'))) {
|
|
340
|
+
const insertionIndex = messages.findIndex((message) => message.role !== 'system');
|
|
341
|
+
const codebaseMessage = { role: 'system', content: codebaseContext };
|
|
342
|
+
if (insertionIndex === -1) {
|
|
343
|
+
messages.push(codebaseMessage);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
messages.splice(insertionIndex, 0, codebaseMessage);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (options?.compact) {
|
|
350
|
+
const compactLimit = 6000;
|
|
351
|
+
const systemMessages = messages.filter((message) => message.role === 'system').map((message) => ({
|
|
352
|
+
...message,
|
|
353
|
+
content: message.content.length > compactLimit
|
|
354
|
+
? `${message.content.slice(0, compactLimit)}\n...[trimmed for first local agent turn]`
|
|
355
|
+
: message.content,
|
|
356
|
+
}));
|
|
357
|
+
const lastUser = [...messages].reverse().find((message) => message.role === 'user');
|
|
358
|
+
return lastUser ? [...systemMessages.slice(0, 2), lastUser] : systemMessages.slice(0, 2);
|
|
359
|
+
}
|
|
321
360
|
return messages;
|
|
322
361
|
}
|
|
362
|
+
normalizeClientV3ToolPath(rawPath) {
|
|
363
|
+
let normalized = String(rawPath || '.').trim().replace(/\\/g, '/');
|
|
364
|
+
if (!normalized || normalized === '/') {
|
|
365
|
+
return '.';
|
|
366
|
+
}
|
|
367
|
+
normalized = normalized.replace(/^vigthoria:\/\/workspace\/?/i, '');
|
|
368
|
+
normalized = normalized.replace(/^\.\/+/, '');
|
|
369
|
+
normalized = normalized.replace(/^workspace\/?/i, '');
|
|
370
|
+
const workspaceRoot = this.currentProjectPath || process.cwd();
|
|
371
|
+
const workspaceName = path.basename(workspaceRoot);
|
|
372
|
+
if (workspaceName) {
|
|
373
|
+
const workspaceKey = workspaceName.toLowerCase().replace(/[\s_./\\-]+/g, '');
|
|
374
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
375
|
+
if (parts.length > 0) {
|
|
376
|
+
const firstKey = parts[0].toLowerCase().replace(/[\s_./\\-]+/g, '');
|
|
377
|
+
if (firstKey === workspaceKey) {
|
|
378
|
+
normalized = parts.slice(1).join('/') || '.';
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
normalized = normalized.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
383
|
+
return normalized || '.';
|
|
384
|
+
}
|
|
385
|
+
resolveClientV3ToolPath(rawPath) {
|
|
386
|
+
const root = this.currentProjectPath || process.cwd();
|
|
387
|
+
const normalized = this.normalizeClientV3ToolPath(rawPath);
|
|
388
|
+
const absoluteTarget = path.resolve(root, normalized === '.' ? '' : normalized);
|
|
389
|
+
if (fs.existsSync(absoluteTarget)) {
|
|
390
|
+
return normalized;
|
|
391
|
+
}
|
|
392
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
393
|
+
if (parts.length === 0) {
|
|
394
|
+
return '.';
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
398
|
+
const firstKey = parts[0].toLowerCase().replace(/[\s_./\\-]+/g, '');
|
|
399
|
+
for (const entry of entries) {
|
|
400
|
+
const entryKey = entry.name.toLowerCase().replace(/[\s_./\\-]+/g, '');
|
|
401
|
+
if (entryKey === firstKey || entryKey.includes(firstKey) || firstKey.includes(entryKey)) {
|
|
402
|
+
const rest = parts.slice(1).join('/');
|
|
403
|
+
const candidate = rest ? `${entry.name}/${rest}` : entry.name;
|
|
404
|
+
if (fs.existsSync(path.resolve(root, candidate))) {
|
|
405
|
+
return candidate.replace(/\\/g, '/');
|
|
406
|
+
}
|
|
407
|
+
if (!rest) {
|
|
408
|
+
return entry.name.replace(/\\/g, '/');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
// Ignore unreadable workspace roots during path repair.
|
|
415
|
+
}
|
|
416
|
+
return normalized;
|
|
417
|
+
}
|
|
418
|
+
normalizeClientV3ToolArgs(args) {
|
|
419
|
+
const normalizedArgs = { ...args };
|
|
420
|
+
for (const key of ['path', 'file_path', 'file', 'target']) {
|
|
421
|
+
if (typeof normalizedArgs[key] === 'string' && normalizedArgs[key].trim()) {
|
|
422
|
+
normalizedArgs[key] = this.resolveClientV3ToolPath(normalizedArgs[key]);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return normalizedArgs;
|
|
426
|
+
}
|
|
427
|
+
async executeClientV3Tool(event) {
|
|
428
|
+
if (!this.tools) {
|
|
429
|
+
return { success: false, output: '', error: 'Local agent tools are not initialized.' };
|
|
430
|
+
}
|
|
431
|
+
const name = String(event.name || '').trim();
|
|
432
|
+
const args = (event.arguments && typeof event.arguments === 'object')
|
|
433
|
+
? event.arguments
|
|
434
|
+
: {};
|
|
435
|
+
let toolName = name;
|
|
436
|
+
let toolArgs = {};
|
|
437
|
+
for (const [key, value] of Object.entries(args)) {
|
|
438
|
+
toolArgs[key] = value == null ? '' : String(value);
|
|
439
|
+
}
|
|
440
|
+
toolArgs = this.normalizeClientV3ToolArgs(toolArgs);
|
|
441
|
+
if (name === 'list_directory') {
|
|
442
|
+
toolName = 'list_dir';
|
|
443
|
+
toolArgs = {
|
|
444
|
+
path: toolArgs.path || '.',
|
|
445
|
+
...(toolArgs.recursive === 'true' || toolArgs.recursive === '1' ? { recursive: 'true' } : {}),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
else if (name === 'search_files') {
|
|
449
|
+
toolName = 'grep';
|
|
450
|
+
toolArgs = {
|
|
451
|
+
path: toolArgs.path || '.',
|
|
452
|
+
pattern: toolArgs.pattern || toolArgs.query || '',
|
|
453
|
+
...(toolArgs.file_pattern ? { includePattern: toolArgs.file_pattern } : {}),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
else if (name === 'read_file') {
|
|
457
|
+
toolName = 'read_file';
|
|
458
|
+
}
|
|
459
|
+
else if (name === 'write_file' || name === 'edit_file') {
|
|
460
|
+
toolName = name;
|
|
461
|
+
}
|
|
462
|
+
if (!toolName) {
|
|
463
|
+
return { success: false, output: '', error: 'Missing V3 client tool name.' };
|
|
464
|
+
}
|
|
465
|
+
const result = await this.tools.execute({ tool: toolName, args: toolArgs });
|
|
466
|
+
return {
|
|
467
|
+
success: result.success === true,
|
|
468
|
+
output: String(result.output || result.message || ''),
|
|
469
|
+
error: result.error ? String(result.error) : '',
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
getActivePersonaMode() {
|
|
473
|
+
if (this.personaOverride)
|
|
474
|
+
return this.personaOverride;
|
|
475
|
+
return normalizePersonaMode(this.config.get('persona')) || 'default';
|
|
476
|
+
}
|
|
477
|
+
buildActivePersonaOverlay() {
|
|
478
|
+
return buildPersonaOverlay(this.getActivePersonaMode(), this.getLastUserPrompt());
|
|
479
|
+
}
|
|
323
480
|
isDiagnosticPrompt(prompt) {
|
|
324
481
|
return /(startup|start up|won'?t start|doesn'?t start|crash|crashes|error|errors|failing|fails|issue|issues|bug|bugs|diagnos|debug|runtime|log|logs|exception|traceback|stack trace|yaml|blocking|blocker)/i.test(prompt);
|
|
325
482
|
}
|
|
@@ -328,10 +485,270 @@ export class ChatCommand {
|
|
|
328
485
|
* question — these should use analysis_only workflow, not full_autonomy.
|
|
329
486
|
*/
|
|
330
487
|
isAnalysisLookupPrompt(prompt) {
|
|
488
|
+
if (this.isImplementationPrompt(prompt)) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
331
491
|
return /^(what|which|where|how many|who|find|list|show|check|inspect|analyze|analyse|audit|explain|describe|summarize|summarise|review|overview|count|read|look at|tell me|locate|search for|does .* exist)/i.test(prompt.trim());
|
|
332
492
|
}
|
|
493
|
+
isImplementationPrompt(prompt) {
|
|
494
|
+
const trimmed = String(prompt || '').trim();
|
|
495
|
+
if (!trimmed)
|
|
496
|
+
return false;
|
|
497
|
+
if (/\b(implement|add|fix|repair|complete|finish|build|create|write|patch|update|modify|edit|make\s+(?:it\s+)?work(?:ing)?|get\s+(?:a\s+)?working)\b/i.test(trimmed)) {
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
if (/\bwhat(?:'s|\s+is|\s+are)\s+missing\b/i.test(trimmed)) {
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
if (/\b(game|html5|pacman|rogue|app|site)\b/i.test(trimmed) && /\b(missing|broken|fix|implement|working|playable)\b/i.test(trimmed)) {
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
if (/continue the previous agent run/i.test(trimmed) && /\b(implement|fix|missing|blocker|remaining)\b/i.test(trimmed)) {
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
getWindowsPromptPathRoots() {
|
|
512
|
+
const roots = new Set();
|
|
513
|
+
const add = (value) => {
|
|
514
|
+
const raw = String(value || '').trim();
|
|
515
|
+
if (!raw)
|
|
516
|
+
return;
|
|
517
|
+
try {
|
|
518
|
+
const resolved = path.resolve(raw);
|
|
519
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
520
|
+
roots.add(resolved);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
// Ignore unreadable discovery roots.
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
const home = os.homedir();
|
|
528
|
+
const cwdRoot = path.parse(process.cwd()).root || path.parse(home).root || '';
|
|
529
|
+
add(process.cwd());
|
|
530
|
+
add(home);
|
|
531
|
+
add(path.join(home, 'Desktop'));
|
|
532
|
+
add(path.join(home, 'Documents'));
|
|
533
|
+
add(process.env.USERPROFILE);
|
|
534
|
+
add(process.env.OneDrive);
|
|
535
|
+
add(process.env.OneDriveCommercial);
|
|
536
|
+
add(process.env.OneDriveConsumer);
|
|
537
|
+
if (process.env.OneDrive) {
|
|
538
|
+
add(path.join(process.env.OneDrive, 'Desktop'));
|
|
539
|
+
add(path.join(process.env.OneDrive, 'Documents'));
|
|
540
|
+
}
|
|
541
|
+
if (cwdRoot) {
|
|
542
|
+
add(cwdRoot);
|
|
543
|
+
add(path.join(cwdRoot, 'vigthoria'));
|
|
544
|
+
add(path.join(cwdRoot, 'Vigthoria'));
|
|
545
|
+
}
|
|
546
|
+
return Array.from(roots);
|
|
547
|
+
}
|
|
548
|
+
findPromptDirectoryByName(rawPath) {
|
|
549
|
+
if (os.platform() !== 'win32')
|
|
550
|
+
return null;
|
|
551
|
+
const normalized = String(rawPath || '').trim().replace(/^\/+/, '').replace(/[\\/]+/g, path.sep);
|
|
552
|
+
if (!normalized)
|
|
553
|
+
return null;
|
|
554
|
+
const wantedParts = normalized.split(/[\\/]+/).filter(Boolean);
|
|
555
|
+
const wantedName = wantedParts[wantedParts.length - 1];
|
|
556
|
+
if (!wantedName)
|
|
557
|
+
return null;
|
|
558
|
+
const maxDepth = 3;
|
|
559
|
+
const maxEntries = 2500;
|
|
560
|
+
const roots = this.getWindowsPromptPathRoots();
|
|
561
|
+
for (const root of roots) {
|
|
562
|
+
let visited = 0;
|
|
563
|
+
const direct = path.join(root, normalized);
|
|
564
|
+
try {
|
|
565
|
+
if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) {
|
|
566
|
+
return direct;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
// Continue with bounded search.
|
|
571
|
+
}
|
|
572
|
+
const stack = [{ dir: root, depth: 0 }];
|
|
573
|
+
while (stack.length > 0 && visited < maxEntries) {
|
|
574
|
+
const current = stack.pop();
|
|
575
|
+
if (!current || current.depth > maxDepth)
|
|
576
|
+
continue;
|
|
577
|
+
let entries;
|
|
578
|
+
try {
|
|
579
|
+
entries = fs.readdirSync(current.dir, { withFileTypes: true });
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
visited += entries.length;
|
|
585
|
+
for (const entry of entries) {
|
|
586
|
+
if (!entry.isDirectory())
|
|
587
|
+
continue;
|
|
588
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name.startsWith('$'))
|
|
589
|
+
continue;
|
|
590
|
+
const fullPath = path.join(current.dir, entry.name);
|
|
591
|
+
if (entry.name.toLowerCase() === wantedName.toLowerCase()) {
|
|
592
|
+
if (wantedParts.length === 1 || fullPath.toLowerCase().endsWith(normalized.toLowerCase())) {
|
|
593
|
+
return fullPath;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (current.depth < maxDepth) {
|
|
597
|
+
stack.push({ dir: fullPath, depth: current.depth + 1 });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
extractExplicitLocalPath(prompt) {
|
|
605
|
+
const resolveExistingPath = (rawPath) => {
|
|
606
|
+
const trimmed = String(rawPath || '').trim().replace(/^['"]|['"]$/g, '');
|
|
607
|
+
if (!trimmed)
|
|
608
|
+
return null;
|
|
609
|
+
const candidates = [];
|
|
610
|
+
candidates.push(path.resolve(trimmed));
|
|
611
|
+
if (os.platform() === 'win32' && /^\/[A-Za-z0-9._ -]/.test(trimmed)) {
|
|
612
|
+
const withoutLeadingSlash = trimmed.replace(/^\/+/, '');
|
|
613
|
+
const cwdRoot = path.parse(process.cwd()).root || '';
|
|
614
|
+
// Allow prompts like "/Vigthoria Games" to resolve as "C:/Vigthoria/Games".
|
|
615
|
+
const asSegments = withoutLeadingSlash.replace(/\s+/g, path.sep);
|
|
616
|
+
for (const root of this.getWindowsPromptPathRoots()) {
|
|
617
|
+
candidates.push(path.resolve(root, withoutLeadingSlash));
|
|
618
|
+
candidates.push(path.resolve(root, asSegments));
|
|
619
|
+
const segmentParts = asSegments.split(/[\\/]+/).filter(Boolean);
|
|
620
|
+
if (segmentParts.length > 1 && path.basename(root).toLowerCase() === segmentParts[0].toLowerCase()) {
|
|
621
|
+
candidates.push(path.resolve(root, ...segmentParts.slice(1)));
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (cwdRoot && !candidates.includes(path.resolve(cwdRoot, withoutLeadingSlash))) {
|
|
625
|
+
candidates.push(path.resolve(cwdRoot, withoutLeadingSlash));
|
|
626
|
+
candidates.push(path.resolve(cwdRoot, asSegments));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
for (const candidate of candidates) {
|
|
630
|
+
try {
|
|
631
|
+
if (fs.existsSync(candidate)) {
|
|
632
|
+
return candidate;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
// Continue trying other normalized candidates.
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const discovered = this.findPromptDirectoryByName(trimmed);
|
|
640
|
+
if (discovered)
|
|
641
|
+
return discovered;
|
|
642
|
+
return null;
|
|
643
|
+
};
|
|
644
|
+
// Quoted paths first (supports spaces safely).
|
|
645
|
+
const quotedPatterns = [
|
|
646
|
+
/"([A-Za-z]:[\\/][^"\r\n]+)"/,
|
|
647
|
+
/'([A-Za-z]:[\\/][^'\r\n]+)'/,
|
|
648
|
+
/"(\/[^"]+)"/,
|
|
649
|
+
/'(\/[^']+)'/,
|
|
650
|
+
];
|
|
651
|
+
for (const pattern of quotedPatterns) {
|
|
652
|
+
const match = prompt.match(pattern);
|
|
653
|
+
if (match?.[1]) {
|
|
654
|
+
const resolved = resolveExistingPath(match[1]);
|
|
655
|
+
if (resolved)
|
|
656
|
+
return resolved;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// Unquoted Windows path with optional spaces, stopping before instruction connectors.
|
|
660
|
+
const windowsMatch = prompt.match(/(?:^|\s)([A-Za-z]:[\\/][^<>"|?*\r\n]+?)(?=(?:\s+(?:and|then|to|for|with|where|that|which|who|make|create|build|analyse|analyze)\b|\s{2,}|[.,;!?)]|$))/i);
|
|
661
|
+
if (windowsMatch?.[1]) {
|
|
662
|
+
const resolved = resolveExistingPath(windowsMatch[1]);
|
|
663
|
+
if (resolved)
|
|
664
|
+
return resolved;
|
|
665
|
+
}
|
|
666
|
+
// Unix-style absolute path with optional spaces (but not URLs).
|
|
667
|
+
if (!/(https?|ftp):\/\//i.test(prompt)) {
|
|
668
|
+
const unixMatch = prompt.match(/(?:^|\s)(\/[a-zA-Z0-9._\-/ ]+?)(?=(?:\s+(?:and|then|to|for|with|where|that|which|who|make|create|build|analyse|analyze)\b|\s{2,}|[.,;!?)]|$))/i);
|
|
669
|
+
if (unixMatch?.[1]) {
|
|
670
|
+
const candidatePath = unixMatch[1]
|
|
671
|
+
.replace(/\s+(and|or|at|in|the|to|for|with|from|by|on)$/i, '')
|
|
672
|
+
.replace(/[.,;!?:)\]]*$/, '')
|
|
673
|
+
.trim();
|
|
674
|
+
if (candidatePath.length > 1) {
|
|
675
|
+
const resolved = resolveExistingPath(candidatePath);
|
|
676
|
+
if (resolved)
|
|
677
|
+
return resolved;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
isUnscopedPromptPathOverrideAllowed() {
|
|
684
|
+
return /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_ALLOW_UNSCOPED_PROMPT_PATHS || ''));
|
|
685
|
+
}
|
|
686
|
+
isPathWithinRoot(candidatePath, rootPath) {
|
|
687
|
+
const candidate = path.resolve(candidatePath);
|
|
688
|
+
const root = path.resolve(rootPath);
|
|
689
|
+
const rel = path.relative(root, candidate);
|
|
690
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
691
|
+
}
|
|
692
|
+
getPromptPathAllowedRoots(baseWorkspace) {
|
|
693
|
+
const roots = new Set();
|
|
694
|
+
const addRoot = (rawValue) => {
|
|
695
|
+
const value = String(rawValue || '').trim();
|
|
696
|
+
if (!value)
|
|
697
|
+
return;
|
|
698
|
+
const resolved = path.resolve(value);
|
|
699
|
+
try {
|
|
700
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
701
|
+
roots.add(resolved);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
// ignore invalid or unreadable roots
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
addRoot(baseWorkspace);
|
|
709
|
+
addRoot(this.currentProjectPath);
|
|
710
|
+
addRoot(process.cwd());
|
|
711
|
+
addRoot(this.config.get('project')?.rootPath || null);
|
|
712
|
+
const envRootsRaw = String(process.env.VIGTHORIA_ALLOWED_WORKSPACE_ROOTS || '').trim();
|
|
713
|
+
if (envRootsRaw) {
|
|
714
|
+
for (const entry of envRootsRaw.split(path.delimiter)) {
|
|
715
|
+
addRoot(entry);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return Array.from(roots);
|
|
719
|
+
}
|
|
720
|
+
resolvePromptWorkspacePath(prompt, baseWorkspace) {
|
|
721
|
+
const explicitPath = this.extractExplicitLocalPath(prompt);
|
|
722
|
+
if (!explicitPath) {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
const candidate = path.resolve(explicitPath);
|
|
726
|
+
if (this.isUnscopedPromptPathOverrideAllowed()) {
|
|
727
|
+
return candidate;
|
|
728
|
+
}
|
|
729
|
+
const runtime = this.getRuntimeEnvironmentContext();
|
|
730
|
+
// On local-machine CLI sessions, allow explicit absolute paths from user prompts.
|
|
731
|
+
// The path still must exist on disk (validated by extractExplicitLocalPath).
|
|
732
|
+
if (runtime.machineScope === 'local-machine') {
|
|
733
|
+
return candidate;
|
|
734
|
+
}
|
|
735
|
+
const allowedRoots = this.getPromptPathAllowedRoots(baseWorkspace);
|
|
736
|
+
const isAllowed = allowedRoots.some((root) => this.isPathWithinRoot(candidate, root));
|
|
737
|
+
if (isAllowed) {
|
|
738
|
+
return candidate;
|
|
739
|
+
}
|
|
740
|
+
if (!this.jsonOutput) {
|
|
741
|
+
console.log(chalk.yellow(`Ignoring path outside allowed workspace roots: ${candidate}`));
|
|
742
|
+
if (allowedRoots.length > 0) {
|
|
743
|
+
const displayRoots = allowedRoots.map((root) => root.replace(/\\/g, '/')).join(', ');
|
|
744
|
+
console.log(chalk.gray(`Allowed roots: ${displayRoots}`));
|
|
745
|
+
}
|
|
746
|
+
console.log(chalk.gray('To allow unrestricted prompt path overrides, set VIGTHORIA_ALLOW_UNSCOPED_PROMPT_PATHS=1.'));
|
|
747
|
+
}
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
333
750
|
isBrowserTaskPrompt(prompt) {
|
|
334
|
-
return /(
|
|
751
|
+
return /(\bbrowser\b|\bchrome\b|\bdevtools\b|\bconsole\b|\bdom\b|network tab|network request|frontend runtime|client-side|client side|rendering|page load|\bwebsocket\b|ui bug|inspect element)/i.test(prompt);
|
|
335
752
|
}
|
|
336
753
|
/**
|
|
337
754
|
* Returns true when a prompt can be answered directly without the full
|
|
@@ -377,13 +794,36 @@ export class ChatCommand {
|
|
|
377
794
|
inferAgentTaskType(prompt) {
|
|
378
795
|
if (this.isDiagnosticPrompt(prompt))
|
|
379
796
|
return 'debugging';
|
|
797
|
+
if (this.isImplementationPrompt(prompt)) {
|
|
798
|
+
return /\b(game|html5|pacman|rogue|playable)\b/i.test(prompt) ? 'game-build' : 'implementation';
|
|
799
|
+
}
|
|
380
800
|
if (/^(what|which|how many|list|show|check|inspect|analyze|analyse|audit|explain|describe|summarize|summarise|review|overview|find|count|read|look at|tell me)/i.test(prompt.trim()))
|
|
381
801
|
return 'analysis';
|
|
382
802
|
return 'implementation';
|
|
383
803
|
}
|
|
804
|
+
bindPromptWorkspace(prompt) {
|
|
805
|
+
const resolved = this.resolvePromptWorkspacePath(prompt, this.currentProjectPath);
|
|
806
|
+
if (!resolved) {
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
const normalizedResolved = path.resolve(resolved);
|
|
810
|
+
const normalizedCurrent = path.resolve(this.currentProjectPath);
|
|
811
|
+
if (normalizedResolved === normalizedCurrent) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (!this.jsonOutput) {
|
|
815
|
+
console.log(chalk.cyan(`📁 Workspace from prompt: ${normalizedResolved}`));
|
|
816
|
+
}
|
|
817
|
+
this.currentProjectPath = normalizedResolved;
|
|
818
|
+
this.tools?.setWorkspaceRoot(normalizedResolved);
|
|
819
|
+
this.projectMemory = new ProjectMemoryService(normalizedResolved);
|
|
820
|
+
}
|
|
384
821
|
buildTaskShapingInstructions(prompt) {
|
|
385
822
|
const instructions = [];
|
|
386
823
|
const runtime = this.getRuntimeEnvironmentContext();
|
|
824
|
+
if (runtime.machineScope === 'local-machine') {
|
|
825
|
+
instructions.push(`Execution environment: local user machine (${runtime.platform}).`, `Project workspace root on this machine: ${runtime.workspacePath}.`, 'All list_dir, read_file, grep, glob, and bash tools operate on this local filesystem — not a remote server copy.', 'Use paths relative to that root only. Never prefix paths with workspace/ or repeat the workspace folder name.', 'Start with list_dir on "." to discover the real folder structure before reading files.', 'If read_file fails, use list_dir or glob to locate the correct relative path, then retry read_file.');
|
|
826
|
+
}
|
|
387
827
|
// Platform-aware routing hints
|
|
388
828
|
if (runtime.platform === 'windows') {
|
|
389
829
|
instructions.push('Platform: Windows. Use list_dir, glob, read_file, and the grep tool for searching.', 'The grep tool handles Windows automatically — do not use bash to call grep, findstr, or Select-String manually.', 'Do not use bash for Unix commands (cat, head, tail, awk, sed, wc).', 'Use read_file to inspect file contents instead of shell commands.', 'All file paths use forward slashes internally.');
|
|
@@ -452,6 +892,82 @@ export class ChatCommand {
|
|
|
452
892
|
// Project Brain memory must not break chat, GoA, or operator execution.
|
|
453
893
|
}
|
|
454
894
|
}
|
|
895
|
+
initializeWorkspaceBrain() {
|
|
896
|
+
if (this.isProjectBrainRuntimeDisabled()) {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
this.workspaceBrain = new WorkspaceBrainService({
|
|
900
|
+
workspacePath: this.currentProjectPath,
|
|
901
|
+
apiBase: String(this.config.get('apiUrl') || 'https://coder.vigthoria.io'),
|
|
902
|
+
getAuthToken: () => this.config.get('authToken'),
|
|
903
|
+
});
|
|
904
|
+
this.tools?.setIndexedCodebaseSearch((query, maxResults) => (this.workspaceBrain?.searchCodebase(query, maxResults) || ''));
|
|
905
|
+
}
|
|
906
|
+
async bootstrapWorkspaceBrain(interactive) {
|
|
907
|
+
if (!this.workspaceBrain) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
const promptIfMissing = interactive
|
|
911
|
+
&& !this.jsonOutput
|
|
912
|
+
&& process.stdin.isTTY
|
|
913
|
+
&& !/^(0|false|no)$/i.test(String(process.env.VIGTHORIA_PROMPT_INDEX || '1'));
|
|
914
|
+
const result = await this.workspaceBrain.ensureIndexed({
|
|
915
|
+
promptIfMissing,
|
|
916
|
+
askToIndex: promptIfMissing
|
|
917
|
+
? async (fileCount, workspaceName) => new Promise((resolve) => {
|
|
918
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
919
|
+
rl.question(chalk.cyan(`Index "${workspaceName}" for Vigthoria Brain? (~${fileCount} source files) [Y/n] `), (answer) => {
|
|
920
|
+
rl.close();
|
|
921
|
+
const normalized = answer.trim().toLowerCase();
|
|
922
|
+
resolve(normalized === 'n' || normalized === 'no' ? 'later' : 'index_now');
|
|
923
|
+
});
|
|
924
|
+
})
|
|
925
|
+
: undefined,
|
|
926
|
+
});
|
|
927
|
+
if (result.indexed && result.fileCount > 0 && !this.jsonOutput) {
|
|
928
|
+
console.log(chalk.gray(`Brain index ready: ${result.fileCount} files, ${result.chunkCount} chunks`));
|
|
929
|
+
}
|
|
930
|
+
else if (result.prompted === 'later' && !this.jsonOutput) {
|
|
931
|
+
console.log(chalk.gray('Workspace indexing skipped. Use /index anytime.'));
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
async reindexWorkspaceBrain() {
|
|
935
|
+
if (!this.workspaceBrain) {
|
|
936
|
+
this.initializeWorkspaceBrain();
|
|
937
|
+
}
|
|
938
|
+
if (!this.workspaceBrain) {
|
|
939
|
+
console.log(chalk.yellow('Workspace Brain is disabled.'));
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const spinner = createSpinner({ text: 'Indexing workspace for Vigthoria Brain...', spinner: 'clock' }).start();
|
|
943
|
+
try {
|
|
944
|
+
const meta = await this.workspaceBrain.reindexWorkspace();
|
|
945
|
+
spinner.stop();
|
|
946
|
+
console.log(chalk.green(`Indexed ${meta.indexedFileCount} files (${meta.totalChunks} chunks). Brain Hub sync attempted.`));
|
|
947
|
+
}
|
|
948
|
+
catch (error) {
|
|
949
|
+
spinner.stop();
|
|
950
|
+
this.logger.error(error instanceof Error ? error.message : String(error));
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
showBrainIndexStatus() {
|
|
954
|
+
if (!this.workspaceBrain) {
|
|
955
|
+
console.log(chalk.yellow('Workspace Brain is disabled (VIGTHORIA_NO_BRAIN=1).'));
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
const status = this.workspaceBrain.getStatus();
|
|
959
|
+
console.log();
|
|
960
|
+
console.log(chalk.white('Workspace Brain Index:'));
|
|
961
|
+
console.log(chalk.gray(`Workspace: ${status.workspacePath}`));
|
|
962
|
+
console.log(chalk.gray(`Files indexed: ${status.indexedFileCount}`));
|
|
963
|
+
console.log(chalk.gray(`Chunks: ${status.totalChunks}`));
|
|
964
|
+
if (status.meta?.indexedAt) {
|
|
965
|
+
console.log(chalk.gray(`Last indexed: ${status.meta.indexedAt}`));
|
|
966
|
+
}
|
|
967
|
+
if (status.meta?.indexHash) {
|
|
968
|
+
console.log(chalk.gray(`Index hash: ${status.meta.indexHash}`));
|
|
969
|
+
}
|
|
970
|
+
}
|
|
455
971
|
async getPromptRuntimeContext(prompt) {
|
|
456
972
|
const runtimeContext = {
|
|
457
973
|
agentRuntime: this.getRuntimeEnvironmentContext(),
|
|
@@ -460,6 +976,22 @@ export class ChatCommand {
|
|
|
460
976
|
if (brainContext) {
|
|
461
977
|
runtimeContext.vigthoriaBrain = brainContext;
|
|
462
978
|
}
|
|
979
|
+
if (this.workspaceBrain) {
|
|
980
|
+
const accountBrain = await this.workspaceBrain.fetchAccountBrainContext();
|
|
981
|
+
if (accountBrain) {
|
|
982
|
+
runtimeContext.accountBrainContext = accountBrain;
|
|
983
|
+
}
|
|
984
|
+
const indexStatus = this.workspaceBrain.getStatus();
|
|
985
|
+
runtimeContext.codebaseIndex = {
|
|
986
|
+
indexedFileCount: indexStatus.indexedFileCount,
|
|
987
|
+
totalChunks: indexStatus.totalChunks,
|
|
988
|
+
indexHash: indexStatus.meta?.indexHash || null,
|
|
989
|
+
};
|
|
990
|
+
const indexedContext = this.workspaceBrain.buildCodebaseContext(prompt);
|
|
991
|
+
if (indexedContext) {
|
|
992
|
+
runtimeContext.codebaseContext = indexedContext;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
463
995
|
if (!this.isBrowserTaskPrompt(prompt)) {
|
|
464
996
|
return runtimeContext;
|
|
465
997
|
}
|
|
@@ -495,6 +1027,9 @@ export class ChatCommand {
|
|
|
495
1027
|
v3ToolCallCount = 0;
|
|
496
1028
|
v3LastActivity = Date.now();
|
|
497
1029
|
v3StreamingStarted = false;
|
|
1030
|
+
v3StreamedTextBuffer = '';
|
|
1031
|
+
v3LiveToolEvidence = [];
|
|
1032
|
+
v3PendingToolCalls = [];
|
|
498
1033
|
/**
|
|
499
1034
|
* Strip server-internal path prefixes from tool output strings.
|
|
500
1035
|
* Prevents exposing paths like /var/www/V3-Code-Agent/... to end users.
|
|
@@ -502,7 +1037,16 @@ export class ChatCommand {
|
|
|
502
1037
|
sanitizeServerPath(text) {
|
|
503
1038
|
if (!text)
|
|
504
1039
|
return text;
|
|
505
|
-
return sanitizeUserFacingPathText(text);
|
|
1040
|
+
return sanitizeUserFacingPathText(this.stripHiddenThoughtBlocks(text));
|
|
1041
|
+
}
|
|
1042
|
+
stripHiddenThoughtBlocks(text) {
|
|
1043
|
+
if (!text)
|
|
1044
|
+
return text;
|
|
1045
|
+
return text
|
|
1046
|
+
.replace(/<\|mask_start\|>[\s\S]*?<\|mask_end\|>/g, '')
|
|
1047
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, '')
|
|
1048
|
+
.replace(/<\|(?:mask_start|mask_end)\|>/g, '')
|
|
1049
|
+
.trim();
|
|
506
1050
|
}
|
|
507
1051
|
describeV3AgentTool(toolName) {
|
|
508
1052
|
const normalized = String(toolName || '').toLowerCase();
|
|
@@ -560,6 +1104,7 @@ export class ChatCommand {
|
|
|
560
1104
|
if (!safeText) {
|
|
561
1105
|
return;
|
|
562
1106
|
}
|
|
1107
|
+
this.v3StreamedTextBuffer += safeText;
|
|
563
1108
|
if (!this.v3StreamingStarted) {
|
|
564
1109
|
this.v3StreamingStarted = true;
|
|
565
1110
|
spinner.stop();
|
|
@@ -572,6 +1117,117 @@ export class ChatCommand {
|
|
|
572
1117
|
}
|
|
573
1118
|
process.stdout.write(safeText);
|
|
574
1119
|
}
|
|
1120
|
+
isGenericV3AgentContent(text) {
|
|
1121
|
+
const value = String(text || '').trim();
|
|
1122
|
+
return !value || /^(v3 agent workflow completed\.?|task completed|agent run finished|workflow completed\.?)$/i.test(value);
|
|
1123
|
+
}
|
|
1124
|
+
hasAlreadyStreamedV3Content(text) {
|
|
1125
|
+
const value = String(text || '').trim();
|
|
1126
|
+
if (!value)
|
|
1127
|
+
return true;
|
|
1128
|
+
return this.v3StreamedTextBuffer.includes(value) || value.includes(this.v3StreamedTextBuffer.trim());
|
|
1129
|
+
}
|
|
1130
|
+
isThinV3Summary(text) {
|
|
1131
|
+
const value = String(text || '').trim();
|
|
1132
|
+
if (!value)
|
|
1133
|
+
return true;
|
|
1134
|
+
if (/^#\s*Workspace overview/m.test(value) || /^## Workspace analysis \(from local file inspection\)/m.test(value)) {
|
|
1135
|
+
return false;
|
|
1136
|
+
}
|
|
1137
|
+
if (this.isGenericV3AgentContent(value))
|
|
1138
|
+
return true;
|
|
1139
|
+
if (/^The V3 agent finished without emitting a dedicated final answer/i.test(value)) {
|
|
1140
|
+
return !/(## Files read|## Directories inspected|### )/i.test(value);
|
|
1141
|
+
}
|
|
1142
|
+
return value.length < 120;
|
|
1143
|
+
}
|
|
1144
|
+
shouldPrintV3FinalContent(text) {
|
|
1145
|
+
if (!text || this.isGenericV3AgentContent(text)) {
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
const value = text.trim();
|
|
1149
|
+
if (/^## Workspace analysis \(from local file inspection\)/m.test(value)) {
|
|
1150
|
+
return true;
|
|
1151
|
+
}
|
|
1152
|
+
if (/Reconstructed task summary|Workspace analysis \(from local file inspection\)/i.test(value)) {
|
|
1153
|
+
return true;
|
|
1154
|
+
}
|
|
1155
|
+
if (!this.v3StreamingStarted) {
|
|
1156
|
+
return true;
|
|
1157
|
+
}
|
|
1158
|
+
return !this.hasAlreadyStreamedV3Content(value);
|
|
1159
|
+
}
|
|
1160
|
+
rememberV3ToolEvidence(event, args = {}) {
|
|
1161
|
+
const output = typeof event?.output === 'string' ? event.output.trim() : '';
|
|
1162
|
+
const errorText = typeof event?.error === 'string' ? event.error.trim() : '';
|
|
1163
|
+
const combined = output || errorText;
|
|
1164
|
+
if (!combined) {
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
const target = String(args.path || args.file_path || args.file || args.target || event?.target || '').trim();
|
|
1168
|
+
this.v3LiveToolEvidence.push({
|
|
1169
|
+
name: String(event?.name || event?.tool || 'unknown_tool'),
|
|
1170
|
+
target: target || undefined,
|
|
1171
|
+
arguments: args,
|
|
1172
|
+
output: combined,
|
|
1173
|
+
success: event?.success !== false,
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
buildUserFacingV3RunReport(prompt, workspacePath, options = {}) {
|
|
1177
|
+
const evidenceBody = this.api.formatV3AgentResponse({
|
|
1178
|
+
events: [],
|
|
1179
|
+
liveToolEvidence: this.v3LiveToolEvidence,
|
|
1180
|
+
});
|
|
1181
|
+
const successes = this.v3LiveToolEvidence.filter((entry) => entry.success !== false);
|
|
1182
|
+
const failures = this.v3LiveToolEvidence.filter((entry) => entry.success === false);
|
|
1183
|
+
if (!evidenceBody && successes.length === 0 && failures.length === 0) {
|
|
1184
|
+
return [
|
|
1185
|
+
'# Agent run summary',
|
|
1186
|
+
'',
|
|
1187
|
+
`**Workspace:** ${workspacePath}`,
|
|
1188
|
+
`**Your request:** ${prompt.trim()}`,
|
|
1189
|
+
'',
|
|
1190
|
+
'The agent finished without successfully reading local files, so no grounded overview could be built.',
|
|
1191
|
+
options.serverNote ? `\n**Server note:** ${options.serverNote}` : '',
|
|
1192
|
+
'',
|
|
1193
|
+
'Try `/continue` with: "Read Vigthoria-dominion/package.json, game.js, src/Game.js, and src/factions/, then write a full overview."',
|
|
1194
|
+
].filter(Boolean).join('\n');
|
|
1195
|
+
}
|
|
1196
|
+
const lines = [
|
|
1197
|
+
'# Workspace overview',
|
|
1198
|
+
'',
|
|
1199
|
+
`**Workspace:** ${workspacePath}`,
|
|
1200
|
+
`**Your request:** ${prompt.trim()}`,
|
|
1201
|
+
options.partial
|
|
1202
|
+
? '**Status:** Partial — the agent used its iteration budget before writing a final narrative. The report below is rebuilt from files it actually read on your machine.'
|
|
1203
|
+
: '**Status:** Complete — summary rebuilt from local file inspection.',
|
|
1204
|
+
'',
|
|
1205
|
+
evidenceBody || '_No readable file excerpts were captured._',
|
|
1206
|
+
];
|
|
1207
|
+
if (failures.length > 0) {
|
|
1208
|
+
const uniqueFails = new Map();
|
|
1209
|
+
for (const entry of failures) {
|
|
1210
|
+
const failPath = entry.target || entry.arguments?.path || entry.name;
|
|
1211
|
+
if (!uniqueFails.has(failPath)) {
|
|
1212
|
+
uniqueFails.set(failPath, entry.output.split('\n').find(Boolean)?.slice(0, 140) || entry.output.slice(0, 140));
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (uniqueFails.size > 0 && !/## Paths not found/i.test(evidenceBody || '')) {
|
|
1216
|
+
lines.push('', '## Paths not found (exploration misses)', ...[...uniqueFails.entries()].slice(0, 12).map(([failPath, detail]) => `- \`${failPath}\`${detail ? ` — ${detail}` : ''}`), '', '_These are usually incorrect guesses (for example `Vigthoria-dominion/entities` instead of `Vigthoria-dominion/src/entities`). They do not mean your project is broken._');
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
lines.push('', '---', 'Use `/continue` if you want a longer narrative summary or a deep dive into a specific subfolder.');
|
|
1220
|
+
return lines.join('\n');
|
|
1221
|
+
}
|
|
1222
|
+
printV3UserReport(report) {
|
|
1223
|
+
const value = String(report || '').trim();
|
|
1224
|
+
if (!value || this.isGenericV3AgentContent(value)) {
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
console.log();
|
|
1228
|
+
console.log(value);
|
|
1229
|
+
console.log();
|
|
1230
|
+
}
|
|
575
1231
|
updateV3AgentSpinner(spinner, event) {
|
|
576
1232
|
if (this.isRawV3StreamPayload(event)) {
|
|
577
1233
|
this.consumeV3StreamPayload(spinner, event).catch((error) => {
|
|
@@ -589,7 +1245,7 @@ export class ChatCommand {
|
|
|
589
1245
|
if (event.type === 'tool_call') {
|
|
590
1246
|
this.v3ToolCallCount += 1;
|
|
591
1247
|
const toolDesc = this.describeV3AgentTool(event.tool || event.name || event.tool_name);
|
|
592
|
-
const toolTarget = event.arguments?.path || event.arguments?.file_path || event.arguments?.pattern || '';
|
|
1248
|
+
const toolTarget = event.arguments?.path || event.arguments?.file_path || event.arguments?.pattern || event.arguments?.query || '';
|
|
593
1249
|
const sanitizedTarget = this.sanitizeServerPath(String(toolTarget));
|
|
594
1250
|
const shortTarget = sanitizedTarget ? ` → ${sanitizedTarget.replace(/\\/g, '/').split('/').slice(-2).join('/')}` : '';
|
|
595
1251
|
const stepLabel = chalk.cyan(` [${this.v3IterationCount}/${this.v3ToolCallCount}]`) + ` ${toolDesc}${shortTarget}`;
|
|
@@ -599,6 +1255,21 @@ export class ChatCommand {
|
|
|
599
1255
|
// Show extra detail for key tools
|
|
600
1256
|
const args = event.arguments || {};
|
|
601
1257
|
const toolName = event.name || event.tool || '';
|
|
1258
|
+
this.v3PendingToolCalls.push({
|
|
1259
|
+
name: String(toolName || 'unknown_tool'),
|
|
1260
|
+
args: args && typeof args === 'object' ? args : {},
|
|
1261
|
+
});
|
|
1262
|
+
if (toolName === 'search_files') {
|
|
1263
|
+
const pattern = String(args.pattern || args.query || '').trim();
|
|
1264
|
+
const searchPath = String(args.path || '.').trim();
|
|
1265
|
+
process.stderr.write(chalk.gray(` search: ${pattern || '(empty)'} in ${this.sanitizeServerPath(searchPath)}\n`));
|
|
1266
|
+
}
|
|
1267
|
+
else if (toolName === 'list_directory') {
|
|
1268
|
+
process.stderr.write(chalk.gray(` list: ${this.sanitizeServerPath(String(args.path || '.'))}${args.recursive ? ' (recursive)' : ''}\n`));
|
|
1269
|
+
}
|
|
1270
|
+
else if (toolName === 'read_file') {
|
|
1271
|
+
process.stderr.write(chalk.gray(` read: ${this.sanitizeServerPath(String(args.path || args.file_path || ''))}\n`));
|
|
1272
|
+
}
|
|
602
1273
|
if ((toolName === 'write_file' || toolName === 'edit_file') && typeof args.content === 'string') {
|
|
603
1274
|
const len = args.content.length;
|
|
604
1275
|
process.stderr.write(chalk.gray(` ${len > 1000 ? Math.round(len / 1024) + ' KB' : len + ' bytes'} content\n`));
|
|
@@ -621,25 +1292,51 @@ export class ChatCommand {
|
|
|
621
1292
|
// Show output for failures, or brief summary for successes — sanitize server paths
|
|
622
1293
|
const rawOutput = typeof event.output === 'string' ? event.output.trim() : '';
|
|
623
1294
|
const output = this.sanitizeServerPath(rawOutput);
|
|
1295
|
+
const pendingIndex = this.v3PendingToolCalls.findIndex((call) => call.name === toolName);
|
|
1296
|
+
const pendingCall = pendingIndex >= 0
|
|
1297
|
+
? this.v3PendingToolCalls.splice(pendingIndex, 1)[0]
|
|
1298
|
+
: this.v3PendingToolCalls.shift();
|
|
1299
|
+
this.rememberV3ToolEvidence(event, pendingCall?.args || {});
|
|
624
1300
|
if (!success && output) {
|
|
625
1301
|
const sanitizedError = this.sanitizeServerPath(typeof event.error === 'string' ? event.error : output);
|
|
626
1302
|
const lines = sanitizedError.split('\n').slice(0, 4);
|
|
627
1303
|
process.stderr.write(chalk.red(` ${lines.join('\n ')}\n`));
|
|
628
1304
|
}
|
|
629
1305
|
else if (success && output && output.length > 0) {
|
|
630
|
-
const
|
|
1306
|
+
const outputLines = output.split('\n').map((line) => line.trimEnd()).filter(Boolean);
|
|
1307
|
+
const firstMeaningfulLine = outputLines.find((line) => !/^## Untrusted workspace data boundary/i.test(line)) || outputLines[0] || '';
|
|
1308
|
+
const brief = firstMeaningfulLine.slice(0, 160);
|
|
631
1309
|
process.stderr.write(chalk.gray(` ${brief}${output.length > 120 ? '…' : ''}\n`));
|
|
632
1310
|
}
|
|
633
1311
|
spinner.start();
|
|
634
1312
|
spinner.text = 'Next step...';
|
|
635
1313
|
return;
|
|
636
1314
|
}
|
|
1315
|
+
if (event.type === 'v3_status') {
|
|
1316
|
+
spinner.text = this.sanitizeServerPath(String(event.content || 'Routing to V3 Agent...'));
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
if (event.type === 'queued') {
|
|
1320
|
+
if (spinner.isSpinning)
|
|
1321
|
+
spinner.stop();
|
|
1322
|
+
process.stderr.write(chalk.yellow(' [Queue] ') + this.sanitizeServerPath(String(event.message || 'Waiting for V3 capacity...')) + '\n');
|
|
1323
|
+
spinner.start();
|
|
1324
|
+
spinner.text = 'Waiting for V3...';
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (event.type === 'heartbeat') {
|
|
1328
|
+
spinner.text = 'V3 Agent is still thinking...';
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
637
1331
|
if (event.type === 'thinking') {
|
|
638
1332
|
this.v3IterationCount += 1;
|
|
639
1333
|
const iterText = this.sanitizeServerPath(event.content || '');
|
|
640
1334
|
if (spinner.isSpinning)
|
|
641
1335
|
spinner.stop();
|
|
642
|
-
process.stderr.write(chalk.cyan(`\n──
|
|
1336
|
+
process.stderr.write(chalk.cyan(`\n── Iteration ${this.v3IterationCount}${iterText ? '' : '...'} ──\n`));
|
|
1337
|
+
if (iterText) {
|
|
1338
|
+
process.stderr.write(chalk.gray(`${iterText}\n`));
|
|
1339
|
+
}
|
|
643
1340
|
spinner.start();
|
|
644
1341
|
spinner.text = 'Analyzing...';
|
|
645
1342
|
return;
|
|
@@ -813,6 +1510,22 @@ export class ChatCommand {
|
|
|
813
1510
|
process.stderr.write(chalk.cyan(' [Start] ') + 'Agent initialized\n');
|
|
814
1511
|
spinner.start();
|
|
815
1512
|
spinner.text = 'Working...';
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
const fallbackStatus = typeof event.status === 'string' ? this.sanitizeServerPath(event.status) : '';
|
|
1516
|
+
const fallbackStage = typeof event.stage === 'string' ? this.sanitizeServerPath(event.stage) : '';
|
|
1517
|
+
const fallbackMessage = typeof event.message === 'string'
|
|
1518
|
+
? this.sanitizeServerPath(event.message)
|
|
1519
|
+
: (typeof event.content === 'string' ? this.sanitizeServerPath(event.content) : '');
|
|
1520
|
+
if (event.type === 'status' || event.type === 'progress' || fallbackStatus || fallbackStage) {
|
|
1521
|
+
if (spinner.isSpinning)
|
|
1522
|
+
spinner.stop();
|
|
1523
|
+
if (fallbackMessage) {
|
|
1524
|
+
process.stderr.write(chalk.cyan(' [V3] ') + `${fallbackMessage}\n`);
|
|
1525
|
+
}
|
|
1526
|
+
spinner.start();
|
|
1527
|
+
spinner.text = fallbackStatus || fallbackStage || 'Working...';
|
|
1528
|
+
return;
|
|
816
1529
|
}
|
|
817
1530
|
}
|
|
818
1531
|
updateOperatorSpinner(spinner, event) {
|
|
@@ -878,6 +1591,7 @@ export class ChatCommand {
|
|
|
878
1591
|
}
|
|
879
1592
|
return;
|
|
880
1593
|
}
|
|
1594
|
+
await this.config.refreshHubModelPreferences().catch(() => null);
|
|
881
1595
|
this.agentMode = options.agent === true;
|
|
882
1596
|
this.operatorMode = options.operator === true;
|
|
883
1597
|
this.workflowTarget = typeof options.workflow === 'string' && options.workflow.trim()
|
|
@@ -886,6 +1600,7 @@ export class ChatCommand {
|
|
|
886
1600
|
this.savePlanToVigFlow = options.savePlan === true;
|
|
887
1601
|
this.jsonOutput = options.json === true;
|
|
888
1602
|
this.autoApprove = options.autoApprove === true || this.jsonOutput;
|
|
1603
|
+
this.personaOverride = options.grant === true ? 'wiener_grant' : null;
|
|
889
1604
|
this.modelExplicitlySelected = Boolean(String(options.model || '').trim());
|
|
890
1605
|
this.currentModel = this.resolveInitialModel(options);
|
|
891
1606
|
this.applyNoAgentGovernance(String(options.model || this.currentModel || ''));
|
|
@@ -898,6 +1613,8 @@ export class ChatCommand {
|
|
|
898
1613
|
this.directToolContinuationCount = 0;
|
|
899
1614
|
this.tools = new AgenticTools(this.logger, this.currentProjectPath, async (action) => this.requestPermission(action), this.autoApprove);
|
|
900
1615
|
this.initializeSession(options.resume === true);
|
|
1616
|
+
this.initializeWorkspaceBrain();
|
|
1617
|
+
await this.bootstrapWorkspaceBrain(!options.prompt);
|
|
901
1618
|
// ── Commando Bridge: connect if --bridge was specified ──────────
|
|
902
1619
|
if (options.bridge) {
|
|
903
1620
|
const bridgeClient = new BridgeClient({
|
|
@@ -956,7 +1673,10 @@ export class ChatCommand {
|
|
|
956
1673
|
bridge.destroy();
|
|
957
1674
|
}
|
|
958
1675
|
}
|
|
959
|
-
|
|
1676
|
+
// Force-exit: undici + chokidar + HTTPS pool keep the Node.js event
|
|
1677
|
+
// loop alive indefinitely in direct prompt mode; a clean exit is safe here.
|
|
1678
|
+
this.api.destroy();
|
|
1679
|
+
process.exit(process.exitCode ?? 0);
|
|
960
1680
|
}
|
|
961
1681
|
await this.startInteractiveChat();
|
|
962
1682
|
const bridge = getBridgeClient();
|
|
@@ -998,11 +1718,57 @@ export class ChatCommand {
|
|
|
998
1718
|
}
|
|
999
1719
|
fs.mkdirSync(this.currentProjectPath, { recursive: true });
|
|
1000
1720
|
}
|
|
1721
|
+
shouldStartWorkspaceWatcher(workspaceRoot) {
|
|
1722
|
+
if (!workspaceRoot) {
|
|
1723
|
+
return false;
|
|
1724
|
+
}
|
|
1725
|
+
const resolved = path.resolve(workspaceRoot);
|
|
1726
|
+
if (!fs.existsSync(resolved)) {
|
|
1727
|
+
return false;
|
|
1728
|
+
}
|
|
1729
|
+
let stat;
|
|
1730
|
+
try {
|
|
1731
|
+
stat = fs.statSync(resolved);
|
|
1732
|
+
}
|
|
1733
|
+
catch {
|
|
1734
|
+
return false;
|
|
1735
|
+
}
|
|
1736
|
+
if (!stat.isDirectory()) {
|
|
1737
|
+
return false;
|
|
1738
|
+
}
|
|
1739
|
+
const homeDir = path.resolve(os.homedir());
|
|
1740
|
+
const normalized = resolved.replace(/\\/g, '/');
|
|
1741
|
+
const normalizedHome = homeDir.replace(/\\/g, '/');
|
|
1742
|
+
// Guardrail: do not watch broad home/root scopes on interactive shells.
|
|
1743
|
+
if (normalized.toLowerCase() === normalizedHome.toLowerCase()) {
|
|
1744
|
+
if (!this.jsonOutput) {
|
|
1745
|
+
console.log(chalk.gray('Info: workspace watcher disabled for home directory scope.'));
|
|
1746
|
+
}
|
|
1747
|
+
return false;
|
|
1748
|
+
}
|
|
1749
|
+
if (/^[a-zA-Z]:\/$/.test(normalized) || normalized === '/') {
|
|
1750
|
+
if (!this.jsonOutput) {
|
|
1751
|
+
console.log(chalk.gray('Info: workspace watcher disabled for filesystem root scope.'));
|
|
1752
|
+
}
|
|
1753
|
+
return false;
|
|
1754
|
+
}
|
|
1755
|
+
return true;
|
|
1756
|
+
}
|
|
1001
1757
|
resolveProjectPath(options) {
|
|
1002
1758
|
const requestedProject = String(options.project || '').trim();
|
|
1003
1759
|
if (requestedProject) {
|
|
1004
1760
|
return path.resolve(requestedProject);
|
|
1005
1761
|
}
|
|
1762
|
+
// Check if prompt contains an explicit local path provided by the user
|
|
1763
|
+
if (options.prompt) {
|
|
1764
|
+
const explicitPath = this.resolvePromptWorkspacePath(options.prompt, process.cwd());
|
|
1765
|
+
if (explicitPath) {
|
|
1766
|
+
if (!this.jsonOutput) {
|
|
1767
|
+
console.log(chalk.gray(`📁 Using project path from prompt: ${explicitPath}`));
|
|
1768
|
+
}
|
|
1769
|
+
return explicitPath;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1006
1772
|
if (this.shouldUseManagedWorkspace(options)) {
|
|
1007
1773
|
const rootPath = this.getManagedWorkspaceRoot();
|
|
1008
1774
|
fs.mkdirSync(rootPath, { recursive: true });
|
|
@@ -1465,6 +2231,26 @@ export class ChatCommand {
|
|
|
1465
2231
|
}
|
|
1466
2232
|
}
|
|
1467
2233
|
async runSimplePrompt(prompt) {
|
|
2234
|
+
if (!this.directPromptMode && !this.operatorMode) {
|
|
2235
|
+
const promptToRun = this.isConfirmationFollowUp(prompt) && this.lastActionableUserInput && this.isRepoGroundedPrompt(this.lastActionableUserInput)
|
|
2236
|
+
? this.lastActionableUserInput
|
|
2237
|
+
: prompt;
|
|
2238
|
+
if (this.isRepoGroundedPrompt(promptToRun) || /\b(build|implement|complete|fix|create|make|edit|write|change|finish)\b/i.test(promptToRun)) {
|
|
2239
|
+
if (!this.tools) {
|
|
2240
|
+
throw new Error('Agent tools are not initialized.');
|
|
2241
|
+
}
|
|
2242
|
+
this.agentMode = true;
|
|
2243
|
+
this.syncInteractiveModeModel('agent');
|
|
2244
|
+
if (this.currentSession) {
|
|
2245
|
+
this.currentSession.agentMode = true;
|
|
2246
|
+
this.currentSession.model = this.currentModel;
|
|
2247
|
+
}
|
|
2248
|
+
console.log(chalk.yellow('This request needs file access, so I am switching to Agent mode and working in this workspace now.'));
|
|
2249
|
+
console.log(chalk.gray('You do not need to confirm with "yes"; I will continue until the agent run finishes or reports a blocker.'));
|
|
2250
|
+
await this.runAgentTurn(promptToRun);
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
1468
2254
|
this.lastActionableUserInput = prompt;
|
|
1469
2255
|
// For direct --prompt mode with simple prompts, use a minimal system
|
|
1470
2256
|
// message to avoid polluting the response with tool/platform context.
|
|
@@ -1525,7 +2311,7 @@ export class ChatCommand {
|
|
|
1525
2311
|
const response = await this.callApi('Send chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel));
|
|
1526
2312
|
if (spinner)
|
|
1527
2313
|
spinner.stop();
|
|
1528
|
-
const finalText = (response.message || '').trim();
|
|
2314
|
+
const finalText = this.sanitizeDirectModeOutput(this.stripHiddenThoughtBlocks(response.message || '')).trim();
|
|
1529
2315
|
const effectiveModel = String(response.model || this.currentModel);
|
|
1530
2316
|
const metadata = this.modelGovernanceFallback
|
|
1531
2317
|
? { modelFallback: this.modelGovernanceFallback }
|
|
@@ -1570,21 +2356,32 @@ export class ChatCommand {
|
|
|
1570
2356
|
}
|
|
1571
2357
|
}
|
|
1572
2358
|
}
|
|
1573
|
-
async runAgentTurn(prompt) {
|
|
2359
|
+
async runAgentTurn(prompt, options = {}) {
|
|
1574
2360
|
if (!this.tools) {
|
|
1575
2361
|
throw new Error('Agent tools are not initialized.');
|
|
1576
2362
|
}
|
|
2363
|
+
this.bindPromptWorkspace(prompt);
|
|
1577
2364
|
const requiresV3Workflow = this.shouldRequireV3AgentWorkflow(prompt);
|
|
1578
2365
|
const handledByDirectFileFlow = await this.tryDirectSingleFileFlow(prompt);
|
|
1579
2366
|
if (handledByDirectFileFlow) {
|
|
1580
2367
|
this.saveSession();
|
|
1581
2368
|
return;
|
|
1582
2369
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
2370
|
+
// Prime the message context with the target file when the direct-file flow was
|
|
2371
|
+
// bypassed (e.g. HTML files routed to V3) so the local agent loop has file
|
|
2372
|
+
// awareness and the ⚙ Executing: read_file banner is always emitted.
|
|
2373
|
+
await this.primeBypassedTargetFileContext(prompt);
|
|
2374
|
+
if (!options.skipLocalLoop && !options.forceV3 && this.shouldPreferLocalAgentLoop(prompt)) {
|
|
2375
|
+
const completed = await this.runLocalAgentLoop(prompt);
|
|
2376
|
+
if (completed) {
|
|
2377
|
+
this.saveSession();
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
if (!this.jsonOutput) {
|
|
2381
|
+
console.log(chalk.yellow('Local chat backend unavailable (self-hosted inference may be offline). Trying V3 agent workflow...'));
|
|
2382
|
+
}
|
|
1586
2383
|
}
|
|
1587
|
-
const handledByV3Workflow = await this.tryV3AgentWorkflow(prompt);
|
|
2384
|
+
const handledByV3Workflow = await this.tryV3AgentWorkflow(prompt, options);
|
|
1588
2385
|
if (handledByV3Workflow) {
|
|
1589
2386
|
this.saveSession();
|
|
1590
2387
|
return;
|
|
@@ -1598,24 +2395,110 @@ export class ChatCommand {
|
|
|
1598
2395
|
}
|
|
1599
2396
|
await this.runLocalAgentLoop(prompt);
|
|
1600
2397
|
}
|
|
2398
|
+
localAgentIterationCount = 0;
|
|
2399
|
+
buildLocalAgentChatOptions(preflight, onRouteAttempt) {
|
|
2400
|
+
const preferredRoute = preflight?.endpoint
|
|
2401
|
+
? this.api.mapPreflightEndpointToRoute(preflight.endpoint)
|
|
2402
|
+
: undefined;
|
|
2403
|
+
return {
|
|
2404
|
+
fastFail: true,
|
|
2405
|
+
singleRoute: true,
|
|
2406
|
+
stream: true,
|
|
2407
|
+
connectTimeoutMs: Number.parseInt(process.env.VIGTHORIA_CHAT_CONNECT_TIMEOUT_MS || '20000', 10),
|
|
2408
|
+
idleTimeoutMs: Number.parseInt(process.env.VIGTHORIA_CHAT_IDLE_TIMEOUT_MS || '120000', 10),
|
|
2409
|
+
preferredRoute: preferredRoute || 'coder',
|
|
2410
|
+
onRouteAttempt,
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
printChatModelPreflight(preflight) {
|
|
2414
|
+
if (this.jsonOutput) {
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2417
|
+
console.log(chalk.gray('Model backend preflight:'));
|
|
2418
|
+
for (const route of preflight.routes) {
|
|
2419
|
+
const marker = route.ok ? chalk.green(' ✓') : chalk.red(' ✗');
|
|
2420
|
+
const detail = route.error ? chalk.gray(` — ${route.error}`) : '';
|
|
2421
|
+
console.log(`${marker} ${route.name}${detail}`);
|
|
2422
|
+
}
|
|
2423
|
+
if (preflight.healthy) {
|
|
2424
|
+
console.log(chalk.gray(`Using: ${preflight.endpoint}`));
|
|
2425
|
+
}
|
|
2426
|
+
else if (preflight.error) {
|
|
2427
|
+
console.log(chalk.yellow(preflight.error));
|
|
2428
|
+
}
|
|
2429
|
+
console.log();
|
|
2430
|
+
}
|
|
1601
2431
|
async runLocalAgentLoop(prompt) {
|
|
1602
2432
|
if (!this.tools) {
|
|
1603
2433
|
throw new Error('Agent tools are not initialized.');
|
|
1604
2434
|
}
|
|
2435
|
+
this.localAgentIterationCount = 0;
|
|
2436
|
+
const runtime = this.getRuntimeEnvironmentContext();
|
|
2437
|
+
if (!this.jsonOutput) {
|
|
2438
|
+
console.log();
|
|
2439
|
+
console.log(chalk.gray('━━━ ROUTING DECISION ━━━'));
|
|
2440
|
+
console.log(chalk.gray('Reason: local-machine-agent-loop'));
|
|
2441
|
+
console.log(chalk.gray(`Platform: ${runtime.platform}`));
|
|
2442
|
+
console.log(chalk.gray(`Machine scope: ${runtime.machineScope}`));
|
|
2443
|
+
console.log(chalk.gray(`Workspace: ${runtime.workspacePath}`));
|
|
2444
|
+
console.log(chalk.gray('Tools execute on your local filesystem.'));
|
|
2445
|
+
console.log(chalk.gray('━'.repeat(30)));
|
|
2446
|
+
console.log();
|
|
2447
|
+
}
|
|
1605
2448
|
this.lastActionableUserInput = prompt;
|
|
1606
2449
|
this.directToolContinuationCount = 0;
|
|
1607
2450
|
this.agentToolEvidence = { discovery: 0, mutation: 0, searchFailed: 0 };
|
|
1608
2451
|
this.tools.clearSessionApprovals();
|
|
1609
2452
|
getBridgeClient()?.emitPrompt({ prompt, mode: this.operatorMode ? 'operator' : 'agent', model: this.currentModel });
|
|
1610
2453
|
this.ensureAgentSystemPrompt();
|
|
1611
|
-
|
|
2454
|
+
const scopedPrompt = this.buildScopedUserPrompt(this.buildContextualAgentPrompt(prompt));
|
|
2455
|
+
this.messages.push({ role: 'user', content: scopedPrompt });
|
|
2456
|
+
await this.primeAgentWorkspaceDiscovery(prompt);
|
|
1612
2457
|
this.saveSession();
|
|
2458
|
+
const preflightSpinner = this.jsonOutput
|
|
2459
|
+
? null
|
|
2460
|
+
: createSpinner({ text: 'Checking model connection (preflight)...', spinner: 'clock' }).start();
|
|
2461
|
+
let preflight;
|
|
2462
|
+
try {
|
|
2463
|
+
preflight = await this.api.runChatModelPreflight(this.currentModel);
|
|
2464
|
+
}
|
|
2465
|
+
finally {
|
|
2466
|
+
if (preflightSpinner) {
|
|
2467
|
+
preflightSpinner.stop();
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
this.printChatModelPreflight(preflight);
|
|
2471
|
+
if (!preflight.healthy) {
|
|
2472
|
+
return false;
|
|
2473
|
+
}
|
|
1613
2474
|
const maxTurns = 10;
|
|
2475
|
+
const preferredRoute = preflight?.endpoint
|
|
2476
|
+
? (this.api.mapPreflightEndpointToRoute(preflight.endpoint) || 'coder')
|
|
2477
|
+
: 'coder';
|
|
1614
2478
|
for (let turn = 0; turn < maxTurns; turn += 1) {
|
|
1615
|
-
const spinner = this.jsonOutput ? null : createSpinner({ text: turn === 0 ? 'Planning...' : 'Continuing...', spinner: 'clock' }).start();
|
|
2479
|
+
const spinner = this.jsonOutput ? null : createSpinner({ text: turn === 0 ? 'Planning first tool step...' : 'Continuing...', spinner: 'clock' }).start();
|
|
2480
|
+
let streamedVisible = '';
|
|
1616
2481
|
let response;
|
|
1617
2482
|
try {
|
|
1618
|
-
response = await this.callApi('Send agent chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel
|
|
2483
|
+
response = await this.callApi('Send agent chat message', () => this.api.chat(this.getMessagesForModel({ compact: turn === 0 }), this.currentModel, false, {
|
|
2484
|
+
fastFail: true,
|
|
2485
|
+
singleRoute: true,
|
|
2486
|
+
stream: true,
|
|
2487
|
+
connectTimeoutMs: Number.parseInt(process.env.VIGTHORIA_CHAT_CONNECT_TIMEOUT_MS || '20000', 10),
|
|
2488
|
+
idleTimeoutMs: Number.parseInt(process.env.VIGTHORIA_CHAT_IDLE_TIMEOUT_MS || '120000', 10),
|
|
2489
|
+
preferredRoute,
|
|
2490
|
+
onRouteAttempt: (routeLabel) => {
|
|
2491
|
+
if (spinner) {
|
|
2492
|
+
spinner.text = routeLabel;
|
|
2493
|
+
}
|
|
2494
|
+
},
|
|
2495
|
+
onStreamDelta: (chunk) => {
|
|
2496
|
+
streamedVisible += chunk;
|
|
2497
|
+
if (spinner) {
|
|
2498
|
+
spinner.text = streamedVisible.trim().slice(-72) || 'Streaming model output...';
|
|
2499
|
+
}
|
|
2500
|
+
},
|
|
2501
|
+
}), 0);
|
|
1619
2502
|
}
|
|
1620
2503
|
catch (firstErr) {
|
|
1621
2504
|
// If we already gathered evidence and the model API fails on a
|
|
@@ -1624,7 +2507,14 @@ export class ChatCommand {
|
|
|
1624
2507
|
this.logger.debug('Agent continuation API call failed, retrying once...');
|
|
1625
2508
|
try {
|
|
1626
2509
|
await new Promise(r => setTimeout(r, 2000));
|
|
1627
|
-
response = await this.callApi('Retry agent chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel
|
|
2510
|
+
response = await this.callApi('Retry agent chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel, false, {
|
|
2511
|
+
fastFail: true,
|
|
2512
|
+
singleRoute: true,
|
|
2513
|
+
stream: true,
|
|
2514
|
+
connectTimeoutMs: Number.parseInt(process.env.VIGTHORIA_CHAT_CONNECT_TIMEOUT_MS || '20000', 10),
|
|
2515
|
+
idleTimeoutMs: Number.parseInt(process.env.VIGTHORIA_CHAT_IDLE_TIMEOUT_MS || '120000', 10),
|
|
2516
|
+
preferredRoute,
|
|
2517
|
+
}), 0);
|
|
1628
2518
|
}
|
|
1629
2519
|
catch (retryErr) {
|
|
1630
2520
|
// Retry also failed — synthesize an answer from evidence
|
|
@@ -1647,13 +2537,57 @@ export class ChatCommand {
|
|
|
1647
2537
|
console.log(fallbackContent);
|
|
1648
2538
|
}
|
|
1649
2539
|
this.saveSession();
|
|
1650
|
-
return;
|
|
2540
|
+
return true;
|
|
1651
2541
|
}
|
|
1652
2542
|
throw retryErr;
|
|
1653
2543
|
}
|
|
1654
2544
|
}
|
|
1655
2545
|
else {
|
|
1656
|
-
|
|
2546
|
+
const cliErr = firstErr instanceof CLIError ? firstErr : classifyError(firstErr);
|
|
2547
|
+
const formatted = formatCLIError(cliErr);
|
|
2548
|
+
if (spinner)
|
|
2549
|
+
spinner.stop();
|
|
2550
|
+
this.rememberBrainEvent('issue', 'Agent model request failed: ' + formatted, 'agent');
|
|
2551
|
+
if (turn === 0 && (cliErr.category === 'model_backend' || cliErr.category === 'network' || cliErr.category === 'timeout')) {
|
|
2552
|
+
if (!this.jsonOutput) {
|
|
2553
|
+
this.logger.error(formatted);
|
|
2554
|
+
const transport = this.api.getLastChatTransportErrors();
|
|
2555
|
+
if (transport.length > 0) {
|
|
2556
|
+
console.log(chalk.gray(`Routes tried: ${transport.slice(0, 3).join(' | ')}`));
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
else {
|
|
2560
|
+
process.exitCode = 1;
|
|
2561
|
+
console.log(JSON.stringify({
|
|
2562
|
+
success: false,
|
|
2563
|
+
mode: 'agent',
|
|
2564
|
+
model: this.currentModel,
|
|
2565
|
+
partial: false,
|
|
2566
|
+
content: '',
|
|
2567
|
+
error: formatted,
|
|
2568
|
+
errorCategory: cliErr.category,
|
|
2569
|
+
metadata: { executionPath: 'local-agent-loop' },
|
|
2570
|
+
}, null, 2));
|
|
2571
|
+
}
|
|
2572
|
+
return false;
|
|
2573
|
+
}
|
|
2574
|
+
if (this.jsonOutput) {
|
|
2575
|
+
process.exitCode = 1;
|
|
2576
|
+
console.log(JSON.stringify({
|
|
2577
|
+
success: false,
|
|
2578
|
+
mode: 'agent',
|
|
2579
|
+
model: this.currentModel,
|
|
2580
|
+
partial: false,
|
|
2581
|
+
content: '',
|
|
2582
|
+
error: formatted,
|
|
2583
|
+
errorCategory: cliErr.category,
|
|
2584
|
+
metadata: { executionPath: 'local-agent-loop' },
|
|
2585
|
+
}, null, 2));
|
|
2586
|
+
}
|
|
2587
|
+
else {
|
|
2588
|
+
this.logger.error(formatted);
|
|
2589
|
+
}
|
|
2590
|
+
return true;
|
|
1657
2591
|
}
|
|
1658
2592
|
}
|
|
1659
2593
|
if (spinner)
|
|
@@ -1663,6 +2597,11 @@ export class ChatCommand {
|
|
|
1663
2597
|
this.messages.push({ role: 'assistant', content: assistantMessage });
|
|
1664
2598
|
const toolCalls = this.extractToolCalls(assistantMessage);
|
|
1665
2599
|
const visibleText = this.stripToolPayloads(assistantMessage).trim();
|
|
2600
|
+
if (visibleText && !this.jsonOutput) {
|
|
2601
|
+
this.localAgentIterationCount += 1;
|
|
2602
|
+
console.log(chalk.cyan(`\n── Iteration ${this.localAgentIterationCount} ──`));
|
|
2603
|
+
console.log(chalk.gray(visibleText));
|
|
2604
|
+
}
|
|
1666
2605
|
getBridgeClient()?.emitModelResponse({
|
|
1667
2606
|
model: this.currentModel,
|
|
1668
2607
|
chars: assistantMessage.length,
|
|
@@ -1681,18 +2620,25 @@ export class ChatCommand {
|
|
|
1681
2620
|
// Detect resignation: model gives up saying files/things were "not found"
|
|
1682
2621
|
// without having tried list_dir to discover the correct path.
|
|
1683
2622
|
const isResignation = /(?:not found|cannot be (?:determined|compared|completed)|do not exist|does not exist|unable to locate|neither.*exist|could not (?:find|locate)|no (?:such|matching) file)/i.test(sanitized) && this.agentToolEvidence.discovery < 4;
|
|
2623
|
+
const actionablePrompt = this.lastActionableUserInput || prompt;
|
|
2624
|
+
const needsRepoWork = this.isDiagnosticPrompt(actionablePrompt) || this.isImplementationPrompt(actionablePrompt);
|
|
1684
2625
|
// Gate 1: First turn with no discovery at all
|
|
1685
|
-
const gate1 = turn === 0 && this.agentToolEvidence.discovery === 0 && (this.
|
|
2626
|
+
const gate1 = turn === 0 && this.agentToolEvidence.discovery === 0 && (needsRepoWork || this.directPromptMode || this.agentMode || isPolicyAck);
|
|
1686
2627
|
// Gate 2: Any turn where the response is just a follow-up question,
|
|
1687
2628
|
// tool-failure echoes, or premature resignation (the model gave up
|
|
1688
2629
|
// instead of retrying with list_dir to find the correct paths)
|
|
1689
|
-
const gate2 = this.directPromptMode && turn < 6 && (isPolicyAck || isFollowUp || isEmptyAfterSanitize || isResignation);
|
|
2630
|
+
const gate2 = (this.directPromptMode || this.agentMode) && turn < 6 && (isPolicyAck || isFollowUp || isEmptyAfterSanitize || isResignation);
|
|
2631
|
+
const gateNoToolsNoWork = needsRepoWork
|
|
2632
|
+
&& this.agentToolEvidence.discovery === 0
|
|
2633
|
+
&& this.agentToolEvidence.mutation === 0
|
|
2634
|
+
&& turn < maxTurns - 1
|
|
2635
|
+
&& (isEmptyAfterSanitize || isPolicyAck || isFollowUp);
|
|
1690
2636
|
// Gate 3: Model outputs code blocks as text instead of using write_file.
|
|
1691
2637
|
// If the response contains ``` code fences but no write_file was called,
|
|
1692
2638
|
// reject and instruct the model to use write_file.
|
|
1693
2639
|
const hasCodeBlocks = (sanitized.match(/```/g) || []).length >= 2;
|
|
1694
2640
|
const gate3 = hasCodeBlocks && this.agentToolEvidence.mutation === 0 && turn < 6;
|
|
1695
|
-
if (gate1 || gate2 || gate3) {
|
|
2641
|
+
if (gate1 || gate2 || gate3 || gateNoToolsNoWork) {
|
|
1696
2642
|
// Remove the useless response from history
|
|
1697
2643
|
if (isPolicyAck || isFollowUp || isEmptyAfterSanitize || isResignation || gate3) {
|
|
1698
2644
|
this.messages.pop();
|
|
@@ -1731,7 +2677,7 @@ export class ChatCommand {
|
|
|
1731
2677
|
console.log(finalContent);
|
|
1732
2678
|
}
|
|
1733
2679
|
this.saveSession();
|
|
1734
|
-
return;
|
|
2680
|
+
return true;
|
|
1735
2681
|
}
|
|
1736
2682
|
await this.executeToolCalls(toolCalls);
|
|
1737
2683
|
this.directToolContinuationCount += 1;
|
|
@@ -1765,7 +2711,7 @@ export class ChatCommand {
|
|
|
1765
2711
|
else {
|
|
1766
2712
|
this.logger.error(errorMsg);
|
|
1767
2713
|
}
|
|
1768
|
-
return;
|
|
2714
|
+
return true;
|
|
1769
2715
|
}
|
|
1770
2716
|
}
|
|
1771
2717
|
if (this.jsonOutput) {
|
|
@@ -1786,6 +2732,66 @@ export class ChatCommand {
|
|
|
1786
2732
|
console.log('Task complete.');
|
|
1787
2733
|
}
|
|
1788
2734
|
this.saveSession();
|
|
2735
|
+
return true;
|
|
2736
|
+
}
|
|
2737
|
+
async primeAgentWorkspaceDiscovery(prompt) {
|
|
2738
|
+
if (!this.tools || this.agentToolEvidence.discovery > 0) {
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
const actionablePrompt = this.buildContextualAgentPrompt(prompt);
|
|
2742
|
+
if (!this.isDiagnosticPrompt(actionablePrompt) && !this.isImplementationPrompt(actionablePrompt)) {
|
|
2743
|
+
return;
|
|
2744
|
+
}
|
|
2745
|
+
const listCall = {
|
|
2746
|
+
tool: 'list_dir',
|
|
2747
|
+
args: { path: '.' },
|
|
2748
|
+
};
|
|
2749
|
+
if (!this.jsonOutput) {
|
|
2750
|
+
console.log(chalk.cyan('⚙ Executing: list_dir → workspace root (agent bootstrap)'));
|
|
2751
|
+
}
|
|
2752
|
+
const result = await this.tools.execute(listCall);
|
|
2753
|
+
const summary = this.formatToolResult(listCall, result);
|
|
2754
|
+
if (!this.jsonOutput) {
|
|
2755
|
+
console.log(result.success ? chalk.gray(summary) : chalk.red(summary));
|
|
2756
|
+
}
|
|
2757
|
+
this.messages.push({ role: 'system', content: summary });
|
|
2758
|
+
getBridgeClient()?.emitToolResult({
|
|
2759
|
+
tool: listCall.tool,
|
|
2760
|
+
success: result.success,
|
|
2761
|
+
preview: (result.output || result.error || '').slice(0, 300),
|
|
2762
|
+
});
|
|
2763
|
+
if (result.success) {
|
|
2764
|
+
this.agentToolEvidence.discovery += 1;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
async primeBypassedTargetFileContext(prompt) {
|
|
2768
|
+
if (!this.directPromptMode || !this.tools) {
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
const targetFile = this.inferTargetFileFromPrompt(prompt);
|
|
2772
|
+
if (!targetFile) {
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
// Only prime if the direct-file flow was bypassed for this file (e.g. HTML routed to V3).
|
|
2776
|
+
// This ensures the local agent loop always has full file awareness before planning.
|
|
2777
|
+
if (!this.shouldBypassDirectSingleFileFlow(targetFile, prompt)) {
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
const readCall = {
|
|
2781
|
+
tool: 'read_file',
|
|
2782
|
+
args: { path: targetFile },
|
|
2783
|
+
};
|
|
2784
|
+
if (!this.jsonOutput) {
|
|
2785
|
+
console.log(chalk.cyan(`⚙ Executing: ${readCall.tool}`));
|
|
2786
|
+
}
|
|
2787
|
+
const readResult = await this.tools.execute(readCall);
|
|
2788
|
+
const readSummary = this.formatToolResult(readCall, readResult);
|
|
2789
|
+
if (!this.jsonOutput) {
|
|
2790
|
+
console.log(readResult.success ? chalk.gray(readSummary) : chalk.red(readSummary));
|
|
2791
|
+
}
|
|
2792
|
+
if (readResult.success && readResult.output) {
|
|
2793
|
+
this.messages.push({ role: 'system', content: readSummary });
|
|
2794
|
+
}
|
|
1789
2795
|
}
|
|
1790
2796
|
async tryDirectSingleFileFlow(prompt) {
|
|
1791
2797
|
if (!this.directPromptMode || !this.tools) {
|
|
@@ -1903,7 +2909,7 @@ export class ChatCommand {
|
|
|
1903
2909
|
}
|
|
1904
2910
|
isConfirmationFollowUp(prompt) {
|
|
1905
2911
|
const normalized = prompt.trim().toLowerCase().replace(/[.!?]+$/g, '').replace(/\s+/g, ' ');
|
|
1906
|
-
return /^(ja|ja bitte|ja bitte mach das|mach das|bitte mach das|genau|ok|okay|yes|yes please|please do|do it|go ahead|continue|proceed|make it so)$/.test(normalized);
|
|
2912
|
+
return /^(ja|ja bitte|ja bitte mach das|mach das|bitte mach das|genau|ok|okay|yes|yes please|please do|do it|go ahead|go on|continue|proceed|make it so|keep going|next)$/.test(normalized);
|
|
1907
2913
|
}
|
|
1908
2914
|
getPreviousActionablePrompt() {
|
|
1909
2915
|
if (this.lastActionableUserInput && !this.isConfirmationFollowUp(this.lastActionableUserInput)) {
|
|
@@ -1942,7 +2948,16 @@ export class ChatCommand {
|
|
|
1942
2948
|
'Do not reinterpret this confirmation as a new website, landing page, template, or index.html task.',
|
|
1943
2949
|
].join('\n');
|
|
1944
2950
|
}
|
|
1945
|
-
async tryV3AgentWorkflow(prompt) {
|
|
2951
|
+
async tryV3AgentWorkflow(prompt, options = {}) {
|
|
2952
|
+
// Extract explicit workspace path from prompt (if provided by user)
|
|
2953
|
+
let promptWorkspacePath = null;
|
|
2954
|
+
try {
|
|
2955
|
+
promptWorkspacePath = this.resolvePromptWorkspacePath(prompt, this.currentProjectPath);
|
|
2956
|
+
}
|
|
2957
|
+
catch {
|
|
2958
|
+
// Path extraction failed, use default workspace
|
|
2959
|
+
}
|
|
2960
|
+
const workspacePath = promptWorkspacePath || this.currentProjectPath;
|
|
1946
2961
|
const contextualPrompt = this.buildContextualAgentPrompt(prompt);
|
|
1947
2962
|
if (contextualPrompt === prompt && !this.isConfirmationFollowUp(prompt)) {
|
|
1948
2963
|
this.lastActionableUserInput = prompt;
|
|
@@ -1950,11 +2965,28 @@ export class ChatCommand {
|
|
|
1950
2965
|
this.messages.push({ role: 'user', content: contextualPrompt });
|
|
1951
2966
|
const runtimeContext = await this.getPromptRuntimeContext(contextualPrompt);
|
|
1952
2967
|
const routingPolicy = this.resolveAgentExecutionPolicy(contextualPrompt);
|
|
2968
|
+
// STREAMING: Log routing decision transparently to user
|
|
2969
|
+
if (!this.jsonOutput) {
|
|
2970
|
+
console.log();
|
|
2971
|
+
console.log(chalk.gray('━━━ ROUTING DECISION ━━━'));
|
|
2972
|
+
console.log(chalk.gray(`Reason: ${routingPolicy.routeReason}`));
|
|
2973
|
+
console.log(chalk.gray(`Model: ${routingPolicy.selectedModel}`));
|
|
2974
|
+
console.log(chalk.gray(`Cloud Eligible: ${routingPolicy.cloudEligible}`));
|
|
2975
|
+
console.log(chalk.gray(`Cloud Selected: ${routingPolicy.cloudSelected}`));
|
|
2976
|
+
if (routingPolicy.heavyTask) {
|
|
2977
|
+
console.log(chalk.gray(`Task Complexity: HEAVY`));
|
|
2978
|
+
}
|
|
2979
|
+
console.log(chalk.gray('━'.repeat(30)));
|
|
2980
|
+
console.log();
|
|
2981
|
+
}
|
|
1953
2982
|
// Reset streaming counters for new workflow
|
|
1954
2983
|
this.v3IterationCount = 0;
|
|
1955
2984
|
this.v3ToolCallCount = 0;
|
|
1956
2985
|
this.v3LastActivity = Date.now();
|
|
1957
2986
|
this.v3StreamingStarted = false;
|
|
2987
|
+
this.v3StreamedTextBuffer = '';
|
|
2988
|
+
this.v3LiveToolEvidence = [];
|
|
2989
|
+
this.v3PendingToolCalls = [];
|
|
1958
2990
|
const taskDisplay = new TaskDisplay(['Analyse workspace', 'Execute tasks', 'Validate output', 'Self-heal'], !this.jsonOutput);
|
|
1959
2991
|
taskDisplay.start(0);
|
|
1960
2992
|
const spinner = this.jsonOutput ? null : createSpinner({
|
|
@@ -2018,25 +3050,59 @@ export class ChatCommand {
|
|
|
2018
3050
|
const executionPrompt = this.buildExecutionPrompt(contextualPrompt);
|
|
2019
3051
|
const agentTaskType = this.inferAgentTaskType(contextualPrompt);
|
|
2020
3052
|
const workspaceContext = {
|
|
2021
|
-
workspacePath:
|
|
2022
|
-
projectPath:
|
|
2023
|
-
targetPath:
|
|
3053
|
+
workspacePath: workspacePath,
|
|
3054
|
+
projectPath: workspacePath,
|
|
3055
|
+
targetPath: workspacePath,
|
|
2024
3056
|
...runtimeContext,
|
|
2025
3057
|
};
|
|
3058
|
+
if (!this.jsonOutput && !this.directPromptMode) {
|
|
3059
|
+
try {
|
|
3060
|
+
const snapshot = this.api.getAgentWorkspaceSnapshot(workspacePath);
|
|
3061
|
+
console.log(chalk.gray(`Workspace sync: ${snapshot.fileCount} files indexed from ${workspacePath}`));
|
|
3062
|
+
if (snapshot.paths.length > 0) {
|
|
3063
|
+
console.log(chalk.gray(`Workspace index: ${snapshot.paths.slice(0, 5).map((filePath) => filePath.replace(/\\/g, '/')).join(', ')}${snapshot.paths.length > 5 ? ', ...' : ''}`));
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
catch {
|
|
3067
|
+
console.log(chalk.gray(`Workspace sync: preparing ${workspacePath}`));
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
2026
3070
|
// Start workspace watcher for bidirectional real-time sync
|
|
2027
3071
|
let watcher = null;
|
|
2028
|
-
|
|
3072
|
+
let workspaceWs = null;
|
|
3073
|
+
const stopWorkspaceSync = () => {
|
|
3074
|
+
watcher?.stop();
|
|
3075
|
+
if (workspaceWs) {
|
|
3076
|
+
workspaceWs.disconnect();
|
|
3077
|
+
workspaceWs = null;
|
|
3078
|
+
}
|
|
3079
|
+
};
|
|
3080
|
+
if (this.shouldStartWorkspaceWatcher(workspacePath)) {
|
|
2029
3081
|
watcher = new WorkspaceWatcher({
|
|
2030
|
-
workspaceRoot:
|
|
3082
|
+
workspaceRoot: workspacePath,
|
|
2031
3083
|
onFileChange: (relativePath, content, action) => {
|
|
2032
3084
|
this.logger.debug(`Local change detected: ${action} ${relativePath}`);
|
|
3085
|
+
workspaceWs?.syncFile(relativePath, content, action);
|
|
2033
3086
|
},
|
|
2034
3087
|
});
|
|
2035
3088
|
watcher.start();
|
|
2036
3089
|
}
|
|
2037
3090
|
try {
|
|
3091
|
+
// Ensure the V3 service key is available before health check
|
|
3092
|
+
await this.api.ensureV3ServiceKey();
|
|
3093
|
+
if (spinner)
|
|
3094
|
+
this.updateV3AgentSpinner(spinner, { type: 'v3_status', content: 'Checking V3 connection (preflight)...' });
|
|
3095
|
+
const healthCheck = await this.api.runV3HealthCheck({ soft: true });
|
|
3096
|
+
if (!healthCheck.healthy) {
|
|
3097
|
+
this.logger.warn('V3 health probe did not confirm readiness; starting SSE agent stream anyway.');
|
|
3098
|
+
if (spinner)
|
|
3099
|
+
this.updateV3AgentSpinner(spinner, { type: 'v3_status', content: 'Starting V3 agent stream...' });
|
|
3100
|
+
}
|
|
3101
|
+
else if (spinner) {
|
|
3102
|
+
this.updateV3AgentSpinner(spinner, { type: 'v3_status', content: 'Connected to V3. Starting agent execution...' });
|
|
3103
|
+
}
|
|
2038
3104
|
const workflowPromise = this.api.runV3AgentWorkflow(executionPrompt, {
|
|
2039
|
-
workspace: { path:
|
|
3105
|
+
workspace: { path: workspacePath },
|
|
2040
3106
|
...workspaceContext,
|
|
2041
3107
|
agentTaskType,
|
|
2042
3108
|
executionSurface: 'cli',
|
|
@@ -2052,6 +3118,32 @@ export class ChatCommand {
|
|
|
2052
3118
|
rawPrompt: prompt,
|
|
2053
3119
|
contextualPrompt,
|
|
2054
3120
|
history: this.getMessagesForModel(),
|
|
3121
|
+
clientToolExecution: true,
|
|
3122
|
+
liveToolEvidence: this.v3LiveToolEvidence,
|
|
3123
|
+
onClientToolExecute: (event) => this.executeClientV3Tool(event),
|
|
3124
|
+
onWorkspaceContext: async ({ contextId, serverWorkspaceRoot }) => {
|
|
3125
|
+
if (workspaceWs || !contextId || !serverWorkspaceRoot || !this.shouldStartWorkspaceWatcher(workspacePath)) {
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
const headers = await this.api.getV3AgentHeaders();
|
|
3129
|
+
const token = String(headers.Authorization?.replace(/^Bearer\s+/i, '')
|
|
3130
|
+
|| headers['X-V3-Service-Key']
|
|
3131
|
+
|| headers['x-v3-service-key']
|
|
3132
|
+
|| '').trim();
|
|
3133
|
+
if (!token) {
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
const baseUrl = this.api.getV3AgentBaseUrls(false)[0] || 'https://coder.vigthoria.io';
|
|
3137
|
+
const serverUrl = baseUrl.replace(/^http/i, 'ws');
|
|
3138
|
+
workspaceWs = new WorkspaceWSClient({
|
|
3139
|
+
serverUrl,
|
|
3140
|
+
token,
|
|
3141
|
+
contextId,
|
|
3142
|
+
workspaceRoot: serverWorkspaceRoot,
|
|
3143
|
+
});
|
|
3144
|
+
workspaceWs.connect();
|
|
3145
|
+
this.logger.debug(`Workspace WS sync bound to context ${contextId}`);
|
|
3146
|
+
},
|
|
2055
3147
|
...runtimeContext,
|
|
2056
3148
|
onStreamEvent: (event) => {
|
|
2057
3149
|
if (event.type === 'plan') {
|
|
@@ -2111,6 +3203,34 @@ export class ChatCommand {
|
|
|
2111
3203
|
if (this.v3StreamingStarted) {
|
|
2112
3204
|
process.stdout.write('\n');
|
|
2113
3205
|
}
|
|
3206
|
+
let finalContent = String(response.content || '').trim();
|
|
3207
|
+
const needsUserReport = this.inferAgentTaskType(prompt) === 'analysis'
|
|
3208
|
+
|| this.isThinV3Summary(finalContent)
|
|
3209
|
+
|| this.v3LiveToolEvidence.length > 0;
|
|
3210
|
+
if (needsUserReport) {
|
|
3211
|
+
const userReport = this.buildUserFacingV3RunReport(prompt, workspacePath, {
|
|
3212
|
+
partial: response.partial === true || this.isThinV3Summary(finalContent),
|
|
3213
|
+
serverNote: finalContent && !this.isThinV3Summary(finalContent) ? finalContent : undefined,
|
|
3214
|
+
});
|
|
3215
|
+
if (userReport && !this.isThinV3Summary(userReport)) {
|
|
3216
|
+
finalContent = userReport;
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
else if (this.isThinV3Summary(finalContent) && this.v3LiveToolEvidence.length > 0) {
|
|
3220
|
+
const evidenceSummary = this.api.formatV3AgentResponse({
|
|
3221
|
+
events: [],
|
|
3222
|
+
liveToolEvidence: this.v3LiveToolEvidence,
|
|
3223
|
+
});
|
|
3224
|
+
if (evidenceSummary && !this.isThinV3Summary(evidenceSummary)) {
|
|
3225
|
+
finalContent = evidenceSummary;
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
response.content = finalContent || response.content;
|
|
3229
|
+
let v3UserReportPrinted = false;
|
|
3230
|
+
if (!this.jsonOutput && finalContent && needsUserReport) {
|
|
3231
|
+
this.printV3UserReport(finalContent);
|
|
3232
|
+
v3UserReportPrinted = true;
|
|
3233
|
+
}
|
|
2114
3234
|
const previewGate = (response.metadata?.previewGate || null);
|
|
2115
3235
|
const workspaceHasOutput = this.api.hasAgentWorkspaceOutput(workspaceContext);
|
|
2116
3236
|
const success = previewGate?.required === true
|
|
@@ -2123,7 +3243,7 @@ export class ChatCommand {
|
|
|
2123
3243
|
}
|
|
2124
3244
|
this.logger.warn('Falling back to legacy CLI agent loop');
|
|
2125
3245
|
this.logger.debug(`V3 agent workflow returned an incomplete result: ${previewGate?.error || 'workspace changes were not fully validated'}`);
|
|
2126
|
-
|
|
3246
|
+
stopWorkspaceSync();
|
|
2127
3247
|
return false;
|
|
2128
3248
|
}
|
|
2129
3249
|
const errorMessage = `V3 agent workflow returned an incomplete result and legacy fallback is disabled. ${previewGate?.error || 'Workspace changes were not fully validated.'}`;
|
|
@@ -2144,7 +3264,7 @@ export class ChatCommand {
|
|
|
2144
3264
|
metadata: { executionPath: 'v3-agent', previewGate },
|
|
2145
3265
|
}, null, 2));
|
|
2146
3266
|
}
|
|
2147
|
-
|
|
3267
|
+
stopWorkspaceSync();
|
|
2148
3268
|
return true;
|
|
2149
3269
|
}
|
|
2150
3270
|
if (!this.jsonOutput && previewGate?.required && previewGate?.passed !== true && workspaceHasOutput) {
|
|
@@ -2164,16 +3284,32 @@ export class ChatCommand {
|
|
|
2164
3284
|
}, null, 2));
|
|
2165
3285
|
}
|
|
2166
3286
|
else if (this.v3StreamingStarted) {
|
|
2167
|
-
// Content
|
|
2168
|
-
|
|
3287
|
+
// Content may have been streamed already, but some V3 runs only stream
|
|
3288
|
+
// status/tool messages and keep the useful final report in response.content.
|
|
3289
|
+
if (!v3UserReportPrinted && !this.jsonOutput) {
|
|
2169
3290
|
console.log(chalk.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
|
|
2170
3291
|
}
|
|
3292
|
+
if (!v3UserReportPrinted && response.content && this.shouldPrintV3FinalContent(response.content)) {
|
|
3293
|
+
console.log(response.content);
|
|
3294
|
+
}
|
|
3295
|
+
else if (!v3UserReportPrinted && response.content && this.isThinV3Summary(response.content)) {
|
|
3296
|
+
console.log(chalk.yellow('V3 agent finished after reading local files, but no final narrative summary was emitted.'));
|
|
3297
|
+
console.log(chalk.gray('Use /continue and ask for a written overview.'));
|
|
3298
|
+
}
|
|
2171
3299
|
}
|
|
2172
3300
|
else if (response.content) {
|
|
2173
|
-
if (!this.directPromptMode) {
|
|
3301
|
+
if (!v3UserReportPrinted && !this.directPromptMode) {
|
|
2174
3302
|
console.log(chalk.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
|
|
2175
3303
|
}
|
|
2176
|
-
|
|
3304
|
+
if (!v3UserReportPrinted && this.shouldPrintV3FinalContent(response.content)) {
|
|
3305
|
+
console.log(response.content);
|
|
3306
|
+
}
|
|
3307
|
+
else if (!v3UserReportPrinted && this.isThinV3Summary(response.content)) {
|
|
3308
|
+
console.log('V3 agent workflow completed, but no final task summary was emitted.');
|
|
3309
|
+
}
|
|
3310
|
+
else if (!v3UserReportPrinted) {
|
|
3311
|
+
console.log(response.content);
|
|
3312
|
+
}
|
|
2177
3313
|
}
|
|
2178
3314
|
else {
|
|
2179
3315
|
if (!this.directPromptMode) {
|
|
@@ -2285,11 +3421,11 @@ export class ChatCommand {
|
|
|
2285
3421
|
this.printAgentRunSummary(this.lastAgentRunOutcome, executorSucceeded, changedFileCount);
|
|
2286
3422
|
}
|
|
2287
3423
|
this.messages.push({ role: 'assistant', content: response.content || 'V3 agent workflow completed.' });
|
|
2288
|
-
|
|
3424
|
+
stopWorkspaceSync();
|
|
2289
3425
|
return true;
|
|
2290
3426
|
}
|
|
2291
3427
|
catch (error) {
|
|
2292
|
-
|
|
3428
|
+
stopWorkspaceSync();
|
|
2293
3429
|
if (!this.api.hasAgentWorkspaceOutput(workspaceContext)) {
|
|
2294
3430
|
const recovered = await this.tryRecoverV3ServiceAndRetry(executionPrompt, prompt, workspaceContext, routingPolicy, spinner, error);
|
|
2295
3431
|
if (recovered) {
|
|
@@ -2491,6 +3627,14 @@ export class ChatCommand {
|
|
|
2491
3627
|
this.showProjectMemory();
|
|
2492
3628
|
continue;
|
|
2493
3629
|
}
|
|
3630
|
+
if (trimmed === '/index') {
|
|
3631
|
+
await this.reindexWorkspaceBrain();
|
|
3632
|
+
continue;
|
|
3633
|
+
}
|
|
3634
|
+
if (trimmed === '/brain') {
|
|
3635
|
+
this.showBrainIndexStatus();
|
|
3636
|
+
continue;
|
|
3637
|
+
}
|
|
2494
3638
|
if (trimmed === '/compact') {
|
|
2495
3639
|
this.compactCurrentSession();
|
|
2496
3640
|
continue;
|
|
@@ -2592,9 +3736,15 @@ export class ChatCommand {
|
|
|
2592
3736
|
this.syncInteractiveModeModel('agent');
|
|
2593
3737
|
console.log(chalk.gray('Agent mode re-enabled for continuation.'));
|
|
2594
3738
|
}
|
|
2595
|
-
await this.runAgentTurn(followUp);
|
|
3739
|
+
await this.runAgentTurn(followUp, { skipLocalLoop: true, forceV3: true });
|
|
2596
3740
|
continue;
|
|
2597
3741
|
}
|
|
3742
|
+
if (/^(?:\/logout|logout)$/i.test(trimmed)) {
|
|
3743
|
+
const { logout } = await import('./auth.js');
|
|
3744
|
+
await logout();
|
|
3745
|
+
console.log(chalk.gray('Session ended.'));
|
|
3746
|
+
break;
|
|
3747
|
+
}
|
|
2598
3748
|
if (trimmed === '/save') {
|
|
2599
3749
|
this.saveSession();
|
|
2600
3750
|
console.log(chalk.green('Session saved.'));
|
|
@@ -2633,6 +3783,8 @@ export class ChatCommand {
|
|
|
2633
3783
|
console.log(' /operator Toggle BMAD operator mode');
|
|
2634
3784
|
console.log(' /context Show current session and project memory');
|
|
2635
3785
|
console.log(' /memory Show Vigthoria project brain status');
|
|
3786
|
+
console.log(' /brain Show workspace codebase index status');
|
|
3787
|
+
console.log(' /index Re-index workspace and sync to Brain Hub');
|
|
2636
3788
|
console.log(' /compact Compact current session into memory summary');
|
|
2637
3789
|
console.log(' /clear Clear conversation');
|
|
2638
3790
|
console.log(' /save Save session');
|
|
@@ -2681,6 +3833,9 @@ export class ChatCommand {
|
|
|
2681
3833
|
console.log(chalk.gray(' Pending: ') + chalk.yellow(unfinishedList.join(', ')) + more);
|
|
2682
3834
|
}
|
|
2683
3835
|
}
|
|
3836
|
+
else if (!executorSucceeded) {
|
|
3837
|
+
console.log(chalk.gray(' The detailed overview is printed above when available.'));
|
|
3838
|
+
}
|
|
2684
3839
|
if (typeof outcome.qualityScore === 'number') {
|
|
2685
3840
|
const score = outcome.qualityScore.toFixed(1);
|
|
2686
3841
|
const colour = outcome.qualityScore >= 70 ? chalk.green : outcome.qualityScore >= 30 ? chalk.yellow : chalk.red;
|
|
@@ -2769,7 +3924,10 @@ export class ChatCommand {
|
|
|
2769
3924
|
const missingLine = o.qualityMissing.length > 0
|
|
2770
3925
|
? `\nMissing pieces: ${o.qualityMissing.slice(0, 6).join(', ')}.`
|
|
2771
3926
|
: '';
|
|
2772
|
-
return `Continue the previous agent run from the current workspace state without re-doing already-completed work.${taskList}${blockerLine}${missingLine}
|
|
3927
|
+
return `Continue the previous agent run from the current workspace state without re-doing already-completed work.${taskList}${blockerLine}${missingLine}
|
|
3928
|
+
Original request was: ${o.prompt}
|
|
3929
|
+
|
|
3930
|
+
Now implement the missing fixes by editing the local workspace files. Use write/edit tools to apply the required code and HTML changes so the project runs.`;
|
|
2773
3931
|
}
|
|
2774
3932
|
/**
|
|
2775
3933
|
* Re-print the last agent run summary, or guide the user when there isn't one.
|
|
@@ -2857,6 +4015,7 @@ export class ChatCommand {
|
|
|
2857
4015
|
});
|
|
2858
4016
|
}
|
|
2859
4017
|
buildAgentSystemPrompt() {
|
|
4018
|
+
const runtime = this.getRuntimeEnvironmentContext();
|
|
2860
4019
|
const toolCatalog = AgenticTools.getToolDefinitions()
|
|
2861
4020
|
.map((tool) => {
|
|
2862
4021
|
const params = tool.parameters
|
|
@@ -2868,6 +4027,7 @@ export class ChatCommand {
|
|
|
2868
4027
|
return [
|
|
2869
4028
|
'Vigthoria CLI agent operating contract.',
|
|
2870
4029
|
`You are operating inside the project root: ${this.currentProjectPath}`,
|
|
4030
|
+
`Execution platform: ${runtime.platform} (${runtime.osPlatform}). Machine scope: ${runtime.machineScope}.`,
|
|
2871
4031
|
`You are operating on the user's LOCAL machine. This CLI is not a server-only runtime.`,
|
|
2872
4032
|
`All file reads/writes and command execution must target the local project/workspace path unless the user explicitly requests remote/server execution.`,
|
|
2873
4033
|
'For command execution: ask for confirmation before risky commands; never redirect normal local work to server paths.',
|
|
@@ -2900,10 +4060,12 @@ export class ChatCommand {
|
|
|
2900
4060
|
].join('\n');
|
|
2901
4061
|
}
|
|
2902
4062
|
buildScopedUserPrompt(prompt) {
|
|
4063
|
+
const runtime = this.getRuntimeEnvironmentContext();
|
|
2903
4064
|
return [
|
|
2904
4065
|
this.buildExecutionPrompt(prompt),
|
|
2905
4066
|
'',
|
|
2906
4067
|
`Project root: ${this.currentProjectPath}`,
|
|
4068
|
+
`Execution platform: ${runtime.platform}. Machine scope: ${runtime.machineScope}.`,
|
|
2907
4069
|
'Stay within this project root unless the user explicitly expands scope.',
|
|
2908
4070
|
'Finish the request and stop once it is complete.',
|
|
2909
4071
|
].join('\n');
|
|
@@ -2965,7 +4127,14 @@ export class ChatCommand {
|
|
|
2965
4127
|
return true;
|
|
2966
4128
|
}
|
|
2967
4129
|
const extension = path.extname(targetFile).toLowerCase();
|
|
2968
|
-
|
|
4130
|
+
if (extension === '')
|
|
4131
|
+
return true;
|
|
4132
|
+
// HTML/HTM files need V3 agent with the 35B model for quality rewrites (canvas,
|
|
4133
|
+
// styling, interactivity). Route them to tryV3AgentWorkflow instead of the
|
|
4134
|
+
// lightweight cloud-chat direct-file path.
|
|
4135
|
+
if (extension === '.html' || extension === '.htm')
|
|
4136
|
+
return true;
|
|
4137
|
+
return false;
|
|
2969
4138
|
}
|
|
2970
4139
|
shouldPreferLocalAgentLoop(prompt) {
|
|
2971
4140
|
if (this.shouldRequireV3AgentWorkflow(prompt)) {
|
|
@@ -2975,16 +4144,44 @@ export class ChatCommand {
|
|
|
2975
4144
|
if (forceV3) {
|
|
2976
4145
|
return false;
|
|
2977
4146
|
}
|
|
4147
|
+
if (this.isBrowserTaskPrompt(prompt)) {
|
|
4148
|
+
return false;
|
|
4149
|
+
}
|
|
2978
4150
|
const runtime = this.getRuntimeEnvironmentContext();
|
|
2979
|
-
|
|
4151
|
+
const forceRemoteAgent = /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_PREFER_V3_AGENT_LOOP
|
|
4152
|
+
|| process.env.VIGTHORIA_FORCE_REMOTE_AGENT
|
|
4153
|
+
|| ''));
|
|
4154
|
+
// CLI sessions on a user's local machine must execute tools locally.
|
|
4155
|
+
// Remote V3 only sees a partial hydrated copy and cannot reach real paths.
|
|
4156
|
+
if (runtime.machineScope === 'local-machine') {
|
|
4157
|
+
if (forceRemoteAgent) {
|
|
4158
|
+
return false;
|
|
4159
|
+
}
|
|
4160
|
+
if (this.isImplementationPrompt(prompt)) {
|
|
4161
|
+
return true;
|
|
4162
|
+
}
|
|
4163
|
+
const preferLocalOptIn = /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_PREFER_LOCAL_AGENT_LOOP || ''));
|
|
4164
|
+
// Read-only analysis on a Windows/macOS/Linux desktop is faster and more
|
|
4165
|
+
// reliable through V3 + client-side read tools than a blocking chat route.
|
|
4166
|
+
if (this.isAnalysisLookupPrompt(prompt) && !preferLocalOptIn) {
|
|
4167
|
+
return false;
|
|
4168
|
+
}
|
|
4169
|
+
return true;
|
|
4170
|
+
}
|
|
4171
|
+
// Server-bindable workspaces keep V3 as the default execution path.
|
|
4172
|
+
const preferLocalOptIn = /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_PREFER_LOCAL_AGENT_LOOP || ''));
|
|
4173
|
+
if (!preferLocalOptIn) {
|
|
4174
|
+
return false;
|
|
4175
|
+
}
|
|
4176
|
+
if (this.directPromptMode && this.isRepoGroundedPrompt(prompt)) {
|
|
4177
|
+
return false;
|
|
4178
|
+
}
|
|
2980
4179
|
if (!runtime.serverBindableWorkspace) {
|
|
2981
4180
|
return true;
|
|
2982
4181
|
}
|
|
2983
|
-
// Interactive sessions should prioritize local edits unless V3 is explicitly forced.
|
|
2984
4182
|
if (!this.directPromptMode) {
|
|
2985
4183
|
return true;
|
|
2986
4184
|
}
|
|
2987
|
-
// For direct prompts on server-bindable Linux roots, keep V3-first behavior.
|
|
2988
4185
|
return runtime.platform !== 'linux';
|
|
2989
4186
|
}
|
|
2990
4187
|
getRuntimeEnvironmentContext() {
|
|
@@ -3026,7 +4223,7 @@ export class ChatCommand {
|
|
|
3026
4223
|
if (!projectPath || !path.isAbsolute(projectPath) || !fs.existsSync(projectPath)) {
|
|
3027
4224
|
return false;
|
|
3028
4225
|
}
|
|
3029
|
-
const configuredRoots = (process.env.VIGTHORIA_SERVER_WORKSPACE_ROOTS || '/var/www/vigthoria-user-workspaces,/var/lib/vigthoria-workspaces')
|
|
4226
|
+
const configuredRoots = (process.env.VIGTHORIA_SERVER_WORKSPACE_ROOTS || '/var/www,/var/www/vigthoria-user-workspaces,/var/lib/vigthoria-workspaces')
|
|
3030
4227
|
.split(',')
|
|
3031
4228
|
.map((entry) => entry.trim())
|
|
3032
4229
|
.filter(Boolean);
|
|
@@ -3316,6 +4513,15 @@ export class ChatCommand {
|
|
|
3316
4513
|
if (fallback) {
|
|
3317
4514
|
return fallback;
|
|
3318
4515
|
}
|
|
4516
|
+
if (this.agentToolEvidence.discovery === 0 && this.agentToolEvidence.mutation === 0) {
|
|
4517
|
+
if (this.isImplementationPrompt(prompt) || this.isDiagnosticPrompt(prompt)) {
|
|
4518
|
+
return [
|
|
4519
|
+
'The agent did not inspect the workspace or run any tools before finishing.',
|
|
4520
|
+
'This usually means the model backend returned an empty response.',
|
|
4521
|
+
'Try the request again. If it keeps happening, check Vigthoria Coder / vLLM logs on the server.',
|
|
4522
|
+
].join(' ');
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
3319
4525
|
return sanitized || 'Task complete.';
|
|
3320
4526
|
}
|
|
3321
4527
|
/**
|
|
@@ -3325,7 +4531,11 @@ export class ChatCommand {
|
|
|
3325
4531
|
* substantive answer.
|
|
3326
4532
|
*/
|
|
3327
4533
|
sanitizeDirectModeOutput(text) {
|
|
3328
|
-
let cleaned = text;
|
|
4534
|
+
let cleaned = this.stripHiddenThoughtBlocks(text);
|
|
4535
|
+
cleaned = cleaned
|
|
4536
|
+
.replace(/<\|mask_start\|>[\s\S]*?<\|mask_end\|>/g, '')
|
|
4537
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, '')
|
|
4538
|
+
.replace(/<\|(?:mask_start|mask_end)\|>/g, '');
|
|
3329
4539
|
// ── Phase 1: Strip entire tool-output blocks ──
|
|
3330
4540
|
// Matches "Tool <name> succeeded/FAILED." through the next blank line,
|
|
3331
4541
|
// next tool header, or end-of-string. The DOTALL-like [\s\S]*? is
|
|
@@ -3398,7 +4608,7 @@ export class ChatCommand {
|
|
|
3398
4608
|
cleaned = kept.join('\n\n');
|
|
3399
4609
|
// Collapse multiple blank lines
|
|
3400
4610
|
cleaned = cleaned.replace(/\n{3,}/g, '\n\n').trim();
|
|
3401
|
-
return cleaned || text;
|
|
4611
|
+
return cleaned || this.stripHiddenThoughtBlocks(text);
|
|
3402
4612
|
}
|
|
3403
4613
|
isDirectModeFollowUpQuestion(text) {
|
|
3404
4614
|
return /^(would you like me|do you want me|which aspect|what aspect|can you clarify|could you clarify|should i focus on|i will follow|i understand|i('ll| will) adhere|provide your|waiting for)/i.test(text.trim());
|
|
@@ -3480,6 +4690,57 @@ export class ChatCommand {
|
|
|
3480
4690
|
.trim();
|
|
3481
4691
|
return normalized || null;
|
|
3482
4692
|
}
|
|
4693
|
+
async tryLocalToolFallback(call, result) {
|
|
4694
|
+
if (result.success || !this.tools) {
|
|
4695
|
+
return result;
|
|
4696
|
+
}
|
|
4697
|
+
const runtime = this.getRuntimeEnvironmentContext();
|
|
4698
|
+
if (runtime.machineScope !== 'local-machine') {
|
|
4699
|
+
return result;
|
|
4700
|
+
}
|
|
4701
|
+
if (call.tool === 'read_file' && call.args.path) {
|
|
4702
|
+
const parent = path.dirname(call.args.path);
|
|
4703
|
+
const listPath = parent === '.' || parent === '' ? '.' : parent;
|
|
4704
|
+
const listResult = await this.tools.execute({
|
|
4705
|
+
tool: 'list_dir',
|
|
4706
|
+
args: { path: listPath },
|
|
4707
|
+
});
|
|
4708
|
+
if (listResult.success && listResult.output) {
|
|
4709
|
+
return {
|
|
4710
|
+
...result,
|
|
4711
|
+
suggestion: `File not found at "${call.args.path}". Parent directory "${listPath}" contains:\n${listResult.output}`,
|
|
4712
|
+
};
|
|
4713
|
+
}
|
|
4714
|
+
const baseName = path.basename(call.args.path);
|
|
4715
|
+
const globResult = await this.tools.execute({
|
|
4716
|
+
tool: 'glob',
|
|
4717
|
+
args: { pattern: `**/${baseName}` },
|
|
4718
|
+
});
|
|
4719
|
+
if (globResult.success && globResult.output?.trim()) {
|
|
4720
|
+
return {
|
|
4721
|
+
...result,
|
|
4722
|
+
suggestion: `File not found at "${call.args.path}". Matching paths:\n${globResult.output}`,
|
|
4723
|
+
};
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
if (call.tool === 'list_dir') {
|
|
4727
|
+
const listPath = call.args.path || '.';
|
|
4728
|
+
const command = runtime.platform === 'windows'
|
|
4729
|
+
? `dir /b "${path.join(this.currentProjectPath, listPath)}"`
|
|
4730
|
+
: `ls -la "${path.join(this.currentProjectPath, listPath)}"`;
|
|
4731
|
+
const shellResult = await this.tools.execute({
|
|
4732
|
+
tool: 'bash',
|
|
4733
|
+
args: { command, cwd: this.currentProjectPath },
|
|
4734
|
+
});
|
|
4735
|
+
if (shellResult.success && shellResult.output) {
|
|
4736
|
+
return {
|
|
4737
|
+
...result,
|
|
4738
|
+
suggestion: `list_dir failed for "${listPath}". Terminal listing:\n${shellResult.output}`,
|
|
4739
|
+
};
|
|
4740
|
+
}
|
|
4741
|
+
}
|
|
4742
|
+
return result;
|
|
4743
|
+
}
|
|
3483
4744
|
async executeToolCalls(toolCalls) {
|
|
3484
4745
|
if (!this.tools) {
|
|
3485
4746
|
throw new Error('Agent tools are not initialized.');
|
|
@@ -3489,10 +4750,15 @@ export class ChatCommand {
|
|
|
3489
4750
|
const verbose = !this.jsonOutput;
|
|
3490
4751
|
for (const call of toolCalls) {
|
|
3491
4752
|
if (verbose) {
|
|
3492
|
-
|
|
4753
|
+
const target = call.args.path || call.args.pattern || call.args.command || '';
|
|
4754
|
+
const detail = target ? chalk.gray(` → ${String(target).replace(/\\/g, '/')}`) : '';
|
|
4755
|
+
console.log(chalk.cyan(`⚙ Executing: ${call.tool}`) + detail);
|
|
3493
4756
|
}
|
|
3494
4757
|
getBridgeClient()?.emitToolCall({ tool: call.tool, args: call.args });
|
|
3495
4758
|
let result = await this.tools.execute(call);
|
|
4759
|
+
if (!result.success) {
|
|
4760
|
+
result = await this.tryLocalToolFallback(call, result);
|
|
4761
|
+
}
|
|
3496
4762
|
// Phase 2: If a search tool failed (search_failed), retry with alternate approach
|
|
3497
4763
|
const searchStatus = result.metadata?.searchStatus;
|
|
3498
4764
|
if (call.tool === 'grep' && searchStatus === 'search_failed') {
|