vigthoria-cli 1.13.20 → 1.13.22

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.
Files changed (185) hide show
  1. package/completions/_vigthoria +49 -0
  2. package/completions/vigthoria.bash +6 -0
  3. package/completions/vigthoria.fish +41 -0
  4. package/dist/commands/auth.d.ts +1 -0
  5. package/dist/commands/auth.js +63 -128
  6. package/dist/commands/background.d.ts +0 -2
  7. package/dist/commands/background.js +26 -34
  8. package/dist/commands/bridge.js +45 -41
  9. package/dist/commands/cancel.d.ts +1 -0
  10. package/dist/commands/cancel.js +22 -16
  11. package/dist/commands/chat.d.ts +3 -42
  12. package/dist/commands/chat.js +378 -1123
  13. package/dist/commands/coding-registration.d.ts +4 -0
  14. package/dist/commands/coding-registration.js +24 -0
  15. package/dist/commands/config.js +23 -29
  16. package/dist/commands/conversation-registration.d.ts +4 -0
  17. package/dist/commands/conversation-registration.js +122 -0
  18. package/dist/commands/deploy.d.ts +8 -2
  19. package/dist/commands/deploy.js +92 -90
  20. package/dist/commands/device.d.ts +1 -2
  21. package/dist/commands/device.js +37 -21
  22. package/dist/commands/doctor-registration.d.ts +4 -0
  23. package/dist/commands/doctor-registration.js +200 -0
  24. package/dist/commands/edit.js +22 -33
  25. package/dist/commands/explain.js +7 -10
  26. package/dist/commands/fork.d.ts +1 -2
  27. package/dist/commands/fork.js +72 -22
  28. package/dist/commands/game.d.ts +1 -1
  29. package/dist/commands/game.js +17 -6
  30. package/dist/commands/generate.js +7 -9
  31. package/dist/commands/history.js +11 -7
  32. package/dist/commands/hub.d.ts +2 -2
  33. package/dist/commands/hub.js +34 -35
  34. package/dist/commands/legion.d.ts +0 -1
  35. package/dist/commands/legion.js +62 -79
  36. package/dist/commands/music.d.ts +1 -2
  37. package/dist/commands/music.js +62 -27
  38. package/dist/commands/platform-registration.d.ts +4 -0
  39. package/dist/commands/platform-registration.js +45 -0
  40. package/dist/commands/preview.d.ts +0 -1
  41. package/dist/commands/preview.js +82 -35
  42. package/dist/commands/product-run-registration.d.ts +4 -0
  43. package/dist/commands/product-run-registration.js +490 -0
  44. package/dist/commands/replay.d.ts +1 -2
  45. package/dist/commands/replay.js +15 -11
  46. package/dist/commands/repo.d.ts +14 -7
  47. package/dist/commands/repo.js +245 -196
  48. package/dist/commands/review.js +9 -12
  49. package/dist/commands/security.d.ts +1 -2
  50. package/dist/commands/security.js +12 -6
  51. package/dist/commands/update-registration.d.ts +3 -0
  52. package/dist/commands/update-registration.js +338 -0
  53. package/dist/commands/v4-registration.d.ts +4 -0
  54. package/dist/commands/v4-registration.js +24 -0
  55. package/dist/commands/v4.d.ts +1 -1
  56. package/dist/commands/v4.js +46 -8
  57. package/dist/commands/wallet.d.ts +1 -3
  58. package/dist/commands/wallet.js +18 -45
  59. package/dist/commands/workflow.d.ts +2 -0
  60. package/dist/commands/workflow.js +52 -39
  61. package/dist/index.js +122 -1589
  62. package/dist/utils/agent-stream-state.d.ts +27 -0
  63. package/dist/utils/agent-stream-state.js +93 -0
  64. package/dist/utils/agentRoute.d.ts +2 -1
  65. package/dist/utils/agentRoute.js +22 -9
  66. package/dist/utils/agentRunOutcome.d.ts +16 -0
  67. package/dist/utils/agentRunOutcome.js +37 -2
  68. package/dist/utils/api-client-factory.d.ts +8 -0
  69. package/dist/utils/api-client-factory.js +8 -0
  70. package/dist/utils/api.d.ts +44 -266
  71. package/dist/utils/api.js +647 -3180
  72. package/dist/utils/auth-session.d.ts +41 -0
  73. package/dist/utils/auth-session.js +224 -0
  74. package/dist/utils/brain-hub-client.d.ts +0 -4
  75. package/dist/utils/brain-hub-client.js +6 -3
  76. package/dist/utils/bridge-client.js +2 -0
  77. package/dist/utils/capability-health-service.d.ts +48 -0
  78. package/dist/utils/capability-health-service.js +64 -0
  79. package/dist/utils/chat-prompt-policy.d.ts +18 -0
  80. package/dist/utils/chat-prompt-policy.js +61 -0
  81. package/dist/utils/chat-tool-call-parser.d.ts +11 -0
  82. package/dist/utils/chat-tool-call-parser.js +71 -0
  83. package/dist/utils/cli-state.d.ts +22 -4
  84. package/dist/utils/cli-state.js +105 -47
  85. package/dist/utils/code-operations-service.d.ts +104 -0
  86. package/dist/utils/code-operations-service.js +837 -0
  87. package/dist/utils/codebase-indexer.d.ts +1 -0
  88. package/dist/utils/codebase-indexer.js +28 -8
  89. package/dist/utils/command-contract.d.ts +61 -0
  90. package/dist/utils/command-contract.js +149 -0
  91. package/dist/utils/command-policy.d.ts +10 -0
  92. package/dist/utils/command-policy.js +54 -0
  93. package/dist/utils/config.d.ts +13 -5
  94. package/dist/utils/config.js +220 -94
  95. package/dist/utils/context-ranker.js +11 -2
  96. package/dist/utils/context-state.d.ts +39 -0
  97. package/dist/utils/context-state.js +52 -0
  98. package/dist/utils/deckEvents.d.ts +0 -5
  99. package/dist/utils/deckEvents.js +3 -2
  100. package/dist/utils/desktop-bridge-client.js +2 -0
  101. package/dist/utils/direct-output-policy.d.ts +3 -0
  102. package/dist/utils/direct-output-policy.js +76 -0
  103. package/dist/utils/durable-json.d.ts +57 -0
  104. package/dist/utils/durable-json.js +319 -0
  105. package/dist/utils/fastAgentRouter.d.ts +5 -6
  106. package/dist/utils/fastAgentRouter.js +17 -25
  107. package/dist/utils/files.js +21 -13
  108. package/dist/utils/frontend-preview-service.d.ts +76 -0
  109. package/dist/utils/frontend-preview-service.js +482 -0
  110. package/dist/utils/goaEvents.d.ts +0 -4
  111. package/dist/utils/goaEvents.js +5 -2
  112. package/dist/utils/logger.d.ts +2 -1
  113. package/dist/utils/logger.js +27 -20
  114. package/dist/utils/mcp-context-client.d.ts +24 -0
  115. package/dist/utils/mcp-context-client.js +58 -0
  116. package/dist/utils/model-governance.d.ts +18 -0
  117. package/dist/utils/model-governance.js +75 -0
  118. package/dist/utils/model-transport-service.d.ts +44 -0
  119. package/dist/utils/model-transport-service.js +111 -0
  120. package/dist/utils/mutation-journal.d.ts +42 -0
  121. package/dist/utils/mutation-journal.js +387 -0
  122. package/dist/utils/network-policy.d.ts +24 -0
  123. package/dist/utils/network-policy.js +202 -0
  124. package/dist/utils/operator-client.d.ts +45 -0
  125. package/dist/utils/operator-client.js +159 -0
  126. package/dist/utils/post-write-validator.js +7 -9
  127. package/dist/utils/preview-screenshot-adapter.d.ts +13 -0
  128. package/dist/utils/preview-screenshot-adapter.js +36 -0
  129. package/dist/utils/process-policy.d.ts +23 -0
  130. package/dist/utils/process-policy.js +284 -0
  131. package/dist/utils/project-memory.d.ts +13 -3
  132. package/dist/utils/project-memory.js +70 -17
  133. package/dist/utils/prompt-workspace-resolver.d.ts +14 -0
  134. package/dist/utils/prompt-workspace-resolver.js +64 -0
  135. package/dist/utils/release-install.d.ts +11 -0
  136. package/dist/utils/release-install.js +155 -0
  137. package/dist/utils/release-policy.d.ts +64 -0
  138. package/dist/utils/release-policy.js +293 -0
  139. package/dist/utils/requestIntent.js +22 -3
  140. package/dist/utils/runtime-capability.d.ts +8 -0
  141. package/dist/utils/runtime-capability.js +16 -0
  142. package/dist/utils/secret-policy.d.ts +20 -0
  143. package/dist/utils/secret-policy.js +160 -0
  144. package/dist/utils/session.d.ts +17 -3
  145. package/dist/utils/session.js +354 -110
  146. package/dist/utils/tool-approval-service.d.ts +17 -0
  147. package/dist/utils/tool-approval-service.js +78 -0
  148. package/dist/utils/tool-capability-providers.d.ts +57 -0
  149. package/dist/utils/tool-capability-providers.js +57 -0
  150. package/dist/utils/tools.d.ts +2 -23
  151. package/dist/utils/tools.js +216 -508
  152. package/dist/utils/update-policy.d.ts +18 -0
  153. package/dist/utils/update-policy.js +137 -0
  154. package/dist/utils/v3-agent-client.d.ts +40 -0
  155. package/dist/utils/v3-agent-client.js +165 -0
  156. package/dist/utils/v3-workspace-service.d.ts +28 -0
  157. package/dist/utils/v3-workspace-service.js +279 -0
  158. package/dist/utils/vigflow-client.d.ts +95 -0
  159. package/dist/utils/vigflow-client.js +105 -0
  160. package/dist/utils/workspace-boundary.d.ts +45 -0
  161. package/dist/utils/workspace-boundary.js +377 -0
  162. package/dist/utils/workspace-brain-service.d.ts +2 -0
  163. package/dist/utils/workspace-brain-service.js +11 -1
  164. package/dist/utils/workspace-cache.js +37 -15
  165. package/dist/utils/workspace-stream.js +35 -10
  166. package/install.ps1 +85 -142
  167. package/install.sh +75 -80
  168. package/package.json +43 -9
  169. package/release-policy.json +43 -0
  170. package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +1 -1
  171. package/scripts/release/generate-completions.mjs +24 -0
  172. package/scripts/release/install-release.mjs +10 -0
  173. package/scripts/release/publish-cli-release.mjs +116 -0
  174. package/scripts/release/publish-cli-release.sh +2 -70
  175. package/scripts/release/resolve-release-manifest.mjs +26 -0
  176. package/scripts/release/test-balanced-model-live.sh +42 -0
  177. package/scripts/release/validate-live-service-gates.sh +90 -0
  178. package/scripts/release/validate-no-go-gates.sh +135 -174
  179. package/scripts/release/verify-runtime-consistency.mjs +4 -1
  180. package/dist/commands/index.d.ts +0 -12
  181. package/dist/commands/index.js +0 -194
  182. package/dist/commands/update.d.ts +0 -9
  183. package/dist/commands/update.js +0 -301
  184. package/dist/utils/templateInstantPath.d.ts +0 -18
  185. package/dist/utils/templateInstantPath.js +0 -228
@@ -5,26 +5,34 @@ import * as path from 'path';
5
5
  import * as readline from 'readline';
6
6
  import { randomUUID } from 'crypto';
7
7
  import { createSpinner } from '../utils/logger.js';
8
- import { APIClient, CLIError, classifyError, formatCLIError, sanitizeUserFacingErrorText, sanitizeUserFacingPathText, propagateError, VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE } from '../utils/api.js';
8
+ import { CLIError, classifyError, formatCLIError, sanitizeUserFacingErrorText, sanitizeUserFacingPathText, propagateError, VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE } from '../utils/api.js';
9
+ import { createAPIClient } from '../utils/api-client-factory.js';
9
10
  import { AgenticTools, robustifyStreamResponse } from '../utils/tools.js';
10
- import { SessionManager } from '../utils/session.js';
11
+ import { SessionManager, assertAgentStatusTransition } from '../utils/session.js';
11
12
  import { BridgeClient, getBridgeClient } from '../utils/bridge-client.js';
12
13
  import { WorkspaceWatcher } from '../utils/workspace-stream.js';
13
14
  import { TaskDisplay } from '../utils/task-display.js';
14
15
  import { ProjectMemoryService } from '../utils/project-memory.js';
15
16
  import { WorkspaceBrainService } from '../utils/workspace-brain-service.js';
17
+ import { assembleContextState, contextLayer } from '../utils/context-state.js';
16
18
  import { buildPersonaOverlay, normalizePersonaMode } from '../utils/persona.js';
17
19
  import { runAgentSessionMenu, shouldShowAgentSessionMenu } from './agent-session-menu.js';
18
20
  import { V4Command } from './v4.js';
19
21
  import { renderDynamicHelp } from '../utils/command-menu.js';
20
- import { createLiveOutcome, evaluateExecutorSuccess, handleTaskEvent, isSubstantiveAgentAnswer, isToolEvidenceStubAnswer, normalizeAgentAnswerContent, noteAnalysisToolUse, } from '../utils/agentRunOutcome.js';
22
+ import { resolvePromptWorkspace } from '../utils/prompt-workspace-resolver.js';
23
+ import { isDirectModeFollowUpQuestion, sanitizeDirectModeOutput, stripHiddenThoughtBlocks } from '../utils/direct-output-policy.js';
24
+ import { createLiveOutcome, evaluateExecutorSuccess, handleRunCompleteEvent, handleTaskEvent, isSubstantiveAgentAnswer, isToolEvidenceStubAnswer, normalizeAgentAnswerContent, noteAnalysisToolUse, } from '../utils/agentRunOutcome.js';
21
25
  import { looksLikeMarkdownReport, renderMarkdownToTerminal, summarizeMarkdownReport, } from '../utils/terminalMarkdown.js';
22
26
  import { emitDeckEvent, isDeckModeEnabled } from '../utils/deckEvents.js';
23
27
  import { formatGoaSystemGrounding, splitGoaContextFromInput, } from '../utils/goaEvents.js';
24
- 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';
28
+ import { inferAgentTaskTypeWithContext, resolvePlannerAgentTimeoutMs, resolveWorkflowType, shouldSkipAnalysisRescue, taskRequiresWorkspaceChanges as promptRequiresWorkspaceChanges, taskRequiresWorkspaceChangesWithContext, buildExecutionHints, isAgentContinuePrompt, isAgentRetryPrompt, isBuiltContinuePrompt, isBuiltRetryPrompt, isBuiltWriteConfirmationPrompt, isConfirmationFollowUp, isWritePermissionGrant, } from '../utils/requestIntent.js';
25
29
  import { resolveAgentRoute } from '../utils/agentRoute.js';
26
- import { runTemplateInstantPath } from '../utils/templateInstantPath.js';
27
30
  import { isV3StreamKeepaliveEvent } from '../utils/v3-stream-events.js';
31
+ import { resolveAutoApproval } from '../utils/process-policy.js';
32
+ import { ChatToolCallParser } from '../utils/chat-tool-call-parser.js';
33
+ import { ChatPromptPolicy } from '../utils/chat-prompt-policy.js';
34
+ import { AgentStreamState } from '../utils/agent-stream-state.js';
35
+ import { CliCommandError, commandFailure, commandNameFromArgv, failureEnvelope, normalizeCommandError, successEnvelope, } from '../utils/command-contract.js';
28
36
  // Stream health policy (corrected 2026-07-04 after TestFarm incident with
29
37
  // Gideon Lenz / C:\vigthoria\Apps\monopoly): agentic tasks have no
30
38
  // predictable duration, so there is intentionally NO overall wall-clock
@@ -63,7 +71,46 @@ const DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS = (() => {
63
71
  })();
64
72
  /** Block-write agent JSON to VIGTHORIA_AGENT_OUTPUT (Windows KVM fsync path). */
