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.
@@ -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.godmode) {
149
- await this.runGodmode(request, options);
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(' --godmode Run calculator + high-intelligence orchestration'));
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 runGodmode(request, options) {
176
+ async runCortex(request, options) {
170
177
  if (!request) {
171
- console.log(chalk_1.default.yellow('Usage: vigthoria legion --godmode "<task description>"'));
172
- console.log(chalk_1.default.gray(' --plan-only Run calculator only (no execution)'));
173
- console.log(chalk_1.default.gray(' --approve Skip confirmation prompt and execute'));
174
- console.log(chalk_1.default.gray(' --auto-charge Attempt direct VigCoin top-up when balance is low'));
175
- console.log(chalk_1.default.gray(' --timeout <sec> Abort remote execution if no result within timeout (default: 120)'));
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
- const selectedModels = this.resolveModelProfiles(options.models);
181
- const quote = this.buildRoleQuote(scan, selectedModels);
182
- const billingQuote = this.buildBillingQuote(quote);
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.printGodmodeQuote(workspace, scan, quote, billingQuote, billingGate);
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('Godmode calculator complete (plan-only).'));
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
- if (!billingGate.canProceed) {
190
- const resolved = await this.resolveBillingInsufficientFunds(billingQuote, billingGate, options);
191
- if (!resolved) {
192
- console.log(chalk_1.default.yellow('Godmode cancelled due to insufficient VigCoin balance.'));
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
- billingGate = await this.evaluateBillingGate(billingQuote);
196
- if (!billingGate.canProceed) {
197
- this.printBillingGateSummary(billingQuote, billingGate);
198
- console.log(chalk_1.default.red('Billing gate still blocked after charge attempt.'));
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
- const autoApprove = options.approve === true && options.noApprove !== true;
203
- const approved = autoApprove ? true : await this.confirmExecution();
204
- if (!approved) {
205
- console.log(chalk_1.default.yellow('Godmode cancelled by user.'));
206
- return;
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 charged = await this.collectExecutionCharge(billingQuote, billingGate);
209
- if (!charged) {
210
- console.log(chalk_1.default.yellow('Godmode cancelled because wallet charge was not completed.'));
211
- return;
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
- const catalog = [
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 = 1200 * complexity;
337
- const estOutputTokens = 1800 * complexity;
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
- buildGodmodeExplicitSteps(execution) {
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: 4,
361
- edge_case: 4,
362
- integration: 5,
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 requestedModel = row?.requestedModel || 'cloud-pro';
369
- const model = row?.model || 'openrouter:deepseek/deepseek-chat';
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
- `[GODMODE:${role.toUpperCase()}]`,
372
- originalRequest,
373
- `Workspace: ${workspace}`,
374
- `Execution contract: force requested_model=${requestedModel} via ${model}; do not substitute a local-only model.`,
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
- max_dependency_chars: role === 'architect' ? 18000 : 12000,
399
- max_artifacts: role === 'architect' ? 24 : 16,
400
- max_context_chars: role === 'architect' ? 240000 : 180000,
401
- max_output_tokens: role === 'detective' ? 3600 : role === 'architect' ? 3400 : role === 'reviewer' ? 3000 : 2400,
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
- '[GODMODE:TESTING]',
622
+ '[CORTEX:TESTING]',
411
623
  originalRequest,
412
624
  `Workspace: ${workspace}`,
413
- 'Validate the architect output, run the narrowest relevant checks, and report concrete failures if any remain.',
414
- ].join('\n'),
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 finalUsd = baseUsd * (1 + (marginPct / 100));
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 = finalUsd / vigcoinRateUsd;
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.fetchGodmodeEntitlement();
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 fetchGodmodeEntitlement() {
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(`Godmode entitlement request ${endpoint}`, err));
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: 'godmode_legion' } },
724
- { endpoint: '/api/wallet/charge', body: { amount, currency: 'VIGCOIN', reason: 'godmode_legion' } },
725
- { endpoint: '/api/billing/topup', body: { vigcoin: amount, reason: 'godmode_legion' } },
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 Godmode execution...').start();
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 Godmode execution.'));
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.finalUsd.toFixed(4)}`));
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
- printGodmodeQuote(workspace, scan, quote, billingQuote, gate) {
855
- const totalCost = billingQuote.finalUsd;
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)} Legion Godmode Calculator ${logger_js_1.CH.hLine.repeat(31)}`));
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
- console.log(chalk_1.default.gray(` ${logger_js_1.CH.bullet} ${row.role.padEnd(11)} ${row.model} $${row.estCostUsd.toFixed(4)}`));
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(` Estimated total: $${totalCost.toFixed(4)}`));
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 Godmode execution? (y/N): ')).trim().toLowerCase();
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 GodMode jobs.
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, godmodeExecution) {
914
- const explicitSteps = godmodeExecution ? this.buildGodmodeExplicitSteps(godmodeExecution) : undefined;
915
- const workspace = godmodeExecution?.workspace || options.project || process.cwd();
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: { workspace },
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() ?? ''; // keep incomplete last line
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; // keep-alive or comment
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 summary = evt.summary ? chalk_1.default.gray(` — ${String(evt.summary).slice(0, 120)}`) : '';
1013
- console.log(` ${icon} ${chalk_1.default.white(String(evt.step_id))} ${chalk_1.default.gray('[' + evt.worker + ']')}${summary}`);
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 || null;
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
- // Show final output from architect/last step
1046
- if (finalResult) {
1047
- const lastStepResult = finalResult.final_output;
1048
- const summary = lastStepResult?.result?.summary || lastStepResult?.summary || '';
1049
- if (summary) {
1050
- console.log();
1051
- console.log(chalk_1.default.white(' Final output:'));
1052
- console.log(chalk_1.default.gray(` ${String(summary).slice(0, 600)}`));
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');