vigthoria-cli 1.11.31 → 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,6 +17,7 @@ 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';
@@ -77,6 +78,7 @@ export class ChatCommand {
77
78
  v3SeenToolResults = new Set();
78
79
  v3StartSeen = false;
79
80
  lastAgentRoute = null;
81
+ pendingGoaContext = null;
80
82
  // Last completed Agent run — used by /retry, /continue, and the final summary block.
81
83
  lastAgentRunOutcome = null;
82
84
  isJwtExpirationError(error) {
@@ -782,6 +784,8 @@ export class ChatCommand {
782
784
  v3IterationCount = 0;
783
785
  v3ToolCallCount = 0;
784
786
  v3LastActivity = Date.now();
787
+ v3IdleWatchInterval = null;
788
+ v3IdleNoticeShown = false;
785
789
  v3StreamingStarted = false;
786
790
  /**
787
791
  * Strip server-internal path prefixes from tool output strings.
@@ -797,10 +801,41 @@ export class ChatCommand {
797
801
  return text;
798
802
  return text
799
803
  .replace(/<\|mask_start\|>[\s\S]*?<\|mask_end\|>/g, '')
800
- .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, '')
801
808
  .replace(/<\|(?:mask_start|mask_end)\|>/g, '')
802
809
  .trim();
803
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
+ }
804
839
  describeV3AgentTool(toolName) {
805
840
  const normalized = String(toolName || '').toLowerCase();
806
841
  if (/read|grep|search|list|find|glob/.test(normalized)) {
@@ -935,6 +970,7 @@ export class ChatCommand {
935
970
  return '';
936
971
  output = output.replace(/<\/think>/gi, '</thinking>');
937
972
  output = output.replace(/<think>/gi, '<thinking>');
973
+ output = output.replace(/<\/redacted_thinking>/gi, '</thinking>');
938
974
  if (this.v3SuppressThinkingStream) {
939
975
  const closeIdx = output.search(/<\/thinking>/i);
940
976
  if (closeIdx < 0) {
@@ -1095,6 +1131,13 @@ export class ChatCommand {
1095
1131
  : (event.content || '');
1096
1132
  if (text) {
1097
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
+ }
1098
1141
  this.writeV3StreamText(spinner, text);
1099
1142
  }
1100
1143
  else {
@@ -1720,6 +1763,10 @@ export class ChatCommand {
1720
1763
  return false;
1721
1764
  }
1722
1765
  async handleDirectPrompt(prompt) {
1766
+ const { goaContext, userPrompt } = splitGoaContextFromInput(prompt);
1767
+ if (goaContext)
1768
+ this.pendingGoaContext = goaContext;
1769
+ const effectivePrompt = userPrompt || prompt;
1723
1770
  // Suppress all setup banners in direct-prompt mode so only the final
1724
1771
  // answer reaches stdout. Interactive (REPL) mode still shows them.
1725
1772
  if (!this.jsonOutput) {
@@ -1732,18 +1779,18 @@ export class ChatCommand {
1732
1779
  console.log();
1733
1780
  }
1734
1781
  if (this.workflowTarget) {
1735
- await this.runWorkflowTargetPrompt(prompt);
1782
+ await this.runWorkflowTargetPrompt(effectivePrompt);
1736
1783
  return;
1737
1784
  }
1738
1785
  // Smart routing: for agent mode, determine if prompt needs tool access
1739
1786
  if (this.agentMode) {
1740
- if (this.isSimpleDirectPrompt(prompt)) {
1787
+ if (this.isSimpleDirectPrompt(effectivePrompt)) {
1741
1788
  // Simple prompt: downgrade to plain chat model, no agent system prompt
1742
1789
  this.currentModel = this.getDefaultChatModel();
1743
- await this.runSimplePrompt(prompt);
1790
+ await this.runSimplePrompt(effectivePrompt);
1744
1791
  }
1745
1792
  else {
1746
- await this.runAgentTurn(prompt);
1793
+ await this.runAgentTurn(effectivePrompt);
1747
1794
  }
1748
1795
  return;
1749
1796
  }
@@ -1752,10 +1799,10 @@ export class ChatCommand {
1752
1799
  this.logger.error(this.operatorAccessMessage());
1753
1800
  return;
1754
1801
  }
1755
- await this.runOperatorTurn(prompt);
1802
+ await this.runOperatorTurn(effectivePrompt);
1756
1803
  return;
1757
1804
  }
1758
- await this.runSimplePrompt(prompt);
1805
+ await this.runSimplePrompt(effectivePrompt);
1759
1806
  }
1760
1807
  formatWorkflowTargetResult(result) {
1761
1808
  if (typeof result === 'string') {
@@ -2186,7 +2233,19 @@ export class ChatCommand {
2186
2233
  if (!this.tools) {
2187
2234
  throw new Error('Agent tools are not initialized.');
2188
2235
  }
2189
- 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);
2190
2249
  this.lastAgentRoute = await resolveAgentRoute(this.api, resolvedPrompt);
2191
2250
  if (!this.jsonOutput) {
2192
2251
  const r = this.lastAgentRoute;
@@ -2941,6 +3000,7 @@ export class ChatCommand {
2941
3000
  spinner: 'clock',
2942
3001
  isSilent: true,
2943
3002
  }).start();
3003
+ this.startV3IdleWatch(spinner);
2944
3004
  // Live run telemetry, used both for the final summary block and for /retry, /continue.
2945
3005
  const liveOutcome = {
2946
3006
  ...createLiveOutcome(),
@@ -3207,8 +3267,8 @@ export class ChatCommand {
3207
3267
  console.log(chalk.yellow(`Template Service preview gate: failed${previewGate.error ? ` - ${previewGate.error}` : ''}`));
3208
3268
  }
3209
3269
  }
3210
- // Show change summary for files touched by the agent
3211
- 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) {
3212
3272
  const fileCount = changedFileCount;
3213
3273
  if (fileCount > 0) {
3214
3274
  console.log(chalk.gray(`\nFiles changed: ${fileCount}`));
@@ -3402,10 +3462,12 @@ export class ChatCommand {
3402
3462
  }, null, 2));
3403
3463
  }
3404
3464
  this.messages.push({ role: 'assistant', content: finalAnswer || runEvaluation.statusHeadline });
3465
+ this.stopV3IdleWatch();
3405
3466
  watcher?.stop();
3406
3467
  return true;
3407
3468
  }
3408
3469
  catch (error) {
3470
+ this.stopV3IdleWatch();
3409
3471
  watcher?.stop();
3410
3472
  if (!this.api.hasAgentWorkspaceOutput(workspaceContext)) {
3411
3473
  const recovered = await this.tryRecoverV3ServiceAndRetry(executionPrompt, prompt, workspaceContext, routingPolicy, spinner, error);
@@ -3619,7 +3681,12 @@ export class ChatCommand {
3619
3681
  return lines.join('\n');
3620
3682
  };
3621
3683
  while (true) {
3622
- 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;
3623
3690
  const trimmed = input.trim();
3624
3691
  if (!trimmed) {
3625
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
  }
@@ -252,6 +290,13 @@ export function evaluateExecutorSuccess(liveOutcome) {
252
290
  || (liveOutcome.analysisToolsUsed >= 4
253
291
  && liveOutcome.analysisReadToolsUsed >= 3
254
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
+ }
255
300
  if (hasFatalError && !hasSubstantiveAnswer) {
256
301
  return {
257
302
  executorSucceeded: false,
@@ -293,6 +338,13 @@ export function evaluateExecutorSuccess(liveOutcome) {
293
338
  uiTheme: 'success',
294
339
  };
295
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
+ }
296
348
  const legacySuccess = liveOutcome.workspaceHasOutput && liveOutcome.changedFileCount > 0;
297
349
  return {
298
350
  executorSucceeded: legacySuccess && !hasFatalError,
@@ -323,8 +323,11 @@ export declare class APIClient {
323
323
  executionOptions?: Record<string, unknown>;
324
324
  }): Promise<VigFlowExecutionResult>;
325
325
  getVigFlowExecutionStatus(executionId: string): Promise<VigFlowExecutionStatus>;
326
- /** Maximum serialized context length accepted by the V3 server. */
326
+ /** Legacy cap prefer resolveV3ContextCharLimit() for token-aligned budgets. */
327
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;
328
331
  buildV3AgentContext(context?: Record<string, any>): string;
329
332
  /**
330
333
  * Compact a V3 context payload so the serialized JSON stays under
@@ -336,6 +339,9 @@ export declare class APIClient {
336
339
  * 5. Drop readmeExcerpt
337
340
  */
338
341
  private compactV3Context;
342
+ /** Normalize common Unix-only commands before Windows cmd/powershell execution. */
343
+ private normalizeWindowsRunCommand;
344
+ private resolveRunCommandCwd;
339
345
  /** Remove duplicate functionIndex entries that poison model context after compaction. */
340
346
  private dedupeBrainPayload;
341
347
  buildMinimalV3AgentContext(context?: Record<string, any>): string;
package/dist/utils/api.js CHANGED
@@ -15,6 +15,7 @@ import { isSubstantiveAgentAnswer, isToolEvidenceStubAnswer } from './agentRunOu
15
15
  import { joinV3WorkspacePath, normalizeV3WorkspaceRelativePath } from './v3-workspace-path.js';
16
16
  import { buildExecutionHints, isPlannerBuildTask, resolvePlannerAgentTimeoutMs } from './requestIntent.js';
17
17
  import { isV3StreamKeepaliveEvent } from './v3-stream-events.js';
18
+ import { resolveV3ContextCharLimit, WORKSPACE_FILE_CHAR_CAP, } from './contextBudget.js';
18
19
  export const VIGTHORIA_HUB_CREDITS_URL = 'https://hub.vigthoria.io/credits';
19
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.';
20
21
  export class CLIError extends Error {
@@ -1449,8 +1450,41 @@ export class APIClient {
1449
1450
  return payload.execution;
1450
1451
  });
1451
1452
  }
1452
- /** Maximum serialized context length accepted by the V3 server. */
1453
- 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
+ }
1454
1488
  buildV3AgentContext(context = {}) {
1455
1489
  const resolvedContext = this.ensureExecutionContext(context);
1456
1490
  const targetPath = resolvedContext.targetPath || resolvedContext.projectPath || resolvedContext.workspacePath || resolvedContext.projectRoot || process.cwd();
@@ -1558,7 +1592,16 @@ export class APIClient {
1558
1592
  * 5. Drop readmeExcerpt
1559
1593
  */
1560
1594
  compactV3Context(payload) {
1561
- 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
+ };
1562
1605
  if (payload.vigthoriaBrain) {
1563
1606
  payload.vigthoriaBrain = this.dedupeBrainPayload(payload.vigthoriaBrain);
1564
1607
  }
@@ -1566,80 +1609,155 @@ export class APIClient {
1566
1609
  payload.vigthoria_brain = payload.vigthoriaBrain || this.dedupeBrainPayload(payload.vigthoria_brain);
1567
1610
  }
1568
1611
  let json = JSON.stringify(payload);
1569
- if (json.length <= LIMIT)
1612
+ compactionMeta.beforeChars = json.length;
1613
+ if (json.length <= LIMIT) {
1614
+ compactionMeta.afterChars = json.length;
1615
+ payload.contextBudgetMeta = compactionMeta;
1570
1616
  return json;
1617
+ }
1618
+ compactionMeta.applied = true;
1619
+ phases.push('over_limit');
1571
1620
  if (!/^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_QUIET_CONTEXT_COMPACTION || ''))) {
1572
- 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`);
1573
1622
  }
1574
1623
  if (process.env.DEBUG || process.env.VIGTHORIA_DEBUG) {
1575
1624
  process.stderr.write(`[context] Payload ${json.length} chars exceeds ${LIMIT} limit, compacting...\n`);
1576
1625
  }
1577
- // Phase 1 — shrink workspaceFiles to fit
1578
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
1579
1642
  if (summary?.workspaceFiles && typeof summary.workspaceFiles === 'object') {
1580
- const fileEntries = Object.entries(summary.workspaceFiles);
1581
1643
  const overhead = json.length - JSON.stringify(summary.workspaceFiles).length;
1582
- const budget = LIMIT - overhead - 512; // reserve a little headroom
1644
+ const budget = LIMIT - overhead - 1024;
1583
1645
  if (budget > 0) {
1584
- const trimmed = {};
1585
- let used = 2; // {}
1586
- for (const [k, v] of fileEntries) {
1587
- const entryLen = JSON.stringify(k).length + 1 + JSON.stringify(v).length + 1;
1588
- if (used + entryLen > budget)
1589
- break;
1590
- trimmed[k] = v;
1591
- used += entryLen;
1592
- }
1593
- 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';
1594
1652
  }
1595
1653
  else {
1596
1654
  delete summary.workspaceFiles;
1597
1655
  }
1598
1656
  json = JSON.stringify(payload);
1599
1657
  if (json.length <= LIMIT)
1600
- return json;
1658
+ return finish();
1659
+ }
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();
1601
1666
  }
1602
- // Phase 2 — drop workspaceFiles entirely
1603
- if (summary?.workspaceFiles) {
1667
+ // Phase 4never fully drop workspaceFiles when remote hydration is required
1668
+ if (summary?.workspaceFiles && !hydrationRequired) {
1604
1669
  delete summary.workspaceFiles;
1605
1670
  json = JSON.stringify(payload);
1606
1671
  if (json.length <= LIMIT)
1607
- return json;
1672
+ return finish();
1608
1673
  }
1609
- // Phase 3 truncate history to last 6 messages
1610
- if (Array.isArray(payload.history) && payload.history.length > 6) {
1611
- 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';
1612
1677
  json = JSON.stringify(payload);
1613
1678
  if (json.length <= LIMIT)
1614
- return json;
1679
+ return finish();
1615
1680
  }
1616
- // Phase 4 — truncate file list
1617
- if (summary?.files && Array.isArray(summary.files) && summary.files.length > 15) {
1618
- 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);
1619
1684
  json = JSON.stringify(payload);
1620
1685
  if (json.length <= LIMIT)
1621
- return json;
1686
+ return finish();
1622
1687
  }
1623
- // Phase 5drop readmeExcerpt
1624
- if (summary?.readmeExcerpt) {
1625
- 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);
1626
1691
  json = JSON.stringify(payload);
1627
1692
  if (json.length <= LIMIT)
1628
- return json;
1693
+ return finish();
1629
1694
  }
1630
- // Phase 6 — drop history entirely
1695
+ // Phase 7 — drop history entirely
1631
1696
  if (Array.isArray(payload.history) && payload.history.length > 0) {
1632
1697
  payload.history = [];
1633
1698
  json = JSON.stringify(payload);
1634
1699
  if (json.length <= LIMIT)
1635
- return json;
1700
+ return finish();
1636
1701
  }
1637
- // Phase 7 — drop localWorkspaceSummary entirely as last resort
1702
+ // Phase 8 — drop localWorkspaceSummary metadata but keep hydration files for remote clients
1638
1703
  if (payload.localWorkspaceSummary) {
1639
- 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
+ };
1640
1714
  json = JSON.stringify(payload);
1641
1715
  }
1642
- 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;
1643
1761
  }
1644
1762
  /** Remove duplicate functionIndex entries that poison model context after compaction. */
1645
1763
  dedupeBrainPayload(brain) {
@@ -2674,18 +2792,8 @@ menu {
2674
2792
  }
2675
2793
  return;
2676
2794
  }
2677
- if (event.type === 'workspace_snapshot' && event.files && typeof event.files === 'object') {
2678
- for (const [rawPath, content] of Object.entries(event.files)) {
2679
- if (typeof content !== 'string') {
2680
- continue;
2681
- }
2682
- const filePath = this.normalizeAgentWorkspaceRelativePath(rawPath, serverRoot || undefined);
2683
- if (filePath) {
2684
- streamedFiles[filePath] = content;
2685
- }
2686
- }
2687
- return;
2688
- }
2795
+ // workspace_snapshot is handled by applyV3AgentStreamEventToWorkspace only
2796
+ // never count hydrated baselines as "files changed".
2689
2797
  // Only count confirmed mutations — not workspace snapshots (hydrated baseline)
2690
2798
  // or tool_call previews that may still fail client-side / read-only guards.
2691
2799
  if (event.type === 'tool_result' && event.success) {
@@ -2796,7 +2904,12 @@ menu {
2796
2904
  const name = String(event.name || event.tool || '').trim();
2797
2905
  const args = event.arguments || {};
2798
2906
  const serverRoot = String(context.__v3ServerWorkspaceRoot || '').trim();
2799
- const target = this.resolveV3ClientToolPath(rootPath, args.path || args.cwd || '.', serverRoot ? [serverRoot] : []);
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
+ }
2800
2913
  if (!target) {
2801
2914
  return { success: false, output: '', error: 'Tool path is outside the workspace.' };
2802
2915
  }
@@ -2911,16 +3024,34 @@ menu {
2911
3024
  let command = String(args.command || '').trim();
2912
3025
  if (!command)
2913
3026
  return { success: false, output: '', error: 'run_command requires command.' };
2914
- if (process.platform === 'win32' && /^ls(\s|$)/i.test(command)) {
2915
- command = command.replace(/^ls\b/i, 'dir');
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 };
2916
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);
2917
3038
  const { exec } = await import('child_process');
2918
3039
  return await new Promise((resolve) => {
2919
- exec(command, {
2920
- cwd: target.absolutePath,
3040
+ const execOptions = {
3041
+ cwd,
2921
3042
  timeout: Number(args.timeout || 30000),
2922
- shell: process.platform === 'win32' ? 'cmd.exe' : undefined,
2923
- }, (error, stdout, stderr) => {
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) => {
2924
3055
  resolve({
2925
3056
  success: !error,
2926
3057
  output: String(stdout || stderr || '').slice(0, 12000),
@@ -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
+ }
@@ -43,7 +43,16 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
43
43
  return null;
44
44
  };
45
45
  const stripScrubbedPlaceholder = (value) => {
46
- const candidate = String(value || '').replace(/^\/+/, '');
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
+ }
47
56
  const placeholderPatterns = [
48
57
  [/^(?:var\/)?www\/\[internal\](?:\/(.*))?$/i, 1],
49
58
  [/^\[internal\](?:\/(.*))?$/i, 1],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.11.31",
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",
@@ -69,7 +69,10 @@
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
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"
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"
73
76
  },
74
77
  "keywords": [
75
78
  "ai",