65
73
  function emitAgentJsonOutput(payload) {
66
- const text = `${JSON.stringify(payload, null, 2)}\n`;
74
+ const command = commandNameFromArgv(process.argv);
75
+ const record = payload && typeof payload === 'object' && !Array.isArray(payload)
76
+ ? { ...payload }
77
+ : { value: payload };
78
+ const succeeded = record.success !== false;
79
+ delete record.success;
80
+ let envelope;
81
+ if (succeeded) {
82
+ const metadata = record.metadata && typeof record.metadata === 'object' && !Array.isArray(record.metadata)
83
+ ? record.metadata
84
+ : {};
85
+ const warnings = Array.isArray(record.warnings)
86
+ ? record.warnings.filter((warning) => typeof warning === 'string')
87
+ : [];
88
+ delete record.metadata;
89
+ delete record.warnings;
90
+ envelope = successEnvelope(command, record, metadata, warnings);
91
+ }
92
+ else {
93
+ const message = typeof record.error === 'string' && record.error.trim()
94
+ ? record.error
95
+ : 'Agent execution failed.';
96
+ const category = record.errorCategory === 'auth'
97
+ ? 'authentication'
98
+ : record.errorCategory === 'network' || record.errorCategory === 'timeout'
99
+ ? 'network'
100
+ : 'execution';
101
+ const partialMutation = record.partialMutation === true || record.partialMutation === 'unknown' || record.partial === true
102
+ ? 'unknown'
103
+ : false;
104
+ delete record.error;
105
+ delete record.errorCategory;
106
+ envelope = failureEnvelope(command, normalizeCommandError(new CliCommandError(message, {
107
+ code: command === 'operator' ? 'OPERATOR_EXECUTION_FAILED' : command === 'agent' ? 'AGENT_EXECUTION_FAILED' : 'CHAT_EXECUTION_FAILED',
108
+ category: partialMutation === 'unknown' ? 'partial_mutation' : category,
109
+ partialMutation,
110
+ details: record,
111
+ })));
112
+ }
113
+ const text = `${JSON.stringify(envelope, null, 2)}\n`;
67
114
  console.log(text.trimEnd());
68
115
  const outPath = process.env.VIGTHORIA_AGENT_OUTPUT?.trim();
69
116
  if (!outPath) {
@@ -100,6 +147,8 @@ export class ChatCommand {
100
147
  modelExplicitlySelected = false;
101
148
  autoApprove = false;
102
149
  personaOverride = null;
150
+ toolCallParser = new ChatToolCallParser();
151
+ promptPolicy;
103
152
  // Phase 5: Agent quality gate — track tool usage for evidence thresholds
104
153
  agentToolEvidence = { discovery: 0, mutation: 0, searchFailed: 0 };
105
154
  operatorMode = false;
@@ -109,12 +158,7 @@ export class ChatCommand {
109
158
  modelGovernanceFallback = null;
110
159
  retryPromptSignature = null;
111
160
  retryPromptStreak = 0;
112
- v3SuppressThinkingStream = false;
113
- v3StreamedAnswerBuffer = '';
114
- v3StreamedAnswerDisplayed = false;
115
- v3SeenToolCalls = new Set();
116
- v3SeenToolResults = new Set();
117
- v3StartSeen = false;
161
+ streamState;
118
162
  lastAgentRoute = null;
119
163
  pendingGoaContext = null;
120
164
  // Last completed Agent run — used by /retry, /continue, and the final summary block.
@@ -373,16 +417,6 @@ export class ChatCommand {
373
417
  routeReason: 'dispatcher-eligible-agent-default',
374
418
  };
375
419
  }
376
- if (this.lastAgentRoute?.path === 'template-instant') {
377
- return {
378
- selectedModel: 'agent',
379
- explicitModel: false,
380
- heavyTask,
381
- cloudEligible,
382
- cloudSelected: false,
383
- routeReason: `template-instant [${this.lastAgentRoute.source}]`,
384
- };
385
- }
386
420
  if (requiresV3Workflow) {
387
421
  return {
388
422
  selectedModel: 'agent',
@@ -457,131 +491,37 @@ export class ChatCommand {
457
491
  return buildPersonaOverlay(this.getActivePersonaMode(), this.getLastUserPrompt());
458
492
  }
459
493
  isDiagnosticPrompt(prompt) {
460
- if (promptRequiresWorkspaceChanges(prompt))
461
- return false;
462
- return /(startup|start up|won'?t start|doesn'?t start|crash|crashes|error|errors|failing|fails|issue|issues|bug|bugs|diagnos|debug|runtime|log|logs|exception|traceback|stack trace|yaml|blocking|blocker)/i.test(prompt);
494
+ return this.promptPolicy.isDiagnostic(prompt);
463
495
  }
464
496
  /**
465
497
  * Returns true when the prompt is a simple lookup / analysis / read-only
466
498
  * question — these should use analysis_only workflow, not full_autonomy.
467
499
  */
468
- isAnalysisLookupPrompt(prompt) {
469
- if (promptRequiresWorkspaceChanges(prompt))
470
- return false;
471
- const trimmed = prompt.trim();
472
- if (/^(what|which|where|how many|who|find|list|show|check|inspect|analyze|analyse|audit|explain|describe|summarize|summarise|review|overview|count|read|look at|tell me|locate|search for|does .* exist)/i.test(trimmed)) {
473
- return true;
474
- }
475
- // Mid-conversation accountability / lookup questions (Gideon regression)
476
- return /\b(where\s+(?:is|are)|what\s+(?:is|are|have|did|was|were)|how\s+(?:do|does|is|are)|what\s+(?:have\s+)?you\s+(?:done|changed|built)|what\s+(?:is|was)\s+(?:your|the)\s+answer)\b/i.test(trimmed);
477
- }
478
- extractExplicitLocalPath(prompt) {
479
- // Try to extract Windows paths (C:\ D:\ etc.)
480
- // Match drive letter followed by colon and path
481
- const windowsMatch = prompt.match(/([A-Za-z]:[\\\/][^\s<>"|?*]*)/);
482
- if (windowsMatch) {
483
- const candidatePath = windowsMatch[1].replace(/\\/g, '\\').trim();
484
- try {
485
- const resolved = require('path').resolve(candidatePath);
486
- const fs = require('fs');
487
- if (fs.existsSync(resolved)) {
488
- return resolved;
489
- }
490
- }
491
- catch {
492
- // Path resolution failed
493
- }
494
- }
495
- // Try to extract Unix paths: / followed by any characters until we hit quotes, angle brackets, pipes, or sentence boundaries
496
- // Supports spaces, special chars, camelCase, etc. (anything a filesystem path might have)
497
- if (!prompt.includes('http://') && !prompt.includes('https://') && !prompt.includes('ftp://')) {
498
- // Match / and everything after it until we hit delimiters or common sentence words
499
- const unixMatch = prompt.match(/(\/(?:[^\s<>"|?*]|\s+(?![^\s<>"|?*]))*)/);
500
- if (unixMatch) {
501
- let candidatePath = unixMatch[1].trim();
502
- // Remove trailing common English words that aren't part of path
503
- candidatePath = candidatePath.replace(/\s+(and|or|at|in|the|to|for|with|from|by|on)$/i, '').trim();
504
- // Remove trailing punctuation
505
- candidatePath = candidatePath.replace(/[.,;!?:)\]]*$/, '').trim();
506
- if (candidatePath.length > 1) {
507
- try {
508
- const resolved = require('path').resolve(candidatePath);
509
- const fs = require('fs');
510
- if (fs.existsSync(resolved)) {
511
- return resolved;
512
- }
513
- }
514
- catch {
515
- // Path resolution failed
516
- }
517
- }
518
- }
519
- }
520
- return null;
521
- }
522
- isUnscopedPromptPathOverrideAllowed() {
523
- return /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_ALLOW_UNSCOPED_PROMPT_PATHS || ''));
524
- }
525
- isPathWithinRoot(candidatePath, rootPath) {
526
- const candidate = path.resolve(candidatePath);
527
- const root = path.resolve(rootPath);
528
- const rel = path.relative(root, candidate);
529
- return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
530
- }
531
- getPromptPathAllowedRoots(baseWorkspace) {
532
- const roots = new Set();
533
- const addRoot = (rawValue) => {
534
- const value = String(rawValue || '').trim();
535
- if (!value)
536
- return;
537
- const resolved = path.resolve(value);
538
- try {
539
- if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
540
- roots.add(resolved);
541
- }
542
- }
543
- catch {
544
- // ignore invalid or unreadable roots
545
- }
546
- };
547
- addRoot(baseWorkspace);
548
- addRoot(this.currentProjectPath);
549
- addRoot(process.cwd());
550
- addRoot(this.config.get('project')?.rootPath || null);
551
- const envRootsRaw = String(process.env.VIGTHORIA_ALLOWED_WORKSPACE_ROOTS || '').trim();
552
- if (envRootsRaw) {
553
- for (const entry of envRootsRaw.split(path.delimiter)) {
554
- addRoot(entry);
555
- }
556
- }
557
- return Array.from(roots);
558
- }
559
500
  resolvePromptWorkspacePath(prompt, baseWorkspace) {
560
- const explicitPath = this.extractExplicitLocalPath(prompt);
561
- if (!explicitPath) {
562
- return null;
563
- }
564
- const candidate = path.resolve(explicitPath);
565
- if (this.isUnscopedPromptPathOverrideAllowed()) {
566
- return candidate;
567
- }
568
- const allowedRoots = this.getPromptPathAllowedRoots(baseWorkspace);
569
- const isAllowed = allowedRoots.some((root) => this.isPathWithinRoot(candidate, root));
570
- if (isAllowed) {
571
- return candidate;
572
- }
573
- if (!this.jsonOutput) {
574
- console.log(chalk.yellow(`Ignoring path outside allowed workspace roots: ${candidate}`));
575
- if (allowedRoots.length > 0) {
576
- const displayRoots = allowedRoots.map((root) => root.replace(/\\/g, '/')).join(', ');
501
+ const envRoots = String(process.env.VIGTHORIA_ALLOWED_WORKSPACE_ROOTS || '')
502
+ .split(path.delimiter)
503
+ .map((entry) => entry.trim())
504
+ .filter(Boolean);
505
+ const result = resolvePromptWorkspace(prompt, {
506
+ baseWorkspace,
507
+ currentProjectPath: this.currentProjectPath,
508
+ cwd: process.cwd(),
509
+ configuredProjectRoot: this.config.get('project')?.rootPath || null,
510
+ additionalRoots: envRoots,
511
+ allowUnscoped: /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_ALLOW_UNSCOPED_PROMPT_PATHS || '')),
512
+ });
513
+ if (result.rejectedPath && !this.jsonOutput) {
514
+ console.log(chalk.yellow(`Ignoring path outside allowed workspace roots: ${result.rejectedPath}`));
515
+ if (result.allowedRoots.length > 0) {
516
+ const displayRoots = result.allowedRoots.map((root) => root.replace(/\\/g, '/')).join(', ');
577
517
  console.log(chalk.gray(`Allowed roots: ${displayRoots}`));
578
518
  }
579
519
  console.log(chalk.gray('To allow unrestricted prompt path overrides, set VIGTHORIA_ALLOW_UNSCOPED_PROMPT_PATHS=1.'));
580
520
  }
581
- return null;
521
+ return result.resolvedPath;
582
522
  }
583
523
  isBrowserTaskPrompt(prompt) {
584
- return /(\bbrowser\b|\bchrome\b|\bdevtools\b|\bconsole\b|\bdom\b|network tab|network request|frontend runtime|client-side|client side|rendering|page load|\bwebsocket\b|ui bug|inspect element)/i.test(prompt);
524
+ return this.promptPolicy.isBrowserTask(prompt);
585
525
  }
586
526
  /**
587
527
  * Returns true when a prompt can be answered directly without the full
@@ -590,28 +530,7 @@ export class ChatCommand {
590
530
  * file scanning, or infrastructure changes.
591
531
  */
592
532
  isOperatorDirectAnswerable(prompt) {
593
- const trimmed = prompt.trim();
594
- // Infrastructure status pings are never repo-grounded analysis.
595
- if (/^(status(\s+check)?|health(\s+check)?|ping)$/i.test(trimmed)) {
596
- return true;
597
- }
598
- // ── Exclusion guard: any prompt that references code artefacts,
599
- // file paths, or analysis verbs MUST go through a tool-backed path
600
- // (BMAD or agent loop), never the toolless direct-answer shortcut.
601
- const repoGrounded = /\b(src\/|\.js\b|\.ts\b|\.py\b|\.jsx\b|\.tsx\b|\.css\b|\.html\b|\.json\b|\.yaml\b|\.yml\b)/i.test(trimmed)
602
- || /\b(file|folder|directory|module|class|function|method|variable|handler|listener|binding|conflict|bug|issue|error)\b/i.test(trimmed)
603
- || /\b(inspect|analyze|analyse|audit|review|find|diagnose|debug|trace|compare|diff|check|investigate)\b/i.test(trimmed)
604
- || /\b(Camera|InputManager|keydown|KeyS|KeyA|KeyW|stopPropagation|addEventListener|handleKeyDown)\b/.test(trimmed)
605
- || /\/[a-zA-Z]/.test(trimmed) // slash-prefixed paths
606
- || /[A-Z][a-z]+\.[a-z_][a-zA-Z]+/.test(trimmed) // ClassName.methodName
607
- || /\b(key\s*code|keydown|keyup|key\s*bind|event\s*listener|event\s*handler|addEventListener|handleKey|onKey)\b/i.test(trimmed);
608
- if (repoGrounded)
609
- return false;
610
- // Only truly trivial prompts are direct-answerable: short echo/smoke
611
- // prompts and factual questions with no code or repo reference.
612
- const isTrivialEcho = trimmed.length < 120
613
- && /^(reply with|say|echo|respond with|return|ping|hello|hi|test|status|what is \d|how much is)\b/i.test(trimmed);
614
- return isTrivialEcho;
533
+ return this.promptPolicy.isOperatorDirectAnswerable(prompt);
615
534
  }
616
535
  /**
617
536
  * Returns true when the prompt references code artefacts, file paths,
@@ -620,51 +539,10 @@ export class ChatCommand {
620
539
  * instead of the toolless BMAD/direct-answer path.
621
540
  */
622
541
  isRepoGroundedPrompt(prompt) {
623
- const trimmed = prompt.trim();
624
- return /\b(src\/|\.js\b|\.ts\b|\.py\b|\.jsx\b|\.tsx\b|\.css\b|\.html\b|\.json\b)/i.test(trimmed)
625
- || /\b(file|folder|module|class|function|method|handler|listener|binding|conflict|bug|error)\b/i.test(trimmed)
626
- || /\b(inspect|analyze|analyse|audit|review|find|diagnose|debug|trace|compare|diff|check|investigate)\b/i.test(trimmed)
627
- || /\/[a-zA-Z]/.test(trimmed)
628
- || /[A-Z][a-z]+\.[a-z_][a-zA-Z]+/.test(trimmed) // ClassName.methodName patterns
629
- || /\b(key\s*code|keydown|keyup|key\s*bind|event\s*listener|event\s*handler|addEventListener|handleKey|onKey)\b/i.test(trimmed);
630
- }
631
- inferAgentTaskType(prompt) {
632
- if (this.isDiagnosticPrompt(prompt))
633
- return 'debugging';
634
- if (this.isAnalysisLookupPrompt(prompt))
635
- return 'analysis';
636
- return sharedInferAgentTaskType(prompt);
637
- }
638
- buildTaskShapingInstructions(prompt) {
639
- const instructions = [];
640
- const runtime = this.getRuntimeEnvironmentContext();
641
- // Platform-aware routing hints
642
- if (runtime.platform === 'windows') {
643
- instructions.push('Platform: Windows. Use list_dir, glob, read_file, and the grep tool for searching.', 'The grep tool handles Windows automatically — do not use bash to call grep, findstr, or Select-String manually.', 'Do not use bash for Unix commands (cat, head, tail, awk, sed, wc).', 'Use read_file to inspect file contents instead of shell commands.', 'All file paths use forward slashes internally.');
644
- }
645
- else if (runtime.platform === 'macos') {
646
- instructions.push('Platform: macOS. Prefer list_dir, grep, and read_file tools for deterministic results.', 'Use workspace-relative paths and verify file existence before claiming a file is missing.');
647
- }
648
- else if (runtime.platform === 'linux') {
649
- instructions.push('Platform: Linux. Prefer list_dir, grep, and read_file for grounded repository inspection.', 'Use exact file paths from tool output when reporting findings or fixes.');
650
- }
651
- if (this.isAnalysisLookupPrompt(prompt)) {
652
- instructions.push('Read-only analysis mode is active.', 'Inspect the workspace with tools and produce a grounded report of findings, gaps, and blockers.', 'Do not create, modify, or delete files unless the user explicitly asked for changes.');
653
- }
654
- if (this.isDiagnosticPrompt(prompt)) {
655
- instructions.push('Diagnostic mode is active.', 'Treat this as a debugging task, not a generic code review or feature request.', 'Start with concrete evidence: logs, runtime errors, config, launch files, and exact symbol references.', 'If log files exist, inspect them before proposing fixes.', 'Do not claim a file, definition, asset, or symbol is missing until you verify that with tools.', 'If a prior diagnosis mentioned a missing symbol or YAML entry, re-check the actual files before repeating it.', 'Prefer grep plus read_file around the exact references involved in the failure.', 'Separate your reasoning into: Evidence, Confirmed Cause, and Remaining Hypotheses.', 'Do not suggest speculative fixes when the current evidence contradicts them.', 'CRITICAL GROUNDING RULE: Every key name, variable name, symbol, or identifier you mention in your final answer MUST appear verbatim in the tool output you received. If a key/symbol does NOT appear in tool output, you MUST NOT mention it as involved in any conflict or issue.', 'CROSS-FILE ATTRIBUTION: When reporting conflicts between two files, a key/symbol is conflicting ONLY if it appears in BOTH files. Read each file carefully and list only the exact keys that appear in the relevant handler/function of EACH file. Do not assume that because one file handles a key, the other file does too.', 'When reporting conflicts between files, cite the exact file name, line number, and the exact string/key from the tool output. Do not paraphrase or substitute key names.', 'Before concluding, re-check: (1) does every key/symbol in my answer actually appear in the evidence I gathered? (2) for each claimed conflict, did I verify the key appears in BOTH files? If not, correct your answer.');
656
- }
657
- if (this.isBrowserTaskPrompt(prompt)) {
658
- instructions.push('Browser-debug mode is active.', 'Prefer concrete browser evidence such as console errors, network failures, DOM state, and websocket behavior.', 'Use the DevTools Bridge only when the user has explicitly enabled it for this run; otherwise ask for permission before requiring local browser tooling.');
659
- }
660
- if (instructions.length === 0) {
661
- return '';
662
- }
663
- return instructions.join('\n');
542
+ return this.promptPolicy.isRepoGrounded(prompt);
664
543
  }
665
544
  buildExecutionPrompt(prompt) {
666
- const shaping = this.buildTaskShapingInstructions(prompt);
667
- return shaping ? `${prompt}\n\n${shaping}` : prompt;
545
+ return this.promptPolicy.buildExecutionPrompt(prompt);
668
546
  }
669
547
  isProjectBrainRuntimeDisabled() {
670
548
  return /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_NO_BRAIN || process.env.VIGTHORIA_BRAIN_DISABLED || ''));
@@ -689,7 +567,11 @@ export class ChatCommand {
689
567
  }
690
568
  }
