vigthoria-cli 1.9.2 → 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 +15 -5
- package/dist/commands/auth.d.ts +28 -38
- package/dist/commands/auth.js +389 -320
- package/dist/commands/chat.d.ts +3 -0
- package/dist/commands/chat.js +66 -15
- package/dist/commands/index.js +1 -1
- package/dist/commands/legion.d.ts +22 -19
- package/dist/commands/legion.js +550 -132
- 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 +2 -1
- package/dist/index.js +114 -33
- package/dist/utils/api.d.ts +25 -70
- package/dist/utils/api.js +784 -695
- package/dist/utils/tools.d.ts +11 -0
- package/dist/utils/tools.js +222 -0
- package/package.json +7 -1
package/dist/commands/legion.js
CHANGED
|
@@ -51,6 +51,8 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
51
51
|
const fs = __importStar(require("fs"));
|
|
52
52
|
const path = __importStar(require("path"));
|
|
53
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");
|
|
54
56
|
const logger_js_1 = require("../utils/logger.js");
|
|
55
57
|
const api_js_1 = require("../utils/api.js");
|
|
56
58
|
// Hyper Loop / Legion runs on the Vigthoria backend only. Local user installs
|
|
@@ -71,6 +73,10 @@ function buildServerHyperloopUrls() {
|
|
|
71
73
|
const HYPERLOOP_URLS = (0, api_js_1.isServerRuntime)()
|
|
72
74
|
? buildServerHyperloopUrls()
|
|
73
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);
|
|
74
80
|
class LegionCommand {
|
|
75
81
|
config;
|
|
76
82
|
logger;
|
|
@@ -145,8 +151,8 @@ class LegionCommand {
|
|
|
145
151
|
});
|
|
146
152
|
}
|
|
147
153
|
async run(request, options) {
|
|
148
|
-
if (options.
|
|
149
|
-
await this.
|
|
154
|
+
if (options.cortex) {
|
|
155
|
+
await this.runCortex(request, options);
|
|
150
156
|
return;
|
|
151
157
|
}
|
|
152
158
|
if (options.workers) {
|
|
@@ -161,71 +167,213 @@ class LegionCommand {
|
|
|
161
167
|
console.log(chalk_1.default.yellow('Usage: vigthoria legion "<task description>"'));
|
|
162
168
|
console.log(chalk_1.default.gray(' --workers List available Legion workers'));
|
|
163
169
|
console.log(chalk_1.default.gray(' --status Show Legion infrastructure status'));
|
|
164
|
-
console.log(chalk_1.default.gray(' --
|
|
170
|
+
console.log(chalk_1.default.gray(' --cortex Vigthoria Cortex: maximum intelligence execution'));
|
|
165
171
|
return;
|
|
166
172
|
}
|
|
167
173
|
await this.planAndExecute(request, options);
|
|
168
174
|
}
|
|
169
|
-
async
|
|
175
|
+
async runCortex(request, options) {
|
|
170
176
|
if (!request) {
|
|
171
|
-
console.log(chalk_1.default.yellow('Usage: vigthoria legion --
|
|
172
|
-
console.log(chalk_1.default.gray(' --plan-only
|
|
173
|
-
console.log(chalk_1.default.gray(' --
|
|
174
|
-
console.log(chalk_1.default.gray(' --
|
|
175
|
-
console.log(chalk_1.default.gray(' --
|
|
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)'));
|
|
176
188
|
return;
|
|
177
189
|
}
|
|
178
190
|
const workspace = options.project || process.cwd();
|
|
179
191
|
const scan = this.scanProject(workspace);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
+
}
|
|
183
217
|
let billingGate = await this.evaluateBillingGate(billingQuote);
|
|
184
|
-
this.
|
|
218
|
+
this.printCortexQuote(workspace, scan, quote, billingQuote, billingGate);
|
|
185
219
|
if (options.planOnly) {
|
|
186
|
-
console.log(chalk_1.default.green('
|
|
220
|
+
console.log(chalk_1.default.green('Cortex estimator complete (plan-only).'));
|
|
187
221
|
return;
|
|
188
222
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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.'));
|
|
193
250
|
return;
|
|
194
251
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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') {
|
|
199
272
|
return;
|
|
200
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;
|
|
201
303
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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;
|
|
207
334
|
}
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
console.log(
|
|
211
|
-
|
|
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}` };
|
|
212
376
|
}
|
|
213
|
-
const enrichedRequest = [
|
|
214
|
-
'[GODMODE EXECUTION]',
|
|
215
|
-
request,
|
|
216
|
-
`Workspace: ${workspace}`,
|
|
217
|
-
`PhaseA: files=${scan.files}, lines=${scan.lines}, import_edges=${scan.importEdges}`,
|
|
218
|
-
'Role model assignment:',
|
|
219
|
-
...quote.map((q) => `- ${q.role}: ${q.model} (requested_model=${q.requestedModel})`),
|
|
220
|
-
`Billing: plan=${billingGate.plan}, master_admin_free=${billingGate.masterAdminFree ? 'true' : 'false'}, vigcoin_required=${billingQuote.vigcoinRequired.toFixed(3)}, vigcoin_rate_usd=${billingQuote.vigcoinRateUsd.toFixed(4)}, total_usd=${billingQuote.finalUsd.toFixed(4)}`,
|
|
221
|
-
'Required flow: Detective repro -> 6-role parallel attack -> Architect synthesis -> test loop until pass.',
|
|
222
|
-
].join('\n');
|
|
223
|
-
await this.planAndExecute(enrichedRequest, options, {
|
|
224
|
-
workspace,
|
|
225
|
-
originalRequest: request,
|
|
226
|
-
scan,
|
|
227
|
-
quote,
|
|
228
|
-
});
|
|
229
377
|
}
|
|
230
378
|
scanProject(workspace) {
|
|
231
379
|
const files = [];
|
|
@@ -274,8 +422,9 @@ class LegionCommand {
|
|
|
274
422
|
topFiles.sort((a, b) => (b.lines + b.imports * 25) - (a.lines + a.imports * 25));
|
|
275
423
|
return { files: files.length, lines, importEdges, topFiles: topFiles.slice(0, 8) };
|
|
276
424
|
}
|
|
277
|
-
resolveModelProfiles(modelsArg) {
|
|
278
|
-
|
|
425
|
+
resolveModelProfiles(modelsArg, tier = 'heavy') {
|
|
426
|
+
// HEAVY tier: strongest available models — highest quality, highest cost.
|
|
427
|
+
const heavyCatalog = [
|
|
279
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 } },
|
|
280
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 } },
|
|
281
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 } },
|
|
@@ -285,6 +434,24 @@ class LegionCommand {
|
|
|
285
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 } },
|
|
286
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 } },
|
|
287
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;
|
|
288
455
|
if (!modelsArg || !modelsArg.trim())
|
|
289
456
|
return catalog;
|
|
290
457
|
const allow = new Set(modelsArg.split(',').map((m) => m.trim()).filter(Boolean));
|
|
@@ -293,7 +460,7 @@ class LegionCommand {
|
|
|
293
460
|
|| allow.has(m.requestedModel)));
|
|
294
461
|
return filtered.length > 0 ? filtered : catalog;
|
|
295
462
|
}
|
|
296
|
-
buildRoleQuote(scan, models) {
|
|
463
|
+
buildRoleQuote(scan, models, tier = 'heavy') {
|
|
297
464
|
const roleWeights = {
|
|
298
465
|
detective: { reasoning: 10, coding: 8, security: 6, speed: 3, synthesis: 7 },
|
|
299
466
|
logic: { reasoning: 8, coding: 10, security: 3, speed: 6, synthesis: 6 },
|
|
@@ -304,19 +471,35 @@ class LegionCommand {
|
|
|
304
471
|
reviewer: { reasoning: 9, coding: 7, security: 7, speed: 6, synthesis: 8 },
|
|
305
472
|
architect: { reasoning: 10, coding: 9, security: 8, speed: 4, synthesis: 10 },
|
|
306
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).
|
|
307
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
|
|
308
501
|
return Object.keys(roleWeights).map((role) => {
|
|
309
502
|
const w = roleWeights[role];
|
|
310
|
-
const preferredByRole = {
|
|
311
|
-
detective: 'gpt-5.5',
|
|
312
|
-
logic: 'o3',
|
|
313
|
-
security: 'opus-4.7',
|
|
314
|
-
performance: 'deepseek-v4-pro',
|
|
315
|
-
edge_case: 'gemini-2.5-pro',
|
|
316
|
-
integration: 'kimi-k2.5',
|
|
317
|
-
reviewer: 'opus-4.7',
|
|
318
|
-
architect: 'gpt-5.5',
|
|
319
|
-
};
|
|
320
503
|
let best = models.find((m) => m.requestedModel === preferredByRole[role]) || models[0];
|
|
321
504
|
if (!best)
|
|
322
505
|
best = models[0];
|
|
@@ -333,13 +516,13 @@ class LegionCommand {
|
|
|
333
516
|
}
|
|
334
517
|
}
|
|
335
518
|
}
|
|
336
|
-
const estInputTokens =
|
|
337
|
-
const estOutputTokens =
|
|
519
|
+
const estInputTokens = baseInputTokens;
|
|
520
|
+
const estOutputTokens = baseOutputTokens;
|
|
338
521
|
const estCostUsd = (estInputTokens / 1_000_000) * best.estInputPer1M + (estOutputTokens / 1_000_000) * best.estOutputPer1M;
|
|
339
522
|
return { role, model: best.id, requestedModel: best.requestedModel, estInputTokens, estOutputTokens, estCostUsd };
|
|
340
523
|
});
|
|
341
524
|
}
|
|
342
|
-
|
|
525
|
+
buildCortexExplicitSteps(execution, speedMode = false) {
|
|
343
526
|
const { originalRequest, workspace, scan, quote } = execution;
|
|
344
527
|
const quoteByRole = new Map(quote.map((row) => [row.role, row]));
|
|
345
528
|
const topFiles = scan.topFiles.slice(0, 5).map((file) => `${file.file} (${file.lines} lines, ${file.imports} imports)`);
|
|
@@ -356,26 +539,47 @@ class LegionCommand {
|
|
|
356
539
|
const roleIterationBudget = {
|
|
357
540
|
detective: 4,
|
|
358
541
|
logic: 5,
|
|
359
|
-
security: 5,
|
|
360
|
-
performance:
|
|
361
|
-
edge_case: 4,
|
|
362
|
-
integration:
|
|
363
|
-
reviewer: 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,
|
|
364
547
|
architect: 5,
|
|
365
548
|
};
|
|
549
|
+
const optionalRoles = new Set(['security', 'performance', 'edge_case', 'integration', 'reviewer']);
|
|
366
550
|
const steps = roleSequence.map(({ role, dependsOn }) => {
|
|
367
551
|
const row = quoteByRole.get(role);
|
|
368
|
-
const
|
|
369
|
-
const
|
|
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;
|
|
370
578
|
const roleObjective = [
|
|
371
|
-
`[
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
`Project scan: files=${scan.files}, lines=${scan.lines}, import_edges=${scan.importEdges}`,
|
|
376
|
-
topFiles.length > 0 ? `Key files: ${topFiles.join('; ')}` : 'Key files: unavailable',
|
|
377
|
-
`Execution budget: at most ${roleIterationBudget[role] || 3} reasoning iterations; prioritize a one-pass final answer and avoid redundant full-repo rescans.`,
|
|
378
|
-
].join('\n');
|
|
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');
|
|
379
583
|
return {
|
|
380
584
|
step_id: role,
|
|
381
585
|
worker_name: 'v3_agent_worker',
|
|
@@ -383,7 +587,7 @@ class LegionCommand {
|
|
|
383
587
|
depends_on: dependsOn,
|
|
384
588
|
priority: role === 'architect' ? 2 : 3,
|
|
385
589
|
retry_policy: {
|
|
386
|
-
max_attempts: 1,
|
|
590
|
+
max_attempts: role === 'performance' || role === 'edge_case' ? 2 : 1,
|
|
387
591
|
strategy: 'repair',
|
|
388
592
|
requires_validation_failure: false,
|
|
389
593
|
},
|
|
@@ -391,14 +595,20 @@ class LegionCommand {
|
|
|
391
595
|
role,
|
|
392
596
|
requested_model: requestedModel,
|
|
393
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'],
|
|
394
603
|
workspace,
|
|
395
604
|
top_files: topFiles,
|
|
396
605
|
max_iterations: roleIterationBudget[role] || 4,
|
|
397
606
|
compact_context: true,
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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,
|
|
402
612
|
request_timeout_seconds: role === 'detective' || role === 'architect' ? 1200 : 720,
|
|
403
613
|
},
|
|
404
614
|
};
|
|
@@ -407,11 +617,11 @@ class LegionCommand {
|
|
|
407
617
|
step_id: 'testing',
|
|
408
618
|
worker_name: 'testing_worker',
|
|
409
619
|
objective: [
|
|
410
|
-
'[
|
|
620
|
+
'[CORTEX:TESTING]',
|
|
411
621
|
originalRequest,
|
|
412
622
|
`Workspace: ${workspace}`,
|
|
413
|
-
'Validate
|
|
414
|
-
].join('
|
|
623
|
+
'Validate architect output with narrow relevant checks and report concrete failures only.',
|
|
624
|
+
].join('\\n'),
|
|
415
625
|
depends_on: ['architect'],
|
|
416
626
|
priority: 2,
|
|
417
627
|
retry_policy: {
|
|
@@ -426,24 +636,45 @@ class LegionCommand {
|
|
|
426
636
|
});
|
|
427
637
|
return steps;
|
|
428
638
|
}
|
|
429
|
-
buildBillingQuote(quote) {
|
|
639
|
+
buildBillingQuote(quote, tier = 'heavy') {
|
|
430
640
|
const baseUsd = quote.reduce((sum, r) => sum + r.estCostUsd, 0);
|
|
431
|
-
const marginPctRaw = Number.parseFloat(String(process.env.VIGTHORIA_GODMODE_MARGIN_PCT || '10'));
|
|
641
|
+
const marginPctRaw = Number.parseFloat(String(process.env.VIGTHORIA_CORTEX_MARGIN_PCT || process.env.VIGTHORIA_GODMODE_MARGIN_PCT || '10'));
|
|
432
642
|
const marginPct = Number.isFinite(marginPctRaw) ? Math.max(0, marginPctRaw) : 10;
|
|
433
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));
|
|
434
661
|
const vigcoinRateRaw = Number.parseFloat(String(process.env.VIGTHORIA_VIGCOIN_USD_RATE || '1'));
|
|
435
662
|
const vigcoinRateUsd = Number.isFinite(vigcoinRateRaw) && vigcoinRateRaw > 0 ? vigcoinRateRaw : 1;
|
|
436
|
-
const vigcoinRequired =
|
|
663
|
+
const vigcoinRequired = retryAdjustedUsd / vigcoinRateUsd;
|
|
437
664
|
return {
|
|
438
665
|
baseUsd,
|
|
439
666
|
marginPct,
|
|
440
667
|
finalUsd,
|
|
668
|
+
retryAdjustedUsd,
|
|
669
|
+
rangeMinUsd,
|
|
670
|
+
rangeMaxUsd,
|
|
671
|
+
tier,
|
|
441
672
|
vigcoinRateUsd,
|
|
442
673
|
vigcoinRequired,
|
|
443
674
|
};
|
|
444
675
|
}
|
|
445
676
|
async evaluateBillingGate(billingQuote) {
|
|
446
|
-
const forcedPlan = String(process.env.VIGTHORIA_GODMODE_FORCE_PLAN || '').trim().toLowerCase();
|
|
677
|
+
const forcedPlan = String(process.env.VIGTHORIA_CORTEX_FORCE_PLAN || process.env.VIGTHORIA_GODMODE_FORCE_PLAN || '').trim().toLowerCase();
|
|
447
678
|
// On-server invocations using a service key run as trusted infrastructure.
|
|
448
679
|
// No user wallet check is needed — cost is tracked at the service level.
|
|
449
680
|
const hasServiceKey = !!(process.env.HYPERLOOP_SERVICE_KEY || process.env.V3_SERVICE_KEY);
|
|
@@ -460,7 +691,7 @@ class LegionCommand {
|
|
|
460
691
|
},
|
|
461
692
|
};
|
|
462
693
|
}
|
|
463
|
-
const entitlement = await this.
|
|
694
|
+
const entitlement = await this.fetchCortexEntitlement();
|
|
464
695
|
const normalizedPlan = forcedPlan || entitlement.plan || this.config.getNormalizedPlan() || 'free';
|
|
465
696
|
const masterAdminFree = this.isMasterAdminFree(normalizedPlan, entitlement.masterAccess, entitlement.isMasterAdmin);
|
|
466
697
|
if (masterAdminFree) {
|
|
@@ -512,8 +743,8 @@ class LegionCommand {
|
|
|
512
743
|
}
|
|
513
744
|
return null;
|
|
514
745
|
}
|
|
515
|
-
async
|
|
516
|
-
if (process.env.VIGTHORIA_GODMODE_FORCE_MASTER_ACCESS === '1') {
|
|
746
|
+
async fetchCortexEntitlement() {
|
|
747
|
+
if (process.env.VIGTHORIA_CORTEX_FORCE_MASTER_ACCESS === '1' || process.env.VIGTHORIA_GODMODE_FORCE_MASTER_ACCESS === '1') {
|
|
517
748
|
return { plan: this.config.getNormalizedPlan() || 'free', masterAccess: true, isMasterAdmin: true };
|
|
518
749
|
}
|
|
519
750
|
const baseUrl = this.getBillingBaseUrl();
|
|
@@ -568,7 +799,7 @@ class LegionCommand {
|
|
|
568
799
|
}
|
|
569
800
|
}
|
|
570
801
|
catch (err) {
|
|
571
|
-
this.logger.warn(this.formatLegionError(`
|
|
802
|
+
this.logger.warn(this.formatLegionError(`Cortex entitlement request ${endpoint}`, err));
|
|
572
803
|
continue;
|
|
573
804
|
}
|
|
574
805
|
}
|
|
@@ -649,8 +880,8 @@ class LegionCommand {
|
|
|
649
880
|
async fetchWalletState() {
|
|
650
881
|
const baseUrl = this.getBillingBaseUrl();
|
|
651
882
|
const headers = this.getHeaders();
|
|
652
|
-
const forcedLow = process.env.VIGTHORIA_GODMODE_FORCE_LOW_CREDIT === '1';
|
|
653
|
-
const forcedBalanceRaw = process.env.VIGTHORIA_GODMODE_FORCE_BALANCE;
|
|
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;
|
|
654
885
|
if (forcedLow) {
|
|
655
886
|
return {
|
|
656
887
|
available: true,
|
|
@@ -720,9 +951,9 @@ class LegionCommand {
|
|
|
720
951
|
const headers = this.getHeaders();
|
|
721
952
|
const amount = Math.max(1, Math.ceil(vigcoinNeeded));
|
|
722
953
|
const chargePayloads = [
|
|
723
|
-
{ endpoint: '/api/viagen6/vigcoin/charge', body: { amount, reason: '
|
|
724
|
-
{ endpoint: '/api/wallet/charge', body: { amount, currency: 'VIGCOIN', reason: '
|
|
725
|
-
{ endpoint: '/api/billing/topup', body: { vigcoin: amount, reason: '
|
|
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' } },
|
|
726
957
|
];
|
|
727
958
|
for (const attempt of chargePayloads) {
|
|
728
959
|
try {
|
|
@@ -766,7 +997,7 @@ class LegionCommand {
|
|
|
766
997
|
if (gate.masterAdminFree) {
|
|
767
998
|
return true;
|
|
768
999
|
}
|
|
769
|
-
const spinner = (0, logger_js_1.createSpinner)('Charging VigCoin wallet for
|
|
1000
|
+
const spinner = (0, logger_js_1.createSpinner)('Charging VigCoin wallet for Cortex execution...').start();
|
|
770
1001
|
const result = await this.attemptDirectCharge(billingQuote.vigcoinRequired);
|
|
771
1002
|
spinner.stop();
|
|
772
1003
|
if (!result.ok) {
|
|
@@ -774,7 +1005,7 @@ class LegionCommand {
|
|
|
774
1005
|
console.log(chalk_1.default.yellow(`Complete purchase first: ${result.checkoutUrl || `${this.getBillingBaseUrl()}/music/store#vigcoins`}`));
|
|
775
1006
|
return false;
|
|
776
1007
|
}
|
|
777
|
-
console.log(chalk_1.default.green('Wallet charged for
|
|
1008
|
+
console.log(chalk_1.default.green('Wallet charged for Cortex execution.'));
|
|
778
1009
|
return true;
|
|
779
1010
|
}
|
|
780
1011
|
async resolveBillingInsufficientFunds(billingQuote, gate, options) {
|
|
@@ -837,7 +1068,7 @@ class LegionCommand {
|
|
|
837
1068
|
console.log(chalk_1.default.green(' Free tier override applied (Master Admin).'));
|
|
838
1069
|
return;
|
|
839
1070
|
}
|
|
840
|
-
console.log(chalk_1.default.gray(` Estimated total (USD): $${billingQuote.
|
|
1071
|
+
console.log(chalk_1.default.gray(` Estimated total (USD): $${billingQuote.retryAdjustedUsd.toFixed(4)}`) + chalk_1.default.gray(' (retry-adjusted expected)'));
|
|
841
1072
|
console.log(chalk_1.default.gray(` VigCoin rate: 1 VIG = $${billingQuote.vigcoinRateUsd.toFixed(4)}`));
|
|
842
1073
|
console.log(chalk_1.default.gray(` VigCoin required: ${billingQuote.vigcoinRequired.toFixed(3)}`));
|
|
843
1074
|
if (gate.wallet.vigcoinBalance !== null) {
|
|
@@ -851,12 +1082,15 @@ class LegionCommand {
|
|
|
851
1082
|
console.log(chalk_1.default.gray(` Purchase URL: ${gate.wallet.purchaseUrl}`));
|
|
852
1083
|
}
|
|
853
1084
|
}
|
|
854
|
-
|
|
855
|
-
const
|
|
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)');
|
|
856
1089
|
console.log();
|
|
857
|
-
console.log(chalk_1.default.bold.white(` ${logger_js_1.CH.hLine.repeat(3)}
|
|
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)}`));
|
|
858
1091
|
console.log();
|
|
859
1092
|
console.log(chalk_1.default.gray(' Workspace: ') + chalk_1.default.white(workspace));
|
|
1093
|
+
console.log(chalk_1.default.gray(' Tier: ') + tierLabel);
|
|
860
1094
|
console.log(chalk_1.default.gray(' Files scanned: ') + chalk_1.default.white(String(scan.files)));
|
|
861
1095
|
console.log(chalk_1.default.gray(' Lines scanned: ') + chalk_1.default.white(String(scan.lines)));
|
|
862
1096
|
console.log(chalk_1.default.gray(' Dependency edges: ') + chalk_1.default.white(String(scan.importEdges)));
|
|
@@ -873,7 +1107,11 @@ class LegionCommand {
|
|
|
873
1107
|
console.log(chalk_1.default.gray(` ${logger_js_1.CH.bullet} ${row.role.padEnd(11)} ${row.model} $${row.estCostUsd.toFixed(4)}`));
|
|
874
1108
|
}
|
|
875
1109
|
console.log();
|
|
876
|
-
console.log(chalk_1.default.yellow(`
|
|
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.'));
|
|
877
1115
|
console.log(chalk_1.default.gray(' Flow: Estimate -> Isolation -> Parallel Attack -> Synthesis'));
|
|
878
1116
|
console.log();
|
|
879
1117
|
this.printBillingGateSummary(billingQuote, gate);
|
|
@@ -886,7 +1124,7 @@ class LegionCommand {
|
|
|
886
1124
|
}
|
|
887
1125
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
888
1126
|
try {
|
|
889
|
-
const answer = (await rl.question('Proceed with
|
|
1127
|
+
const answer = (await rl.question('Proceed with Cortex execution? (y/N): ')).trim().toLowerCase();
|
|
890
1128
|
return answer === 'y' || answer === 'yes';
|
|
891
1129
|
}
|
|
892
1130
|
finally {
|
|
@@ -896,7 +1134,7 @@ class LegionCommand {
|
|
|
896
1134
|
/**
|
|
897
1135
|
* SSE streaming URL for the Legion execution endpoint.
|
|
898
1136
|
* Always hits Hyper Loop directly (port 8020) with the service key to avoid
|
|
899
|
-
* gateway JWT expiry killing long-running
|
|
1137
|
+
* gateway JWT expiry killing long-running Cortex jobs.
|
|
900
1138
|
*/
|
|
901
1139
|
getLegionStreamUrl() {
|
|
902
1140
|
const envOverride = String(process.env.VIGTHORIA_HYPERLOOP_URL || '').trim().replace(/\/$/, '');
|
|
@@ -910,12 +1148,16 @@ class LegionCommand {
|
|
|
910
1148
|
process.env.V3_SERVICE_KEY ||
|
|
911
1149
|
'');
|
|
912
1150
|
}
|
|
913
|
-
async planAndExecute(request, options,
|
|
914
|
-
const explicitSteps =
|
|
915
|
-
const workspace =
|
|
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();
|
|
916
1154
|
const body = {
|
|
917
1155
|
request,
|
|
918
|
-
context: {
|
|
1156
|
+
context: {
|
|
1157
|
+
workspace,
|
|
1158
|
+
governor_budget_secs: 600,
|
|
1159
|
+
governor_optional_roles: ['security', 'performance', 'edge_case', 'integration', 'reviewer'],
|
|
1160
|
+
},
|
|
919
1161
|
constraints: {
|
|
920
1162
|
active_only: true,
|
|
921
1163
|
execution_timeout_seconds: options.timeoutSec,
|
|
@@ -930,7 +1172,6 @@ class LegionCommand {
|
|
|
930
1172
|
headers['X-Service-Key'] = serviceKey;
|
|
931
1173
|
}
|
|
932
1174
|
else {
|
|
933
|
-
// Fallback: pass user JWT (works if token is still valid)
|
|
934
1175
|
const token = this.config.get('authToken');
|
|
935
1176
|
if (token) {
|
|
936
1177
|
headers['Authorization'] = `Bearer ${token}`;
|
|
@@ -941,7 +1182,6 @@ class LegionCommand {
|
|
|
941
1182
|
const startTime = Date.now();
|
|
942
1183
|
let response;
|
|
943
1184
|
try {
|
|
944
|
-
// No AbortSignal timeout — SSE keeps alive; server controls lifetime
|
|
945
1185
|
response = await fetch(streamUrl, {
|
|
946
1186
|
method: 'POST',
|
|
947
1187
|
headers,
|
|
@@ -951,20 +1191,19 @@ class LegionCommand {
|
|
|
951
1191
|
catch (connErr) {
|
|
952
1192
|
spinner.stop();
|
|
953
1193
|
this.logger.error(`Cannot connect to Hyper Loop at ${streamUrl}: ${connErr?.message || connErr}`);
|
|
954
|
-
return;
|
|
1194
|
+
return { status: 'failed', plannedSteps: 0, completedSteps: 0 };
|
|
955
1195
|
}
|
|
956
1196
|
if (!response.ok) {
|
|
957
1197
|
spinner.stop();
|
|
958
1198
|
const errBody = await response.text().catch(() => '');
|
|
959
1199
|
this.logger.error(`Legion stream ${response.status}: ${(0, api_js_1.describeUpstreamStatus)(response.status)} — ${errBody.slice(0, 200)}`);
|
|
960
|
-
return;
|
|
1200
|
+
return { status: 'failed', plannedSteps: 0, completedSteps: 0 };
|
|
961
1201
|
}
|
|
962
1202
|
if (!response.body) {
|
|
963
1203
|
spinner.stop();
|
|
964
1204
|
this.logger.error('Legion stream returned no response body');
|
|
965
|
-
return;
|
|
1205
|
+
return { status: 'failed', plannedSteps: 0, completedSteps: 0 };
|
|
966
1206
|
}
|
|
967
|
-
// ── SSE consumer ─────────────────────────────────────────────
|
|
968
1207
|
spinner.stop();
|
|
969
1208
|
console.log();
|
|
970
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)}`));
|
|
@@ -974,6 +1213,22 @@ class LegionCommand {
|
|
|
974
1213
|
let finalResult = null;
|
|
975
1214
|
let stepsTotal = 0;
|
|
976
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 = [];
|
|
977
1232
|
try {
|
|
978
1233
|
const reader = response.body.getReader();
|
|
979
1234
|
while (true) {
|
|
@@ -982,11 +1237,11 @@ class LegionCommand {
|
|
|
982
1237
|
break;
|
|
983
1238
|
buffer += decoder.decode(value, { stream: true });
|
|
984
1239
|
const lines = buffer.split('\n');
|
|
985
|
-
buffer = lines.pop() ?? '';
|
|
1240
|
+
buffer = lines.pop() ?? '';
|
|
986
1241
|
for (const line of lines) {
|
|
987
1242
|
const trimmed = line.trim();
|
|
988
1243
|
if (!trimmed || trimmed.startsWith(':'))
|
|
989
|
-
continue;
|
|
1244
|
+
continue;
|
|
990
1245
|
if (!trimmed.startsWith('data:'))
|
|
991
1246
|
continue;
|
|
992
1247
|
const jsonStr = trimmed.slice(5).trim();
|
|
@@ -997,6 +1252,7 @@ class LegionCommand {
|
|
|
997
1252
|
catch {
|
|
998
1253
|
continue;
|
|
999
1254
|
}
|
|
1255
|
+
streamEvents.push(evt);
|
|
1000
1256
|
switch (evt.event) {
|
|
1001
1257
|
case 'plan':
|
|
1002
1258
|
stepsTotal = evt.steps_total || 0;
|
|
@@ -1009,17 +1265,87 @@ class LegionCommand {
|
|
|
1009
1265
|
case 'step_complete': {
|
|
1010
1266
|
stepsDone = Number(evt.steps_done) || 0;
|
|
1011
1267
|
const icon = evt.status === 'completed' ? chalk_1.default.green(logger_js_1.CH.success) : chalk_1.default.red(logger_js_1.CH.error);
|
|
1012
|
-
const
|
|
1013
|
-
|
|
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
|
+
}
|
|
1014
1326
|
break;
|
|
1015
1327
|
}
|
|
1016
1328
|
case 'complete':
|
|
1017
|
-
finalResult = evt.result ||
|
|
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
|
+
}
|
|
1018
1342
|
if (evt.status === 'completed') {
|
|
1343
|
+
finalStatus = 'completed';
|
|
1019
1344
|
console.log();
|
|
1020
1345
|
console.log(chalk_1.default.green(` ${logger_js_1.CH.success} Legion completed successfully`));
|
|
1021
1346
|
}
|
|
1022
1347
|
else if (evt.status === 'failed') {
|
|
1348
|
+
finalStatus = 'failed';
|
|
1023
1349
|
console.log();
|
|
1024
1350
|
console.log(chalk_1.default.red(` ${logger_js_1.CH.error} Legion execution failed`));
|
|
1025
1351
|
if (evt.error)
|
|
@@ -1027,6 +1353,7 @@ class LegionCommand {
|
|
|
1027
1353
|
}
|
|
1028
1354
|
break;
|
|
1029
1355
|
case 'error':
|
|
1356
|
+
finalStatus = 'failed';
|
|
1030
1357
|
console.log(chalk_1.default.red(` ${logger_js_1.CH.error} Stream error: ${String(evt.error).slice(0, 300)}`));
|
|
1031
1358
|
break;
|
|
1032
1359
|
}
|
|
@@ -1035,6 +1362,7 @@ class LegionCommand {
|
|
|
1035
1362
|
}
|
|
1036
1363
|
catch (streamErr) {
|
|
1037
1364
|
this.logger.error(`Legion stream read error: ${streamErr?.message || streamErr}`);
|
|
1365
|
+
finalStatus = 'failed';
|
|
1038
1366
|
}
|
|
1039
1367
|
const elapsedSec = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
1040
1368
|
if (stepsTotal > 0 && stepsDone < stepsTotal) {
|
|
@@ -1042,17 +1370,107 @@ class LegionCommand {
|
|
|
1042
1370
|
}
|
|
1043
1371
|
console.log();
|
|
1044
1372
|
console.log(chalk_1.default.gray(` Time: ${elapsedSec}s`));
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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));
|
|
1054
1396
|
}
|
|
1055
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 };
|
|
1056
1474
|
}
|
|
1057
1475
|
formatLegionError(context, err) {
|
|
1058
1476
|
const message = err?.message || String(err || 'Unknown error');
|