vigthoria-cli 1.10.55 → 1.11.2

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.
@@ -13,6 +13,7 @@ import { TaskDisplay } from '../utils/task-display.js';
13
13
  import { ProjectMemoryService } from '../utils/project-memory.js';
14
14
  import { buildPersonaOverlay, normalizePersonaMode } from '../utils/persona.js';
15
15
  import { runAgentSessionMenu, shouldShowAgentSessionMenu } from './agent-session-menu.js';
16
+ import { createLiveOutcome, evaluateExecutorSuccess, handleTaskEvent, noteAnalysisToolUse, } from '../utils/agentRunOutcome.js';
16
17
  const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
17
18
  const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
18
19
  if (!rawValue) {
@@ -353,7 +354,12 @@ export class ChatCommand {
353
354
  * question — these should use analysis_only workflow, not full_autonomy.
354
355
  */
355
356
  isAnalysisLookupPrompt(prompt) {
356
- return /^(what|which|where|how many|who|find|list|show|check|inspect|analyze|analyse|audit|explain|describe|summarize|summarise|review|overview|count|read|look at|tell me|locate|search for|does .* exist)/i.test(prompt.trim());
357
+ const trimmed = prompt.trim();
358
+ if (/^(what|which|where|how many|who|find|list|show|check|inspect|analyze|analyse|audit|explain|describe|summarize|summarise|review|overview|count|read|look at|tell me|locate|search for|does .* exist)/i.test(trimmed)) {
359
+ return true;
360
+ }
361
+ // Mid-conversation accountability / lookup questions (Gideon regression)
362
+ return /\b(where\s+(?:is|are)|what\s+(?:is|are|have|did|was|were)|how\s+(?:do|does|is|are)|what\s+(?:have\s+)?you\s+(?:done|changed|built)|what\s+(?:is|was)\s+(?:your|the)\s+answer)\b/i.test(trimmed);
357
363
  }
358
364
  extractExplicitLocalPath(prompt) {
359
365
  // Try to extract Windows paths (C:\ D:\ etc.)
@@ -2362,16 +2368,11 @@ export class ChatCommand {
2362
2368
  }).start();
2363
2369
  // Live run telemetry, used both for the final summary block and for /retry, /continue.
2364
2370
  const liveOutcome = {
2365
- tasksSucceeded: 0,
2366
- tasksTotal: 0,
2367
- failedTaskIds: new Set(),
2371
+ ...createLiveOutcome(),
2368
2372
  unfinishedTaskIds: new Set(),
2369
2373
  qualityScore: null,
2370
2374
  qualityMissing: [],
2371
2375
  qualityBlockers: [],
2372
- plannerError: null,
2373
- executorError: null,
2374
- executorFailed: false,
2375
2376
  };
2376
2377
  const parsePlannerSummary = (raw) => {
2377
2378
  if (!raw || typeof raw !== 'string')
@@ -2487,20 +2488,27 @@ export class ChatCommand {
2487
2488
  }
2488
2489
  else if (event.type === 'executor_complete') {
2489
2490
  const summary = event.summary || {};
2490
- const tid = summary.task_id || summary.id;
2491
- if (summary.status === 'failed') {
2492
- if (tid)
2493
- liveOutcome.failedTaskIds.add(String(tid));
2494
- liveOutcome.executorFailed = true;
2491
+ handleTaskEvent(summary, liveOutcome);
2492
+ if (summary.status === 'failed' || summary.status === 'stalled_error') {
2495
2493
  const err = typeof summary.error === 'string' ? summary.error.trim() : '';
2496
2494
  if (err && !liveOutcome.executorError)
2497
2495
  liveOutcome.executorError = err;
2498
2496
  }
2499
- else if (summary.status === 'completed' || summary.status === 'success') {
2500
- if (tid)
2501
- liveOutcome.failedTaskIds.delete(String(tid));
2502
- liveOutcome.tasksSucceeded += 1;
2497
+ }
2498
+ else if (event.type === 'tool_call' || event.type === 'tool_result') {
2499
+ const toolName = String(event.name || event.tool || '').trim();
2500
+ if (toolName)
2501
+ noteAnalysisToolUse(toolName, liveOutcome);
2502
+ }
2503
+ else if (event.type === 'complete') {
2504
+ const tt = Number(event.tasks_total);
2505
+ if (Number.isFinite(tt) && tt > 0) {
2506
+ liveOutcome.tasksTotal = tt;
2507
+ }
2508
+ if (Number(event.discovery_tools_used) > 0) {
2509
+ liveOutcome.analysisToolsUsed = Math.max(liveOutcome.analysisToolsUsed, Number(event.discovery_tools_used));
2503
2510
  }
2511
+ taskDisplay.complete(1);
2504
2512
  }
2505
2513
  else if (event.type === 'executor_error') {
2506
2514
  const msg = typeof event.error === 'string' ? event.error : '';
@@ -2520,9 +2528,6 @@ export class ChatCommand {
2520
2528
  parsePlannerSummary(msg);
2521
2529
  }
2522
2530
  }
2523
- else if (event.type === 'complete') {
2524
- taskDisplay.complete(1);
2525
- }
2526
2531
  if (spinner)
2527
2532
  this.updateV3AgentSpinner(spinner, event);
2528
2533
  },
@@ -2539,6 +2544,9 @@ export class ChatCommand {
2539
2544
  const workspaceHasOutput = this.api.hasAgentWorkspaceOutput(workspaceContext);
2540
2545
  const changedFileCount = response.changedFiles ? Object.keys(response.changedFiles).length : 0;
2541
2546
  const requiresWorkspaceChanges = this.taskRequiresWorkspaceChanges(prompt);
2547
+ liveOutcome.changedFileCount = changedFileCount;
2548
+ liveOutcome.requiresWorkspaceChanges = requiresWorkspaceChanges;
2549
+ liveOutcome.workspaceHasOutput = workspaceHasOutput;
2542
2550
  const success = previewGate?.required === true
2543
2551
  ? (previewGate?.passed === true || workspaceHasOutput)
2544
2552
  : true;
@@ -2576,20 +2584,7 @@ export class ChatCommand {
2576
2584
  if (!this.jsonOutput && previewGate?.required && previewGate?.passed !== true && workspaceHasOutput) {
2577
2585
  console.log(chalk.yellow(`Template Service preview gate did not fully validate this output, but generated workspace files were preserved${previewGate?.error ? `: ${previewGate.error}` : '.'}`));
2578
2586
  }
2579
- if (this.jsonOutput) {
2580
- console.log(JSON.stringify({
2581
- success,
2582
- mode: 'agent',
2583
- model: routingPolicy.selectedModel,
2584
- routingPolicy,
2585
- taskId: response.taskId || null,
2586
- contextId: response.contextId || null,
2587
- partial: response.partial === true,
2588
- content: response.content || 'V3 agent workflow completed.',
2589
- metadata: response.metadata || {},
2590
- }, null, 2));
2591
- }
2592
- else if (this.v3StreamingStarted) {
2587
+ if (this.v3StreamingStarted) {
2593
2588
  // Content was already streamed to stdout in real-time; skip duplicate print.
2594
2589
  if (!this.jsonOutput) {
2595
2590
  console.log(chalk.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
@@ -2634,10 +2629,8 @@ export class ChatCommand {
2634
2629
  // executor errors and the spinner needs to clearly say "failed", not "✓".
2635
2630
  let selfHealStatus = null;
2636
2631
  let selfHealTool = null;
2637
- const executorSucceeded = !liveOutcome.executorFailed
2638
- && !liveOutcome.plannerError
2639
- && !liveOutcome.executorError
2640
- && (requiresWorkspaceChanges ? (workspaceHasOutput && changedFileCount > 0) : true);
2632
+ const runEvaluation = evaluateExecutorSuccess(liveOutcome);
2633
+ const executorSucceeded = runEvaluation.executorSucceeded;
2641
2634
  if (!executorSucceeded && requiresWorkspaceChanges && changedFileCount === 0 && !liveOutcome.executorError) {
2642
2635
  const clientToolErrors = this.api.getClientToolErrors();
2643
2636
  const transportErrors = this.api.getLastChatTransportErrors();
@@ -2709,6 +2702,9 @@ export class ChatCommand {
2709
2702
  }
2710
2703
  taskDisplay.finalize();
2711
2704
  // ────────────────────────────────────────────────────────────────
2705
+ if (!executorSucceeded) {
2706
+ process.exitCode = 1;
2707
+ }
2712
2708
  this.lastAgentRunOutcome = {
2713
2709
  prompt,
2714
2710
  taskId: response.taskId || null,
@@ -2734,9 +2730,37 @@ export class ChatCommand {
2734
2730
  finishedAt: Date.now(),
2735
2731
  };
2736
2732
  if (!this.jsonOutput && !this.directPromptMode) {
2737
- this.printAgentRunSummary(this.lastAgentRunOutcome, executorSucceeded, changedFileCount);
2733
+ this.printAgentRunSummary(this.lastAgentRunOutcome, runEvaluation, changedFileCount);
2738
2734
  }
2739
- this.messages.push({ role: 'assistant', content: response.content || 'V3 agent workflow completed.' });
2735
+ if (this.jsonOutput) {
2736
+ if (!executorSucceeded) {
2737
+ process.exitCode = 1;
2738
+ }
2739
+ console.log(JSON.stringify({
2740
+ success: executorSucceeded,
2741
+ mode: 'agent',
2742
+ model: routingPolicy.selectedModel,
2743
+ routingPolicy,
2744
+ taskId: response.taskId || null,
2745
+ contextId: response.contextId || null,
2746
+ partial: response.partial === true || (!executorSucceeded && liveOutcome.tasksSucceeded > 0),
2747
+ content: response.content || runEvaluation.statusHeadline,
2748
+ statusHeadline: runEvaluation.statusHeadline,
2749
+ tasksSucceeded: liveOutcome.tasksSucceeded,
2750
+ tasksTotal: liveOutcome.tasksTotal,
2751
+ metadata: {
2752
+ ...(response.metadata || {}),
2753
+ outcomeTruth: {
2754
+ executorSucceeded,
2755
+ uiTheme: runEvaluation.uiTheme,
2756
+ tasksSucceeded: liveOutcome.tasksSucceeded,
2757
+ tasksTotal: liveOutcome.tasksTotal,
2758
+ },
2759
+ previewGate,
2760
+ },
2761
+ }, null, 2));
2762
+ }
2763
+ this.messages.push({ role: 'assistant', content: response.content || runEvaluation.statusHeadline });
2740
2764
  watcher?.stop();
2741
2765
  return true;
2742
2766
  }
@@ -2765,6 +2789,10 @@ export class ChatCommand {
2765
2789
  : 'Agent mode is unavailable right now. Please retry shortly or run vigthoria login if the issue persists.';
2766
2790
  this.logger.error(errorMessage);
2767
2791
  this.messages.push({ role: 'assistant', content: errorMessage });
2792
+ liveOutcome.streamAborted = true;
2793
+ if (!liveOutcome.executorError) {
2794
+ liveOutcome.executorError = safeDetail || 'Connection aborted';
2795
+ }
2768
2796
  // Resolve any half-rendered TaskDisplay spinners before the prompt
2769
2797
  // comes back, otherwise the user sees `⟳ Execute tasks` next to `>`.
2770
2798
  try {
@@ -2796,20 +2824,34 @@ export class ChatCommand {
2796
2824
  workspaceSyncIssue: null,
2797
2825
  finishedAt: Date.now(),
2798
2826
  };
2827
+ if (!this.jsonOutput) {
2828
+ process.exitCode = 1;
2829
+ }
2799
2830
  if (this.jsonOutput) {
2800
2831
  process.exitCode = 1;
2801
2832
  console.log(JSON.stringify({
2802
2833
  success: false,
2803
2834
  mode: 'agent',
2804
2835
  model: routingPolicy.selectedModel,
2805
- partial: false,
2836
+ partial: liveOutcome.tasksSucceeded > 0 && liveOutcome.tasksTotal > 0,
2806
2837
  content: '',
2807
2838
  error: errorMessage,
2839
+ statusHeadline: evaluateExecutorSuccess(liveOutcome).statusHeadline,
2840
+ tasksSucceeded: liveOutcome.tasksSucceeded,
2841
+ tasksTotal: liveOutcome.tasksTotal,
2808
2842
  metadata: { executionPath: 'v3-agent' },
2809
2843
  }, null, 2));
2810
2844
  }
2811
2845
  else if (!this.directPromptMode) {
2812
- this.printAgentRunSummary(this.lastAgentRunOutcome, false, 0);
2846
+ const failedEval = evaluateExecutorSuccess({
2847
+ ...liveOutcome,
2848
+ executorFailed: true,
2849
+ executorError: liveOutcome.executorError || safeDetail,
2850
+ changedFileCount: 0,
2851
+ requiresWorkspaceChanges: this.taskRequiresWorkspaceChanges(prompt),
2852
+ workspaceHasOutput: this.api.hasAgentWorkspaceOutput(workspaceContext),
2853
+ });
2854
+ this.printAgentRunSummary(this.lastAgentRunOutcome, failedEval, 0);
2813
2855
  }
2814
2856
  return true;
2815
2857
  }
@@ -3106,7 +3148,8 @@ export class ChatCommand {
3106
3148
  * - User can answer "What do I type next?" without reading 200 scrollback lines.
3107
3149
  * - Never leave the spinners (`⟳`, `–`) ambiguous when the prompt returns.
3108
3150
  */
3109
- printAgentRunSummary(outcome, executorSucceeded, changedFileCount) {
3151
+ printAgentRunSummary(outcome, evaluation, changedFileCount) {
3152
+ const executorSucceeded = evaluation.executorSucceeded;
3110
3153
  const bar = chalk.gray('─'.repeat(63));
3111
3154
  const ok = chalk.green('✓');
3112
3155
  const warn = chalk.yellow('⚠');
@@ -3116,15 +3159,9 @@ export class ChatCommand {
3116
3159
  const hasTaskInfo = outcome.tasksTotal > 0 || failedList.length > 0 || unfinishedList.length > 0;
3117
3160
  console.log('');
3118
3161
  console.log(bar);
3119
- if (executorSucceeded && outcome.selfHealStatus !== 'partial' && outcome.selfHealStatus !== 'failed' && failedList.length === 0) {
3120
- console.log(`${ok} ${chalk.bold('Agent run finished')}${changedFileCount ? chalk.gray(` — ${changedFileCount} file${changedFileCount === 1 ? '' : 's'} changed`) : ''}`);
3121
- }
3122
- else if (executorSucceeded) {
3123
- console.log(`${warn} ${chalk.bold('Agent run finished with warnings')}${changedFileCount ? chalk.gray(` — ${changedFileCount} file${changedFileCount === 1 ? '' : 's'} changed`) : ''}`);
3124
- }
3125
- else {
3126
- console.log(`${bad} ${chalk.bold('Agent run did not complete')}${changedFileCount ? chalk.gray(` — ${changedFileCount} file${changedFileCount === 1 ? '' : 's'} changed`) : ''}`);
3127
- }
3162
+ const headlineIcon = evaluation.uiTheme === 'success' ? ok : evaluation.uiTheme === 'warning' ? warn : bad;
3163
+ const headlineText = evaluation.statusHeadline.replace(/^[✓⚠✕]\s*/, '');
3164
+ console.log(`${headlineIcon} ${chalk.bold(headlineText)}${changedFileCount ? chalk.gray(` — ${changedFileCount} file${changedFileCount === 1 ? '' : 's'} changed`) : ''}`);
3128
3165
  if (hasTaskInfo) {
3129
3166
  const succ = outcome.tasksTotal > 0 ? `${outcome.tasksSucceeded}/${outcome.tasksTotal}` : `${outcome.tasksSucceeded}`;
3130
3167
  console.log(chalk.gray(` Tasks completed: ${succ}`));
@@ -3293,12 +3330,18 @@ export class ChatCommand {
3293
3330
  return;
3294
3331
  }
3295
3332
  const o = this.lastAgentRunOutcome;
3296
- const executorSucceeded = o.failedTaskIds.length === 0
3297
- && o.unfinishedTaskIds.length === 0
3298
- && !o.plannerError
3299
- && !o.executorError
3300
- && o.hasOutput;
3301
- this.printAgentRunSummary(o, executorSucceeded, 0);
3333
+ const statusEval = evaluateExecutorSuccess({
3334
+ ...createLiveOutcome(),
3335
+ executorFailed: o.failedTaskIds.length > 0,
3336
+ plannerError: o.plannerError,
3337
+ executorError: o.executorError,
3338
+ tasksTotal: o.tasksTotal,
3339
+ tasksSucceeded: o.tasksSucceeded,
3340
+ requiresWorkspaceChanges: this.taskRequiresWorkspaceChanges(o.prompt),
3341
+ workspaceHasOutput: o.hasOutput,
3342
+ failedTaskIds: new Set(o.failedTaskIds),
3343
+ });
3344
+ this.printAgentRunSummary(o, statusEval, 0);
3302
3345
  }
3303
3346
  showContext() {
3304
3347
  if (!this.currentSession) {
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Task-and-quality-validated success evaluation for V3 agent runs.
3
+ * Decouples CLI verdict from raw file-change counts (1.11.0+).
4
+ */
5
+ export interface LiveOutcome {
6
+ executorFailed: boolean;
7
+ plannerError: string | null;
8
+ executorError: string | null;
9
+ tasksTotal: number;
10
+ tasksSucceeded: number;
11
+ changedFileCount: number;
12
+ requiresWorkspaceChanges: boolean;
13
+ workspaceHasOutput: boolean;
14
+ failedTaskIds: Set<string>;
15
+ completedTaskIds: Set<string>;
16
+ analysisToolsUsed: number;
17
+ streamAborted?: boolean;
18
+ }
19
+ export interface TaskSummaryEvent {
20
+ status?: string;
21
+ task_id?: string;
22
+ id?: string;
23
+ }
24
+ export interface RunEvaluation {
25
+ executorSucceeded: boolean;
26
+ statusHeadline: string;
27
+ uiTheme: 'success' | 'warning' | 'error';
28
+ }
29
+ export declare function createLiveOutcome(): LiveOutcome;
30
+ /** Process executor_complete / task summary events from the V3 SSE stream. */
31
+ export declare function handleTaskEvent(summary: TaskSummaryEvent, liveOutcome: LiveOutcome): void;
32
+ /** Track read/discovery tool usage for analysis-turn accountability. */
33
+ export declare function noteAnalysisToolUse(toolName: string, liveOutcome: LiveOutcome): void;
34
+ /**
35
+ * Compute the real CLI success verdict.
36
+ * When tasks were planned, ALL must succeed — file changes alone never imply victory.
37
+ */
38
+ export declare function evaluateExecutorSuccess(liveOutcome: LiveOutcome): RunEvaluation;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Task-and-quality-validated success evaluation for V3 agent runs.
3
+ * Decouples CLI verdict from raw file-change counts (1.11.0+).
4
+ */
5
+ export function createLiveOutcome() {
6
+ return {
7
+ executorFailed: false,
8
+ plannerError: null,
9
+ executorError: null,
10
+ tasksTotal: 0,
11
+ tasksSucceeded: 0,
12
+ changedFileCount: 0,
13
+ requiresWorkspaceChanges: false,
14
+ workspaceHasOutput: false,
15
+ failedTaskIds: new Set(),
16
+ completedTaskIds: new Set(),
17
+ analysisToolsUsed: 0,
18
+ streamAborted: false,
19
+ };
20
+ }
21
+ /** Process executor_complete / task summary events from the V3 SSE stream. */
22
+ export function handleTaskEvent(summary, liveOutcome) {
23
+ const tid = summary.task_id || summary.id;
24
+ const status = String(summary.status || '').toLowerCase();
25
+ if (!liveOutcome.completedTaskIds) {
26
+ liveOutcome.completedTaskIds = new Set();
27
+ }
28
+ if (status === 'completed' || status === 'success') {
29
+ if (tid) {
30
+ const id = String(tid);
31
+ if (liveOutcome.completedTaskIds.has(id))
32
+ return;
33
+ liveOutcome.completedTaskIds.add(id);
34
+ liveOutcome.failedTaskIds.delete(id);
35
+ }
36
+ liveOutcome.tasksSucceeded += 1;
37
+ }
38
+ else if (status === 'failed' || status === 'blocked' || status === 'stalled_error') {
39
+ if (tid) {
40
+ liveOutcome.failedTaskIds.add(String(tid));
41
+ }
42
+ liveOutcome.executorFailed = true;
43
+ if (status === 'stalled_error') {
44
+ liveOutcome.streamAborted = true;
45
+ }
46
+ }
47
+ }
48
+ const READ_DISCOVERY_TOOLS = new Set([
49
+ 'read_file',
50
+ 'grep',
51
+ 'list_directory',
52
+ 'glob',
53
+ 'search_project',
54
+ 'list_dir',
55
+ 'dir',
56
+ 'read',
57
+ ]);
58
+ /** Track read/discovery tool usage for analysis-turn accountability. */
59
+ export function noteAnalysisToolUse(toolName, liveOutcome) {
60
+ const normalized = String(toolName || '').trim().toLowerCase();
61
+ if (READ_DISCOVERY_TOOLS.has(normalized)) {
62
+ liveOutcome.analysisToolsUsed += 1;
63
+ }
64
+ }
65
+ /**
66
+ * Compute the real CLI success verdict.
67
+ * When tasks were planned, ALL must succeed — file changes alone never imply victory.
68
+ */
69
+ export function evaluateExecutorSuccess(liveOutcome) {
70
+ const hasFatalError = liveOutcome.executorFailed
71
+ || Boolean(liveOutcome.plannerError)
72
+ || Boolean(liveOutcome.executorError)
73
+ || Boolean(liveOutcome.streamAborted);
74
+ if (liveOutcome.streamAborted && liveOutcome.tasksTotal > 0) {
75
+ const partial = liveOutcome.tasksSucceeded > 0;
76
+ return {
77
+ executorSucceeded: false,
78
+ statusHeadline: partial
79
+ ? `⚠ ${liveOutcome.tasksSucceeded}/${liveOutcome.tasksTotal} tasks completed — connection aborted`
80
+ : `✕ Connection aborted — 0/${liveOutcome.tasksTotal} tasks completed`,
81
+ uiTheme: 'warning',
82
+ };
83
+ }
84
+ if (hasFatalError && liveOutcome.tasksTotal === 0) {
85
+ return {
86
+ executorSucceeded: false,
87
+ statusHeadline: '✕ Execution Error',
88
+ uiTheme: 'error',
89
+ };
90
+ }
91
+ if (liveOutcome.tasksTotal > 0) {
92
+ const allTasksPassed = liveOutcome.tasksSucceeded === liveOutcome.tasksTotal;
93
+ const partialTasksPassed = liveOutcome.tasksSucceeded > 0;
94
+ const failedCount = liveOutcome.failedTaskIds.size;
95
+ if (allTasksPassed && !hasFatalError) {
96
+ return {
97
+ executorSucceeded: true,
98
+ statusHeadline: `✓ Agent run finished (${liveOutcome.tasksSucceeded}/${liveOutcome.tasksTotal} tasks completed)`,
99
+ uiTheme: 'success',
100
+ };
101
+ }
102
+ if (partialTasksPassed) {
103
+ return {
104
+ executorSucceeded: false,
105
+ statusHeadline: `⚠ Warning: Partial Run (${liveOutcome.tasksSucceeded}/${liveOutcome.tasksTotal} tasks completed${failedCount ? `, ${failedCount} failed` : ''})`,
106
+ uiTheme: 'warning',
107
+ };
108
+ }
109
+ return {
110
+ executorSucceeded: false,
111
+ statusHeadline: `✕ Failed: 0/${liveOutcome.tasksTotal} tasks completed${liveOutcome.changedFileCount > 0 ? ' (stub writes or stalls detected)' : ''}`,
112
+ uiTheme: 'error',
113
+ };
114
+ }
115
+ // Analysis / verification turns with no planned task graph
116
+ if (!liveOutcome.requiresWorkspaceChanges) {
117
+ const analysisOk = liveOutcome.analysisToolsUsed > 0 || liveOutcome.workspaceHasOutput;
118
+ if (!analysisOk && hasFatalError) {
119
+ return {
120
+ executorSucceeded: false,
121
+ statusHeadline: '✕ Analysis failed — no workspace evidence gathered',
122
+ uiTheme: 'error',
123
+ };
124
+ }
125
+ if (!analysisOk) {
126
+ return {
127
+ executorSucceeded: false,
128
+ statusHeadline: '⚠ Warning: No read tools confirmed — answer may be incomplete',
129
+ uiTheme: 'warning',
130
+ };
131
+ }
132
+ return {
133
+ executorSucceeded: true,
134
+ statusHeadline: '✓ Analysis completed',
135
+ uiTheme: 'success',
136
+ };
137
+ }
138
+ const legacySuccess = liveOutcome.workspaceHasOutput && liveOutcome.changedFileCount > 0;
139
+ return {
140
+ executorSucceeded: legacySuccess && !hasFatalError,
141
+ statusHeadline: legacySuccess && !hasFatalError
142
+ ? '✓ Agent run finished'
143
+ : '✕ No validated workspace output',
144
+ uiTheme: legacySuccess && !hasFatalError ? 'success' : 'error',
145
+ };
146
+ }
@@ -334,6 +334,8 @@ export declare class APIClient {
334
334
  * 5. Drop readmeExcerpt
335
335
  */
336
336
  private compactV3Context;
337
+ /** Remove duplicate functionIndex entries that poison model context after compaction. */
338
+ private dedupeBrainPayload;
337
339
  buildMinimalV3AgentContext(context?: Record<string, any>): string;
338
340
  private extractEmergencyAppName;
339
341
  private materializeEmergencySaaSWorkspace;
package/dist/utils/api.js CHANGED
@@ -1513,6 +1513,12 @@ export class APIClient {
1513
1513
  */
1514
1514
  compactV3Context(payload) {
1515
1515
  const LIMIT = APIClient.V3_CONTEXT_CHAR_LIMIT;
1516
+ if (payload.vigthoriaBrain) {
1517
+ payload.vigthoriaBrain = this.dedupeBrainPayload(payload.vigthoriaBrain);
1518
+ }
1519
+ if (payload.vigthoria_brain) {
1520
+ payload.vigthoria_brain = payload.vigthoriaBrain || this.dedupeBrainPayload(payload.vigthoria_brain);
1521
+ }
1516
1522
  let json = JSON.stringify(payload);
1517
1523
  if (json.length <= LIMIT)
1518
1524
  return json;
@@ -1586,6 +1592,51 @@ export class APIClient {
1586
1592
  }
1587
1593
  return json;
1588
1594
  }
1595
+ /** Remove duplicate functionIndex entries that poison model context after compaction. */
1596
+ dedupeBrainPayload(brain) {
1597
+ if (!brain || typeof brain !== 'object') {
1598
+ return brain;
1599
+ }
1600
+ const copy = { ...brain };
1601
+ const dedupeIndexEntries = (entries) => {
1602
+ const seen = new Set();
1603
+ const unique = [];
1604
+ for (const entry of entries) {
1605
+ if (!entry || typeof entry !== 'object')
1606
+ continue;
1607
+ const file = String(entry.file || '')
1608
+ .replace(/\\/g, '/')
1609
+ .replace(/^home\/user\//i, '')
1610
+ .toLowerCase();
1611
+ const name = String(entry.name || '');
1612
+ const line = String(entry.line ?? '');
1613
+ const key = `${name}::${file}::${line}`;
1614
+ if (seen.has(key))
1615
+ continue;
1616
+ seen.add(key);
1617
+ unique.push(entry);
1618
+ }
1619
+ return unique;
1620
+ };
1621
+ if (copy.functionIndex && typeof copy.functionIndex === 'object') {
1622
+ const deduped = {};
1623
+ for (const [symbol, entries] of Object.entries(copy.functionIndex)) {
1624
+ deduped[symbol] = Array.isArray(entries) ? dedupeIndexEntries(entries) : [];
1625
+ }
1626
+ copy.functionIndex = deduped;
1627
+ }
1628
+ if (copy.files && typeof copy.files === 'object') {
1629
+ const normalized = {};
1630
+ for (const [filePath, meta] of Object.entries(copy.files)) {
1631
+ const norm = String(filePath).replace(/\\/g, '/').replace(/^home\/user\//i, '');
1632
+ if (!(norm in normalized)) {
1633
+ normalized[norm] = meta;
1634
+ }
1635
+ }
1636
+ copy.files = normalized;
1637
+ }
1638
+ return copy;
1639
+ }
1589
1640
  buildMinimalV3AgentContext(context = {}) {
1590
1641
  const resolvedContext = this.ensureExecutionContext(context);
1591
1642
  const targetPath = this.resolveAgentTargetPath(resolvedContext)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.10.55",
3
+ "version": "1.11.2",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,7 +36,8 @@
36
36
  "start": "node dist/index.js",
37
37
  "dev": "ts-node src/index.ts",
38
38
  "test": "npm run test:cli",
39
- "test:cli": "npm run build && npm run precheck:services && node scripts/test-cli-suite.js",
39
+ "test:agent:outcome": "npm run build && node scripts/test-agent-run-outcome.js",
40
+ "test:cli": "npm run build && npm run precheck:services && node scripts/test-cli-suite.js && npm run test:agent:outcome",
40
41
  "test:regression": "npm run build && node scripts/test-regression-1.6.22.js",
41
42
  "test:agent:smoke": "npm run build && node scripts/test-agent-smoke.js",
42
43
  "test:agent:routing": "npm run build && node scripts/test-agent-routing-policy.js",
@@ -131,4 +131,7 @@ assert fb.get('reason') == 'governance-blocked-model', j
131
131
  print('[pass] governance fallback metadata')
132
132
  PY
133
133
 
134
+ echo "[outcome-truth] agent run outcome regression"
135
+ npm run test:agent:outcome
136
+
134
137
  echo "ALL NO-GO GATES PASSED"