vigthoria-cli 1.8.19 → 1.9.5

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.
@@ -38,6 +38,15 @@ export declare class ChatCommand {
38
38
  private workflowTarget;
39
39
  private savePlanToVigFlow;
40
40
  private jsonOutput;
41
+ private modelGovernanceFallback;
42
+ private isJwtExpirationError;
43
+ private isNetworkError;
44
+ private isTimeoutError;
45
+ private toUserFacingApiError;
46
+ private handleApiError;
47
+ private callApi;
48
+ private callSyncApi;
49
+ private applyNoAgentGovernance;
41
50
  private syncInteractiveModeModel;
42
51
  private hasOperatorAccess;
43
52
  private operatorAccessMessage;
@@ -81,6 +90,9 @@ export declare class ChatCommand {
81
90
  */
82
91
  private sanitizeServerPath;
83
92
  private describeV3AgentTool;
93
+ private isRawV3StreamPayload;
94
+ private consumeV3StreamPayload;
95
+ private writeV3StreamText;
84
96
  private updateV3AgentSpinner;
85
97
  private updateOperatorSpinner;
86
98
  constructor(config: Config, logger: Logger);
@@ -48,6 +48,7 @@ const tools_js_1 = require("../utils/tools.js");
48
48
  const session_js_1 = require("../utils/session.js");
49
49
  const bridge_client_js_1 = require("../utils/bridge-client.js");
50
50
  const workspace_stream_js_1 = require("../utils/workspace-stream.js");
51
+ const task_display_js_1 = require("../utils/task-display.js");
51
52
  const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
52
53
  const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
53
54
  if (!rawValue) {
@@ -65,12 +66,9 @@ const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
65
66
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
66
67
  })();
67
68
  const DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS = (() => {
68
- const rawValue = process.env.VIGTHORIA_AGENT_SOFT_TIMEOUT_MS || process.env.V3_AGENT_SOFT_TIMEOUT_MS;
69
- if (!rawValue) {
70
- return 0;
71
- }
69
+ const rawValue = process.env.VIGTHORIA_AGENT_SOFT_TIMEOUT_MS || process.env.V3_AGENT_SOFT_TIMEOUT_MS || '300000';
72
70
  const parsed = Number.parseInt(rawValue, 10);
73
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
71
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 300000;
74
72
  })();
75
73
  class ChatCommand {
76
74
  config;
@@ -94,6 +92,140 @@ class ChatCommand {
94
92
  workflowTarget = null;
95
93
  savePlanToVigFlow = false;
96
94
  jsonOutput = false;
95
+ modelGovernanceFallback = null;
96
+ isJwtExpirationError(error) {
97
+ const candidate = error;
98
+ const message = `${candidate?.message || ''} ${typeof candidate?.details === 'string' ? candidate.details : ''}`.toLowerCase();
99
+ const code = String(candidate?.code || '').toLowerCase();
100
+ return candidate?.status === 401 && (code.includes('token_expired') ||
101
+ code.includes('jwt_expired') ||
102
+ message.includes('jwt expired') ||
103
+ message.includes('token expired') ||
104
+ message.includes('expired token') ||
105
+ message.includes('session expired'));
106
+ }
107
+ isNetworkError(error) {
108
+ const candidate = error;
109
+ const code = String(candidate?.code || candidate?.cause?.code || '').toUpperCase();
110
+ const name = String(candidate?.name || candidate?.cause?.name || '').toLowerCase();
111
+ const message = String(candidate?.message || candidate?.cause?.message || '').toLowerCase();
112
+ return [
113
+ 'ECONNRESET',
114
+ 'ECONNREFUSED',
115
+ 'EHOSTUNREACH',
116
+ 'ENETUNREACH',
117
+ 'ENOTFOUND',
118
+ 'EAI_AGAIN',
119
+ 'UND_ERR_CONNECT_TIMEOUT',
120
+ 'UND_ERR_HEADERS_TIMEOUT',
121
+ 'UND_ERR_BODY_TIMEOUT',
122
+ ].includes(code) ||
123
+ name.includes('fetcherror') ||
124
+ message.includes('network') ||
125
+ message.includes('fetch failed') ||
126
+ message.includes('socket') ||
127
+ message.includes('connection');
128
+ }
129
+ isTimeoutError(error) {
130
+ const candidate = error;
131
+ const code = String(candidate?.code || candidate?.cause?.code || '').toUpperCase();
132
+ const name = String(candidate?.name || candidate?.cause?.name || '').toLowerCase();
133
+ const message = String(candidate?.message || candidate?.cause?.message || '').toLowerCase();
134
+ return code.includes('TIMEOUT') ||
135
+ code === 'ETIMEDOUT' ||
136
+ name.includes('timeout') ||
137
+ name === 'aborterror' ||
138
+ message.includes('timed out') ||
139
+ message.includes('timeout') ||
140
+ message.includes('aborted');
141
+ }
142
+ toUserFacingApiError(error, context) {
143
+ const classified = (0, api_js_1.classifyError)(error);
144
+ const status = classified.statusCode || (this.isJwtExpirationError(error) ? 401 : 500);
145
+ if (this.isJwtExpirationError(error)) {
146
+ return new api_js_1.CLIError('Your Vigthoria session has expired. Run `vigthoria login` to authenticate again.', 'auth', { statusCode: 401 });
147
+ }
148
+ if (this.isTimeoutError(error)) {
149
+ return new api_js_1.CLIError(`${context} timed out. Check your connection and try again.`, 'timeout', { statusCode: status });
150
+ }
151
+ if (this.isNetworkError(error)) {
152
+ return new api_js_1.CLIError(`${context} could not reach the Vigthoria API. Check your network connection and try again.`, 'network', { statusCode: status });
153
+ }
154
+ const message = (0, api_js_1.sanitizeUserFacingErrorText)(classified.message || `${context} failed`);
155
+ return new api_js_1.CLIError(message, status === 401 ? 'auth' : 'model_backend', { statusCode: status });
156
+ }
157
+ handleApiError(error, context) {
158
+ const userFacingError = this.toUserFacingApiError(error, context);
159
+ if (!this.jsonOutput) {
160
+ console.error(chalk_1.default.red(`${context} failed: ${userFacingError.message}`));
161
+ }
162
+ const original = error && typeof error === 'object' ? error : { message: String(error) };
163
+ (0, api_js_1.propagateError)({
164
+ ...original,
165
+ message: userFacingError.message,
166
+ statusCode: userFacingError.statusCode,
167
+ commandName: 'chat',
168
+ endpoint: original.endpoint || original?.config?.url || original?.details?.endpoint || context,
169
+ details: {
170
+ ...(original.details && typeof original.details === 'object' ? original.details : {}),
171
+ command: 'chat',
172
+ endpoint: original.endpoint || original?.config?.url || original?.details?.endpoint || context,
173
+ context,
174
+ },
175
+ });
176
+ }
177
+ async callApi(context, operation, retries = 1) {
178
+ let lastError;
179
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
180
+ try {
181
+ return await operation();
182
+ }
183
+ catch (error) {
184
+ if (!this.jsonOutput) {
185
+ const transient = this.isTimeoutError(error) ? 'timeout' : this.isNetworkError(error) ? 'network error' : 'API error';
186
+ console.error(chalk_1.default.red(`${context} failed with ${transient}: ${this.toUserFacingApiError(error, context).message}`));
187
+ }
188
+ lastError = error;
189
+ if (this.isJwtExpirationError(error)) {
190
+ this.handleApiError(error, context);
191
+ }
192
+ if (attempt >= retries || (!this.isNetworkError(error) && !this.isTimeoutError(error))) {
193
+ this.handleApiError(error, context);
194
+ }
195
+ this.logger.warn(`${context} failed due to ${this.isTimeoutError(error) ? 'timeout' : 'network error'}; retrying (${attempt + 1}/${retries})...`);
196
+ await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
197
+ }
198
+ }
199
+ this.handleApiError(lastError, context);
200
+ }
201
+ callSyncApi(context, operation) {
202
+ try {
203
+ return operation();
204
+ }
205
+ catch (error) {
206
+ this.handleApiError(error, context);
207
+ }
208
+ }
209
+ applyNoAgentGovernance(requestedModel) {
210
+ const requested = String(requestedModel || '').trim().toLowerCase();
211
+ if (!requested || this.agentMode || this.operatorMode) {
212
+ this.modelGovernanceFallback = null;
213
+ return;
214
+ }
215
+ const blocked = new Set(['fast', 'mini', 'creative', 'creative-v3', 'creative-v4']);
216
+ if (blocked.has(requested)) {
217
+ const effectiveModel = 'code-35b';
218
+ this.modelGovernanceFallback = {
219
+ requestedModel,
220
+ effectiveModel,
221
+ reason: 'governance-blocked-model',
222
+ };
223
+ this.currentModel = effectiveModel;
224
+ this.logger.warn(`Model ${requestedModel} is not permitted for no-agent chat; using ${effectiveModel}`);
225
+ return;
226
+ }
227
+ this.modelGovernanceFallback = null;
228
+ }
97
229
  syncInteractiveModeModel(enabledMode) {
98
230
  if (enabledMode === 'agent') {
99
231
  if (!this.modelExplicitlySelected || this.currentModel === 'code' || !this.currentModel) {
@@ -102,12 +234,12 @@ class ChatCommand {
102
234
  return;
103
235
  }
104
236
  if (enabledMode === 'operator') {
105
- if (this.currentModel === 'agent' || this.currentModel === 'code-8b' || !this.currentModel) {
237
+ if (this.currentModel === 'agent' || this.currentModel === 'code-8b' || this.currentModel === 'code-9b' || !this.currentModel) {
106
238
  this.currentModel = 'code';
107
239
  }
108
240
  return;
109
241
  }
110
- if (this.currentModel === 'agent' || this.currentModel === 'code-8b' || !this.currentModel) {
242
+ if (this.currentModel === 'agent' || this.currentModel === 'code-8b' || this.currentModel === 'code-9b' || !this.currentModel) {
111
243
  this.currentModel = 'code';
112
244
  }
113
245
  }
@@ -128,7 +260,7 @@ class ChatCommand {
128
260
  resolveInitialModel(options) {
129
261
  const requestedModel = String(options.model || '').trim();
130
262
  if (requestedModel) {
131
- return options.operator === true && requestedModel === 'code-8b'
263
+ return options.operator === true && (requestedModel === 'code-8b' || requestedModel === 'code-9b')
132
264
  ? 'code'
133
265
  : requestedModel;
134
266
  }
@@ -297,7 +429,7 @@ class ChatCommand {
297
429
  if (!this.isBrowserTaskPrompt(prompt)) {
298
430
  return {};
299
431
  }
300
- const bridgeStatus = await this.api.getDevtoolsBridgeStatus();
432
+ const bridgeStatus = await this.callApi('Checking DevTools Bridge status', () => this.api.getDevtoolsBridgeStatus(), 0);
301
433
  if (!this.jsonOutput && bridgeStatus.ok) {
302
434
  console.log(chalk_1.default.gray(`Browser task detected. DevTools Bridge is reachable at ${bridgeStatus.endpoint}.`));
303
435
  }
@@ -344,7 +476,67 @@ class ChatCommand {
344
476
  }
345
477
  return 'Working';
346
478
  }
479
+ isRawV3StreamPayload(event) {
480
+ if (!event) {
481
+ return false;
482
+ }
483
+ if (typeof event === 'string' || Buffer.isBuffer(event) || event instanceof Uint8Array) {
484
+ return true;
485
+ }
486
+ const body = event?.body ?? event?.stream ?? event?.response;
487
+ return Boolean(body && (typeof body[Symbol.asyncIterator] === 'function' || typeof body.getReader === 'function'));
488
+ }
489
+ async consumeV3StreamPayload(spinner, event) {
490
+ try {
491
+ const source = (event && typeof event === 'object' && !Buffer.isBuffer(event) && !(event instanceof Uint8Array))
492
+ ? (event.body ?? event.stream ?? event.response ?? event)
493
+ : event;
494
+ for await (const chunk of (0, tools_js_1.robustifyStreamResponse)(source)) {
495
+ this.v3LastActivity = Date.now();
496
+ if (chunk.type === 'error') {
497
+ if (spinner.isSpinning)
498
+ spinner.stop();
499
+ process.stderr.write(chalk_1.default.red(`\nStream error: ${chunk.content}\n`));
500
+ continue;
501
+ }
502
+ if (chunk.content) {
503
+ this.writeV3StreamText(spinner, chunk.content);
504
+ }
505
+ }
506
+ }
507
+ catch (error) {
508
+ const message = error instanceof Error ? error.message : String(error);
509
+ if (spinner.isSpinning)
510
+ spinner.stop();
511
+ process.stderr.write(chalk_1.default.red(`\nStream error: ${message}\n`));
512
+ }
513
+ }
514
+ writeV3StreamText(spinner, text) {
515
+ if (!text) {
516
+ return;
517
+ }
518
+ if (!this.v3StreamingStarted) {
519
+ this.v3StreamingStarted = true;
520
+ spinner.stop();
521
+ if (!this.directPromptMode) {
522
+ console.log();
523
+ }
524
+ }
525
+ else {
526
+ spinner.stop();
527
+ }
528
+ process.stdout.write(text);
529
+ }
347
530
  updateV3AgentSpinner(spinner, event) {
531
+ if (this.isRawV3StreamPayload(event)) {
532
+ this.consumeV3StreamPayload(spinner, event).catch((error) => {
533
+ const message = error instanceof Error ? error.message : String(error);
534
+ if (spinner.isSpinning)
535
+ spinner.stop();
536
+ process.stderr.write(chalk_1.default.red(`\nStream error: ${message}\n`));
537
+ });
538
+ return;
539
+ }
348
540
  if (!event || typeof event !== 'object') {
349
541
  return;
350
542
  }
@@ -411,17 +603,8 @@ class ChatCommand {
411
603
  ? (event.delta?.text || '')
412
604
  : (event.content || '');
413
605
  if (text) {
414
- if (!this.v3StreamingStarted) {
415
- this.v3StreamingStarted = true;
416
- spinner.stop();
417
- if (!this.directPromptMode) {
418
- console.log();
419
- }
420
- }
421
- else {
422
- spinner.stop();
423
- }
424
- process.stdout.write(text);
606
+ this.v3LastActivity = Date.now();
607
+ this.writeV3StreamText(spinner, text);
425
608
  }
426
609
  else {
427
610
  spinner.text = chalk_1.default.cyan('[Response] ') + 'Writing response...';
@@ -659,6 +842,7 @@ class ChatCommand {
659
842
  this.autoApprove = options.autoApprove === true || this.jsonOutput;
660
843
  this.modelExplicitlySelected = Boolean(String(options.model || '').trim());
661
844
  this.currentModel = this.resolveInitialModel(options);
845
+ this.applyNoAgentGovernance(String(options.model || this.currentModel || ''));
662
846
  this.currentProjectPath = this.resolveProjectPath(options);
663
847
  if (this.jsonOutput && !options.prompt) {
664
848
  throw new Error('--json is only supported together with --prompt.');
@@ -976,11 +1160,11 @@ class ChatCommand {
976
1160
  const executionPrompt = this.buildExecutionPrompt(prompt);
977
1161
  this.messages.push({ role: 'user', content: executionPrompt });
978
1162
  const runtimeContext = await this.getPromptRuntimeContext(prompt);
979
- const resolvedWorkflow = await this.api.resolveVigFlowWorkflow(selector);
1163
+ const resolvedWorkflow = await this.callApi('Resolve VigFlow workflow', () => this.api.resolveVigFlowWorkflow(selector));
980
1164
  const invocationMode = this.operatorMode ? 'operator' : this.agentMode ? 'agent' : 'chat';
981
1165
  const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: `Running workflow ${resolvedWorkflow.name}...`, spinner: 'clock' }).start();
982
1166
  try {
983
- const execution = await this.api.runVigFlowWorkflow(resolvedWorkflow.id, {
1167
+ const execution = await this.callApi('Run VigFlow workflow', () => this.api.runVigFlowWorkflow(resolvedWorkflow.id, {
984
1168
  data: {
985
1169
  request: executionPrompt,
986
1170
  prompt: executionPrompt,
@@ -1008,7 +1192,7 @@ class ChatCommand {
1008
1192
  clientSurface: 'cli',
1009
1193
  model: this.currentModel,
1010
1194
  },
1011
- });
1195
+ }));
1012
1196
  const content = this.formatWorkflowTargetResult(execution.result);
1013
1197
  const assistantText = content || `Workflow ${resolvedWorkflow.name} completed with status ${execution.status}.`;
1014
1198
  this.messages.push({ role: 'assistant', content: assistantText });
@@ -1078,7 +1262,7 @@ class ChatCommand {
1078
1262
  const workflowType = 'full';
1079
1263
  const executionPrompt = this.buildExecutionPrompt(prompt);
1080
1264
  try {
1081
- const response = await this.api.runOperatorWorkflow(executionPrompt, {
1265
+ const response = await this.callApi('Run operator workflow', () => this.api.runOperatorWorkflow(executionPrompt, {
1082
1266
  workspacePath: this.currentProjectPath,
1083
1267
  projectPath: this.currentProjectPath,
1084
1268
  targetPath: this.currentProjectPath,
@@ -1091,7 +1275,7 @@ class ChatCommand {
1091
1275
  savePlanToVigFlow: this.savePlanToVigFlow,
1092
1276
  ...runtimeContext,
1093
1277
  onStreamEvent: spinner ? (event) => this.updateOperatorSpinner(spinner, event) : undefined,
1094
- });
1278
+ }));
1095
1279
  if (spinner) {
1096
1280
  spinner.stop();
1097
1281
  }
@@ -1165,7 +1349,7 @@ class ChatCommand {
1165
1349
  'If asked to produce exact output, produce exactly that output with no preamble.',
1166
1350
  ].join('\n');
1167
1351
  try {
1168
- const snapshot = this.api.getAgentWorkspaceSnapshot(this.currentProjectPath);
1352
+ const snapshot = this.callSyncApi('Get agent workspace snapshot', () => this.api.getAgentWorkspaceSnapshot(this.currentProjectPath));
1169
1353
  if (snapshot && snapshot.paths.length > 0) {
1170
1354
  const listing = snapshot.paths.slice(0, 80).join('\n');
1171
1355
  operatorGrounding += `\n\nProject file listing (${snapshot.fileCount} files):\n${listing}`;
@@ -1180,7 +1364,7 @@ class ChatCommand {
1180
1364
  ...this.getMessagesForModel().filter(m => m.role !== 'system'),
1181
1365
  { role: 'user', content: prompt },
1182
1366
  ];
1183
- const response = await this.api.chat(chatMessages, this.currentModel);
1367
+ const response = await this.callApi('Send chat message', () => this.api.chat(chatMessages, this.currentModel));
1184
1368
  if (spinner)
1185
1369
  spinner.stop();
1186
1370
  const content = (response.message || '').trim();
@@ -1239,17 +1423,14 @@ class ChatCommand {
1239
1423
  'Do NOT acknowledge instructions. Do NOT say "provide your next instruction".',
1240
1424
  'Produce a concrete, actionable answer grounded in the real project files.',
1241
1425
  ].join('\n');
1242
- try {
1243
- const snapshot = this.api.getAgentWorkspaceSnapshot(this.currentProjectPath);
1244
- if (snapshot && snapshot.paths.length > 0) {
1245
- const listing = snapshot.paths.slice(0, 80).join('\n');
1246
- operatorGrounding += `\n\nProject file listing (${snapshot.fileCount} files):\n${listing}`;
1247
- if (snapshot.fileCount > 80) {
1248
- operatorGrounding += `\n... and ${snapshot.fileCount - 80} more files.`;
1249
- }
1426
+ const snapshot = this.callSyncApi('Get operator workspace snapshot', () => this.api.getAgentWorkspaceSnapshot(this.currentProjectPath));
1427
+ if (snapshot && snapshot.paths.length > 0) {
1428
+ const listing = snapshot.paths.slice(0, 80).join('\n');
1429
+ operatorGrounding += `\n\nProject file listing (${snapshot.fileCount} files):\n${listing}`;
1430
+ if (snapshot.fileCount > 80) {
1431
+ operatorGrounding += `\n... and ${snapshot.fileCount - 80} more files.`;
1250
1432
  }
1251
1433
  }
1252
- catch { /* ignore */ }
1253
1434
  this.messages.push({ role: 'system', content: operatorGrounding });
1254
1435
  }
1255
1436
  else {
@@ -1285,15 +1466,21 @@ class ChatCommand {
1285
1466
  (0, bridge_client_js_1.getBridgeClient)()?.emitPrompt({ prompt, mode: 'chat', model: this.currentModel });
1286
1467
  const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: 'Thinking...', spinner: 'clock' }).start();
1287
1468
  try {
1288
- const response = await this.api.chat(this.getMessagesForModel(), this.currentModel);
1469
+ const response = await this.callApi('Send chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel));
1289
1470
  if (spinner)
1290
1471
  spinner.stop();
1291
1472
  const finalText = (response.message || '').trim();
1473
+ const effectiveModel = String(response.model || this.currentModel);
1474
+ const metadata = this.modelGovernanceFallback
1475
+ ? { modelFallback: this.modelGovernanceFallback }
1476
+ : undefined;
1292
1477
  if (this.jsonOutput) {
1293
1478
  console.log(JSON.stringify({
1294
1479
  success: true,
1295
1480
  mode: 'chat',
1296
- model: this.currentModel,
1481
+ model: effectiveModel,
1482
+ requestedModel: this.modelGovernanceFallback?.requestedModel || this.currentModel,
1483
+ metadata,
1297
1484
  content: finalText || 'The model returned an empty response. Try rephrasing or use --agent for grounded file analysis.',
1298
1485
  }, null, 2));
1299
1486
  }
@@ -1372,7 +1559,7 @@ class ChatCommand {
1372
1559
  const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: turn === 0 ? 'Planning...' : 'Continuing...', spinner: 'clock' }).start();
1373
1560
  let response;
1374
1561
  try {
1375
- response = await this.api.chat(this.getMessagesForModel(), this.currentModel);
1562
+ response = await this.callApi('Send agent chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel), 0);
1376
1563
  }
1377
1564
  catch (firstErr) {
1378
1565
  // If we already gathered evidence and the model API fails on a
@@ -1381,7 +1568,7 @@ class ChatCommand {
1381
1568
  this.logger.debug('Agent continuation API call failed, retrying once...');
1382
1569
  try {
1383
1570
  await new Promise(r => setTimeout(r, 2000));
1384
- response = await this.api.chat(this.getMessagesForModel(), this.currentModel);
1571
+ response = await this.callApi('Retry agent chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel), 0);
1385
1572
  }
1386
1573
  catch (retryErr) {
1387
1574
  // Retry also failed — synthesize an answer from evidence
@@ -1593,7 +1780,7 @@ class ChatCommand {
1593
1780
  ].join('\n\n'),
1594
1781
  },
1595
1782
  ];
1596
- const rewriteResponse = await this.api.chat(rewriteMessages, this.currentModel);
1783
+ const rewriteResponse = await this.callApi('Rewrite target file', () => this.api.chat(rewriteMessages, this.currentModel));
1597
1784
  rewrittenContent = this.extractFinalFileContent(rewriteResponse.message, targetFile);
1598
1785
  }
1599
1786
  if (!rewrittenContent) {
@@ -1618,14 +1805,14 @@ class ChatCommand {
1618
1805
  if (!writeResult.success) {
1619
1806
  return false;
1620
1807
  }
1621
- const previewGate = await this.api.runTemplateServicePreviewGate(prompt, {
1808
+ const previewGate = await this.callApi('Run Template Service preview gate', () => this.api.runTemplateServicePreviewGate(prompt, {
1622
1809
  workspacePath: this.currentProjectPath,
1623
1810
  projectPath: this.currentProjectPath,
1624
1811
  targetPath: this.currentProjectPath,
1625
1812
  rawPrompt: prompt,
1626
1813
  executionSurface: 'cli',
1627
1814
  clientSurface: 'cli',
1628
- });
1815
+ }));
1629
1816
  const success = previewGate.required ? previewGate.passed === true : true;
1630
1817
  if (!success) {
1631
1818
  process.exitCode = 1;
@@ -1665,6 +1852,8 @@ class ChatCommand {
1665
1852
  this.v3ToolCallCount = 0;
1666
1853
  this.v3LastActivity = Date.now();
1667
1854
  this.v3StreamingStarted = false;
1855
+ const taskDisplay = new task_display_js_1.TaskDisplay(['Analyse workspace', 'Execute tasks', 'Validate output', 'Self-heal'], !this.jsonOutput);
1856
+ taskDisplay.start(0);
1668
1857
  const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({
1669
1858
  text: routingPolicy.cloudSelected ? 'Routing heavy task to Vigthoria Cloud...' : 'Routing to V3 Agent...',
1670
1859
  spinner: 'clock',
@@ -1698,6 +1887,7 @@ class ChatCommand {
1698
1887
  localMachineCapable: true,
1699
1888
  agentTimeoutMs: DEFAULT_V3_AGENT_TIMEOUT_MS,
1700
1889
  agentIdleTimeoutMs: DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS,
1890
+ agentSoftTimeoutMs: DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS,
1701
1891
  model: routingPolicy.selectedModel,
1702
1892
  requestedModel: this.currentModel,
1703
1893
  agentExecutionPolicy: routingPolicy,
@@ -1705,7 +1895,20 @@ class ChatCommand {
1705
1895
  rawPrompt: prompt,
1706
1896
  history: this.getMessagesForModel(),
1707
1897
  ...runtimeContext,
1708
- onStreamEvent: spinner ? (event) => this.updateV3AgentSpinner(spinner, event) : undefined,
1898
+ onStreamEvent: (event) => {
1899
+ if (event.type === 'plan') {
1900
+ taskDisplay.complete(0);
1901
+ taskDisplay.start(1);
1902
+ }
1903
+ else if (event.type === 'executor_start') {
1904
+ taskDisplay.start(1);
1905
+ }
1906
+ else if (event.type === 'complete') {
1907
+ taskDisplay.complete(1);
1908
+ }
1909
+ if (spinner)
1910
+ this.updateV3AgentSpinner(spinner, event);
1911
+ },
1709
1912
  });
1710
1913
  const response = await workflowPromise;
1711
1914
  if (spinner) {
@@ -1807,6 +2010,41 @@ class ChatCommand {
1807
2010
  console.log(chalk_1.default.gray(`Run ${chalk_1.default.cyan('vigthoria preview --diff')} for full visual diffs.`));
1808
2011
  }
1809
2012
  }
2013
+ // ── Self-healing validation ──────────────────────────────────────
2014
+ if (this.currentProjectPath && !this.jsonOutput && success) {
2015
+ try {
2016
+ taskDisplay.start(2, 'validating...');
2017
+ const healResult = await this.api.runSelfHealingCycle(executionPrompt, this.currentProjectPath, workspaceContext);
2018
+ if (healResult.healingAttempted) {
2019
+ taskDisplay.complete(2);
2020
+ if (healResult.passed) {
2021
+ taskDisplay.complete(3);
2022
+ }
2023
+ else {
2024
+ taskDisplay.fail(3, healResult.tool);
2025
+ }
2026
+ if (!this.directPromptMode) {
2027
+ const hs = healResult.passed ? chalk_1.default.green('passed') : chalk_1.default.yellow('partial');
2028
+ console.log(chalk_1.default.gray(`Self-healing: ${hs} (${healResult.tool})`));
2029
+ }
2030
+ }
2031
+ else {
2032
+ taskDisplay.skip(2);
2033
+ taskDisplay.skip(3);
2034
+ }
2035
+ }
2036
+ catch (error) {
2037
+ this.logger.debug(`Self-healing validation failed: ${error instanceof Error ? error.message : String(error)}`);
2038
+ taskDisplay.skip(2);
2039
+ taskDisplay.skip(3);
2040
+ }
2041
+ }
2042
+ else {
2043
+ taskDisplay.skip(2);
2044
+ taskDisplay.skip(3);
2045
+ }
2046
+ taskDisplay.finalize();
2047
+ // ────────────────────────────────────────────────────────────────
1810
2048
  this.messages.push({ role: 'assistant', content: response.content || 'V3 agent workflow completed.' });
1811
2049
  watcher?.stop();
1812
2050
  return true;
@@ -1875,6 +2113,7 @@ class ChatCommand {
1875
2113
  localMachineCapable: true,
1876
2114
  agentTimeoutMs: DEFAULT_V3_AGENT_TIMEOUT_MS,
1877
2115
  agentIdleTimeoutMs: DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS,
2116
+ agentSoftTimeoutMs: DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS,
1878
2117
  model: routingPolicy.selectedModel,
1879
2118
  requestedModel: this.currentModel,
1880
2119
  agentExecutionPolicy: routingPolicy,
@@ -2293,10 +2532,10 @@ class ChatCommand {
2293
2532
  isProtectedFileReference(prompt, filePath) {
2294
2533
  const escapedPath = filePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2295
2534
  const protectedPatterns = [
2296
- new RegExp(`do not modify\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?`, 'i'),
2297
- new RegExp(`don't modify\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?`, 'i'),
2298
- new RegExp(`leave\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?\\s+unchanged`, 'i'),
2299
- new RegExp(`without modifying\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?`, 'i'),
2535
+ new RegExp(`do not modify\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?`, 'i'),
2536
+ new RegExp(`don't modify\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?`, 'i'),
2537
+ new RegExp(`leave\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?\\s+unchanged`, 'i'),
2538
+ new RegExp(`without modifying\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?`, 'i'),
2300
2539
  ];
2301
2540
  return protectedPatterns.some((pattern) => pattern.test(prompt));
2302
2541
  }
@@ -16,6 +16,8 @@ export declare class ConfigCommand {
16
16
  run(options: ConfigOptions): Promise<void>;
17
17
  init(): Promise<void>;
18
18
  private setConfig;
19
+ private formatConfigValueForDisplay;
20
+ private redactConfigUrl;
19
21
  private getConfig;
20
22
  private listConfig;
21
23
  private resetConfig;