vigthoria-cli 1.10.55 → 1.11.0

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 { 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.)
@@ -2372,6 +2378,10 @@ export class ChatCommand {
2372
2378
  plannerError: null,
2373
2379
  executorError: null,
2374
2380
  executorFailed: false,
2381
+ analysisToolsUsed: 0,
2382
+ changedFileCount: 0,
2383
+ requiresWorkspaceChanges: false,
2384
+ workspaceHasOutput: false,
2375
2385
  };
2376
2386
  const parsePlannerSummary = (raw) => {
2377
2387
  if (!raw || typeof raw !== 'string')
@@ -2487,20 +2497,23 @@ export class ChatCommand {
2487
2497
  }
2488
2498
  else if (event.type === 'executor_complete') {
2489
2499
  const summary = event.summary || {};
2490
- const tid = summary.task_id || summary.id;
2500
+ handleTaskEvent(summary, liveOutcome);
2491
2501
  if (summary.status === 'failed') {
2492
- if (tid)
2493
- liveOutcome.failedTaskIds.add(String(tid));
2494
- liveOutcome.executorFailed = true;
2495
2502
  const err = typeof summary.error === 'string' ? summary.error.trim() : '';
2496
2503
  if (err && !liveOutcome.executorError)
2497
2504
  liveOutcome.executorError = err;
2498
2505
  }
2499
- else if (summary.status === 'completed' || summary.status === 'success') {
2500
- if (tid)
2501
- liveOutcome.failedTaskIds.delete(String(tid));
2502
- liveOutcome.tasksSucceeded += 1;
2506
+ }
2507
+ else if (event.type === 'tool_call' || event.type === 'tool_result') {
2508
+ const toolName = String(event.name || event.tool || '').trim();
2509
+ if (toolName)
2510
+ noteAnalysisToolUse(toolName, liveOutcome);
2511
+ }
2512
+ else if (event.type === 'complete') {
2513
+ if (Number(event.discovery_tools_used) > 0) {
2514
+ liveOutcome.analysisToolsUsed = Math.max(liveOutcome.analysisToolsUsed, Number(event.discovery_tools_used));
2503
2515
  }
2516
+ taskDisplay.complete(1);
2504
2517
  }
2505
2518
  else if (event.type === 'executor_error') {
2506
2519
  const msg = typeof event.error === 'string' ? event.error : '';
@@ -2520,9 +2533,6 @@ export class ChatCommand {
2520
2533
  parsePlannerSummary(msg);
2521
2534
  }
2522
2535
  }
2523
- else if (event.type === 'complete') {
2524
- taskDisplay.complete(1);
2525
- }
2526
2536
  if (spinner)
2527
2537
  this.updateV3AgentSpinner(spinner, event);
2528
2538
  },
@@ -2539,6 +2549,9 @@ export class ChatCommand {
2539
2549
  const workspaceHasOutput = this.api.hasAgentWorkspaceOutput(workspaceContext);
2540
2550
  const changedFileCount = response.changedFiles ? Object.keys(response.changedFiles).length : 0;
2541
2551
  const requiresWorkspaceChanges = this.taskRequiresWorkspaceChanges(prompt);
2552
+ liveOutcome.changedFileCount = changedFileCount;
2553
+ liveOutcome.requiresWorkspaceChanges = requiresWorkspaceChanges;
2554
+ liveOutcome.workspaceHasOutput = workspaceHasOutput;
2542
2555
  const success = previewGate?.required === true
2543
2556
  ? (previewGate?.passed === true || workspaceHasOutput)
2544
2557
  : true;
@@ -2634,10 +2647,8 @@ export class ChatCommand {
2634
2647
  // executor errors and the spinner needs to clearly say "failed", not "✓".
2635
2648
  let selfHealStatus = null;
2636
2649
  let selfHealTool = null;
2637
- const executorSucceeded = !liveOutcome.executorFailed
2638
- && !liveOutcome.plannerError
2639
- && !liveOutcome.executorError
2640
- && (requiresWorkspaceChanges ? (workspaceHasOutput && changedFileCount > 0) : true);
2650
+ const runEvaluation = evaluateExecutorSuccess(liveOutcome);
2651
+ const executorSucceeded = runEvaluation.executorSucceeded;
2641
2652
  if (!executorSucceeded && requiresWorkspaceChanges && changedFileCount === 0 && !liveOutcome.executorError) {
2642
2653
  const clientToolErrors = this.api.getClientToolErrors();
2643
2654
  const transportErrors = this.api.getLastChatTransportErrors();
@@ -2734,7 +2745,7 @@ export class ChatCommand {
2734
2745
  finishedAt: Date.now(),
2735
2746
  };
2736
2747
  if (!this.jsonOutput && !this.directPromptMode) {
2737
- this.printAgentRunSummary(this.lastAgentRunOutcome, executorSucceeded, changedFileCount);
2748
+ this.printAgentRunSummary(this.lastAgentRunOutcome, runEvaluation, changedFileCount);
2738
2749
  }
2739
2750
  this.messages.push({ role: 'assistant', content: response.content || 'V3 agent workflow completed.' });
2740
2751
  watcher?.stop();
@@ -2809,7 +2820,19 @@ export class ChatCommand {
2809
2820
  }, null, 2));
2810
2821
  }
2811
2822
  else if (!this.directPromptMode) {
2812
- this.printAgentRunSummary(this.lastAgentRunOutcome, false, 0);
2823
+ const failedEval = evaluateExecutorSuccess({
2824
+ executorFailed: true,
2825
+ plannerError: liveOutcome.plannerError,
2826
+ executorError: liveOutcome.executorError || safeDetail,
2827
+ tasksTotal: liveOutcome.tasksTotal,
2828
+ tasksSucceeded: liveOutcome.tasksSucceeded,
2829
+ changedFileCount: 0,
2830
+ requiresWorkspaceChanges: this.taskRequiresWorkspaceChanges(prompt),
2831
+ workspaceHasOutput: this.api.hasAgentWorkspaceOutput(workspaceContext),
2832
+ failedTaskIds: liveOutcome.failedTaskIds,
2833
+ analysisToolsUsed: liveOutcome.analysisToolsUsed,
2834
+ });
2835
+ this.printAgentRunSummary(this.lastAgentRunOutcome, failedEval, 0);
2813
2836
  }
2814
2837
  return true;
2815
2838
  }