691
569
  canPromptForWorkspaceIndex() {
692
- if (this.jsonOutput || !process.stdin.isTTY || !process.stdout.isTTY) {
570
+ // A direct `--prompt` invocation is a bounded automation surface even when
571
+ // wrapped in `script`/ConPTY and therefore appears to have a TTY. Likewise,
572
+ // --auto-approve cannot answer an unrelated optional indexing question.
573
+ // Never block these modes on an interactive Brain opt-in prompt.
574
+ if (this.jsonOutput || this.directPromptMode || this.autoApprove || !process.stdin.isTTY || !process.stdout.isTTY) {
693
575
  return false;
694
576
  }
695
577
  return !/^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_NO_INDEX_PROMPT || ''));
@@ -789,22 +671,40 @@ export class ChatCommand {
789
671
  agentRuntime: this.getRuntimeEnvironmentContext(),
790
672
  };
791
673
  const brainContext = this.buildProjectBrainRuntimeContext(prompt, this.operatorMode ? 'vigthoria-cli.operator' : this.agentMode ? 'vigthoria-cli.agent' : 'vigthoria-cli.chat');
792
- if (brainContext) {
793
- runtimeContext.vigthoriaBrain = brainContext;
794
- if (typeof brainContext.context === 'string' && brainContext.context.trim()) {
795
- runtimeContext.projectMemory = brainContext.context;
796
- }
797
- }
798
674
  const workspaceBrainContext = await this.buildWorkspaceBrainRuntimeContext(prompt);
799
675
  if (workspaceBrainContext.workspaceBrain) {
800
676
  runtimeContext.workspaceBrain = workspaceBrainContext.workspaceBrain;
801
677
  }
802
- if (typeof workspaceBrainContext.codebaseContext === 'string' && workspaceBrainContext.codebaseContext.trim()) {
803
- runtimeContext.codebaseContext = workspaceBrainContext.codebaseContext;
804
- }
805
- if (typeof workspaceBrainContext.accountBrainContext === 'string' && workspaceBrainContext.accountBrainContext.trim()) {
806
- runtimeContext.accountBrainContext = workspaceBrainContext.accountBrainContext;
807
- }
678
+ const contextState = assembleContextState({
679
+ ...(this.workflowTarget ? { 'workflow-memory': { metadata: { workflowTarget: this.workflowTarget } } } : {}),
680
+ 'session-memory': { content: this.sessionManager.buildMemoryContext(this.currentSession) },
681
+ 'project-memory': {
682
+ content: typeof brainContext?.context === 'string' ? brainContext.context : '',
683
+ metadata: brainContext ? { ...brainContext, context: undefined } : undefined,
684
+ },
685
+ 'codebase-index': {
686
+ content: typeof workspaceBrainContext.codebaseContext === 'string' ? workspaceBrainContext.codebaseContext : '',
687
+ metadata: workspaceBrainContext.workspaceBrain,
688
+ },
689
+ 'account-brain': {
690
+ content: typeof workspaceBrainContext.accountBrainContext === 'string' ? workspaceBrainContext.accountBrainContext : '',
691
+ },
692
+ 'workspace-hashes': { metadata: { role: 'change-priority-only', semanticAuthority: false } },
693
+ });
694
+ runtimeContext.contextState = contextState;
695
+ // Explicit server-compatibility adapters. These values are derived from
696
+ // contextState and have no independent persistence owner.
697
+ const projectLayer = contextLayer(contextState, 'project-memory');
698
+ const codebaseLayer = contextLayer(contextState, 'codebase-index');
699
+ const accountLayer = contextLayer(contextState, 'account-brain');
700
+ if (projectLayer) {
701
+ runtimeContext.projectMemory = projectLayer.content || '';
702
+ runtimeContext.vigthoriaBrain = { ...(brainContext || {}), context: projectLayer.content || '' };
703
+ }
704
+ if (codebaseLayer?.content)
705
+ runtimeContext.codebaseContext = codebaseLayer.content;
706
+ if (accountLayer?.content)
707
+ runtimeContext.accountBrainContext = accountLayer.content;
808
708
  if (!this.isBrowserTaskPrompt(prompt)) {
809
709
  return runtimeContext;
810
710
  }
@@ -836,12 +736,6 @@ export class ChatCommand {
836
736
  devtoolsBridgeEndpoint: bridgeStatus.endpoint,
837
737
  };
838
738
  }
