vigthoria-cli 1.9.2 → 1.9.5

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