vigthoria-cli 1.13.18 → 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 +84 -135
  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 +5 -42
  12. package/dist/commands/chat.js +462 -1122
  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 +32 -42
  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 -1498
  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 +45 -269
  71. package/dist/utils/api.js +664 -3210
  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 +227 -99
  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 +6 -7
  106. package/dist/utils/fastAgentRouter.js +21 -30
  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 +34 -2
  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 +86 -143
  167. package/install.sh +76 -81
  168. package/package.json +44 -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 -172
  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
@@ -3,27 +3,36 @@ import * as fs from 'fs';
3
3
  import * as os from 'os';
4
4
  import * as path from 'path';
5
5
  import * as readline from 'readline';
6
+ import { randomUUID } from 'crypto';
6
7
  import { createSpinner } from '../utils/logger.js';
7
- 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';
8
10
  import { AgenticTools, robustifyStreamResponse } from '../utils/tools.js';
9
- import { SessionManager } from '../utils/session.js';
11
+ import { SessionManager, assertAgentStatusTransition } from '../utils/session.js';
10
12
  import { BridgeClient, getBridgeClient } from '../utils/bridge-client.js';
11
13
  import { WorkspaceWatcher } from '../utils/workspace-stream.js';
12
14
  import { TaskDisplay } from '../utils/task-display.js';
13
15
  import { ProjectMemoryService } from '../utils/project-memory.js';
14
16
  import { WorkspaceBrainService } from '../utils/workspace-brain-service.js';
17
+ import { assembleContextState, contextLayer } from '../utils/context-state.js';
15
18
  import { buildPersonaOverlay, normalizePersonaMode } from '../utils/persona.js';
16
19
  import { runAgentSessionMenu, shouldShowAgentSessionMenu } from './agent-session-menu.js';
17
20
  import { V4Command } from './v4.js';
18
21
  import { renderDynamicHelp } from '../utils/command-menu.js';
19
- 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';
20
25
  import { looksLikeMarkdownReport, renderMarkdownToTerminal, summarizeMarkdownReport, } from '../utils/terminalMarkdown.js';
21
26
  import { emitDeckEvent, isDeckModeEnabled } from '../utils/deckEvents.js';
22
27
  import { formatGoaSystemGrounding, splitGoaContextFromInput, } from '../utils/goaEvents.js';
23
- 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';
24
29
  import { resolveAgentRoute } from '../utils/agentRoute.js';
25
- import { runTemplateInstantPath } from '../utils/templateInstantPath.js';
26
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';
27
36
  // Stream health policy (corrected 2026-07-04 after TestFarm incident with
28
37
  // Gideon Lenz / C:\vigthoria\Apps\monopoly): agentic tasks have no
29
38
  // predictable duration, so there is intentionally NO overall wall-clock
@@ -62,7 +71,46 @@ const DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS = (() => {
62
71
  })();
63
72
  /** Block-write agent JSON to VIGTHORIA_AGENT_OUTPUT (Windows KVM fsync path). */