839
- v3IterationCount = 0;
840
- v3ToolCallCount = 0;
841
- v3LastActivity = Date.now();
842
- v3IdleWatchInterval = null;
843
- v3IdleNoticeShown = false;
844
- v3StreamingStarted = false;
845
739
  /**
846
740
  * Strip server-internal path prefixes from tool output strings.
847
741
  * Prevents exposing paths like /var/www/V3-Code-Agent/... to end users.
@@ -849,56 +743,13 @@ export class ChatCommand {
849
743
  sanitizeServerPath(text) {
850
744
  if (!text)
851
745
  return text;
852
- return sanitizeUserFacingPathText(this.stripHiddenThoughtBlocks(text));
853
- }
854
- stripHiddenThoughtBlocks(text) {
855
- if (!text)
856
- return text;
857
- return text
858
- .replace(/<\|mask_start\|>[\s\S]*?<\|mask_end\|>/g, '')
859
- .replace(/<think>[\s\S]*?<\/redacted_thinking>/gi, '')
860
- .replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
861
- .replace(/<\/redacted_thinking>/gi, '')
862
- .replace(/<\/thinking>/gi, '')
863
- .replace(/<\|(?:mask_start|mask_end)\|>/g, '')
864
- .trim();
746
+ return sanitizeUserFacingPathText(stripHiddenThoughtBlocks(text));
865
747
  }
866
748
  startV3IdleWatch(spinner) {
867
- this.stopV3IdleWatch();
868
- this.v3IdleNoticeShown = false;
869
- if (this.jsonOutput || !spinner) {
870
- return;
871
- }
872
- this.v3IdleWatchInterval = setInterval(() => {
873
- const idleMs = Date.now() - this.v3LastActivity;
874
- if (idleMs < 15_000) {
875
- // Activity resumed — re-arm the notice so a later stall is reported too.
876
- this.v3IdleNoticeShown = false;
877
- return;
878
- }
879
- if (this.v3IdleNoticeShown) {
880
- return;
881
- }
882
- this.v3IdleNoticeShown = true;
883
- if (spinner.isSpinning) {
884
- spinner.stop();
885
- }
886
- const seconds = Math.round(idleMs / 1000);
887
- const idleTimeoutSec = Math.round(DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS / 1000);
888
- process.stderr.write(chalk.yellow(` [Wait] Model still working (${seconds}s) — this can legitimately take a while for large tasks. ` +
889
- (idleTimeoutSec > 0
890
- ? `Only complete silence (~${idleTimeoutSec}s with zero bytes received) will trigger an auto-recover/retry — active progress is never interrupted.\n`
891
- : `\n`)));
892
- spinner.start();
893
- spinner.text = 'Waiting for model response...';
894
- }, 5_000);
749
+ this.streamState.startIdleWatch(spinner);
895
750
  }
896
751
  stopV3IdleWatch() {
897
- if (this.v3IdleWatchInterval) {
898
- clearInterval(this.v3IdleWatchInterval);
899
- this.v3IdleWatchInterval = null;
900
- }
901
- this.v3IdleNoticeShown = false;
752
+ this.streamState.stopIdleWatch();
902
753
  }
903
754
  describeV3AgentTool(toolName) {
904
755
  const normalized = String(toolName || '').toLowerCase();
@@ -965,7 +816,7 @@ export class ChatCommand {
965
816
  ? (event.body ?? event.stream ?? event.response ?? event)
966
817
  : event;
967
818
  for await (const chunk of robustifyStreamResponse(source)) {
968
- this.v3LastActivity = Date.now();
819
+ this.streamState.noteActivity();
969
820
  if (chunk.type === 'error') {
970
821
  if (spinner.isSpinning)
971
822
  spinner.stop();
@@ -992,8 +843,8 @@ export class ChatCommand {
992
843
  if (!safeText) {
993
844
  return;
994
845
  }
995
- if (!this.v3StreamingStarted) {
996
- this.v3StreamingStarted = true;
846
+ if (!this.streamState.streamingStarted) {
847
+ this.streamState.streamingStarted = true;
997
848
  spinner.stop();
998
849
  if (!this.directPromptMode) {
999
850
  console.log();
@@ -1002,27 +853,27 @@ export class ChatCommand {
1002
853
  else {
1003
854
  spinner.stop();
1004
855
  }
1005
- this.v3StreamedAnswerBuffer += safeText;
1006
- spinner.text = looksLikeMarkdownReport(this.v3StreamedAnswerBuffer)
856
+ this.streamState.streamedAnswerBuffer += safeText;
857
+ spinner.text = looksLikeMarkdownReport(this.streamState.streamedAnswerBuffer)
1007
858
  ? 'Writing analysis report...'
1008
859
  : 'Writing response...';
1009
860
  }
1010
861
  displayV3StreamedAnswer() {
1011
- if (this.jsonOutput || this.v3StreamedAnswerDisplayed) {
862
+ if (this.jsonOutput || this.streamState.streamedAnswerDisplayed) {
1012
863
  return;
1013
864
  }
1014
- const content = this.v3StreamedAnswerBuffer.trim();
865
+ const content = this.streamState.streamedAnswerBuffer.trim();
1015
866
  if (!isSubstantiveAgentAnswer(content)) {
1016
867
  return;
1017
868
  }
1018
869
  emitDeckEvent({ type: 'report_ready', markdown: content });
1019
870
  if (isDeckModeEnabled()) {
1020
- this.v3StreamedAnswerDisplayed = true;
871
+ this.streamState.streamedAnswerDisplayed = true;
1021
872
  return;
1022
873
  }
1023
874
  console.log('');
1024
875
  console.log(renderMarkdownToTerminal(content, this.getTerminalContentWidth()));
1025
- this.v3StreamedAnswerDisplayed = true;
876
+ this.streamState.streamedAnswerDisplayed = true;
1026
877
  }
1027
878
  renderAgentAnswerForTerminal(content) {
1028
879
  const normalized = String(content || '').trim();
@@ -1035,37 +886,7 @@ export class ChatCommand {
1035
886
  return Math.max(60, Math.min(cols - 2, 100));
1036
887
  }
1037
888
  sanitizeV3VisibleStreamText(text) {
1038
- let output = String(text || '');
1039
- if (!output)
1040
- return '';
1041
- output = output.replace(/<\/think>/gi, '</thinking>');
1042
- output = output.replace(/<think>/gi, '<thinking>');
1043
- output = output.replace(/<\/redacted_thinking>/gi, '</thinking>');
1044
- if (this.v3SuppressThinkingStream) {
1045
- const closeIdx = output.search(/<\/thinking>/i);
1046
- if (closeIdx < 0) {
1047
- return '';
1048
- }
1049
- output = output.slice(closeIdx).replace(/^<\/thinking>/i, '');
1050
- this.v3SuppressThinkingStream = false;
1051
- }
1052
- output = output.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
1053
- if (/<thinking>/i.test(output)) {
1054
- output = output.replace(/<thinking>[\s\S]*$/i, '');
1055
- this.v3SuppressThinkingStream = true;
1056
- }
1057
- output = output
1058
- .replace(/```json\s*\[\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*\]\s*```/gi, '')
1059
- .replace(/```json\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*```/gi, '')
1060
- .replace(/^\s*(?:json\s*)?\[\s*\{[\s\S]*?"tool"[\s\S]*$/gim, '')
1061
- .replace(/^\s*(?:list_dir|read_file|write_file|edit_file|glob|grep|bash)\s*$/gim, '')
1062
- .replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
1063
- .replace(/<tool_code>[\s\S]*?<\/tool_code>/gi, '')
1064
- .replace(/<read_file>\s*<path>[\s\S]*?<\/path>\s*<\/read_file>/gi, '')
1065
- .replace(/<(?:grep|list_directory|search_files|glob|tool_result)\b[\s\S]*?<\/(?:grep|list_directory|search_files|glob|tool_result)>/gi, '')
1066
- .replace(/bootstrap discovery completed\s*[—-]\s*inspect the most relevant files next\.?/gi, '')
1067
- .replace(/\n{3,}/g, '\n\n');
1068
- return output;
889
+ return this.streamState.sanitizeVisibleText(text);
1069
890
  }
1070
891
  writeAgentActivityLine(text) {
1071
892
  if (this.jsonOutput)
@@ -1085,29 +906,29 @@ export class ChatCommand {
1085
906
  if (!event || typeof event !== 'object') {
1086
907
  return;
1087
908
  }
1088
- this.v3LastActivity = Date.now();
909
+ this.streamState.noteActivity();
1089
910
  if (isV3StreamKeepaliveEvent(event)) {
1090
911
  return;
1091
912
  }
1092
913
  if (event.type === 'tool_call') {
1093
914
  const eventKey = this.toolEventKey(event);
1094
- if (this.v3SeenToolCalls.has(eventKey)) {
915
+ if (this.streamState.seenToolCalls.has(eventKey)) {
1095
916
  return;
1096
917
  }
1097
- this.v3SeenToolCalls.add(eventKey);
1098
- this.v3ToolCallCount += 1;
918
+ this.streamState.seenToolCalls.add(eventKey);
919
+ this.streamState.toolCallCount += 1;
1099
920
  const toolName = event.name || event.tool || event.tool_name || '';
1100
921
  emitDeckEvent({
1101
922
  type: 'tool_start',
1102
923
  tool: toolName,
1103
- index: this.v3ToolCallCount,
924
+ index: this.streamState.toolCallCount,
1104
925
  target: event.arguments?.path || event.arguments?.file_path || event.arguments?.pattern || '',
1105
926
  });
1106
927
  const toolDesc = this.describeV3AgentTool(event.tool || event.name || event.tool_name);
1107
928
  const toolTarget = event.arguments?.path || event.arguments?.file_path || event.arguments?.pattern || '';
1108
929
  const sanitizedTarget = this.sanitizeServerPath(String(toolTarget));
1109
930
  const shortTarget = sanitizedTarget ? ` -> ${sanitizedTarget.replace(/\\/g, '/').split('/').slice(-2).join('/')}` : '';
1110
- const stepLabel = chalk.cyan(` [${this.v3ToolCallCount}]`) + ` ${toolDesc}${shortTarget}`;
931
+ const stepLabel = chalk.cyan(` [${this.streamState.toolCallCount}]`) + ` ${toolDesc}${shortTarget}`;
1111
932
  if (spinner.isSpinning)
1112
933
  spinner.stop();
1113
934
  this.writeAgentActivityLine(this.fitTerminalText(stepLabel) + '\n');
@@ -1130,18 +951,18 @@ export class ChatCommand {
1130
951
  }
1131
952
  if (event.type === 'tool_result') {
1132
953
  const eventKey = this.toolEventKey(event);
1133
- if (eventKey && this.v3SeenToolResults.has(eventKey)) {
954
+ if (eventKey && this.streamState.seenToolResults.has(eventKey)) {
1134
955
  return;
1135
956
  }
1136
957
  if (eventKey)
1137
- this.v3SeenToolResults.add(eventKey);
958
+ this.streamState.seenToolResults.add(eventKey);
1138
959
  const success = event.success !== false;
1139
960
  const toolName = event.name || event.tool || '';
1140
961
  emitDeckEvent({
1141
962
  type: 'tool_end',
1142
963
  tool: toolName,
1143
964
  success,
1144
- index: this.v3ToolCallCount,
965
+ index: this.streamState.toolCallCount,
1145
966
  });
1146
967
  const indicator = success ? chalk.green(' ✓') : chalk.red(' ✗');
1147
968
  if (spinner.isSpinning)
@@ -1207,7 +1028,7 @@ export class ChatCommand {
1207
1028
  return;
1208
1029
  }
1209
1030
  if (event.type === 'thinking') {
1210
- this.v3IterationCount += 1;
1031
+ this.streamState.iterationCount += 1;
1211
1032
  const iterText = this.sanitizeServerPath(event.content || '');
1212
1033
  const isGenericStep = !iterText
1213
1034
  || /read-only analysis turn|run at least one discovery tool/i.test(iterText)
@@ -1226,7 +1047,7 @@ export class ChatCommand {
1226
1047
  ? (event.delta?.text || '')
1227
1048
  : (event.content || '');
1228
1049
  if (text) {
1229
- this.v3LastActivity = Date.now();
1050
+ this.streamState.noteActivity();
1230
1051
  if (event.type === 'message' && /^\[Context\]/i.test(String(text).trim())) {
1231
1052
  if (spinner.isSpinning)
1232
1053
  spinner.stop();
@@ -1243,16 +1064,16 @@ export class ChatCommand {
1243
1064
  }
1244
1065
  if (event.type === 'complete') {
1245
1066
  const elapsed = event.elapsed || '';
1246
- const iters = event.iterations || this.v3IterationCount;
1247
- const tools = event.tool_calls || this.v3ToolCallCount;
1067
+ const iters = event.iterations || this.streamState.iterationCount;
1068
+ const tools = event.tool_calls || this.streamState.toolCallCount;
1248
1069
  if (spinner.isSpinning)
1249
1070
  spinner.stop();
1250
1071
  this.displayV3StreamedAnswer();
1251
1072
  emitDeckEvent({
1252
1073
  type: 'run_complete',
1253
1074
  success: true,
1254
- tools: event.tool_calls || this.v3ToolCallCount,
1255
- iterations: event.iterations || this.v3IterationCount,
1075
+ tools: event.tool_calls || this.streamState.toolCallCount,
1076
+ iterations: event.iterations || this.streamState.iterationCount,
1256
1077
  elapsed: event.elapsed || '',
1257
1078
  });
1258
1079
  let statLine = `${iters} iterations, ${tools} tool calls`;
@@ -1268,10 +1089,11 @@ export class ChatCommand {
1268
1089
  process.stdout.write(chalk.green(`\n✓ Complete`) + ` - ${this.fitTerminalText(statLine, 14)}\n`);
1269
1090
  // Show seal quality score if available
1270
1091
  if (event.seal_score && typeof event.seal_score.overall === 'number') {
1271
- const score = event.seal_score.overall;
1092
+ const rawScore = event.seal_score.overall;
1093
+ const score = rawScore <= 10 ? rawScore * 10 : rawScore;
1272
1094
  const tier = event.seal_score.tier || '';
1273
- const scoreColor = score >= 7 ? chalk.green : score >= 5 ? chalk.yellow : chalk.red;
1274
- process.stdout.write(chalk.cyan(' [Quality] ') + scoreColor(`${score}/10`) + (tier ? chalk.gray(` (${tier})`) : '') + '\n');
1095
+ const scoreColor = score >= 70 ? chalk.green : score >= 50 ? chalk.yellow : chalk.red;
1096
+ process.stdout.write(chalk.cyan(' [Quality] ') + scoreColor(`${score}/100`) + (tier ? chalk.gray(` (${tier})`) : '') + '\n');
1275
1097
  }
1276
1098
  return;
1277
1099
  }
@@ -1426,11 +1248,11 @@ export class ChatCommand {
1426
1248
  return;
1427
1249
  }
1428
1250
  if (event.type === 'start') {
1429
- if (this.v3StartSeen) {
1251
+ if (this.streamState.startSeen) {
1430
1252
  spinner.text = 'Working...';
1431
1253
  return;
1432
1254
  }
1433
- this.v3StartSeen = true;
1255
+ this.streamState.startSeen = true;
1434
1256
  if (spinner.isSpinning)
1435
1257
  spinner.stop();
1436
1258
  process.stderr.write(chalk.cyan(' [Start] ') + 'Agent initialized\n');
@@ -1504,20 +1326,20 @@ export class ChatCommand {
1504
1326
  this.config = config;
1505
1327
  this.logger = logger;
1506
1328
  this.commandProgram = commandProgram;
1507
- this.api = new APIClient(config, logger);
1329
+ this.api = createAPIClient(config, logger);
1330
+ this.promptPolicy = new ChatPromptPolicy(() => this.getRuntimeEnvironmentContext());
1331
+ this.streamState = new AgentStreamState({
1332
+ isJsonOutput: () => this.jsonOutput,
1333
+ idleTimeoutMs: () => DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS,
1334
+ });
1508
1335
  this.sessionManager = new SessionManager();
1509
1336
  }
1510
1337
  async run(options) {
1511
1338
  const hasRuntimeToken = Boolean(process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN);
1512
1339
  if (!this.config.isAuthenticated() && !hasRuntimeToken) {
1513
- if (options.json) {
1514
- process.exitCode = 1;
1515
- emitAgentJsonOutput({ success: false, error: 'Not authenticated. Run: vigthoria login' });
1516
- }
1517
- else {
1518
- this.logger.error('Not authenticated. Run: vigthoria login');
1519
- }
1520
- return;
1340
+ throw new CliCommandError('Not authenticated. Run: vigthoria login', {
1341
+ code: 'AUTH_REQUIRED', category: 'authentication',
1342
+ });
1521
1343
  }
1522
1344
  this.agentMode = options.agent === true;
1523
1345
  this.operatorMode = options.operator === true;
@@ -1526,7 +1348,7 @@ export class ChatCommand {
1526
1348
  : null;
1527
1349
  this.savePlanToVigFlow = options.savePlan === true;
1528
1350
  this.jsonOutput = options.json === true;
1529
- this.autoApprove = options.autoApprove === true || this.jsonOutput;
1351
+ this.autoApprove = resolveAutoApproval(options.autoApprove, this.jsonOutput);
1530
1352
  this.personaOverride = options.grant === true ? 'wiener_grant' : null;
1531
1353
  this.modelExplicitlySelected = this.isHardExplicitAgentModelSelection(options);
1532
1354
  this.currentModel = this.resolveInitialModel(options);
@@ -1582,19 +1404,23 @@ export class ChatCommand {
1582
1404
  this.saveSession();
1583
1405
  }
1584
1406
  this.operatorMode = false;
1585
- this.logger.error(this.operatorAccessMessage());
1586
- return;
1407
+ throw new CliCommandError(this.operatorAccessMessage(), { code: 'OPERATOR_ACCESS_REQUIRED', category: 'authorization' });
1587
1408
  }
1588
1409
  if (options.retry || options.continue) {
1589
1410
  if (!this.agentMode) {
1590
- throw new Error('--retry and --continue require agent mode.');
1411
+ throw new CliCommandError('--retry and --continue require agent mode.', { code: 'AGENT_MODE_REQUIRED', category: 'usage' });
1591
1412
  }
1592
1413
  const followUp = options.retry ? this.buildRetryPrompt() : this.buildContinuePrompt();
1593
1414
  if (!followUp) {
1594
- throw new Error('No previous agent run to retry or continue. Run an agent prompt first with the same --project, or use --resume.');
1415
+ throw new CliCommandError('No previous agent run to retry or continue. Run an agent prompt first with the same --project, or use --resume.', {
1416
+ code: 'AGENT_RUN_NOT_FOUND',
1417
+ category: 'usage',
1418
+ });
1595
1419
  }
1596
1420
  this.directPromptMode = true;
1597
1421
  await this.runAgentTurn(followUp);
1422
+ this.workspaceBrain?.destroy();
1423
+ this.api.destroy();
1598
1424
  return;
1599
1425
  }
1600
1426
  if (options.prompt) {
@@ -1606,35 +1432,34 @@ export class ChatCommand {
1606
1432
  const parsed = Number.parseInt(rawValue, 10);
1607
1433
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
1608
1434
  })();
1609
- let timedOut = false;
1610
- const timeoutId = options.bridge && bridgePromptTimeoutMs > 0
1611
- ? setTimeout(() => {
1612
- timedOut = true;
1613
- const b = getBridgeClient();
1614
- if (b) {
1615
- b.emitEnd({ reason: 'timeout' });
1616
- b.destroy();
1617
- }
1618
- if (!this.jsonOutput) {
1619
- this.logger.error('Bridge prompt timed out after ' + Math.round(bridgePromptTimeoutMs / 1000) + ' seconds.');
1620
- }
1621
- process.exitCode = 1;
1622
- }, bridgePromptTimeoutMs)
1623
- : null;
1624
- await this.handleDirectPrompt(options.prompt);
1625
- if (timeoutId)
1626
- clearTimeout(timeoutId);
1627
- if (!timedOut) {
1628
- const bridge = getBridgeClient();
1629
- if (bridge) {
1630
- bridge.emitEnd({ reason: 'prompt-complete' });
1631
- bridge.destroy();
1435
+ let timeoutId = null;
1436
+ try {
1437
+ const promptTask = this.handleDirectPrompt(options.prompt);
1438
+ if (options.bridge && bridgePromptTimeoutMs > 0) {
1439
+ await Promise.race([
1440
+ promptTask,
1441
+ new Promise((_resolve, reject) => {
1442
+ timeoutId = setTimeout(() => reject(new CliCommandError(`Bridge prompt timed out after ${Math.round(bridgePromptTimeoutMs / 1000)} seconds.`, { code: 'BRIDGE_PROMPT_TIMEOUT', category: 'network' })), bridgePromptTimeoutMs);
1443
+ }),
1444
+ ]);
1445
+ }
1446
+ else {
1447
+ await promptTask;
1632
1448
  }
1449
+ getBridgeClient()?.emitEnd({ reason: 'prompt-complete' });
1633
1450
  }
1634
- // Force-exit: undici + chokidar + HTTPS pool keep the Node.js event
1635
- // loop alive indefinitely in direct prompt mode; a clean exit is safe here.
1636
- this.api.destroy();
1637
- process.exit(process.exitCode ?? 0);
1451
+ catch (error) {
1452
+ getBridgeClient()?.emitEnd({ reason: 'prompt-failed' });
1453
+ throw error;
1454
+ }
1455
+ finally {
1456
+ if (timeoutId)
1457
+ clearTimeout(timeoutId);
1458
+ getBridgeClient()?.destroy();
1459
+ this.workspaceBrain?.destroy();
1460
+ this.api.destroy();
1461
+ }
1462
+ return;
1638
1463
  }
1639
1464
  await this.startInteractiveChat();
1640
1465
  const bridge = getBridgeClient();
@@ -1642,6 +1467,8 @@ export class ChatCommand {
1642
1467
  bridge.emitEnd({ reason: 'interactive-exit' });
1643
1468
  bridge.destroy();
1644
1469
  }
1470
+ this.workspaceBrain?.destroy();
1471
+ this.api.destroy();
1645
1472
  }
1646
1473
  /** Handle an inbound admin command from the Commando Bridge. */