@@ -3106,7 +3129,8 @@ export class ChatCommand {
3106
3129
  * - User can answer "What do I type next?" without reading 200 scrollback lines.
3107
3130
  * - Never leave the spinners (`⟳`, `–`) ambiguous when the prompt returns.
3108
3131
  */
3109
- printAgentRunSummary(outcome, executorSucceeded, changedFileCount) {
3132
+ printAgentRunSummary(outcome, evaluation, changedFileCount) {
3133
+ const executorSucceeded = evaluation.executorSucceeded;
3110
3134
  const bar = chalk.gray('─'.repeat(63));
3111
3135
  const ok = chalk.green('✓');
3112
3136
  const warn = chalk.yellow('⚠');
@@ -3116,15 +3140,9 @@ export class ChatCommand {
3116
3140
  const hasTaskInfo = outcome.tasksTotal > 0 || failedList.length > 0 || unfinishedList.length > 0;
3117
3141
  console.log('');
3118
3142
  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
- }
3143
+ const headlineIcon = evaluation.uiTheme === 'success' ? ok : evaluation.uiTheme === 'warning' ? warn : bad;
3144
+ const headlineText = evaluation.statusHeadline.replace(/^[✓⚠✕]\s*/, '');
3145
+ console.log(`${headlineIcon} ${chalk.bold(headlineText)}${changedFileCount ? chalk.gray(` — ${changedFileCount} file${changedFileCount === 1 ? '' : 's'} changed`) : ''}`);
3128
3146
  if (hasTaskInfo) {
3129
3147
  const succ = outcome.tasksTotal > 0 ? `${outcome.tasksSucceeded}/${outcome.tasksTotal}` : `${outcome.tasksSucceeded}`;
3130
3148
  console.log(chalk.gray(` Tasks completed: ${succ}`));
@@ -3293,12 +3311,19 @@ export class ChatCommand {
3293
3311
  return;
3294
3312
  }
3295
3313
  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);
3314
+ const statusEval = evaluateExecutorSuccess({
3315
+ executorFailed: o.failedTaskIds.length > 0,
3316
+ plannerError: o.plannerError,
3317
+ executorError: o.executorError,
3318
+ tasksTotal: o.tasksTotal,
3319
+ tasksSucceeded: o.tasksSucceeded,
3320
+ changedFileCount: 0,
3321
+ requiresWorkspaceChanges: this.taskRequiresWorkspaceChanges(o.prompt),
3322
+ workspaceHasOutput: o.hasOutput,
3323
+ failedTaskIds: new Set(o.failedTaskIds),
3324
+ analysisToolsUsed: 0,
3325
+ });
3326
+ this.printAgentRunSummary(o, statusEval, 0);
3302
3327
  }
3303
3328
  showContext() {
3304
3329
  if (!this.currentSession) {
@@ -0,0 +1,36 @@
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
+ analysisToolsUsed: number;
16
+ }
17
+ export interface TaskSummaryEvent {
18
+ status?: string;
19
+ task_id?: string;
20
+ id?: string;
21
+ }
22
+ export interface RunEvaluation {
23
+ executorSucceeded: boolean;
24
+ statusHeadline: string;
25
+ uiTheme: 'success' | 'warning' | 'error';
26
+ }
27
+ export declare function createLiveOutcome(): LiveOutcome;
28
+ /** Process executor_complete / task summary events from the V3 SSE stream. */
29
+ export declare function handleTaskEvent(summary: TaskSummaryEvent, liveOutcome: LiveOutcome): void;
30
+ /** Track read/discovery tool usage for analysis-turn accountability. */
31
+ export declare function noteAnalysisToolUse(toolName: string, liveOutcome: LiveOutcome): void;
32
+ /**
33
+ * Compute the real CLI success verdict.
34
+ * When tasks were planned, ALL must succeed — file changes alone never imply victory.
35
+ */
36
+ export declare function evaluateExecutorSuccess(liveOutcome: LiveOutcome): RunEvaluation;
@@ -0,0 +1,123 @@
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
+ analysisToolsUsed: 0,
17
+ };
18
+ }
19
+ /** Process executor_complete / task summary events from the V3 SSE stream. */
20
+ export function handleTaskEvent(summary, liveOutcome) {
21
+ const tid = summary.task_id || summary.id;
22
+ const status = String(summary.status || '').toLowerCase();
23
+ if (status === 'completed' || status === 'success') {
24
+ if (tid) {
25
+ liveOutcome.failedTaskIds.delete(String(tid));
26
+ }
27
+ liveOutcome.tasksSucceeded += 1;
28
+ }
29
+ else if (status === 'failed' || status === 'blocked') {
30
+ if (tid) {
31
+ liveOutcome.failedTaskIds.add(String(tid));
32
+ }
33
+ liveOutcome.executorFailed = true;
34
+ }
35
+ }
36
+ const READ_DISCOVERY_TOOLS = new Set([
37
+ 'read_file',
38
+ 'grep',
39
+ 'list_directory',
40
+ 'glob',
41
+ 'search_project',
42
+ 'list_dir',
43
+ 'dir',
44
+ 'read',
45
+ ]);
46
+ /** Track read/discovery tool usage for analysis-turn accountability. */
47
+ export function noteAnalysisToolUse(toolName, liveOutcome) {
48
+ const normalized = String(toolName || '').trim().toLowerCase();
49
+ if (READ_DISCOVERY_TOOLS.has(normalized)) {
50
+ liveOutcome.analysisToolsUsed += 1;
51
+ }
52
+ }
53
+ /**
54
+ * Compute the real CLI success verdict.
55
+ * When tasks were planned, ALL must succeed — file changes alone never imply victory.
56
+ */
57
+ export function evaluateExecutorSuccess(liveOutcome) {
58
+ const hasFatalError = liveOutcome.executorFailed
59
+ || Boolean(liveOutcome.plannerError)
60
+ || Boolean(liveOutcome.executorError);
61
+ if (hasFatalError && liveOutcome.tasksTotal === 0) {
62
+ return {
63
+ executorSucceeded: false,
64
+ statusHeadline: '✕ Execution Error',
65
+ uiTheme: 'error',
66
+ };
67
+ }
68
+ if (liveOutcome.tasksTotal > 0) {
69
+ const allTasksPassed = liveOutcome.tasksSucceeded === liveOutcome.tasksTotal;
70
+ const partialTasksPassed = liveOutcome.tasksSucceeded > 0;
71
+ const failedCount = liveOutcome.failedTaskIds.size;
72
+ if (allTasksPassed && !hasFatalError) {
73
+ return {
74
+ executorSucceeded: true,
75
+ statusHeadline: `✓ Agent run finished (${liveOutcome.tasksSucceeded}/${liveOutcome.tasksTotal} tasks completed)`,
76
+ uiTheme: 'success',
77
+ };
78
+ }
79
+ if (partialTasksPassed) {
80
+ return {
81
+ executorSucceeded: false,
82
+ statusHeadline: `⚠ Warning: Partial Run (${liveOutcome.tasksSucceeded}/${liveOutcome.tasksTotal} tasks completed${failedCount ? `, ${failedCount} failed` : ''})`,
83
+ uiTheme: 'warning',
84
+ };
85
+ }
86
+ return {
87
+ executorSucceeded: false,
88
+ statusHeadline: `✕ Failed: 0/${liveOutcome.tasksTotal} tasks completed${liveOutcome.changedFileCount > 0 ? ' (stub writes or stalls detected)' : ''}`,
89
+ uiTheme: 'error',
90
+ };
91
+ }
92
+ // Analysis / verification turns with no planned task graph
93
+ if (!liveOutcome.requiresWorkspaceChanges) {
94
+ const analysisOk = liveOutcome.analysisToolsUsed > 0 || liveOutcome.workspaceHasOutput;
95
+ if (!analysisOk && hasFatalError) {
96
+ return {
97
+ executorSucceeded: false,
98
+ statusHeadline: '✕ Analysis failed — no workspace evidence gathered',
99
+ uiTheme: 'error',
100
+ };
101
+ }
102
+ if (!analysisOk) {
103
+ return {
104
+ executorSucceeded: false,
105
+ statusHeadline: '⚠ Warning: No read tools confirmed — answer may be incomplete',
106
+ uiTheme: 'warning',
107
+ };
108
+ }
109
+ return {
110
+ executorSucceeded: true,
111
+ statusHeadline: '✓ Analysis completed',
112
+ uiTheme: 'success',
113
+ };
114
+ }
115
+ const legacySuccess = liveOutcome.workspaceHasOutput && liveOutcome.changedFileCount > 0;
116
+ return {
117
+ executorSucceeded: legacySuccess && !hasFatalError,
118
+ statusHeadline: legacySuccess && !hasFatalError
119
+ ? '✓ Agent run finished'
120
+ : '✕ No validated workspace output',
121
+ uiTheme: legacySuccess && !hasFatalError ? 'success' : 'error',
122
+ };
123
+ }
@@ -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.0",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",