64
73
  function emitAgentJsonOutput(payload) {
65
- 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`;
66
114
  console.log(text.trimEnd());
67
115
  const outPath = process.env.VIGTHORIA_AGENT_OUTPUT?.trim();
68
116
  if (!outPath) {
@@ -99,6 +147,8 @@ export class ChatCommand {
99
147
  modelExplicitlySelected = false;
100
148
  autoApprove = false;
101
149
  personaOverride = null;
150
+ toolCallParser = new ChatToolCallParser();
151
+ promptPolicy;
102
152
  // Phase 5: Agent quality gate — track tool usage for evidence thresholds
103
153
  agentToolEvidence = { discovery: 0, mutation: 0, searchFailed: 0 };
104
154
  operatorMode = false;
@@ -108,12 +158,7 @@ export class ChatCommand {
108
158
  modelGovernanceFallback = null;
109
159
  retryPromptSignature = null;
110
160
  retryPromptStreak = 0;
111
- v3SuppressThinkingStream = false;
112
- v3StreamedAnswerBuffer = '';
113
- v3StreamedAnswerDisplayed = false;
114
- v3SeenToolCalls = new Set();
115
- v3SeenToolResults = new Set();
116
- v3StartSeen = false;
161
+ streamState;
117
162
  lastAgentRoute = null;
118
163
  pendingGoaContext = null;
119
164
  // Last completed Agent run — used by /retry, /continue, and the final summary block.
@@ -372,16 +417,6 @@ export class ChatCommand {
372
417
  routeReason: 'dispatcher-eligible-agent-default',
373
418
  };
374
419
  }
375
- if (this.lastAgentRoute?.path === 'template-instant') {
376
- return {
377
- selectedModel: 'agent',
378
- explicitModel: false,
379
- heavyTask,
380
- cloudEligible,
381
- cloudSelected: false,
382
- routeReason: `template-instant [${this.lastAgentRoute.source}]`,
383
- };
384
- }
385
420
  if (requiresV3Workflow) {
386
421
  return {
387
422
  selectedModel: 'agent',
@@ -456,131 +491,37 @@ export class ChatCommand {
456
491
  return buildPersonaOverlay(this.getActivePersonaMode(), this.getLastUserPrompt());
457
492
  }
458
493
  isDiagnosticPrompt(prompt) {
459
- if (promptRequiresWorkspaceChanges(prompt))
460
- return false;
461
- 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);
462
495
  }
463
496
  /**
464
497
  * Returns true when the prompt is a simple lookup / analysis / read-only
465
498
  * question — these should use analysis_only workflow, not full_autonomy.
466
499
  */
467
- isAnalysisLookupPrompt(prompt) {
468
- if (promptRequiresWorkspaceChanges(prompt))
469
- return false;
470
- const trimmed = prompt.trim();
471
- 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)) {
472
- return true;
473
- }
474
- // Mid-conversation accountability / lookup questions (Gideon regression)
475
- 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);
476
- }
477
- extractExplicitLocalPath(prompt) {
478
- // Try to extract Windows paths (C:\ D:\ etc.)
479
- // Match drive letter followed by colon and path
480
- const windowsMatch = prompt.match(/([A-Za-z]:[\\\/][^\s<>"|?*]*)/);
481
- if (windowsMatch) {
482
- const candidatePath = windowsMatch[1].replace(/\\/g, '\\').trim();
483
- try {
484
- const resolved = require('path').resolve(candidatePath);
485
- const fs = require('fs');
486
- if (fs.existsSync(resolved)) {
487
- return resolved;
488
- }
489
- }
490
- catch {
491
- // Path resolution failed
492
- }
493
- }
494
- // Try to extract Unix paths: / followed by any characters until we hit quotes, angle brackets, pipes, or sentence boundaries
495
- // Supports spaces, special chars, camelCase, etc. (anything a filesystem path might have)
496
- if (!prompt.includes('http://') && !prompt.includes('https://') && !prompt.includes('ftp://')) {
497
- // Match / and everything after it until we hit delimiters or common sentence words
498
- const unixMatch = prompt.match(/(\/(?:[^\s<>"|?*]|\s+(?![^\s<>"|?*]))*)/);
499
- if (unixMatch) {
500
- let candidatePath = unixMatch[1].trim();
501
- // Remove trailing common English words that aren't part of path
502
- candidatePath = candidatePath.replace(/\s+(and|or|at|in|the|to|for|with|from|by|on)$/i, '').trim();
503
- // Remove trailing punctuation
504
- candidatePath = candidatePath.replace(/[.,;!?:)\]]*$/, '').trim();
505
- if (candidatePath.length > 1) {
506
- try {
507
- const resolved = require('path').resolve(candidatePath);
508
- const fs = require('fs');
509
- if (fs.existsSync(resolved)) {
510
- return resolved;
511
- }
512
- }
513
- catch {
514
- // Path resolution failed
515
- }
516
- }
517
- }
518
- }
519
- return null;
520
- }
521
- isUnscopedPromptPathOverrideAllowed() {
522
- return /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_ALLOW_UNSCOPED_PROMPT_PATHS || ''));
523
- }
524
- isPathWithinRoot(candidatePath, rootPath) {
525
- const candidate = path.resolve(candidatePath);
526
- const root = path.resolve(rootPath);
527
- const rel = path.relative(root, candidate);
528
- return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
529
- }
530
- getPromptPathAllowedRoots(baseWorkspace) {
531
- const roots = new Set();
532
- const addRoot = (rawValue) => {
533
- const value = String(rawValue || '').trim();
534
- if (!value)
535
- return;
536
- const resolved = path.resolve(value);
537
- try {
538
- if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
539
- roots.add(resolved);
540
- }
541
- }
542
- catch {
543
- // ignore invalid or unreadable roots
544
- }
545
- };
546
- addRoot(baseWorkspace);
547
- addRoot(this.currentProjectPath);
548
- addRoot(process.cwd());
549
- addRoot(this.config.get('project')?.rootPath || null);
550
- const envRootsRaw = String(process.env.VIGTHORIA_ALLOWED_WORKSPACE_ROOTS || '').trim();
551
- if (envRootsRaw) {
552
- for (const entry of envRootsRaw.split(path.delimiter)) {
553
- addRoot(entry);
554
- }
555
- }
556
- return Array.from(roots);
557
- }
558
500
  resolvePromptWorkspacePath(prompt, baseWorkspace) {
559
- const explicitPath = this.extractExplicitLocalPath(prompt);
560
- if (!explicitPath) {
561
- return null;
562
- }
563
- const candidate = path.resolve(explicitPath);
564
- if (this.isUnscopedPromptPathOverrideAllowed()) {
565
- return candidate;
566
- }
567
- const allowedRoots = this.getPromptPathAllowedRoots(baseWorkspace);
568
- const isAllowed = allowedRoots.some((root) => this.isPathWithinRoot(candidate, root));
569
- if (isAllowed) {
570
- return candidate;
571
- }
572
- if (!this.jsonOutput) {
573
- console.log(chalk.yellow(`Ignoring path outside allowed workspace roots: ${candidate}`));
574
- if (allowedRoots.length > 0) {
575
- 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(', ');
576
517
  console.log(chalk.gray(`Allowed roots: ${displayRoots}`));
577
518
  }
578
519
  console.log(chalk.gray('To allow unrestricted prompt path overrides, set VIGTHORIA_ALLOW_UNSCOPED_PROMPT_PATHS=1.'));
579
520
  }
580
- return null;
521
+ return result.resolvedPath;
581
522
  }
582
523
  isBrowserTaskPrompt(prompt) {
583
- 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);
584
525
  }
585
526
  /**
586
527
  * Returns true when a prompt can be answered directly without the full
@@ -589,28 +530,7 @@ export class ChatCommand {
589
530
  * file scanning, or infrastructure changes.
590
531
  */
591
532
  isOperatorDirectAnswerable(prompt) {
592
- const trimmed = prompt.trim();
593
- // Infrastructure status pings are never repo-grounded analysis.
594
- if (/^(status(\s+check)?|health(\s+check)?|ping)$/i.test(trimmed)) {
595
- return true;
596
- }
597
- // ── Exclusion guard: any prompt that references code artefacts,
598
- // file paths, or analysis verbs MUST go through a tool-backed path
599
- // (BMAD or agent loop), never the toolless direct-answer shortcut.
600
- 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)
601
- || /\b(file|folder|directory|module|class|function|method|variable|handler|listener|binding|conflict|bug|issue|error)\b/i.test(trimmed)
602
- || /\b(inspect|analyze|analyse|audit|review|find|diagnose|debug|trace|compare|diff|check|investigate)\b/i.test(trimmed)
603
- || /\b(Camera|InputManager|keydown|KeyS|KeyA|KeyW|stopPropagation|addEventListener|handleKeyDown)\b/.test(trimmed)
604
- || /\/[a-zA-Z]/.test(trimmed) // slash-prefixed paths
605
- || /[A-Z][a-z]+\.[a-z_][a-zA-Z]+/.test(trimmed) // ClassName.methodName
606
- || /\b(key\s*code|keydown|keyup|key\s*bind|event\s*listener|event\s*handler|addEventListener|handleKey|onKey)\b/i.test(trimmed);
607
- if (repoGrounded)
608
- return false;
609
- // Only truly trivial prompts are direct-answerable: short echo/smoke
610
- // prompts and factual questions with no code or repo reference.
611
- const isTrivialEcho = trimmed.length < 120
612
- && /^(reply with|say|echo|respond with|return|ping|hello|hi|test|status|what is \d|how much is)\b/i.test(trimmed);
613
- return isTrivialEcho;
533
+ return this.promptPolicy.isOperatorDirectAnswerable(prompt);
614
534
  }
615
535
  /**
616
536
  * Returns true when the prompt references code artefacts, file paths,
@@ -619,51 +539,10 @@ export class ChatCommand {
619
539
  * instead of the toolless BMAD/direct-answer path.
620
540
  */
621
541
  isRepoGroundedPrompt(prompt) {
622
- const trimmed = prompt.trim();
623
- return /\b(src\/|\.js\b|\.ts\b|\.py\b|\.jsx\b|\.tsx\b|\.css\b|\.html\b|\.json\b)/i.test(trimmed)
624
- || /\b(file|folder|module|class|function|method|handler|listener|binding|conflict|bug|error)\b/i.test(trimmed)
625
- || /\b(inspect|analyze|analyse|audit|review|find|diagnose|debug|trace|compare|diff|check|investigate)\b/i.test(trimmed)
626
- || /\/[a-zA-Z]/.test(trimmed)
627
- || /[A-Z][a-z]+\.[a-z_][a-zA-Z]+/.test(trimmed) // ClassName.methodName patterns
628
- || /\b(key\s*code|keydown|keyup|key\s*bind|event\s*listener|event\s*handler|addEventListener|handleKey|onKey)\b/i.test(trimmed);
629
- }
630
- inferAgentTaskType(prompt) {
631
- if (this.isDiagnosticPrompt(prompt))
632
- return 'debugging';
633
- if (this.isAnalysisLookupPrompt(prompt))
634
- return 'analysis';
635
- return sharedInferAgentTaskType(prompt);
636
- }
637
- buildTaskShapingInstructions(prompt) {
638
- const instructions = [];
639
- const runtime = this.getRuntimeEnvironmentContext();
640
- // Platform-aware routing hints
641
- if (runtime.platform === 'windows') {
642
- 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.');
643
- }
644
- else if (runtime.platform === 'macos') {
645
- 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.');
646
- }
647
- else if (runtime.platform === 'linux') {
648
- 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.');
649
- }
650
- if (this.isAnalysisLookupPrompt(prompt)) {
651
- 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.');
652
- }
653
- if (this.isDiagnosticPrompt(prompt)) {
654
- 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.');
655
- }
656
- if (this.isBrowserTaskPrompt(prompt)) {
657
- 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.');
658
- }
659
- if (instructions.length === 0) {
660
- return '';
661
- }
662
- return instructions.join('\n');
542
+ return this.promptPolicy.isRepoGrounded(prompt);
663
543
  }
664
544
  buildExecutionPrompt(prompt) {
665
- const shaping = this.buildTaskShapingInstructions(prompt);
666
- return shaping ? `${prompt}\n\n${shaping}` : prompt;
545
+ return this.promptPolicy.buildExecutionPrompt(prompt);
667
546
  }
668
547
  isProjectBrainRuntimeDisabled() {
669
548
  return /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_NO_BRAIN || process.env.VIGTHORIA_BRAIN_DISABLED || ''));
@@ -688,7 +567,11 @@ export class ChatCommand {
688
567
  }
689
568
  }
690
569
  canPromptForWorkspaceIndex() {
691
- 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) {
692
575
  return false;
693
576
  }
694
577
  return !/^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_NO_INDEX_PROMPT || ''));
@@ -788,22 +671,40 @@ export class ChatCommand {
788
671
  agentRuntime: this.getRuntimeEnvironmentContext(),
789
672
  };
790
673
  const brainContext = this.buildProjectBrainRuntimeContext(prompt, this.operatorMode ? 'vigthoria-cli.operator' : this.agentMode ? 'vigthoria-cli.agent' : 'vigthoria-cli.chat');
791
- if (brainContext) {
792
- runtimeContext.vigthoriaBrain = brainContext;
793
- if (typeof brainContext.context === 'string' && brainContext.context.trim()) {
794
- runtimeContext.projectMemory = brainContext.context;
795
- }
796
- }
797
674
  const workspaceBrainContext = await this.buildWorkspaceBrainRuntimeContext(prompt);
798
675
  if (workspaceBrainContext.workspaceBrain) {
799
676
  runtimeContext.workspaceBrain = workspaceBrainContext.workspaceBrain;
800
677
  }
801
- if (typeof workspaceBrainContext.codebaseContext === 'string' && workspaceBrainContext.codebaseContext.trim()) {
802
- runtimeContext.codebaseContext = workspaceBrainContext.codebaseContext;
803
- }
804
- if (typeof workspaceBrainContext.accountBrainContext === 'string' && workspaceBrainContext.accountBrainContext.trim()) {
805
- runtimeContext.accountBrainContext = workspaceBrainContext.accountBrainContext;
806
- }
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;
807
708
  if (!this.isBrowserTaskPrompt(prompt)) {
808
709
  return runtimeContext;
809
710
  }
@@ -835,12 +736,6 @@ export class ChatCommand {
835
736
  devtoolsBridgeEndpoint: bridgeStatus.endpoint,
836
737
  };
837
738
  }
838
- v3IterationCount = 0;
839
- v3ToolCallCount = 0;
840
- v3LastActivity = Date.now();
841
- v3IdleWatchInterval = null;
842
- v3IdleNoticeShown = false;
843
- v3StreamingStarted = false;
844
739
  /**
845
740
  * Strip server-internal path prefixes from tool output strings.
846
741
  * Prevents exposing paths like /var/www/V3-Code-Agent/... to end users.
@@ -848,56 +743,13 @@ export class ChatCommand {
848
743
  sanitizeServerPath(text) {
849
744
  if (!text)
850
745
  return text;
851
- return sanitizeUserFacingPathText(this.stripHiddenThoughtBlocks(text));
852
- }
853
- stripHiddenThoughtBlocks(text) {
854
- if (!text)
855
- return text;
856
- return text
857
- .replace(/<\|mask_start\|>[\s\S]*?<\|mask_end\|>/g, '')
858
- .replace(/<think>[\s\S]*?<\/redacted_thinking>/gi, '')
859
- .replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
860
- .replace(/<\/redacted_thinking>/gi, '')
861
- .replace(/<\/thinking>/gi, '')
862
- .replace(/<\|(?:mask_start|mask_end)\|>/g, '')
863
- .trim();
746
+ return sanitizeUserFacingPathText(stripHiddenThoughtBlocks(text));
864
747
  }
865
748
  startV3IdleWatch(spinner) {
866
- this.stopV3IdleWatch();
867
- this.v3IdleNoticeShown = false;
868
- if (this.jsonOutput || !spinner) {
869
- return;
870
- }
871
- this.v3IdleWatchInterval = setInterval(() => {
872
- const idleMs = Date.now() - this.v3LastActivity;
873
- if (idleMs < 15_000) {
874
- // Activity resumed — re-arm the notice so a later stall is reported too.
875
- this.v3IdleNoticeShown = false;
876
- return;
877
- }
878
- if (this.v3IdleNoticeShown) {
879
- return;
880
- }
881
- this.v3IdleNoticeShown = true;
882
- if (spinner.isSpinning) {
883
- spinner.stop();
884
- }
885
- const seconds = Math.round(idleMs / 1000);
886
- const idleTimeoutSec = Math.round(DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS / 1000);
887
- process.stderr.write(chalk.yellow(` [Wait] Model still working (${seconds}s) — this can legitimately take a while for large tasks. ` +
888
- (idleTimeoutSec > 0
889
- ? `Only complete silence (~${idleTimeoutSec}s with zero bytes received) will trigger an auto-recover/retry — active progress is never interrupted.\n`
890
- : `\n`)));
891
- spinner.start();
892
- spinner.text = 'Waiting for model response...';
893
- }, 5_000);
749
+ this.streamState.startIdleWatch(spinner);
894
750
  }
895
751
  stopV3IdleWatch() {
896
- if (this.v3IdleWatchInterval) {
897
- clearInterval(this.v3IdleWatchInterval);
898
- this.v3IdleWatchInterval = null;
899
- }
900
- this.v3IdleNoticeShown = false;
752
+ this.streamState.stopIdleWatch();
901
753
  }
902
754
  describeV3AgentTool(toolName) {
903
755
  const normalized = String(toolName || '').toLowerCase();
@@ -964,7 +816,7 @@ export class ChatCommand {
964
816
  ? (event.body ?? event.stream ?? event.response ?? event)
965
817
  : event;
966
818
  for await (const chunk of robustifyStreamResponse(source)) {
967
- this.v3LastActivity = Date.now();
819
+ this.streamState.noteActivity();
968
820
  if (chunk.type === 'error') {
969
821
  if (spinner.isSpinning)
970
822
  spinner.stop();
@@ -991,8 +843,8 @@ export class ChatCommand {
991
843
  if (!safeText) {
992
844
  return;
993
845
  }
994
- if (!this.v3StreamingStarted) {
995
- this.v3StreamingStarted = true;
846
+ if (!this.streamState.streamingStarted) {
847
+ this.streamState.streamingStarted = true;
996
848
  spinner.stop();
997
849
  if (!this.directPromptMode) {
998
850
  console.log();
@@ -1001,27 +853,27 @@ export class ChatCommand {
1001
853
  else {
1002
854
  spinner.stop();
1003
855
  }
1004
- this.v3StreamedAnswerBuffer += safeText;
1005
- spinner.text = looksLikeMarkdownReport(this.v3StreamedAnswerBuffer)
856
+ this.streamState.streamedAnswerBuffer += safeText;
857
+ spinner.text = looksLikeMarkdownReport(this.streamState.streamedAnswerBuffer)
1006
858
  ? 'Writing analysis report...'
1007
859
  : 'Writing response...';
1008
860
  }
1009
861
  displayV3StreamedAnswer() {
1010
- if (this.jsonOutput || this.v3StreamedAnswerDisplayed) {
862
+ if (this.jsonOutput || this.streamState.streamedAnswerDisplayed) {
1011
863
  return;
1012
864
  }
1013
- const content = this.v3StreamedAnswerBuffer.trim();
865
+ const content = this.streamState.streamedAnswerBuffer.trim();
1014
866
  if (!isSubstantiveAgentAnswer(content)) {
1015
867
  return;
1016
868
  }
1017
869
  emitDeckEvent({ type: 'report_ready', markdown: content });
1018
870
  if (isDeckModeEnabled()) {
1019
- this.v3StreamedAnswerDisplayed = true;
871
+ this.streamState.streamedAnswerDisplayed = true;
1020
872
  return;
1021
873
  }
1022
874
  console.log('');
1023
875
  console.log(renderMarkdownToTerminal(content, this.getTerminalContentWidth()));
1024
- this.v3StreamedAnswerDisplayed = true;
876
+ this.streamState.streamedAnswerDisplayed = true;
1025
877
  }
1026
878
  renderAgentAnswerForTerminal(content) {
1027
879
  const normalized = String(content || '').trim();
@@ -1034,37 +886,7 @@ export class ChatCommand {
1034
886
  return Math.max(60, Math.min(cols - 2, 100));
1035
887
  }
1036
888
  sanitizeV3VisibleStreamText(text) {
1037
- let output = String(text || '');
1038
- if (!output)
1039
- return '';
1040
- output = output.replace(/<\/think>/gi, '</thinking>');
1041
- output = output.replace(/<think>/gi, '<thinking>');
1042
- output = output.replace(/<\/redacted_thinking>/gi, '</thinking>');
1043
- if (this.v3SuppressThinkingStream) {
1044
- const closeIdx = output.search(/<\/thinking>/i);
1045
- if (closeIdx < 0) {
1046
- return '';
1047
- }
1048
- output = output.slice(closeIdx).replace(/^<\/thinking>/i, '');
1049
- this.v3SuppressThinkingStream = false;
1050
- }
1051
- output = output.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
1052
- if (/<thinking>/i.test(output)) {
1053
- output = output.replace(/<thinking>[\s\S]*$/i, '');
1054
- this.v3SuppressThinkingStream = true;
1055
- }
1056
- output = output
1057
- .replace(/```json\s*\[\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*\]\s*```/gi, '')
1058
- .replace(/```json\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*```/gi, '')
1059
- .replace(/^\s*(?:json\s*)?\[\s*\{[\s\S]*?"tool"[\s\S]*$/gim, '')
1060
- .replace(/^\s*(?:list_dir|read_file|write_file|edit_file|glob|grep|bash)\s*$/gim, '')
1061
- .replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
1062
- .replace(/<tool_code>[\s\S]*?<\/tool_code>/gi, '')
1063
- .replace(/<read_file>\s*<path>[\s\S]*?<\/path>\s*<\/read_file>/gi, '')
1064
- .replace(/<(?:grep|list_directory|search_files|glob|tool_result)\b[\s\S]*?<\/(?:grep|list_directory|search_files|glob|tool_result)>/gi, '')
1065
- .replace(/bootstrap discovery completed\s*[—-]\s*inspect the most relevant files next\.?/gi, '')
1066
- .replace(/\n{3,}/g, '\n\n');
1067
- return output;
889
+ return this.streamState.sanitizeVisibleText(text);
1068
890
  }
1069
891
  writeAgentActivityLine(text) {
1070
892
  if (this.jsonOutput)
@@ -1084,29 +906,29 @@ export class ChatCommand {
1084
906
  if (!event || typeof event !== 'object') {
1085
907
  return;
1086
908
  }
1087
- this.v3LastActivity = Date.now();
909
+ this.streamState.noteActivity();
1088
910
  if (isV3StreamKeepaliveEvent(event)) {
1089
911
  return;
1090
912
  }
1091
913
  if (event.type === 'tool_call') {
1092
914
  const eventKey = this.toolEventKey(event);
1093
- if (this.v3SeenToolCalls.has(eventKey)) {
915
+ if (this.streamState.seenToolCalls.has(eventKey)) {
1094
916
  return;
1095
917
  }
1096
- this.v3SeenToolCalls.add(eventKey);
1097
- this.v3ToolCallCount += 1;
918
+ this.streamState.seenToolCalls.add(eventKey);
919
+ this.streamState.toolCallCount += 1;
1098
920
  const toolName = event.name || event.tool || event.tool_name || '';
1099
921
  emitDeckEvent({
1100
922
  type: 'tool_start',
1101
923
  tool: toolName,
1102
- index: this.v3ToolCallCount,
924
+ index: this.streamState.toolCallCount,
1103
925
  target: event.arguments?.path || event.arguments?.file_path || event.arguments?.pattern || '',
1104
926
  });
1105
927
  const toolDesc = this.describeV3AgentTool(event.tool || event.name || event.tool_name);
1106
928
  const toolTarget = event.arguments?.path || event.arguments?.file_path || event.arguments?.pattern || '';
1107
929
  const sanitizedTarget = this.sanitizeServerPath(String(toolTarget));
1108
930
  const shortTarget = sanitizedTarget ? ` -> ${sanitizedTarget.replace(/\\/g, '/').split('/').slice(-2).join('/')}` : '';
1109
- const stepLabel = chalk.cyan(` [${this.v3ToolCallCount}]`) + ` ${toolDesc}${shortTarget}`;
931
+ const stepLabel = chalk.cyan(` [${this.streamState.toolCallCount}]`) + ` ${toolDesc}${shortTarget}`;
1110
932
  if (spinner.isSpinning)
1111
933
  spinner.stop();
1112
934
  this.writeAgentActivityLine(this.fitTerminalText(stepLabel) + '\n');
@@ -1129,18 +951,18 @@ export class ChatCommand {
1129
951
  }
1130
952
  if (event.type === 'tool_result') {
1131
953
  const eventKey = this.toolEventKey(event);
1132
- if (eventKey && this.v3SeenToolResults.has(eventKey)) {
954
+ if (eventKey && this.streamState.seenToolResults.has(eventKey)) {
1133
955
  return;
1134
956
  }
1135
957
  if (eventKey)
1136
- this.v3SeenToolResults.add(eventKey);
958
+ this.streamState.seenToolResults.add(eventKey);
1137
959
  const success = event.success !== false;
1138
960
  const toolName = event.name || event.tool || '';
1139
961
  emitDeckEvent({
1140
962
  type: 'tool_end',
1141
963
  tool: toolName,
1142
964
  success,
1143
- index: this.v3ToolCallCount,
965
+ index: this.streamState.toolCallCount,
1144
966
  });
1145
967
  const indicator = success ? chalk.green(' ✓') : chalk.red(' ✗');
1146
968
  if (spinner.isSpinning)
@@ -1206,7 +1028,7 @@ export class ChatCommand {
1206
1028
  return;
1207
1029
  }
1208
1030
  if (event.type === 'thinking') {
1209
- this.v3IterationCount += 1;
1031
+ this.streamState.iterationCount += 1;
1210
1032
  const iterText = this.sanitizeServerPath(event.content || '');
1211
1033
  const isGenericStep = !iterText
1212
1034
  || /read-only analysis turn|run at least one discovery tool/i.test(iterText)
@@ -1225,7 +1047,7 @@ export class ChatCommand {
1225
1047
  ? (event.delta?.text || '')
1226
1048
  : (event.content || '');
1227
1049
  if (text) {
1228
- this.v3LastActivity = Date.now();
1050
+ this.streamState.noteActivity();
1229
1051
  if (event.type === 'message' && /^\[Context\]/i.test(String(text).trim())) {
1230
1052
  if (spinner.isSpinning)
1231
1053
  spinner.stop();
@@ -1242,16 +1064,16 @@ export class ChatCommand {
1242
1064
  }
1243
1065
  if (event.type === 'complete') {
1244
1066
  const elapsed = event.elapsed || '';
1245
- const iters = event.iterations || this.v3IterationCount;
1246
- const tools = event.tool_calls || this.v3ToolCallCount;
1067
+ const iters = event.iterations || this.streamState.iterationCount;
1068
+ const tools = event.tool_calls || this.streamState.toolCallCount;
1247
1069
  if (spinner.isSpinning)
1248
1070
  spinner.stop();
1249
1071
  this.displayV3StreamedAnswer();
1250
1072
  emitDeckEvent({
1251
1073
  type: 'run_complete',
1252
1074
  success: true,
1253
- tools: event.tool_calls || this.v3ToolCallCount,
1254
- iterations: event.iterations || this.v3IterationCount,
1075
+ tools: event.tool_calls || this.streamState.toolCallCount,
1076
+ iterations: event.iterations || this.streamState.iterationCount,
1255
1077
  elapsed: event.elapsed || '',
1256
1078
  });
1257
1079
  let statLine = `${iters} iterations, ${tools} tool calls`;
@@ -1267,10 +1089,11 @@ export class ChatCommand {
1267
1089
  process.stdout.write(chalk.green(`\n✓ Complete`) + ` - ${this.fitTerminalText(statLine, 14)}\n`);
1268
1090
  // Show seal quality score if available
1269
1091
  if (event.seal_score && typeof event.seal_score.overall === 'number') {
1270
- const score = event.seal_score.overall;
1092
+ const rawScore = event.seal_score.overall;
1093
+ const score = rawScore <= 10 ? rawScore * 10 : rawScore;
1271
1094
  const tier = event.seal_score.tier || '';
1272
- const scoreColor = score >= 7 ? chalk.green : score >= 5 ? chalk.yellow : chalk.red;
1273
- 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');
1274
1097
  }
1275
1098
  return;
1276
1099
  }
@@ -1425,11 +1248,11 @@ export class ChatCommand {
1425
1248
  return;
1426
1249
  }
1427
1250
  if (event.type === 'start') {
1428
- if (this.v3StartSeen) {
1251
+ if (this.streamState.startSeen) {
1429
1252
  spinner.text = 'Working...';
1430
1253
  return;
1431
1254
  }
1432
- this.v3StartSeen = true;
1255
+ this.streamState.startSeen = true;
1433
1256
  if (spinner.isSpinning)
1434
1257
  spinner.stop();
1435
1258
  process.stderr.write(chalk.cyan(' [Start] ') + 'Agent initialized\n');
@@ -1503,20 +1326,20 @@ export class ChatCommand {
1503
1326
  this.config = config;
1504
1327
  this.logger = logger;
1505
1328
  this.commandProgram = commandProgram;
1506
- 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
+ });
1507
1335
  this.sessionManager = new SessionManager();
1508
1336
  }
1509
1337
  async run(options) {
1510
1338
  const hasRuntimeToken = Boolean(process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN);
1511
1339
  if (!this.config.isAuthenticated() && !hasRuntimeToken) {
1512
- if (options.json) {
1513
- process.exitCode = 1;
1514
- emitAgentJsonOutput({ success: false, error: 'Not authenticated. Run: vigthoria login' });
1515
- }
1516
- else {
1517
- this.logger.error('Not authenticated. Run: vigthoria login');
1518
- }
1519
- return;
1340
+ throw new CliCommandError('Not authenticated. Run: vigthoria login', {
1341
+ code: 'AUTH_REQUIRED', category: 'authentication',
1342
+ });
1520
1343
  }
1521
1344
  this.agentMode = options.agent === true;
1522
1345
  this.operatorMode = options.operator === true;
@@ -1525,7 +1348,7 @@ export class ChatCommand {
1525
1348
  : null;
1526
1349
  this.savePlanToVigFlow = options.savePlan === true;
1527
1350
  this.jsonOutput = options.json === true;
1528
- this.autoApprove = options.autoApprove === true || this.jsonOutput;
1351
+ this.autoApprove = resolveAutoApproval(options.autoApprove, this.jsonOutput);
1529
1352
  this.personaOverride = options.grant === true ? 'wiener_grant' : null;
1530
1353
  this.modelExplicitlySelected = this.isHardExplicitAgentModelSelection(options);
1531
1354
  this.currentModel = this.resolveInitialModel(options);
@@ -1581,19 +1404,23 @@ export class ChatCommand {
1581
1404
  this.saveSession();
1582
1405
  }
1583
1406
  this.operatorMode = false;
1584
- this.logger.error(this.operatorAccessMessage());
1585
- return;
1407
+ throw new CliCommandError(this.operatorAccessMessage(), { code: 'OPERATOR_ACCESS_REQUIRED', category: 'authorization' });
1586
1408
  }
1587
1409
  if (options.retry || options.continue) {
1588
1410
  if (!this.agentMode) {
1589
- 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' });
1590
1412
  }
1591
1413
  const followUp = options.retry ? this.buildRetryPrompt() : this.buildContinuePrompt();
1592
1414
  if (!followUp) {
1593
- 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
+ });
1594
1419
  }
1595
1420
  this.directPromptMode = true;
1596
1421
  await this.runAgentTurn(followUp);
1422
+ this.workspaceBrain?.destroy();
1423
+ this.api.destroy();
1597
1424
  return;
1598
1425
  }
1599
1426
  if (options.prompt) {
@@ -1605,35 +1432,34 @@ export class ChatCommand {
1605
1432
  const parsed = Number.parseInt(rawValue, 10);
1606
1433
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
1607
1434
  })();
1608
- let timedOut = false;
1609
- const timeoutId = options.bridge && bridgePromptTimeoutMs > 0
1610
- ? setTimeout(() => {
1611
- timedOut = true;
1612
- const b = getBridgeClient();
1613
- if (b) {
1614
- b.emitEnd({ reason: 'timeout' });
1615
- b.destroy();
1616
- }
1617
- if (!this.jsonOutput) {
1618
- this.logger.error('Bridge prompt timed out after ' + Math.round(bridgePromptTimeoutMs / 1000) + ' seconds.');
1619
- }
1620
- process.exitCode = 1;
1621
- }, bridgePromptTimeoutMs)
1622
- : null;
1623
- await this.handleDirectPrompt(options.prompt);
1624
- if (timeoutId)
1625
- clearTimeout(timeoutId);
1626
- if (!timedOut) {
1627
- const bridge = getBridgeClient();
1628
- if (bridge) {
1629
- bridge.emitEnd({ reason: 'prompt-complete' });
1630
- 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;
1631
1448
  }
1449
+ getBridgeClient()?.emitEnd({ reason: 'prompt-complete' });
1632
1450
  }
1633
- // Force-exit: undici + chokidar + HTTPS pool keep the Node.js event
1634
- // loop alive indefinitely in direct prompt mode; a clean exit is safe here.
1635
- this.api.destroy();
1636
- 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;
1637
1463
  }
1638
1464
  await this.startInteractiveChat();
1639
1465
  const bridge = getBridgeClient();
@@ -1641,6 +1467,8 @@ export class ChatCommand {
1641
1467
  bridge.emitEnd({ reason: 'interactive-exit' });
1642
1468
  bridge.destroy();
1643
1469
  }
1470
+ this.workspaceBrain?.destroy();
1471
+ this.api.destroy();
1644
1472
  }
1645
1473
  /** Handle an inbound admin command from the Commando Bridge. */
1646
1474
  handleAdminCommand(cmd) {
@@ -1660,7 +1488,9 @@ export class ChatCommand {
1660
1488
  if (!this.jsonOutput)
1661
1489
  console.log(chalk.red(`[bridge] Abort requested by admin`));
1662
1490
  getBridgeClient()?.emitEnd({ reason: 'admin-abort' });
1663
- process.exit(0);
1491
+ process.exitCode = 130;
1492
+ this.workspaceBrain?.destroy();
1493
+ this.api.destroy();
1664
1494
  break;
1665
1495
  default:
1666
1496
  getBridgeClient()?.emitError({ message: `Unknown admin command: ${cmd.action}` });
@@ -1838,6 +1668,35 @@ export class ChatCommand {
1838
1668
  originalPrompt: saved.originalPrompt ?? null,
1839
1669
  };
1840
1670
  }
1671
+ else if (this.currentSession.activeAgentRun?.prompt) {
1672
+ const checkpoint = this.currentSession.activeAgentRun;
1673
+ this.lastAgentRunOutcome = {
1674
+ prompt: checkpoint.prompt,
1675
+ originalPrompt: checkpoint.originalPrompt || checkpoint.prompt,
1676
+ taskId: checkpoint.executionId,
1677
+ contextId: checkpoint.contextId,
1678
+ tasksSucceeded: 0,
1679
+ tasksTotal: 0,
1680
+ failedTaskIds: [...(checkpoint.failedTaskIds || [])],
1681
+ unfinishedTaskIds: checkpoint.unfinishedTaskIds?.length
1682
+ ? [...checkpoint.unfinishedTaskIds]
1683
+ : ['resume-interrupted-agent-run'],
1684
+ qualityScore: null,
1685
+ qualityMissing: [],
1686
+ qualityBlockers: checkpoint.error ? [checkpoint.error] : [],
1687
+ hasOutput: false,
1688
+ answerContent: null,
1689
+ selfHealStatus: 'skipped',
1690
+ selfHealTool: null,
1691
+ plannerError: null,
1692
+ executorError: checkpoint.error || 'The previous client process ended before the agent run completed.',
1693
+ clientToolErrors: [],
1694
+ transportErrors: [],
1695
+ workspacePath: checkpoint.workspacePath || this.currentProjectPath,
1696
+ workspaceSyncIssue: null,
1697
+ finishedAt: Date.parse(checkpoint.updatedAt) || Date.now(),
1698
+ };
1699
+ }
1841
1700
  return;
1842
1701
  }
1843
1702
  }
@@ -1906,8 +1765,7 @@ export class ChatCommand {
1906
1765
  }
1907
1766
  if (this.operatorMode) {
1908
1767
  if (!this.hasOperatorAccess()) {
1909
- this.logger.error(this.operatorAccessMessage());
1910
- return;
1768
+ throw new CliCommandError(this.operatorAccessMessage(), { code: 'OPERATOR_ACCESS_REQUIRED', category: 'authorization' });
1911
1769
  }
1912
1770
  await this.runOperatorTurn(effectivePrompt);
1913
1771
  return;
@@ -1987,13 +1845,13 @@ export class ChatCommand {
1987
1845
  spinner.stop();
1988
1846
  }
1989
1847
  if (this.jsonOutput) {
1990
- console.log(JSON.stringify({
1848
+ emitAgentJsonOutput({
1991
1849
  success: true,
1992
1850
  workflow: resolvedWorkflow,
1993
1851
  mode: invocationMode,
1994
1852
  execution,
1995
1853
  content,
1996
- }, null, 2));
1854
+ });
1997
1855
  return;
1998
1856
  }
1999
1857
  this.logger.success(`Workflow target ${resolvedWorkflow.name} ${execution.status}`);
@@ -2014,8 +1872,7 @@ export class ChatCommand {
2014
1872
  }
2015
1873
  async runOperatorTurn(prompt) {
2016
1874
  if (!this.hasOperatorAccess()) {
2017
- this.logger.error(this.operatorAccessMessage());
2018
- return;
1875
+ throw new CliCommandError(this.operatorAccessMessage(), { code: 'OPERATOR_ACCESS_REQUIRED', category: 'authorization' });
2019
1876
  }
2020
1877
  // ── Fast-path: direct-answerable prompts skip the full BMAD workflow ──
2021
1878
  // The BMAD orchestrator scans the entire workspace (can be 1000+ files)
@@ -2075,7 +1932,7 @@ export class ChatCommand {
2075
1932
  throw new CLIError('Operator workflow returned a non-actionable acknowledgement instead of a grounded result.', 'model_backend');
2076
1933
  }
2077
1934
  if (this.jsonOutput) {
2078
- console.log(JSON.stringify({
1935
+ emitAgentJsonOutput({
2079
1936
  success: true,
2080
1937
  mode: 'operator',
2081
1938
  workflowId: response.workflowId,
@@ -2083,7 +1940,7 @@ export class ChatCommand {
2083
1940
  contextId: response.contextId || null,
2084
1941
  content: response.content || 'Operator workflow completed.',
2085
1942
  metadata: response.metadata || {},
2086
- }, null, 2));
1943
+ });
2087
1944
  }
2088
1945
  else {
2089
1946
  console.log(response.content || 'Operator workflow completed.');
@@ -2101,23 +1958,8 @@ export class ChatCommand {
2101
1958
  }
2102
1959
  const cliErr = error instanceof CLIError ? error : classifyError(error);
2103
1960
  const errorMsg = formatCLIError(cliErr);
2104
- if (!this.jsonOutput) {
2105
- this.logger.error('Operator workflow failed');
2106
- }
2107
1961
  this.rememberBrainEvent('issue', `GoA operator workflow failed: ${errorMsg}`, 'operator');
2108
- if (this.jsonOutput) {
2109
- process.exitCode = 1;
2110
- console.log(JSON.stringify({
2111
- success: false,
2112
- mode: 'operator',
2113
- content: '',
2114
- error: errorMsg,
2115
- errorCategory: cliErr.category,
2116
- }, null, 2));
2117
- }
2118
- else {
2119
- this.logger.error(errorMsg);
2120
- }
1962
+ throw commandFailure(error, { code: 'OPERATOR_EXECUTION_FAILED', message: errorMsg });
2121
1963
  }
2122
1964
  }
2123
1965
  /**
@@ -2163,12 +2005,12 @@ export class ChatCommand {
2163
2005
  throw new CLIError('Operator returned a non-actionable acknowledgement instead of a grounded result.', 'model_backend');
2164
2006
  }
2165
2007
  if (this.jsonOutput) {
2166
- console.log(JSON.stringify({
2008
+ emitAgentJsonOutput({
2167
2009
  success: true,
2168
2010
  mode: 'operator',
2169
2011
  content,
2170
2012
  metadata: { source: 'operator-direct', mode: 'operator' },
2171
- }, null, 2));
2013
+ });
2172
2014
  }
2173
2015
  else {
2174
2016
  console.log(content);
@@ -2182,20 +2024,7 @@ export class ChatCommand {
2182
2024
  spinner.stop();
2183
2025
  const cliErr = error instanceof CLIError ? error : classifyError(error);
2184
2026
  const errorMsg = formatCLIError(cliErr);
2185
- if (this.jsonOutput) {
2186
- process.exitCode = 1;
2187
- console.log(JSON.stringify({
2188
- success: false,
2189
- mode: 'operator',
2190
- content: '',
2191
- error: errorMsg,
2192
- errorCategory: cliErr.category,
2193
- }, null, 2));
2194
- }
2195
- else {
2196
- this.logger.error('Operator direct answer failed');
2197
- this.logger.error(errorMsg);
2198
- }
2027
+ throw commandFailure(error, { code: 'OPERATOR_DIRECT_FAILED', message: errorMsg });
2199
2028
  }
2200
2029
  }
2201
2030
  async runSimplePrompt(prompt) {
@@ -2294,20 +2123,20 @@ export class ChatCommand {
2294
2123
  const response = await this.callApi('Send chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel));
2295
2124
  if (spinner)
2296
2125
  spinner.stop();
2297
- const finalText = this.sanitizeDirectModeOutput(this.stripHiddenThoughtBlocks(response.message || '')).trim();
2126
+ const finalText = sanitizeDirectModeOutput(stripHiddenThoughtBlocks(response.message || '')).trim();
2298
2127
  const effectiveModel = String(response.model || this.currentModel);
2299
2128
  const metadata = this.modelGovernanceFallback
2300
2129
  ? { modelFallback: this.modelGovernanceFallback }
2301
2130
  : undefined;
2302
2131
  if (this.jsonOutput) {
2303
- console.log(JSON.stringify({
2132
+ emitAgentJsonOutput({
2304
2133
  success: true,
2305
2134
  mode: 'chat',
2306
2135
  model: effectiveModel,
2307
2136
  requestedModel: this.modelGovernanceFallback?.requestedModel || this.currentModel,
2308
2137
  metadata,
2309
2138
  content: finalText || 'The model returned an empty response. Try rephrasing or use --agent for grounded file analysis.',
2310
- }, null, 2));
2139
+ });
2311
2140
  }
2312
2141
  else if (finalText) {
2313
2142
  console.log(finalText);
@@ -2323,20 +2152,7 @@ export class ChatCommand {
2323
2152
  spinner.stop();
2324
2153
  const cliErr = error instanceof CLIError ? error : classifyError(error);
2325
2154
  const errorMsg = formatCLIError(cliErr);
2326
- if (this.jsonOutput) {
2327
- process.exitCode = 1;
2328
- console.log(JSON.stringify({
2329
- success: false,
2330
- mode: 'chat',
2331
- model: this.currentModel,
2332
- content: '',
2333
- error: errorMsg,
2334
- errorCategory: cliErr.category,
2335
- }, null, 2));
2336
- }
2337
- else {
2338
- this.logger.error(errorMsg);
2339
- }
2155
+ throw commandFailure(error, { code: 'CHAT_EXECUTION_FAILED', message: errorMsg });
2340
2156
  }
2341
2157
  }
2342
2158
  async runAgentTurn(prompt) {
@@ -2376,10 +2192,9 @@ export class ChatCommand {
2376
2192
  return;
2377
2193
  }
2378
2194
  const errorMessage = 'The V3 planner/executor workflow is unavailable. Refusing to fall back to legacy template, direct-file, or local write paths.';
2379
- this.logger.error(errorMessage);
2380
2195
  this.messages.push({ role: 'assistant', content: errorMessage });
2381
- process.exitCode = 1;
2382
2196
  this.saveSession();
2197
+ throw new CliCommandError(errorMessage, { code: 'AGENT_UNAVAILABLE', category: 'execution' });
2383
2198
  }
2384
2199
  resolveAgentTurnPrompt(prompt) {
2385
2200
  if (isBuiltContinuePrompt(prompt) || isBuiltRetryPrompt(prompt) || isBuiltWriteConfirmationPrompt(prompt)) {
@@ -2551,33 +2366,38 @@ export class ChatCommand {
2551
2366
  spinner.stop();
2552
2367
  const evidenceSummary = this.synthesizeEvidenceFromHistory();
2553
2368
  if (evidenceSummary) {
2554
- const fallbackContent = this.sanitizeDirectModeOutput(evidenceSummary);
2369
+ const fallbackContent = sanitizeDirectModeOutput(evidenceSummary);
2555
2370
  const liveOutcome = this.buildLocalLoopLiveOutcome(prompt);
2556
2371
  const evaluation = evaluateExecutorSuccess(liveOutcome);
2557
2372
  this.persistLocalLoopOutcome(prompt, evaluation, liveOutcome);
2558
2373
  if (this.jsonOutput) {
2559
- process.exitCode = evaluation.executorSucceeded ? 0 : 1;
2560
- console.log(JSON.stringify({
2561
- success: evaluation.executorSucceeded,
2562
- mode: 'agent',
2563
- model: this.currentModel,
2564
- partial: !evaluation.executorSucceeded,
2565
- content: fallbackContent,
2566
- statusHeadline: evaluation.statusHeadline,
2567
- metadata: {
2568
- executionPath: 'local-agent-loop',
2569
- recovered: true,
2570
- outcomeTruth: {
2571
- executorSucceeded: evaluation.executorSucceeded,
2572
- 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
+ },
2573
2389
  },
2574
- },
2575
- }, null, 2));
2390
+ });
2576
2391
  }
2577
2392
  else {
2578
2393
  console.log(fallbackContent);
2579
2394
  }
2580
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
+ }
2581
2401
  return;
2582
2402
  }
2583
2403
  throw retryErr;
@@ -2589,23 +2409,7 @@ export class ChatCommand {
2589
2409
  if (spinner)
2590
2410
  spinner.stop();
2591
2411
  this.rememberBrainEvent('issue', 'Agent API preflight failed: ' + formatted, 'agent');
2592
- if (this.jsonOutput) {
2593
- process.exitCode = 1;
2594
- console.log(JSON.stringify({
2595
- success: false,
2596
- mode: 'agent',
2597
- model: this.currentModel,
2598
- partial: false,
2599
- content: '',
2600
- error: formatted,
2601
- errorCategory: cliErr.category,
2602
- metadata: { executionPath: 'local-agent-loop' },
2603
- }, null, 2));
2604
- }
2605
- else {
2606
- this.logger.error(formatted);
2607
- }
2608
- return;
2412
+ throw commandFailure(firstErr, { code: 'AGENT_PREFLIGHT_FAILED', message: formatted });
2609
2413
  }
2610
2414
  }
2611
2415
  if (spinner)
@@ -2627,8 +2431,8 @@ export class ChatCommand {
2627
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());
2628
2432
  // Detect if the response is effectively empty or just a follow-up
2629
2433
  // question after we strip tool output echoes.
2630
- const sanitized = this.sanitizeDirectModeOutput(visibleText.trim());
2631
- const isFollowUp = this.isDirectModeFollowUpQuestion(sanitized);
2434
+ const sanitized = sanitizeDirectModeOutput(visibleText.trim());
2435
+ const isFollowUp = isDirectModeFollowUpQuestion(sanitized);
2632
2436
  const isEmptyAfterSanitize = !sanitized || sanitized.length < 10;
2633
2437
  // Detect resignation: model gives up saying files/things were "not found"
2634
2438
  // without having tried list_dir to discover the correct path.
@@ -2671,29 +2475,34 @@ export class ChatCommand {
2671
2475
  const evaluation = evaluateExecutorSuccess(liveOutcome);
2672
2476
  this.persistLocalLoopOutcome(prompt, evaluation, liveOutcome);
2673
2477
  if (this.jsonOutput) {
2674
- process.exitCode = evaluation.executorSucceeded ? 0 : 1;
2675
- console.log(JSON.stringify({
2676
- success: evaluation.executorSucceeded,
2677
- mode: 'agent',
2678
- model: this.currentModel,
2679
- partial: !evaluation.executorSucceeded,
2680
- content: finalContent,
2681
- statusHeadline: evaluation.statusHeadline,
2682
- metadata: {
2683
- executionPath: 'local-agent-loop',
2684
- outcomeTruth: {
2685
- executorSucceeded: evaluation.executorSucceeded,
2686
- uiTheme: evaluation.uiTheme,
2687
- analysisToolsUsed: liveOutcome.analysisToolsUsed,
2688
- 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
+ },
2689
2494
  },
2690
- },
2691
- }, null, 2));
2495
+ });
2692
2496
  }
2693
2497
  else {
2694
2498
  console.log(finalContent);
2695
2499
  }
2696
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
+ }
2697
2506
  return;
2698
2507
  }
2699
2508
  await this.executeToolCalls(toolCalls);
@@ -2710,25 +2519,7 @@ export class ChatCommand {
2710
2519
  const cliErr = error instanceof CLIError ? error : classifyError(error);
2711
2520
  const errorMsg = formatCLIError(cliErr);
2712
2521
  this.rememberBrainEvent('issue', `Agent turn failed: ${errorMsg}`, 'agent');
2713
- if (this.jsonOutput) {
2714
- process.exitCode = 1;
2715
- console.log(JSON.stringify({
2716
- success: false,
2717
- mode: 'agent',
2718
- model: this.currentModel,
2719
- partial: false,
2720
- content: '',
2721
- error: errorMsg,
2722
- errorCategory: cliErr.category,
2723
- metadata: {
2724
- executionPath: 'local-agent-loop',
2725
- },
2726
- }, null, 2));
2727
- }
2728
- else {
2729
- this.logger.error(errorMsg);
2730
- }
2731
- return;
2522
+ throw commandFailure(error, { code: 'AGENT_TURN_FAILED', message: errorMsg });
2732
2523
  }
2733
2524
  }
2734
2525
  const liveOutcome = this.buildLocalLoopLiveOutcome(prompt);
@@ -2738,29 +2529,11 @@ export class ChatCommand {
2738
2529
  executorError: 'Agent exhausted the maximum local tool loop turns before reaching a clean completion.',
2739
2530
  });
2740
2531
  this.persistLocalLoopOutcome(prompt, exhaustedEval, liveOutcome);
2741
- if (this.jsonOutput) {
2742
- process.exitCode = 1;
2743
- console.log(JSON.stringify({
2744
- success: false,
2745
- mode: 'agent',
2746
- model: this.currentModel,
2747
- partial: true,
2748
- content: 'Task complete.',
2749
- error: 'Agent exhausted the maximum local tool loop turns before reaching a clean completion.',
2750
- statusHeadline: exhaustedEval.statusHeadline,
2751
- metadata: {
2752
- executionPath: 'local-agent-loop',
2753
- outcomeTruth: {
2754
- executorSucceeded: false,
2755
- uiTheme: exhaustedEval.uiTheme,
2756
- },
2757
- },
2758
- }, null, 2));
2759
- }
2760
- else {
2761
- console.log('Task complete.');
2762
- }
2763
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
+ });
2764
2537
  }
2765
2538
  async primeBypassedTargetFileContext(prompt) {
2766
2539
  if (!this.directPromptMode || !this.tools) {
@@ -2791,235 +2564,6 @@ export class ChatCommand {
2791
2564
  this.messages.push({ role: 'system', content: readSummary });
2792
2565
  }
2793
2566
  }
2794
- /**
2795
- * Sub-second path for trivial HTML/hello-world: Template Service → index.html (no V3 planner).
2796
- */
2797
- async tryTemplateInstantPath(prompt) {
2798
- const route = this.lastAgentRoute;
2799
- if (route?.path !== 'template-instant' && !isTrivialHtmlPageRequest(prompt)) {
2800
- return false;
2801
- }
2802
- let workspacePath = this.currentProjectPath;
2803
- try {
2804
- const fromPrompt = this.resolvePromptWorkspacePath(prompt, this.currentProjectPath);
2805
- if (fromPrompt)
2806
- workspacePath = fromPrompt;
2807
- }
2808
- catch {
2809
- // use default workspace
2810
- }
2811
- if (!this.jsonOutput) {
2812
- console.log();
2813
- console.log(chalk.gray('━━━ ROUTING DECISION ━━━'));
2814
- const r = this.lastAgentRoute;
2815
- console.log(chalk.gray(`Reason: ${r?.reason || 'template-instant (trivial HTML page)'}`));
2816
- console.log(chalk.gray(`Path: template-instant [${r?.source || 'regex'}]`));
2817
- console.log(chalk.gray('Model: template-service'));
2818
- console.log(chalk.gray('Cloud Eligible: false'));
2819
- console.log(chalk.gray('Cloud Selected: false'));
2820
- console.log(chalk.gray('━'.repeat(30)));
2821
- console.log();
2822
- console.log(chalk.cyan('⚡ Template instant path — matching template and writing index.html...'));
2823
- }
2824
- this.messages.push({ role: 'user', content: prompt });
2825
- const mustComplete = route?.path === 'template-instant';
2826
- let result = await runTemplateInstantPath(this.api, prompt, workspacePath, {
2827
- allowFallback: true,
2828
- });
2829
- if (!result.ok && mustComplete) {
2830
- result = await runTemplateInstantPath(this.api, prompt, workspacePath, {
2831
- allowFallback: true,
2832
- forceMinimalFallback: true,
2833
- });
2834
- }
2835
- if (!result.ok) {
2836
- if (!this.jsonOutput) {
2837
- console.log(chalk.yellow(`Template instant path skipped: ${result.error || 'no match'}`));
2838
- }
2839
- return false;
2840
- }
2841
- const summary = [
2842
- `Created ${result.entryPath || 'index.html'} via template-instant`,
2843
- result.templateName ? `(${result.templateName})` : '',
2844
- result.processingMs ? `in ${Math.round(result.processingMs)}ms` : '',
2845
- result.usedFallback ? '(minimal fallback)' : '',
2846
- ].filter(Boolean).join(' ');
2847
- this.messages.push({ role: 'assistant', content: summary });
2848
- const liveOutcome = createLiveOutcome();
2849
- liveOutcome.requiresWorkspaceChanges = true;
2850
- liveOutcome.workspaceHasOutput = true;
2851
- liveOutcome.changedFileCount = 1;
2852
- liveOutcome.tasksTotal = 1;
2853
- liveOutcome.tasksSucceeded = 1;
2854
- liveOutcome.answerContent = summary;
2855
- const evaluation = evaluateExecutorSuccess(liveOutcome);
2856
- this.commitAgentRunOutcome({
2857
- prompt,
2858
- originalPrompt: null,
2859
- taskId: null,
2860
- contextId: null,
2861
- tasksSucceeded: 1,
2862
- tasksTotal: 1,
2863
- failedTaskIds: [],
2864
- unfinishedTaskIds: [],
2865
- qualityScore: null,
2866
- qualityMissing: [],
2867
- qualityBlockers: [],
2868
- hasOutput: true,
2869
- answerContent: summary,
2870
- selfHealStatus: 'skipped',
2871
- selfHealTool: null,
2872
- plannerError: null,
2873
- executorError: null,
2874
- clientToolErrors: [],
2875
- transportErrors: [],
2876
- workspacePath: workspacePath || null,
2877
- workspaceSyncIssue: null,
2878
- finishedAt: Date.now(),
2879
- }, prompt);
2880
- if (!this.jsonOutput) {
2881
- console.log(chalk.green(`✓ ${summary}`));
2882
- console.log(chalk.gray(' Run: vigthoria preview'));
2883
- this.printAgentRunSummary(this.lastAgentRunOutcome, evaluation, 1);
2884
- }
2885
- else {
2886
- const routingPolicy = this.resolveAgentExecutionPolicy(prompt);
2887
- console.log(JSON.stringify({
2888
- success: evaluation.executorSucceeded,
2889
- mode: 'agent',
2890
- model: routingPolicy.selectedModel,
2891
- routingPolicy,
2892
- taskId: null,
2893
- contextId: null,
2894
- partial: false,
2895
- content: summary,
2896
- statusHeadline: evaluation.statusHeadline,
2897
- tasksSucceeded: 1,
2898
- tasksTotal: 1,
2899
- metadata: {
2900
- source: 'template-instant',
2901
- templateName: result.templateName,
2902
- processingMs: result.processingMs,
2903
- route: this.lastAgentRoute,
2904
- },
2905
- }, null, 2));
2906
- }
2907
- return true;
2908
- }
2909
- async tryDirectSingleFileFlow(prompt) {
2910
- if (!this.directPromptMode || !this.tools) {
2911
- return false;
2912
- }
2913
- const targetFile = this.inferTargetFileFromPrompt(prompt);
2914
- if (!targetFile) {
2915
- return false;
2916
- }
2917
- if (this.shouldBypassDirectSingleFileFlow(targetFile, prompt)) {
2918
- return false;
2919
- }
2920
- const readCall = {
2921
- tool: 'read_file',
2922
- args: { path: targetFile },
2923
- };
2924
- if (!this.jsonOutput) {
2925
- console.log(chalk.cyan(`⚙ Executing: ${readCall.tool}`));
2926
- }
2927
- const readResult = await this.tools.execute(readCall);
2928
- const readSummary = this.formatToolResult(readCall, readResult);
2929
- if (!this.jsonOutput) {
2930
- console.log(readResult.success ? chalk.gray(readSummary) : chalk.red(readSummary));
2931
- }
2932
- this.messages.push({ role: 'system', content: readSummary });
2933
- if (!readResult.success || !readResult.output) {
2934
- return false;
2935
- }
2936
- let rewrittenContent = this.tryDeterministicSingleFileRewrite(prompt, targetFile, readResult.output);
2937
- if (!rewrittenContent) {
2938
- const rewriteMessages = [
2939
- {
2940
- role: 'system',
2941
- content: [
2942
- 'You are repairing a single file for a CLI agent task.',
2943
- `Return only the final contents for ${targetFile}.`,
2944
- 'Do not use Markdown fences.',
2945
- 'Do not add explanations before or after the file contents.',
2946
- 'Produce complete, runnable code or markup.',
2947
- ].join('\n'),
2948
- },
2949
- {
2950
- role: 'user',
2951
- content: [
2952
- `Task: ${prompt}`,
2953
- `Target file: ${targetFile}`,
2954
- 'Current file contents:',
2955
- readResult.output,
2956
- ].join('\n\n'),
2957
- },
2958
- ];
2959
- const rewriteResponse = await this.callApi('Rewrite target file', () => this.api.chat(rewriteMessages, this.currentModel));
2960
- rewrittenContent = this.extractFinalFileContent(rewriteResponse.message, targetFile);
2961
- }
2962
- if (!rewrittenContent) {
2963
- return false;
2964
- }
2965
- const writeCall = {
2966
- tool: 'write_file',
2967
- args: {
2968
- path: targetFile,
2969
- content: rewrittenContent,
2970
- },
2971
- };
2972
- if (!this.jsonOutput) {
2973
- console.log(chalk.cyan(`⚙ Executing: ${writeCall.tool}`));
2974
- }
2975
- const writeResult = await this.tools.execute(writeCall);
2976
- const writeSummary = this.formatToolResult(writeCall, writeResult);
2977
- if (!this.jsonOutput) {
2978
- console.log(writeResult.success ? chalk.gray(writeSummary) : chalk.red(writeSummary));
2979
- }
2980
- this.messages.push({ role: 'system', content: writeSummary });
2981
- if (!writeResult.success) {
2982
- return false;
2983
- }
2984
- const previewGate = await this.callApi('Run Template Service preview gate', () => this.api.runTemplateServicePreviewGate(prompt, {
2985
- workspacePath: this.currentProjectPath,
2986
- projectPath: this.currentProjectPath,
2987
- targetPath: this.currentProjectPath,
2988
- rawPrompt: prompt,
2989
- executionSurface: 'cli',
2990
- clientSurface: 'cli',
2991
- }));
2992
- const success = previewGate.required ? previewGate.passed === true : true;
2993
- if (!success) {
2994
- process.exitCode = 1;
2995
- }
2996
- if (this.jsonOutput) {
2997
- console.log(JSON.stringify({
2998
- success,
2999
- mode: 'agent',
3000
- model: this.currentModel,
3001
- partial: false,
3002
- content: `Updated ${targetFile}.`,
3003
- metadata: {
3004
- executionPath: 'direct-single-file',
3005
- targetFile,
3006
- previewGate,
3007
- },
3008
- }, null, 2));
3009
- }
3010
- else {
3011
- console.log(`Updated ${targetFile}.`);
3012
- if (previewGate.required) {
3013
- if (previewGate.passed) {
3014
- console.log(chalk.gray(`Template Service preview gate: passed via ${previewGate.backendUrl || 'unknown backend'}`));
3015
- }
3016
- else {
3017
- console.log(chalk.yellow(`Template Service preview gate: failed${previewGate.error ? ` - ${previewGate.error}` : ''}`));
3018
- }
3019
- }
3020
- }
3021
- return true;
3022
- }
3023
2567
  taskRequiresWorkspaceChanges(prompt) {
3024
2568
  return promptRequiresWorkspaceChanges(prompt);
3025
2569
  }
@@ -3099,15 +2643,7 @@ export class ChatCommand {
3099
2643
  console.log();
3100
2644
  }
3101
2645
  // Reset streaming counters for new workflow
3102
- this.v3IterationCount = 0;
3103
- this.v3ToolCallCount = 0;
3104
- this.v3LastActivity = Date.now();
3105
- this.v3StreamingStarted = false;
3106
- this.v3StreamedAnswerBuffer = '';
3107
- this.v3StreamedAnswerDisplayed = false;
3108
- this.v3StartSeen = false;
3109
- this.v3SeenToolCalls.clear();
3110
- this.v3SeenToolResults.clear();
2646
+ this.streamState.reset();
3111
2647
  emitDeckEvent({ type: 'agent_start' });
3112
2648
  const taskDisplay = new TaskDisplay(['Analyse workspace', 'Execute tasks', 'Validate output', 'Self-heal'], false);
3113
2649
  taskDisplay.start(0);
@@ -3186,6 +2722,12 @@ export class ChatCommand {
3186
2722
  || isAgentRetryPrompt(contextualPrompt)
3187
2723
  || isBuiltContinuePrompt(contextualPrompt)
3188
2724
  || isBuiltRetryPrompt(contextualPrompt);
2725
+ const initialExecutionStatus = isAgentRetryPrompt(contextualPrompt)
2726
+ || isBuiltRetryPrompt(contextualPrompt)
2727
+ ? 'retry'
2728
+ : shouldResumePlan
2729
+ ? 'continue'
2730
+ : 'submitted';
3189
2731
  const agentExecutionHints = shouldResumePlan
3190
2732
  ? {
3191
2733
  ...executionHints,
@@ -3193,18 +2735,39 @@ export class ChatCommand {
3193
2735
  ...(resumeTaskIds.length > 0 ? { remaining_task_ids: resumeTaskIds } : {}),
3194
2736
  }
3195
2737
  : executionHints;
2738
+ const previousExecutionId = this.currentSession?.activeAgentRun?.executionId || null;
2739
+ const executionId = `cli-${Date.now()}-${randomUUID().slice(0, 8)}`;
3196
2740
  const workspaceContext = {
3197
2741
  workspacePath: workspacePath,
3198
2742
  projectPath: workspacePath,
3199
2743
  targetPath: workspacePath,
2744
+ contextId: executionId,
2745
+ executionId,
3200
2746
  ...runtimeContext,
3201
2747
  };
2748
+ this.persistAgentExecutionCheckpoint({
2749
+ executionId,
2750
+ parentExecutionId: shouldResumePlan ? previousExecutionId : null,
2751
+ contextId: executionId,
2752
+ prompt,
2753
+ originalPrompt: priorOutcome?.originalPrompt || priorOutcome?.prompt || prompt,
2754
+ workspacePath,
2755
+ userTier: String(this.config.get('subscription')?.plan || '').trim() || null,
2756
+ taskType: agentTaskType,
2757
+ workflowType,
2758
+ status: initialExecutionStatus,
2759
+ submittedAt: new Date().toISOString(),
2760
+ updatedAt: new Date().toISOString(),
2761
+ failedTaskIds: resumeTaskIds,
2762
+ unfinishedTaskIds: resumeTaskIds.length ? resumeTaskIds : ['agent-run-in-progress'],
2763
+ error: null,
2764
+ });
3202
2765
  // Start workspace watcher for bidirectional real-time sync
3203
2766
  let watcher = null;
3204
2767
  if (this.shouldStartWorkspaceWatcher(workspacePath)) {
3205
2768
  watcher = new WorkspaceWatcher({
3206
2769
  workspaceRoot: workspacePath,
3207
- onFileChange: (relativePath, content, action) => {
2770
+ onFileChange: (relativePath, _content, action) => {
3208
2771
  this.logger.debug(`Local change detected: ${action} ${relativePath}`);
3209
2772
  },
3210
2773
  });
@@ -3252,6 +2815,7 @@ export class ChatCommand {
3252
2815
  return;
3253
2816
  }
3254
2817
  if (event.type === 'plan') {
2818
+ this.updateAgentExecutionCheckpoint({ status: 'running' });
3255
2819
  taskDisplay.complete(0);
3256
2820
  const tasks = event?.plan?.tasks;
3257
2821
  if (Array.isArray(tasks) && tasks.length > 0) {
@@ -3277,10 +2841,7 @@ export class ChatCommand {
3277
2841
  noteAnalysisToolUse(toolName, liveOutcome);
3278
2842
  }
3279
2843
  else if (event.type === 'complete') {
3280
- const tt = Number(event.tasks_total);
3281
- if (Number.isFinite(tt) && tt > 0) {
3282
- liveOutcome.tasksTotal = tt;
3283
- }
2844
+ handleRunCompleteEvent(event, liveOutcome);
3284
2845
  if (Number(event.discovery_tools_used) > 0) {
3285
2846
  liveOutcome.analysisToolsUsed = Math.max(liveOutcome.analysisToolsUsed, Number(event.discovery_tools_used));
3286
2847
  }
@@ -3327,23 +2888,23 @@ export class ChatCommand {
3327
2888
  if (spinner) {
3328
2889
  spinner.stop();
3329
2890
  }
3330
- if (this.v3StreamingStarted && !this.v3StreamedAnswerDisplayed) {
2891
+ if (this.streamState.streamingStarted && !this.streamState.streamedAnswerDisplayed) {
3331
2892
  this.displayV3StreamedAnswer();
3332
2893
  }
3333
2894
  const previewGate = (response.metadata?.previewGate || null);
3334
2895
  const workspaceHasOutput = this.api.hasAgentWorkspaceOutput(workspaceContext);
3335
2896
  const changedFileCount = response.changedFiles ? Object.keys(response.changedFiles).length : 0;
3336
2897
  const requiresWorkspaceChanges = taskRequiresWorkspaceChangesWithContext(contextualPrompt, intentContext);
3337
- const answerContent = normalizeAgentAnswerContent(response.content, this.v3StreamedAnswerBuffer);
2898
+ const answerContent = normalizeAgentAnswerContent(response.content, this.streamState.streamedAnswerBuffer);
3338
2899
  liveOutcome.changedFileCount = changedFileCount;
3339
2900
  liveOutcome.requiresWorkspaceChanges = requiresWorkspaceChanges;
3340
2901
  liveOutcome.workspaceHasOutput = requiresWorkspaceChanges ? workspaceHasOutput : false;
3341
2902
  liveOutcome.answerContent = answerContent;
3342
2903
  const success = previewGate?.required === true
3343
- ? (previewGate?.passed === true || workspaceHasOutput)
2904
+ ? previewGate?.passed === true && previewGate?.skipped !== true
3344
2905
  : true;
3345
2906
  if (!success) {
3346
- if (this.isLegacyAgentFallbackAllowed()) {
2907
+ if (previewGate?.required !== true && this.isLegacyAgentFallbackAllowed()) {
3347
2908
  if (spinner) {
3348
2909
  spinner.stop();
3349
2910
  }
@@ -3356,25 +2917,16 @@ export class ChatCommand {
3356
2917
  if (spinner) {
3357
2918
  spinner.stop();
3358
2919
  }
3359
- this.logger.error(errorMessage);
2920
+ if (!this.jsonOutput)
2921
+ this.logger.error(errorMessage);
3360
2922
  this.messages.push({ role: 'assistant', content: errorMessage });
3361
- if (this.jsonOutput) {
3362
- process.exitCode = 1;
3363
- console.log(JSON.stringify({
3364
- success: false,
3365
- mode: 'agent',
3366
- model: routingPolicy.selectedModel,
3367
- partial: false,
3368
- content: '',
3369
- error: errorMessage,
3370
- metadata: { executionPath: 'v3-agent', previewGate },
3371
- }, null, 2));
3372
- }
3373
2923
  watcher?.stop();
3374
- return true;
3375
- }
3376
- if (!this.jsonOutput && previewGate?.required && previewGate?.passed !== true && workspaceHasOutput) {
3377
- 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
+ });
3378
2930
  }
3379
2931
  if (!this.jsonOutput && previewGate?.required) {
3380
2932
  if (previewGate.passed) {
@@ -3407,7 +2959,10 @@ export class ChatCommand {
3407
2959
  liveOutcome.answerContent = '';
3408
2960
  }
3409
2961
  const runEvaluation = evaluateExecutorSuccess(liveOutcome);
3410
- 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
+ }
3411
2966
  let finalAnswer = answerContent;
3412
2967
  if (isToolEvidenceStubAnswer(finalAnswer)) {
3413
2968
  finalAnswer = '';
@@ -3427,7 +2982,7 @@ export class ChatCommand {
3427
2982
  executorSucceeded = false;
3428
2983
  }
3429
2984
  if (!this.jsonOutput && !this.directPromptMode) {
3430
- if (this.v3StreamedAnswerDisplayed) {
2985
+ if (this.streamState.streamedAnswerDisplayed) {
3431
2986
  // Highlighted report already printed once above.
3432
2987
  }
3433
2988
  else if (isSubstantiveAgentAnswer(finalAnswer)) {
@@ -3523,9 +3078,6 @@ export class ChatCommand {
3523
3078
  }
3524
3079
  taskDisplay.finalize();
3525
3080
  // ────────────────────────────────────────────────────────────────
3526
- if (!executorSucceeded) {
3527
- process.exitCode = 1;
3528
- }
3529
3081
  this.commitAgentRunOutcome({
3530
3082
  prompt,
3531
3083
  originalPrompt: null,
@@ -3552,15 +3104,38 @@ export class ChatCommand {
3552
3104
  : null,
3553
3105
  finishedAt: Date.now(),
3554
3106
  }, prompt);
3107
+ this.updateAgentExecutionCheckpoint({
3108
+ status: executorSucceeded ? 'completed' : 'failed',
3109
+ contextId: response.contextId || executionId,
3110
+ failedTaskIds: [...liveOutcome.failedTaskIds],
3111
+ unfinishedTaskIds: [...liveOutcome.unfinishedTaskIds],
3112
+ error: executorSucceeded
3113
+ ? null
3114
+ : (liveOutcome.executorError || liveOutcome.plannerError || runEvaluation.statusHeadline),
3115
+ });
3555
3116
  if (!this.jsonOutput && !this.directPromptMode) {
3556
3117
  if (this.lastAgentRunOutcome) {
3557
3118
  this.printAgentRunSummary(this.lastAgentRunOutcome, runEvaluation, changedFileCount);
3558
3119
  }
3559
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
+ }
3560
3138
  if (this.jsonOutput) {
3561
- if (!executorSucceeded) {
3562
- process.exitCode = 1;
3563
- }
3564
3139
  emitAgentJsonOutput({
3565
3140
  success: executorSucceeded,
3566
3141
  mode: 'agent',
@@ -3593,6 +3168,11 @@ export class ChatCommand {
3593
3168
  catch (error) {
3594
3169
  this.stopV3IdleWatch();
3595
3170
  watcher?.stop();
3171
+ if (error instanceof CliCommandError) {
3172
+ if (spinner)
3173
+ spinner.stop();
3174
+ throw error;
3175
+ }
3596
3176
  if (!this.api.hasAgentWorkspaceOutput(workspaceContext)) {
3597
3177
  const recovered = await this.tryRecoverV3ServiceAndRetry(executionPrompt, prompt, workspaceContext, routingPolicy, spinner, error);
3598
3178
  if (recovered) {
@@ -3653,25 +3233,13 @@ export class ChatCommand {
3653
3233
  workspaceSyncIssue: null,
3654
3234
  finishedAt: Date.now(),
3655
3235
  }, prompt);
3656
- if (!this.jsonOutput) {
3657
- process.exitCode = 1;
3658
- }
3659
- if (this.jsonOutput) {
3660
- process.exitCode = 1;
3661
- console.log(JSON.stringify({
3662
- success: false,
3663
- mode: 'agent',
3664
- model: routingPolicy.selectedModel,
3665
- partial: liveOutcome.tasksSucceeded > 0 && liveOutcome.tasksTotal > 0,
3666
- content: '',
3667
- error: errorMessage,
3668
- statusHeadline: evaluateExecutorSuccess(liveOutcome).statusHeadline,
3669
- tasksSucceeded: liveOutcome.tasksSucceeded,
3670
- tasksTotal: liveOutcome.tasksTotal,
3671
- metadata: { executionPath: 'v3-agent' },
3672
- }, null, 2));
3673
- }
3674
- else if (!this.directPromptMode) {
3236
+ this.updateAgentExecutionCheckpoint({
3237
+ status: 'failed',
3238
+ failedTaskIds: [...liveOutcome.failedTaskIds],
3239
+ unfinishedTaskIds: [...liveOutcome.unfinishedTaskIds],
3240
+ error: liveOutcome.executorError || safeDetail || 'Connection aborted',
3241
+ });
3242
+ if (!this.jsonOutput && !this.directPromptMode) {
3675
3243
  const failedEval = evaluateExecutorSuccess({
3676
3244
  ...liveOutcome,
3677
3245
  executorFailed: true,
@@ -3684,7 +3252,20 @@ export class ChatCommand {
3684
3252
  this.printAgentRunSummary(this.lastAgentRunOutcome, failedEval, 0);
3685
3253
  }
3686
3254
  }
3687
- 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
+ });
3688
3269
  }
3689
3270
  }
3690
3271
  async tryRecoverV3ServiceAndRetry(executionPrompt, rawPrompt, workspaceContext, routingPolicy, spinner, error) {
@@ -3698,12 +3279,13 @@ export class ChatCommand {
3698
3279
  if (!this.jsonOutput) {
3699
3280
  console.log(chalk.yellow(`V3 recovery: ${recovery.message} Retrying once...`));
3700
3281
  }
3701
- this.v3IterationCount = 0;
3702
- this.v3ToolCallCount = 0;
3703
- this.v3LastActivity = Date.now();
3704
- this.v3StreamingStarted = false;
3705
- 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;
3706
3287
  try {
3288
+ this.updateAgentExecutionCheckpoint({ status: 'running' });
3707
3289
  const retryPriorPrompt = this.getPreviousActionablePrompt();
3708
3290
  const retryContext = { priorPrompt: retryPriorPrompt || null };
3709
3291
  const retryTaskType = inferAgentTaskTypeWithContext(rawPrompt, retryContext);
@@ -3730,27 +3312,40 @@ export class ChatCommand {
3730
3312
  if (spinner && spinner.isSpinning) {
3731
3313
  spinner.stop();
3732
3314
  }
3733
- if (this.v3StreamingStarted) {
3315
+ if (this.streamState.streamingStarted) {
3734
3316
  process.stdout.write('\n');
3735
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
+ }
3736
3326
  if (this.jsonOutput) {
3737
- console.log(JSON.stringify({
3327
+ emitAgentJsonOutput({
3738
3328
  success: true,
3739
3329
  mode: 'agent',
3740
3330
  model: routingPolicy.selectedModel,
3741
3331
  routingPolicy,
3742
3332
  taskId: retryResponse.taskId || null,
3743
3333
  contextId: retryResponse.contextId || null,
3744
- partial: retryResponse.partial === true,
3334
+ partial: false,
3745
3335
  content: retryResponse.content || 'V3 agent workflow completed after recovery.',
3746
3336
  metadata: { ...(retryResponse.metadata || {}), recoveryAttempted: true },
3747
- }, null, 2));
3337
+ });
3748
3338
  }
3749
- else if (!this.v3StreamingStarted && retryResponse.content) {
3339
+ else if (!this.streamState.streamingStarted && retryResponse.content) {
3750
3340
  console.log(chalk.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
3751
3341
  console.log(retryResponse.content);
3752
3342
  }
3753
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
+ });
3754
3349
  return true;
3755
3350
  }
3756
3351
  catch {
@@ -4357,6 +3952,13 @@ export class ChatCommand {
4357
3952
  }
4358
3953
  inferTargetFilesFromPrompt(prompt) {
4359
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
+ ]);
4360
3962
  const matches = Array.from(prompt.matchAll(/([A-Za-z0-9_./-]+\.[A-Za-z0-9_-]+)/g));
4361
3963
  for (const match of matches) {
4362
3964
  const candidate = match[1];
@@ -4364,7 +3966,15 @@ export class ChatCommand {
4364
3966
  continue;
4365
3967
  }
4366
3968
  const resolved = path.resolve(this.currentProjectPath, candidate);
4367
- 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)) {
4368
3978
  if (!candidates.includes(candidate)) {
4369
3979
  candidates.push(candidate);
4370
3980
  }
@@ -4375,36 +3985,6 @@ export class ChatCommand {
4375
3985
  inferTargetFileFromPrompt(prompt) {
4376
3986
  return this.inferTargetFilesFromPrompt(prompt)[0] || null;
4377
3987
  }
4378
- workspaceContainsHtmlEntry(rootDir) {
4379
- const stack = [rootDir];
4380
- while (stack.length > 0) {
4381
- const currentDir = stack.pop();
4382
- if (!currentDir) {
4383
- continue;
4384
- }
4385
- let entries = [];
4386
- try {
4387
- entries = fs.readdirSync(currentDir, { withFileTypes: true });
4388
- }
4389
- catch {
4390
- continue;
4391
- }
4392
- for (const entry of entries) {
4393
- if (entry.name === 'node_modules' || entry.name === '.git') {
4394
- continue;
4395
- }
4396
- const fullPath = path.join(currentDir, entry.name);
4397
- if (entry.isDirectory()) {
4398
- stack.push(fullPath);
4399
- continue;
4400
- }
4401
- if (entry.isFile() && entry.name.toLowerCase().endsWith('.html')) {
4402
- return true;
4403
- }
4404
- }
4405
- }
4406
- return false;
4407
- }
4408
3988
  shouldBypassDirectSingleFileFlow(targetFile, prompt) {
4409
3989
  const referencedFiles = this.inferTargetFilesFromPrompt(prompt);
4410
3990
  const editableFiles = referencedFiles.filter((filePath) => !this.isProtectedFileReferenceSafe(prompt, filePath));
@@ -4421,36 +4001,6 @@ export class ChatCommand {
4421
4001
  return true;
4422
4002
  return false;
4423
4003
  }
4424
- shouldPreferLocalAgentLoop(prompt) {
4425
- if (this.shouldRequireV3AgentWorkflow(prompt)) {
4426
- return false;
4427
- }
4428
- const forceV3 = /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_FORCE_V3_AGENT || ''));
4429
- if (forceV3) {
4430
- return false;
4431
- }
4432
- if (this.isBrowserTaskPrompt(prompt)) {
4433
- return false;
4434
- }
4435
- // Product default: Agent mode routes through V3.
4436
- // Local loop is opt-in for explicit offline/debug scenarios only.
4437
- // Explicit local paths now influence project root resolution, not routing mode.
4438
- const preferLocalOptIn = /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_PREFER_LOCAL_AGENT_LOOP || ''));
4439
- if (!preferLocalOptIn) {
4440
- return false;
4441
- }
4442
- const runtime = this.getRuntimeEnvironmentContext();
4443
- if (this.directPromptMode && this.isRepoGroundedPrompt(prompt)) {
4444
- return false;
4445
- }
4446
- if (!runtime.serverBindableWorkspace) {
4447
- return true;
4448
- }
4449
- if (!this.directPromptMode) {
4450
- return true;
4451
- }
4452
- return runtime.platform !== 'linux';
4453
- }
4454
4004
  getRuntimeEnvironmentContext() {
4455
4005
  const osPlatform = os.platform();
4456
4006
  const platform = osPlatform === 'win32'
@@ -4538,16 +4088,6 @@ export class ChatCommand {
4538
4088
  ];
4539
4089
  return protectedPhrases.some((phrase) => normalizedPrompt.includes(phrase));
4540
4090
  }
4541
- isProtectedFileReference(prompt, filePath) {
4542
- const escapedPath = filePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
4543
- const protectedPatterns = [
4544
- new RegExp(`do not modify\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?`, 'i'),
4545
- new RegExp(`don't modify\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?`, 'i'),
4546
- new RegExp(`leave\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?\\s+unchanged`, 'i'),
4547
- new RegExp(`without modifying\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?`, 'i'),
4548
- ];
4549
- return protectedPatterns.some((pattern) => pattern.test(prompt));
4550
- }
4551
4091
  buildContinuationPrompt() {
4552
4092
  const { discovery, searchFailed } = this.agentToolEvidence;
4553
4093
  const evidenceLines = [];
@@ -4641,109 +4181,10 @@ export class ChatCommand {
4641
4181
  return lines.join('\n');
4642
4182
  }
4643
4183
  extractToolCalls(message) {
4644
- const calls = [];
4645
- const seen = new Set();
4646
- const addCall = (call) => {
4647
- if (!call)
4648
- return;
4649
- const key = `${call.tool}::${JSON.stringify(call.args)}`;
4650
- if (!seen.has(key)) {
4651
- seen.add(key);
4652
- calls.push(call);
4653
- }
4654
- };
4655
- const wrapperRegex = /<tool_call>([\s\S]*?)<\/tool_call>/g;
4656
- for (const match of message.matchAll(wrapperRegex)) {
4657
- addCall(this.parseToolPayload(match[1] || ''));
4658
- }
4659
- for (const call of this.parseLegacyFunctionToolCalls(message)) {
4660
- addCall(call);
4661
- }
4662
- return calls;
4663
- }
4664
- normalizeCliToolName(name) {
4665
- const normalized = name.trim().toLowerCase();
4666
- const aliases = {
4667
- list_directory: 'list_dir',
4668
- listdirectory: 'list_dir',
4669
- ls: 'list_dir',
4670
- dir: 'list_dir',
4671
- readfile: 'read_file',
4672
- writefile: 'write_file',
4673
- editfile: 'edit_file',
4674
- shell: 'bash',
4675
- command: 'bash',
4676
- run_command: 'bash',
4677
- };
4678
- return aliases[normalized] || normalized;
4679
- }
4680
- parseLegacyFunctionToolCalls(message) {
4681
- const calls = [];
4682
- const functionRegex = /<function\s*=\s*["']?([A-Za-z0-9_-]+)["']?>\s*([\s\S]*?)(?:<\/function>|$)/gi;
4683
- for (const match of message.matchAll(functionRegex)) {
4684
- const tool = this.normalizeCliToolName(match[1] || '');
4685
- if (!tool)
4686
- continue;
4687
- const body = match[2] || '';
4688
- const args = {};
4689
- const parameterRegex = /<parameter\s*=\s*["']?([A-Za-z0-9_-]+)["']?>\s*([\s\S]*?)(?=<parameter\s*=|<\/function>|<\/parameter>|$)/gi;
4690
- for (const paramMatch of body.matchAll(parameterRegex)) {
4691
- const key = paramMatch[1] || '';
4692
- if (!key)
4693
- continue;
4694
- args[key] = (paramMatch[2] || '').replace(/<\/parameter>\s*$/i, '').trim();
4695
- }
4696
- calls.push({ tool, args });
4697
- }
4698
- return calls;
4699
- }
4700
- parseToolPayload(payload) {
4701
- const normalized = payload.trim();
4702
- if (!normalized) {
4703
- return null;
4704
- }
4705
- try {
4706
- const parsed = JSON.parse(normalized);
4707
- if (typeof parsed.tool !== 'string') {
4708
- return null;
4709
- }
4710
- const args = {};
4711
- if (parsed.args && typeof parsed.args === 'object') {
4712
- for (const [key, value] of Object.entries(parsed.args)) {
4713
- args[key] = typeof value === 'string' ? value : JSON.stringify(value);
4714
- }
4715
- }
4716
- return { tool: this.normalizeCliToolName(parsed.tool), args };
4717
- }
4718
- catch {
4719
- return null;
4720
- }
4184
+ return this.toolCallParser.extract(message);
4721
4185
  }
4722
4186
  stripToolPayloads(message) {
4723
- return message
4724
- .replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '')
4725
- .replace(/<function\s*=\s*["']?[A-Za-z0-9_-]+["']?>[\s\S]*?(?:<\/function>|$)/gi, '')
4726
- .trim();
4727
- }
4728
- extractFinalFileContent(message, targetFile) {
4729
- const trimmed = message.trim();
4730
- if (!trimmed) {
4731
- return '';
4732
- }
4733
- const fencedMatch = trimmed.match(/```(?:[A-Za-z0-9_-]+)?\n([\s\S]*?)```/);
4734
- if (fencedMatch && fencedMatch[1]) {
4735
- return fencedMatch[1].trim();
4736
- }
4737
- const extension = path.extname(targetFile).toLowerCase();
4738
- if (extension === '.html') {
4739
- const htmlStart = trimmed.indexOf('<!DOCTYPE html>') >= 0
4740
- ? trimmed.indexOf('<!DOCTYPE html>')
4741
- : trimmed.indexOf('<html');
4742
- if (htmlStart >= 0) {
4743
- return trimmed.slice(htmlStart).trim();
4744
- }
4745
- }
4746
- return trimmed;
4187
+ return this.toolCallParser.strip(message);
4747
4188
  }
4748
4189
  /**
4749
4190
  * Synthesize a best-effort answer from tool evidence already collected
@@ -4772,8 +4213,8 @@ export class ChatCommand {
4772
4213
  resolveDirectModeCompletion(prompt, visibleText) {
4773
4214
  // Sanitize first — strip tool output and echoed instructions before
4774
4215
  // deciding if the model actually answered the question.
4775
- const sanitized = this.sanitizeDirectModeOutput((visibleText || '').trim());
4776
- if (sanitized && !this.isDirectModeFollowUpQuestion(sanitized)) {
4216
+ const sanitized = sanitizeDirectModeOutput((visibleText || '').trim());
4217
+ if (sanitized && !isDirectModeFollowUpQuestion(sanitized)) {
4777
4218
  return sanitized;
4778
4219
  }
4779
4220
  const fallback = this.buildLocalAnalysisFallback(prompt);
@@ -4782,95 +4223,6 @@ export class ChatCommand {
4782
4223
  }
4783
4224
  return sanitized || 'Task complete.';
4784
4225
  }
4785
- /**
4786
- * Strip system-prompt echoes, tool execution headers, grounding-rule
4787
- * parroting, recovery banners, and multi-line tool output blocks from
4788
- * the model's final answer so that --json output contains only the
4789
- * substantive answer.
4790
- */
4791
- sanitizeDirectModeOutput(text) {
4792
- let cleaned = this.stripHiddenThoughtBlocks(text);
4793
- cleaned = cleaned
4794
- .replace(/<\|mask_start\|>[\s\S]*?<\|mask_end\|>/g, '')
4795
- .replace(/<think>[\s\S]*?<\/think>/gi, '')
4796
- .replace(/<\|(?:mask_start|mask_end)\|>/g, '');
4797
- // ── Phase 1: Strip entire tool-output blocks ──
4798
- // Matches "Tool <name> succeeded/FAILED." through the next blank line,
4799
- // next tool header, or end-of-string. The DOTALL-like [\s\S]*? is
4800
- // terminated by whichever boundary comes first.
4801
- 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, '');
4802
- // ── Phase 2: Strip echoed system-prompt / grounding lines ──
4803
- const contaminationPatterns = [
4804
- /^\[Agent recovered from backend failure[^\]]*\]\s*/m,
4805
- /^Evidence gathered before backend failure:?\s*/m,
4806
- /^MANDATORY CROSS-FILE EVIDENCE[^\n]*\n?/m,
4807
- /^CONFIRMED CONFLICTING keys[^\n]*/m,
4808
- /^Keys found ONLY in [^\n]*/m,
4809
- /^CONSTRAINT: Your answer MUST[^\n]*/m,
4810
- /^GROUNDING CHECK:[^\n]*/m,
4811
- /^VERIFICATION PROTOCOL[^\n]*/m,
4812
- /^Quality gate:[^\n]*/m,
4813
- /^Evidence collected:[^\n]*/m,
4814
- /^Warning: \d+ search tool[^\n]*/m,
4815
- /^Tool results received for direct mode[^\n]*/m,
4816
- /^Step \d+ complete\. Task:[^\n]*/m,
4817
- /^Original user request:[^\n]*/m,
4818
- /^Project root boundary:[^\n]*/m,
4819
- /^Do not declare success[^\n]*/m,
4820
- /^Keep working from concrete[^\n]*/m,
4821
- /^Continue with tool calls if more[^\n]*/m,
4822
- /^IMPORTANT: Your response must be ONLY[^\n]*/m,
4823
- /^Because this is a debugging[^\n]*/m,
4824
- /^If a user is asking which file[^\n]*/m,
4825
- /^If the request is already[^\n]*/m,
4826
- /^If more work is required[^\n]*/m,
4827
- /^Do not ask follow-up[^\n]*/m,
4828
- /^CRITICAL GROUNDING RULE:[^\n]*/m,
4829
- /^CROSS-FILE ATTRIBUTION:[^\n]*/m,
4830
- /^EVIDENCE-GROUNDING RULE:[^\n]*/m,
4831
- /^CROSS-FILE RULE:[^\n]*/m,
4832
- /^OUTPUT DISCIPLINE:[^\n]*/m,
4833
- /^Vigthoria CLI agent operating contract\.[^\n]*/m,
4834
- /^You are operating inside the project root:[^\n]*/m,
4835
- /^CRITICAL: Begin working on the user's task[^\n]*/m,
4836
- /^File: \S+\s*$/m,
4837
- /^Search status: \S+\s*$/m,
4838
- /^Output:\s*$/m,
4839
- /^\[\.\.\. ?truncated\]\s*$/m,
4840
- /^---\s*$/m,
4841
- ];
4842
- for (const pat of contaminationPatterns) {
4843
- cleaned = cleaned.replace(pat, '');
4844
- }
4845
- // ── Phase 3: Paragraph-level filter ──
4846
- // Split into paragraph blocks and discard any that are pure tool
4847
- // output, file content dumps, or system-instruction echoes.
4848
- const paragraphs = cleaned.split(/\n\n+/);
4849
- const kept = paragraphs.filter(p => {
4850
- const t = p.trim();
4851
- if (!t)
4852
- return false;
4853
- // Discard lingering tool headers
4854
- if (/^Tool (?:read_file|grep|list_dir|glob|bash|write_file) /i.test(t))
4855
- return false;
4856
- // Discard paragraphs that are mostly file-content dumps (many
4857
- // lines of code with typical code tokens like { } ; = function class)
4858
- const lines = t.split('\n');
4859
- if (lines.length > 6) {
4860
- const codeLines = lines.filter(l => /[{};=]|function |class |const |let |var |import |export |switch |case /.test(l));
4861
- if (codeLines.length / lines.length > 0.4)
4862
- return false;
4863
- }
4864
- return true;
4865
- });
4866
- cleaned = kept.join('\n\n');
4867
- // Collapse multiple blank lines
4868
- cleaned = cleaned.replace(/\n{3,}/g, '\n\n').trim();
4869
- return cleaned || this.stripHiddenThoughtBlocks(text);
4870
- }
4871
- isDirectModeFollowUpQuestion(text) {
4872
- 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());
4873
- }
4874
4226
  buildLocalAnalysisFallback(prompt) {
4875
4227
  if (!/(analyse|analyze|audit|overview|inspect|review|summari[sz]e|actual state)/i.test(prompt)) {
4876
4228
  return '';
@@ -4912,42 +4264,6 @@ export class ChatCommand {
4912
4264
  return '';
4913
4265
  }
4914
4266
  }
4915
- tryDeterministicSingleFileRewrite(prompt, targetFile, currentContent) {
4916
- const extension = path.extname(targetFile).toLowerCase();
4917
- if (extension !== '.html') {
4918
- return null;
4919
- }
4920
- const exactText = this.extractExactTextRequirement(prompt);
4921
- if (!exactText) {
4922
- return null;
4923
- }
4924
- if (!/hero\s+(?:paragraph|copy)/i.test(prompt)) {
4925
- return null;
4926
- }
4927
- const heroCopyPattern = /(<p[^>]*class=["'][^"']*hero-copy[^"']*["'][^>]*>)([\s\S]*?)(<\/p>)/i;
4928
- if (heroCopyPattern.test(currentContent)) {
4929
- const nextContent = currentContent.replace(heroCopyPattern, `$1${exactText}$3`);
4930
- return nextContent !== currentContent ? nextContent : null;
4931
- }
4932
- const heroParagraphPattern = /(<main[^>]*class=["'][^"']*hero[^"']*["'][^>]*>[\s\S]*?<p[^>]*>)([\s\S]*?)(<\/p>)/i;
4933
- if (heroParagraphPattern.test(currentContent)) {
4934
- const nextContent = currentContent.replace(heroParagraphPattern, `$1${exactText}$3`);
4935
- return nextContent !== currentContent ? nextContent : null;
4936
- }
4937
- return null;
4938
- }
4939
- extractExactTextRequirement(prompt) {
4940
- const match = prompt.match(/reads exactly:\s*([\s\S]*?)(?:\s+Do not\b|\s+Do\b|$)/i);
4941
- if (!match || !match[1]) {
4942
- return null;
4943
- }
4944
- const normalized = match[1]
4945
- .trim()
4946
- .replace(/^[`"']+/, '')
4947
- .replace(/[`"']+$/, '')
4948
- .trim();
4949
- return normalized || null;
4950
- }
4951
4267
  async executeToolCalls(toolCalls) {
4952
4268
  if (!this.tools) {
4953
4269
  throw new Error('Agent tools are not initialized.');
@@ -5056,17 +4372,41 @@ export class ChatCommand {
5056
4372
  sessionSummary: this.currentSession.memorySummary || '',
5057
4373
  });
5058
4374
  }
4375
+ persistAgentExecutionCheckpoint(checkpoint) {
4376
+ if (!this.currentSession) {
4377
+ this.currentSession = this.sessionManager.create(this.currentProjectPath, this.currentModel, this.agentMode, this.operatorMode);
4378
+ }
4379
+ this.currentSession.activeAgentRun = checkpoint;
4380
+ this.saveSession();
4381
+ }
4382
+ updateAgentExecutionCheckpoint(update) {
4383
+ if (!this.currentSession?.activeAgentRun) {
4384
+ return;
4385
+ }
4386
+ if (update.status) {
4387
+ assertAgentStatusTransition(this.currentSession.activeAgentRun.status, update.status);
4388
+ }
4389
+ this.currentSession.activeAgentRun = {
4390
+ ...this.currentSession.activeAgentRun,
4391
+ ...update,
4392
+ updatedAt: new Date().toISOString(),
4393
+ };
4394
+ this.saveSession();
4395
+ }
5059
4396
  async requestPermission(action) {
5060
4397
  if (this.autoApprove) {
5061
4398
  return true;
5062
4399
  }
4400
+ if (this.jsonOutput) {
4401
+ return false;
4402
+ }
5063
4403
  const rl = readline.createInterface({
5064
4404
  input: process.stdin,
5065
4405
  output: process.stdout,
5066
4406
  });
5067
4407
  console.log(action);
5068
4408
  const answer = await new Promise((resolve) => {
5069
- 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);
5070
4410
  });
5071
4411
  rl.close();
5072
4412
  const normalized = answer.trim().toLowerCase();