vigthoria-cli 1.11.29 → 1.11.36

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.
@@ -54,6 +54,7 @@ export declare class ChatCommand {
54
54
  private v3SeenToolResults;
55
55
  private v3StartSeen;
56
56
  private lastAgentRoute;
57
+ private pendingGoaContext;
57
58
  private lastAgentRunOutcome;
58
59
  private isJwtExpirationError;
59
60
  private isNetworkError;
@@ -116,6 +117,8 @@ export declare class ChatCommand {
116
117
  private v3IterationCount;
117
118
  private v3ToolCallCount;
118
119
  private v3LastActivity;
120
+ private v3IdleWatchInterval;
121
+ private v3IdleNoticeShown;
119
122
  private v3StreamingStarted;
120
123
  /**
121
124
  * Strip server-internal path prefixes from tool output strings.
@@ -123,6 +126,8 @@ export declare class ChatCommand {
123
126
  */
124
127
  private sanitizeServerPath;
125
128
  private stripHiddenThoughtBlocks;
129
+ private startV3IdleWatch;
130
+ private stopV3IdleWatch;
126
131
  private describeV3AgentTool;
127
132
  private terminalColumns;
128
133
  private fitTerminalText;
@@ -17,9 +17,11 @@ import { runAgentSessionMenu, shouldShowAgentSessionMenu } from './agent-session
17
17
  import { createLiveOutcome, evaluateExecutorSuccess, handleTaskEvent, isSubstantiveAgentAnswer, isToolEvidenceStubAnswer, normalizeAgentAnswerContent, noteAnalysisToolUse, } from '../utils/agentRunOutcome.js';
18
18
  import { looksLikeMarkdownReport, renderMarkdownToTerminal, summarizeMarkdownReport, } from '../utils/terminalMarkdown.js';
19
19
  import { emitDeckEvent, isDeckModeEnabled } from '../utils/deckEvents.js';
20
+ import { formatGoaSystemGrounding, splitGoaContextFromInput, } from '../utils/goaEvents.js';
20
21
  import { inferAgentTaskType as sharedInferAgentTaskType, inferAgentTaskTypeWithContext, isTrivialHtmlPageRequest, resolvePlannerAgentTimeoutMs, resolveWorkflowType, shouldSkipAnalysisRescue, taskRequiresWorkspaceChanges as promptRequiresWorkspaceChanges, taskRequiresWorkspaceChangesWithContext, buildExecutionHints, isAgentContinuePrompt, isAgentRetryPrompt, isBuiltContinuePrompt, isBuiltRetryPrompt, isBuiltWriteConfirmationPrompt, isConfirmationFollowUp, isWritePermissionGrant, } from '../utils/requestIntent.js';
21
22
  import { resolveAgentRoute } from '../utils/agentRoute.js';
22
23
  import { runTemplateInstantPath } from '../utils/templateInstantPath.js';
24
+ import { isV3StreamKeepaliveEvent } from '../utils/v3-stream-events.js';
23
25
  const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
24
26
  const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
25
27
  if (!rawValue) {
@@ -76,6 +78,7 @@ export class ChatCommand {
76
78
  v3SeenToolResults = new Set();
77
79
  v3StartSeen = false;
78
80
  lastAgentRoute = null;
81
+ pendingGoaContext = null;
79
82
  // Last completed Agent run — used by /retry, /continue, and the final summary block.
80
83
  lastAgentRunOutcome = null;
81
84
  isJwtExpirationError(error) {
@@ -781,6 +784,8 @@ export class ChatCommand {
781
784
  v3IterationCount = 0;
782
785
  v3ToolCallCount = 0;
783
786
  v3LastActivity = Date.now();
787
+ v3IdleWatchInterval = null;
788
+ v3IdleNoticeShown = false;
784
789
  v3StreamingStarted = false;
785
790
  /**
786
791
  * Strip server-internal path prefixes from tool output strings.
@@ -796,10 +801,41 @@ export class ChatCommand {
796
801
  return text;
797
802
  return text
798
803
  .replace(/<\|mask_start\|>[\s\S]*?<\|mask_end\|>/g, '')
799
- .replace(/<think>[\s\S]*?<\/think>/gi, '')
804
+ .replace(/<think>[\s\S]*?<\/redacted_thinking>/gi, '')
805
+ .replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
806
+ .replace(/<\/redacted_thinking>/gi, '')
807
+ .replace(/<\/thinking>/gi, '')
800
808
  .replace(/<\|(?:mask_start|mask_end)\|>/g, '')
801
809
  .trim();
802
810
  }
811
+ startV3IdleWatch(spinner) {
812
+ this.stopV3IdleWatch();
813
+ this.v3IdleNoticeShown = false;
814
+ if (this.jsonOutput || !spinner) {
815
+ return;
816
+ }
817
+ this.v3IdleWatchInterval = setInterval(() => {
818
+ const idleMs = Date.now() - this.v3LastActivity;
819
+ if (idleMs < 15_000 || this.v3IdleNoticeShown) {
820
+ return;
821
+ }
822
+ this.v3IdleNoticeShown = true;
823
+ if (spinner.isSpinning) {
824
+ spinner.stop();
825
+ }
826
+ const seconds = Math.round(idleMs / 1000);
827
+ process.stderr.write(chalk.yellow(` [Wait] Model still working (${seconds}s) — large context or server retry may take up to 90s...\n`));
828
+ spinner.start();
829
+ spinner.text = 'Waiting for model response...';
830
+ }, 5_000);
831
+ }
832
+ stopV3IdleWatch() {
833
+ if (this.v3IdleWatchInterval) {
834
+ clearInterval(this.v3IdleWatchInterval);
835
+ this.v3IdleWatchInterval = null;
836
+ }
837
+ this.v3IdleNoticeShown = false;
838
+ }
803
839
  describeV3AgentTool(toolName) {
804
840
  const normalized = String(toolName || '').toLowerCase();
805
841
  if (/read|grep|search|list|find|glob/.test(normalized)) {
@@ -934,6 +970,7 @@ export class ChatCommand {
934
970
  return '';
935
971
  output = output.replace(/<\/think>/gi, '</thinking>');
936
972
  output = output.replace(/<think>/gi, '<thinking>');
973
+ output = output.replace(/<\/redacted_thinking>/gi, '</thinking>');
937
974
  if (this.v3SuppressThinkingStream) {
938
975
  const closeIdx = output.search(/<\/thinking>/i);
939
976
  if (closeIdx < 0) {
@@ -979,6 +1016,9 @@ export class ChatCommand {
979
1016
  return;
980
1017
  }
981
1018
  this.v3LastActivity = Date.now();
1019
+ if (isV3StreamKeepaliveEvent(event)) {
1020
+ return;
1021
+ }
982
1022
  if (event.type === 'tool_call') {
983
1023
  const eventKey = this.toolEventKey(event);
984
1024
  if (this.v3SeenToolCalls.has(eventKey)) {
@@ -1091,6 +1131,13 @@ export class ChatCommand {
1091
1131
  : (event.content || '');
1092
1132
  if (text) {
1093
1133
  this.v3LastActivity = Date.now();
1134
+ if (event.type === 'message' && /^\[Context\]/i.test(String(text).trim())) {
1135
+ if (spinner.isSpinning)
1136
+ spinner.stop();
1137
+ process.stderr.write(chalk.cyan(` ${this.fitTerminalText(String(text).trim(), 8)}\n`));
1138
+ spinner.text = 'Analyzing...';
1139
+ return;
1140
+ }
1094
1141
  this.writeV3StreamText(spinner, text);
1095
1142
  }
1096
1143
  else {
@@ -1138,11 +1185,6 @@ export class ChatCommand {
1138
1185
  const quality = this.sanitizeServerPath(plan.quality_profile || '');
1139
1186
  const status = typeof plan.status === 'string' ? plan.status : '';
1140
1187
  const summary = typeof plan.summary === 'string' ? this.sanitizeServerPath(plan.summary) : '';
1141
- if (status === 'planning') {
1142
- const elapsed = Number.isFinite(Number(plan.elapsed_seconds)) ? Number(plan.elapsed_seconds) : 0;
1143
- spinner.text = elapsed > 0 ? `Planning... (${elapsed}s elapsed)` : 'Planning...';
1144
- return;
1145
- }
1146
1188
  if (spinner.isSpinning)
1147
1189
  spinner.stop();
1148
1190
  process.stdout.write(chalk.cyan(` [Plan] `) + this.fitTerminalText(`Task: ${planKind || 'analyzing'}${quality ? ` (${quality})` : ''}`, 9) + '\n');
@@ -1721,6 +1763,10 @@ export class ChatCommand {
1721
1763
  return false;
1722
1764
  }
1723
1765
  async handleDirectPrompt(prompt) {
1766
+ const { goaContext, userPrompt } = splitGoaContextFromInput(prompt);
1767
+ if (goaContext)
1768
+ this.pendingGoaContext = goaContext;
1769
+ const effectivePrompt = userPrompt || prompt;
1724
1770
  // Suppress all setup banners in direct-prompt mode so only the final
1725
1771
  // answer reaches stdout. Interactive (REPL) mode still shows them.
1726
1772
  if (!this.jsonOutput) {
@@ -1733,18 +1779,18 @@ export class ChatCommand {
1733
1779
  console.log();
1734
1780
  }
1735
1781
  if (this.workflowTarget) {
1736
- await this.runWorkflowTargetPrompt(prompt);
1782
+ await this.runWorkflowTargetPrompt(effectivePrompt);
1737
1783
  return;
1738
1784
  }
1739
1785
  // Smart routing: for agent mode, determine if prompt needs tool access
1740
1786
  if (this.agentMode) {
1741
- if (this.isSimpleDirectPrompt(prompt)) {
1787
+ if (this.isSimpleDirectPrompt(effectivePrompt)) {
1742
1788
  // Simple prompt: downgrade to plain chat model, no agent system prompt
1743
1789
  this.currentModel = this.getDefaultChatModel();
1744
- await this.runSimplePrompt(prompt);
1790
+ await this.runSimplePrompt(effectivePrompt);
1745
1791
  }
1746
1792
  else {
1747
- await this.runAgentTurn(prompt);
1793
+ await this.runAgentTurn(effectivePrompt);
1748
1794
  }
1749
1795
  return;
1750
1796
  }
@@ -1753,10 +1799,10 @@ export class ChatCommand {
1753
1799
  this.logger.error(this.operatorAccessMessage());
1754
1800
  return;
1755
1801
  }
1756
- await this.runOperatorTurn(prompt);
1802
+ await this.runOperatorTurn(effectivePrompt);
1757
1803
  return;
1758
1804
  }
1759
- await this.runSimplePrompt(prompt);
1805
+ await this.runSimplePrompt(effectivePrompt);
1760
1806
  }
1761
1807
  formatWorkflowTargetResult(result) {
1762
1808
  if (typeof result === 'string') {
@@ -2187,7 +2233,19 @@ export class ChatCommand {
2187
2233
  if (!this.tools) {
2188
2234
  throw new Error('Agent tools are not initialized.');
2189
2235
  }
2190
- const resolvedPrompt = this.resolveAgentTurnPrompt(prompt);
2236
+ const { goaContext, userPrompt } = splitGoaContextFromInput(prompt);
2237
+ if (goaContext) {
2238
+ this.pendingGoaContext = goaContext;
2239
+ }
2240
+ const effectivePrompt = userPrompt || prompt;
2241
+ if (this.pendingGoaContext) {
2242
+ this.messages.push({
2243
+ role: 'system',
2244
+ content: formatGoaSystemGrounding(this.pendingGoaContext),
2245
+ });
2246
+ this.pendingGoaContext = null;
2247
+ }
2248
+ const resolvedPrompt = this.resolveAgentTurnPrompt(effectivePrompt);
2191
2249
  this.lastAgentRoute = await resolveAgentRoute(this.api, resolvedPrompt);
2192
2250
  if (!this.jsonOutput) {
2193
2251
  const r = this.lastAgentRoute;
@@ -2942,6 +3000,7 @@ export class ChatCommand {
2942
3000
  spinner: 'clock',
2943
3001
  isSilent: true,
2944
3002
  }).start();
3003
+ this.startV3IdleWatch(spinner);
2945
3004
  // Live run telemetry, used both for the final summary block and for /retry, /continue.
2946
3005
  const liveOutcome = {
2947
3006
  ...createLiveOutcome(),
@@ -3003,6 +3062,21 @@ export class ChatCommand {
3003
3062
  fastPath: this.lastAgentRoute?.fastPath,
3004
3063
  priorPrompt: priorPrompt || null,
3005
3064
  });
3065
+ const priorOutcome = this.lastAgentRunOutcome;
3066
+ const resumeTaskIds = priorOutcome
3067
+ ? [...new Set([...(priorOutcome.failedTaskIds || []), ...(priorOutcome.unfinishedTaskIds || [])])]
3068
+ : [];
3069
+ const shouldResumePlan = isAgentContinuePrompt(contextualPrompt)
3070
+ || isAgentRetryPrompt(contextualPrompt)
3071
+ || isBuiltContinuePrompt(contextualPrompt)
3072
+ || isBuiltRetryPrompt(contextualPrompt);
3073
+ const agentExecutionHints = shouldResumePlan
3074
+ ? {
3075
+ ...executionHints,
3076
+ resume_existing_plan: true,
3077
+ ...(resumeTaskIds.length > 0 ? { remaining_task_ids: resumeTaskIds } : {}),
3078
+ }
3079
+ : executionHints;
3006
3080
  const workspaceContext = {
3007
3081
  workspacePath: workspacePath,
3008
3082
  projectPath: workspacePath,
@@ -3039,7 +3113,7 @@ export class ChatCommand {
3039
3113
  ...workspaceContext,
3040
3114
  agentTaskType,
3041
3115
  workflowType,
3042
- executionHints,
3116
+ executionHints: agentExecutionHints,
3043
3117
  fastPath: this.lastAgentRoute?.fastPath,
3044
3118
  agentRoute: this.lastAgentRoute,
3045
3119
  executionSurface: 'cli',
@@ -3057,20 +3131,15 @@ export class ChatCommand {
3057
3131
  history: this.getMessagesForModel(),
3058
3132
  ...runtimeContext,
3059
3133
  onStreamEvent: (event) => {
3134
+ if (isV3StreamKeepaliveEvent(event)) {
3135
+ return;
3136
+ }
3060
3137
  if (event.type === 'plan') {
3061
- const plan = event?.plan || {};
3062
- const isPlanningKeepalive = plan.status === 'planning' && !Array.isArray(plan.tasks);
3063
- if (isPlanningKeepalive) {
3064
- const elapsed = Number.isFinite(Number(plan.elapsed_seconds)) ? Number(plan.elapsed_seconds) : 0;
3065
- taskDisplay.start(0, elapsed > 0 ? `planning (${elapsed}s)` : 'planning...');
3066
- }
3067
- else {
3068
- taskDisplay.complete(0);
3069
- const tasks = plan?.tasks;
3070
- if (Array.isArray(tasks) && tasks.length > 0) {
3071
- taskDisplay.start(1);
3072
- liveOutcome.tasksTotal = tasks.length;
3073
- }
3138
+ taskDisplay.complete(0);
3139
+ const tasks = event?.plan?.tasks;
3140
+ if (Array.isArray(tasks) && tasks.length > 0) {
3141
+ taskDisplay.start(1);
3142
+ liveOutcome.tasksTotal = tasks.length;
3074
3143
  }
3075
3144
  }
3076
3145
  else if (event.type === 'executor_start') {
@@ -3113,10 +3182,19 @@ export class ChatCommand {
3113
3182
  }
3114
3183
  else if (event.type === 'error') {
3115
3184
  const msg = typeof event.message === 'string' ? event.message : '';
3116
- if (/plan|planner|dependency graph/i.test(msg)) {
3185
+ parsePlannerSummary(msg);
3186
+ const plannerSummary = /Planner-Executor completed/i.test(msg);
3187
+ const allTasksSucceeded = liveOutcome.tasksTotal > 0
3188
+ && liveOutcome.tasksSucceeded >= liveOutcome.tasksTotal
3189
+ && liveOutcome.failedTaskIds.size === 0;
3190
+ if (plannerSummary) {
3191
+ if (!allTasksSucceeded && !liveOutcome.plannerError) {
3192
+ liveOutcome.plannerError = msg;
3193
+ }
3194
+ }
3195
+ else if (/plan|planner|dependency graph/i.test(msg)) {
3117
3196
  if (!liveOutcome.plannerError)
3118
3197
  liveOutcome.plannerError = msg;
3119
- parsePlannerSummary(msg);
3120
3198
  }
3121
3199
  else if (/executor|task failed|iteration/i.test(msg)) {
3122
3200
  if (!liveOutcome.executorError)
@@ -3189,8 +3267,8 @@ export class ChatCommand {
3189
3267
  console.log(chalk.yellow(`Template Service preview gate: failed${previewGate.error ? ` - ${previewGate.error}` : ''}`));
3190
3268
  }
3191
3269
  }
3192
- // Show change summary for files touched by the agent
3193
- if (!this.jsonOutput && !this.directPromptMode && response.changedFiles) {
3270
+ // Show change summary only when the agent actually wrote files this run
3271
+ if (!this.jsonOutput && !this.directPromptMode && response.changedFiles && requiresWorkspaceChanges) {
3194
3272
  const fileCount = changedFileCount;
3195
3273
  if (fileCount > 0) {
3196
3274
  console.log(chalk.gray(`\nFiles changed: ${fileCount}`));
@@ -3384,10 +3462,12 @@ export class ChatCommand {
3384
3462
  }, null, 2));
3385
3463
  }
3386
3464
  this.messages.push({ role: 'assistant', content: finalAnswer || runEvaluation.statusHeadline });
3465
+ this.stopV3IdleWatch();
3387
3466
  watcher?.stop();
3388
3467
  return true;
3389
3468
  }
3390
3469
  catch (error) {
3470
+ this.stopV3IdleWatch();
3391
3471
  watcher?.stop();
3392
3472
  if (!this.api.hasAgentWorkspaceOutput(workspaceContext)) {
3393
3473
  const recovered = await this.tryRecoverV3ServiceAndRetry(executionPrompt, prompt, workspaceContext, routingPolicy, spinner, error);
@@ -3601,7 +3681,12 @@ export class ChatCommand {
3601
3681
  return lines.join('\n');
3602
3682
  };
3603
3683
  while (true) {
3604
- const input = await readMultiLineInput();
3684
+ const rawInput = await readMultiLineInput();
3685
+ const { goaContext, userPrompt: strippedInput } = splitGoaContextFromInput(rawInput);
3686
+ if (goaContext) {
3687
+ this.pendingGoaContext = goaContext;
3688
+ }
3689
+ const input = strippedInput || rawInput;
3605
3690
  const trimmed = input.trim();
3606
3691
  if (!trimmed) {
3607
3692
  continue;
@@ -34,6 +34,10 @@ export interface RunEvaluation {
34
34
  }
35
35
  export declare function createLiveOutcome(): LiveOutcome;
36
36
  export declare function isExecutorTimeoutFailure(liveOutcome: LiveOutcome): boolean;
37
+ export declare function isInferenceDegradedFailure(liveOutcome: LiveOutcome): boolean;
38
+ /** Strip model thinking blocks and stray closing tags from user-visible answer text. */
39
+ export declare function stripAgentThinkingTags(text: string): string;
40
+ export declare function isGenericFallbackAnswer(text: string): boolean;
37
41
  /** Tool-trace stubs like `[read_file] "index.html"` or `<read_file>…</read_file>` — not a user-facing answer. */
38
42
  export declare function isToolEvidenceStubAnswer(text: string): boolean;
39
43
  /** True when text looks like a real answer, not an executor/system placeholder. */
@@ -30,10 +30,21 @@ const LIST_ONLY_DISCOVERY_TOOLS = new Set([
30
30
  'dir',
31
31
  ]);
32
32
  const GENERIC_SUMMARY_RE = /^(completed the requested analysis|reviewed the workspace without writing changes|task completed|done|finished|analysis completed)([.\s]|$)/i;
33
+ /** Planner fallback / degraded inference — generic greeting unrelated to the user question. */
34
+ const GENERIC_FALLBACK_GREETING_RE = /^(?:hello!?|hi!?|hey!?)\s*(?:how can i assist|how can i help|what would you like|whether you'?re looking to build)/i;
35
+ /** System-prompt fragments leaked when the model falls back without real context. */
36
+ const SYSTEM_PROMPT_LEAK_RE = /avoid global namespace pollution unless explicitly needed|minify assets only when explicitly requested|include proper accessibility attributes \(aria-label/i;
33
37
  export function isExecutorTimeoutFailure(liveOutcome) {
34
38
  const errorText = String(liveOutcome.executorError || '').trim();
35
39
  return /executor llm call timed out|fast fallback also failed|executor model timed out/i.test(errorText);
36
40
  }
41
+ export function isInferenceDegradedFailure(liveOutcome) {
42
+ const answer = stripAgentThinkingTags(liveOutcome.answerContent || '');
43
+ if (!answer)
44
+ return false;
45
+ return isGenericFallbackAnswer(answer)
46
+ && (liveOutcome.analysisToolsUsed > 0 || liveOutcome.requiresWorkspaceChanges);
47
+ }
37
48
  function looksLikeRawV3EventPayload(text) {
38
49
  const trimmed = String(text || '').trim();
39
50
  if (!/^[{[]/.test(trimmed))
@@ -56,9 +67,34 @@ function looksLikeRawV3EventPayload(text) {
56
67
  return false;
57
68
  }
58
69
  }
70
+ /** Strip model thinking blocks and stray closing tags from user-visible answer text. */
71
+ export function stripAgentThinkingTags(text) {
72
+ let output = String(text || '');
73
+ if (!output)
74
+ return '';
75
+ output = output.replace(/<\/think>/gi, '</thinking>');
76
+ output = output.replace(/<think>/gi, '<thinking>');
77
+ output = output.replace(/<\/redacted_thinking>/gi, '</thinking>');
78
+ output = output.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
79
+ output = output.replace(/<thinking>[\s\S]*$/i, '');
80
+ output = output.replace(/<\/thinking>/gi, '');
81
+ return output.trim();
82
+ }
83
+ export function isGenericFallbackAnswer(text) {
84
+ const trimmed = stripAgentThinkingTags(text);
85
+ if (!trimmed)
86
+ return true;
87
+ if (GENERIC_FALLBACK_GREETING_RE.test(trimmed))
88
+ return true;
89
+ if (SYSTEM_PROMPT_LEAK_RE.test(trimmed))
90
+ return true;
91
+ if (/^now,?\s+i'?m ready to assist/i.test(trimmed) && trimmed.length < 400)
92
+ return true;
93
+ return false;
94
+ }
59
95
  /** Tool-trace stubs like `[read_file] "index.html"` or `<read_file>…</read_file>` — not a user-facing answer. */
60
96
  export function isToolEvidenceStubAnswer(text) {
61
- const trimmed = String(text || '').trim();
97
+ const trimmed = stripAgentThinkingTags(text);
62
98
  if (!trimmed)
63
99
  return true;
64
100
  if (looksLikeRawV3EventPayload(trimmed))
@@ -89,13 +125,15 @@ export function isToolEvidenceStubAnswer(text) {
89
125
  }
90
126
  /** True when text looks like a real answer, not an executor/system placeholder. */
91
127
  export function isSubstantiveAgentAnswer(text) {
92
- const trimmed = String(text || '').trim();
128
+ const trimmed = stripAgentThinkingTags(text);
93
129
  if (trimmed.length < 80)
94
130
  return false;
95
131
  if (EXECUTOR_PLACEHOLDER_RE.test(trimmed))
96
132
  return false;
97
133
  if (/^v3 agent workflow completed\.?$/i.test(trimmed))
98
134
  return false;
135
+ if (isGenericFallbackAnswer(trimmed))
136
+ return false;
99
137
  if (isToolEvidenceStubAnswer(trimmed))
100
138
  return false;
101
139
  return true;
@@ -103,7 +141,7 @@ export function isSubstantiveAgentAnswer(text) {
103
141
  /** Pick the first substantive answer from response body and/or streamed text. */
104
142
  export function normalizeAgentAnswerContent(...sources) {
105
143
  for (const source of sources) {
106
- const text = String(source || '').trim();
144
+ const text = stripAgentThinkingTags(String(source || ''));
107
145
  if (isSubstantiveAgentAnswer(text))
108
146
  return text;
109
147
  }
@@ -206,6 +244,23 @@ export function evaluateExecutorSuccess(liveOutcome) {
206
244
  const allTasksPassed = liveOutcome.tasksSucceeded === liveOutcome.tasksTotal;
207
245
  const partialTasksPassed = liveOutcome.tasksSucceeded > 0;
208
246
  const failedCount = liveOutcome.failedTaskIds.size;
247
+ if (allTasksPassed && failedCount === 0 && !liveOutcome.streamAborted) {
248
+ const qualityOnlyPlannerNote = Boolean(liveOutcome.plannerError)
249
+ && /Planner-Executor completed:\s*\d+\s*\/\s*\d+\s+tasks succeeded/i.test(String(liveOutcome.plannerError));
250
+ const recoveredAfterTransientErrors = liveOutcome.executorFailed
251
+ || Boolean(liveOutcome.executorError)
252
+ || qualityOnlyPlannerNote;
253
+ if (!liveOutcome.plannerError || qualityOnlyPlannerNote || recoveredAfterTransientErrors) {
254
+ const qualityNote = qualityOnlyPlannerNote || /Quality (?:audit|blockers)/i.test(String(liveOutcome.plannerError || ''));
255
+ return {
256
+ executorSucceeded: true,
257
+ statusHeadline: qualityNote
258
+ ? `✓ Tasks complete (${liveOutcome.tasksSucceeded}/${liveOutcome.tasksTotal}) — quality audit below target`
259
+ : `✓ Agent run finished (${liveOutcome.tasksSucceeded}/${liveOutcome.tasksTotal} tasks completed)`,
260
+ uiTheme: qualityNote ? 'warning' : 'success',
261
+ };
262
+ }
263
+ }
209
264
  if (allTasksPassed && !hasFatalError) {
210
265
  return {
211
266
  executorSucceeded: true,
@@ -235,6 +290,13 @@ export function evaluateExecutorSuccess(liveOutcome) {
235
290
  || (liveOutcome.analysisToolsUsed >= 4
236
291
  && liveOutcome.analysisReadToolsUsed >= 3
237
292
  && answerText.length >= 700);
293
+ if (isInferenceDegradedFailure(liveOutcome)) {
294
+ return {
295
+ executorSucceeded: false,
296
+ statusHeadline: '✕ Model inference degraded — generic fallback answer, not your question — try /retry',
297
+ uiTheme: 'error',
298
+ };
299
+ }
238
300
  if (hasFatalError && !hasSubstantiveAnswer) {
239
301
  return {
240
302
  executorSucceeded: false,
@@ -276,6 +338,13 @@ export function evaluateExecutorSuccess(liveOutcome) {
276
338
  uiTheme: 'success',
277
339
  };
278
340
  }
341
+ if (isInferenceDegradedFailure(liveOutcome)) {
342
+ return {
343
+ executorSucceeded: false,
344
+ statusHeadline: '✕ Model inference degraded — server could not run the code model — try /retry in a minute',
345
+ uiTheme: 'error',
346
+ };
347
+ }
279
348
  const legacySuccess = liveOutcome.workspaceHasOutput && liveOutcome.changedFileCount > 0;
280
349
  return {
281
350
  executorSucceeded: legacySuccess && !hasFatalError,
@@ -272,6 +272,7 @@ export declare class APIClient {
272
272
  */
273
273
  isAnalysisOnlyTask(message?: string, context?: Record<string, any>): boolean;
274
274
  private normalizeWorkspaceRelativePath;
275
+ private collectAgentStateSyncPaths;
275
276
  private listFrontendWorkspaceFiles;
276
277
  private chooseFrontendPreviewEntry;
277
278
  private extractLinkedFrontendAssets;
@@ -322,8 +323,11 @@ export declare class APIClient {
322
323
  executionOptions?: Record<string, unknown>;
323
324
  }): Promise<VigFlowExecutionResult>;
324
325
  getVigFlowExecutionStatus(executionId: string): Promise<VigFlowExecutionStatus>;
325
- /** Maximum serialized context length accepted by the V3 server. */
326
+ /** Legacy cap prefer resolveV3ContextCharLimit() for token-aligned budgets. */
326
327
  private static readonly V3_CONTEXT_CHAR_LIMIT;
328
+ private static readonly HYDRATION_PRIORITY_BASENAMES;
329
+ /** Keep critical workspace files for server hydration when context must shrink. */
330
+ private compactWorkspaceFilesToBudget;
327
331
  buildV3AgentContext(context?: Record<string, any>): string;
328
332
  /**
329
333
  * Compact a V3 context payload so the serialized JSON stays under
@@ -335,6 +339,9 @@ export declare class APIClient {
335
339
  * 5. Drop readmeExcerpt
336
340
  */
337
341
  private compactV3Context;
342
+ /** Normalize common Unix-only commands before Windows cmd/powershell execution. */
343
+ private normalizeWindowsRunCommand;
344
+ private resolveRunCommandCwd;
338
345
  /** Remove duplicate functionIndex entries that poison model context after compaction. */
339
346
  private dedupeBrainPayload;
340
347
  buildMinimalV3AgentContext(context?: Record<string, any>): string;
package/dist/utils/api.js CHANGED
@@ -14,6 +14,8 @@ import { getChangedFiles } from './workspace-cache.js';
14
14
  import { isSubstantiveAgentAnswer, isToolEvidenceStubAnswer } from './agentRunOutcome.js';
15
15
  import { joinV3WorkspacePath, normalizeV3WorkspaceRelativePath } from './v3-workspace-path.js';
16
16
  import { buildExecutionHints, isPlannerBuildTask, resolvePlannerAgentTimeoutMs } from './requestIntent.js';
17
+ import { isV3StreamKeepaliveEvent } from './v3-stream-events.js';
18
+ import { resolveV3ContextCharLimit, WORKSPACE_FILE_CHAR_CAP, } from './contextBudget.js';
17
19
  export const VIGTHORIA_HUB_CREDITS_URL = 'https://hub.vigthoria.io/credits';
18
20
  export const VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE = 'Vigthoria Server is temporarily not available. Please try again later. If the issue persists, please contact support.';
19
21
  export class CLIError extends Error {
@@ -732,6 +734,30 @@ export class APIClient {
732
734
  normalizeWorkspaceRelativePath(filePath) {
733
735
  return String(filePath || '').trim().replace(/\\/g, '/').replace(/^\.\//, '');
734
736
  }
737
+ collectAgentStateSyncPaths(rootPath) {
738
+ const paths = [];
739
+ const progressPath = path.join(rootPath, '.vigthoria', 'agent-state', 'progress.json');
740
+ if (!fs.existsSync(progressPath)) {
741
+ return paths;
742
+ }
743
+ paths.push('.vigthoria/agent-state/progress.json');
744
+ const tasksDir = path.join(rootPath, '.vigthoria', 'agent-state', 'tasks');
745
+ if (!fs.existsSync(tasksDir)) {
746
+ return paths;
747
+ }
748
+ try {
749
+ for (const entry of fs.readdirSync(tasksDir, { withFileTypes: true })) {
750
+ if (!entry.isFile() || !entry.name.endsWith('.json')) {
751
+ continue;
752
+ }
753
+ paths.push(`.vigthoria/agent-state/tasks/${entry.name}`);
754
+ }
755
+ }
756
+ catch {
757
+ // Ignore unreadable task snapshots.
758
+ }
759
+ return paths;
760
+ }
735
761
  listFrontendWorkspaceFiles(rootPath) {
736
762
  const results = [];
737
763
  const stack = [rootPath];
@@ -1424,8 +1450,41 @@ export class APIClient {
1424
1450
  return payload.execution;
1425
1451
  });
1426
1452
  }
1427
- /** Maximum serialized context length accepted by the V3 server. */
1428
- static V3_CONTEXT_CHAR_LIMIT = 95_000;
1453
+ /** Legacy cap prefer resolveV3ContextCharLimit() for token-aligned budgets. */
1454
+ static V3_CONTEXT_CHAR_LIMIT = 100_000;
1455
+ static HYDRATION_PRIORITY_BASENAMES = new Set([
1456
+ 'package.json', 'index.html', 'game.js', 'main.js', 'app.js', 'script.js',
1457
+ 'styles.css', 'style.css', 'README.md', 'manifest.json',
1458
+ ]);
1459
+ /** Keep critical workspace files for server hydration when context must shrink. */
1460
+ compactWorkspaceFilesToBudget(files, budgetChars) {
1461
+ const entries = Object.entries(files);
1462
+ const rank = (filePath) => {
1463
+ const normalized = filePath.replace(/\\/g, '/');
1464
+ const base = normalized.split('/').pop() || '';
1465
+ if (APIClient.HYDRATION_PRIORITY_BASENAMES.has(base))
1466
+ return 0;
1467
+ if (/^\.vigthoria\/agent-state\//i.test(normalized))
1468
+ return 1;
1469
+ if (/\.(html?|js|mjs|cjs|css|json|ts|tsx|jsx)$/i.test(base))
1470
+ return 2;
1471
+ return 3;
1472
+ };
1473
+ entries.sort((a, b) => rank(a[0]) - rank(b[0]) || a[0].localeCompare(b[0]));
1474
+ const trimmed = {};
1475
+ let used = 2;
1476
+ for (const [filePath, content] of entries) {
1477
+ const maxContentLen = rank(filePath) <= 1 ? content.length : Math.min(content.length, WORKSPACE_FILE_CHAR_CAP);
1478
+ const clipped = content.length > maxContentLen ? `${content.slice(0, maxContentLen)}\n/* … truncated for remote context … */` : content;
1479
+ const entryLen = JSON.stringify(filePath).length + 1 + JSON.stringify(clipped).length + 1;
1480
+ if (used + entryLen > budgetChars) {
1481
+ continue;
1482
+ }
1483
+ trimmed[filePath] = clipped;
1484
+ used += entryLen;
1485
+ }
1486
+ return trimmed;
1487
+ }
1429
1488
  buildV3AgentContext(context = {}) {
1430
1489
  const resolvedContext = this.ensureExecutionContext(context);
1431
1490
  const targetPath = resolvedContext.targetPath || resolvedContext.projectPath || resolvedContext.workspacePath || resolvedContext.projectRoot || process.cwd();
@@ -1533,7 +1592,16 @@ export class APIClient {
1533
1592
  * 5. Drop readmeExcerpt
1534
1593
  */
1535
1594
  compactV3Context(payload) {
1536
- const LIMIT = APIClient.V3_CONTEXT_CHAR_LIMIT;
1595
+ const LIMIT = resolveV3ContextCharLimit();
1596
+ const phases = [];
1597
+ const compactionMeta = {
1598
+ applied: false,
1599
+ beforeChars: 0,
1600
+ afterChars: 0,
1601
+ charLimit: LIMIT,
1602
+ estimatedInputBudget: Math.floor(LIMIT / 4),
1603
+ phases,
1604
+ };
1537
1605
  if (payload.vigthoriaBrain) {
1538
1606
  payload.vigthoriaBrain = this.dedupeBrainPayload(payload.vigthoriaBrain);
1539
1607
  }
@@ -1541,80 +1609,155 @@ export class APIClient {
1541
1609
  payload.vigthoria_brain = payload.vigthoriaBrain || this.dedupeBrainPayload(payload.vigthoria_brain);
1542
1610
  }
1543
1611
  let json = JSON.stringify(payload);
1544
- if (json.length <= LIMIT)
1612
+ compactionMeta.beforeChars = json.length;
1613
+ if (json.length <= LIMIT) {
1614
+ compactionMeta.afterChars = json.length;
1615
+ payload.contextBudgetMeta = compactionMeta;
1545
1616
  return json;
1617
+ }
1618
+ compactionMeta.applied = true;
1619
+ phases.push('over_limit');
1546
1620
  if (!/^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_QUIET_CONTEXT_COMPACTION || ''))) {
1547
- process.stderr.write(`Workspace context is large - sent a focused summary (${Math.round(LIMIT / 1000)}k limit).\n`);
1621
+ process.stderr.write(`Workspace context is large sending a focused summary (~${Math.round(LIMIT / 1000)}k char budget, token-aligned).\n`);
1548
1622
  }
1549
1623
  if (process.env.DEBUG || process.env.VIGTHORIA_DEBUG) {
1550
1624
  process.stderr.write(`[context] Payload ${json.length} chars exceeds ${LIMIT} limit, compacting...\n`);
1551
1625
  }
1552
- // Phase 1 — shrink workspaceFiles to fit
1553
1626
  const summary = payload.localWorkspaceSummary;
1627
+ const hydrationRequired = payload.workspaceHydrationRequired === true;
1628
+ const finish = () => {
1629
+ compactionMeta.afterChars = json.length;
1630
+ payload.contextBudgetMeta = compactionMeta;
1631
+ return json;
1632
+ };
1633
+ // Phase 1 — truncate history before touching workspace file hydration
1634
+ if (Array.isArray(payload.history) && payload.history.length > 6) {
1635
+ payload.history = payload.history.slice(-6);
1636
+ phases.push('trim_history_6');
1637
+ json = JSON.stringify(payload);
1638
+ if (json.length <= LIMIT)
1639
+ return finish();
1640
+ }
1641
+ // Phase 2 — shrink workspaceFiles (priority order) to fit
1554
1642
  if (summary?.workspaceFiles && typeof summary.workspaceFiles === 'object') {
1555
- const fileEntries = Object.entries(summary.workspaceFiles);
1556
1643
  const overhead = json.length - JSON.stringify(summary.workspaceFiles).length;
1557
- const budget = LIMIT - overhead - 512; // reserve a little headroom
1644
+ const budget = LIMIT - overhead - 1024;
1558
1645
  if (budget > 0) {
1559
- const trimmed = {};
1560
- let used = 2; // {}
1561
- for (const [k, v] of fileEntries) {
1562
- const entryLen = JSON.stringify(k).length + 1 + JSON.stringify(v).length + 1;
1563
- if (used + entryLen > budget)
1564
- break;
1565
- trimmed[k] = v;
1566
- used += entryLen;
1567
- }
1568
- summary.workspaceFiles = trimmed;
1646
+ summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, budget);
1647
+ summary.workspaceFilesCompaction = Object.keys(summary.workspaceFiles).length > 0 ? 'priority-trimmed' : 'empty';
1648
+ }
1649
+ else if (hydrationRequired) {
1650
+ summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, 8_000);
1651
+ summary.workspaceFilesCompaction = 'priority-minimal';
1569
1652
  }
1570
1653
  else {
1571
1654
  delete summary.workspaceFiles;
1572
1655
  }
1573
1656
  json = JSON.stringify(payload);
1574
1657
  if (json.length <= LIMIT)
1575
- return json;
1658
+ return finish();
1576
1659
  }
1577
- // Phase 2 — drop workspaceFiles entirely
1578
- if (summary?.workspaceFiles) {
1660
+ // Phase 3 — drop readme before dropping hydration payload
1661
+ if (summary?.readmeExcerpt) {
1662
+ delete summary.readmeExcerpt;
1663
+ json = JSON.stringify(payload);
1664
+ if (json.length <= LIMIT)
1665
+ return finish();
1666
+ }
1667
+ // Phase 4 — never fully drop workspaceFiles when remote hydration is required
1668
+ if (summary?.workspaceFiles && !hydrationRequired) {
1579
1669
  delete summary.workspaceFiles;
1580
1670
  json = JSON.stringify(payload);
1581
1671
  if (json.length <= LIMIT)
1582
- return json;
1672
+ return finish();
1583
1673
  }
1584
- // Phase 3 truncate history to last 6 messages
1585
- if (Array.isArray(payload.history) && payload.history.length > 6) {
1586
- payload.history = payload.history.slice(-6);
1674
+ if (hydrationRequired && summary?.workspaceFiles && typeof summary.workspaceFiles === 'object') {
1675
+ summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, 4_000);
1676
+ summary.workspaceFilesCompaction = 'priority-minimal';
1587
1677
  json = JSON.stringify(payload);
1588
1678
  if (json.length <= LIMIT)
1589
- return json;
1679
+ return finish();
1590
1680
  }
1591
- // Phase 4 — truncate file list
1592
- if (summary?.files && Array.isArray(summary.files) && summary.files.length > 15) {
1593
- summary.files = summary.files.slice(0, 15);
1681
+ // Phase 5 — truncate history to last 3 messages
1682
+ if (Array.isArray(payload.history) && payload.history.length > 3) {
1683
+ payload.history = payload.history.slice(-3);
1594
1684
  json = JSON.stringify(payload);
1595
1685
  if (json.length <= LIMIT)
1596
- return json;
1686
+ return finish();
1597
1687
  }
1598
- // Phase 5drop readmeExcerpt
1599
- if (summary?.readmeExcerpt) {
1600
- delete summary.readmeExcerpt;
1688
+ // Phase 6truncate file list
1689
+ if (summary?.files && Array.isArray(summary.files) && summary.files.length > 15) {
1690
+ summary.files = summary.files.slice(0, 15);
1601
1691
  json = JSON.stringify(payload);
1602
1692
  if (json.length <= LIMIT)
1603
- return json;
1693
+ return finish();
1604
1694
  }
1605
- // Phase 6 — drop history entirely
1695
+ // Phase 7 — drop history entirely
1606
1696
  if (Array.isArray(payload.history) && payload.history.length > 0) {
1607
1697
  payload.history = [];
1608
1698
  json = JSON.stringify(payload);
1609
1699
  if (json.length <= LIMIT)
1610
- return json;
1700
+ return finish();
1611
1701
  }
1612
- // Phase 7 — drop localWorkspaceSummary entirely as last resort
1702
+ // Phase 8 — drop localWorkspaceSummary metadata but keep hydration files for remote clients
1613
1703
  if (payload.localWorkspaceSummary) {
1614
- payload.localWorkspaceSummary = { path: summary?.path, name: summary?.name, fileCount: summary?.fileCount };
1704
+ const keepFiles = hydrationRequired && summary?.workspaceFiles && typeof summary.workspaceFiles === 'object'
1705
+ ? summary.workspaceFiles
1706
+ : undefined;
1707
+ payload.localWorkspaceSummary = {
1708
+ path: summary?.path,
1709
+ name: summary?.name,
1710
+ fileCount: summary?.fileCount,
1711
+ files: Array.isArray(summary?.files) ? summary.files.slice(0, 20) : [],
1712
+ ...(keepFiles ? { workspaceFiles: keepFiles, workspaceFilesCompaction: summary?.workspaceFilesCompaction || 'priority-minimal' } : {}),
1713
+ };
1615
1714
  json = JSON.stringify(payload);
1616
1715
  }
1617
- return json;
1716
+ if (hydrationRequired && json.length > LIMIT) {
1717
+ process.stderr.write('Warning: workspace context is still large after compaction; remote agent may have partial project visibility. Consider closing other context or using /compact.\n');
1718
+ }
1719
+ return finish();
1720
+ }
1721
+ /** Normalize common Unix-only commands before Windows cmd/powershell execution. */
1722
+ normalizeWindowsRunCommand(command) {
1723
+ let cmd = String(command || '').trim();
1724
+ if (!cmd)
1725
+ return cmd;
1726
+ if (/^\/[^\s/]/.test(cmd) && !/^\/[a-z]:/i.test(cmd)) {
1727
+ throw new Error('Command starts with "/" which Windows interprets as an invalid switch. Use workspace-relative paths without a leading slash.');
1728
+ }
1729
+ const replacements = [
1730
+ [/^ls(\s|$)/i, 'dir$1'],
1731
+ [/^cat\s+/i, 'type '],
1732
+ [/^pwd\s*$/i, 'cd'],
1733
+ [/^rm\s+-rf?\s+/i, 'rmdir /s /q '],
1734
+ [/^rm\s+/i, 'del '],
1735
+ [/^cp\s+/i, 'copy '],
1736
+ [/^mv\s+/i, 'move '],
1737
+ ];
1738
+ for (const [pattern, replacement] of replacements) {
1739
+ cmd = cmd.replace(pattern, replacement);
1740
+ }
1741
+ return cmd;
1742
+ }
1743
+ resolveRunCommandCwd(rootPath, args, serverRoot) {
1744
+ const cwdRaw = args.cwd ?? args.path ?? '.';
1745
+ let target = this.resolveV3ClientToolPath(rootPath, cwdRaw, serverRoot ? [serverRoot] : []);
1746
+ if (!target) {
1747
+ target = this.resolveV3ClientToolPath(rootPath, '.', serverRoot ? [serverRoot] : []);
1748
+ }
1749
+ if (!target) {
1750
+ return path.resolve(rootPath);
1751
+ }
1752
+ try {
1753
+ if (fs.existsSync(target.absolutePath) && fs.statSync(target.absolutePath).isFile()) {
1754
+ return path.dirname(target.absolutePath);
1755
+ }
1756
+ }
1757
+ catch {
1758
+ // fall through to directory cwd
1759
+ }
1760
+ return target.absolutePath;
1618
1761
  }
1619
1762
  /** Remove duplicate functionIndex entries that poison model context after compaction. */
1620
1763
  dedupeBrainPayload(brain) {
@@ -2401,7 +2544,8 @@ menu {
2401
2544
  };
2402
2545
  const snapshot = this.getAgentWorkspaceSnapshot(rootPath);
2403
2546
  const candidatePaths = snapshot.paths.filter((entry) => !/(^|[\/\\])\.vigthoria([\/\\]|$)/.test(entry));
2404
- let orderedPaths = candidatePaths;
2547
+ const agentStatePaths = this.collectAgentStateSyncPaths(rootPath);
2548
+ let orderedPaths = [...agentStatePaths, ...candidatePaths];
2405
2549
  try {
2406
2550
  const ranked = buildSemanticContext(rootPath, requestFocus, 60).topFiles.map((file) => file.path);
2407
2551
  const changes = getChangedFiles(rootPath, candidatePaths);
@@ -2478,7 +2622,9 @@ menu {
2478
2622
  continue;
2479
2623
  if (/(^|[\/\\])\.(git|hg)([\/\\]|$)/.test(relativePath))
2480
2624
  continue;
2481
- if (/(^|[\/\\])\.vigthoria([\/\\]|$)/.test(relativePath))
2625
+ const normalizedPath = relativePath.replace(/\\/g, '/');
2626
+ const isAgentStateSync = /^\.vigthoria\/agent-state\//i.test(normalizedPath);
2627
+ if (!isAgentStateSync && /(^|[\/\\])\.vigthoria([\/\\]|$)/.test(relativePath))
2482
2628
  continue;
2483
2629
  const absolutePath = path.join(rootPath, relativePath);
2484
2630
  try {
@@ -2646,6 +2792,8 @@ menu {
2646
2792
  }
2647
2793
  return;
2648
2794
  }
2795
+ // workspace_snapshot is handled by applyV3AgentStreamEventToWorkspace only —
2796
+ // never count hydrated baselines as "files changed".
2649
2797
  // Only count confirmed mutations — not workspace snapshots (hydrated baseline)
2650
2798
  // or tool_call previews that may still fail client-side / read-only guards.
2651
2799
  if (event.type === 'tool_result' && event.success) {
@@ -2694,9 +2842,17 @@ menu {
2694
2842
  }
2695
2843
  return false;
2696
2844
  }
2697
- resolveV3ClientToolPath(rootPath, rawPath) {
2698
- const relativePath = this.normalizeAgentWorkspaceRelativePath(String(rawPath || '.'), rootPath);
2699
- if (!relativePath && String(rawPath || '.').trim() !== '.') {
2845
+ resolveV3ClientToolPath(rootPath, rawPath, sourceRoots = []) {
2846
+ const raw = String(rawPath || '.').trim();
2847
+ const roots = [rootPath, ...sourceRoots.filter(Boolean)];
2848
+ let relativePath = '';
2849
+ for (const sourceRoot of roots) {
2850
+ relativePath = this.normalizeAgentWorkspaceRelativePath(raw, sourceRoot);
2851
+ if (relativePath || raw === '.' || raw === '') {
2852
+ break;
2853
+ }
2854
+ }
2855
+ if (!relativePath && raw !== '.' && raw !== '') {
2700
2856
  return null;
2701
2857
  }
2702
2858
  const absolutePath = relativePath ? joinV3WorkspacePath(rootPath, relativePath) : path.resolve(rootPath);
@@ -2747,7 +2903,13 @@ menu {
2747
2903
  }
2748
2904
  const name = String(event.name || event.tool || '').trim();
2749
2905
  const args = event.arguments || {};
2750
- const target = this.resolveV3ClientToolPath(rootPath, args.path || args.cwd || '.');
2906
+ const serverRoot = String(context.__v3ServerWorkspaceRoot || '').trim();
2907
+ const isRunCommand = name === 'run_command';
2908
+ const pathArg = isRunCommand ? (args.cwd ?? args.path ?? '.') : (args.path || args.cwd || '.');
2909
+ let target = this.resolveV3ClientToolPath(rootPath, pathArg, serverRoot ? [serverRoot] : []);
2910
+ if (!target && isRunCommand) {
2911
+ target = this.resolveV3ClientToolPath(rootPath, '.', serverRoot ? [serverRoot] : []);
2912
+ }
2751
2913
  if (!target) {
2752
2914
  return { success: false, output: '', error: 'Tool path is outside the workspace.' };
2753
2915
  }
@@ -2831,6 +2993,14 @@ menu {
2831
2993
  error: `File not found: ${target.relativePath}. Write the file before running syntax_check.`,
2832
2994
  };
2833
2995
  }
2996
+ const stat = fs.statSync(target.absolutePath);
2997
+ if (stat.isDirectory()) {
2998
+ return {
2999
+ success: false,
3000
+ output: '',
3001
+ error: `Expected a file, but path is a directory: ${target.relativePath}`,
3002
+ };
3003
+ }
2834
3004
  const ext = path.extname(target.absolutePath).toLowerCase();
2835
3005
  if (ext === '.json') {
2836
3006
  JSON.parse(fs.readFileSync(target.absolutePath, 'utf8'));
@@ -2851,12 +3021,37 @@ menu {
2851
3021
  return { success: true, output: `Syntax check passed: ${target.relativePath}` };
2852
3022
  }
2853
3023
  if (name === 'run_command') {
2854
- const command = String(args.command || '').trim();
3024
+ let command = String(args.command || '').trim();
2855
3025
  if (!command)
2856
3026
  return { success: false, output: '', error: 'run_command requires command.' };
3027
+ try {
3028
+ if (process.platform === 'win32') {
3029
+ command = this.normalizeWindowsRunCommand(command);
3030
+ }
3031
+ }
3032
+ catch (error) {
3033
+ return { success: false, output: '', error: error.message };
3034
+ }
3035
+ const cwd = this.resolveRunCommandCwd(rootPath, args, serverRoot);
3036
+ const usePowerShell = process.platform === 'win32'
3037
+ && /^(npm|npx|node|yarn|pnpm|git|python|pip|powershell)\b/i.test(command);
2857
3038
  const { exec } = await import('child_process');
2858
3039
  return await new Promise((resolve) => {
2859
- exec(command, { cwd: target.absolutePath, timeout: Number(args.timeout || 30000) }, (error, stdout, stderr) => {
3040
+ const execOptions = {
3041
+ cwd,
3042
+ timeout: Number(args.timeout || 30000),
3043
+ maxBuffer: 1024 * 1024,
3044
+ env: process.env,
3045
+ };
3046
+ if (usePowerShell) {
3047
+ execOptions.shell = 'powershell.exe';
3048
+ execOptions.windowsHide = true;
3049
+ command = `-NoProfile -ExecutionPolicy Bypass -Command ${JSON.stringify(command)}`;
3050
+ }
3051
+ else if (process.platform === 'win32') {
3052
+ execOptions.shell = 'cmd.exe';
3053
+ }
3054
+ exec(command, execOptions, (error, stdout, stderr) => {
2860
3055
  resolve({
2861
3056
  success: !error,
2862
3057
  output: String(stdout || stderr || '').slice(0, 12000),
@@ -3688,6 +3883,7 @@ document.addEventListener('DOMContentLoaded', () => {
3688
3883
  }
3689
3884
  if (!serverWorkspaceRoot && event.type === 'context' && typeof event.workspace_root === 'string' && event.workspace_root.trim()) {
3690
3885
  serverWorkspaceRoot = event.workspace_root.trim();
3886
+ context.__v3ServerWorkspaceRoot = serverWorkspaceRoot;
3691
3887
  }
3692
3888
  if (event.type === 'client_tool_request') {
3693
3889
  const toolTask = this.handleV3ClientToolRequest(event, context, response.url).catch((error) => {
@@ -3721,7 +3917,7 @@ document.addEventListener('DOMContentLoaded', () => {
3721
3917
  }
3722
3918
  }
3723
3919
  }
3724
- if (typeof context.onStreamEvent === 'function') {
3920
+ if (typeof context.onStreamEvent === 'function' && !isV3StreamKeepaliveEvent(userEvent)) {
3725
3921
  try {
3726
3922
  context.onStreamEvent(userEvent);
3727
3923
  }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Client-side context budget helpers — aligned with V3 server context_budget.py.
3
+ * Initial agent context JSON should leave headroom for system prompt + tool loops.
4
+ */
5
+ export declare const DEFAULT_CODE_RUNTIME_CTX: number;
6
+ export declare const CONTEXT_SAFETY_MARGIN: number;
7
+ export declare const DEFAULT_MAX_OUTPUT_TOKENS: number;
8
+ export declare const TOKEN_BUDGET_SAFETY_FACTOR: number;
9
+ export declare const CHARS_PER_TOKEN_EST = 4;
10
+ /** Max chars per file in workspaceFiles hydration (matches server TOOL_HISTORY cap order-of-magnitude). */
11
+ export declare const WORKSPACE_FILE_CHAR_CAP: number;
12
+ export declare function estimateTokens(text: string): number;
13
+ export declare function estimateTokensForBudget(text: string): number;
14
+ export declare function estimateJsonTokens(value: unknown): number;
15
+ export declare function resolveInputBudget(runtimeNctx?: number, maxOutput?: number): number;
16
+ /**
17
+ * Char limit for the initial V3 context JSON payload.
18
+ * ~45% of input token budget × 4 chars/token, clamped 32k–100k.
19
+ */
20
+ export declare function resolveV3ContextCharLimit(runtimeNctx?: number): number;
21
+ export declare function compactToolListingOutput(name: string, output: string, charCap: number): string;
22
+ export interface ContextCompactionMeta {
23
+ applied: boolean;
24
+ beforeChars: number;
25
+ afterChars: number;
26
+ charLimit: number;
27
+ estimatedInputBudget: number;
28
+ phases: string[];
29
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Client-side context budget helpers — aligned with V3 server context_budget.py.
3
+ * Initial agent context JSON should leave headroom for system prompt + tool loops.
4
+ */
5
+ export const DEFAULT_CODE_RUNTIME_CTX = Number(process.env.V3_CODE_35B_RUNTIME_CTX || '49152');
6
+ export const CONTEXT_SAFETY_MARGIN = Number(process.env.V3_CONTEXT_SAFETY_MARGIN || '2048');
7
+ export const DEFAULT_MAX_OUTPUT_TOKENS = Number(process.env.V3_MAX_TOKENS || '16384');
8
+ export const TOKEN_BUDGET_SAFETY_FACTOR = Number(process.env.V3_TOKEN_BUDGET_SAFETY_FACTOR || '1.2');
9
+ export const CHARS_PER_TOKEN_EST = 4;
10
+ /** Max chars per file in workspaceFiles hydration (matches server TOOL_HISTORY cap order-of-magnitude). */
11
+ export const WORKSPACE_FILE_CHAR_CAP = Number(process.env.V3_TOOL_HISTORY_CHAR_CAP || '12000');
12
+ export function estimateTokens(text) {
13
+ return Math.max(1, Math.ceil(String(text || '').length / CHARS_PER_TOKEN_EST));
14
+ }
15
+ export function estimateTokensForBudget(text) {
16
+ return Math.ceil(estimateTokens(text) * TOKEN_BUDGET_SAFETY_FACTOR);
17
+ }
18
+ export function estimateJsonTokens(value) {
19
+ try {
20
+ return estimateTokens(JSON.stringify(value ?? ''));
21
+ }
22
+ catch {
23
+ return 1;
24
+ }
25
+ }
26
+ export function resolveInputBudget(runtimeNctx = DEFAULT_CODE_RUNTIME_CTX, maxOutput = DEFAULT_MAX_OUTPUT_TOKENS) {
27
+ const cappedOutput = Math.max(4096, Math.min(maxOutput, Math.floor(runtimeNctx / 4)));
28
+ return Math.max(4096, Math.floor((runtimeNctx - cappedOutput - CONTEXT_SAFETY_MARGIN) / TOKEN_BUDGET_SAFETY_FACTOR));
29
+ }
30
+ /**
31
+ * Char limit for the initial V3 context JSON payload.
32
+ * ~45% of input token budget × 4 chars/token, clamped 32k–100k.
33
+ */
34
+ export function resolveV3ContextCharLimit(runtimeNctx = DEFAULT_CODE_RUNTIME_CTX) {
35
+ const inputBudget = resolveInputBudget(runtimeNctx);
36
+ const initialTurnTokens = Math.floor(inputBudget * 0.45);
37
+ const charLimit = initialTurnTokens * CHARS_PER_TOKEN_EST;
38
+ return Math.min(100_000, Math.max(32_000, charLimit));
39
+ }
40
+ export function compactToolListingOutput(name, output, charCap) {
41
+ const text = String(output || '');
42
+ if (text.length <= charCap)
43
+ return text;
44
+ if (['list_directory', 'glob', 'grep', 'search_project'].includes(name)) {
45
+ const lines = text.split('\n');
46
+ if (lines.length > 80) {
47
+ const head = lines.slice(0, 40).join('\n');
48
+ const tail = lines.slice(-20).join('\n');
49
+ return `${head}\n\n... [${lines.length} lines total, middle omitted for context budget] ...\n\n${tail}`;
50
+ }
51
+ }
52
+ const head = Math.max(1200, Math.floor(charCap / 3));
53
+ const tail = Math.max(1200, Math.floor(charCap / 3));
54
+ return `${text.slice(0, head)}\n\n...[truncated for context budget]...\n\n${text.slice(-tail)}`;
55
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Workbench GOA orchestrator context (VIGTHORIA_DECK_MODE / comfort prompt auto-route).
3
+ * Emitted as: @vigthoria-goa:{json}
4
+ */
5
+ export interface GoaOrchestratorContext {
6
+ workspace_type?: 'GREENFIELD' | 'BROWNFIELD';
7
+ analysis?: Record<string, unknown>;
8
+ instruction_count?: number;
9
+ executed?: boolean;
10
+ [key: string]: unknown;
11
+ }
12
+ export declare function parseGoaLine(line: string): GoaOrchestratorContext | null;
13
+ export declare function stripGoaLines(text: string): string;
14
+ /** Split Workbench-injected GOA prefix from the user prompt. */
15
+ export declare function splitGoaContextFromInput(input: string): {
16
+ goaContext: GoaOrchestratorContext | null;
17
+ userPrompt: string;
18
+ };
19
+ export declare function formatGoaSystemGrounding(context: GoaOrchestratorContext): string;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Workbench GOA orchestrator context (VIGTHORIA_DECK_MODE / comfort prompt auto-route).
3
+ * Emitted as: @vigthoria-goa:{json}
4
+ */
5
+ const GOA_PREFIX = '@vigthoria-goa:';
6
+ export function parseGoaLine(line) {
7
+ const trimmed = line.trim();
8
+ if (!trimmed.startsWith(GOA_PREFIX))
9
+ return null;
10
+ try {
11
+ return JSON.parse(trimmed.slice(GOA_PREFIX.length));
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ export function stripGoaLines(text) {
18
+ return text
19
+ .split('\n')
20
+ .filter((line) => !line.trim().startsWith(GOA_PREFIX))
21
+ .join('\n');
22
+ }
23
+ /** Split Workbench-injected GOA prefix from the user prompt. */
24
+ export function splitGoaContextFromInput(input) {
25
+ const lines = input.split('\n');
26
+ let goaContext = null;
27
+ const rest = [];
28
+ for (const line of lines) {
29
+ const parsed = parseGoaLine(line);
30
+ if (parsed && !goaContext) {
31
+ goaContext = parsed;
32
+ continue;
33
+ }
34
+ rest.push(line);
35
+ }
36
+ return { goaContext, userPrompt: rest.join('\n').trim() };
37
+ }
38
+ export function formatGoaSystemGrounding(context) {
39
+ const parts = [
40
+ 'VIGTHORIA GOA ORCHESTRATOR CONTEXT (pre-executed by Workbench — do not re-scaffold greenfield templates):',
41
+ `- workspace_type: ${context.workspace_type || 'unknown'}`,
42
+ ];
43
+ if (context.executed === true) {
44
+ parts.push('- Workbench already executed the GOA payload (template write / AST pre-scan / stabilize).');
45
+ parts.push('- Continue from the current workspace state; prefer surgical edits over full rewrites.');
46
+ }
47
+ if (context.analysis && typeof context.analysis === 'object') {
48
+ const a = context.analysis;
49
+ if (typeof a.detected_framework === 'string')
50
+ parts.push(`- framework: ${a.detected_framework}`);
51
+ if (Array.isArray(a.target_files) && a.target_files.length) {
52
+ parts.push(`- target_files: ${a.target_files.slice(0, 6).join(', ')}`);
53
+ }
54
+ if (typeof a.anchor_hint === 'string')
55
+ parts.push(`- anchor: ${a.anchor_hint}`);
56
+ if (typeof a.template_match === 'string')
57
+ parts.push(`- template_match: ${a.template_match}`);
58
+ }
59
+ if (typeof context.instruction_count === 'number') {
60
+ parts.push(`- instructions_planned: ${context.instruction_count}`);
61
+ }
62
+ return parts.join('\n');
63
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Detect V3 agent SSE keepalive/heartbeat events that exist only to keep
3
+ * connections alive — not substantive work the CLI should surface as progress.
4
+ */
5
+ export declare function isV3StreamKeepaliveEvent(event: unknown): boolean;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Detect V3 agent SSE keepalive/heartbeat events that exist only to keep
3
+ * connections alive — not substantive work the CLI should surface as progress.
4
+ */
5
+ const PLANNING_KEEPALIVE = /^Planning in progress\.\.\.\s*\(\d+s(?:\s+elapsed)?\)$/i;
6
+ const PARALLEL_KEEPALIVE = /^Parallel tasks in progress\.\.\.\s*\(\d+s(?:\s+elapsed)?\)$/i;
7
+ const GENERIC_ELAPSED_KEEPALIVE = /\bin progress\b.*\(\d+s(?:\s+elapsed)?\)$/i;
8
+ export function isV3StreamKeepaliveEvent(event) {
9
+ if (!event || typeof event !== 'object') {
10
+ return false;
11
+ }
12
+ const record = event;
13
+ const type = typeof record.type === 'string' ? record.type : '';
14
+ if (type === 'heartbeat') {
15
+ return true;
16
+ }
17
+ if (type === 'progress' || type === 'status') {
18
+ const content = String(record.content || record.message || record.status || '').trim();
19
+ if (!content) {
20
+ return false;
21
+ }
22
+ if (PLANNING_KEEPALIVE.test(content) || PARALLEL_KEEPALIVE.test(content)) {
23
+ return true;
24
+ }
25
+ if (GENERIC_ELAPSED_KEEPALIVE.test(content)) {
26
+ return true;
27
+ }
28
+ }
29
+ if (type === 'plan') {
30
+ const plan = record.plan;
31
+ if (plan && typeof plan === 'object') {
32
+ const planRecord = plan;
33
+ const status = typeof planRecord.status === 'string' ? planRecord.status : '';
34
+ const tasks = planRecord.tasks;
35
+ if (status === 'planning' && !Array.isArray(tasks)) {
36
+ return true;
37
+ }
38
+ }
39
+ }
40
+ return false;
41
+ }
@@ -42,6 +42,45 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
42
42
  }
43
43
  return null;
44
44
  };
45
+ const stripScrubbedPlaceholder = (value) => {
46
+ let candidate = String(value || '').replace(/^\/+/, '');
47
+ // Hybrid paths models echo back after SSE scrubbing, e.g. C:/var/www/[internal]/game.js
48
+ const hybridWin = candidate.match(/^[a-zA-Z]:\/(?:var\/)?www\/\[internal\](?:\/(.*))?$/i);
49
+ if (hybridWin) {
50
+ return safeRelative(hybridWin[1] || '');
51
+ }
52
+ const hybridWinBare = candidate.match(/^[a-zA-Z]:\/\[internal\](?:\/(.*))?$/i);
53
+ if (hybridWinBare) {
54
+ return safeRelative(hybridWinBare[1] || '');
55
+ }
56
+ const placeholderPatterns = [
57
+ [/^(?:var\/)?www\/\[internal\](?:\/(.*))?$/i, 1],
58
+ [/^\[internal\](?:\/(.*))?$/i, 1],
59
+ [/^\[Vigthoria service\](?:\/(.*))?$/i, 1],
60
+ [/^\[Vigthoria Agent\](?:\/(.*))?$/i, 1],
61
+ [/^\[Vigthoria storage\](?:\/(.*))?$/i, 1],
62
+ [/^\[temp\](?:\/(.*))?$/i, 1],
63
+ [/^\[home\](?:\/(.*))?$/i, 1],
64
+ [/^\[workspace\](?:\/(.*))?$/i, 1],
65
+ [/^(?:var\/)?www\/\[Vigthoria service\](?:\/(.*))?$/i, 1],
66
+ ];
67
+ for (const [pattern, groupIndex] of placeholderPatterns) {
68
+ const match = candidate.match(pattern);
69
+ if (match) {
70
+ return safeRelative(match[groupIndex] || '');
71
+ }
72
+ }
73
+ const bracketSplit = candidate.split(/\[(?:internal|Vigthoria service|Vigthoria Agent|temp|home|workspace)\]/i);
74
+ if (bracketSplit.length > 1) {
75
+ const tail = bracketSplit[bracketSplit.length - 1].replace(/^\/+/, '');
76
+ return safeRelative(tail);
77
+ }
78
+ return null;
79
+ };
80
+ const scrubbed = stripScrubbedPlaceholder(input);
81
+ if (scrubbed !== null) {
82
+ return scrubbed;
83
+ }
45
84
  const decoded = decodeBoundaryUri(input);
46
85
  if (decoded !== null) {
47
86
  return decoded;
@@ -72,6 +111,18 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
72
111
  }
73
112
  const normalizedRoot = String(rootPath || '').trim().replace(/\\/g, '/').replace(/\/+$/g, '');
74
113
  if (normalizedRoot) {
114
+ if (/^[a-zA-Z]:\//.test(input)) {
115
+ const winRoot = normalizedRoot;
116
+ const inputLower = input.toLowerCase();
117
+ const winRootLower = winRoot.toLowerCase();
118
+ if (inputLower === winRootLower || inputLower === `${winRootLower}/`) {
119
+ return '';
120
+ }
121
+ if (inputLower.startsWith(`${winRootLower}/`)) {
122
+ return safeRelative(input.slice(winRoot.length + 1));
123
+ }
124
+ return '';
125
+ }
75
126
  const rootNoLeadingSlash = normalizedRoot.replace(/^\//, '');
76
127
  const rootBase = path.posix.basename(normalizedRoot);
77
128
  const prefixes = [
@@ -90,7 +141,11 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
90
141
  return safeRelative(input.slice(embeddedIndex + embeddedRoot.length));
91
142
  }
92
143
  }
93
- return safeRelative(input);
144
+ const relative = safeRelative(input);
145
+ if (relative === 'workspace') {
146
+ return '';
147
+ }
148
+ return relative;
94
149
  }
95
150
  /** Join workspace root + relative path without path.resolve URI corruption on Windows. */
96
151
  export function joinV3WorkspacePath(rootPath, relativePath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.11.29",
3
+ "version": "1.11.36",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,7 +37,7 @@
37
37
  "dev": "ts-node src/index.ts",
38
38
  "test": "npm run test:cli",
39
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
+ "test:cli": "npm run build && npm run precheck:services && node scripts/test-cli-suite.js && npm run test:agent:outcome && npm run test:v3-stream-mutation",
41
41
  "test:regression": "npm run build && node scripts/test-regression-1.6.22.js",
42
42
  "test:agent:smoke": "npm run build && node scripts/test-agent-smoke.js",
43
43
  "test:agent:routing": "npm run build && node scripts/test-agent-routing-policy.js",
@@ -68,7 +68,11 @@
68
68
  "test:model:governance": "npm run build && node scripts/test-model-governance-no-agent.js",
69
69
  "validate:no-go": "bash scripts/release/validate-no-go-gates.sh",
70
70
  "test:legion:billing:e2e": "npm run build && node scripts/test-legion-godmode-billing-e2e.js",
71
- "test:windows:v3-sync": "npm run build && node scripts/test-windows-v3-sync-recovery.js"
71
+ "test:windows:v3-sync": "npm run build && node scripts/test-windows-v3-sync-recovery.js",
72
+ "test:v3-workspace-path": "npm run build && node scripts/test-v3-workspace-path.js",
73
+ "test:v3-stream-mutation": "npm run build && node scripts/test-v3-stream-mutation.js",
74
+ "test:context:budget": "npm run build && node scripts/test-context-budget.js",
75
+ "test:pitfall:context": "npm run build && node scripts/test-pitfall-context-smoke.js"
72
76
  },
73
77
  "keywords": [
74
78
  "ai",