vigthoria-cli 1.8.15 → 1.9.2

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;
@@ -118,8 +127,7 @@ export declare class ChatCommand {
118
127
  private runLocalAgentLoop;
119
128
  private tryDirectSingleFileFlow;
120
129
  private tryV3AgentWorkflow;
121
- private isSaaSRescuePrompt;
122
- private tryCommandLevelSaaSRescue;
130
+ private tryRecoverV3ServiceAndRetry;
123
131
  private startInteractiveChat;
124
132
  private showHelp;
125
133
  private showContext;
@@ -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
  }
@@ -494,6 +626,43 @@ class ChatCommand {
494
626
  spinner.text = status === 'planning' ? 'Planning...' : 'Executing plan...';
495
627
  return;
496
628
  }
629
+ if (event.type === 'executor_start') {
630
+ if (spinner.isSpinning)
631
+ spinner.stop();
632
+ process.stderr.write(chalk_1.default.cyan(' [Executor] ') + `Starting ${event.task_id || 'task'}${event.title ? ` - ${event.title}` : ''}
633
+ `);
634
+ spinner.start();
635
+ spinner.text = 'Vigthoria Executor running...';
636
+ return;
637
+ }
638
+ if (event.type === 'executor_error') {
639
+ if (spinner.isSpinning)
640
+ spinner.stop();
641
+ const msg = (0, api_js_1.sanitizeUserFacingErrorText)(String(event.error || 'Executor error')) || 'Executor error';
642
+ process.stderr.write(chalk_1.default.red(' [Executor] ') + `Vigthoria Executor encountered an issue: ${msg}
643
+ `);
644
+ spinner.start();
645
+ spinner.text = 'Recovering executor...';
646
+ return;
647
+ }
648
+ if (event.type === 'executor_complete') {
649
+ if (spinner.isSpinning)
650
+ spinner.stop();
651
+ const summary = event.summary || {};
652
+ const status = String(summary.status || 'completed');
653
+ const changed = Array.isArray(summary.changed_files) ? summary.changed_files.length : 0;
654
+ if (status === 'failed') {
655
+ process.stderr.write(chalk_1.default.red(' [Executor] ') + `Vigthoria Executor task failed${summary.task_id ? ` (${summary.task_id})` : ''}.
656
+ `);
657
+ }
658
+ else {
659
+ process.stderr.write(chalk_1.default.green(' [Executor] ') + `Task completed${summary.task_id ? ` (${summary.task_id})` : ''}${changed ? `, ${changed} files changed` : ''}.
660
+ `);
661
+ }
662
+ spinner.start();
663
+ spinner.text = 'Continuing plan...';
664
+ return;
665
+ }
497
666
  if (event.type === 'file_mutation') {
498
667
  const rawPath = typeof event.path === 'string' ? this.sanitizeServerPath(event.path) : '';
499
668
  const filePath = rawPath ? rawPath.replace(/\\/g, '/').split('/').slice(-2).join('/') : '';
@@ -516,7 +685,18 @@ class ChatCommand {
516
685
  else {
517
686
  if (spinner.isSpinning)
518
687
  spinner.stop();
519
- process.stderr.write(chalk_1.default.red(' [Error] ') + (event.message || 'Agent error') + '\n');
688
+ const message = (0, api_js_1.sanitizeUserFacingErrorText)(String(event.message || 'Agent error')) || 'Agent error';
689
+ const plannerLike = /plan|planner|dependency graph/i.test(message);
690
+ const executorLike = /executor|task failed|iteration/i.test(message);
691
+ if (plannerLike) {
692
+ process.stderr.write(chalk_1.default.red(' [Planner] ') + `Vigthoria Planner encountered an issue: ${message}\n`);
693
+ }
694
+ else if (executorLike) {
695
+ process.stderr.write(chalk_1.default.red(' [Executor] ') + `Vigthoria Executor encountered an issue: ${message}\n`);
696
+ }
697
+ else {
698
+ process.stderr.write(chalk_1.default.red(' [Error] ') + message + '\n');
699
+ }
520
700
  }
521
701
  return;
522
702
  }
@@ -611,6 +791,7 @@ class ChatCommand {
611
791
  this.autoApprove = options.autoApprove === true || this.jsonOutput;
612
792
  this.modelExplicitlySelected = Boolean(String(options.model || '').trim());
613
793
  this.currentModel = this.resolveInitialModel(options);
794
+ this.applyNoAgentGovernance(String(options.model || this.currentModel || ''));
614
795
  this.currentProjectPath = this.resolveProjectPath(options);
615
796
  if (this.jsonOutput && !options.prompt) {
616
797
  throw new Error('--json is only supported together with --prompt.');
@@ -928,11 +1109,11 @@ class ChatCommand {
928
1109
  const executionPrompt = this.buildExecutionPrompt(prompt);
929
1110
  this.messages.push({ role: 'user', content: executionPrompt });
930
1111
  const runtimeContext = await this.getPromptRuntimeContext(prompt);
931
- const resolvedWorkflow = await this.api.resolveVigFlowWorkflow(selector);
1112
+ const resolvedWorkflow = await this.callApi('Resolve VigFlow workflow', () => this.api.resolveVigFlowWorkflow(selector));
932
1113
  const invocationMode = this.operatorMode ? 'operator' : this.agentMode ? 'agent' : 'chat';
933
1114
  const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: `Running workflow ${resolvedWorkflow.name}...`, spinner: 'clock' }).start();
934
1115
  try {
935
- const execution = await this.api.runVigFlowWorkflow(resolvedWorkflow.id, {
1116
+ const execution = await this.callApi('Run VigFlow workflow', () => this.api.runVigFlowWorkflow(resolvedWorkflow.id, {
936
1117
  data: {
937
1118
  request: executionPrompt,
938
1119
  prompt: executionPrompt,
@@ -960,7 +1141,7 @@ class ChatCommand {
960
1141
  clientSurface: 'cli',
961
1142
  model: this.currentModel,
962
1143
  },
963
- });
1144
+ }));
964
1145
  const content = this.formatWorkflowTargetResult(execution.result);
965
1146
  const assistantText = content || `Workflow ${resolvedWorkflow.name} completed with status ${execution.status}.`;
966
1147
  this.messages.push({ role: 'assistant', content: assistantText });
@@ -1030,7 +1211,7 @@ class ChatCommand {
1030
1211
  const workflowType = 'full';
1031
1212
  const executionPrompt = this.buildExecutionPrompt(prompt);
1032
1213
  try {
1033
- const response = await this.api.runOperatorWorkflow(executionPrompt, {
1214
+ const response = await this.callApi('Run operator workflow', () => this.api.runOperatorWorkflow(executionPrompt, {
1034
1215
  workspacePath: this.currentProjectPath,
1035
1216
  projectPath: this.currentProjectPath,
1036
1217
  targetPath: this.currentProjectPath,
@@ -1043,7 +1224,7 @@ class ChatCommand {
1043
1224
  savePlanToVigFlow: this.savePlanToVigFlow,
1044
1225
  ...runtimeContext,
1045
1226
  onStreamEvent: spinner ? (event) => this.updateOperatorSpinner(spinner, event) : undefined,
1046
- });
1227
+ }));
1047
1228
  if (spinner) {
1048
1229
  spinner.stop();
1049
1230
  }
@@ -1117,7 +1298,7 @@ class ChatCommand {
1117
1298
  'If asked to produce exact output, produce exactly that output with no preamble.',
1118
1299
  ].join('\n');
1119
1300
  try {
1120
- const snapshot = this.api.getAgentWorkspaceSnapshot(this.currentProjectPath);
1301
+ const snapshot = this.callSyncApi('Get agent workspace snapshot', () => this.api.getAgentWorkspaceSnapshot(this.currentProjectPath));
1121
1302
  if (snapshot && snapshot.paths.length > 0) {
1122
1303
  const listing = snapshot.paths.slice(0, 80).join('\n');
1123
1304
  operatorGrounding += `\n\nProject file listing (${snapshot.fileCount} files):\n${listing}`;
@@ -1132,7 +1313,7 @@ class ChatCommand {
1132
1313
  ...this.getMessagesForModel().filter(m => m.role !== 'system'),
1133
1314
  { role: 'user', content: prompt },
1134
1315
  ];
1135
- const response = await this.api.chat(chatMessages, this.currentModel);
1316
+ const response = await this.callApi('Send chat message', () => this.api.chat(chatMessages, this.currentModel));
1136
1317
  if (spinner)
1137
1318
  spinner.stop();
1138
1319
  const content = (response.message || '').trim();
@@ -1191,17 +1372,14 @@ class ChatCommand {
1191
1372
  'Do NOT acknowledge instructions. Do NOT say "provide your next instruction".',
1192
1373
  'Produce a concrete, actionable answer grounded in the real project files.',
1193
1374
  ].join('\n');
1194
- try {
1195
- const snapshot = this.api.getAgentWorkspaceSnapshot(this.currentProjectPath);
1196
- if (snapshot && snapshot.paths.length > 0) {
1197
- const listing = snapshot.paths.slice(0, 80).join('\n');
1198
- operatorGrounding += `\n\nProject file listing (${snapshot.fileCount} files):\n${listing}`;
1199
- if (snapshot.fileCount > 80) {
1200
- operatorGrounding += `\n... and ${snapshot.fileCount - 80} more files.`;
1201
- }
1375
+ const snapshot = this.callSyncApi('Get operator workspace snapshot', () => this.api.getAgentWorkspaceSnapshot(this.currentProjectPath));
1376
+ if (snapshot && snapshot.paths.length > 0) {
1377
+ const listing = snapshot.paths.slice(0, 80).join('\n');
1378
+ operatorGrounding += `\n\nProject file listing (${snapshot.fileCount} files):\n${listing}`;
1379
+ if (snapshot.fileCount > 80) {
1380
+ operatorGrounding += `\n... and ${snapshot.fileCount - 80} more files.`;
1202
1381
  }
1203
1382
  }
1204
- catch { /* ignore */ }
1205
1383
  this.messages.push({ role: 'system', content: operatorGrounding });
1206
1384
  }
1207
1385
  else {
@@ -1237,15 +1415,21 @@ class ChatCommand {
1237
1415
  (0, bridge_client_js_1.getBridgeClient)()?.emitPrompt({ prompt, mode: 'chat', model: this.currentModel });
1238
1416
  const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: 'Thinking...', spinner: 'clock' }).start();
1239
1417
  try {
1240
- const response = await this.api.chat(this.getMessagesForModel(), this.currentModel);
1418
+ const response = await this.callApi('Send chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel));
1241
1419
  if (spinner)
1242
1420
  spinner.stop();
1243
1421
  const finalText = (response.message || '').trim();
1422
+ const effectiveModel = String(response.model || this.currentModel);
1423
+ const metadata = this.modelGovernanceFallback
1424
+ ? { modelFallback: this.modelGovernanceFallback }
1425
+ : undefined;
1244
1426
  if (this.jsonOutput) {
1245
1427
  console.log(JSON.stringify({
1246
1428
  success: true,
1247
1429
  mode: 'chat',
1248
- model: this.currentModel,
1430
+ model: effectiveModel,
1431
+ requestedModel: this.modelGovernanceFallback?.requestedModel || this.currentModel,
1432
+ metadata,
1249
1433
  content: finalText || 'The model returned an empty response. Try rephrasing or use --agent for grounded file analysis.',
1250
1434
  }, null, 2));
1251
1435
  }
@@ -1324,7 +1508,7 @@ class ChatCommand {
1324
1508
  const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: turn === 0 ? 'Planning...' : 'Continuing...', spinner: 'clock' }).start();
1325
1509
  let response;
1326
1510
  try {
1327
- response = await this.api.chat(this.getMessagesForModel(), this.currentModel);
1511
+ response = await this.callApi('Send agent chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel), 0);
1328
1512
  }
1329
1513
  catch (firstErr) {
1330
1514
  // If we already gathered evidence and the model API fails on a
@@ -1333,7 +1517,7 @@ class ChatCommand {
1333
1517
  this.logger.debug('Agent continuation API call failed, retrying once...');
1334
1518
  try {
1335
1519
  await new Promise(r => setTimeout(r, 2000));
1336
- response = await this.api.chat(this.getMessagesForModel(), this.currentModel);
1520
+ response = await this.callApi('Retry agent chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel), 0);
1337
1521
  }
1338
1522
  catch (retryErr) {
1339
1523
  // Retry also failed — synthesize an answer from evidence
@@ -1545,7 +1729,7 @@ class ChatCommand {
1545
1729
  ].join('\n\n'),
1546
1730
  },
1547
1731
  ];
1548
- const rewriteResponse = await this.api.chat(rewriteMessages, this.currentModel);
1732
+ const rewriteResponse = await this.callApi('Rewrite target file', () => this.api.chat(rewriteMessages, this.currentModel));
1549
1733
  rewrittenContent = this.extractFinalFileContent(rewriteResponse.message, targetFile);
1550
1734
  }
1551
1735
  if (!rewrittenContent) {
@@ -1570,14 +1754,14 @@ class ChatCommand {
1570
1754
  if (!writeResult.success) {
1571
1755
  return false;
1572
1756
  }
1573
- const previewGate = await this.api.runTemplateServicePreviewGate(prompt, {
1757
+ const previewGate = await this.callApi('Run Template Service preview gate', () => this.api.runTemplateServicePreviewGate(prompt, {
1574
1758
  workspacePath: this.currentProjectPath,
1575
1759
  projectPath: this.currentProjectPath,
1576
1760
  targetPath: this.currentProjectPath,
1577
1761
  rawPrompt: prompt,
1578
1762
  executionSurface: 'cli',
1579
1763
  clientSurface: 'cli',
1580
- });
1764
+ }));
1581
1765
  const success = previewGate.required ? previewGate.passed === true : true;
1582
1766
  if (!success) {
1583
1767
  process.exitCode = 1;
@@ -1612,12 +1796,13 @@ class ChatCommand {
1612
1796
  async tryV3AgentWorkflow(prompt) {
1613
1797
  const runtimeContext = await this.getPromptRuntimeContext(prompt);
1614
1798
  const routingPolicy = this.resolveAgentExecutionPolicy(prompt);
1615
- const rescueEligible = this.isSaaSRescuePrompt(prompt);
1616
1799
  // Reset streaming counters for new workflow
1617
1800
  this.v3IterationCount = 0;
1618
1801
  this.v3ToolCallCount = 0;
1619
1802
  this.v3LastActivity = Date.now();
1620
1803
  this.v3StreamingStarted = false;
1804
+ const taskDisplay = new task_display_js_1.TaskDisplay(['Analyse workspace', 'Execute tasks', 'Validate output', 'Self-heal'], !this.jsonOutput);
1805
+ taskDisplay.start(0);
1621
1806
  const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({
1622
1807
  text: routingPolicy.cloudSelected ? 'Routing heavy task to Vigthoria Cloud...' : 'Routing to V3 Agent...',
1623
1808
  spinner: 'clock',
@@ -1651,6 +1836,7 @@ class ChatCommand {
1651
1836
  localMachineCapable: true,
1652
1837
  agentTimeoutMs: DEFAULT_V3_AGENT_TIMEOUT_MS,
1653
1838
  agentIdleTimeoutMs: DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS,
1839
+ agentSoftTimeoutMs: DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS,
1654
1840
  model: routingPolicy.selectedModel,
1655
1841
  requestedModel: this.currentModel,
1656
1842
  agentExecutionPolicy: routingPolicy,
@@ -1658,16 +1844,22 @@ class ChatCommand {
1658
1844
  rawPrompt: prompt,
1659
1845
  history: this.getMessagesForModel(),
1660
1846
  ...runtimeContext,
1661
- onStreamEvent: spinner ? (event) => this.updateV3AgentSpinner(spinner, event) : undefined,
1847
+ onStreamEvent: (event) => {
1848
+ if (event.type === 'plan') {
1849
+ taskDisplay.complete(0);
1850
+ taskDisplay.start(1);
1851
+ }
1852
+ else if (event.type === 'executor_start') {
1853
+ taskDisplay.start(1);
1854
+ }
1855
+ else if (event.type === 'complete') {
1856
+ taskDisplay.complete(1);
1857
+ }
1858
+ if (spinner)
1859
+ this.updateV3AgentSpinner(spinner, event);
1860
+ },
1662
1861
  });
1663
- const response = await (rescueEligible && DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS > 0
1664
- ? Promise.race([
1665
- workflowPromise,
1666
- new Promise((_, reject) => {
1667
- setTimeout(() => reject(new Error('V3_SAAS_SOFT_TIMEOUT')), DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS);
1668
- }),
1669
- ])
1670
- : workflowPromise);
1862
+ const response = await workflowPromise;
1671
1863
  if (spinner) {
1672
1864
  spinner.stop();
1673
1865
  }
@@ -1767,15 +1959,50 @@ class ChatCommand {
1767
1959
  console.log(chalk_1.default.gray(`Run ${chalk_1.default.cyan('vigthoria preview --diff')} for full visual diffs.`));
1768
1960
  }
1769
1961
  }
1962
+ // ── Self-healing validation ──────────────────────────────────────
1963
+ if (this.currentProjectPath && !this.jsonOutput && success) {
1964
+ try {
1965
+ taskDisplay.start(2, 'validating...');
1966
+ const healResult = await this.api.runSelfHealingCycle(executionPrompt, this.currentProjectPath, workspaceContext);
1967
+ if (healResult.healingAttempted) {
1968
+ taskDisplay.complete(2);
1969
+ if (healResult.passed) {
1970
+ taskDisplay.complete(3);
1971
+ }
1972
+ else {
1973
+ taskDisplay.fail(3, healResult.tool);
1974
+ }
1975
+ if (!this.directPromptMode) {
1976
+ const hs = healResult.passed ? chalk_1.default.green('passed') : chalk_1.default.yellow('partial');
1977
+ console.log(chalk_1.default.gray(`Self-healing: ${hs} (${healResult.tool})`));
1978
+ }
1979
+ }
1980
+ else {
1981
+ taskDisplay.skip(2);
1982
+ taskDisplay.skip(3);
1983
+ }
1984
+ }
1985
+ catch (error) {
1986
+ this.logger.debug(`Self-healing validation failed: ${error instanceof Error ? error.message : String(error)}`);
1987
+ taskDisplay.skip(2);
1988
+ taskDisplay.skip(3);
1989
+ }
1990
+ }
1991
+ else {
1992
+ taskDisplay.skip(2);
1993
+ taskDisplay.skip(3);
1994
+ }
1995
+ taskDisplay.finalize();
1996
+ // ────────────────────────────────────────────────────────────────
1770
1997
  this.messages.push({ role: 'assistant', content: response.content || 'V3 agent workflow completed.' });
1771
1998
  watcher?.stop();
1772
1999
  return true;
1773
2000
  }
1774
2001
  catch (error) {
1775
2002
  watcher?.stop();
1776
- if (rescueEligible && !this.api.hasAgentWorkspaceOutput(workspaceContext)) {
1777
- const rescued = await this.tryCommandLevelSaaSRescue(executionPrompt, prompt, workspaceContext, routingPolicy, spinner, error);
1778
- if (rescued) {
2003
+ if (!this.api.hasAgentWorkspaceOutput(workspaceContext)) {
2004
+ const recovered = await this.tryRecoverV3ServiceAndRetry(executionPrompt, prompt, workspaceContext, routingPolicy, spinner, error);
2005
+ if (recovered) {
1779
2006
  return true;
1780
2007
  }
1781
2008
  }
@@ -1811,60 +2038,68 @@ class ChatCommand {
1811
2038
  return true;
1812
2039
  }
1813
2040
  }
1814
- isSaaSRescuePrompt(prompt) {
1815
- return /(saas|dashboard|analytics|billing|team management|activity feed|login screen)/i.test(prompt);
1816
- }
1817
- async tryCommandLevelSaaSRescue(executionPrompt, rawPrompt, workspaceContext, routingPolicy, spinner, error) {
1818
- const apiAny = this.api;
1819
- if (typeof apiAny.materializeEmergencySaaSWorkspace !== 'function') {
2041
+ async tryRecoverV3ServiceAndRetry(executionPrompt, rawPrompt, workspaceContext, routingPolicy, spinner, error) {
2042
+ const recovery = await this.api.attemptV3ServiceRecovery((error && error.message) || '');
2043
+ if (!recovery.recovered) {
1820
2044
  return false;
1821
2045
  }
1822
- const appName = apiAny.materializeEmergencySaaSWorkspace(executionPrompt, workspaceContext);
1823
- if (!appName) {
1824
- return false;
1825
- }
1826
- if (typeof apiAny.ensureAgentFrontendPolish === 'function') {
1827
- await apiAny.ensureAgentFrontendPolish(executionPrompt, workspaceContext);
1828
- }
1829
- const previewGate = await this.api.runTemplateServicePreviewGate(executionPrompt, {
1830
- ...workspaceContext,
1831
- rawPrompt,
1832
- });
1833
- if (spinner) {
2046
+ if (spinner && spinner.isSpinning) {
1834
2047
  spinner.stop();
1835
2048
  }
1836
- const rescueMessage = `Recovered a local SaaS workspace scaffold for ${appName} after the V3 workflow stalled without writing files.`;
1837
- if (this.jsonOutput) {
1838
- console.log(JSON.stringify({
1839
- success: true,
1840
- mode: 'agent',
1841
- model: routingPolicy.selectedModel,
1842
- routingPolicy,
1843
- taskId: null,
1844
- contextId: null,
1845
- partial: true,
1846
- content: rescueMessage,
1847
- metadata: {
1848
- source: 'cli-saas-rescue',
1849
- previewGate,
1850
- rescueReason: error.message,
1851
- },
1852
- }, null, 2));
2049
+ if (!this.jsonOutput) {
2050
+ console.log(chalk_1.default.yellow(`V3 recovery: ${recovery.message} Retrying once...`));
1853
2051
  }
1854
- else {
1855
- console.log(chalk_1.default.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
1856
- console.log(rescueMessage);
1857
- if (previewGate.required) {
1858
- if (previewGate.passed) {
1859
- console.log(chalk_1.default.gray(`Template Service preview gate: passed via ${previewGate.backendUrl || 'unknown backend'}`));
1860
- }
1861
- else {
1862
- console.log(chalk_1.default.yellow(`Template Service preview gate: failed${previewGate.error ? ` - ${previewGate.error}` : ''}`));
1863
- }
2052
+ this.v3IterationCount = 0;
2053
+ this.v3ToolCallCount = 0;
2054
+ this.v3LastActivity = Date.now();
2055
+ this.v3StreamingStarted = false;
2056
+ try {
2057
+ const retryResponse = await this.api.runV3AgentWorkflow(executionPrompt, {
2058
+ workspace: { path: this.currentProjectPath },
2059
+ ...workspaceContext,
2060
+ executionSurface: 'cli',
2061
+ clientSurface: 'cli',
2062
+ localMachineCapable: true,
2063
+ agentTimeoutMs: DEFAULT_V3_AGENT_TIMEOUT_MS,
2064
+ agentIdleTimeoutMs: DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS,
2065
+ agentSoftTimeoutMs: DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS,
2066
+ model: routingPolicy.selectedModel,
2067
+ requestedModel: this.currentModel,
2068
+ agentExecutionPolicy: routingPolicy,
2069
+ legacyFallbackAllowed: this.isLegacyAgentFallbackAllowed(),
2070
+ rawPrompt,
2071
+ history: this.getMessagesForModel(),
2072
+ onStreamEvent: spinner ? (event) => this.updateV3AgentSpinner(spinner, event) : undefined,
2073
+ });
2074
+ if (spinner && spinner.isSpinning) {
2075
+ spinner.stop();
1864
2076
  }
2077
+ if (this.v3StreamingStarted) {
2078
+ process.stdout.write('\n');
2079
+ }
2080
+ if (this.jsonOutput) {
2081
+ console.log(JSON.stringify({
2082
+ success: true,
2083
+ mode: 'agent',
2084
+ model: routingPolicy.selectedModel,
2085
+ routingPolicy,
2086
+ taskId: retryResponse.taskId || null,
2087
+ contextId: retryResponse.contextId || null,
2088
+ partial: retryResponse.partial === true,
2089
+ content: retryResponse.content || 'V3 agent workflow completed after recovery.',
2090
+ metadata: { ...(retryResponse.metadata || {}), recoveryAttempted: true },
2091
+ }, null, 2));
2092
+ }
2093
+ else if (!this.v3StreamingStarted && retryResponse.content) {
2094
+ console.log(chalk_1.default.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
2095
+ console.log(retryResponse.content);
2096
+ }
2097
+ this.messages.push({ role: 'assistant', content: retryResponse.content || 'V3 agent workflow completed after recovery.' });
2098
+ return true;
2099
+ }
2100
+ catch {
2101
+ return false;
1865
2102
  }
1866
- this.messages.push({ role: 'assistant', content: rescueMessage });
1867
- return true;
1868
2103
  }
1869
2104
  async startInteractiveChat() {
1870
2105
  const chatTitle = this.operatorMode