1647
1474
  handleAdminCommand(cmd) {
@@ -1661,7 +1488,9 @@ export class ChatCommand {
1661
1488
  if (!this.jsonOutput)
1662
1489
  console.log(chalk.red(`[bridge] Abort requested by admin`));
1663
1490
  getBridgeClient()?.emitEnd({ reason: 'admin-abort' });
1664
- process.exit(0);
1491
+ process.exitCode = 130;
1492
+ this.workspaceBrain?.destroy();
1493
+ this.api.destroy();
1665
1494
  break;
1666
1495
  default:
1667
1496
  getBridgeClient()?.emitError({ message: `Unknown admin command: ${cmd.action}` });
@@ -1936,8 +1765,7 @@ export class ChatCommand {
1936
1765
  }
1937
1766
  if (this.operatorMode) {
1938
1767
  if (!this.hasOperatorAccess()) {
1939
- this.logger.error(this.operatorAccessMessage());
1940
- return;
1768
+ throw new CliCommandError(this.operatorAccessMessage(), { code: 'OPERATOR_ACCESS_REQUIRED', category: 'authorization' });
1941
1769
  }
1942
1770
  await this.runOperatorTurn(effectivePrompt);
1943
1771
  return;
@@ -2017,13 +1845,13 @@ export class ChatCommand {
2017
1845
  spinner.stop();
2018
1846
  }
2019
1847
  if (this.jsonOutput) {
2020
- console.log(JSON.stringify({
1848
+ emitAgentJsonOutput({
2021
1849
  success: true,
2022
1850
  workflow: resolvedWorkflow,
2023
1851
  mode: invocationMode,
2024
1852
  execution,
2025
1853
  content,
2026
- }, null, 2));
1854
+ });
2027
1855
  return;
2028
1856
  }
2029
1857
  this.logger.success(`Workflow target ${resolvedWorkflow.name} ${execution.status}`);
@@ -2044,8 +1872,7 @@ export class ChatCommand {
2044
1872
  }
2045
1873
  async runOperatorTurn(prompt) {
2046
1874
  if (!this.hasOperatorAccess()) {
2047
- this.logger.error(this.operatorAccessMessage());
2048
- return;
1875
+ throw new CliCommandError(this.operatorAccessMessage(), { code: 'OPERATOR_ACCESS_REQUIRED', category: 'authorization' });
2049
1876
  }
2050
1877
  // ── Fast-path: direct-answerable prompts skip the full BMAD workflow ──
2051
1878
  // The BMAD orchestrator scans the entire workspace (can be 1000+ files)
@@ -2105,7 +1932,7 @@ export class ChatCommand {
2105
1932
  throw new CLIError('Operator workflow returned a non-actionable acknowledgement instead of a grounded result.', 'model_backend');
2106
1933
  }
2107
1934
  if (this.jsonOutput) {
2108
- console.log(JSON.stringify({
1935
+ emitAgentJsonOutput({
2109
1936
  success: true,
2110
1937
  mode: 'operator',
2111
1938
  workflowId: response.workflowId,
@@ -2113,7 +1940,7 @@ export class ChatCommand {
2113
1940
  contextId: response.contextId || null,
2114
1941
  content: response.content || 'Operator workflow completed.',
2115
1942
  metadata: response.metadata || {},
2116
- }, null, 2));
1943
+ });
2117
1944
  }
2118
1945
  else {
2119
1946
  console.log(response.content || 'Operator workflow completed.');
@@ -2131,23 +1958,8 @@ export class ChatCommand {
2131
1958
  }
2132
1959
  const cliErr = error instanceof CLIError ? error : classifyError(error);
2133
1960
  const errorMsg = formatCLIError(cliErr);
2134
- if (!this.jsonOutput) {
2135
- this.logger.error('Operator workflow failed');
2136
- }
2137
1961
  this.rememberBrainEvent('issue', `GoA operator workflow failed: ${errorMsg}`, 'operator');
2138
- if (this.jsonOutput) {
2139
- process.exitCode = 1;
2140
- console.log(JSON.stringify({
2141
- success: false,
2142
- mode: 'operator',
2143
- content: '',
2144
- error: errorMsg,
2145
- errorCategory: cliErr.category,
2146
- }, null, 2));
2147
- }
2148
- else {
2149
- this.logger.error(errorMsg);
2150
- }
1962
+ throw commandFailure(error, { code: 'OPERATOR_EXECUTION_FAILED', message: errorMsg });
2151
1963
  }
2152
1964
  }
2153
1965
  /**
@@ -2193,12 +2005,12 @@ export class ChatCommand {
2193
2005
  throw new CLIError('Operator returned a non-actionable acknowledgement instead of a grounded result.', 'model_backend');
2194
2006
  }
2195
2007
  if (this.jsonOutput) {
2196
- console.log(JSON.stringify({
2008
+ emitAgentJsonOutput({
2197
2009
  success: true,
2198
2010
  mode: 'operator',
2199
2011
  content,
2200
2012
  metadata: { source: 'operator-direct', mode: 'operator' },
2201
- }, null, 2));
2013
+ });
2202
2014
  }
2203
2015
  else {
2204
2016
  console.log(content);
@@ -2212,20 +2024,7 @@ export class ChatCommand {
2212
2024
  spinner.stop();
2213
2025
  const cliErr = error instanceof CLIError ? error : classifyError(error);
2214
2026
  const errorMsg = formatCLIError(cliErr);
2215
- if (this.jsonOutput) {
2216
- process.exitCode = 1;
2217
- console.log(JSON.stringify({
2218
- success: false,
2219
- mode: 'operator',
2220
- content: '',
2221
- error: errorMsg,
2222
- errorCategory: cliErr.category,
2223
- }, null, 2));
2224
- }
2225
- else {
2226
- this.logger.error('Operator direct answer failed');
2227
- this.logger.error(errorMsg);
2228
- }
2027
+ throw commandFailure(error, { code: 'OPERATOR_DIRECT_FAILED', message: errorMsg });
2229
2028
  }
2230
2029
  }
2231
2030
  async runSimplePrompt(prompt) {
@@ -2324,20 +2123,20 @@ export class ChatCommand {
2324
2123
  const response = await this.callApi('Send chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel));
2325
2124
  if (spinner)
2326
2125
  spinner.stop();
2327
- const finalText = this.sanitizeDirectModeOutput(this.stripHiddenThoughtBlocks(response.message || '')).trim();
2126
+ const finalText = sanitizeDirectModeOutput(stripHiddenThoughtBlocks(response.message || '')).trim();
2328
2127
  const effectiveModel = String(response.model || this.currentModel);
2329
2128
  const metadata = this.modelGovernanceFallback
2330
2129
  ? { modelFallback: this.modelGovernanceFallback }
2331
2130
  : undefined;
2332
2131
  if (this.jsonOutput) {
2333
- console.log(JSON.stringify({
2132
+ emitAgentJsonOutput({
2334
2133
  success: true,
2335
2134
  mode: 'chat',
2336
2135
  model: effectiveModel,
2337
2136
  requestedModel: this.modelGovernanceFallback?.requestedModel || this.currentModel,
2338
2137
  metadata,
2339
2138
  content: finalText || 'The model returned an empty response. Try rephrasing or use --agent for grounded file analysis.',
2340
- }, null, 2));
2139
+ });
2341
2140
  }
2342
2141
  else if (finalText) {
2343
2142
  console.log(finalText);
@@ -2353,20 +2152,7 @@ export class ChatCommand {
2353
2152
  spinner.stop();
2354
2153
  const cliErr = error instanceof CLIError ? error : classifyError(error);
2355
2154
  const errorMsg = formatCLIError(cliErr);
2356
- if (this.jsonOutput) {
2357
- process.exitCode = 1;
2358
- console.log(JSON.stringify({
2359
- success: false,
2360
- mode: 'chat',
2361
- model: this.currentModel,
2362
- content: '',
2363
- error: errorMsg,
2364
- errorCategory: cliErr.category,
2365
- }, null, 2));
2366
- }
2367
- else {
2368
- this.logger.error(errorMsg);
2369
- }
2155
+ throw commandFailure(error, { code: 'CHAT_EXECUTION_FAILED', message: errorMsg });
2370
2156
  }
2371
2157
  }
2372
2158
  async runAgentTurn(prompt) {
@@ -2406,10 +2192,9 @@ export class ChatCommand {
2406
2192
  return;
2407
2193
  }
2408
2194
  const errorMessage = 'The V3 planner/executor workflow is unavailable. Refusing to fall back to legacy template, direct-file, or local write paths.';
2409
- this.logger.error(errorMessage);
2410
2195
  this.messages.push({ role: 'assistant', content: errorMessage });
2411
- process.exitCode = 1;
2412
2196
  this.saveSession();
2197
+ throw new CliCommandError(errorMessage, { code: 'AGENT_UNAVAILABLE', category: 'execution' });
2413
2198
  }
2414
2199
  resolveAgentTurnPrompt(prompt) {
2415
2200
  if (isBuiltContinuePrompt(prompt) || isBuiltRetryPrompt(prompt) || isBuiltWriteConfirmationPrompt(prompt)) {
@@ -2581,33 +2366,38 @@ export class ChatCommand {
2581
2366
  spinner.stop();
2582
2367
  const evidenceSummary = this.synthesizeEvidenceFromHistory();
2583
2368
  if (evidenceSummary) {
2584
- const fallbackContent = this.sanitizeDirectModeOutput(evidenceSummary);
2369
+ const fallbackContent = sanitizeDirectModeOutput(evidenceSummary);
2585
2370
  const liveOutcome = this.buildLocalLoopLiveOutcome(prompt);
2586
2371
  const evaluation = evaluateExecutorSuccess(liveOutcome);
2587
2372
  this.persistLocalLoopOutcome(prompt, evaluation, liveOutcome);
2588
2373
  if (this.jsonOutput) {
2589
- process.exitCode = evaluation.executorSucceeded ? 0 : 1;
2590
- console.log(JSON.stringify({
2591
- success: evaluation.executorSucceeded,
2592
- mode: 'agent',
2593
- model: this.currentModel,
2594
- partial: !evaluation.executorSucceeded,
2595
- content: fallbackContent,
2596
- statusHeadline: evaluation.statusHeadline,
2597
- metadata: {
2598
- executionPath: 'local-agent-loop',
2599
- recovered: true,
2600
- outcomeTruth: {
2601
- executorSucceeded: evaluation.executorSucceeded,
2602
- uiTheme: evaluation.uiTheme,
2374
+ if (evaluation.executorSucceeded)
2375
+ emitAgentJsonOutput({
2376
+ success: true,
2377
+ mode: 'agent',
2378
+ model: this.currentModel,
2379
+ partial: !evaluation.executorSucceeded,
2380
+ content: fallbackContent,
2381
+ statusHeadline: evaluation.statusHeadline,
2382
+ metadata: {
2383
+ executionPath: 'local-agent-loop',
2384
+ recovered: true,
2385
+ outcomeTruth: {
2386
+ executorSucceeded: evaluation.executorSucceeded,
2387
+ uiTheme: evaluation.uiTheme,
2388
+ },
2603
2389
  },
2604
- },
2605
- }, null, 2));
2390
+ });
2606
2391
  }
2607
2392
  else {
2608
2393
  console.log(fallbackContent);
2609
2394
  }
2610
2395
  this.saveSession();
2396
+ if (!evaluation.executorSucceeded) {
2397
+ throw new CliCommandError(evaluation.statusHeadline, {
2398
+ code: 'AGENT_INCOMPLETE', details: { content: fallbackContent, executionPath: 'local-agent-loop' },
2399
+ });
2400
+ }
2611
2401
  return;
2612
2402
  }
2613
2403
  throw retryErr;
@@ -2619,23 +2409,7 @@ export class ChatCommand {
2619
2409
  if (spinner)
2620
2410
  spinner.stop();
2621
2411
  this.rememberBrainEvent('issue', 'Agent API preflight failed: ' + formatted, 'agent');
2622
- if (this.jsonOutput) {
2623
- process.exitCode = 1;
2624
- console.log(JSON.stringify({
2625
- success: false,
2626
- mode: 'agent',
2627
- model: this.currentModel,
2628
- partial: false,
2629
- content: '',
2630
- error: formatted,
2631
- errorCategory: cliErr.category,
2632
- metadata: { executionPath: 'local-agent-loop' },
2633
- }, null, 2));
2634
- }
2635
- else {
2636
- this.logger.error(formatted);
2637
- }
2638
- return;
2412
+ throw commandFailure(firstErr, { code: 'AGENT_PREFLIGHT_FAILED', message: formatted });
2639
2413
  }
2640
2414
  }
2641
2415
  if (spinner)
@@ -2657,8 +2431,8 @@ export class ChatCommand {
2657
2431
  const isPolicyAck = /^(i will follow|i understand|i('ll| will) adhere|understood[.,!]|sure[.,!]|i('ll| will) use the tools|i('ll| will) proceed|let me know|provide your|waiting for)/i.test(visibleText.trim());
2658
2432
  // Detect if the response is effectively empty or just a follow-up
2659
2433
  // question after we strip tool output echoes.
2660
- const sanitized = this.sanitizeDirectModeOutput(visibleText.trim());
2661
- const isFollowUp = this.isDirectModeFollowUpQuestion(sanitized);
2434
+ const sanitized = sanitizeDirectModeOutput(visibleText.trim());
2435
+ const isFollowUp = isDirectModeFollowUpQuestion(sanitized);
2662
2436
  const isEmptyAfterSanitize = !sanitized || sanitized.length < 10;
2663
2437
  // Detect resignation: model gives up saying files/things were "not found"
2664
2438
  // without having tried list_dir to discover the correct path.
@@ -2701,29 +2475,34 @@ export class ChatCommand {
2701
2475
  const evaluation = evaluateExecutorSuccess(liveOutcome);
2702
2476
  this.persistLocalLoopOutcome(prompt, evaluation, liveOutcome);
2703
2477
  if (this.jsonOutput) {
2704
- process.exitCode = evaluation.executorSucceeded ? 0 : 1;
2705
- console.log(JSON.stringify({
2706
- success: evaluation.executorSucceeded,
2707
- mode: 'agent',
2708
- model: this.currentModel,
2709
- partial: !evaluation.executorSucceeded,
2710
- content: finalContent,
2711
- statusHeadline: evaluation.statusHeadline,
2712
- metadata: {
2713
- executionPath: 'local-agent-loop',
2714
- outcomeTruth: {
2715
- executorSucceeded: evaluation.executorSucceeded,
2716
- uiTheme: evaluation.uiTheme,
2717
- analysisToolsUsed: liveOutcome.analysisToolsUsed,
2718
- changedFileCount: liveOutcome.changedFileCount,
2478
+ if (evaluation.executorSucceeded)
2479
+ emitAgentJsonOutput({
2480
+ success: true,
2481
+ mode: 'agent',
2482
+ model: this.currentModel,
2483
+ partial: !evaluation.executorSucceeded,
2484
+ content: finalContent,
2485
+ statusHeadline: evaluation.statusHeadline,
2486
+ metadata: {
2487
+ executionPath: 'local-agent-loop',
2488
+ outcomeTruth: {
2489
+ executorSucceeded: evaluation.executorSucceeded,
2490
+ uiTheme: evaluation.uiTheme,
2491
+ analysisToolsUsed: liveOutcome.analysisToolsUsed,
2492
+ changedFileCount: liveOutcome.changedFileCount,
2493
+ },
2719
2494
  },
2720
- },
2721
- }, null, 2));
2495
+ });
2722
2496
  }
2723
2497
  else {
2724
2498
  console.log(finalContent);
2725
2499
  }
2726
2500
  this.saveSession();
2501
+ if (!evaluation.executorSucceeded) {
2502
+ throw new CliCommandError(evaluation.statusHeadline, {
2503
+ code: 'AGENT_INCOMPLETE', details: { content: finalContent, executionPath: 'local-agent-loop' },
2504
+ });
2505
+ }
2727
2506
  return;
2728
2507
  }
2729
2508
  await this.executeToolCalls(toolCalls);
@@ -2740,25 +2519,7 @@ export class ChatCommand {
2740
2519
  const cliErr = error instanceof CLIError ? error : classifyError(error);
2741
2520
  const errorMsg = formatCLIError(cliErr);
2742
2521
  this.rememberBrainEvent('issue', `Agent turn failed: ${errorMsg}`, 'agent');
2743
- if (this.jsonOutput) {
2744
- process.exitCode = 1;
2745
- console.log(JSON.stringify({
2746
- success: false,
2747
- mode: 'agent',
2748
- model: this.currentModel,
2749
- partial: false,
2750
- content: '',
2751
- error: errorMsg,
2752
- errorCategory: cliErr.category,
2753
- metadata: {
2754
- executionPath: 'local-agent-loop',
2755
- },
2756
- }, null, 2));
2757
- }
2758
- else {
2759
- this.logger.error(errorMsg);
2760
- }
2761
- return;
2522
+ throw commandFailure(error, { code: 'AGENT_TURN_FAILED', message: errorMsg });
2762
2523
  }
2763
2524
  }
2764
2525
  const liveOutcome = this.buildLocalLoopLiveOutcome(prompt);
@@ -2768,29 +2529,11 @@ export class ChatCommand {
2768
2529
  executorError: 'Agent exhausted the maximum local tool loop turns before reaching a clean completion.',
2769
2530
  });
2770
2531
  this.persistLocalLoopOutcome(prompt, exhaustedEval, liveOutcome);
2771
- if (this.jsonOutput) {
2772
- process.exitCode = 1;
2773
- console.log(JSON.stringify({
2774
- success: false,
2775
- mode: 'agent',
2776
- model: this.currentModel,
2777
- partial: true,
2778
- content: 'Task complete.',
2779
- error: 'Agent exhausted the maximum local tool loop turns before reaching a clean completion.',
2780
- statusHeadline: exhaustedEval.statusHeadline,
2781
- metadata: {
2782
- executionPath: 'local-agent-loop',
2783
- outcomeTruth: {
2784
- executorSucceeded: false,
2785
- uiTheme: exhaustedEval.uiTheme,
2786
- },
2787
- },
2788
- }, null, 2));
2789
- }
2790
- else {
2791
- console.log('Task complete.');
2792
- }
2793
2532
  this.saveSession();
2533
+ throw new CliCommandError('Agent exhausted the maximum local tool loop turns before reaching a clean completion.', {
2534
+ code: 'AGENT_TURN_LIMIT_EXHAUSTED',
2535
+ details: { statusHeadline: exhaustedEval.statusHeadline, executionPath: 'local-agent-loop' },
2536
+ });
2794
2537
  }
2795
2538
  async primeBypassedTargetFileContext(prompt) {
2796
2539
  if (!this.directPromptMode || !this.tools) {
@@ -2821,235 +2564,6 @@ export class ChatCommand {
2821
2564
  this.messages.push({ role: 'system', content: readSummary });
2822
2565
  }
2823
2566
  }
2824
- /**
2825
- * Sub-second path for trivial HTML/hello-world: Template Service → index.html (no V3 planner).
2826
- */
2827
- async tryTemplateInstantPath(prompt) {
2828
- const route = this.lastAgentRoute;
2829
- if (route?.path !== 'template-instant' && !isTrivialHtmlPageRequest(prompt)) {
2830
- return false;
2831
- }
2832
- let workspacePath = this.currentProjectPath;
2833
- try {
2834
- const fromPrompt = this.resolvePromptWorkspacePath(prompt, this.currentProjectPath);
2835
- if (fromPrompt)
2836
- workspacePath = fromPrompt;
2837
- }
2838
- catch {
2839
- // use default workspace
2840
- }
2841
- if (!this.jsonOutput) {
2842
- console.log();
2843
- console.log(chalk.gray('━━━ ROUTING DECISION ━━━'));
2844
- const r = this.lastAgentRoute;
2845
- console.log(chalk.gray(`Reason: ${r?.reason || 'template-instant (trivial HTML page)'}`));
2846
- console.log(chalk.gray(`Path: template-instant [${r?.source || 'regex'}]`));
2847
- console.log(chalk.gray('Model: template-service'));
2848
- console.log(chalk.gray('Cloud Eligible: false'));
2849
- console.log(chalk.gray('Cloud Selected: false'));
2850
- console.log(chalk.gray('━'.repeat(30)));
2851
- console.log();
2852
- console.log(chalk.cyan('⚡ Template instant path — matching template and writing index.html...'));
2853
- }
2854
- this.messages.push({ role: 'user', content: prompt });
2855
- const mustComplete = route?.path === 'template-instant';
2856
- let result = await runTemplateInstantPath(this.api, prompt, workspacePath, {
2857
- allowFallback: true,
2858
- });
2859
- if (!result.ok && mustComplete) {
2860
- result = await runTemplateInstantPath(this.api, prompt, workspacePath, {
2861
- allowFallback: true,
2862
- forceMinimalFallback: true,
2863
- });
2864
- }
2865
- if (!result.ok) {
2866
- if (!this.jsonOutput) {
2867
- console.log(chalk.yellow(`Template instant path skipped: ${result.error || 'no match'}`));
2868
- }
2869
- return false;
2870
- }
2871
- const summary = [
2872
- `Created ${result.entryPath || 'index.html'} via template-instant`,
2873
- result.templateName ? `(${result.templateName})` : '',
2874
- result.processingMs ? `in ${Math.round(result.processingMs)}ms` : '',
2875
- result.usedFallback ? '(minimal fallback)' : '',
2876
- ].filter(Boolean).join(' ');
2877
- this.messages.push({ role: 'assistant', content: summary });
2878
- const liveOutcome = createLiveOutcome();
2879
- liveOutcome.requiresWorkspaceChanges = true;
2880
- liveOutcome.workspaceHasOutput = true;
2881
- liveOutcome.changedFileCount = 1;
2882
- liveOutcome.tasksTotal = 1;
2883
- liveOutcome.tasksSucceeded = 1;
2884
- liveOutcome.answerContent = summary;
2885
- const evaluation = evaluateExecutorSuccess(liveOutcome);
2886
- this.commitAgentRunOutcome({
2887
- prompt,
2888
- originalPrompt: null,
2889
- taskId: null,
2890
- contextId: null,
2891
- tasksSucceeded: 1,
2892
- tasksTotal: 1,
2893
- failedTaskIds: [],
2894
- unfinishedTaskIds: [],
2895
- qualityScore: null,
2896
- qualityMissing: [],
2897
- qualityBlockers: [],
2898
- hasOutput: true,
2899
- answerContent: summary,
2900
- selfHealStatus: 'skipped',
2901
- selfHealTool: null,
2902
- plannerError: null,
2903
- executorError: null,
2904
- clientToolErrors: [],
2905
- transportErrors: [],
2906
- workspacePath: workspacePath || null,
2907
- workspaceSyncIssue: null,
2908
- finishedAt: Date.now(),
2909
- }, prompt);
2910
- if (!this.jsonOutput) {
2911
- console.log(chalk.green(`✓ ${summary}`));
2912
- console.log(chalk.gray(' Run: vigthoria preview'));
2913
- this.printAgentRunSummary(this.lastAgentRunOutcome, evaluation, 1);
2914
- }
2915
- else {
2916
- const routingPolicy = this.resolveAgentExecutionPolicy(prompt);
2917
- console.log(JSON.stringify({
2918
- success: evaluation.executorSucceeded,
2919
- mode: 'agent',
2920
- model: routingPolicy.selectedModel,
2921
- routingPolicy,
2922
- taskId: null,
2923
- contextId: null,
2924
- partial: false,
2925
- content: summary,
2926
- statusHeadline: evaluation.statusHeadline,
2927
- tasksSucceeded: 1,
2928
- tasksTotal: 1,
2929
- metadata: {
2930
- source: 'template-instant',
2931
- templateName: result.templateName,
2932
- processingMs: result.processingMs,
2933
- route: this.lastAgentRoute,
2934
- },
2935
- }, null, 2));
2936
- }
2937
- return true;
2938
- }
2939
- async tryDirectSingleFileFlow(prompt) {
2940
- if (!this.directPromptMode || !this.tools) {
2941
- return false;
2942
- }
2943
- const targetFile = this.inferTargetFileFromPrompt(prompt);
2944
- if (!targetFile) {
2945
- return false;
2946
- }
2947
- if (this.shouldBypassDirectSingleFileFlow(targetFile, prompt)) {
2948
- return false;
2949
- }
2950
- const readCall = {
2951
- tool: 'read_file',
2952
- args: { path: targetFile },
2953
- };
2954
- if (!this.jsonOutput) {
2955
- console.log(chalk.cyan(`⚙ Executing: ${readCall.tool}`));
2956
- }
2957
- const readResult = await this.tools.execute(readCall);
2958
- const readSummary = this.formatToolResult(readCall, readResult);
2959
- if (!this.jsonOutput) {
2960
- console.log(readResult.success ? chalk.gray(readSummary) : chalk.red(readSummary));
2961
- }
2962
- this.messages.push({ role: 'system', content: readSummary });
2963
- if (!readResult.success || !readResult.output) {
2964
- return false;
2965
- }
2966
- let rewrittenContent = this.tryDeterministicSingleFileRewrite(prompt, targetFile, readResult.output);
2967
- if (!rewrittenContent) {
2968
- const rewriteMessages = [
2969
- {
2970
- role: 'system',
2971
- content: [
2972
- 'You are repairing a single file for a CLI agent task.',
2973
- `Return only the final contents for ${targetFile}.`,
2974
- 'Do not use Markdown fences.',
2975
- 'Do not add explanations before or after the file contents.',
2976
- 'Produce complete, runnable code or markup.',
2977
- ].join('\n'),
2978
- },
2979
- {
2980
- role: 'user',
2981
- content: [
2982
- `Task: ${prompt}`,
2983
- `Target file: ${targetFile}`,
2984
- 'Current file contents:',
2985
- readResult.output,
2986
- ].join('\n\n'),
2987
- },
2988
- ];
2989
- const rewriteResponse = await this.callApi('Rewrite target file', () => this.api.chat(rewriteMessages, this.currentModel));
2990
- rewrittenContent = this.extractFinalFileContent(rewriteResponse.message, targetFile);
2991
- }
2992
- if (!rewrittenContent) {
2993
- return false;
2994
- }
2995
- const writeCall = {
2996
- tool: 'write_file',
2997
- args: {
2998
- path: targetFile,
2999
- content: rewrittenContent,
3000
- },
3001
- };
3002
- if (!this.jsonOutput) {
3003
- console.log(chalk.cyan(`⚙ Executing: ${writeCall.tool}`));
3004
- }
3005
- const writeResult = await this.tools.execute(writeCall);
3006
- const writeSummary = this.formatToolResult(writeCall, writeResult);
3007
- if (!this.jsonOutput) {
3008
- console.log(writeResult.success ? chalk.gray(writeSummary) : chalk.red(writeSummary));
3009
- }
3010
- this.messages.push({ role: 'system', content: writeSummary });
3011
- if (!writeResult.success) {
3012
- return false;
3013
- }
3014
- const previewGate = await this.callApi('Run Template Service preview gate', () => this.api.runTemplateServicePreviewGate(prompt, {
3015
- workspacePath: this.currentProjectPath,
3016
- projectPath: this.currentProjectPath,
3017
- targetPath: this.currentProjectPath,
3018
- rawPrompt: prompt,
3019
- executionSurface: 'cli',
3020
- clientSurface: 'cli',
3021
- }));
3022
- const success = previewGate.required ? previewGate.passed === true : true;
3023
- if (!success) {
3024
- process.exitCode = 1;
3025
- }
3026
- if (this.jsonOutput) {
3027
- console.log(JSON.stringify({
3028
- success,
3029
- mode: 'agent',
3030
- model: this.currentModel,
3031
- partial: false,
3032
- content: `Updated ${targetFile}.`,
3033
- metadata: {
3034
- executionPath: 'direct-single-file',
3035
- targetFile,
3036
- previewGate,
3037
- },
3038
- }, null, 2));
3039
- }
3040
- else {
3041
- console.log(`Updated ${targetFile}.`);
3042
- if (previewGate.required) {
3043
- if (previewGate.passed) {
3044
- console.log(chalk.gray(`Template Service preview gate: passed via ${previewGate.backendUrl || 'unknown backend'}`));
3045
- }
3046
- else {
3047
- console.log(chalk.yellow(`Template Service preview gate: failed${previewGate.error ? ` - ${previewGate.error}` : ''}`));
3048
- }
3049
- }
3050
- }
3051
- return true;
3052
- }
3053
2567
  taskRequiresWorkspaceChanges(prompt) {
3054
2568
  return promptRequiresWorkspaceChanges(prompt);
3055
2569
  }
@@ -3129,15 +2643,7 @@ export class ChatCommand {
3129
2643
  console.log();
3130
2644
  }
3131
2645
  // Reset streaming counters for new workflow
3132
- this.v3IterationCount = 0;
3133
- this.v3ToolCallCount = 0;
3134
- this.v3LastActivity = Date.now();
3135
- this.v3StreamingStarted = false;
3136
- this.v3StreamedAnswerBuffer = '';
3137
- this.v3StreamedAnswerDisplayed = false;
3138
- this.v3StartSeen = false;
3139
- this.v3SeenToolCalls.clear();
3140
- this.v3SeenToolResults.clear();
2646
+ this.streamState.reset();
3141
2647
  emitDeckEvent({ type: 'agent_start' });
3142
2648
  const taskDisplay = new TaskDisplay(['Analyse workspace', 'Execute tasks', 'Validate output', 'Self-heal'], false);
3143
2649
  taskDisplay.start(0);
@@ -3216,6 +2722,12 @@ export class ChatCommand {
3216
2722
  || isAgentRetryPrompt(contextualPrompt)
3217
2723
  || isBuiltContinuePrompt(contextualPrompt)
3218
2724
  || isBuiltRetryPrompt(contextualPrompt);
2725
+ const initialExecutionStatus = isAgentRetryPrompt(contextualPrompt)
2726
+ || isBuiltRetryPrompt(contextualPrompt)
2727
+ ? 'retry'
2728
+ : shouldResumePlan
2729
+ ? 'continue'
2730
+ : 'submitted';
3219
2731
  const agentExecutionHints = shouldResumePlan
3220
2732
  ? {
3221
2733
  ...executionHints,
@@ -3243,7 +2755,7 @@ export class ChatCommand {
3243
2755
  userTier: String(this.config.get('subscription')?.plan || '').trim() || null,
3244
2756
  taskType: agentTaskType,
3245
2757
  workflowType,
3246
- status: 'submitted',
2758
+ status: initialExecutionStatus,
3247
2759
  submittedAt: new Date().toISOString(),
3248
2760
  updatedAt: new Date().toISOString(),
3249
2761
  failedTaskIds: resumeTaskIds,
@@ -3255,7 +2767,7 @@ export class ChatCommand {
3255
2767
  if (this.shouldStartWorkspaceWatcher(workspacePath)) {
3256
2768
  watcher = new WorkspaceWatcher({
3257
2769
  workspaceRoot: workspacePath,
3258
- onFileChange: (relativePath, content, action) => {
2770
+ onFileChange: (relativePath, _content, action) => {
3259
2771
  this.logger.debug(`Local change detected: ${action} ${relativePath}`);
3260
2772
  },
3261
2773
  });
@@ -3329,10 +2841,7 @@ export class ChatCommand {
3329
2841
  noteAnalysisToolUse(toolName, liveOutcome);
3330
2842
  }
3331
2843
  else if (event.type === 'complete') {
3332
- const tt = Number(event.tasks_total);
3333
- if (Number.isFinite(tt) && tt > 0) {
3334
- liveOutcome.tasksTotal = tt;
3335
- }
2844
+ handleRunCompleteEvent(event, liveOutcome);
3336
2845
  if (Number(event.discovery_tools_used) > 0) {
3337
2846
  liveOutcome.analysisToolsUsed = Math.max(liveOutcome.analysisToolsUsed, Number(event.discovery_tools_used));
3338
2847
  }
@@ -3379,23 +2888,23 @@ export class ChatCommand {
3379
2888
  if (spinner) {
3380
2889
  spinner.stop();
3381
2890
  }
3382
- if (this.v3StreamingStarted && !this.v3StreamedAnswerDisplayed) {
2891
+ if (this.streamState.streamingStarted && !this.streamState.streamedAnswerDisplayed) {
3383
2892
  this.displayV3StreamedAnswer();
3384
2893
  }
3385
2894
  const previewGate = (response.metadata?.previewGate || null);
3386
2895
  const workspaceHasOutput = this.api.hasAgentWorkspaceOutput(workspaceContext);
3387
2896
  const changedFileCount = response.changedFiles ? Object.keys(response.changedFiles).length : 0;
3388
2897
  const requiresWorkspaceChanges = taskRequiresWorkspaceChangesWithContext(contextualPrompt, intentContext);
3389
- const answerContent = normalizeAgentAnswerContent(response.content, this.v3StreamedAnswerBuffer);
2898
+ const answerContent = normalizeAgentAnswerContent(response.content, this.streamState.streamedAnswerBuffer);
3390
2899
  liveOutcome.changedFileCount = changedFileCount;
3391
2900
  liveOutcome.requiresWorkspaceChanges = requiresWorkspaceChanges;
3392
2901
  liveOutcome.workspaceHasOutput = requiresWorkspaceChanges ? workspaceHasOutput : false;
3393
2902
  liveOutcome.answerContent = answerContent;
3394
2903
  const success = previewGate?.required === true
3395
- ? (previewGate?.passed === true || workspaceHasOutput)
2904
+ ? previewGate?.passed === true && previewGate?.skipped !== true
3396
2905
  : true;
3397
2906
  if (!success) {
3398
- if (this.isLegacyAgentFallbackAllowed()) {
2907
+ if (previewGate?.required !== true && this.isLegacyAgentFallbackAllowed()) {
3399
2908
  if (spinner) {
3400
2909
  spinner.stop();
3401
2910
  }
@@ -3408,25 +2917,16 @@ export class ChatCommand {
3408
2917
  if (spinner) {
3409
2918
  spinner.stop();
3410
2919
  }
3411
- this.logger.error(errorMessage);
2920
+ if (!this.jsonOutput)
2921
+ this.logger.error(errorMessage);
3412
2922
  this.messages.push({ role: 'assistant', content: errorMessage });
3413
- if (this.jsonOutput) {
3414
- process.exitCode = 1;
3415
- console.log(JSON.stringify({
3416
- success: false,
3417
- mode: 'agent',
3418
- model: routingPolicy.selectedModel,
3419
- partial: false,
3420
- content: '',
3421
- error: errorMessage,
3422
- metadata: { executionPath: 'v3-agent', previewGate },
3423
- }, null, 2));
3424
- }
3425
2923
  watcher?.stop();
3426
- return true;
3427
- }
3428
- if (!this.jsonOutput && previewGate?.required && previewGate?.passed !== true && workspaceHasOutput) {
3429
- console.log(chalk.yellow(`Template Service preview gate did not fully validate this output, but generated workspace files were preserved${previewGate?.error ? `: ${previewGate.error}` : '.'}`));
2924
+ throw new CliCommandError(errorMessage, {
2925
+ code: 'AGENT_PREVIEW_FAILED',
2926
+ category: previewGate?.partialMutation === false ? 'execution' : 'partial_mutation',
2927
+ partialMutation: previewGate?.partialMutation ?? 'unknown',
2928
+ details: { executionPath: 'v3-agent', previewGate },
2929
+ });
3430
2930
  }
3431
2931
  if (!this.jsonOutput && previewGate?.required) {
3432
2932
  if (previewGate.passed) {
@@ -3459,7 +2959,10 @@ export class ChatCommand {
3459
2959
  liveOutcome.answerContent = '';
3460
2960
  }
3461
2961
  const runEvaluation = evaluateExecutorSuccess(liveOutcome);
3462
- let executorSucceeded = runEvaluation.executorSucceeded;
2962
+ let executorSucceeded = runEvaluation.executorSucceeded && success;
2963
+ if (!success && !liveOutcome.executorError) {
2964
+ liveOutcome.executorError = previewGate?.error || 'Template Service preview proof did not pass.';
2965
+ }
3463
2966
  let finalAnswer = answerContent;
3464
2967
  if (isToolEvidenceStubAnswer(finalAnswer)) {
3465
2968
  finalAnswer = '';
@@ -3479,7 +2982,7 @@ export class ChatCommand {
3479
2982
  executorSucceeded = false;
3480
2983
  }
3481
2984
  if (!this.jsonOutput && !this.directPromptMode) {
3482
- if (this.v3StreamedAnswerDisplayed) {
2985
+ if (this.streamState.streamedAnswerDisplayed) {
3483
2986
  // Highlighted report already printed once above.
3484
2987
  }
3485
2988
  else if (isSubstantiveAgentAnswer(finalAnswer)) {
@@ -3575,9 +3078,6 @@ export class ChatCommand {
3575
3078
  }
3576
3079
  taskDisplay.finalize();
3577
3080
  // ────────────────────────────────────────────────────────────────
3578
- if (!executorSucceeded) {
3579
- process.exitCode = 1;
3580
- }
3581
3081
  this.commitAgentRunOutcome({
3582
3082
  prompt,
3583
3083
  originalPrompt: null,
@@ -3618,10 +3118,24 @@ export class ChatCommand {
3618
3118
  this.printAgentRunSummary(this.lastAgentRunOutcome, runEvaluation, changedFileCount);
3619
3119
  }
3620
3120
  }
3121
+ if (!executorSucceeded) {
3122
+ throw new CliCommandError(runEvaluation.statusHeadline, {
3123
+ code: 'AGENT_INCOMPLETE',
3124
+ category: response.partial === true || liveOutcome.tasksSucceeded > 0 ? 'partial_mutation' : 'execution',
3125
+ partialMutation: response.partial === true || liveOutcome.tasksSucceeded > 0 ? 'unknown' : false,
3126
+ details: {
3127
+ mode: 'agent',
3128
+ model: routingPolicy.selectedModel,
3129
+ taskId: response.taskId || null,
3130
+ contextId: response.contextId || null,
3131
+ statusHeadline: runEvaluation.statusHeadline,
3132
+ tasksSucceeded: liveOutcome.tasksSucceeded,
3133
+ tasksTotal: liveOutcome.tasksTotal,
3134
+ previewGate,
3135
+ },
3136
+ });
3137
+ }
3621
3138
  if (this.jsonOutput) {
3622
- if (!executorSucceeded) {
3623
- process.exitCode = 1;
3624
- }
3625
3139
  emitAgentJsonOutput({
3626
3140
  success: executorSucceeded,
3627
3141
  mode: 'agent',
@@ -3654,6 +3168,11 @@ export class ChatCommand {
3654
3168
  catch (error) {
3655
3169
  this.stopV3IdleWatch();
3656
3170
  watcher?.stop();
3171
+ if (error instanceof CliCommandError) {
3172
+ if (spinner)
3173
+ spinner.stop();
3174
+ throw error;
3175
+ }
3657
3176
  if (!this.api.hasAgentWorkspaceOutput(workspaceContext)) {
3658
3177
  const recovered = await this.tryRecoverV3ServiceAndRetry(executionPrompt, prompt, workspaceContext, routingPolicy, spinner, error);
3659
3178
  if (recovered) {
@@ -3720,25 +3239,7 @@ export class ChatCommand {
3720
3239
  unfinishedTaskIds: [...liveOutcome.unfinishedTaskIds],
3721
3240
  error: liveOutcome.executorError || safeDetail || 'Connection aborted',
3722
3241
  });
3723
- if (!this.jsonOutput) {
3724
- process.exitCode = 1;
3725
- }
3726
- if (this.jsonOutput) {
3727
- process.exitCode = 1;
3728
- console.log(JSON.stringify({
3729
- success: false,
3730
- mode: 'agent',
3731
- model: routingPolicy.selectedModel,
3732
- partial: liveOutcome.tasksSucceeded > 0 && liveOutcome.tasksTotal > 0,
3733
- content: '',
3734
- error: errorMessage,
3735
- statusHeadline: evaluateExecutorSuccess(liveOutcome).statusHeadline,
3736
- tasksSucceeded: liveOutcome.tasksSucceeded,
3737
- tasksTotal: liveOutcome.tasksTotal,
3738
- metadata: { executionPath: 'v3-agent' },
3739
- }, null, 2));
3740
- }
3741
- else if (!this.directPromptMode) {
3242
+ if (!this.jsonOutput && !this.directPromptMode) {
3742
3243
  const failedEval = evaluateExecutorSuccess({
3743
3244
  ...liveOutcome,
3744
3245
  executorFailed: true,
@@ -3751,7 +3252,20 @@ export class ChatCommand {
3751
3252
  this.printAgentRunSummary(this.lastAgentRunOutcome, failedEval, 0);
3752
3253
  }
3753
3254
  }
3754
- return true;
3255
+ throw new CliCommandError(errorMessage, {
3256
+ code: 'AGENT_EXECUTION_FAILED',
3257
+ category: liveOutcome.tasksSucceeded > 0 ? 'partial_mutation' : 'execution',
3258
+ partialMutation: liveOutcome.tasksSucceeded > 0 ? 'unknown' : false,
3259
+ details: {
3260
+ executionPath: 'v3-agent',
3261
+ mode: 'agent',
3262
+ model: routingPolicy.selectedModel,
3263
+ tasksSucceeded: liveOutcome.tasksSucceeded,
3264
+ tasksTotal: liveOutcome.tasksTotal,
3265
+ statusHeadline: evaluateExecutorSuccess(liveOutcome).statusHeadline,
3266
+ },
3267
+ cause: error,
3268
+ });
3755
3269
  }
3756
3270
  }
3757
3271
  async tryRecoverV3ServiceAndRetry(executionPrompt, rawPrompt, workspaceContext, routingPolicy, spinner, error) {
@@ -3765,12 +3279,13 @@ export class ChatCommand {
3765
3279
  if (!this.jsonOutput) {
3766
3280
  console.log(chalk.yellow(`V3 recovery: ${recovery.message} Retrying once...`));
3767
3281
  }
3768
- this.v3IterationCount = 0;
3769
- this.v3ToolCallCount = 0;
3770
- this.v3LastActivity = Date.now();
3771
- this.v3StreamingStarted = false;
3772
- this.v3StartSeen = false;
3282
+ this.streamState.iterationCount = 0;
3283
+ this.streamState.toolCallCount = 0;
3284
+ this.streamState.noteActivity();
3285
+ this.streamState.streamingStarted = false;
3286
+ this.streamState.startSeen = false;
3773
3287
  try {
3288
+ this.updateAgentExecutionCheckpoint({ status: 'running' });
3774
3289
  const retryPriorPrompt = this.getPreviousActionablePrompt();
3775
3290
  const retryContext = { priorPrompt: retryPriorPrompt || null };
3776
3291
  const retryTaskType = inferAgentTaskTypeWithContext(rawPrompt, retryContext);
@@ -3797,27 +3312,40 @@ export class ChatCommand {
3797
3312
  if (spinner && spinner.isSpinning) {
3798
3313
  spinner.stop();
3799
3314
  }
3800
- if (this.v3StreamingStarted) {
3315
+ if (this.streamState.streamingStarted) {
3801
3316
  process.stdout.write('\n');
3802
3317
  }
3318
+ if (retryResponse.partial === true) {
3319
+ this.updateAgentExecutionCheckpoint({
3320
+ status: 'failed',
3321
+ contextId: retryResponse.contextId || this.currentSession?.activeAgentRun?.contextId,
3322
+ error: 'Recovery run returned a partial result.',
3323
+ });
3324
+ return false;
3325
+ }
3803
3326
  if (this.jsonOutput) {
3804
- console.log(JSON.stringify({
3327
+ emitAgentJsonOutput({
3805
3328
  success: true,
3806
3329
  mode: 'agent',
3807
3330
  model: routingPolicy.selectedModel,
3808
3331
  routingPolicy,
3809
3332
  taskId: retryResponse.taskId || null,
3810
3333
  contextId: retryResponse.contextId || null,
3811
- partial: retryResponse.partial === true,
3334
+ partial: false,
3812
3335
  content: retryResponse.content || 'V3 agent workflow completed after recovery.',
3813
3336
  metadata: { ...(retryResponse.metadata || {}), recoveryAttempted: true },
3814
- }, null, 2));
3337
+ });
3815
3338
  }
3816
- else if (!this.v3StreamingStarted && retryResponse.content) {
3339
+ else if (!this.streamState.streamingStarted && retryResponse.content) {
3817
3340
  console.log(chalk.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
3818
3341
  console.log(retryResponse.content);
3819
3342
  }
3820
3343
  this.messages.push({ role: 'assistant', content: retryResponse.content || 'V3 agent workflow completed after recovery.' });
3344
+ this.updateAgentExecutionCheckpoint({
3345
+ status: 'completed',
3346
+ contextId: retryResponse.contextId || this.currentSession?.activeAgentRun?.contextId,
3347
+ error: null,
3348
+ });
3821
3349
  return true;
3822
3350
  }
3823
3351
  catch {
@@ -4424,6 +3952,13 @@ export class ChatCommand {
4424
3952
  }
4425
3953
  inferTargetFilesFromPrompt(prompt) {
4426
3954
  const candidates = [];
3955
+ const sourceExtensions = new Set([
3956
+ '.c', '.cc', '.cpp', '.cs', '.css', '.csv', '.go', '.graphql', '.h', '.hpp',
3957
+ '.htm', '.html', '.ini', '.java', '.js', '.json', '.jsx', '.less', '.md',
3958
+ '.mjs', '.php', '.prisma', '.py', '.rb', '.rs', '.sass', '.scss', '.sh',
3959
+ '.sql', '.svelte', '.svg', '.toml', '.ts', '.tsx', '.txt', '.vue', '.xml',
3960
+ '.yaml', '.yml',
3961
+ ]);
4427
3962
  const matches = Array.from(prompt.matchAll(/([A-Za-z0-9_./-]+\.[A-Za-z0-9_-]+)/g));
4428
3963
  for (const match of matches) {
4429
3964
  const candidate = match[1];
@@ -4431,7 +3966,15 @@ export class ChatCommand {
4431
3966
  continue;
4432
3967
  }
4433
3968
  const resolved = path.resolve(this.currentProjectPath, candidate);
4434
- if (resolved.startsWith(this.currentProjectPath)) {
3969
+ const extension = path.extname(candidate).toLowerCase();
3970
+ // Dotted API/property references (for example `element.style`) are not
3971
+ // workspace files. Accept a non-standard extension only when that exact
3972
+ // path already exists, so real uncommon project files remain addressable.
3973
+ if (!sourceExtensions.has(extension) && !fs.existsSync(resolved)) {
3974
+ continue;
3975
+ }
3976
+ const relative = path.relative(this.currentProjectPath, resolved);
3977
+ if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) {
4435
3978
  if (!candidates.includes(candidate)) {
4436
3979
  candidates.push(candidate);
4437
3980
  }
@@ -4442,36 +3985,6 @@ export class ChatCommand {
4442
3985
  inferTargetFileFromPrompt(prompt) {
4443
3986
  return this.inferTargetFilesFromPrompt(prompt)[0] || null;
4444
3987
  }
4445
- workspaceContainsHtmlEntry(rootDir) {
4446
- const stack = [rootDir];
4447
- while (stack.length > 0) {
4448
- const currentDir = stack.pop();
4449
- if (!currentDir) {
4450
- continue;
4451
- }
4452
- let entries = [];
4453
- try {
4454
- entries = fs.readdirSync(currentDir, { withFileTypes: true });
4455
- }
4456
- catch {
4457
- continue;
4458
- }
4459
- for (const entry of entries) {
4460
- if (entry.name === 'node_modules' || entry.name === '.git') {
4461
- continue;
4462
- }
4463
- const fullPath = path.join(currentDir, entry.name);
4464
- if (entry.isDirectory()) {
4465
- stack.push(fullPath);
4466
- continue;
4467
- }
4468
- if (entry.isFile() && entry.name.toLowerCase().endsWith('.html')) {
4469
- return true;
4470
- }
4471
- }
4472
- }
4473
- return false;
4474
- }
4475
3988
  shouldBypassDirectSingleFileFlow(targetFile, prompt) {
4476
3989
  const referencedFiles = this.inferTargetFilesFromPrompt(prompt);
4477
3990
  const editableFiles = referencedFiles.filter((filePath) => !this.isProtectedFileReferenceSafe(prompt, filePath));
@@ -4488,36 +4001,6 @@ export class ChatCommand {
4488
4001
  return true;
4489
4002
  return false;
4490
4003
  }
4491
- shouldPreferLocalAgentLoop(prompt) {
4492
- if (this.shouldRequireV3AgentWorkflow(prompt)) {
4493
- return false;
4494
- }
4495
- const forceV3 = /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_FORCE_V3_AGENT || ''));
4496
- if (forceV3) {
4497
- return false;
4498
- }
4499
- if (this.isBrowserTaskPrompt(prompt)) {
4500
- return false;
4501
- }
4502
- // Product default: Agent mode routes through V3.
4503
- // Local loop is opt-in for explicit offline/debug scenarios only.
4504
- // Explicit local paths now influence project root resolution, not routing mode.
4505
- const preferLocalOptIn = /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_PREFER_LOCAL_AGENT_LOOP || ''));
4506
- if (!preferLocalOptIn) {
4507
- return false;
4508
- }
4509
- const runtime = this.getRuntimeEnvironmentContext();
4510
- if (this.directPromptMode && this.isRepoGroundedPrompt(prompt)) {
4511
- return false;
4512
- }
4513
- if (!runtime.serverBindableWorkspace) {
4514
- return true;
4515
- }
4516
- if (!this.directPromptMode) {
4517
- return true;
4518
- }
4519
- return runtime.platform !== 'linux';
4520
- }
4521
4004
  getRuntimeEnvironmentContext() {
4522
4005
  const osPlatform = os.platform();
4523
4006
  const platform = osPlatform === 'win32'
@@ -4605,16 +4088,6 @@ export class ChatCommand {
4605
4088
  ];
4606
4089
  return protectedPhrases.some((phrase) => normalizedPrompt.includes(phrase));
4607
4090
  }
4608
- isProtectedFileReference(prompt, filePath) {
4609
- const escapedPath = filePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
4610
- const protectedPatterns = [
4611
- new RegExp(`do not modify\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?`, 'i'),
4612
- new RegExp(`don't modify\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?`, 'i'),
4613
- new RegExp(`leave\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?\\s+unchanged`, 'i'),
4614
- new RegExp(`without modifying\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?`, 'i'),
4615
- ];
4616
- return protectedPatterns.some((pattern) => pattern.test(prompt));
4617
- }
4618
4091
  buildContinuationPrompt() {
4619
4092
  const { discovery, searchFailed } = this.agentToolEvidence;
4620
4093
  const evidenceLines = [];
@@ -4708,109 +4181,10 @@ export class ChatCommand {
4708
4181
  return lines.join('\n');
4709
4182
  }
4710
4183
  extractToolCalls(message) {
4711
- const calls = [];
4712
- const seen = new Set();
4713
- const addCall = (call) => {
4714
- if (!call)
4715
- return;
4716
- const key = `${call.tool}::${JSON.stringify(call.args)}`;
4717
- if (!seen.has(key)) {
4718
- seen.add(key);
4719
- calls.push(call);
4720
- }
4721
- };
4722
- const wrapperRegex = /<tool_call>([\s\S]*?)<\/tool_call>/g;
4723
- for (const match of message.matchAll(wrapperRegex)) {
4724
- addCall(this.parseToolPayload(match[1] || ''));
4725
- }
4726
- for (const call of this.parseLegacyFunctionToolCalls(message)) {
4727
- addCall(call);
4728
- }
4729
- return calls;
4730
- }
4731
- normalizeCliToolName(name) {
4732
- const normalized = name.trim().toLowerCase();
4733
- const aliases = {
4734
- list_directory: 'list_dir',
4735
- listdirectory: 'list_dir',
4736
- ls: 'list_dir',
4737
- dir: 'list_dir',
4738
- readfile: 'read_file',
4739
- writefile: 'write_file',
4740
- editfile: 'edit_file',
4741
- shell: 'bash',
4742
- command: 'bash',
4743
- run_command: 'bash',
4744
- };
4745
- return aliases[normalized] || normalized;
4746
- }
4747
- parseLegacyFunctionToolCalls(message) {
4748
- const calls = [];
4749
- const functionRegex = /<function\s*=\s*["']?([A-Za-z0-9_-]+)["']?>\s*([\s\S]*?)(?:<\/function>|$)/gi;
4750
- for (const match of message.matchAll(functionRegex)) {
4751
- const tool = this.normalizeCliToolName(match[1] || '');
4752
- if (!tool)
4753
- continue;
4754
- const body = match[2] || '';
4755
- const args = {};
4756
- const parameterRegex = /<parameter\s*=\s*["']?([A-Za-z0-9_-]+)["']?>\s*([\s\S]*?)(?=<parameter\s*=|<\/function>|<\/parameter>|$)/gi;
4757
- for (const paramMatch of body.matchAll(parameterRegex)) {
4758
- const key = paramMatch[1] || '';
4759
- if (!key)
4760
- continue;
4761
- args[key] = (paramMatch[2] || '').replace(/<\/parameter>\s*$/i, '').trim();
4762
- }
4763
- calls.push({ tool, args });
4764
- }
4765
- return calls;
4766
- }
4767
- parseToolPayload(payload) {
4768
- const normalized = payload.trim();
4769
- if (!normalized) {
4770
- return null;
4771
- }
4772
- try {
4773
- const parsed = JSON.parse(normalized);
4774
- if (typeof parsed.tool !== 'string') {
4775
- return null;
4776
- }
4777
- const args = {};
4778
- if (parsed.args && typeof parsed.args === 'object') {
4779
- for (const [key, value] of Object.entries(parsed.args)) {
4780
- args[key] = typeof value === 'string' ? value : JSON.stringify(value);
4781
- }
4782
- }
4783
- return { tool: this.normalizeCliToolName(parsed.tool), args };
4784
- }
4785
- catch {
4786
- return null;
4787
- }
4184
+ return this.toolCallParser.extract(message);
4788
4185
  }
4789
4186
  stripToolPayloads(message) {
4790
- return message
4791
- .replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '')
4792
- .replace(/<function\s*=\s*["']?[A-Za-z0-9_-]+["']?>[\s\S]*?(?:<\/function>|$)/gi, '')
4793
- .trim();
4794
- }
4795
- extractFinalFileContent(message, targetFile) {
4796
- const trimmed = message.trim();
4797
- if (!trimmed) {
4798
- return '';
4799
- }
4800
- const fencedMatch = trimmed.match(/```(?:[A-Za-z0-9_-]+)?\n([\s\S]*?)```/);
4801
- if (fencedMatch && fencedMatch[1]) {
4802
- return fencedMatch[1].trim();
4803
- }
4804
- const extension = path.extname(targetFile).toLowerCase();
4805
- if (extension === '.html') {
4806
- const htmlStart = trimmed.indexOf('<!DOCTYPE html>') >= 0
4807
- ? trimmed.indexOf('<!DOCTYPE html>')
4808
- : trimmed.indexOf('<html');
4809
- if (htmlStart >= 0) {
4810
- return trimmed.slice(htmlStart).trim();
4811
- }
4812
- }
4813
- return trimmed;
4187
+ return this.toolCallParser.strip(message);
4814
4188
  }
4815
4189
  /**
4816
4190
  * Synthesize a best-effort answer from tool evidence already collected
@@ -4839,8 +4213,8 @@ export class ChatCommand {
4839
4213
  resolveDirectModeCompletion(prompt, visibleText) {
4840
4214
  // Sanitize first — strip tool output and echoed instructions before
4841
4215
  // deciding if the model actually answered the question.
4842
- const sanitized = this.sanitizeDirectModeOutput((visibleText || '').trim());
4843
- if (sanitized && !this.isDirectModeFollowUpQuestion(sanitized)) {
4216
+ const sanitized = sanitizeDirectModeOutput((visibleText || '').trim());
4217
+ if (sanitized && !isDirectModeFollowUpQuestion(sanitized)) {
4844
4218
  return sanitized;
4845
4219
  }
4846
4220
  const fallback = this.buildLocalAnalysisFallback(prompt);
@@ -4849,95 +4223,6 @@ export class ChatCommand {
4849
4223
  }
4850
4224
  return sanitized || 'Task complete.';
4851
4225
  }
4852
- /**
4853
- * Strip system-prompt echoes, tool execution headers, grounding-rule
4854
- * parroting, recovery banners, and multi-line tool output blocks from
4855
- * the model's final answer so that --json output contains only the
4856
- * substantive answer.
4857
- */
4858
- sanitizeDirectModeOutput(text) {
4859
- let cleaned = this.stripHiddenThoughtBlocks(text);
4860
- cleaned = cleaned
4861
- .replace(/<\|mask_start\|>[\s\S]*?<\|mask_end\|>/g, '')
4862
- .replace(/<think>[\s\S]*?<\/think>/gi, '')
4863
- .replace(/<\|(?:mask_start|mask_end)\|>/g, '');
4864
- // ── Phase 1: Strip entire tool-output blocks ──
4865
- // Matches "Tool <name> succeeded/FAILED." through the next blank line,
4866
- // next tool header, or end-of-string. The DOTALL-like [\s\S]*? is
4867
- // terminated by whichever boundary comes first.
4868
- cleaned = cleaned.replace(/Tool (?:read_file|grep|list_dir|glob|bash|write_file|edit_file|ssh_exec|task|multi_edit|codebase_search) (?:succeeded|FAILED)\.[\s\S]*?(?=\nTool |\n\n|$)/g, '');
4869
- // ── Phase 2: Strip echoed system-prompt / grounding lines ──
4870
- const contaminationPatterns = [
4871
- /^\[Agent recovered from backend failure[^\]]*\]\s*/m,
4872
- /^Evidence gathered before backend failure:?\s*/m,
4873
- /^MANDATORY CROSS-FILE EVIDENCE[^\n]*\n?/m,
4874
- /^CONFIRMED CONFLICTING keys[^\n]*/m,
4875
- /^Keys found ONLY in [^\n]*/m,
4876
- /^CONSTRAINT: Your answer MUST[^\n]*/m,
4877
- /^GROUNDING CHECK:[^\n]*/m,
4878
- /^VERIFICATION PROTOCOL[^\n]*/m,
4879
- /^Quality gate:[^\n]*/m,
4880
- /^Evidence collected:[^\n]*/m,
4881
- /^Warning: \d+ search tool[^\n]*/m,
4882
- /^Tool results received for direct mode[^\n]*/m,
4883
- /^Step \d+ complete\. Task:[^\n]*/m,
4884
- /^Original user request:[^\n]*/m,
4885
- /^Project root boundary:[^\n]*/m,
4886
- /^Do not declare success[^\n]*/m,
4887
- /^Keep working from concrete[^\n]*/m,
4888
- /^Continue with tool calls if more[^\n]*/m,
4889
- /^IMPORTANT: Your response must be ONLY[^\n]*/m,
4890
- /^Because this is a debugging[^\n]*/m,
4891
- /^If a user is asking which file[^\n]*/m,
4892
- /^If the request is already[^\n]*/m,
4893
- /^If more work is required[^\n]*/m,
4894
- /^Do not ask follow-up[^\n]*/m,
4895
- /^CRITICAL GROUNDING RULE:[^\n]*/m,
4896
- /^CROSS-FILE ATTRIBUTION:[^\n]*/m,
4897
- /^EVIDENCE-GROUNDING RULE:[^\n]*/m,
4898
- /^CROSS-FILE RULE:[^\n]*/m,
4899
- /^OUTPUT DISCIPLINE:[^\n]*/m,
4900
- /^Vigthoria CLI agent operating contract\.[^\n]*/m,
4901
- /^You are operating inside the project root:[^\n]*/m,
4902
- /^CRITICAL: Begin working on the user's task[^\n]*/m,
4903
- /^File: \S+\s*$/m,
4904
- /^Search status: \S+\s*$/m,
4905
- /^Output:\s*$/m,
4906
- /^\[\.\.\. ?truncated\]\s*$/m,
4907
- /^---\s*$/m,
4908
- ];
4909
- for (const pat of contaminationPatterns) {
4910
- cleaned = cleaned.replace(pat, '');
4911
- }
4912
- // ── Phase 3: Paragraph-level filter ──
4913
- // Split into paragraph blocks and discard any that are pure tool
4914
- // output, file content dumps, or system-instruction echoes.
4915
- const paragraphs = cleaned.split(/\n\n+/);
4916
- const kept = paragraphs.filter(p => {
4917
- const t = p.trim();
4918
- if (!t)
4919
- return false;
4920
- // Discard lingering tool headers
4921
- if (/^Tool (?:read_file|grep|list_dir|glob|bash|write_file) /i.test(t))
4922
- return false;
4923
- // Discard paragraphs that are mostly file-content dumps (many
4924
- // lines of code with typical code tokens like { } ; = function class)
4925
- const lines = t.split('\n');
4926
- if (lines.length > 6) {
4927
- const codeLines = lines.filter(l => /[{};=]|function |class |const |let |var |import |export |switch |case /.test(l));
4928
- if (codeLines.length / lines.length > 0.4)
4929
- return false;
4930
- }
4931
- return true;
4932
- });
4933
- cleaned = kept.join('\n\n');
4934
- // Collapse multiple blank lines
4935
- cleaned = cleaned.replace(/\n{3,}/g, '\n\n').trim();
4936
- return cleaned || this.stripHiddenThoughtBlocks(text);
4937
- }
4938
- isDirectModeFollowUpQuestion(text) {
4939
- return /^(would you like me|do you want me|which aspect|what aspect|can you clarify|could you clarify|should i focus on|i will follow|i understand|i('ll| will) adhere|provide your|waiting for)/i.test(text.trim());
4940
- }
4941
4226
  buildLocalAnalysisFallback(prompt) {
4942
4227
  if (!/(analyse|analyze|audit|overview|inspect|review|summari[sz]e|actual state)/i.test(prompt)) {
4943
4228
  return '';
@@ -4979,42 +4264,6 @@ export class ChatCommand {
4979
4264
  return '';
4980
4265
  }
4981
4266
  }
4982
- tryDeterministicSingleFileRewrite(prompt, targetFile, currentContent) {
4983
- const extension = path.extname(targetFile).toLowerCase();
4984
- if (extension !== '.html') {
4985
- return null;
4986
- }
4987
- const exactText = this.extractExactTextRequirement(prompt);
4988
- if (!exactText) {
4989
- return null;
4990
- }
4991
- if (!/hero\s+(?:paragraph|copy)/i.test(prompt)) {
4992
- return null;
4993
- }
4994
- const heroCopyPattern = /(<p[^>]*class=["'][^"']*hero-copy[^"']*["'][^>]*>)([\s\S]*?)(<\/p>)/i;
4995
- if (heroCopyPattern.test(currentContent)) {
4996
- const nextContent = currentContent.replace(heroCopyPattern, `$1${exactText}$3`);
4997
- return nextContent !== currentContent ? nextContent : null;
4998
- }
4999
- const heroParagraphPattern = /(<main[^>]*class=["'][^"']*hero[^"']*["'][^>]*>[\s\S]*?<p[^>]*>)([\s\S]*?)(<\/p>)/i;
5000
- if (heroParagraphPattern.test(currentContent)) {
5001
- const nextContent = currentContent.replace(heroParagraphPattern, `$1${exactText}$3`);
5002
- return nextContent !== currentContent ? nextContent : null;
5003
- }
5004
- return null;
5005
- }
5006
- extractExactTextRequirement(prompt) {
5007
- const match = prompt.match(/reads exactly:\s*([\s\S]*?)(?:\s+Do not\b|\s+Do\b|$)/i);
5008
- if (!match || !match[1]) {
5009
- return null;
5010
- }
5011
- const normalized = match[1]
5012
- .trim()
5013
- .replace(/^[`"']+/, '')
5014
- .replace(/[`"']+$/, '')
5015
- .trim();
5016
- return normalized || null;
5017
- }
5018
4267
  async executeToolCalls(toolCalls) {
5019
4268
  if (!this.tools) {
5020
4269
  throw new Error('Agent tools are not initialized.');
@@ -5134,6 +4383,9 @@ export class ChatCommand {
5134
4383
  if (!this.currentSession?.activeAgentRun) {
5135
4384
  return;
5136
4385
  }
4386
+ if (update.status) {
4387
+ assertAgentStatusTransition(this.currentSession.activeAgentRun.status, update.status);
4388
+ }
5137
4389
  this.currentSession.activeAgentRun = {
5138
4390
  ...this.currentSession.activeAgentRun,
5139
4391
  ...update,
@@ -5145,13 +4397,16 @@ export class ChatCommand {
5145
4397
  if (this.autoApprove) {
5146
4398
  return true;
5147
4399
  }
4400
+ if (this.jsonOutput) {
4401
+ return false;
4402
+ }
5148
4403
  const rl = readline.createInterface({
5149
4404
  input: process.stdin,
5150
4405
  output: process.stdout,
5151
4406
  });
5152
4407
  console.log(action);
5153
4408
  const answer = await new Promise((resolve) => {
5154
- rl.question(chalk.yellow('Approve? [y]es / [n]o / [a]ll this turn / [p]ersist: '), resolve);
4409
+ rl.question(chalk.yellow('Approve? [y]es / [n]o / [a] exact operation this turn / [p]ersist exact operation: '), resolve);
5155
4410
  });
5156
4411
  rl.close();
5157
4412
  const normalized = answer.trim().toLowerCase();