vigthoria-cli 1.9.2 → 1.9.8
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 +461 -313
- package/dist/commands/bridge.js +3 -8
- package/dist/commands/chat.d.ts +3 -0
- package/dist/commands/chat.js +97 -34
- package/dist/commands/index.js +1 -1
- package/dist/commands/legion.d.ts +22 -19
- package/dist/commands/legion.js +561 -134
- package/dist/commands/preview.js +32 -7
- package/dist/commands/repo.js +19 -13
- package/dist/commands/security.d.ts +20 -0
- package/dist/commands/security.js +98 -0
- 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 +147 -40
- package/dist/utils/api.d.ts +25 -70
- package/dist/utils/api.js +875 -693
- package/dist/utils/config.js +1 -1
- package/dist/utils/tools.d.ts +11 -0
- package/dist/utils/tools.js +251 -5
- package/install.ps1 +322 -0
- package/install.sh +314 -0
- package/package.json +18 -3
- package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +159 -0
- package/scripts/release/publish-cli-release.sh +73 -0
- package/scripts/release/validate-no-go-gates.sh +129 -0
- package/scripts/release/verify-runtime-consistency.mjs +64 -0
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,11 @@ 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 CORTEX_PLATFORM_FEE_PCT = 10;
|
|
80
|
+
const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
|
|
74
81
|
class LegionCommand {
|
|
75
82
|
config;
|
|
76
83
|
logger;
|
|
@@ -145,8 +152,8 @@ class LegionCommand {
|
|
|
145
152
|
});
|
|
146
153
|
}
|
|
147
154
|
async run(request, options) {
|
|
148
|
-
if (options.
|
|
149
|
-
await this.
|
|
155
|
+
if (options.cortex) {
|
|
156
|
+
await this.runCortex(request, options);
|
|
150
157
|
return;
|
|
151
158
|
}
|
|
152
159
|
if (options.workers) {
|
|
@@ -161,71 +168,214 @@ class LegionCommand {
|
|
|
161
168
|
console.log(chalk_1.default.yellow('Usage: vigthoria legion "<task description>"'));
|
|
162
169
|
console.log(chalk_1.default.gray(' --workers List available Legion workers'));
|
|
163
170
|
console.log(chalk_1.default.gray(' --status Show Legion infrastructure status'));
|
|
164
|
-
console.log(chalk_1.default.gray(' --
|
|
171
|
+
console.log(chalk_1.default.gray(' --cortex Vigthoria Cortex: maximum intelligence execution'));
|
|
165
172
|
return;
|
|
166
173
|
}
|
|
167
174
|
await this.planAndExecute(request, options);
|
|
168
175
|
}
|
|
169
|
-
async
|
|
176
|
+
async runCortex(request, options) {
|
|
170
177
|
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(' --
|
|
178
|
+
console.log(chalk_1.default.yellow('Usage: vigthoria legion --cortex "<task description>"'));
|
|
179
|
+
console.log(chalk_1.default.gray(' --plan-only Run calculator only (no execution)'));
|
|
180
|
+
console.log(chalk_1.default.gray(' --force-budget Allow execution above hard safe-stop budget'));
|
|
181
|
+
console.log(chalk_1.default.gray(' --ignore-preflight Bypass mandatory preflight checks (no warranty)'));
|
|
182
|
+
console.log(chalk_1.default.gray(' --speed Speed mode: optional role skip on convergence'));
|
|
183
|
+
console.log(chalk_1.default.gray(' --tier heavy|lite Model tier: heavy=strongest LLMs (default), lite=efficient+affordable'));
|
|
184
|
+
console.log(chalk_1.default.gray(' --repro-cmd <cmd> Run a local reproducibility command before spend'));
|
|
185
|
+
console.log(chalk_1.default.gray(' --expect-repro-fail Require repro command to fail before execution'));
|
|
186
|
+
console.log(chalk_1.default.gray(' --approve Skip initial confirmation prompt and execute'));
|
|
187
|
+
console.log(chalk_1.default.gray(' --auto-charge Attempt direct VigCoin top-up when balance is low'));
|
|
188
|
+
console.log(chalk_1.default.gray(' --timeout <sec> Abort remote execution if no result within timeout (default: 120)'));
|
|
176
189
|
return;
|
|
177
190
|
}
|
|
178
191
|
const workspace = options.project || process.cwd();
|
|
179
192
|
const scan = this.scanProject(workspace);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
193
|
+
if (!options.ignorePreflight) {
|
|
194
|
+
const preflight = await this.runMandatoryPreflight(workspace, options.reproCmd, options.expectReproFail === true);
|
|
195
|
+
if (!preflight.ok) {
|
|
196
|
+
console.log(chalk_1.default.red('Cortex preflight failed.'));
|
|
197
|
+
console.log(chalk_1.default.red(` ${preflight.reason}`));
|
|
198
|
+
console.log(chalk_1.default.yellow('Execution halted before any cloud spend. Use --ignore-preflight to override (no warranty).'));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
console.log(chalk_1.default.green('Preflight passed.'));
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
console.log(chalk_1.default.yellow('Preflight bypassed by --ignore-preflight (no warranty).'));
|
|
205
|
+
}
|
|
206
|
+
const tier = (options.tier === 'lite') ? 'lite' : 'heavy';
|
|
207
|
+
const selectedModels = this.resolveModelProfiles(options.models, tier);
|
|
208
|
+
const quote = this.buildRoleQuote(scan, selectedModels, tier);
|
|
209
|
+
const billingQuote = this.buildBillingQuote(quote, tier);
|
|
210
|
+
if (billingQuote.retryAdjustedUsd > CORTEX_WARN_BUDGET_USD) {
|
|
211
|
+
console.log(chalk_1.default.yellow(`Estimated spend exceeds warning threshold ($${CORTEX_WARN_BUDGET_USD.toFixed(2)}).`));
|
|
212
|
+
}
|
|
183
213
|
let billingGate = await this.evaluateBillingGate(billingQuote);
|
|
184
|
-
this.
|
|
214
|
+
this.printCortexQuote(workspace, scan, quote, billingQuote, billingGate);
|
|
215
|
+
if (billingQuote.retryAdjustedUsd > CORTEX_HARD_BUDGET_USD && !options.forceBudget && !billingGate.masterAdminFree) {
|
|
216
|
+
console.log(chalk_1.default.red(`Estimated spend exceeds hard budget ceiling (${CORTEX_HARD_BUDGET_USD.toFixed(2)}).`));
|
|
217
|
+
console.log(chalk_1.default.yellow('Re-run with --force-budget to continue.'));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
185
220
|
if (options.planOnly) {
|
|
186
|
-
console.log(chalk_1.default.green('
|
|
221
|
+
console.log(chalk_1.default.green('Cortex estimator complete (plan-only).'));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const autoApprove = options.approve === true && options.noApprove !== true;
|
|
225
|
+
const approved = autoApprove ? true : await this.confirmExecution();
|
|
226
|
+
if (!approved) {
|
|
227
|
+
console.log(chalk_1.default.yellow('Cortex cancelled by user.'));
|
|
187
228
|
return;
|
|
188
229
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
230
|
+
let round = 1;
|
|
231
|
+
let cumulativeUsd = 0;
|
|
232
|
+
let currentQuote = billingQuote;
|
|
233
|
+
let lastFailure = null;
|
|
234
|
+
while (round <= CORTEX_MAX_ROUNDS) {
|
|
235
|
+
if (!billingGate.canProceed) {
|
|
236
|
+
const resolved = await this.resolveBillingInsufficientFunds(currentQuote, billingGate, options);
|
|
237
|
+
if (!resolved) {
|
|
238
|
+
console.log(chalk_1.default.yellow('Cortex cancelled due to insufficient VigCoin balance.'));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
billingGate = await this.evaluateBillingGate(currentQuote);
|
|
242
|
+
if (!billingGate.canProceed) {
|
|
243
|
+
this.printBillingGateSummary(currentQuote, billingGate);
|
|
244
|
+
console.log(chalk_1.default.red('Billing gate still blocked after charge attempt.'));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const charged = await this.collectExecutionCharge(currentQuote, billingGate);
|
|
249
|
+
if (!charged) {
|
|
250
|
+
console.log(chalk_1.default.yellow('Cortex cancelled because wallet charge was not completed.'));
|
|
193
251
|
return;
|
|
194
252
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
253
|
+
cumulativeUsd += currentQuote.finalUsd;
|
|
254
|
+
const enrichedRequest = [
|
|
255
|
+
'[CORTEX EXECUTION]',
|
|
256
|
+
request,
|
|
257
|
+
`Workspace: ${workspace}`,
|
|
258
|
+
`PhaseA: files=${scan.files}, lines=${scan.lines}, import_edges=${scan.importEdges}`,
|
|
259
|
+
'Role model assignment:',
|
|
260
|
+
...quote.map((q) => `- ${q.role}: ${q.model} (requested_model=${q.requestedModel})`),
|
|
261
|
+
`Billing: round=${round}, cumulative_usd=${cumulativeUsd.toFixed(4)}, total_usd=${currentQuote.finalUsd.toFixed(4)}, plan=${billingGate.plan}, master_admin_free=${billingGate.masterAdminFree ? 'true' : 'false'}`,
|
|
262
|
+
`Execution mode: cloud-only core roles, speed_mode=${options.speed === true ? 'true' : 'false'}, tier=${tier}`,
|
|
263
|
+
lastFailure ? `Repair focus: previous failure at step=${lastFailure.failedStepId || 'unknown'} worker=${lastFailure.failedWorker || 'unknown'}` : '',
|
|
264
|
+
].filter(Boolean).join('\\n');
|
|
265
|
+
const execution = await this.planAndExecute(enrichedRequest, options, {
|
|
266
|
+
workspace,
|
|
267
|
+
originalRequest: request,
|
|
268
|
+
scan,
|
|
269
|
+
quote,
|
|
270
|
+
tier,
|
|
271
|
+
});
|
|
272
|
+
if (execution.status === 'completed') {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const failedRole = String(execution.failedStepId || '').toLowerCase();
|
|
276
|
+
if (this.isCriticalRoleFailure(failedRole)) {
|
|
277
|
+
console.log(chalk_1.default.red(`Fail-fast: critical role '${failedRole || 'unknown'}' failed. Manual correction required.`));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (!this.isOptionalRepairRoleFailure(failedRole)) {
|
|
281
|
+
console.log(chalk_1.default.red('Execution failed and is not eligible for automatic optional-role repair.'));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (round >= CORTEX_MAX_ROUNDS) {
|
|
285
|
+
console.log(chalk_1.default.red('Optional-role auto-repair budget exhausted.'));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const nextQuote = this.estimateAdditionalLoopQuote(currentQuote, execution);
|
|
289
|
+
const projectedTotal = cumulativeUsd + nextQuote.retryAdjustedUsd;
|
|
290
|
+
if (projectedTotal > CORTEX_HARD_BUDGET_USD && !options.forceBudget) {
|
|
291
|
+
console.log(chalk_1.default.red(`Additional loop would exceed hard budget ceiling ($${CORTEX_HARD_BUDGET_USD.toFixed(2)}).`));
|
|
292
|
+
console.log(chalk_1.default.yellow('Re-run with --force-budget to allow paid continuation.'));
|
|
199
293
|
return;
|
|
200
294
|
}
|
|
295
|
+
const continueApproved = await this.confirmAdditionalLoopCharge(nextQuote, round + 1, execution);
|
|
296
|
+
if (!continueApproved) {
|
|
297
|
+
console.log(chalk_1.default.yellow('Termination: user declined additional budget for next round.'));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
currentQuote = nextQuote;
|
|
301
|
+
billingGate = await this.evaluateBillingGate(currentQuote);
|
|
302
|
+
lastFailure = execution;
|
|
303
|
+
round += 1;
|
|
201
304
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
305
|
+
}
|
|
306
|
+
isCriticalRoleFailure(role) {
|
|
307
|
+
return role === 'logic' || role === 'security';
|
|
308
|
+
}
|
|
309
|
+
isOptionalRepairRoleFailure(role) {
|
|
310
|
+
return role === 'performance' || role === 'edge_case';
|
|
311
|
+
}
|
|
312
|
+
estimateAdditionalLoopQuote(previousQuote, execution) {
|
|
313
|
+
const failedRole = String(execution.failedStepId || '').toLowerCase();
|
|
314
|
+
const factor = this.isOptionalRepairRoleFailure(failedRole) ? 0.35 : 0.55;
|
|
315
|
+
const baseUsd = Math.max(0.1, previousQuote.baseUsd * factor);
|
|
316
|
+
const finalUsd = Math.max(0.15, previousQuote.finalUsd * factor);
|
|
317
|
+
const retryAdjustedUsd = Math.max(0.15, previousQuote.retryAdjustedUsd * factor);
|
|
318
|
+
const rangeMinUsd = finalUsd;
|
|
319
|
+
const rangeMaxUsd = retryAdjustedUsd;
|
|
320
|
+
return {
|
|
321
|
+
baseUsd,
|
|
322
|
+
marginPct: previousQuote.marginPct,
|
|
323
|
+
finalUsd,
|
|
324
|
+
retryAdjustedUsd,
|
|
325
|
+
rangeMinUsd,
|
|
326
|
+
rangeMaxUsd,
|
|
327
|
+
tier: previousQuote.tier,
|
|
328
|
+
vigcoinRateUsd: previousQuote.vigcoinRateUsd,
|
|
329
|
+
vigcoinRequired: retryAdjustedUsd / previousQuote.vigcoinRateUsd,
|
|
330
|
+
platformFeePct: previousQuote.platformFeePct,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
async confirmAdditionalLoopCharge(nextQuote, nextRound, execution) {
|
|
334
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
335
|
+
return false;
|
|
207
336
|
}
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
console.log(
|
|
211
|
-
|
|
337
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
338
|
+
try {
|
|
339
|
+
console.log();
|
|
340
|
+
console.log(chalk_1.default.yellow('Budget depleted for current round. Additional paid loop required.'));
|
|
341
|
+
console.log(chalk_1.default.gray(` Next round: ${nextRound}`));
|
|
342
|
+
console.log(chalk_1.default.gray(` Failed step: ${execution.failedStepId || 'unknown'} (${execution.failedWorker || 'unknown'})`));
|
|
343
|
+
console.log(chalk_1.default.gray(` Additional estimate: $${nextQuote.finalUsd.toFixed(4)} / ${nextQuote.vigcoinRequired.toFixed(3)} VIG`));
|
|
344
|
+
const answer = (await rl.question('Confirm additional VigCoin deduction and continue? (y/N): ')).trim().toLowerCase();
|
|
345
|
+
return answer === 'y' || answer === 'yes';
|
|
346
|
+
}
|
|
347
|
+
finally {
|
|
348
|
+
rl.close();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async runMandatoryPreflight(workspace, reproCmd, expectReproFail = false) {
|
|
352
|
+
try {
|
|
353
|
+
const stat = fs.statSync(workspace);
|
|
354
|
+
if (!stat.isDirectory()) {
|
|
355
|
+
return { ok: false, reason: `Workspace is not a directory: ${workspace}` };
|
|
356
|
+
}
|
|
357
|
+
fs.accessSync(workspace, fs.constants.R_OK | fs.constants.W_OK);
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
return { ok: false, reason: `Workspace access check failed: ${err?.message || err}` };
|
|
361
|
+
}
|
|
362
|
+
if (!reproCmd || !reproCmd.trim()) {
|
|
363
|
+
return { ok: true, reason: 'Workspace checks passed (no repro command supplied).' };
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
await execAsync(reproCmd, { cwd: workspace, timeout: 180000, maxBuffer: 1024 * 1024 * 8 });
|
|
367
|
+
if (expectReproFail) {
|
|
368
|
+
return { ok: false, reason: 'Repro command succeeded but --expect-repro-fail requires a failure signal.' };
|
|
369
|
+
}
|
|
370
|
+
return { ok: true, reason: 'Repro command succeeded.' };
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
if (expectReproFail) {
|
|
374
|
+
return { ok: true, reason: 'Repro command failed as expected.' };
|
|
375
|
+
}
|
|
376
|
+
const stderr = String(err?.stderr || err?.message || err).slice(0, 300);
|
|
377
|
+
return { ok: false, reason: `Repro command failed: ${stderr}` };
|
|
212
378
|
}
|
|
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
379
|
}
|
|
230
380
|
scanProject(workspace) {
|
|
231
381
|
const files = [];
|
|
@@ -274,8 +424,9 @@ class LegionCommand {
|
|
|
274
424
|
topFiles.sort((a, b) => (b.lines + b.imports * 25) - (a.lines + a.imports * 25));
|
|
275
425
|
return { files: files.length, lines, importEdges, topFiles: topFiles.slice(0, 8) };
|
|
276
426
|
}
|
|
277
|
-
resolveModelProfiles(modelsArg) {
|
|
278
|
-
|
|
427
|
+
resolveModelProfiles(modelsArg, tier = 'heavy') {
|
|
428
|
+
// HEAVY tier: strongest available models — highest quality, highest cost.
|
|
429
|
+
const heavyCatalog = [
|
|
279
430
|
{ 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
431
|
{ 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
432
|
{ 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 +436,24 @@ class LegionCommand {
|
|
|
285
436
|
{ 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
437
|
{ 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
438
|
];
|
|
439
|
+
// LITE tier: efficient models — high quality at ~5-10x lower cost than heavy.
|
|
440
|
+
// detective/architect → claude-sonnet-4-5 (best reasoning per dollar)
|
|
441
|
+
// security/reviewer → claude-haiku-3-5 (strong safety, very cheap)
|
|
442
|
+
// logic → o4-mini (reasoning specialist, fraction of o3 cost)
|
|
443
|
+
// edge_case → gemini-2.0-flash (long context, fast, cheap)
|
|
444
|
+
// integration → kimi-k2.5 (best codebase mapping at its price point)
|
|
445
|
+
// performance → deepseek-v3 (coding analysis, ~20% of DeepSeek-V4-Pro cost)
|
|
446
|
+
const liteCatalog = [
|
|
447
|
+
{ 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 } },
|
|
448
|
+
{ 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 } },
|
|
449
|
+
{ 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 } },
|
|
450
|
+
{ 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 } },
|
|
451
|
+
{ 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 } },
|
|
452
|
+
{ 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 } },
|
|
453
|
+
{ 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 } },
|
|
454
|
+
{ 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 } },
|
|
455
|
+
];
|
|
456
|
+
const catalog = tier === 'lite' ? liteCatalog : heavyCatalog;
|
|
288
457
|
if (!modelsArg || !modelsArg.trim())
|
|
289
458
|
return catalog;
|
|
290
459
|
const allow = new Set(modelsArg.split(',').map((m) => m.trim()).filter(Boolean));
|
|
@@ -293,7 +462,7 @@ class LegionCommand {
|
|
|
293
462
|
|| allow.has(m.requestedModel)));
|
|
294
463
|
return filtered.length > 0 ? filtered : catalog;
|
|
295
464
|
}
|
|
296
|
-
buildRoleQuote(scan, models) {
|
|
465
|
+
buildRoleQuote(scan, models, tier = 'heavy') {
|
|
297
466
|
const roleWeights = {
|
|
298
467
|
detective: { reasoning: 10, coding: 8, security: 6, speed: 3, synthesis: 7 },
|
|
299
468
|
logic: { reasoning: 8, coding: 10, security: 3, speed: 6, synthesis: 6 },
|
|
@@ -304,19 +473,35 @@ class LegionCommand {
|
|
|
304
473
|
reviewer: { reasoning: 9, coding: 7, security: 7, speed: 6, synthesis: 8 },
|
|
305
474
|
architect: { reasoning: 10, coding: 9, security: 8, speed: 4, synthesis: 10 },
|
|
306
475
|
};
|
|
476
|
+
// Preferred model per role for each tier.
|
|
477
|
+
const preferredByRoleHeavy = {
|
|
478
|
+
detective: 'gpt-5.5',
|
|
479
|
+
logic: 'o3',
|
|
480
|
+
security: 'opus-4.7',
|
|
481
|
+
performance: 'deepseek-v4-pro',
|
|
482
|
+
edge_case: 'gemini-2.5-pro',
|
|
483
|
+
integration: 'kimi-k2.5',
|
|
484
|
+
reviewer: 'opus-4.7',
|
|
485
|
+
architect: 'gpt-5.5',
|
|
486
|
+
};
|
|
487
|
+
const preferredByRoleLite = {
|
|
488
|
+
detective: 'claude-sonnet-4-5',
|
|
489
|
+
logic: 'o4-mini',
|
|
490
|
+
security: 'claude-haiku-3-5',
|
|
491
|
+
performance: 'deepseek-v3',
|
|
492
|
+
edge_case: 'gemini-2.0-flash',
|
|
493
|
+
integration: 'kimi-k2.5',
|
|
494
|
+
reviewer: 'claude-haiku-3-5',
|
|
495
|
+
architect: 'claude-sonnet-4-5',
|
|
496
|
+
};
|
|
497
|
+
const preferredByRole = tier === 'lite' ? preferredByRoleLite : preferredByRoleHeavy;
|
|
498
|
+
// Real-world token volumes from observed Cortex runs (not theoretical minimums).
|
|
499
|
+
// Base: 8k input tokens per role (context + project scan + prompt).
|
|
307
500
|
const complexity = Math.max(1, Math.ceil((scan.lines / 4000) + (scan.importEdges / 200)));
|
|
501
|
+
const baseInputTokens = 8000 * complexity; // observed avg: 8k-25k per role
|
|
502
|
+
const baseOutputTokens = 2200 * complexity; // observed avg: 1.8k-3.6k per role
|
|
308
503
|
return Object.keys(roleWeights).map((role) => {
|
|
309
504
|
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
505
|
let best = models.find((m) => m.requestedModel === preferredByRole[role]) || models[0];
|
|
321
506
|
if (!best)
|
|
322
507
|
best = models[0];
|
|
@@ -333,13 +518,13 @@ class LegionCommand {
|
|
|
333
518
|
}
|
|
334
519
|
}
|
|
335
520
|
}
|
|
336
|
-
const estInputTokens =
|
|
337
|
-
const estOutputTokens =
|
|
521
|
+
const estInputTokens = baseInputTokens;
|
|
522
|
+
const estOutputTokens = baseOutputTokens;
|
|
338
523
|
const estCostUsd = (estInputTokens / 1_000_000) * best.estInputPer1M + (estOutputTokens / 1_000_000) * best.estOutputPer1M;
|
|
339
524
|
return { role, model: best.id, requestedModel: best.requestedModel, estInputTokens, estOutputTokens, estCostUsd };
|
|
340
525
|
});
|
|
341
526
|
}
|
|
342
|
-
|
|
527
|
+
buildCortexExplicitSteps(execution, speedMode = false) {
|
|
343
528
|
const { originalRequest, workspace, scan, quote } = execution;
|
|
344
529
|
const quoteByRole = new Map(quote.map((row) => [row.role, row]));
|
|
345
530
|
const topFiles = scan.topFiles.slice(0, 5).map((file) => `${file.file} (${file.lines} lines, ${file.imports} imports)`);
|
|
@@ -356,26 +541,47 @@ class LegionCommand {
|
|
|
356
541
|
const roleIterationBudget = {
|
|
357
542
|
detective: 4,
|
|
358
543
|
logic: 5,
|
|
359
|
-
security: 5,
|
|
360
|
-
performance:
|
|
361
|
-
edge_case: 4,
|
|
362
|
-
integration:
|
|
363
|
-
reviewer: 5,
|
|
544
|
+
security: speedMode ? 3 : 5,
|
|
545
|
+
performance: speedMode ? 3 : 5,
|
|
546
|
+
edge_case: speedMode ? 3 : 4,
|
|
547
|
+
integration: speedMode ? 3 : 6,
|
|
548
|
+
reviewer: speedMode ? 3 : 5,
|
|
364
549
|
architect: 5,
|
|
365
550
|
};
|
|
551
|
+
const optionalRoles = new Set(['security', 'performance', 'edge_case', 'integration', 'reviewer']);
|
|
366
552
|
const steps = roleSequence.map(({ role, dependsOn }) => {
|
|
367
553
|
const row = quoteByRole.get(role);
|
|
368
|
-
const
|
|
369
|
-
const
|
|
554
|
+
const isOptionalRole = optionalRoles.has(role);
|
|
555
|
+
const requestedModel = role === 'detective'
|
|
556
|
+
? 'gpt-5.5'
|
|
557
|
+
: role === 'architect'
|
|
558
|
+
? (scan.lines <= 3000 ? 'opus-4.7' : 'gpt-5.5')
|
|
559
|
+
: (row?.requestedModel || 'cloud-pro');
|
|
560
|
+
const model = role === 'detective'
|
|
561
|
+
? 'openrouter:openai/gpt-5.5'
|
|
562
|
+
: role === 'architect'
|
|
563
|
+
? (requestedModel === 'opus-4.7' ? 'openrouter:anthropic/claude-opus-4.7' : 'openrouter:openai/gpt-5.5')
|
|
564
|
+
: (row?.model || 'openrouter:deepseek/deepseek-chat');
|
|
565
|
+
// Compact instruction packet: role tag + request digest + role micro-prompt.
|
|
566
|
+
// Full workspace, scan stats, and key files are already in the payload.
|
|
567
|
+
const ROLE_MICRO_PROMPTS = {
|
|
568
|
+
detective: 'Root-cause analysis. Identify failing paths, unknown risks, and systemic gaps. Output: numbered findings with confidence level.',
|
|
569
|
+
logic: 'Verify iteration bounds, fallback chains, state transitions, race conditions. Output: PASS/FAIL per subsystem with evidence.',
|
|
570
|
+
security: 'Audit for auth bypass, injection flaws, unsafe exec, secret exposure, SSRF. Verdict: SAFE/RISK/CRITICAL per finding.',
|
|
571
|
+
performance: 'Find N+1 queries, unbounded loops, sync I/O in async paths, memory leaks. Output: hotspot list with severity.',
|
|
572
|
+
edge_case: 'Probe boundary conditions, null/undefined paths, concurrent mutations, retry storms. Output: concrete failure scenarios.',
|
|
573
|
+
integration: 'Verify endpoint registration, auth middleware wiring, env var presence, cross-service contracts. PASS/FAIL per check.',
|
|
574
|
+
reviewer: 'Final gate review. Confirm contract coverage, flag regressions. Verdict: CONDITIONAL-GO or NO-GO with rationale.',
|
|
575
|
+
architect: 'Synthesize all role findings into a production-readiness verdict with a prioritized action list.',
|
|
576
|
+
};
|
|
577
|
+
const reqDigest = originalRequest.length > 220
|
|
578
|
+
? originalRequest.slice(0, 220) + '\u2026'
|
|
579
|
+
: originalRequest;
|
|
370
580
|
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');
|
|
581
|
+
`[CORTEX:${role.toUpperCase()}] model=${requestedModel} budget=${roleIterationBudget[role] || 3}iter`,
|
|
582
|
+
reqDigest,
|
|
583
|
+
ROLE_MICRO_PROMPTS[role] || 'Analyse the codebase and deliver your findings concisely.',
|
|
584
|
+
].join('\\n');
|
|
379
585
|
return {
|
|
380
586
|
step_id: role,
|
|
381
587
|
worker_name: 'v3_agent_worker',
|
|
@@ -383,7 +589,7 @@ class LegionCommand {
|
|
|
383
589
|
depends_on: dependsOn,
|
|
384
590
|
priority: role === 'architect' ? 2 : 3,
|
|
385
591
|
retry_policy: {
|
|
386
|
-
max_attempts: 1,
|
|
592
|
+
max_attempts: role === 'performance' || role === 'edge_case' ? 2 : 1,
|
|
387
593
|
strategy: 'repair',
|
|
388
594
|
requires_validation_failure: false,
|
|
389
595
|
},
|
|
@@ -391,14 +597,20 @@ class LegionCommand {
|
|
|
391
597
|
role,
|
|
392
598
|
requested_model: requestedModel,
|
|
393
599
|
quoted_model: model,
|
|
600
|
+
enforce_cloud_only: role === 'detective' || role === 'architect',
|
|
601
|
+
speed_mode: speedMode,
|
|
602
|
+
optional_role: isOptionalRole,
|
|
603
|
+
allow_convergence_skip: speedMode && isOptionalRole,
|
|
604
|
+
convergence_sources: ['detective', 'logic'],
|
|
394
605
|
workspace,
|
|
395
606
|
top_files: topFiles,
|
|
396
607
|
max_iterations: roleIterationBudget[role] || 4,
|
|
397
608
|
compact_context: true,
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
609
|
+
context_sniper_max_depth: 2,
|
|
610
|
+
max_dependency_chars: role === 'architect' ? 8000 : 4000,
|
|
611
|
+
max_artifacts: role === 'architect' ? 12 : 8,
|
|
612
|
+
max_context_chars: role === 'architect' ? 90000 : 60000,
|
|
613
|
+
max_output_tokens: role === 'detective' ? 3600 : role === 'architect' ? 3400 : role === 'reviewer' ? 2800 : 2200,
|
|
402
614
|
request_timeout_seconds: role === 'detective' || role === 'architect' ? 1200 : 720,
|
|
403
615
|
},
|
|
404
616
|
};
|
|
@@ -407,11 +619,11 @@ class LegionCommand {
|
|
|
407
619
|
step_id: 'testing',
|
|
408
620
|
worker_name: 'testing_worker',
|
|
409
621
|
objective: [
|
|
410
|
-
'[
|
|
622
|
+
'[CORTEX:TESTING]',
|
|
411
623
|
originalRequest,
|
|
412
624
|
`Workspace: ${workspace}`,
|
|
413
|
-
'Validate
|
|
414
|
-
].join('
|
|
625
|
+
'Validate architect output with narrow relevant checks and report concrete failures only.',
|
|
626
|
+
].join('\\n'),
|
|
415
627
|
depends_on: ['architect'],
|
|
416
628
|
priority: 2,
|
|
417
629
|
retry_policy: {
|
|
@@ -426,24 +638,48 @@ class LegionCommand {
|
|
|
426
638
|
});
|
|
427
639
|
return steps;
|
|
428
640
|
}
|
|
429
|
-
buildBillingQuote(quote) {
|
|
641
|
+
buildBillingQuote(quote, tier = 'heavy') {
|
|
430
642
|
const baseUsd = quote.reduce((sum, r) => sum + r.estCostUsd, 0);
|
|
431
|
-
const marginPctRaw = Number.parseFloat(String(process.env.VIGTHORIA_GODMODE_MARGIN_PCT || '10'));
|
|
643
|
+
const marginPctRaw = Number.parseFloat(String(process.env.VIGTHORIA_CORTEX_MARGIN_PCT || process.env.VIGTHORIA_GODMODE_MARGIN_PCT || '10'));
|
|
432
644
|
const marginPct = Number.isFinite(marginPctRaw) ? Math.max(0, marginPctRaw) : 10;
|
|
433
|
-
const
|
|
645
|
+
const platformFeePct = CORTEX_PLATFORM_FEE_PCT;
|
|
646
|
+
const platformFeeMultiplier = 1 + (platformFeePct / 100);
|
|
647
|
+
const finalUsd = baseUsd * (1 + (marginPct / 100)) * platformFeeMultiplier;
|
|
648
|
+
// Retry-adjusted estimate: all 7 critical roles can trigger the quality-gate
|
|
649
|
+
// retry in state_manager.py (adds +2 iterations per degraded role).
|
|
650
|
+
// Probability of retry per critical role ≈ 45% (from production telemetry).
|
|
651
|
+
// RETRY_ITER_RATIO = additional spend when retry fires: +2 iters / avg 5 iters base ≈ 40%.
|
|
652
|
+
const CRITICAL_ROLES = new Set(['detective', 'logic', 'security', 'performance', 'integration', 'reviewer', 'architect']);
|
|
653
|
+
const RETRY_PROB = 0.45;
|
|
654
|
+
const RETRY_ITER_RATIO = 0.40;
|
|
655
|
+
const retryExtra = quote
|
|
656
|
+
.filter((r) => CRITICAL_ROLES.has(r.role))
|
|
657
|
+
.reduce((sum, r) => sum + r.estCostUsd * RETRY_PROB * RETRY_ITER_RATIO, 0);
|
|
658
|
+
const retryAdjustedUsd = (baseUsd + retryExtra) * (1 + (marginPct / 100)) * platformFeeMultiplier;
|
|
659
|
+
// Range bounds.
|
|
660
|
+
const rangeMinUsd = finalUsd; // single pass, no retries
|
|
661
|
+
const maxRetryExtra = quote
|
|
662
|
+
.filter((r) => CRITICAL_ROLES.has(r.role))
|
|
663
|
+
.reduce((sum, r) => sum + r.estCostUsd * RETRY_ITER_RATIO, 0);
|
|
664
|
+
const rangeMaxUsd = (baseUsd + maxRetryExtra) * (1 + (marginPct / 100)) * platformFeeMultiplier;
|
|
434
665
|
const vigcoinRateRaw = Number.parseFloat(String(process.env.VIGTHORIA_VIGCOIN_USD_RATE || '1'));
|
|
435
666
|
const vigcoinRateUsd = Number.isFinite(vigcoinRateRaw) && vigcoinRateRaw > 0 ? vigcoinRateRaw : 1;
|
|
436
|
-
const vigcoinRequired =
|
|
667
|
+
const vigcoinRequired = retryAdjustedUsd / vigcoinRateUsd;
|
|
437
668
|
return {
|
|
438
669
|
baseUsd,
|
|
439
670
|
marginPct,
|
|
440
671
|
finalUsd,
|
|
672
|
+
retryAdjustedUsd,
|
|
673
|
+
rangeMinUsd,
|
|
674
|
+
rangeMaxUsd,
|
|
675
|
+
tier,
|
|
441
676
|
vigcoinRateUsd,
|
|
442
677
|
vigcoinRequired,
|
|
678
|
+
platformFeePct,
|
|
443
679
|
};
|
|
444
680
|
}
|
|
445
681
|
async evaluateBillingGate(billingQuote) {
|
|
446
|
-
const forcedPlan = String(process.env.VIGTHORIA_GODMODE_FORCE_PLAN || '').trim().toLowerCase();
|
|
682
|
+
const forcedPlan = String(process.env.VIGTHORIA_CORTEX_FORCE_PLAN || process.env.VIGTHORIA_GODMODE_FORCE_PLAN || '').trim().toLowerCase();
|
|
447
683
|
// On-server invocations using a service key run as trusted infrastructure.
|
|
448
684
|
// No user wallet check is needed — cost is tracked at the service level.
|
|
449
685
|
const hasServiceKey = !!(process.env.HYPERLOOP_SERVICE_KEY || process.env.V3_SERVICE_KEY);
|
|
@@ -460,7 +696,7 @@ class LegionCommand {
|
|
|
460
696
|
},
|
|
461
697
|
};
|
|
462
698
|
}
|
|
463
|
-
const entitlement = await this.
|
|
699
|
+
const entitlement = await this.fetchCortexEntitlement();
|
|
464
700
|
const normalizedPlan = forcedPlan || entitlement.plan || this.config.getNormalizedPlan() || 'free';
|
|
465
701
|
const masterAdminFree = this.isMasterAdminFree(normalizedPlan, entitlement.masterAccess, entitlement.isMasterAdmin);
|
|
466
702
|
if (masterAdminFree) {
|
|
@@ -512,8 +748,8 @@ class LegionCommand {
|
|
|
512
748
|
}
|
|
513
749
|
return null;
|
|
514
750
|
}
|
|
515
|
-
async
|
|
516
|
-
if (process.env.VIGTHORIA_GODMODE_FORCE_MASTER_ACCESS === '1') {
|
|
751
|
+
async fetchCortexEntitlement() {
|
|
752
|
+
if (process.env.VIGTHORIA_CORTEX_FORCE_MASTER_ACCESS === '1' || process.env.VIGTHORIA_GODMODE_FORCE_MASTER_ACCESS === '1') {
|
|
517
753
|
return { plan: this.config.getNormalizedPlan() || 'free', masterAccess: true, isMasterAdmin: true };
|
|
518
754
|
}
|
|
519
755
|
const baseUrl = this.getBillingBaseUrl();
|
|
@@ -568,7 +804,7 @@ class LegionCommand {
|
|
|
568
804
|
}
|
|
569
805
|
}
|
|
570
806
|
catch (err) {
|
|
571
|
-
this.logger.warn(this.formatLegionError(`
|
|
807
|
+
this.logger.warn(this.formatLegionError(`Cortex entitlement request ${endpoint}`, err));
|
|
572
808
|
continue;
|
|
573
809
|
}
|
|
574
810
|
}
|
|
@@ -649,8 +885,8 @@ class LegionCommand {
|
|
|
649
885
|
async fetchWalletState() {
|
|
650
886
|
const baseUrl = this.getBillingBaseUrl();
|
|
651
887
|
const headers = this.getHeaders();
|
|
652
|
-
const forcedLow = process.env.VIGTHORIA_GODMODE_FORCE_LOW_CREDIT === '1';
|
|
653
|
-
const forcedBalanceRaw = process.env.VIGTHORIA_GODMODE_FORCE_BALANCE;
|
|
888
|
+
const forcedLow = process.env.VIGTHORIA_CORTEX_FORCE_LOW_CREDIT === '1' || process.env.VIGTHORIA_GODMODE_FORCE_LOW_CREDIT === '1';
|
|
889
|
+
const forcedBalanceRaw = process.env.VIGTHORIA_CORTEX_FORCE_BALANCE || process.env.VIGTHORIA_GODMODE_FORCE_BALANCE;
|
|
654
890
|
if (forcedLow) {
|
|
655
891
|
return {
|
|
656
892
|
available: true,
|
|
@@ -720,9 +956,9 @@ class LegionCommand {
|
|
|
720
956
|
const headers = this.getHeaders();
|
|
721
957
|
const amount = Math.max(1, Math.ceil(vigcoinNeeded));
|
|
722
958
|
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: '
|
|
959
|
+
{ endpoint: '/api/viagen6/vigcoin/charge', body: { amount, reason: 'cortex_legion' } },
|
|
960
|
+
{ endpoint: '/api/wallet/charge', body: { amount, currency: 'VIGCOIN', reason: 'cortex_legion' } },
|
|
961
|
+
{ endpoint: '/api/billing/topup', body: { vigcoin: amount, reason: 'cortex_legion' } },
|
|
726
962
|
];
|
|
727
963
|
for (const attempt of chargePayloads) {
|
|
728
964
|
try {
|
|
@@ -766,7 +1002,7 @@ class LegionCommand {
|
|
|
766
1002
|
if (gate.masterAdminFree) {
|
|
767
1003
|
return true;
|
|
768
1004
|
}
|
|
769
|
-
const spinner = (0, logger_js_1.createSpinner)('Charging VigCoin wallet for
|
|
1005
|
+
const spinner = (0, logger_js_1.createSpinner)('Charging VigCoin wallet for Cortex execution...').start();
|
|
770
1006
|
const result = await this.attemptDirectCharge(billingQuote.vigcoinRequired);
|
|
771
1007
|
spinner.stop();
|
|
772
1008
|
if (!result.ok) {
|
|
@@ -774,7 +1010,7 @@ class LegionCommand {
|
|
|
774
1010
|
console.log(chalk_1.default.yellow(`Complete purchase first: ${result.checkoutUrl || `${this.getBillingBaseUrl()}/music/store#vigcoins`}`));
|
|
775
1011
|
return false;
|
|
776
1012
|
}
|
|
777
|
-
console.log(chalk_1.default.green('Wallet charged for
|
|
1013
|
+
console.log(chalk_1.default.green('Wallet charged for Cortex execution.'));
|
|
778
1014
|
return true;
|
|
779
1015
|
}
|
|
780
1016
|
async resolveBillingInsufficientFunds(billingQuote, gate, options) {
|
|
@@ -837,7 +1073,8 @@ class LegionCommand {
|
|
|
837
1073
|
console.log(chalk_1.default.green(' Free tier override applied (Master Admin).'));
|
|
838
1074
|
return;
|
|
839
1075
|
}
|
|
840
|
-
console.log(chalk_1.default.gray(` Estimated total (USD): $${billingQuote.
|
|
1076
|
+
console.log(chalk_1.default.gray(` Estimated total (USD): $${billingQuote.retryAdjustedUsd.toFixed(4)}`) + chalk_1.default.gray(' (retry-adjusted expected)'));
|
|
1077
|
+
console.log(chalk_1.default.gray(` Platform fee: +${billingQuote.platformFeePct.toFixed(0)}% (already included in all estimates)`));
|
|
841
1078
|
console.log(chalk_1.default.gray(` VigCoin rate: 1 VIG = $${billingQuote.vigcoinRateUsd.toFixed(4)}`));
|
|
842
1079
|
console.log(chalk_1.default.gray(` VigCoin required: ${billingQuote.vigcoinRequired.toFixed(3)}`));
|
|
843
1080
|
if (gate.wallet.vigcoinBalance !== null) {
|
|
@@ -851,12 +1088,15 @@ class LegionCommand {
|
|
|
851
1088
|
console.log(chalk_1.default.gray(` Purchase URL: ${gate.wallet.purchaseUrl}`));
|
|
852
1089
|
}
|
|
853
1090
|
}
|
|
854
|
-
|
|
855
|
-
const
|
|
1091
|
+
printCortexQuote(workspace, scan, quote, billingQuote, gate) {
|
|
1092
|
+
const tierLabel = billingQuote.tier === 'lite'
|
|
1093
|
+
? chalk_1.default.cyan('LITE') + chalk_1.default.gray(' — efficient (claude-sonnet/haiku, o4-mini, gemini-flash, deepseek-v3)')
|
|
1094
|
+
: chalk_1.default.magenta('HEAVY') + chalk_1.default.gray(' — strongest LLMs (gpt-5.5, opus-4.7, o3, gemini-2.5-pro)');
|
|
856
1095
|
console.log();
|
|
857
|
-
console.log(chalk_1.default.bold.white(` ${logger_js_1.CH.hLine.repeat(3)}
|
|
1096
|
+
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
1097
|
console.log();
|
|
859
1098
|
console.log(chalk_1.default.gray(' Workspace: ') + chalk_1.default.white(workspace));
|
|
1099
|
+
console.log(chalk_1.default.gray(' Tier: ') + tierLabel);
|
|
860
1100
|
console.log(chalk_1.default.gray(' Files scanned: ') + chalk_1.default.white(String(scan.files)));
|
|
861
1101
|
console.log(chalk_1.default.gray(' Lines scanned: ') + chalk_1.default.white(String(scan.lines)));
|
|
862
1102
|
console.log(chalk_1.default.gray(' Dependency edges: ') + chalk_1.default.white(String(scan.importEdges)));
|
|
@@ -870,11 +1110,18 @@ class LegionCommand {
|
|
|
870
1110
|
console.log();
|
|
871
1111
|
console.log(chalk_1.default.white(' Role assignment and estimated cost:'));
|
|
872
1112
|
for (const row of quote) {
|
|
873
|
-
|
|
1113
|
+
const publicModelLabel = row.requestedModel || row.model.replace(/^openrouter:/i, '').split('/').pop() || 'managed-model';
|
|
1114
|
+
const roleEstWithFee = row.estCostUsd * (1 + (billingQuote.platformFeePct / 100));
|
|
1115
|
+
console.log(chalk_1.default.gray(` ${logger_js_1.CH.bullet} ${row.role.padEnd(11)} ${publicModelLabel} $${roleEstWithFee.toFixed(4)}`));
|
|
874
1116
|
}
|
|
875
1117
|
console.log();
|
|
876
|
-
console.log(chalk_1.default.yellow(`
|
|
1118
|
+
console.log(chalk_1.default.yellow(` Cost range (single-pass best case): $${billingQuote.rangeMinUsd.toFixed(4)}`));
|
|
1119
|
+
console.log(chalk_1.default.yellow(` Cost range (expected with retries): $${billingQuote.retryAdjustedUsd.toFixed(4)}`) + chalk_1.default.gray(' ← use this for budget planning'));
|
|
1120
|
+
console.log(chalk_1.default.yellow(` Cost range (worst case, all retries): $${billingQuote.rangeMaxUsd.toFixed(4)}`));
|
|
1121
|
+
console.log(chalk_1.default.gray(' Retry model: 45% chance per critical role triggers quality-gate (+40% iterations per retry).'));
|
|
1122
|
+
console.log(chalk_1.default.gray(' A mid-run checkpoint will appear when 70% of the expected estimate is consumed.'));
|
|
877
1123
|
console.log(chalk_1.default.gray(' Flow: Estimate -> Isolation -> Parallel Attack -> Synthesis'));
|
|
1124
|
+
console.log(chalk_1.default.gray(` All displayed costs include platform fee (+${billingQuote.platformFeePct.toFixed(0)}%).`));
|
|
878
1125
|
console.log();
|
|
879
1126
|
this.printBillingGateSummary(billingQuote, gate);
|
|
880
1127
|
console.log();
|
|
@@ -886,7 +1133,7 @@ class LegionCommand {
|
|
|
886
1133
|
}
|
|
887
1134
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
888
1135
|
try {
|
|
889
|
-
const answer = (await rl.question('Proceed with
|
|
1136
|
+
const answer = (await rl.question('Proceed with Cortex execution? (y/N): ')).trim().toLowerCase();
|
|
890
1137
|
return answer === 'y' || answer === 'yes';
|
|
891
1138
|
}
|
|
892
1139
|
finally {
|
|
@@ -896,7 +1143,7 @@ class LegionCommand {
|
|
|
896
1143
|
/**
|
|
897
1144
|
* SSE streaming URL for the Legion execution endpoint.
|
|
898
1145
|
* Always hits Hyper Loop directly (port 8020) with the service key to avoid
|
|
899
|
-
* gateway JWT expiry killing long-running
|
|
1146
|
+
* gateway JWT expiry killing long-running Cortex jobs.
|
|
900
1147
|
*/
|
|
901
1148
|
getLegionStreamUrl() {
|
|
902
1149
|
const envOverride = String(process.env.VIGTHORIA_HYPERLOOP_URL || '').trim().replace(/\/$/, '');
|
|
@@ -910,12 +1157,16 @@ class LegionCommand {
|
|
|
910
1157
|
process.env.V3_SERVICE_KEY ||
|
|
911
1158
|
'');
|
|
912
1159
|
}
|
|
913
|
-
async planAndExecute(request, options,
|
|
914
|
-
const explicitSteps =
|
|
915
|
-
const workspace =
|
|
1160
|
+
async planAndExecute(request, options, cortexExecution) {
|
|
1161
|
+
const explicitSteps = cortexExecution ? this.buildCortexExplicitSteps(cortexExecution, options.speed === true) : undefined;
|
|
1162
|
+
const workspace = cortexExecution?.workspace || options.project || process.cwd();
|
|
916
1163
|
const body = {
|
|
917
1164
|
request,
|
|
918
|
-
context: {
|
|
1165
|
+
context: {
|
|
1166
|
+
workspace,
|
|
1167
|
+
governor_budget_secs: 600,
|
|
1168
|
+
governor_optional_roles: ['security', 'performance', 'edge_case', 'integration', 'reviewer'],
|
|
1169
|
+
},
|
|
919
1170
|
constraints: {
|
|
920
1171
|
active_only: true,
|
|
921
1172
|
execution_timeout_seconds: options.timeoutSec,
|
|
@@ -930,7 +1181,6 @@ class LegionCommand {
|
|
|
930
1181
|
headers['X-Service-Key'] = serviceKey;
|
|
931
1182
|
}
|
|
932
1183
|
else {
|
|
933
|
-
// Fallback: pass user JWT (works if token is still valid)
|
|
934
1184
|
const token = this.config.get('authToken');
|
|
935
1185
|
if (token) {
|
|
936
1186
|
headers['Authorization'] = `Bearer ${token}`;
|
|
@@ -941,7 +1191,6 @@ class LegionCommand {
|
|
|
941
1191
|
const startTime = Date.now();
|
|
942
1192
|
let response;
|
|
943
1193
|
try {
|
|
944
|
-
// No AbortSignal timeout — SSE keeps alive; server controls lifetime
|
|
945
1194
|
response = await fetch(streamUrl, {
|
|
946
1195
|
method: 'POST',
|
|
947
1196
|
headers,
|
|
@@ -951,20 +1200,19 @@ class LegionCommand {
|
|
|
951
1200
|
catch (connErr) {
|
|
952
1201
|
spinner.stop();
|
|
953
1202
|
this.logger.error(`Cannot connect to Hyper Loop at ${streamUrl}: ${connErr?.message || connErr}`);
|
|
954
|
-
return;
|
|
1203
|
+
return { status: 'failed', plannedSteps: 0, completedSteps: 0 };
|
|
955
1204
|
}
|
|
956
1205
|
if (!response.ok) {
|
|
957
1206
|
spinner.stop();
|
|
958
1207
|
const errBody = await response.text().catch(() => '');
|
|
959
1208
|
this.logger.error(`Legion stream ${response.status}: ${(0, api_js_1.describeUpstreamStatus)(response.status)} — ${errBody.slice(0, 200)}`);
|
|
960
|
-
return;
|
|
1209
|
+
return { status: 'failed', plannedSteps: 0, completedSteps: 0 };
|
|
961
1210
|
}
|
|
962
1211
|
if (!response.body) {
|
|
963
1212
|
spinner.stop();
|
|
964
1213
|
this.logger.error('Legion stream returned no response body');
|
|
965
|
-
return;
|
|
1214
|
+
return { status: 'failed', plannedSteps: 0, completedSteps: 0 };
|
|
966
1215
|
}
|
|
967
|
-
// ── SSE consumer ─────────────────────────────────────────────
|
|
968
1216
|
spinner.stop();
|
|
969
1217
|
console.log();
|
|
970
1218
|
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 +1222,22 @@ class LegionCommand {
|
|
|
974
1222
|
let finalResult = null;
|
|
975
1223
|
let stepsTotal = 0;
|
|
976
1224
|
let stepsDone = 0;
|
|
1225
|
+
let finalStatus = 'failed';
|
|
1226
|
+
let failedStepId = '';
|
|
1227
|
+
let failedWorker = '';
|
|
1228
|
+
const streamEvents = [];
|
|
1229
|
+
// Mid-run budget checkpoint: accumulate estimated spend per completed role.
|
|
1230
|
+
const roleQuoteIndex = new Map((cortexExecution?.quote || []).map((q) => [q.role, q.estCostUsd]));
|
|
1231
|
+
const marginPctMidrun = Number.isFinite(Number.parseFloat(String(process.env.VIGTHORIA_CORTEX_MARGIN_PCT || '10')))
|
|
1232
|
+
? Math.max(0, Number.parseFloat(String(process.env.VIGTHORIA_CORTEX_MARGIN_PCT || '10')))
|
|
1233
|
+
: 10;
|
|
1234
|
+
const baseEstimateUsd = (cortexExecution?.quote || []).reduce((s, q) => s + q.estCostUsd, 0);
|
|
1235
|
+
const budgetCheckpointThreshold = cortexExecution
|
|
1236
|
+
? baseEstimateUsd * (1 + marginPctMidrun / 100) * (1 + CORTEX_PLATFORM_FEE_PCT / 100) * 0.70
|
|
1237
|
+
: Infinity;
|
|
1238
|
+
let accumulatedEstUsd = 0;
|
|
1239
|
+
let budgetCheckpointFired = false;
|
|
1240
|
+
const completedRoleSummaries = [];
|
|
977
1241
|
try {
|
|
978
1242
|
const reader = response.body.getReader();
|
|
979
1243
|
while (true) {
|
|
@@ -982,11 +1246,11 @@ class LegionCommand {
|
|
|
982
1246
|
break;
|
|
983
1247
|
buffer += decoder.decode(value, { stream: true });
|
|
984
1248
|
const lines = buffer.split('\n');
|
|
985
|
-
buffer = lines.pop() ?? '';
|
|
1249
|
+
buffer = lines.pop() ?? '';
|
|
986
1250
|
for (const line of lines) {
|
|
987
1251
|
const trimmed = line.trim();
|
|
988
1252
|
if (!trimmed || trimmed.startsWith(':'))
|
|
989
|
-
continue;
|
|
1253
|
+
continue;
|
|
990
1254
|
if (!trimmed.startsWith('data:'))
|
|
991
1255
|
continue;
|
|
992
1256
|
const jsonStr = trimmed.slice(5).trim();
|
|
@@ -997,6 +1261,7 @@ class LegionCommand {
|
|
|
997
1261
|
catch {
|
|
998
1262
|
continue;
|
|
999
1263
|
}
|
|
1264
|
+
streamEvents.push(evt);
|
|
1000
1265
|
switch (evt.event) {
|
|
1001
1266
|
case 'plan':
|
|
1002
1267
|
stepsTotal = evt.steps_total || 0;
|
|
@@ -1009,17 +1274,87 @@ class LegionCommand {
|
|
|
1009
1274
|
case 'step_complete': {
|
|
1010
1275
|
stepsDone = Number(evt.steps_done) || 0;
|
|
1011
1276
|
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
|
-
|
|
1277
|
+
const stepSummaryRaw = String(evt.summary || '');
|
|
1278
|
+
const summarySnip = stepSummaryRaw ? chalk_1.default.gray(` — ${stepSummaryRaw.slice(0, 120)}`) : '';
|
|
1279
|
+
console.log(` ${icon} ${chalk_1.default.white(String(evt.step_id))} ${chalk_1.default.gray('[' + evt.worker + ']')}${summarySnip}`);
|
|
1280
|
+
if (evt.status !== 'completed' && !failedStepId) {
|
|
1281
|
+
failedStepId = String(evt.step_id || '');
|
|
1282
|
+
failedWorker = String(evt.worker || '');
|
|
1283
|
+
}
|
|
1284
|
+
// Track spend and trigger mid-run budget checkpoint.
|
|
1285
|
+
const stepRole = String(evt.step_id || '');
|
|
1286
|
+
accumulatedEstUsd += (roleQuoteIndex.get(stepRole) || 0) * (1 + CORTEX_PLATFORM_FEE_PCT / 100);
|
|
1287
|
+
completedRoleSummaries.push({ role: stepRole, status: String(evt.status || ''), summary: stepSummaryRaw.slice(0, 200) });
|
|
1288
|
+
if (!budgetCheckpointFired
|
|
1289
|
+
&& cortexExecution
|
|
1290
|
+
&& accumulatedEstUsd >= budgetCheckpointThreshold
|
|
1291
|
+
&& budgetCheckpointThreshold < Infinity
|
|
1292
|
+
&& process.stdin.isTTY
|
|
1293
|
+
&& process.stdout.isTTY) {
|
|
1294
|
+
budgetCheckpointFired = true;
|
|
1295
|
+
const remainingRoles = (cortexExecution.quote || [])
|
|
1296
|
+
.filter((q) => !completedRoleSummaries.some((c) => c.role === q.role))
|
|
1297
|
+
.map((q) => q.role);
|
|
1298
|
+
const remainingEstUsd = (cortexExecution.quote || [])
|
|
1299
|
+
.filter((q) => remainingRoles.includes(q.role))
|
|
1300
|
+
.reduce((s, q) => s + (q.estCostUsd * (1 + CORTEX_PLATFORM_FEE_PCT / 100)), 0);
|
|
1301
|
+
console.log();
|
|
1302
|
+
console.log(chalk_1.default.bold.yellow(' ━━━ Mid-Run Budget Checkpoint ━━━'));
|
|
1303
|
+
console.log(chalk_1.default.gray(` Consumed so far (estimated): $${accumulatedEstUsd.toFixed(4)}`));
|
|
1304
|
+
console.log(chalk_1.default.gray(` Estimated remaining: $${remainingEstUsd.toFixed(4)}`));
|
|
1305
|
+
console.log();
|
|
1306
|
+
console.log(chalk_1.default.white(' Roles completed:'));
|
|
1307
|
+
for (const c of completedRoleSummaries) {
|
|
1308
|
+
const roleIcon = c.status === 'completed' ? chalk_1.default.green('✔') : chalk_1.default.red('✘');
|
|
1309
|
+
const roleSumSnip = c.summary ? ` — ${c.summary.slice(0, 100)}` : '';
|
|
1310
|
+
console.log(chalk_1.default.gray(` ${roleIcon} ${c.role.padEnd(11)}${roleSumSnip}`));
|
|
1311
|
+
}
|
|
1312
|
+
console.log();
|
|
1313
|
+
console.log(chalk_1.default.white(' Remaining roles: ') + chalk_1.default.gray(remainingRoles.join(', ') || 'none'));
|
|
1314
|
+
console.log();
|
|
1315
|
+
const checkpointRl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1316
|
+
let continueRun = false;
|
|
1317
|
+
try {
|
|
1318
|
+
const ans = (await checkpointRl.question(' Budget checkpoint — continue execution? (y/N): ')).trim().toLowerCase();
|
|
1319
|
+
continueRun = ans === 'y' || ans === 'yes';
|
|
1320
|
+
}
|
|
1321
|
+
finally {
|
|
1322
|
+
checkpointRl.close();
|
|
1323
|
+
}
|
|
1324
|
+
if (!continueRun) {
|
|
1325
|
+
console.log(chalk_1.default.yellow(' Cortex stopped by user at budget checkpoint.'));
|
|
1326
|
+
console.log(chalk_1.default.gray(` State: ${stepsDone} of ${stepsTotal} steps completed.`));
|
|
1327
|
+
finalStatus = 'failed';
|
|
1328
|
+
failedStepId = failedStepId || 'user_checkpoint_stop';
|
|
1329
|
+
// Break out of the for-of lines loop; the outer while(true) will end when reader.read() drains.
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
console.log(chalk_1.default.green(' Continuing execution…'));
|
|
1333
|
+
console.log();
|
|
1334
|
+
}
|
|
1014
1335
|
break;
|
|
1015
1336
|
}
|
|
1016
1337
|
case 'complete':
|
|
1017
|
-
finalResult = evt.result ||
|
|
1338
|
+
finalResult = evt.result || finalResult;
|
|
1339
|
+
if (Number.isFinite(Number(evt.steps_done))) {
|
|
1340
|
+
stepsDone = Number(evt.steps_done) || stepsDone;
|
|
1341
|
+
}
|
|
1342
|
+
if (Number.isFinite(Number(evt.steps_total))) {
|
|
1343
|
+
stepsTotal = Number(evt.steps_total) || stepsTotal;
|
|
1344
|
+
}
|
|
1345
|
+
if (!failedStepId && evt.failed_step_id) {
|
|
1346
|
+
failedStepId = String(evt.failed_step_id || '');
|
|
1347
|
+
}
|
|
1348
|
+
if (!failedWorker && evt.failed_worker) {
|
|
1349
|
+
failedWorker = String(evt.failed_worker || '');
|
|
1350
|
+
}
|
|
1018
1351
|
if (evt.status === 'completed') {
|
|
1352
|
+
finalStatus = 'completed';
|
|
1019
1353
|
console.log();
|
|
1020
1354
|
console.log(chalk_1.default.green(` ${logger_js_1.CH.success} Legion completed successfully`));
|
|
1021
1355
|
}
|
|
1022
1356
|
else if (evt.status === 'failed') {
|
|
1357
|
+
finalStatus = 'failed';
|
|
1023
1358
|
console.log();
|
|
1024
1359
|
console.log(chalk_1.default.red(` ${logger_js_1.CH.error} Legion execution failed`));
|
|
1025
1360
|
if (evt.error)
|
|
@@ -1027,6 +1362,7 @@ class LegionCommand {
|
|
|
1027
1362
|
}
|
|
1028
1363
|
break;
|
|
1029
1364
|
case 'error':
|
|
1365
|
+
finalStatus = 'failed';
|
|
1030
1366
|
console.log(chalk_1.default.red(` ${logger_js_1.CH.error} Stream error: ${String(evt.error).slice(0, 300)}`));
|
|
1031
1367
|
break;
|
|
1032
1368
|
}
|
|
@@ -1035,6 +1371,7 @@ class LegionCommand {
|
|
|
1035
1371
|
}
|
|
1036
1372
|
catch (streamErr) {
|
|
1037
1373
|
this.logger.error(`Legion stream read error: ${streamErr?.message || streamErr}`);
|
|
1374
|
+
finalStatus = 'failed';
|
|
1038
1375
|
}
|
|
1039
1376
|
const elapsedSec = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
1040
1377
|
if (stepsTotal > 0 && stepsDone < stepsTotal) {
|
|
@@ -1042,17 +1379,107 @@ class LegionCommand {
|
|
|
1042
1379
|
}
|
|
1043
1380
|
console.log();
|
|
1044
1381
|
console.log(chalk_1.default.gray(` Time: ${elapsedSec}s`));
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1382
|
+
const lastStepResult = finalResult?.final_output;
|
|
1383
|
+
const summary = String(lastStepResult?.result?.summary || lastStepResult?.summary || '');
|
|
1384
|
+
if (summary) {
|
|
1385
|
+
console.log();
|
|
1386
|
+
console.log(chalk_1.default.white(' Final output:'));
|
|
1387
|
+
console.log(chalk_1.default.gray(` ${summary.slice(0, 600)}`));
|
|
1388
|
+
}
|
|
1389
|
+
if (cortexExecution) {
|
|
1390
|
+
const report = this.buildCortexRunReport({
|
|
1391
|
+
generatedAt: new Date().toISOString(),
|
|
1392
|
+
status: finalStatus,
|
|
1393
|
+
elapsedSeconds: Number(elapsedSec),
|
|
1394
|
+
workspace: cortexExecution.workspace,
|
|
1395
|
+
request: cortexExecution.originalRequest,
|
|
1396
|
+
plannedSteps: stepsTotal,
|
|
1397
|
+
completedSteps: stepsDone,
|
|
1398
|
+
modifiedFiles: this.extractModifiedFiles(finalResult),
|
|
1399
|
+
workers: [],
|
|
1400
|
+
finalSummary: summary,
|
|
1401
|
+
}, streamEvents);
|
|
1402
|
+
const files = this.writeCortexSummaryReport(report);
|
|
1403
|
+
console.log(chalk_1.default.gray(' Cortex summary: ' + files.markdownPath));
|
|
1404
|
+
console.log(chalk_1.default.gray(' Cortex report JSON: ' + files.jsonPath));
|
|
1054
1405
|
}
|
|
1055
1406
|
console.log();
|
|
1407
|
+
return {
|
|
1408
|
+
status: finalStatus,
|
|
1409
|
+
plannedSteps: stepsTotal,
|
|
1410
|
+
completedSteps: stepsDone,
|
|
1411
|
+
failedStepId: failedStepId || undefined,
|
|
1412
|
+
failedWorker: failedWorker || undefined,
|
|
1413
|
+
finalSummary: summary || undefined,
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
buildCortexRunReport(base, streamEvents) {
|
|
1417
|
+
const workers = streamEvents
|
|
1418
|
+
.filter((evt) => evt && evt.event === 'step_complete')
|
|
1419
|
+
.map((evt) => ({
|
|
1420
|
+
stepId: String(evt.step_id || ''),
|
|
1421
|
+
worker: String(evt.worker || ''),
|
|
1422
|
+
status: String(evt.status || ''),
|
|
1423
|
+
summary: String(evt.summary || ''),
|
|
1424
|
+
}));
|
|
1425
|
+
return { ...base, workers };
|
|
1426
|
+
}
|
|
1427
|
+
extractModifiedFiles(finalResult) {
|
|
1428
|
+
if (!finalResult || typeof finalResult !== 'object')
|
|
1429
|
+
return [];
|
|
1430
|
+
const candidates = [];
|
|
1431
|
+
const finalOutput = (finalResult.final_output && typeof finalResult.final_output === 'object') ? finalResult.final_output : null;
|
|
1432
|
+
const result = (finalOutput?.result && typeof finalOutput.result === 'object') ? finalOutput.result : null;
|
|
1433
|
+
for (const key of ['modified_files', 'changed_files', 'files_changed', 'touched_files']) {
|
|
1434
|
+
const top = finalResult[key];
|
|
1435
|
+
const res = result ? result[key] : undefined;
|
|
1436
|
+
if (Array.isArray(top))
|
|
1437
|
+
candidates.push(...top.map((x) => String(x || '').trim()));
|
|
1438
|
+
if (Array.isArray(res))
|
|
1439
|
+
candidates.push(...res.map((x) => String(x || '').trim()));
|
|
1440
|
+
}
|
|
1441
|
+
return Array.from(new Set(candidates.filter((x) => x.length > 0)));
|
|
1442
|
+
}
|
|
1443
|
+
writeCortexSummaryReport(report) {
|
|
1444
|
+
const reportsDir = path.join(report.workspace, '.vigthoria', 'reports');
|
|
1445
|
+
fs.mkdirSync(reportsDir, { recursive: true });
|
|
1446
|
+
const stamp = report.generatedAt.replace(/[:.]/g, '-');
|
|
1447
|
+
const markdownPath = path.join(reportsDir, 'CORTEX_SUMMARY_' + stamp + '.md');
|
|
1448
|
+
const jsonPath = path.join(reportsDir, 'CORTEX_SUMMARY_' + stamp + '.json');
|
|
1449
|
+
const workerLines = report.workers.length > 0
|
|
1450
|
+
? report.workers.map((w) => '- ' + w.stepId + ' [' + w.worker + '] ' + w.status + (w.summary ? ': ' + w.summary : '')).join('\n')
|
|
1451
|
+
: '- No worker step summaries captured';
|
|
1452
|
+
const modifiedLines = report.modifiedFiles.length > 0
|
|
1453
|
+
? report.modifiedFiles.map((f) => '- ' + f).join('\n')
|
|
1454
|
+
: '- No modified files reported by backend outputs';
|
|
1455
|
+
const markdown = [
|
|
1456
|
+
'# Cortex Run Summary',
|
|
1457
|
+
'',
|
|
1458
|
+
'Generated: ' + report.generatedAt,
|
|
1459
|
+
'Status: ' + report.status,
|
|
1460
|
+
'Elapsed: ' + report.elapsedSeconds + 's',
|
|
1461
|
+
'Workspace: ' + report.workspace,
|
|
1462
|
+
'',
|
|
1463
|
+
'## Request',
|
|
1464
|
+
report.request,
|
|
1465
|
+
'',
|
|
1466
|
+
'## Execution',
|
|
1467
|
+
'- Planned steps: ' + report.plannedSteps,
|
|
1468
|
+
'- Completed steps: ' + report.completedSteps,
|
|
1469
|
+
'',
|
|
1470
|
+
'## Worker Results',
|
|
1471
|
+
workerLines,
|
|
1472
|
+
'',
|
|
1473
|
+
'## Modified Files',
|
|
1474
|
+
modifiedLines,
|
|
1475
|
+
'',
|
|
1476
|
+
'## Final Summary',
|
|
1477
|
+
report.finalSummary || 'No final summary reported by backend',
|
|
1478
|
+
'',
|
|
1479
|
+
].join('\n');
|
|
1480
|
+
fs.writeFileSync(markdownPath, markdown, 'utf8');
|
|
1481
|
+
fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2) + '\n', 'utf8');
|
|
1482
|
+
return { markdownPath, jsonPath };
|
|
1056
1483
|
}
|
|
1057
1484
|
formatLegionError(context, err) {
|
|
1058
1485
|
const message = err?.message || String(err || 'Unknown error');
|