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.
- package/README.md +16 -10
- package/dist/commands/auth.d.ts +36 -18
- package/dist/commands/auth.js +440 -329
- package/dist/commands/chat.d.ts +12 -0
- package/dist/commands/chat.js +287 -48
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +40 -20
- package/dist/commands/index.d.ts +12 -0
- package/dist/commands/index.js +182 -0
- package/dist/commands/legion.d.ts +49 -7
- package/dist/commands/legion.js +1418 -72
- package/dist/commands/preview.js +32 -7
- package/dist/commands/repo.js +19 -13
- package/dist/commands/update.d.ts +9 -0
- package/dist/commands/update.js +235 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +472 -51
- package/dist/utils/api.d.ts +24 -9
- package/dist/utils/api.js +720 -159
- package/dist/utils/config.js +9 -10
- package/dist/utils/context-ranker.d.ts +24 -0
- package/dist/utils/context-ranker.js +147 -0
- package/dist/utils/post-write-validator.d.ts +25 -0
- package/dist/utils/post-write-validator.js +138 -0
- package/dist/utils/session.d.ts +19 -0
- package/dist/utils/session.js +91 -6
- package/dist/utils/task-display.d.ts +31 -0
- package/dist/utils/task-display.js +115 -0
- package/dist/utils/tools.d.ts +26 -0
- package/dist/utils/tools.js +563 -58
- package/dist/utils/workspace-cache.d.ts +31 -0
- package/dist/utils/workspace-cache.js +96 -0
- package/package.json +13 -3
package/dist/commands/legion.js
CHANGED
|
@@ -9,12 +9,50 @@
|
|
|
9
9
|
* vigthoria legion --workers List available Legion workers
|
|
10
10
|
* vigthoria legion --status Show Legion infrastructure status
|
|
11
11
|
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
12
45
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
46
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
47
|
};
|
|
15
48
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
49
|
exports.LegionCommand = void 0;
|
|
17
50
|
const chalk_1 = __importDefault(require("chalk"));
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
const readline = __importStar(require("readline/promises"));
|
|
54
|
+
const node_child_process_1 = require("node:child_process");
|
|
55
|
+
const node_util_1 = require("node:util");
|
|
18
56
|
const logger_js_1 = require("../utils/logger.js");
|
|
19
57
|
const api_js_1 = require("../utils/api.js");
|
|
20
58
|
// Hyper Loop / Legion runs on the Vigthoria backend only. Local user installs
|
|
@@ -35,6 +73,10 @@ function buildServerHyperloopUrls() {
|
|
|
35
73
|
const HYPERLOOP_URLS = (0, api_js_1.isServerRuntime)()
|
|
36
74
|
? buildServerHyperloopUrls()
|
|
37
75
|
: (process.env.VIGTHORIA_HYPERLOOP_URL ? [process.env.VIGTHORIA_HYPERLOOP_URL] : []);
|
|
76
|
+
const CORTEX_WARN_BUDGET_USD = 3.5;
|
|
77
|
+
const CORTEX_HARD_BUDGET_USD = 5.0;
|
|
78
|
+
const CORTEX_MAX_ROUNDS = 2;
|
|
79
|
+
const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
|
|
38
80
|
class LegionCommand {
|
|
39
81
|
config;
|
|
40
82
|
logger;
|
|
@@ -42,8 +84,30 @@ class LegionCommand {
|
|
|
42
84
|
this.config = config;
|
|
43
85
|
this.logger = logger;
|
|
44
86
|
}
|
|
87
|
+
getHyperloopUrls() {
|
|
88
|
+
const urls = new Set();
|
|
89
|
+
const configuredApiUrl = String(this.config.get('apiUrl') || '').trim().replace(/\/$/, '');
|
|
90
|
+
if (configuredApiUrl) {
|
|
91
|
+
urls.add(`${configuredApiUrl}/api/hyperloop`);
|
|
92
|
+
}
|
|
93
|
+
const envUrl = String(process.env.VIGTHORIA_HYPERLOOP_URL || '').trim().replace(/\/$/, '');
|
|
94
|
+
if (envUrl) {
|
|
95
|
+
urls.add(envUrl);
|
|
96
|
+
}
|
|
97
|
+
if ((0, api_js_1.isServerRuntime)()) {
|
|
98
|
+
for (const internal of buildServerHyperloopUrls()) {
|
|
99
|
+
urls.add(internal.replace(/\/$/, ''));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return Array.from(urls);
|
|
103
|
+
}
|
|
45
104
|
getHeaders() {
|
|
46
105
|
const headers = { 'Content-Type': 'application/json' };
|
|
106
|
+
const serviceKey = this.getLegionServiceKey();
|
|
107
|
+
if (serviceKey) {
|
|
108
|
+
headers['X-Service-Key'] = serviceKey;
|
|
109
|
+
return headers;
|
|
110
|
+
}
|
|
47
111
|
const token = this.config.get('authToken');
|
|
48
112
|
if (token) {
|
|
49
113
|
headers['Authorization'] = `Bearer ${token}`;
|
|
@@ -51,7 +115,46 @@ class LegionCommand {
|
|
|
51
115
|
}
|
|
52
116
|
return headers;
|
|
53
117
|
}
|
|
118
|
+
async readJsonResponse(response, context) {
|
|
119
|
+
try {
|
|
120
|
+
return await response.json();
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
(0, api_js_1.propagateError)({
|
|
124
|
+
...((err && typeof err === 'object') ? err : { message: String(err) }),
|
|
125
|
+
message: `${context} returned invalid JSON: ${err?.message || String(err)}`,
|
|
126
|
+
statusCode: response.status,
|
|
127
|
+
commandName: 'legion',
|
|
128
|
+
endpoint: response.url || context,
|
|
129
|
+
details: {
|
|
130
|
+
...((err?.details && typeof err.details === 'object') ? err.details : {}),
|
|
131
|
+
command: 'legion',
|
|
132
|
+
endpoint: response.url || context,
|
|
133
|
+
context,
|
|
134
|
+
status: response.status,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
propagateLegionApiError(context, endpoint, err) {
|
|
140
|
+
const original = err && typeof err === 'object' ? err : { message: String(err) };
|
|
141
|
+
(0, api_js_1.propagateError)({
|
|
142
|
+
...original,
|
|
143
|
+
commandName: 'legion',
|
|
144
|
+
endpoint: original.endpoint || original?.config?.url || original?.details?.endpoint || endpoint || context,
|
|
145
|
+
details: {
|
|
146
|
+
...(original.details && typeof original.details === 'object' ? original.details : {}),
|
|
147
|
+
command: 'legion',
|
|
148
|
+
endpoint: original.endpoint || original?.config?.url || original?.details?.endpoint || endpoint || context,
|
|
149
|
+
context,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
54
153
|
async run(request, options) {
|
|
154
|
+
if (options.cortex) {
|
|
155
|
+
await this.runCortex(request, options);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
55
158
|
if (options.workers) {
|
|
56
159
|
await this.showWorkers();
|
|
57
160
|
return;
|
|
@@ -64,99 +167,1334 @@ class LegionCommand {
|
|
|
64
167
|
console.log(chalk_1.default.yellow('Usage: vigthoria legion "<task description>"'));
|
|
65
168
|
console.log(chalk_1.default.gray(' --workers List available Legion workers'));
|
|
66
169
|
console.log(chalk_1.default.gray(' --status Show Legion infrastructure status'));
|
|
170
|
+
console.log(chalk_1.default.gray(' --cortex Vigthoria Cortex: maximum intelligence execution'));
|
|
67
171
|
return;
|
|
68
172
|
}
|
|
69
173
|
await this.planAndExecute(request, options);
|
|
70
174
|
}
|
|
71
|
-
async
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
175
|
+
async runCortex(request, options) {
|
|
176
|
+
if (!request) {
|
|
177
|
+
console.log(chalk_1.default.yellow('Usage: vigthoria legion --cortex "<task description>"'));
|
|
178
|
+
console.log(chalk_1.default.gray(' --plan-only Run calculator only (no execution)'));
|
|
179
|
+
console.log(chalk_1.default.gray(' --force-budget Allow execution above hard safe-stop budget'));
|
|
180
|
+
console.log(chalk_1.default.gray(' --ignore-preflight Bypass mandatory preflight checks (no warranty)'));
|
|
181
|
+
console.log(chalk_1.default.gray(' --speed Speed mode: optional role skip on convergence'));
|
|
182
|
+
console.log(chalk_1.default.gray(' --tier heavy|lite Model tier: heavy=strongest LLMs (default), lite=efficient+affordable'));
|
|
183
|
+
console.log(chalk_1.default.gray(' --repro-cmd <cmd> Run a local reproducibility command before spend'));
|
|
184
|
+
console.log(chalk_1.default.gray(' --expect-repro-fail Require repro command to fail before execution'));
|
|
185
|
+
console.log(chalk_1.default.gray(' --approve Skip initial confirmation prompt and execute'));
|
|
186
|
+
console.log(chalk_1.default.gray(' --auto-charge Attempt direct VigCoin top-up when balance is low'));
|
|
187
|
+
console.log(chalk_1.default.gray(' --timeout <sec> Abort remote execution if no result within timeout (default: 120)'));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const workspace = options.project || process.cwd();
|
|
191
|
+
const scan = this.scanProject(workspace);
|
|
192
|
+
if (!options.ignorePreflight) {
|
|
193
|
+
const preflight = await this.runMandatoryPreflight(workspace, options.reproCmd, options.expectReproFail === true);
|
|
194
|
+
if (!preflight.ok) {
|
|
195
|
+
console.log(chalk_1.default.red('Cortex preflight failed.'));
|
|
196
|
+
console.log(chalk_1.default.red(` ${preflight.reason}`));
|
|
197
|
+
console.log(chalk_1.default.yellow('Execution halted before any cloud spend. Use --ignore-preflight to override (no warranty).'));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
console.log(chalk_1.default.green('Preflight passed.'));
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
console.log(chalk_1.default.yellow('Preflight bypassed by --ignore-preflight (no warranty).'));
|
|
204
|
+
}
|
|
205
|
+
const tier = (options.tier === 'lite') ? 'lite' : 'heavy';
|
|
206
|
+
const selectedModels = this.resolveModelProfiles(options.models, tier);
|
|
207
|
+
const quote = this.buildRoleQuote(scan, selectedModels, tier);
|
|
208
|
+
const billingQuote = this.buildBillingQuote(quote, tier);
|
|
209
|
+
if (billingQuote.retryAdjustedUsd > CORTEX_WARN_BUDGET_USD) {
|
|
210
|
+
console.log(chalk_1.default.yellow(`Estimated spend exceeds warning threshold ($${CORTEX_WARN_BUDGET_USD.toFixed(2)}).`));
|
|
211
|
+
}
|
|
212
|
+
if (billingQuote.retryAdjustedUsd > CORTEX_HARD_BUDGET_USD && !options.forceBudget) {
|
|
213
|
+
console.log(chalk_1.default.red(`Estimated spend exceeds hard budget ceiling ($${CORTEX_HARD_BUDGET_USD.toFixed(2)}).`));
|
|
214
|
+
console.log(chalk_1.default.yellow('Re-run with --force-budget to continue.'));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
let billingGate = await this.evaluateBillingGate(billingQuote);
|
|
218
|
+
this.printCortexQuote(workspace, scan, quote, billingQuote, billingGate);
|
|
219
|
+
if (options.planOnly) {
|
|
220
|
+
console.log(chalk_1.default.green('Cortex estimator complete (plan-only).'));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const autoApprove = options.approve === true && options.noApprove !== true;
|
|
224
|
+
const approved = autoApprove ? true : await this.confirmExecution();
|
|
225
|
+
if (!approved) {
|
|
226
|
+
console.log(chalk_1.default.yellow('Cortex cancelled by user.'));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
let round = 1;
|
|
230
|
+
let cumulativeUsd = 0;
|
|
231
|
+
let currentQuote = billingQuote;
|
|
232
|
+
let lastFailure = null;
|
|
233
|
+
while (round <= CORTEX_MAX_ROUNDS) {
|
|
234
|
+
if (!billingGate.canProceed) {
|
|
235
|
+
const resolved = await this.resolveBillingInsufficientFunds(currentQuote, billingGate, options);
|
|
236
|
+
if (!resolved) {
|
|
237
|
+
console.log(chalk_1.default.yellow('Cortex cancelled due to insufficient VigCoin balance.'));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
billingGate = await this.evaluateBillingGate(currentQuote);
|
|
241
|
+
if (!billingGate.canProceed) {
|
|
242
|
+
this.printBillingGateSummary(currentQuote, billingGate);
|
|
243
|
+
console.log(chalk_1.default.red('Billing gate still blocked after charge attempt.'));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const charged = await this.collectExecutionCharge(currentQuote, billingGate);
|
|
248
|
+
if (!charged) {
|
|
249
|
+
console.log(chalk_1.default.yellow('Cortex cancelled because wallet charge was not completed.'));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
cumulativeUsd += currentQuote.finalUsd;
|
|
253
|
+
const enrichedRequest = [
|
|
254
|
+
'[CORTEX EXECUTION]',
|
|
255
|
+
request,
|
|
256
|
+
`Workspace: ${workspace}`,
|
|
257
|
+
`PhaseA: files=${scan.files}, lines=${scan.lines}, import_edges=${scan.importEdges}`,
|
|
258
|
+
'Role model assignment:',
|
|
259
|
+
...quote.map((q) => `- ${q.role}: ${q.model} (requested_model=${q.requestedModel})`),
|
|
260
|
+
`Billing: round=${round}, cumulative_usd=${cumulativeUsd.toFixed(4)}, total_usd=${currentQuote.finalUsd.toFixed(4)}, plan=${billingGate.plan}, master_admin_free=${billingGate.masterAdminFree ? 'true' : 'false'}`,
|
|
261
|
+
`Execution mode: cloud-only core roles, speed_mode=${options.speed === true ? 'true' : 'false'}, tier=${tier}`,
|
|
262
|
+
lastFailure ? `Repair focus: previous failure at step=${lastFailure.failedStepId || 'unknown'} worker=${lastFailure.failedWorker || 'unknown'}` : '',
|
|
263
|
+
].filter(Boolean).join('\\n');
|
|
264
|
+
const execution = await this.planAndExecute(enrichedRequest, options, {
|
|
265
|
+
workspace,
|
|
266
|
+
originalRequest: request,
|
|
267
|
+
scan,
|
|
268
|
+
quote,
|
|
269
|
+
tier,
|
|
270
|
+
});
|
|
271
|
+
if (execution.status === 'completed') {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const failedRole = String(execution.failedStepId || '').toLowerCase();
|
|
275
|
+
if (this.isCriticalRoleFailure(failedRole)) {
|
|
276
|
+
console.log(chalk_1.default.red(`Fail-fast: critical role '${failedRole || 'unknown'}' failed. Manual correction required.`));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (!this.isOptionalRepairRoleFailure(failedRole)) {
|
|
280
|
+
console.log(chalk_1.default.red('Execution failed and is not eligible for automatic optional-role repair.'));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (round >= CORTEX_MAX_ROUNDS) {
|
|
284
|
+
console.log(chalk_1.default.red('Optional-role auto-repair budget exhausted.'));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const nextQuote = this.estimateAdditionalLoopQuote(currentQuote, execution);
|
|
288
|
+
const projectedTotal = cumulativeUsd + nextQuote.retryAdjustedUsd;
|
|
289
|
+
if (projectedTotal > CORTEX_HARD_BUDGET_USD && !options.forceBudget) {
|
|
290
|
+
console.log(chalk_1.default.red(`Additional loop would exceed hard budget ceiling ($${CORTEX_HARD_BUDGET_USD.toFixed(2)}).`));
|
|
291
|
+
console.log(chalk_1.default.yellow('Re-run with --force-budget to allow paid continuation.'));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const continueApproved = await this.confirmAdditionalLoopCharge(nextQuote, round + 1, execution);
|
|
295
|
+
if (!continueApproved) {
|
|
296
|
+
console.log(chalk_1.default.yellow('Termination: user declined additional budget for next round.'));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
currentQuote = nextQuote;
|
|
300
|
+
billingGate = await this.evaluateBillingGate(currentQuote);
|
|
301
|
+
lastFailure = execution;
|
|
302
|
+
round += 1;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
isCriticalRoleFailure(role) {
|
|
306
|
+
return role === 'logic' || role === 'security';
|
|
307
|
+
}
|
|
308
|
+
isOptionalRepairRoleFailure(role) {
|
|
309
|
+
return role === 'performance' || role === 'edge_case';
|
|
310
|
+
}
|
|
311
|
+
estimateAdditionalLoopQuote(previousQuote, execution) {
|
|
312
|
+
const failedRole = String(execution.failedStepId || '').toLowerCase();
|
|
313
|
+
const factor = this.isOptionalRepairRoleFailure(failedRole) ? 0.35 : 0.55;
|
|
314
|
+
const baseUsd = Math.max(0.1, previousQuote.baseUsd * factor);
|
|
315
|
+
const finalUsd = Math.max(0.15, previousQuote.finalUsd * factor);
|
|
316
|
+
const retryAdjustedUsd = Math.max(0.15, previousQuote.retryAdjustedUsd * factor);
|
|
317
|
+
const rangeMinUsd = finalUsd;
|
|
318
|
+
const rangeMaxUsd = retryAdjustedUsd;
|
|
319
|
+
return {
|
|
320
|
+
baseUsd,
|
|
321
|
+
marginPct: previousQuote.marginPct,
|
|
322
|
+
finalUsd,
|
|
323
|
+
retryAdjustedUsd,
|
|
324
|
+
rangeMinUsd,
|
|
325
|
+
rangeMaxUsd,
|
|
326
|
+
tier: previousQuote.tier,
|
|
327
|
+
vigcoinRateUsd: previousQuote.vigcoinRateUsd,
|
|
328
|
+
vigcoinRequired: retryAdjustedUsd / previousQuote.vigcoinRateUsd,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
async confirmAdditionalLoopCharge(nextQuote, nextRound, execution) {
|
|
332
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
336
|
+
try {
|
|
337
|
+
console.log();
|
|
338
|
+
console.log(chalk_1.default.yellow('Budget depleted for current round. Additional paid loop required.'));
|
|
339
|
+
console.log(chalk_1.default.gray(` Next round: ${nextRound}`));
|
|
340
|
+
console.log(chalk_1.default.gray(` Failed step: ${execution.failedStepId || 'unknown'} (${execution.failedWorker || 'unknown'})`));
|
|
341
|
+
console.log(chalk_1.default.gray(` Additional estimate: $${nextQuote.finalUsd.toFixed(4)} / ${nextQuote.vigcoinRequired.toFixed(3)} VIG`));
|
|
342
|
+
const answer = (await rl.question('Confirm additional VigCoin deduction and continue? (y/N): ')).trim().toLowerCase();
|
|
343
|
+
return answer === 'y' || answer === 'yes';
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
rl.close();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async runMandatoryPreflight(workspace, reproCmd, expectReproFail = false) {
|
|
350
|
+
try {
|
|
351
|
+
const stat = fs.statSync(workspace);
|
|
352
|
+
if (!stat.isDirectory()) {
|
|
353
|
+
return { ok: false, reason: `Workspace is not a directory: ${workspace}` };
|
|
354
|
+
}
|
|
355
|
+
fs.accessSync(workspace, fs.constants.R_OK | fs.constants.W_OK);
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
return { ok: false, reason: `Workspace access check failed: ${err?.message || err}` };
|
|
359
|
+
}
|
|
360
|
+
if (!reproCmd || !reproCmd.trim()) {
|
|
361
|
+
return { ok: true, reason: 'Workspace checks passed (no repro command supplied).' };
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
await execAsync(reproCmd, { cwd: workspace, timeout: 180000, maxBuffer: 1024 * 1024 * 8 });
|
|
365
|
+
if (expectReproFail) {
|
|
366
|
+
return { ok: false, reason: 'Repro command succeeded but --expect-repro-fail requires a failure signal.' };
|
|
367
|
+
}
|
|
368
|
+
return { ok: true, reason: 'Repro command succeeded.' };
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
if (expectReproFail) {
|
|
372
|
+
return { ok: true, reason: 'Repro command failed as expected.' };
|
|
373
|
+
}
|
|
374
|
+
const stderr = String(err?.stderr || err?.message || err).slice(0, 300);
|
|
375
|
+
return { ok: false, reason: `Repro command failed: ${stderr}` };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
scanProject(workspace) {
|
|
379
|
+
const files = [];
|
|
380
|
+
const stack = [workspace];
|
|
381
|
+
const ignored = new Set(['.git', 'node_modules', 'dist', 'build', 'coverage', '.next', '__pycache__', '.venv']);
|
|
382
|
+
const exts = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java']);
|
|
383
|
+
while (stack.length > 0 && files.length < 4000) {
|
|
384
|
+
const dir = stack.pop();
|
|
385
|
+
if (!dir)
|
|
386
|
+
continue;
|
|
387
|
+
let entries = [];
|
|
388
|
+
try {
|
|
389
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
for (const e of entries) {
|
|
395
|
+
if (ignored.has(e.name))
|
|
396
|
+
continue;
|
|
397
|
+
const full = path.join(dir, e.name);
|
|
398
|
+
if (e.isDirectory()) {
|
|
399
|
+
stack.push(full);
|
|
400
|
+
}
|
|
401
|
+
else if (e.isFile() && exts.has(path.extname(e.name).toLowerCase())) {
|
|
402
|
+
files.push(full);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
let lines = 0;
|
|
407
|
+
let importEdges = 0;
|
|
408
|
+
const topFiles = [];
|
|
409
|
+
for (const file of files) {
|
|
410
|
+
try {
|
|
411
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
412
|
+
const fileLines = content.split('\n').length;
|
|
413
|
+
const imports = (content.match(/^\s*(import\s+.+from\s+['"].+['"]|from\s+.+\s+import\s+.+|require\(.+\))/gm) || []).length;
|
|
414
|
+
lines += fileLines;
|
|
415
|
+
importEdges += imports;
|
|
416
|
+
topFiles.push({ file: path.relative(workspace, file).replace(/\\/g, '/'), lines: fileLines, imports });
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
topFiles.sort((a, b) => (b.lines + b.imports * 25) - (a.lines + a.imports * 25));
|
|
423
|
+
return { files: files.length, lines, importEdges, topFiles: topFiles.slice(0, 8) };
|
|
424
|
+
}
|
|
425
|
+
resolveModelProfiles(modelsArg, tier = 'heavy') {
|
|
426
|
+
// HEAVY tier: strongest available models — highest quality, highest cost.
|
|
427
|
+
const heavyCatalog = [
|
|
428
|
+
{ id: 'openrouter:openai/gpt-5.5', requestedModel: 'gpt-5.5', provider: 'openrouter', estInputPer1M: 5.0, estOutputPer1M: 30.0, capability: { reasoning: 10, coding: 10, security: 9, speed: 6, synthesis: 10 } },
|
|
429
|
+
{ id: 'openrouter:anthropic/claude-opus-4.7', requestedModel: 'opus-4.7', provider: 'openrouter', estInputPer1M: 5.0, estOutputPer1M: 25.0, capability: { reasoning: 10, coding: 9, security: 10, speed: 6, synthesis: 10 } },
|
|
430
|
+
{ id: 'openrouter:openai/o3', requestedModel: 'o3', provider: 'openrouter', estInputPer1M: 2.0, estOutputPer1M: 8.0, capability: { reasoning: 10, coding: 9, security: 9, speed: 6, synthesis: 9 } },
|
|
431
|
+
{ id: 'openrouter:google/gemini-2.5-pro', requestedModel: 'gemini-2.5-pro', provider: 'openrouter', estInputPer1M: 1.25, estOutputPer1M: 10.0, capability: { reasoning: 9, coding: 9, security: 8, speed: 8, synthesis: 9 } },
|
|
432
|
+
{ id: 'openrouter:deepseek/deepseek-v4-pro', requestedModel: 'deepseek-v4-pro', provider: 'openrouter', estInputPer1M: 0.435, estOutputPer1M: 0.87, capability: { reasoning: 8, coding: 9, security: 8, speed: 10, synthesis: 8 } },
|
|
433
|
+
{ id: 'openrouter:moonshotai/kimi-k2.5', requestedModel: 'kimi-k2.5', provider: 'openrouter', estInputPer1M: 0.44, estOutputPer1M: 2.0, capability: { reasoning: 9, coding: 9, security: 8, speed: 8, synthesis: 9 } },
|
|
434
|
+
{ id: 'openrouter:deepseek/deepseek-r1', requestedModel: 'cloud-reason', provider: 'openrouter', estInputPer1M: 0.7, estOutputPer1M: 2.5, capability: { reasoning: 9, coding: 8, security: 8, speed: 7, synthesis: 8 } },
|
|
435
|
+
{ id: 'openrouter:deepseek/deepseek-chat', requestedModel: 'cloud-pro', provider: 'openrouter', estInputPer1M: 0.32, estOutputPer1M: 0.89, capability: { reasoning: 7, coding: 8, security: 7, speed: 10, synthesis: 7 } },
|
|
436
|
+
];
|
|
437
|
+
// LITE tier: efficient models — high quality at ~5-10x lower cost than heavy.
|
|
438
|
+
// detective/architect → claude-sonnet-4-5 (best reasoning per dollar)
|
|
439
|
+
// security/reviewer → claude-haiku-3-5 (strong safety, very cheap)
|
|
440
|
+
// logic → o4-mini (reasoning specialist, fraction of o3 cost)
|
|
441
|
+
// edge_case → gemini-2.0-flash (long context, fast, cheap)
|
|
442
|
+
// integration → kimi-k2.5 (best codebase mapping at its price point)
|
|
443
|
+
// performance → deepseek-v3 (coding analysis, ~20% of DeepSeek-V4-Pro cost)
|
|
444
|
+
const liteCatalog = [
|
|
445
|
+
{ id: 'openrouter:anthropic/claude-sonnet-4-5', requestedModel: 'claude-sonnet-4-5', provider: 'openrouter', estInputPer1M: 3.0, estOutputPer1M: 15.0, capability: { reasoning: 9, coding: 9, security: 8, speed: 8, synthesis: 9 } },
|
|
446
|
+
{ id: 'openrouter:anthropic/claude-haiku-3-5', requestedModel: 'claude-haiku-3-5', provider: 'openrouter', estInputPer1M: 0.8, estOutputPer1M: 4.0, capability: { reasoning: 8, coding: 8, security: 9, speed: 10, synthesis: 8 } },
|
|
447
|
+
{ id: 'openrouter:openai/o4-mini', requestedModel: 'o4-mini', provider: 'openrouter', estInputPer1M: 1.1, estOutputPer1M: 4.4, capability: { reasoning: 9, coding: 8, security: 8, speed: 8, synthesis: 8 } },
|
|
448
|
+
{ id: 'openrouter:google/gemini-2.0-flash', requestedModel: 'gemini-2.0-flash', provider: 'openrouter', estInputPer1M: 0.1, estOutputPer1M: 0.4, capability: { reasoning: 7, coding: 8, security: 7, speed: 10, synthesis: 7 } },
|
|
449
|
+
{ id: 'openrouter:moonshotai/kimi-k2.5', requestedModel: 'kimi-k2.5', provider: 'openrouter', estInputPer1M: 0.44, estOutputPer1M: 2.0, capability: { reasoning: 9, coding: 9, security: 8, speed: 8, synthesis: 9 } },
|
|
450
|
+
{ id: 'openrouter:deepseek/deepseek-chat-v3', requestedModel: 'deepseek-v3', provider: 'openrouter', estInputPer1M: 0.27, estOutputPer1M: 1.1, capability: { reasoning: 8, coding: 9, security: 7, speed: 10, synthesis: 8 } },
|
|
451
|
+
{ id: 'openrouter:deepseek/deepseek-r1-distill-llama-70b', requestedModel: 'cloud-reason-lite', provider: 'openrouter', estInputPer1M: 0.23, estOutputPer1M: 0.69, capability: { reasoning: 8, coding: 7, security: 7, speed: 8, synthesis: 7 } },
|
|
452
|
+
{ id: 'openrouter:deepseek/deepseek-chat', requestedModel: 'cloud-pro', provider: 'openrouter', estInputPer1M: 0.32, estOutputPer1M: 0.89, capability: { reasoning: 7, coding: 8, security: 7, speed: 10, synthesis: 7 } },
|
|
453
|
+
];
|
|
454
|
+
const catalog = tier === 'lite' ? liteCatalog : heavyCatalog;
|
|
455
|
+
if (!modelsArg || !modelsArg.trim())
|
|
456
|
+
return catalog;
|
|
457
|
+
const allow = new Set(modelsArg.split(',').map((m) => m.trim()).filter(Boolean));
|
|
458
|
+
const filtered = catalog.filter((m) => (allow.has(m.id)
|
|
459
|
+
|| allow.has(m.id.replace('openrouter:', ''))
|
|
460
|
+
|| allow.has(m.requestedModel)));
|
|
461
|
+
return filtered.length > 0 ? filtered : catalog;
|
|
462
|
+
}
|
|
463
|
+
buildRoleQuote(scan, models, tier = 'heavy') {
|
|
464
|
+
const roleWeights = {
|
|
465
|
+
detective: { reasoning: 10, coding: 8, security: 6, speed: 3, synthesis: 7 },
|
|
466
|
+
logic: { reasoning: 8, coding: 10, security: 3, speed: 6, synthesis: 6 },
|
|
467
|
+
security: { reasoning: 7, coding: 7, security: 10, speed: 5, synthesis: 6 },
|
|
468
|
+
performance: { reasoning: 9, coding: 9, security: 7, speed: 5, synthesis: 7 },
|
|
469
|
+
edge_case: { reasoning: 8, coding: 7, security: 7, speed: 7, synthesis: 5 },
|
|
470
|
+
integration: { reasoning: 8, coding: 8, security: 6, speed: 5, synthesis: 8 },
|
|
471
|
+
reviewer: { reasoning: 9, coding: 7, security: 7, speed: 6, synthesis: 8 },
|
|
472
|
+
architect: { reasoning: 10, coding: 9, security: 8, speed: 4, synthesis: 10 },
|
|
473
|
+
};
|
|
474
|
+
// Preferred model per role for each tier.
|
|
475
|
+
const preferredByRoleHeavy = {
|
|
476
|
+
detective: 'gpt-5.5',
|
|
477
|
+
logic: 'o3',
|
|
478
|
+
security: 'opus-4.7',
|
|
479
|
+
performance: 'deepseek-v4-pro',
|
|
480
|
+
edge_case: 'gemini-2.5-pro',
|
|
481
|
+
integration: 'kimi-k2.5',
|
|
482
|
+
reviewer: 'opus-4.7',
|
|
483
|
+
architect: 'gpt-5.5',
|
|
484
|
+
};
|
|
485
|
+
const preferredByRoleLite = {
|
|
486
|
+
detective: 'claude-sonnet-4-5',
|
|
487
|
+
logic: 'o4-mini',
|
|
488
|
+
security: 'claude-haiku-3-5',
|
|
489
|
+
performance: 'deepseek-v3',
|
|
490
|
+
edge_case: 'gemini-2.0-flash',
|
|
491
|
+
integration: 'kimi-k2.5',
|
|
492
|
+
reviewer: 'claude-haiku-3-5',
|
|
493
|
+
architect: 'claude-sonnet-4-5',
|
|
494
|
+
};
|
|
495
|
+
const preferredByRole = tier === 'lite' ? preferredByRoleLite : preferredByRoleHeavy;
|
|
496
|
+
// Real-world token volumes from observed Cortex runs (not theoretical minimums).
|
|
497
|
+
// Base: 8k input tokens per role (context + project scan + prompt).
|
|
498
|
+
const complexity = Math.max(1, Math.ceil((scan.lines / 4000) + (scan.importEdges / 200)));
|
|
499
|
+
const baseInputTokens = 8000 * complexity; // observed avg: 8k-25k per role
|
|
500
|
+
const baseOutputTokens = 2200 * complexity; // observed avg: 1.8k-3.6k per role
|
|
501
|
+
return Object.keys(roleWeights).map((role) => {
|
|
502
|
+
const w = roleWeights[role];
|
|
503
|
+
let best = models.find((m) => m.requestedModel === preferredByRole[role]) || models[0];
|
|
504
|
+
if (!best)
|
|
505
|
+
best = models[0];
|
|
506
|
+
// Fallback scoring when preferred model is filtered out by --models.
|
|
507
|
+
if (!models.some((m) => m.requestedModel === preferredByRole[role])) {
|
|
508
|
+
let bestScore = -1;
|
|
509
|
+
for (const m of models) {
|
|
510
|
+
const score = m.capability.reasoning * w.reasoning + m.capability.coding * w.coding + m.capability.security * w.security + m.capability.speed * w.speed + m.capability.synthesis * w.synthesis;
|
|
511
|
+
const costPenalty = (m.estInputPer1M + m.estOutputPer1M) * 0.1;
|
|
512
|
+
const finalScore = score - costPenalty;
|
|
513
|
+
if (finalScore > bestScore) {
|
|
514
|
+
best = m;
|
|
515
|
+
bestScore = finalScore;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const estInputTokens = baseInputTokens;
|
|
520
|
+
const estOutputTokens = baseOutputTokens;
|
|
521
|
+
const estCostUsd = (estInputTokens / 1_000_000) * best.estInputPer1M + (estOutputTokens / 1_000_000) * best.estOutputPer1M;
|
|
522
|
+
return { role, model: best.id, requestedModel: best.requestedModel, estInputTokens, estOutputTokens, estCostUsd };
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
buildCortexExplicitSteps(execution, speedMode = false) {
|
|
526
|
+
const { originalRequest, workspace, scan, quote } = execution;
|
|
527
|
+
const quoteByRole = new Map(quote.map((row) => [row.role, row]));
|
|
528
|
+
const topFiles = scan.topFiles.slice(0, 5).map((file) => `${file.file} (${file.lines} lines, ${file.imports} imports)`);
|
|
529
|
+
const roleSequence = [
|
|
530
|
+
{ role: 'detective', dependsOn: [] },
|
|
531
|
+
{ role: 'logic', dependsOn: ['detective'] },
|
|
532
|
+
{ role: 'security', dependsOn: ['detective'] },
|
|
533
|
+
{ role: 'performance', dependsOn: ['detective'] },
|
|
534
|
+
{ role: 'edge_case', dependsOn: ['detective'] },
|
|
535
|
+
{ role: 'integration', dependsOn: ['detective'] },
|
|
536
|
+
{ role: 'reviewer', dependsOn: ['detective'] },
|
|
537
|
+
{ role: 'architect', dependsOn: ['logic', 'security', 'performance', 'edge_case', 'integration', 'reviewer'] },
|
|
538
|
+
];
|
|
539
|
+
const roleIterationBudget = {
|
|
540
|
+
detective: 4,
|
|
541
|
+
logic: 5,
|
|
542
|
+
security: speedMode ? 3 : 5,
|
|
543
|
+
performance: speedMode ? 3 : 5,
|
|
544
|
+
edge_case: speedMode ? 3 : 4,
|
|
545
|
+
integration: speedMode ? 3 : 6,
|
|
546
|
+
reviewer: speedMode ? 3 : 5,
|
|
547
|
+
architect: 5,
|
|
548
|
+
};
|
|
549
|
+
const optionalRoles = new Set(['security', 'performance', 'edge_case', 'integration', 'reviewer']);
|
|
550
|
+
const steps = roleSequence.map(({ role, dependsOn }) => {
|
|
551
|
+
const row = quoteByRole.get(role);
|
|
552
|
+
const isOptionalRole = optionalRoles.has(role);
|
|
553
|
+
const requestedModel = role === 'detective'
|
|
554
|
+
? 'gpt-5.5'
|
|
555
|
+
: role === 'architect'
|
|
556
|
+
? (scan.lines <= 3000 ? 'opus-4.7' : 'gpt-5.5')
|
|
557
|
+
: (row?.requestedModel || 'cloud-pro');
|
|
558
|
+
const model = role === 'detective'
|
|
559
|
+
? 'openrouter:openai/gpt-5.5'
|
|
560
|
+
: role === 'architect'
|
|
561
|
+
? (requestedModel === 'opus-4.7' ? 'openrouter:anthropic/claude-opus-4.7' : 'openrouter:openai/gpt-5.5')
|
|
562
|
+
: (row?.model || 'openrouter:deepseek/deepseek-chat');
|
|
563
|
+
// Compact instruction packet: role tag + request digest + role micro-prompt.
|
|
564
|
+
// Full workspace, scan stats, and key files are already in the payload.
|
|
565
|
+
const ROLE_MICRO_PROMPTS = {
|
|
566
|
+
detective: 'Root-cause analysis. Identify failing paths, unknown risks, and systemic gaps. Output: numbered findings with confidence level.',
|
|
567
|
+
logic: 'Verify iteration bounds, fallback chains, state transitions, race conditions. Output: PASS/FAIL per subsystem with evidence.',
|
|
568
|
+
security: 'Audit for auth bypass, injection flaws, unsafe exec, secret exposure, SSRF. Verdict: SAFE/RISK/CRITICAL per finding.',
|
|
569
|
+
performance: 'Find N+1 queries, unbounded loops, sync I/O in async paths, memory leaks. Output: hotspot list with severity.',
|
|
570
|
+
edge_case: 'Probe boundary conditions, null/undefined paths, concurrent mutations, retry storms. Output: concrete failure scenarios.',
|
|
571
|
+
integration: 'Verify endpoint registration, auth middleware wiring, env var presence, cross-service contracts. PASS/FAIL per check.',
|
|
572
|
+
reviewer: 'Final gate review. Confirm contract coverage, flag regressions. Verdict: CONDITIONAL-GO or NO-GO with rationale.',
|
|
573
|
+
architect: 'Synthesize all role findings into a production-readiness verdict with a prioritized action list.',
|
|
574
|
+
};
|
|
575
|
+
const reqDigest = originalRequest.length > 220
|
|
576
|
+
? originalRequest.slice(0, 220) + '\u2026'
|
|
577
|
+
: originalRequest;
|
|
578
|
+
const roleObjective = [
|
|
579
|
+
`[CORTEX:${role.toUpperCase()}] model=${requestedModel} budget=${roleIterationBudget[role] || 3}iter`,
|
|
580
|
+
reqDigest,
|
|
581
|
+
ROLE_MICRO_PROMPTS[role] || 'Analyse the codebase and deliver your findings concisely.',
|
|
582
|
+
].join('\\n');
|
|
583
|
+
return {
|
|
584
|
+
step_id: role,
|
|
585
|
+
worker_name: 'v3_agent_worker',
|
|
586
|
+
objective: roleObjective,
|
|
587
|
+
depends_on: dependsOn,
|
|
588
|
+
priority: role === 'architect' ? 2 : 3,
|
|
589
|
+
retry_policy: {
|
|
590
|
+
max_attempts: role === 'performance' || role === 'edge_case' ? 2 : 1,
|
|
591
|
+
strategy: 'repair',
|
|
592
|
+
requires_validation_failure: false,
|
|
593
|
+
},
|
|
594
|
+
payload: {
|
|
595
|
+
role,
|
|
596
|
+
requested_model: requestedModel,
|
|
597
|
+
quoted_model: model,
|
|
598
|
+
enforce_cloud_only: role === 'detective' || role === 'architect',
|
|
599
|
+
speed_mode: speedMode,
|
|
600
|
+
optional_role: isOptionalRole,
|
|
601
|
+
allow_convergence_skip: speedMode && isOptionalRole,
|
|
602
|
+
convergence_sources: ['detective', 'logic'],
|
|
603
|
+
workspace,
|
|
604
|
+
top_files: topFiles,
|
|
605
|
+
max_iterations: roleIterationBudget[role] || 4,
|
|
606
|
+
compact_context: true,
|
|
607
|
+
context_sniper_max_depth: 2,
|
|
608
|
+
max_dependency_chars: role === 'architect' ? 8000 : 4000,
|
|
609
|
+
max_artifacts: role === 'architect' ? 12 : 8,
|
|
610
|
+
max_context_chars: role === 'architect' ? 90000 : 60000,
|
|
611
|
+
max_output_tokens: role === 'detective' ? 3600 : role === 'architect' ? 3400 : role === 'reviewer' ? 2800 : 2200,
|
|
612
|
+
request_timeout_seconds: role === 'detective' || role === 'architect' ? 1200 : 720,
|
|
613
|
+
},
|
|
614
|
+
};
|
|
615
|
+
});
|
|
616
|
+
steps.push({
|
|
617
|
+
step_id: 'testing',
|
|
618
|
+
worker_name: 'testing_worker',
|
|
619
|
+
objective: [
|
|
620
|
+
'[CORTEX:TESTING]',
|
|
621
|
+
originalRequest,
|
|
622
|
+
`Workspace: ${workspace}`,
|
|
623
|
+
'Validate architect output with narrow relevant checks and report concrete failures only.',
|
|
624
|
+
].join('\\n'),
|
|
625
|
+
depends_on: ['architect'],
|
|
626
|
+
priority: 2,
|
|
627
|
+
retry_policy: {
|
|
628
|
+
max_attempts: 1,
|
|
629
|
+
strategy: 'repair',
|
|
630
|
+
requires_validation_failure: false,
|
|
77
631
|
},
|
|
78
|
-
|
|
632
|
+
payload: {
|
|
633
|
+
workspace,
|
|
634
|
+
validation_min_score: 80,
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
return steps;
|
|
638
|
+
}
|
|
639
|
+
buildBillingQuote(quote, tier = 'heavy') {
|
|
640
|
+
const baseUsd = quote.reduce((sum, r) => sum + r.estCostUsd, 0);
|
|
641
|
+
const marginPctRaw = Number.parseFloat(String(process.env.VIGTHORIA_CORTEX_MARGIN_PCT || process.env.VIGTHORIA_GODMODE_MARGIN_PCT || '10'));
|
|
642
|
+
const marginPct = Number.isFinite(marginPctRaw) ? Math.max(0, marginPctRaw) : 10;
|
|
643
|
+
const finalUsd = baseUsd * (1 + (marginPct / 100));
|
|
644
|
+
// Retry-adjusted estimate: all 7 critical roles can trigger the quality-gate
|
|
645
|
+
// retry in state_manager.py (adds +2 iterations per degraded role).
|
|
646
|
+
// Probability of retry per critical role ≈ 45% (from production telemetry).
|
|
647
|
+
// RETRY_ITER_RATIO = additional spend when retry fires: +2 iters / avg 5 iters base ≈ 40%.
|
|
648
|
+
const CRITICAL_ROLES = new Set(['detective', 'logic', 'security', 'performance', 'integration', 'reviewer', 'architect']);
|
|
649
|
+
const RETRY_PROB = 0.45;
|
|
650
|
+
const RETRY_ITER_RATIO = 0.40;
|
|
651
|
+
const retryExtra = quote
|
|
652
|
+
.filter((r) => CRITICAL_ROLES.has(r.role))
|
|
653
|
+
.reduce((sum, r) => sum + r.estCostUsd * RETRY_PROB * RETRY_ITER_RATIO, 0);
|
|
654
|
+
const retryAdjustedUsd = (baseUsd + retryExtra) * (1 + (marginPct / 100));
|
|
655
|
+
// Range bounds.
|
|
656
|
+
const rangeMinUsd = finalUsd; // single pass, no retries
|
|
657
|
+
const maxRetryExtra = quote
|
|
658
|
+
.filter((r) => CRITICAL_ROLES.has(r.role))
|
|
659
|
+
.reduce((sum, r) => sum + r.estCostUsd * RETRY_ITER_RATIO, 0);
|
|
660
|
+
const rangeMaxUsd = (baseUsd + maxRetryExtra) * (1 + (marginPct / 100));
|
|
661
|
+
const vigcoinRateRaw = Number.parseFloat(String(process.env.VIGTHORIA_VIGCOIN_USD_RATE || '1'));
|
|
662
|
+
const vigcoinRateUsd = Number.isFinite(vigcoinRateRaw) && vigcoinRateRaw > 0 ? vigcoinRateRaw : 1;
|
|
663
|
+
const vigcoinRequired = retryAdjustedUsd / vigcoinRateUsd;
|
|
664
|
+
return {
|
|
665
|
+
baseUsd,
|
|
666
|
+
marginPct,
|
|
667
|
+
finalUsd,
|
|
668
|
+
retryAdjustedUsd,
|
|
669
|
+
rangeMinUsd,
|
|
670
|
+
rangeMaxUsd,
|
|
671
|
+
tier,
|
|
672
|
+
vigcoinRateUsd,
|
|
673
|
+
vigcoinRequired,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
async evaluateBillingGate(billingQuote) {
|
|
677
|
+
const forcedPlan = String(process.env.VIGTHORIA_CORTEX_FORCE_PLAN || process.env.VIGTHORIA_GODMODE_FORCE_PLAN || '').trim().toLowerCase();
|
|
678
|
+
// On-server invocations using a service key run as trusted infrastructure.
|
|
679
|
+
// No user wallet check is needed — cost is tracked at the service level.
|
|
680
|
+
const hasServiceKey = !!(process.env.HYPERLOOP_SERVICE_KEY || process.env.V3_SERVICE_KEY);
|
|
681
|
+
if (hasServiceKey) {
|
|
682
|
+
return {
|
|
683
|
+
plan: forcedPlan || this.config.getNormalizedPlan() || 'service',
|
|
684
|
+
masterAdminFree: true,
|
|
685
|
+
canProceed: true,
|
|
686
|
+
wallet: {
|
|
687
|
+
available: true,
|
|
688
|
+
vigcoinBalance: null,
|
|
689
|
+
source: 'service_key_access',
|
|
690
|
+
purchaseUrl: null,
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const entitlement = await this.fetchCortexEntitlement();
|
|
695
|
+
const normalizedPlan = forcedPlan || entitlement.plan || this.config.getNormalizedPlan() || 'free';
|
|
696
|
+
const masterAdminFree = this.isMasterAdminFree(normalizedPlan, entitlement.masterAccess, entitlement.isMasterAdmin);
|
|
697
|
+
if (masterAdminFree) {
|
|
698
|
+
return {
|
|
699
|
+
plan: normalizedPlan,
|
|
700
|
+
masterAdminFree,
|
|
701
|
+
canProceed: true,
|
|
702
|
+
wallet: {
|
|
703
|
+
available: true,
|
|
704
|
+
vigcoinBalance: null,
|
|
705
|
+
source: 'master_admin_access',
|
|
706
|
+
purchaseUrl: null,
|
|
707
|
+
},
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
const wallet = await this.fetchWalletState();
|
|
711
|
+
if (!wallet.available || wallet.vigcoinBalance === null) {
|
|
712
|
+
return {
|
|
713
|
+
plan: normalizedPlan,
|
|
714
|
+
masterAdminFree,
|
|
715
|
+
canProceed: false,
|
|
716
|
+
wallet,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
return {
|
|
720
|
+
plan: normalizedPlan,
|
|
721
|
+
masterAdminFree,
|
|
722
|
+
canProceed: wallet.vigcoinBalance >= billingQuote.vigcoinRequired,
|
|
723
|
+
wallet,
|
|
79
724
|
};
|
|
80
|
-
|
|
725
|
+
}
|
|
726
|
+
parseBooleanCandidate(value) {
|
|
727
|
+
if (typeof value === 'boolean') {
|
|
728
|
+
return value;
|
|
729
|
+
}
|
|
730
|
+
if (typeof value === 'number') {
|
|
731
|
+
if (value === 1)
|
|
732
|
+
return true;
|
|
733
|
+
if (value === 0)
|
|
734
|
+
return false;
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
if (typeof value === 'string') {
|
|
738
|
+
const v = value.trim().toLowerCase();
|
|
739
|
+
if (['1', 'true', 'yes', 'enabled', 'on'].includes(v))
|
|
740
|
+
return true;
|
|
741
|
+
if (['0', 'false', 'no', 'disabled', 'off'].includes(v))
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
async fetchCortexEntitlement() {
|
|
747
|
+
if (process.env.VIGTHORIA_CORTEX_FORCE_MASTER_ACCESS === '1' || process.env.VIGTHORIA_GODMODE_FORCE_MASTER_ACCESS === '1') {
|
|
748
|
+
return { plan: this.config.getNormalizedPlan() || 'free', masterAccess: true, isMasterAdmin: true };
|
|
749
|
+
}
|
|
750
|
+
const baseUrl = this.getBillingBaseUrl();
|
|
751
|
+
const headers = this.getHeaders();
|
|
752
|
+
const endpoints = ['/api/user/subscription', '/api/user/info', '/api/user/profile'];
|
|
753
|
+
for (const endpoint of endpoints) {
|
|
81
754
|
try {
|
|
82
|
-
const response = await fetch(`${baseUrl}
|
|
83
|
-
method: '
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
signal: AbortSignal.timeout(300000),
|
|
755
|
+
const response = await fetch(`${baseUrl}${endpoint}`, {
|
|
756
|
+
method: 'GET',
|
|
757
|
+
signal: AbortSignal.timeout(12000),
|
|
758
|
+
headers,
|
|
87
759
|
});
|
|
88
|
-
if (!response.ok)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
760
|
+
if (!response.ok)
|
|
761
|
+
continue;
|
|
762
|
+
const payload = await response.json();
|
|
763
|
+
const data = (payload && typeof payload === 'object') ? payload : {};
|
|
764
|
+
const plan = String(data.subscription?.plan
|
|
765
|
+
|| data.subscription_plan
|
|
766
|
+
|| data.plan
|
|
767
|
+
|| data.user?.subscription_plan
|
|
768
|
+
|| data.user?.subscription?.plan
|
|
769
|
+
|| '').trim().toLowerCase() || null;
|
|
770
|
+
const accessCandidates = [
|
|
771
|
+
data.godmode_master_access,
|
|
772
|
+
data.godmodeMasterAccess,
|
|
773
|
+
data.subscription?.godmode_master_access,
|
|
774
|
+
data.subscription?.godmodeMasterAccess,
|
|
775
|
+
data.user?.godmode_master_access,
|
|
776
|
+
data.user?.godmodeMasterAccess,
|
|
777
|
+
];
|
|
778
|
+
const masterRoleCandidates = [
|
|
779
|
+
data.user?.isMasterAdmin,
|
|
780
|
+
data.user?.is_master_admin,
|
|
781
|
+
data.subscription?.isMasterAdmin,
|
|
782
|
+
data.subscription?.is_master_admin,
|
|
783
|
+
data.isMasterAdmin,
|
|
784
|
+
data.is_master_admin,
|
|
785
|
+
data.user?.admin_role === 'master_admin',
|
|
786
|
+
data.admin_role === 'master_admin',
|
|
787
|
+
];
|
|
788
|
+
const roleHit = masterRoleCandidates
|
|
789
|
+
.map((c) => this.parseBooleanCandidate(c))
|
|
790
|
+
.find((v) => v !== null) ?? false;
|
|
791
|
+
for (const c of accessCandidates) {
|
|
792
|
+
const parsed = this.parseBooleanCandidate(c);
|
|
793
|
+
if (parsed !== null) {
|
|
794
|
+
return { plan, masterAccess: parsed, isMasterAdmin: Boolean(roleHit) };
|
|
122
795
|
}
|
|
123
796
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
console.log();
|
|
127
|
-
console.log(chalk_1.default.gray(` Time: ${(result.elapsed_seconds || result.elapsed || 0).toFixed(1)}s`));
|
|
797
|
+
if (plan) {
|
|
798
|
+
return { plan, masterAccess: false, isMasterAdmin: Boolean(roleHit) };
|
|
128
799
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
800
|
+
}
|
|
801
|
+
catch (err) {
|
|
802
|
+
this.logger.warn(this.formatLegionError(`Cortex entitlement request ${endpoint}`, err));
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return {
|
|
807
|
+
plan: this.config.getNormalizedPlan() || null,
|
|
808
|
+
masterAccess: false,
|
|
809
|
+
isMasterAdmin: false,
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
isMasterAdminFree(plan, masterAccess, isMasterAdmin) {
|
|
813
|
+
if (!masterAccess) {
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
if (isMasterAdmin) {
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
return plan === 'master_admin';
|
|
820
|
+
}
|
|
821
|
+
getBillingBaseUrl() {
|
|
822
|
+
const configured = String(this.config.get('apiUrl') || '').trim().replace(/\/$/, '');
|
|
823
|
+
if (configured) {
|
|
824
|
+
return configured;
|
|
825
|
+
}
|
|
826
|
+
return 'https://coder.vigthoria.io';
|
|
827
|
+
}
|
|
828
|
+
parseNumericCandidate(value) {
|
|
829
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
830
|
+
return value;
|
|
831
|
+
}
|
|
832
|
+
if (typeof value === 'string') {
|
|
833
|
+
const parsed = Number.parseFloat(value.trim());
|
|
834
|
+
if (Number.isFinite(parsed)) {
|
|
835
|
+
return parsed;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
extractVigcoinBalance(data) {
|
|
841
|
+
const candidates = [
|
|
842
|
+
data.vigcoinBalance,
|
|
843
|
+
data.vigcoin_balance,
|
|
844
|
+
data.balance,
|
|
845
|
+
data.credits,
|
|
846
|
+
data.credit_balance,
|
|
847
|
+
data.wallet?.vigcoinBalance,
|
|
848
|
+
data.wallet?.vigcoin_balance,
|
|
849
|
+
data.wallet?.balance,
|
|
850
|
+
data.wallet?.credits,
|
|
851
|
+
data.subscription?.vigcoinBalance,
|
|
852
|
+
data.subscription?.vigcoin_balance,
|
|
853
|
+
data.subscription?.credits,
|
|
854
|
+
data.user?.vigcoinBalance,
|
|
855
|
+
data.user?.vigcoin_balance,
|
|
856
|
+
data.user?.credits,
|
|
857
|
+
data.data?.vigcoinBalance,
|
|
858
|
+
data.data?.vigcoin_balance,
|
|
859
|
+
data.data?.balance,
|
|
860
|
+
data.data?.credits,
|
|
861
|
+
];
|
|
862
|
+
for (const candidate of candidates) {
|
|
863
|
+
const parsed = this.parseNumericCandidate(candidate);
|
|
864
|
+
if (parsed !== null) {
|
|
865
|
+
return parsed;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
getPurchaseUrlFromPayload(baseUrl, data) {
|
|
871
|
+
const raw = data.purchaseUrl || data.purchase_url || data.checkoutUrl || data.checkout_url || data.url || data.data?.checkoutUrl || data.data?.url;
|
|
872
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
873
|
+
if (/^https?:\/\//i.test(raw.trim())) {
|
|
874
|
+
return raw.trim();
|
|
875
|
+
}
|
|
876
|
+
return `${baseUrl}${raw.startsWith('/') ? '' : '/'}${raw.trim()}`;
|
|
877
|
+
}
|
|
878
|
+
return `${baseUrl}/billing`;
|
|
879
|
+
}
|
|
880
|
+
async fetchWalletState() {
|
|
881
|
+
const baseUrl = this.getBillingBaseUrl();
|
|
882
|
+
const headers = this.getHeaders();
|
|
883
|
+
const forcedLow = process.env.VIGTHORIA_CORTEX_FORCE_LOW_CREDIT === '1' || process.env.VIGTHORIA_GODMODE_FORCE_LOW_CREDIT === '1';
|
|
884
|
+
const forcedBalanceRaw = process.env.VIGTHORIA_CORTEX_FORCE_BALANCE || process.env.VIGTHORIA_GODMODE_FORCE_BALANCE;
|
|
885
|
+
if (forcedLow) {
|
|
886
|
+
return {
|
|
887
|
+
available: true,
|
|
888
|
+
vigcoinBalance: 0,
|
|
889
|
+
source: 'forced_low_credit',
|
|
890
|
+
purchaseUrl: `${baseUrl}/music/store#vigcoins`,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
if (forcedBalanceRaw) {
|
|
894
|
+
const forced = this.parseNumericCandidate(forcedBalanceRaw);
|
|
895
|
+
if (forced !== null) {
|
|
896
|
+
return {
|
|
897
|
+
available: true,
|
|
898
|
+
vigcoinBalance: forced,
|
|
899
|
+
source: 'forced_balance',
|
|
900
|
+
purchaseUrl: `${baseUrl}/music/store#vigcoins`,
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
const endpoints = [
|
|
905
|
+
'/api/viagen6/vigcoin/balance',
|
|
906
|
+
'/api/user/subscription',
|
|
907
|
+
'/api/user/info',
|
|
908
|
+
'/api/user/profile',
|
|
909
|
+
'/api/wallet/balance',
|
|
910
|
+
'/api/billing/wallet',
|
|
911
|
+
'/api/billing/credits',
|
|
912
|
+
];
|
|
913
|
+
for (const endpoint of endpoints) {
|
|
914
|
+
try {
|
|
915
|
+
const response = await fetch(`${baseUrl}${endpoint}`, {
|
|
916
|
+
method: 'GET',
|
|
917
|
+
signal: AbortSignal.timeout(12000),
|
|
918
|
+
headers,
|
|
919
|
+
});
|
|
920
|
+
if (!response.ok) {
|
|
921
|
+
continue;
|
|
132
922
|
}
|
|
133
|
-
|
|
134
|
-
|
|
923
|
+
const payload = await this.readJsonResponse(response, `wallet balance request ${endpoint}`);
|
|
924
|
+
const payloadObj = (payload && typeof payload === 'object') ? payload : {};
|
|
925
|
+
const balance = this.extractVigcoinBalance(payloadObj);
|
|
926
|
+
if (balance === null) {
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
available: true,
|
|
931
|
+
vigcoinBalance: balance,
|
|
932
|
+
source: endpoint,
|
|
933
|
+
purchaseUrl: this.getPurchaseUrlFromPayload(baseUrl, payloadObj) || `${baseUrl}/music/store#vigcoins`,
|
|
934
|
+
};
|
|
135
935
|
}
|
|
136
|
-
catch (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
936
|
+
catch (err) {
|
|
937
|
+
this.logger.warn(this.formatLegionError(`wallet balance request ${endpoint}`, err));
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return {
|
|
942
|
+
available: false,
|
|
943
|
+
vigcoinBalance: null,
|
|
944
|
+
source: null,
|
|
945
|
+
purchaseUrl: `${baseUrl}/music/store#vigcoins`,
|
|
946
|
+
error: 'Wallet endpoint unavailable from current gateway session',
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
async attemptDirectCharge(vigcoinNeeded) {
|
|
950
|
+
const baseUrl = this.getBillingBaseUrl();
|
|
951
|
+
const headers = this.getHeaders();
|
|
952
|
+
const amount = Math.max(1, Math.ceil(vigcoinNeeded));
|
|
953
|
+
const chargePayloads = [
|
|
954
|
+
{ endpoint: '/api/viagen6/vigcoin/charge', body: { amount, reason: 'cortex_legion' } },
|
|
955
|
+
{ endpoint: '/api/wallet/charge', body: { amount, currency: 'VIGCOIN', reason: 'cortex_legion' } },
|
|
956
|
+
{ endpoint: '/api/billing/topup', body: { vigcoin: amount, reason: 'cortex_legion' } },
|
|
957
|
+
];
|
|
958
|
+
for (const attempt of chargePayloads) {
|
|
959
|
+
try {
|
|
960
|
+
const response = await fetch(`${baseUrl}${attempt.endpoint}`, {
|
|
961
|
+
method: 'POST',
|
|
962
|
+
signal: AbortSignal.timeout(15000),
|
|
963
|
+
headers,
|
|
964
|
+
body: JSON.stringify(attempt.body),
|
|
965
|
+
});
|
|
966
|
+
const payload = await response.json().catch((err) => {
|
|
967
|
+
this.logger.warn(this.formatLegionError(`direct VigCoin charge response ${attempt.endpoint}`, err));
|
|
968
|
+
return {};
|
|
969
|
+
});
|
|
970
|
+
const payloadObj = (payload && typeof payload === 'object') ? payload : {};
|
|
971
|
+
const checkoutUrl = this.getPurchaseUrlFromPayload(baseUrl, payloadObj) || `${baseUrl}/music/store#vigcoins`;
|
|
972
|
+
if (!response.ok) {
|
|
973
|
+
continue;
|
|
141
974
|
}
|
|
142
|
-
|
|
975
|
+
const charged = payloadObj.charged === true || payloadObj.success === true || payloadObj.status === 'charged';
|
|
976
|
+
if (charged) {
|
|
977
|
+
return { ok: true, checkoutUrl: checkoutUrl || undefined, note: `Charge accepted by ${attempt.endpoint}` };
|
|
978
|
+
}
|
|
979
|
+
return {
|
|
980
|
+
ok: false,
|
|
981
|
+
checkoutUrl: checkoutUrl || undefined,
|
|
982
|
+
note: payloadObj.error || payloadObj.message || `Checkout required via ${attempt.endpoint}`,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
catch (err) {
|
|
986
|
+
this.logger.warn(this.formatLegionError(`direct VigCoin charge request ${attempt.endpoint}`, err));
|
|
143
987
|
continue;
|
|
144
988
|
}
|
|
145
989
|
}
|
|
990
|
+
return {
|
|
991
|
+
ok: false,
|
|
992
|
+
checkoutUrl: `${baseUrl}/music/store#vigcoins`,
|
|
993
|
+
note: 'No direct charge endpoint accepted this request',
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
async collectExecutionCharge(billingQuote, gate) {
|
|
997
|
+
if (gate.masterAdminFree) {
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
const spinner = (0, logger_js_1.createSpinner)('Charging VigCoin wallet for Cortex execution...').start();
|
|
1001
|
+
const result = await this.attemptDirectCharge(billingQuote.vigcoinRequired);
|
|
146
1002
|
spinner.stop();
|
|
147
|
-
|
|
1003
|
+
if (!result.ok) {
|
|
1004
|
+
console.log(chalk_1.default.red(result.note || 'Wallet charge failed.'));
|
|
1005
|
+
console.log(chalk_1.default.yellow(`Complete purchase first: ${result.checkoutUrl || `${this.getBillingBaseUrl()}/music/store#vigcoins`}`));
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
console.log(chalk_1.default.green('Wallet charged for Cortex execution.'));
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
1011
|
+
async resolveBillingInsufficientFunds(billingQuote, gate, options) {
|
|
1012
|
+
this.printBillingGateSummary(billingQuote, gate);
|
|
1013
|
+
if (!gate.wallet.available) {
|
|
1014
|
+
console.log(chalk_1.default.red('Unable to verify wallet balance from server. Execution is blocked.'));
|
|
1015
|
+
if (gate.wallet.purchaseUrl) {
|
|
1016
|
+
console.log(chalk_1.default.gray(`Open billing portal: ${gate.wallet.purchaseUrl}`));
|
|
1017
|
+
}
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
if (options.autoCharge) {
|
|
1021
|
+
const spinner = (0, logger_js_1.createSpinner)('Attempting direct wallet charge...').start();
|
|
1022
|
+
const result = await this.attemptDirectCharge(billingQuote.vigcoinRequired - (gate.wallet.vigcoinBalance || 0));
|
|
1023
|
+
spinner.stop();
|
|
1024
|
+
if (result.ok) {
|
|
1025
|
+
console.log(chalk_1.default.green('Direct charge succeeded. Re-checking wallet balance...'));
|
|
1026
|
+
return true;
|
|
1027
|
+
}
|
|
1028
|
+
console.log(chalk_1.default.yellow(result.note || 'Direct charge did not complete.'));
|
|
1029
|
+
console.log(chalk_1.default.yellow(`Complete purchase first: ${result.checkoutUrl || gate.wallet.purchaseUrl || `${this.getBillingBaseUrl()}/billing`}`));
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1033
|
+
console.log(chalk_1.default.yellow('Low balance detected in non-interactive mode. Re-run with --auto-charge or top up first.'));
|
|
1034
|
+
if (gate.wallet.purchaseUrl) {
|
|
1035
|
+
console.log(chalk_1.default.gray(`Billing portal: ${gate.wallet.purchaseUrl}`));
|
|
1036
|
+
}
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1040
|
+
try {
|
|
1041
|
+
const answer = (await rl.question('VigCoin low. Choose action: [c]harge now, [p]urchase first, [n] cancel: ')).trim().toLowerCase();
|
|
1042
|
+
if (answer === 'c' || answer === 'charge') {
|
|
1043
|
+
const spinner = (0, logger_js_1.createSpinner)('Attempting direct wallet charge...').start();
|
|
1044
|
+
const result = await this.attemptDirectCharge(billingQuote.vigcoinRequired - (gate.wallet.vigcoinBalance || 0));
|
|
1045
|
+
spinner.stop();
|
|
1046
|
+
if (result.ok) {
|
|
1047
|
+
console.log(chalk_1.default.green('Direct charge succeeded. Re-checking wallet balance...'));
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
1050
|
+
console.log(chalk_1.default.yellow(result.note || 'Direct charge did not complete.'));
|
|
1051
|
+
console.log(chalk_1.default.yellow(`Complete purchase first: ${result.checkoutUrl || gate.wallet.purchaseUrl || `${this.getBillingBaseUrl()}/billing`}`));
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
1054
|
+
if (answer === 'p' || answer === 'purchase') {
|
|
1055
|
+
console.log(chalk_1.default.yellow(`Purchase VigCoin first: ${gate.wallet.purchaseUrl || `${this.getBillingBaseUrl()}/billing`}`));
|
|
1056
|
+
return false;
|
|
1057
|
+
}
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
1060
|
+
finally {
|
|
1061
|
+
rl.close();
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
printBillingGateSummary(billingQuote, gate) {
|
|
1065
|
+
console.log(chalk_1.default.white(' Billing gate:'));
|
|
1066
|
+
console.log(chalk_1.default.gray(` Plan: ${gate.plan}`));
|
|
1067
|
+
if (gate.masterAdminFree) {
|
|
1068
|
+
console.log(chalk_1.default.green(' Free tier override applied (Master Admin).'));
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
console.log(chalk_1.default.gray(` Estimated total (USD): $${billingQuote.retryAdjustedUsd.toFixed(4)}`) + chalk_1.default.gray(' (retry-adjusted expected)'));
|
|
1072
|
+
console.log(chalk_1.default.gray(` VigCoin rate: 1 VIG = $${billingQuote.vigcoinRateUsd.toFixed(4)}`));
|
|
1073
|
+
console.log(chalk_1.default.gray(` VigCoin required: ${billingQuote.vigcoinRequired.toFixed(3)}`));
|
|
1074
|
+
if (gate.wallet.vigcoinBalance !== null) {
|
|
1075
|
+
const color = gate.wallet.vigcoinBalance >= billingQuote.vigcoinRequired ? chalk_1.default.green : chalk_1.default.red;
|
|
1076
|
+
console.log(chalk_1.default.gray(' Wallet balance: ') + color(gate.wallet.vigcoinBalance.toFixed(3)) + (gate.wallet.source ? chalk_1.default.gray(` (source: ${gate.wallet.source})`) : ''));
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
console.log(chalk_1.default.red(` Wallet balance: unavailable${gate.wallet.error ? ` (${gate.wallet.error})` : ''}`));
|
|
1080
|
+
}
|
|
1081
|
+
if (gate.wallet.purchaseUrl) {
|
|
1082
|
+
console.log(chalk_1.default.gray(` Purchase URL: ${gate.wallet.purchaseUrl}`));
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
printCortexQuote(workspace, scan, quote, billingQuote, gate) {
|
|
1086
|
+
const tierLabel = billingQuote.tier === 'lite'
|
|
1087
|
+
? chalk_1.default.cyan('LITE') + chalk_1.default.gray(' — efficient (claude-sonnet/haiku, o4-mini, gemini-flash, deepseek-v3)')
|
|
1088
|
+
: chalk_1.default.magenta('HEAVY') + chalk_1.default.gray(' — strongest LLMs (gpt-5.5, opus-4.7, o3, gemini-2.5-pro)');
|
|
1089
|
+
console.log();
|
|
1090
|
+
console.log(chalk_1.default.bold.white(` ${logger_js_1.CH.hLine.repeat(3)} Vigthoria Cortex Estimator ${logger_js_1.CH.hLine.repeat(31)}`));
|
|
1091
|
+
console.log();
|
|
1092
|
+
console.log(chalk_1.default.gray(' Workspace: ') + chalk_1.default.white(workspace));
|
|
1093
|
+
console.log(chalk_1.default.gray(' Tier: ') + tierLabel);
|
|
1094
|
+
console.log(chalk_1.default.gray(' Files scanned: ') + chalk_1.default.white(String(scan.files)));
|
|
1095
|
+
console.log(chalk_1.default.gray(' Lines scanned: ') + chalk_1.default.white(String(scan.lines)));
|
|
1096
|
+
console.log(chalk_1.default.gray(' Dependency edges: ') + chalk_1.default.white(String(scan.importEdges)));
|
|
1097
|
+
if (scan.topFiles.length > 0) {
|
|
1098
|
+
console.log();
|
|
1099
|
+
console.log(chalk_1.default.white(' Top context files:'));
|
|
1100
|
+
for (const f of scan.topFiles) {
|
|
1101
|
+
console.log(chalk_1.default.gray(` ${logger_js_1.CH.bullet} ${f.file} (${f.lines} lines, ${f.imports} imports)`));
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
console.log();
|
|
1105
|
+
console.log(chalk_1.default.white(' Role assignment and estimated cost:'));
|
|
1106
|
+
for (const row of quote) {
|
|
1107
|
+
console.log(chalk_1.default.gray(` ${logger_js_1.CH.bullet} ${row.role.padEnd(11)} ${row.model} $${row.estCostUsd.toFixed(4)}`));
|
|
1108
|
+
}
|
|
1109
|
+
console.log();
|
|
1110
|
+
console.log(chalk_1.default.yellow(` Cost range (single-pass best case): $${billingQuote.rangeMinUsd.toFixed(4)}`));
|
|
1111
|
+
console.log(chalk_1.default.yellow(` Cost range (expected with retries): $${billingQuote.retryAdjustedUsd.toFixed(4)}`) + chalk_1.default.gray(' ← use this for budget planning'));
|
|
1112
|
+
console.log(chalk_1.default.yellow(` Cost range (worst case, all retries): $${billingQuote.rangeMaxUsd.toFixed(4)}`));
|
|
1113
|
+
console.log(chalk_1.default.gray(' Retry model: 45% chance per critical role triggers quality-gate (+40% iterations per retry).'));
|
|
1114
|
+
console.log(chalk_1.default.gray(' A mid-run checkpoint will appear when 70% of the expected estimate is consumed.'));
|
|
1115
|
+
console.log(chalk_1.default.gray(' Flow: Estimate -> Isolation -> Parallel Attack -> Synthesis'));
|
|
1116
|
+
console.log();
|
|
1117
|
+
this.printBillingGateSummary(billingQuote, gate);
|
|
1118
|
+
console.log();
|
|
1119
|
+
}
|
|
1120
|
+
async confirmExecution() {
|
|
1121
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1122
|
+
console.log(chalk_1.default.yellow('Non-interactive terminal detected. Re-run with --approve to continue execution.'));
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1126
|
+
try {
|
|
1127
|
+
const answer = (await rl.question('Proceed with Cortex execution? (y/N): ')).trim().toLowerCase();
|
|
1128
|
+
return answer === 'y' || answer === 'yes';
|
|
1129
|
+
}
|
|
1130
|
+
finally {
|
|
1131
|
+
rl.close();
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* SSE streaming URL for the Legion execution endpoint.
|
|
1136
|
+
* Always hits Hyper Loop directly (port 8020) with the service key to avoid
|
|
1137
|
+
* gateway JWT expiry killing long-running Cortex jobs.
|
|
1138
|
+
*/
|
|
1139
|
+
getLegionStreamUrl() {
|
|
1140
|
+
const envOverride = String(process.env.VIGTHORIA_HYPERLOOP_URL || '').trim().replace(/\/$/, '');
|
|
1141
|
+
if (envOverride)
|
|
1142
|
+
return `${envOverride}/legion/stream`;
|
|
1143
|
+
return 'http://localhost:8020/api/hyperloop/legion/stream';
|
|
1144
|
+
}
|
|
1145
|
+
getLegionServiceKey() {
|
|
1146
|
+
// Service key lets on-server CLI calls bypass gateway JWT validation
|
|
1147
|
+
return String(process.env.HYPERLOOP_SERVICE_KEY ||
|
|
1148
|
+
process.env.V3_SERVICE_KEY ||
|
|
1149
|
+
'');
|
|
1150
|
+
}
|
|
1151
|
+
async planAndExecute(request, options, cortexExecution) {
|
|
1152
|
+
const explicitSteps = cortexExecution ? this.buildCortexExplicitSteps(cortexExecution, options.speed === true) : undefined;
|
|
1153
|
+
const workspace = cortexExecution?.workspace || options.project || process.cwd();
|
|
1154
|
+
const body = {
|
|
1155
|
+
request,
|
|
1156
|
+
context: {
|
|
1157
|
+
workspace,
|
|
1158
|
+
governor_budget_secs: 600,
|
|
1159
|
+
governor_optional_roles: ['security', 'performance', 'edge_case', 'integration', 'reviewer'],
|
|
1160
|
+
},
|
|
1161
|
+
constraints: {
|
|
1162
|
+
active_only: true,
|
|
1163
|
+
execution_timeout_seconds: options.timeoutSec,
|
|
1164
|
+
preferred_workers: explicitSteps ? undefined : ['v3_agent_worker'],
|
|
1165
|
+
explicit_steps: explicitSteps,
|
|
1166
|
+
},
|
|
1167
|
+
};
|
|
1168
|
+
const streamUrl = this.getLegionStreamUrl();
|
|
1169
|
+
const serviceKey = this.getLegionServiceKey();
|
|
1170
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
1171
|
+
if (serviceKey) {
|
|
1172
|
+
headers['X-Service-Key'] = serviceKey;
|
|
1173
|
+
}
|
|
1174
|
+
else {
|
|
1175
|
+
const token = this.config.get('authToken');
|
|
1176
|
+
if (token) {
|
|
1177
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
1178
|
+
headers['Cookie'] = `vigthoria-auth-token=${token}`;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
const spinner = (0, logger_js_1.createSpinner)('Connecting to Legion SSE stream...').start();
|
|
1182
|
+
const startTime = Date.now();
|
|
1183
|
+
let response;
|
|
1184
|
+
try {
|
|
1185
|
+
response = await fetch(streamUrl, {
|
|
1186
|
+
method: 'POST',
|
|
1187
|
+
headers,
|
|
1188
|
+
body: JSON.stringify(body),
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
catch (connErr) {
|
|
1192
|
+
spinner.stop();
|
|
1193
|
+
this.logger.error(`Cannot connect to Hyper Loop at ${streamUrl}: ${connErr?.message || connErr}`);
|
|
1194
|
+
return { status: 'failed', plannedSteps: 0, completedSteps: 0 };
|
|
1195
|
+
}
|
|
1196
|
+
if (!response.ok) {
|
|
1197
|
+
spinner.stop();
|
|
1198
|
+
const errBody = await response.text().catch(() => '');
|
|
1199
|
+
this.logger.error(`Legion stream ${response.status}: ${(0, api_js_1.describeUpstreamStatus)(response.status)} — ${errBody.slice(0, 200)}`);
|
|
1200
|
+
return { status: 'failed', plannedSteps: 0, completedSteps: 0 };
|
|
1201
|
+
}
|
|
1202
|
+
if (!response.body) {
|
|
1203
|
+
spinner.stop();
|
|
1204
|
+
this.logger.error('Legion stream returned no response body');
|
|
1205
|
+
return { status: 'failed', plannedSteps: 0, completedSteps: 0 };
|
|
1206
|
+
}
|
|
1207
|
+
spinner.stop();
|
|
1208
|
+
console.log();
|
|
1209
|
+
console.log(chalk_1.default.bold.white(` ${logger_js_1.CH.hLine.repeat(3)} Legion Execution Report ${logger_js_1.CH.hLine.repeat(34)}`));
|
|
1210
|
+
console.log();
|
|
1211
|
+
const decoder = new TextDecoder();
|
|
1212
|
+
let buffer = '';
|
|
1213
|
+
let finalResult = null;
|
|
1214
|
+
let stepsTotal = 0;
|
|
1215
|
+
let stepsDone = 0;
|
|
1216
|
+
let finalStatus = 'failed';
|
|
1217
|
+
let failedStepId = '';
|
|
1218
|
+
let failedWorker = '';
|
|
1219
|
+
const streamEvents = [];
|
|
1220
|
+
// Mid-run budget checkpoint: accumulate estimated spend per completed role.
|
|
1221
|
+
const roleQuoteIndex = new Map((cortexExecution?.quote || []).map((q) => [q.role, q.estCostUsd]));
|
|
1222
|
+
const marginPctMidrun = Number.isFinite(Number.parseFloat(String(process.env.VIGTHORIA_CORTEX_MARGIN_PCT || '10')))
|
|
1223
|
+
? Math.max(0, Number.parseFloat(String(process.env.VIGTHORIA_CORTEX_MARGIN_PCT || '10')))
|
|
1224
|
+
: 10;
|
|
1225
|
+
const baseEstimateUsd = (cortexExecution?.quote || []).reduce((s, q) => s + q.estCostUsd, 0);
|
|
1226
|
+
const budgetCheckpointThreshold = cortexExecution
|
|
1227
|
+
? baseEstimateUsd * (1 + marginPctMidrun / 100) * 0.70
|
|
1228
|
+
: Infinity;
|
|
1229
|
+
let accumulatedEstUsd = 0;
|
|
1230
|
+
let budgetCheckpointFired = false;
|
|
1231
|
+
const completedRoleSummaries = [];
|
|
1232
|
+
try {
|
|
1233
|
+
const reader = response.body.getReader();
|
|
1234
|
+
while (true) {
|
|
1235
|
+
const { done, value } = await reader.read();
|
|
1236
|
+
if (done)
|
|
1237
|
+
break;
|
|
1238
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1239
|
+
const lines = buffer.split('\n');
|
|
1240
|
+
buffer = lines.pop() ?? '';
|
|
1241
|
+
for (const line of lines) {
|
|
1242
|
+
const trimmed = line.trim();
|
|
1243
|
+
if (!trimmed || trimmed.startsWith(':'))
|
|
1244
|
+
continue;
|
|
1245
|
+
if (!trimmed.startsWith('data:'))
|
|
1246
|
+
continue;
|
|
1247
|
+
const jsonStr = trimmed.slice(5).trim();
|
|
1248
|
+
let evt;
|
|
1249
|
+
try {
|
|
1250
|
+
evt = JSON.parse(jsonStr);
|
|
1251
|
+
}
|
|
1252
|
+
catch {
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
streamEvents.push(evt);
|
|
1256
|
+
switch (evt.event) {
|
|
1257
|
+
case 'plan':
|
|
1258
|
+
stepsTotal = evt.steps_total || 0;
|
|
1259
|
+
console.log(chalk_1.default.gray(` Planned workers: ${stepsTotal} steps queued`));
|
|
1260
|
+
console.log();
|
|
1261
|
+
break;
|
|
1262
|
+
case 'batch_start':
|
|
1263
|
+
console.log(chalk_1.default.gray(` ▶ Running: ${evt.workers.join(', ')} (${evt.steps_done}/${evt.steps_total})`));
|
|
1264
|
+
break;
|
|
1265
|
+
case 'step_complete': {
|
|
1266
|
+
stepsDone = Number(evt.steps_done) || 0;
|
|
1267
|
+
const icon = evt.status === 'completed' ? chalk_1.default.green(logger_js_1.CH.success) : chalk_1.default.red(logger_js_1.CH.error);
|
|
1268
|
+
const stepSummaryRaw = String(evt.summary || '');
|
|
1269
|
+
const summarySnip = stepSummaryRaw ? chalk_1.default.gray(` — ${stepSummaryRaw.slice(0, 120)}`) : '';
|
|
1270
|
+
console.log(` ${icon} ${chalk_1.default.white(String(evt.step_id))} ${chalk_1.default.gray('[' + evt.worker + ']')}${summarySnip}`);
|
|
1271
|
+
if (evt.status !== 'completed' && !failedStepId) {
|
|
1272
|
+
failedStepId = String(evt.step_id || '');
|
|
1273
|
+
failedWorker = String(evt.worker || '');
|
|
1274
|
+
}
|
|
1275
|
+
// Track spend and trigger mid-run budget checkpoint.
|
|
1276
|
+
const stepRole = String(evt.step_id || '');
|
|
1277
|
+
accumulatedEstUsd += roleQuoteIndex.get(stepRole) || 0;
|
|
1278
|
+
completedRoleSummaries.push({ role: stepRole, status: String(evt.status || ''), summary: stepSummaryRaw.slice(0, 200) });
|
|
1279
|
+
if (!budgetCheckpointFired
|
|
1280
|
+
&& cortexExecution
|
|
1281
|
+
&& accumulatedEstUsd >= budgetCheckpointThreshold
|
|
1282
|
+
&& budgetCheckpointThreshold < Infinity
|
|
1283
|
+
&& process.stdin.isTTY
|
|
1284
|
+
&& process.stdout.isTTY) {
|
|
1285
|
+
budgetCheckpointFired = true;
|
|
1286
|
+
const remainingRoles = (cortexExecution.quote || [])
|
|
1287
|
+
.filter((q) => !completedRoleSummaries.some((c) => c.role === q.role))
|
|
1288
|
+
.map((q) => q.role);
|
|
1289
|
+
const remainingEstUsd = (cortexExecution.quote || [])
|
|
1290
|
+
.filter((q) => remainingRoles.includes(q.role))
|
|
1291
|
+
.reduce((s, q) => s + q.estCostUsd, 0);
|
|
1292
|
+
console.log();
|
|
1293
|
+
console.log(chalk_1.default.bold.yellow(' ━━━ Mid-Run Budget Checkpoint ━━━'));
|
|
1294
|
+
console.log(chalk_1.default.gray(` Consumed so far (estimated): $${accumulatedEstUsd.toFixed(4)}`));
|
|
1295
|
+
console.log(chalk_1.default.gray(` Estimated remaining: $${remainingEstUsd.toFixed(4)}`));
|
|
1296
|
+
console.log();
|
|
1297
|
+
console.log(chalk_1.default.white(' Roles completed:'));
|
|
1298
|
+
for (const c of completedRoleSummaries) {
|
|
1299
|
+
const roleIcon = c.status === 'completed' ? chalk_1.default.green('✔') : chalk_1.default.red('✘');
|
|
1300
|
+
const roleSumSnip = c.summary ? ` — ${c.summary.slice(0, 100)}` : '';
|
|
1301
|
+
console.log(chalk_1.default.gray(` ${roleIcon} ${c.role.padEnd(11)}${roleSumSnip}`));
|
|
1302
|
+
}
|
|
1303
|
+
console.log();
|
|
1304
|
+
console.log(chalk_1.default.white(' Remaining roles: ') + chalk_1.default.gray(remainingRoles.join(', ') || 'none'));
|
|
1305
|
+
console.log();
|
|
1306
|
+
const checkpointRl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1307
|
+
let continueRun = false;
|
|
1308
|
+
try {
|
|
1309
|
+
const ans = (await checkpointRl.question(' Budget checkpoint — continue execution? (y/N): ')).trim().toLowerCase();
|
|
1310
|
+
continueRun = ans === 'y' || ans === 'yes';
|
|
1311
|
+
}
|
|
1312
|
+
finally {
|
|
1313
|
+
checkpointRl.close();
|
|
1314
|
+
}
|
|
1315
|
+
if (!continueRun) {
|
|
1316
|
+
console.log(chalk_1.default.yellow(' Cortex stopped by user at budget checkpoint.'));
|
|
1317
|
+
console.log(chalk_1.default.gray(` State: ${stepsDone} of ${stepsTotal} steps completed.`));
|
|
1318
|
+
finalStatus = 'failed';
|
|
1319
|
+
failedStepId = failedStepId || 'user_checkpoint_stop';
|
|
1320
|
+
// Break out of the for-of lines loop; the outer while(true) will end when reader.read() drains.
|
|
1321
|
+
break;
|
|
1322
|
+
}
|
|
1323
|
+
console.log(chalk_1.default.green(' Continuing execution…'));
|
|
1324
|
+
console.log();
|
|
1325
|
+
}
|
|
1326
|
+
break;
|
|
1327
|
+
}
|
|
1328
|
+
case 'complete':
|
|
1329
|
+
finalResult = evt.result || finalResult;
|
|
1330
|
+
if (Number.isFinite(Number(evt.steps_done))) {
|
|
1331
|
+
stepsDone = Number(evt.steps_done) || stepsDone;
|
|
1332
|
+
}
|
|
1333
|
+
if (Number.isFinite(Number(evt.steps_total))) {
|
|
1334
|
+
stepsTotal = Number(evt.steps_total) || stepsTotal;
|
|
1335
|
+
}
|
|
1336
|
+
if (!failedStepId && evt.failed_step_id) {
|
|
1337
|
+
failedStepId = String(evt.failed_step_id || '');
|
|
1338
|
+
}
|
|
1339
|
+
if (!failedWorker && evt.failed_worker) {
|
|
1340
|
+
failedWorker = String(evt.failed_worker || '');
|
|
1341
|
+
}
|
|
1342
|
+
if (evt.status === 'completed') {
|
|
1343
|
+
finalStatus = 'completed';
|
|
1344
|
+
console.log();
|
|
1345
|
+
console.log(chalk_1.default.green(` ${logger_js_1.CH.success} Legion completed successfully`));
|
|
1346
|
+
}
|
|
1347
|
+
else if (evt.status === 'failed') {
|
|
1348
|
+
finalStatus = 'failed';
|
|
1349
|
+
console.log();
|
|
1350
|
+
console.log(chalk_1.default.red(` ${logger_js_1.CH.error} Legion execution failed`));
|
|
1351
|
+
if (evt.error)
|
|
1352
|
+
console.log(chalk_1.default.red(` Error: ${String(evt.error).slice(0, 300)}`));
|
|
1353
|
+
}
|
|
1354
|
+
break;
|
|
1355
|
+
case 'error':
|
|
1356
|
+
finalStatus = 'failed';
|
|
1357
|
+
console.log(chalk_1.default.red(` ${logger_js_1.CH.error} Stream error: ${String(evt.error).slice(0, 300)}`));
|
|
1358
|
+
break;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
catch (streamErr) {
|
|
1364
|
+
this.logger.error(`Legion stream read error: ${streamErr?.message || streamErr}`);
|
|
1365
|
+
finalStatus = 'failed';
|
|
1366
|
+
}
|
|
1367
|
+
const elapsedSec = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
1368
|
+
if (stepsTotal > 0 && stepsDone < stepsTotal) {
|
|
1369
|
+
console.log(chalk_1.default.yellow(` ${logger_js_1.CH.warn} Legion stream ended after ${stepsDone}/${stepsTotal} steps`));
|
|
1370
|
+
}
|
|
1371
|
+
console.log();
|
|
1372
|
+
console.log(chalk_1.default.gray(` Time: ${elapsedSec}s`));
|
|
1373
|
+
const lastStepResult = finalResult?.final_output;
|
|
1374
|
+
const summary = String(lastStepResult?.result?.summary || lastStepResult?.summary || '');
|
|
1375
|
+
if (summary) {
|
|
1376
|
+
console.log();
|
|
1377
|
+
console.log(chalk_1.default.white(' Final output:'));
|
|
1378
|
+
console.log(chalk_1.default.gray(` ${summary.slice(0, 600)}`));
|
|
1379
|
+
}
|
|
1380
|
+
if (cortexExecution) {
|
|
1381
|
+
const report = this.buildCortexRunReport({
|
|
1382
|
+
generatedAt: new Date().toISOString(),
|
|
1383
|
+
status: finalStatus,
|
|
1384
|
+
elapsedSeconds: Number(elapsedSec),
|
|
1385
|
+
workspace: cortexExecution.workspace,
|
|
1386
|
+
request: cortexExecution.originalRequest,
|
|
1387
|
+
plannedSteps: stepsTotal,
|
|
1388
|
+
completedSteps: stepsDone,
|
|
1389
|
+
modifiedFiles: this.extractModifiedFiles(finalResult),
|
|
1390
|
+
workers: [],
|
|
1391
|
+
finalSummary: summary,
|
|
1392
|
+
}, streamEvents);
|
|
1393
|
+
const files = this.writeCortexSummaryReport(report);
|
|
1394
|
+
console.log(chalk_1.default.gray(' Cortex summary: ' + files.markdownPath));
|
|
1395
|
+
console.log(chalk_1.default.gray(' Cortex report JSON: ' + files.jsonPath));
|
|
1396
|
+
}
|
|
1397
|
+
console.log();
|
|
1398
|
+
return {
|
|
1399
|
+
status: finalStatus,
|
|
1400
|
+
plannedSteps: stepsTotal,
|
|
1401
|
+
completedSteps: stepsDone,
|
|
1402
|
+
failedStepId: failedStepId || undefined,
|
|
1403
|
+
failedWorker: failedWorker || undefined,
|
|
1404
|
+
finalSummary: summary || undefined,
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
buildCortexRunReport(base, streamEvents) {
|
|
1408
|
+
const workers = streamEvents
|
|
1409
|
+
.filter((evt) => evt && evt.event === 'step_complete')
|
|
1410
|
+
.map((evt) => ({
|
|
1411
|
+
stepId: String(evt.step_id || ''),
|
|
1412
|
+
worker: String(evt.worker || ''),
|
|
1413
|
+
status: String(evt.status || ''),
|
|
1414
|
+
summary: String(evt.summary || ''),
|
|
1415
|
+
}));
|
|
1416
|
+
return { ...base, workers };
|
|
1417
|
+
}
|
|
1418
|
+
extractModifiedFiles(finalResult) {
|
|
1419
|
+
if (!finalResult || typeof finalResult !== 'object')
|
|
1420
|
+
return [];
|
|
1421
|
+
const candidates = [];
|
|
1422
|
+
const finalOutput = (finalResult.final_output && typeof finalResult.final_output === 'object') ? finalResult.final_output : null;
|
|
1423
|
+
const result = (finalOutput?.result && typeof finalOutput.result === 'object') ? finalOutput.result : null;
|
|
1424
|
+
for (const key of ['modified_files', 'changed_files', 'files_changed', 'touched_files']) {
|
|
1425
|
+
const top = finalResult[key];
|
|
1426
|
+
const res = result ? result[key] : undefined;
|
|
1427
|
+
if (Array.isArray(top))
|
|
1428
|
+
candidates.push(...top.map((x) => String(x || '').trim()));
|
|
1429
|
+
if (Array.isArray(res))
|
|
1430
|
+
candidates.push(...res.map((x) => String(x || '').trim()));
|
|
1431
|
+
}
|
|
1432
|
+
return Array.from(new Set(candidates.filter((x) => x.length > 0)));
|
|
1433
|
+
}
|
|
1434
|
+
writeCortexSummaryReport(report) {
|
|
1435
|
+
const reportsDir = path.join(report.workspace, '.vigthoria', 'reports');
|
|
1436
|
+
fs.mkdirSync(reportsDir, { recursive: true });
|
|
1437
|
+
const stamp = report.generatedAt.replace(/[:.]/g, '-');
|
|
1438
|
+
const markdownPath = path.join(reportsDir, 'CORTEX_SUMMARY_' + stamp + '.md');
|
|
1439
|
+
const jsonPath = path.join(reportsDir, 'CORTEX_SUMMARY_' + stamp + '.json');
|
|
1440
|
+
const workerLines = report.workers.length > 0
|
|
1441
|
+
? report.workers.map((w) => '- ' + w.stepId + ' [' + w.worker + '] ' + w.status + (w.summary ? ': ' + w.summary : '')).join('\n')
|
|
1442
|
+
: '- No worker step summaries captured';
|
|
1443
|
+
const modifiedLines = report.modifiedFiles.length > 0
|
|
1444
|
+
? report.modifiedFiles.map((f) => '- ' + f).join('\n')
|
|
1445
|
+
: '- No modified files reported by backend outputs';
|
|
1446
|
+
const markdown = [
|
|
1447
|
+
'# Cortex Run Summary',
|
|
1448
|
+
'',
|
|
1449
|
+
'Generated: ' + report.generatedAt,
|
|
1450
|
+
'Status: ' + report.status,
|
|
1451
|
+
'Elapsed: ' + report.elapsedSeconds + 's',
|
|
1452
|
+
'Workspace: ' + report.workspace,
|
|
1453
|
+
'',
|
|
1454
|
+
'## Request',
|
|
1455
|
+
report.request,
|
|
1456
|
+
'',
|
|
1457
|
+
'## Execution',
|
|
1458
|
+
'- Planned steps: ' + report.plannedSteps,
|
|
1459
|
+
'- Completed steps: ' + report.completedSteps,
|
|
1460
|
+
'',
|
|
1461
|
+
'## Worker Results',
|
|
1462
|
+
workerLines,
|
|
1463
|
+
'',
|
|
1464
|
+
'## Modified Files',
|
|
1465
|
+
modifiedLines,
|
|
1466
|
+
'',
|
|
1467
|
+
'## Final Summary',
|
|
1468
|
+
report.finalSummary || 'No final summary reported by backend',
|
|
1469
|
+
'',
|
|
1470
|
+
].join('\n');
|
|
1471
|
+
fs.writeFileSync(markdownPath, markdown, 'utf8');
|
|
1472
|
+
fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2) + '\n', 'utf8');
|
|
1473
|
+
return { markdownPath, jsonPath };
|
|
1474
|
+
}
|
|
1475
|
+
formatLegionError(context, err) {
|
|
1476
|
+
const message = err?.message || String(err || 'Unknown error');
|
|
1477
|
+
const cause = err?.cause?.message ? ` (${err.cause.message})` : '';
|
|
1478
|
+
return `${context} failed: ${message}${cause}`;
|
|
148
1479
|
}
|
|
149
1480
|
async showWorkers() {
|
|
150
1481
|
const spinner = (0, logger_js_1.createSpinner)('Fetching Legion worker catalog...').start();
|
|
151
|
-
|
|
1482
|
+
let lastError = null;
|
|
1483
|
+
for (const baseUrl of this.getHyperloopUrls()) {
|
|
152
1484
|
try {
|
|
153
1485
|
const response = await fetch(`${baseUrl}/legion/workers`, {
|
|
154
1486
|
signal: AbortSignal.timeout(10000),
|
|
155
1487
|
headers: this.getHeaders(),
|
|
156
1488
|
});
|
|
157
|
-
if (!response.ok)
|
|
1489
|
+
if (!response.ok) {
|
|
1490
|
+
const errBody = await response.text().catch((err) => {
|
|
1491
|
+
this.logger.warn(this.formatLegionError(`worker catalog error body at ${baseUrl}`, err));
|
|
1492
|
+
return '';
|
|
1493
|
+
});
|
|
1494
|
+
lastError = `Legion worker catalog request at ${baseUrl} failed: ${response.status} ${(0, api_js_1.describeUpstreamStatus)(response.status)}${errBody ? ` — ${errBody.slice(0, 200)}` : ''}`;
|
|
158
1495
|
continue;
|
|
159
|
-
|
|
1496
|
+
}
|
|
1497
|
+
const data = await this.readJsonResponse(response, `worker catalog request at ${baseUrl}`);
|
|
160
1498
|
spinner.stop();
|
|
161
1499
|
console.log();
|
|
162
1500
|
console.log(chalk_1.default.bold.white(` ${logger_js_1.CH.hLine.repeat(3)} Legion Worker Catalog ${logger_js_1.CH.hLine.repeat(37)}`));
|
|
@@ -186,24 +1524,28 @@ class LegionCommand {
|
|
|
186
1524
|
console.log();
|
|
187
1525
|
return;
|
|
188
1526
|
}
|
|
189
|
-
catch {
|
|
1527
|
+
catch (err) {
|
|
1528
|
+
lastError = this.formatLegionError(`worker catalog request at ${baseUrl}`, err);
|
|
190
1529
|
continue;
|
|
191
1530
|
}
|
|
192
1531
|
}
|
|
193
1532
|
spinner.stop();
|
|
194
|
-
this.logger.error('Could not reach Hyper Loop. Is vigthoria-hyper-loop running?');
|
|
1533
|
+
this.logger.error(lastError || 'Could not reach Hyper Loop. Is vigthoria-hyper-loop running?');
|
|
195
1534
|
}
|
|
196
1535
|
async showStatus() {
|
|
197
1536
|
const spinner = (0, logger_js_1.createSpinner)('Checking Legion infrastructure...').start();
|
|
198
|
-
|
|
1537
|
+
let lastError = null;
|
|
1538
|
+
for (const baseUrl of this.getHyperloopUrls()) {
|
|
199
1539
|
try {
|
|
200
1540
|
const response = await fetch(`${baseUrl}/status`, {
|
|
201
1541
|
signal: AbortSignal.timeout(10000),
|
|
202
1542
|
headers: this.getHeaders(),
|
|
203
1543
|
});
|
|
204
|
-
if (!response.ok)
|
|
1544
|
+
if (!response.ok) {
|
|
1545
|
+
lastError = `Legion status check at ${baseUrl} failed: ${response.status} ${(0, api_js_1.describeUpstreamStatus)(response.status)}`;
|
|
205
1546
|
continue;
|
|
206
|
-
|
|
1547
|
+
}
|
|
1548
|
+
const data = await this.readJsonResponse(response, `status check at ${baseUrl}`);
|
|
207
1549
|
spinner.stop();
|
|
208
1550
|
console.log();
|
|
209
1551
|
console.log(chalk_1.default.bold.white(` ${logger_js_1.CH.hLine.repeat(3)} Legion Infrastructure ${logger_js_1.CH.hLine.repeat(37)}`));
|
|
@@ -225,13 +1567,17 @@ class LegionCommand {
|
|
|
225
1567
|
console.log();
|
|
226
1568
|
return;
|
|
227
1569
|
}
|
|
228
|
-
catch {
|
|
1570
|
+
catch (err) {
|
|
1571
|
+
lastError = this.formatLegionError(`status check at ${baseUrl}`, err);
|
|
229
1572
|
continue;
|
|
230
1573
|
}
|
|
231
1574
|
}
|
|
232
1575
|
spinner.stop();
|
|
233
1576
|
console.log();
|
|
234
1577
|
console.log(chalk_1.default.gray(' Hyper Loop: ') + chalk_1.default.red('offline'));
|
|
1578
|
+
if (lastError) {
|
|
1579
|
+
this.logger.error(lastError);
|
|
1580
|
+
}
|
|
235
1581
|
console.log();
|
|
236
1582
|
}
|
|
237
1583
|
}
|