vigthoria-cli 1.11.11 → 1.11.17

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.
@@ -28,6 +28,7 @@ export declare class ChatCommand {
28
28
  private tools;
29
29
  private sessionManager;
30
30
  private projectMemory;
31
+ private workspaceBrain;
31
32
  private currentSession;
32
33
  private agentMode;
33
34
  private currentProjectPath;
@@ -51,6 +52,7 @@ export declare class ChatCommand {
51
52
  private v3StreamedAnswerDisplayed;
52
53
  private v3SeenToolCalls;
53
54
  private v3SeenToolResults;
55
+ private lastAgentRoute;
54
56
  private lastAgentRunOutcome;
55
57
  private isJwtExpirationError;
56
58
  private isNetworkError;
@@ -103,7 +105,11 @@ export declare class ChatCommand {
103
105
  private buildTaskShapingInstructions;
104
106
  private buildExecutionPrompt;
105
107
  private isProjectBrainRuntimeDisabled;
108
+ private getWorkspaceBrainService;
109
+ private canPromptForWorkspaceIndex;
110
+ private promptWorkspaceIndex;
106
111
  private buildProjectBrainRuntimeContext;
112
+ private buildWorkspaceBrainRuntimeContext;
107
113
  private rememberBrainEvent;
108
114
  private getPromptRuntimeContext;
109
115
  private v3IterationCount;
@@ -168,6 +174,10 @@ export declare class ChatCommand {
168
174
  private persistLocalLoopOutcome;
169
175
  private runLocalAgentLoop;
170
176
  private primeBypassedTargetFileContext;
177
+ /**
178
+ * Sub-second path for trivial HTML/hello-world: Template Service → index.html (no V3 planner).
179
+ */
180
+ private tryTemplateInstantPath;
171
181
  private tryDirectSingleFileFlow;
172
182
  private isConfirmationFollowUp;
173
183
  private taskRequiresWorkspaceChanges;
@@ -11,10 +11,15 @@ import { BridgeClient, getBridgeClient } from '../utils/bridge-client.js';
11
11
  import { WorkspaceWatcher } from '../utils/workspace-stream.js';
12
12
  import { TaskDisplay } from '../utils/task-display.js';
13
13
  import { ProjectMemoryService } from '../utils/project-memory.js';
14
+ import { WorkspaceBrainService } from '../utils/workspace-brain-service.js';
14
15
  import { buildPersonaOverlay, normalizePersonaMode } from '../utils/persona.js';
15
16
  import { runAgentSessionMenu, shouldShowAgentSessionMenu } from './agent-session-menu.js';
16
17
  import { createLiveOutcome, evaluateExecutorSuccess, handleTaskEvent, isSubstantiveAgentAnswer, isToolEvidenceStubAnswer, normalizeAgentAnswerContent, noteAnalysisToolUse, } from '../utils/agentRunOutcome.js';
17
18
  import { looksLikeMarkdownReport, renderMarkdownToTerminal, summarizeMarkdownReport, } from '../utils/terminalMarkdown.js';
19
+ import { emitDeckEvent, isDeckModeEnabled } from '../utils/deckEvents.js';
20
+ import { inferAgentTaskType as sharedInferAgentTaskType, isTrivialHtmlPageRequest, resolvePlannerAgentTimeoutMs, resolveWorkflowType, shouldSkipAnalysisRescue, taskRequiresWorkspaceChanges as promptRequiresWorkspaceChanges, buildExecutionHints, } from '../utils/requestIntent.js';
21
+ import { resolveAgentRoute } from '../utils/agentRoute.js';
22
+ import { runTemplateInstantPath } from '../utils/templateInstantPath.js';
18
23
  const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
19
24
  const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
20
25
  if (!rawValue) {
@@ -47,6 +52,7 @@ export class ChatCommand {
47
52
  tools = null;
48
53
  sessionManager;
49
54
  projectMemory = null;
55
+ workspaceBrain = null;
50
56
  currentSession = null;
51
57
  agentMode = false;
52
58
  currentProjectPath = process.cwd();
@@ -71,6 +77,7 @@ export class ChatCommand {
71
77
  v3StreamedAnswerDisplayed = false;
72
78
  v3SeenToolCalls = new Set();
73
79
  v3SeenToolResults = new Set();
80
+ lastAgentRoute = null;
74
81
  // Last completed Agent run — used by /retry, /continue, and the final summary block.
75
82
  lastAgentRunOutcome = null;
76
83
  isJwtExpirationError(error) {
@@ -304,6 +311,16 @@ export class ChatCommand {
304
311
  routeReason: 'explicit-model-selection',
305
312
  };
306
313
  }
314
+ if (this.lastAgentRoute?.path === 'template-instant') {
315
+ return {
316
+ selectedModel: 'agent',
317
+ explicitModel: false,
318
+ heavyTask,
319
+ cloudEligible,
320
+ cloudSelected: false,
321
+ routeReason: `template-instant [${this.lastAgentRoute.source}]`,
322
+ };
323
+ }
307
324
  if (requiresV3Workflow) {
308
325
  return {
309
326
  selectedModel: 'agent',
@@ -314,6 +331,16 @@ export class ChatCommand {
314
331
  routeReason: 'v3-required-frontend-task',
315
332
  };
316
333
  }
334
+ if (this.lastAgentRoute?.source === 'balanced-4b' || this.lastAgentRoute?.source === 'merged') {
335
+ return {
336
+ selectedModel: 'agent',
337
+ explicitModel: false,
338
+ heavyTask,
339
+ cloudEligible,
340
+ cloudSelected: false,
341
+ routeReason: `balanced-4b-router → ${this.lastAgentRoute.taskKind}`,
342
+ };
343
+ }
317
344
  if (this.config.shouldUseCloudForHeavyTask(prompt)) {
318
345
  return {
319
346
  selectedModel: 'cloud',
@@ -550,14 +577,7 @@ export class ChatCommand {
550
577
  return 'debugging';
551
578
  if (this.isAnalysisLookupPrompt(prompt))
552
579
  return 'analysis';
553
- if (/\b(analy[sz]e|analyse|audit|review|inspect|identify\s+gaps?|gap\s+analysis|production\s+blockers?|read[\s-]?only)\b/i.test(prompt)) {
554
- return 'analysis';
555
- }
556
- if (/\b(implement|build|create|write|fix|repair|scaffold|deploy|add feature)\b/i.test(prompt)
557
- && !/\b(analy[sz]e|analyse|audit|review|inspect)\b/i.test(prompt)) {
558
- return /\b(game|html5|pacman|rogue|playable)\b/i.test(prompt) ? 'game-build' : 'implementation';
559
- }
560
- return 'analysis';
580
+ return sharedInferAgentTaskType(prompt);
561
581
  }
562
582
  buildTaskShapingInstructions(prompt) {
563
583
  const instructions = [];
@@ -593,6 +613,45 @@ export class ChatCommand {
593
613
  isProjectBrainRuntimeDisabled() {
594
614
  return /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_NO_BRAIN || process.env.VIGTHORIA_BRAIN_DISABLED || ''));
595
615
  }
616
+ getWorkspaceBrainService() {
617
+ if (this.isProjectBrainRuntimeDisabled()) {
618
+ return null;
619
+ }
620
+ try {
621
+ if (!this.workspaceBrain) {
622
+ this.workspaceBrain = new WorkspaceBrainService({
623
+ workspacePath: this.currentProjectPath,
624
+ apiBase: this.config.get('apiUrl'),
625
+ getAuthToken: () => this.config.get('authToken'),
626
+ autoIndex: false,
627
+ });
628
+ }
629
+ return this.workspaceBrain;
630
+ }
631
+ catch {
632
+ return null;
633
+ }
634
+ }
635
+ canPromptForWorkspaceIndex() {
636
+ if (this.jsonOutput || !process.stdin.isTTY || !process.stdout.isTTY) {
637
+ return false;
638
+ }
639
+ return !/^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_NO_INDEX_PROMPT || ''));
640
+ }
641
+ async promptWorkspaceIndex(fileCount, workspaceName) {
642
+ if (!this.canPromptForWorkspaceIndex()) {
643
+ return 'later';
644
+ }
645
+ const answer = await new Promise((resolve) => {
646
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
647
+ const prompt = `Index "${workspaceName}" for Vigthoria Brain (${fileCount} files) to improve large-context tasks? [y/N] `;
648
+ rl.question(chalk.cyan(prompt), (value) => {
649
+ rl.close();
650
+ resolve(value);
651
+ });
652
+ });
653
+ return /^(y|yes)$/i.test(answer.trim()) ? 'index_now' : 'later';
654
+ }
596
655
  buildProjectBrainRuntimeContext(prompt, source) {
597
656
  if (this.isProjectBrainRuntimeDisabled()) {
598
657
  return undefined;
@@ -619,6 +678,42 @@ export class ChatCommand {
619
678
  return undefined;
620
679
  }
621
680
  }
681
+ async buildWorkspaceBrainRuntimeContext(prompt) {
682
+ const service = this.getWorkspaceBrainService();
683
+ if (!service) {
684
+ return {};
685
+ }
686
+ try {
687
+ const result = await service.ensureIndexed({
688
+ promptIfMissing: this.canPromptForWorkspaceIndex(),
689
+ askToIndex: (fileCount, workspaceName) => this.promptWorkspaceIndex(fileCount, workspaceName),
690
+ });
691
+ const status = service.getStatus();
692
+ const context = {
693
+ workspaceBrain: {
694
+ schema: 'vigthoria.workspace-brain.v1',
695
+ indexed: result.indexed,
696
+ prompted: result.prompted || null,
697
+ indexedFileCount: status.indexedFileCount || result.fileCount,
698
+ totalChunks: status.totalChunks || result.chunkCount,
699
+ indexedAt: status.meta?.indexedAt || null,
700
+ indexHash: status.meta?.indexHash || null,
701
+ },
702
+ };
703
+ const codebaseContext = service.buildCodebaseContext(prompt);
704
+ if (codebaseContext) {
705
+ context.codebaseContext = codebaseContext;
706
+ }
707
+ const accountBrainContext = await service.fetchAccountBrainContext().catch(() => '');
708
+ if (accountBrainContext) {
709
+ context.accountBrainContext = accountBrainContext;
710
+ }
711
+ return context;
712
+ }
713
+ catch {
714
+ return {};
715
+ }
716
+ }
622
717
  rememberBrainEvent(type, text, mode) {
623
718
  if (this.isProjectBrainRuntimeDisabled()) {
624
719
  return;
@@ -640,6 +735,19 @@ export class ChatCommand {
640
735
  const brainContext = this.buildProjectBrainRuntimeContext(prompt, this.operatorMode ? 'vigthoria-cli.operator' : this.agentMode ? 'vigthoria-cli.agent' : 'vigthoria-cli.chat');
641
736
  if (brainContext) {
642
737
  runtimeContext.vigthoriaBrain = brainContext;
738
+ if (typeof brainContext.context === 'string' && brainContext.context.trim()) {
739
+ runtimeContext.projectMemory = brainContext.context;
740
+ }
741
+ }
742
+ const workspaceBrainContext = await this.buildWorkspaceBrainRuntimeContext(prompt);
743
+ if (workspaceBrainContext.workspaceBrain) {
744
+ runtimeContext.workspaceBrain = workspaceBrainContext.workspaceBrain;
745
+ }
746
+ if (typeof workspaceBrainContext.codebaseContext === 'string' && workspaceBrainContext.codebaseContext.trim()) {
747
+ runtimeContext.codebaseContext = workspaceBrainContext.codebaseContext;
748
+ }
749
+ if (typeof workspaceBrainContext.accountBrainContext === 'string' && workspaceBrainContext.accountBrainContext.trim()) {
750
+ runtimeContext.accountBrainContext = workspaceBrainContext.accountBrainContext;
643
751
  }
644
752
  if (!this.isBrowserTaskPrompt(prompt)) {
645
753
  return runtimeContext;
@@ -803,6 +911,11 @@ export class ChatCommand {
803
911
  if (!isSubstantiveAgentAnswer(content)) {
804
912
  return;
805
913
  }
914
+ emitDeckEvent({ type: 'report_ready', markdown: content });
915
+ if (isDeckModeEnabled()) {
916
+ this.v3StreamedAnswerDisplayed = true;
917
+ return;
918
+ }
806
919
  console.log('');
807
920
  console.log(renderMarkdownToTerminal(content, this.getTerminalContentWidth()));
808
921
  this.v3StreamedAnswerDisplayed = true;
@@ -875,6 +988,13 @@ export class ChatCommand {
875
988
  }
876
989
  this.v3SeenToolCalls.add(eventKey);
877
990
  this.v3ToolCallCount += 1;
991
+ const toolName = event.name || event.tool || event.tool_name || '';
992
+ emitDeckEvent({
993
+ type: 'tool_start',
994
+ tool: toolName,
995
+ index: this.v3ToolCallCount,
996
+ target: event.arguments?.path || event.arguments?.file_path || event.arguments?.pattern || '',
997
+ });
878
998
  const toolDesc = this.describeV3AgentTool(event.tool || event.name || event.tool_name);
879
999
  const toolTarget = event.arguments?.path || event.arguments?.file_path || event.arguments?.pattern || '';
880
1000
  const sanitizedTarget = this.sanitizeServerPath(String(toolTarget));
@@ -885,16 +1005,16 @@ export class ChatCommand {
885
1005
  this.writeAgentActivityLine(this.fitTerminalText(stepLabel) + '\n');
886
1006
  // Show extra detail for key tools
887
1007
  const args = event.arguments || {};
888
- const toolName = event.name || event.tool || '';
889
- if ((toolName === 'write_file' || toolName === 'edit_file') && typeof args.content === 'string') {
1008
+ const toolNameForDetail = event.name || event.tool || '';
1009
+ if ((toolNameForDetail === 'write_file' || toolNameForDetail === 'edit_file') && typeof args.content === 'string') {
890
1010
  const len = args.content.length;
891
1011
  this.writeAgentActivityLine(chalk.gray(` ${len > 1000 ? Math.round(len / 1024) + ' KB' : len + ' bytes'} content\n`));
892
1012
  }
893
- else if (toolName === 'bash' && typeof args.command === 'string') {
1013
+ else if (toolNameForDetail === 'bash' && typeof args.command === 'string') {
894
1014
  const command = this.sanitizeServerPath(args.command);
895
1015
  this.writeAgentActivityLine(chalk.gray(` ${this.fitTerminalText(`$ ${command}`, 6)}\n`));
896
1016
  }
897
- else if (toolName === 'read_file') {
1017
+ else if (toolNameForDetail === 'read_file') {
898
1018
  this.writeAgentActivityLine(chalk.gray(` ${this.fitTerminalText(`reading ${sanitizedTarget || 'file'}`, 6)}\n`));
899
1019
  }
900
1020
  spinner.text = `Running ${toolDesc}...`;
@@ -909,6 +1029,12 @@ export class ChatCommand {
909
1029
  this.v3SeenToolResults.add(eventKey);
910
1030
  const success = event.success !== false;
911
1031
  const toolName = event.name || event.tool || '';
1032
+ emitDeckEvent({
1033
+ type: 'tool_end',
1034
+ tool: toolName,
1035
+ success,
1036
+ index: this.v3ToolCallCount,
1037
+ });
912
1038
  const indicator = success ? chalk.green(' ✓') : chalk.red(' ✗');
913
1039
  if (spinner.isSpinning)
914
1040
  spinner.stop();
@@ -981,6 +1107,13 @@ export class ChatCommand {
981
1107
  if (spinner.isSpinning)
982
1108
  spinner.stop();
983
1109
  this.displayV3StreamedAnswer();
1110
+ emitDeckEvent({
1111
+ type: 'run_complete',
1112
+ success: true,
1113
+ tools: event.tool_calls || this.v3ToolCallCount,
1114
+ iterations: event.iterations || this.v3IterationCount,
1115
+ elapsed: event.elapsed || '',
1116
+ });
984
1117
  let statLine = `${iters} iterations, ${tools} tool calls`;
985
1118
  if (elapsed)
986
1119
  statLine += `, ${elapsed}`;
@@ -2031,7 +2164,20 @@ export class ChatCommand {
2031
2164
  if (!this.tools) {
2032
2165
  throw new Error('Agent tools are not initialized.');
2033
2166
  }
2167
+ this.lastAgentRoute = await resolveAgentRoute(this.api, prompt);
2168
+ if (!this.jsonOutput) {
2169
+ const r = this.lastAgentRoute;
2170
+ const routerNote = r.routerLatencyMs
2171
+ ? chalk.gray(` (${r.routerModel || 'balanced-4b'} ${r.routerLatencyMs}ms)`)
2172
+ : '';
2173
+ this.logger.debug(`Agent route: ${r.path} / ${r.taskKind} [${r.source}]${routerNote} — ${r.reason}`);
2174
+ }
2034
2175
  const requiresV3Workflow = this.shouldRequireV3AgentWorkflow(prompt);
2176
+ const handledByTemplateInstant = await this.tryTemplateInstantPath(prompt);
2177
+ if (handledByTemplateInstant) {
2178
+ this.saveSession();
2179
+ return;
2180
+ }
2035
2181
  const handledByDirectFileFlow = await this.tryDirectSingleFileFlow(prompt);
2036
2182
  if (handledByDirectFileFlow) {
2037
2183
  this.saveSession();
@@ -2371,6 +2517,98 @@ export class ChatCommand {
2371
2517
  this.messages.push({ role: 'system', content: readSummary });
2372
2518
  }
2373
2519
  }
2520
+ /**
2521
+ * Sub-second path for trivial HTML/hello-world: Template Service → index.html (no V3 planner).
2522
+ */
2523
+ async tryTemplateInstantPath(prompt) {
2524
+ const route = this.lastAgentRoute;
2525
+ if (route?.path !== 'template-instant' && !isTrivialHtmlPageRequest(prompt)) {
2526
+ return false;
2527
+ }
2528
+ let workspacePath = this.currentProjectPath;
2529
+ try {
2530
+ const fromPrompt = this.resolvePromptWorkspacePath(prompt, this.currentProjectPath);
2531
+ if (fromPrompt)
2532
+ workspacePath = fromPrompt;
2533
+ }
2534
+ catch {
2535
+ // use default workspace
2536
+ }
2537
+ if (!this.jsonOutput) {
2538
+ console.log();
2539
+ console.log(chalk.gray('━━━ ROUTING DECISION ━━━'));
2540
+ const r = this.lastAgentRoute;
2541
+ console.log(chalk.gray(`Reason: ${r?.reason || 'template-instant (trivial HTML page)'}`));
2542
+ console.log(chalk.gray(`Path: template-instant [${r?.source || 'regex'}]`));
2543
+ console.log(chalk.gray('Model: template-service'));
2544
+ console.log(chalk.gray('Cloud Eligible: false'));
2545
+ console.log(chalk.gray('Cloud Selected: false'));
2546
+ console.log(chalk.gray('━'.repeat(30)));
2547
+ console.log();
2548
+ console.log(chalk.cyan('⚡ Template instant path — matching template and writing index.html...'));
2549
+ }
2550
+ this.messages.push({ role: 'user', content: prompt });
2551
+ const mustComplete = route?.path === 'template-instant';
2552
+ let result = await runTemplateInstantPath(this.api, prompt, workspacePath, {
2553
+ allowFallback: true,
2554
+ });
2555
+ if (!result.ok && mustComplete) {
2556
+ result = await runTemplateInstantPath(this.api, prompt, workspacePath, {
2557
+ allowFallback: true,
2558
+ forceMinimalFallback: true,
2559
+ });
2560
+ }
2561
+ if (!result.ok) {
2562
+ if (!this.jsonOutput) {
2563
+ console.log(chalk.yellow(`Template instant path skipped: ${result.error || 'no match'}`));
2564
+ }
2565
+ return false;
2566
+ }
2567
+ const summary = [
2568
+ `Created ${result.entryPath || 'index.html'} via template-instant`,
2569
+ result.templateName ? `(${result.templateName})` : '',
2570
+ result.processingMs ? `in ${Math.round(result.processingMs)}ms` : '',
2571
+ result.usedFallback ? '(minimal fallback)' : '',
2572
+ ].filter(Boolean).join(' ');
2573
+ this.messages.push({ role: 'assistant', content: summary });
2574
+ const liveOutcome = createLiveOutcome();
2575
+ liveOutcome.requiresWorkspaceChanges = true;
2576
+ liveOutcome.workspaceHasOutput = true;
2577
+ liveOutcome.changedFileCount = 1;
2578
+ liveOutcome.tasksTotal = 1;
2579
+ liveOutcome.tasksSucceeded = 1;
2580
+ liveOutcome.answerContent = summary;
2581
+ const evaluation = evaluateExecutorSuccess(liveOutcome);
2582
+ this.lastAgentRunOutcome = {
2583
+ prompt,
2584
+ taskId: null,
2585
+ contextId: null,
2586
+ tasksSucceeded: 1,
2587
+ tasksTotal: 1,
2588
+ failedTaskIds: [],
2589
+ unfinishedTaskIds: [],
2590
+ qualityScore: null,
2591
+ qualityMissing: [],
2592
+ qualityBlockers: [],
2593
+ hasOutput: true,
2594
+ answerContent: summary,
2595
+ selfHealStatus: 'skipped',
2596
+ selfHealTool: null,
2597
+ plannerError: null,
2598
+ executorError: null,
2599
+ clientToolErrors: [],
2600
+ transportErrors: [],
2601
+ workspacePath: workspacePath || null,
2602
+ workspaceSyncIssue: null,
2603
+ finishedAt: Date.now(),
2604
+ };
2605
+ if (!this.jsonOutput) {
2606
+ console.log(chalk.green(`✓ ${summary}`));
2607
+ console.log(chalk.gray(' Run: vigthoria preview'));
2608
+ this.printAgentRunSummary(this.lastAgentRunOutcome, evaluation, 1);
2609
+ }
2610
+ return true;
2611
+ }
2374
2612
  async tryDirectSingleFileFlow(prompt) {
2375
2613
  if (!this.directPromptMode || !this.tools) {
2376
2614
  return false;
@@ -2490,13 +2728,7 @@ export class ChatCommand {
2490
2728
  return /^(ja|ja bitte|ja bitte mach das|mach das|bitte mach das|genau|ok|okay|yes|yes please|please do|do it|go ahead|continue|proceed|make it so)$/.test(normalized);
2491
2729
  }
2492
2730
  taskRequiresWorkspaceChanges(prompt) {
2493
- const text = String(prompt || '').trim();
2494
- const readOnlyIntent = /\b(analy[sz]e|analyse|analysis|audit|review|inspect|proof|understand|summari[sz]e|scan|read[\s-]?only|where we left|what is|how does|show me|tell me|identify\s+gaps?|gap\s+analysis|production\s+blockers?)\b/i.test(text);
2495
- const explicitWriteIntent = /\b(build|create|make|implement|complete|fix|repair|edit|modify|write|generate|add|finish|scaffold|develop|update|change|refactor)\b/i.test(text);
2496
- if (readOnlyIntent && !explicitWriteIntent) {
2497
- return false;
2498
- }
2499
- return explicitWriteIntent && /\b(file|project|game|app|website|html5|frontend|component|feature|code|workspace|repo)\b/i.test(text);
2731
+ return promptRequiresWorkspaceChanges(prompt);
2500
2732
  }
2501
2733
  getPreviousActionablePrompt() {
2502
2734
  if (this.lastActionableUserInput && !this.isConfirmationFollowUp(this.lastActionableUserInput)) {
@@ -2578,6 +2810,7 @@ export class ChatCommand {
2578
2810
  this.v3StreamedAnswerDisplayed = false;
2579
2811
  this.v3SeenToolCalls.clear();
2580
2812
  this.v3SeenToolResults.clear();
2813
+ emitDeckEvent({ type: 'agent_start' });
2581
2814
  const taskDisplay = new TaskDisplay(['Analyse workspace', 'Execute tasks', 'Validate output', 'Self-heal'], false);
2582
2815
  taskDisplay.start(0);
2583
2816
  const spinner = this.jsonOutput ? null : createSpinner({
@@ -2636,7 +2869,11 @@ export class ChatCommand {
2636
2869
  }
2637
2870
  };
2638
2871
  const executionPrompt = this.buildExecutionPrompt(contextualPrompt);
2639
- const agentTaskType = this.inferAgentTaskType(prompt);
2872
+ const agentTaskType = this.lastAgentRoute?.taskKind || this.inferAgentTaskType(prompt);
2873
+ const workflowType = resolveWorkflowType(agentTaskType, prompt);
2874
+ const executionHints = buildExecutionHints(agentTaskType, prompt, {
2875
+ fastPath: this.lastAgentRoute?.fastPath,
2876
+ });
2640
2877
  const workspaceContext = {
2641
2878
  workspacePath: workspacePath,
2642
2879
  projectPath: workspacePath,
@@ -2672,10 +2909,14 @@ export class ChatCommand {
2672
2909
  workspace: { path: workspacePath },
2673
2910
  ...workspaceContext,
2674
2911
  agentTaskType,
2912
+ workflowType,
2913
+ executionHints,
2914
+ fastPath: this.lastAgentRoute?.fastPath,
2915
+ agentRoute: this.lastAgentRoute,
2675
2916
  executionSurface: 'cli',
2676
2917
  clientSurface: 'cli',
2677
2918
  localMachineCapable: true,
2678
- agentTimeoutMs: DEFAULT_V3_AGENT_TIMEOUT_MS,
2919
+ agentTimeoutMs: resolvePlannerAgentTimeoutMs(DEFAULT_V3_AGENT_TIMEOUT_MS, agentTaskType, prompt),
2679
2920
  agentIdleTimeoutMs: DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS,
2680
2921
  agentSoftTimeoutMs: DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS,
2681
2922
  model: routingPolicy.selectedModel,
@@ -2768,7 +3009,7 @@ export class ChatCommand {
2768
3009
  const previewGate = (response.metadata?.previewGate || null);
2769
3010
  const workspaceHasOutput = this.api.hasAgentWorkspaceOutput(workspaceContext);
2770
3011
  const changedFileCount = response.changedFiles ? Object.keys(response.changedFiles).length : 0;
2771
- const requiresWorkspaceChanges = this.taskRequiresWorkspaceChanges(prompt);
3012
+ const requiresWorkspaceChanges = promptRequiresWorkspaceChanges(prompt);
2772
3013
  const answerContent = normalizeAgentAnswerContent(response.content, this.v3StreamedAnswerBuffer);
2773
3014
  liveOutcome.changedFileCount = changedFileCount;
2774
3015
  liveOutcome.requiresWorkspaceChanges = requiresWorkspaceChanges;
@@ -2847,7 +3088,11 @@ export class ChatCommand {
2847
3088
  if (isToolEvidenceStubAnswer(finalAnswer)) {
2848
3089
  finalAnswer = '';
2849
3090
  }
2850
- const needsAnalysisRescue = !requiresWorkspaceChanges && !this.jsonOutput
3091
+ const needsAnalysisRescue = !shouldSkipAnalysisRescue({
3092
+ tasksTotal: liveOutcome.tasksTotal,
3093
+ changedFileCount,
3094
+ requiresWorkspaceChanges,
3095
+ }) && !this.jsonOutput
2851
3096
  && (!executorSucceeded || !isSubstantiveAgentAnswer(finalAnswer));
2852
3097
  if (needsAnalysisRescue) {
2853
3098
  const rescued = await this.tryAnalysisLocalRescue(prompt, workspacePath);
@@ -3119,13 +3364,16 @@ export class ChatCommand {
3119
3364
  this.v3LastActivity = Date.now();
3120
3365
  this.v3StreamingStarted = false;
3121
3366
  try {
3367
+ const retryTaskType = this.inferAgentTaskType(rawPrompt);
3122
3368
  const retryResponse = await this.api.runV3AgentWorkflow(executionPrompt, {
3123
3369
  workspace: { path: this.currentProjectPath },
3124
3370
  ...workspaceContext,
3125
3371
  executionSurface: 'cli',
3126
3372
  clientSurface: 'cli',
3127
3373
  localMachineCapable: true,
3128
- agentTimeoutMs: DEFAULT_V3_AGENT_TIMEOUT_MS,
3374
+ agentTaskType: retryTaskType,
3375
+ workflowType: resolveWorkflowType(retryTaskType, rawPrompt),
3376
+ agentTimeoutMs: resolvePlannerAgentTimeoutMs(DEFAULT_V3_AGENT_TIMEOUT_MS, retryTaskType, rawPrompt),
3129
3377
  agentIdleTimeoutMs: DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS,
3130
3378
  agentSoftTimeoutMs: DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS,
3131
3379
  model: routingPolicy.selectedModel,
@@ -3397,6 +3645,15 @@ export class ChatCommand {
3397
3645
  */
3398
3646
  printAgentRunSummary(outcome, evaluation, changedFileCount) {
3399
3647
  const executorSucceeded = evaluation.executorSucceeded;
3648
+ if (outcome.answerContent && isSubstantiveAgentAnswer(outcome.answerContent)) {
3649
+ const reportSummary = summarizeMarkdownReport(outcome.answerContent);
3650
+ emitDeckEvent({
3651
+ type: 'run_summary',
3652
+ success: executorSucceeded,
3653
+ title: reportSummary?.title || 'Agent run complete',
3654
+ sections: reportSummary?.sections || [],
3655
+ });
3656
+ }
3400
3657
  const bar = chalk.gray('─'.repeat(63));
3401
3658
  const ok = chalk.green('✓');
3402
3659
  const warn = chalk.yellow('⚠');
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Unified agent routing: regex (instant) → optional Balanced 4B → V3 execution hints.
3
+ */
4
+ import type { APIClient } from './api.js';
5
+ import { type AgentRoutePath } from './fastAgentRouter.js';
6
+ export type { AgentRoutePath };
7
+ export interface AgentRouteDecision {
8
+ path: AgentRoutePath;
9
+ taskKind: string;
10
+ fastPath?: string;
11
+ confidence: number;
12
+ reason: string;
13
+ source: 'regex-trivial' | 'regex' | 'balanced-4b' | 'merged';
14
+ routerLatencyMs?: number;
15
+ routerModel?: string;
16
+ }
17
+ export declare function resolveAgentRoute(api: APIClient, prompt: string): Promise<AgentRouteDecision>;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Unified agent routing: regex (instant) → optional Balanced 4B → V3 execution hints.
3
+ */
4
+ import { inferAgentTaskType, isTrivialHtmlPageRequest, isContinueOrRetryPrompt, hasGameIntent, hasWebPageIntent, taskRequiresWorkspaceChanges, stripExecutionShaping, } from './requestIntent.js';
5
+ import { isFastInferenceRouterAvailable, routeWithBalanced4b, shouldUseBalancedRouter, } from './fastAgentRouter.js';
6
+ function normalizeTaskKind(value, fallback) {
7
+ const kind = String(value || fallback).trim().toLowerCase();
8
+ const allowed = new Set([
9
+ 'web-build', 'game-build', 'implementation', 'analysis', 'debugging', 'verification', 'build', 'repair',
10
+ ]);
11
+ if (allowed.has(kind)) {
12
+ return kind === 'build' ? 'implementation' : kind;
13
+ }
14
+ return fallback;
15
+ }
16
+ function mergeLlmRoute(regexTask, prompt, parsed) {
17
+ const llmPath = String(parsed.path || '').toLowerCase();
18
+ const llmTask = normalizeTaskKind(parsed.task_kind || parsed.taskKind, regexTask);
19
+ const llmFast = parsed.fast_path ?? parsed.fastPath ?? undefined;
20
+ const confidence = Number(parsed.confidence) || 0.75;
21
+ const reason = String(parsed.reason || 'balanced-4b router');
22
+ // Regex game/web signals override wrong LLM labels (hello-world → v3-agent bug).
23
+ if (isTrivialHtmlPageRequest(prompt) || (hasWebPageIntent(prompt) && !hasGameIntent(prompt) && llmTask === 'game-build')) {
24
+ return {
25
+ path: 'template-instant',
26
+ taskKind: 'web-build',
27
+ fastPath: 'template-instant',
28
+ confidence: Math.max(confidence, 0.92),
29
+ reason: 'regex web-page override',
30
+ source: 'merged',
31
+ };
32
+ }
33
+ if (llmPath === 'template-instant' || llmPath === 'html-template' || llmFast === 'template-instant') {
34
+ if (hasWebPageIntent(prompt) || isTrivialHtmlPageRequest(prompt)) {
35
+ return {
36
+ path: 'template-instant',
37
+ taskKind: 'web-build',
38
+ fastPath: 'template-instant',
39
+ confidence,
40
+ reason,
41
+ source: 'merged',
42
+ };
43
+ }
44
+ }
45
+ if (llmPath === 'analysis-only' || llmTask === 'analysis' || llmTask === 'debugging') {
46
+ if (!taskRequiresWorkspaceChanges(prompt)) {
47
+ return {
48
+ path: 'analysis-only',
49
+ taskKind: llmTask === 'debugging' ? 'debugging' : 'analysis',
50
+ confidence,
51
+ reason,
52
+ source: 'balanced-4b',
53
+ };
54
+ }
55
+ }
56
+ if (hasGameIntent(prompt) || llmTask === 'game-build') {
57
+ return {
58
+ path: 'v3-agent',
59
+ taskKind: 'game-build',
60
+ confidence,
61
+ reason,
62
+ source: 'merged',
63
+ };
64
+ }
65
+ const taskKind = (regexTask === 'web-build' || regexTask === 'game-build') ? regexTask : (llmTask || regexTask);
66
+ return {
67
+ path: 'v3-agent',
68
+ taskKind,
69
+ fastPath: typeof llmFast === 'string' ? llmFast : undefined,
70
+ confidence,
71
+ reason,
72
+ source: 'balanced-4b',
73
+ };
74
+ }
75
+ export async function resolveAgentRoute(api, prompt) {
76
+ const stripped = stripExecutionShaping(prompt);
77
+ if (isContinueOrRetryPrompt(stripped)) {
78
+ const regexTask = inferAgentTaskType(stripped);
79
+ return {
80
+ path: 'v3-agent',
81
+ taskKind: regexTask === 'analysis' ? 'implementation' : regexTask,
82
+ confidence: 0.9,
83
+ reason: 'continue/retry follow-up (regex)',
84
+ source: 'regex',
85
+ };
86
+ }
87
+ if (isTrivialHtmlPageRequest(stripped)) {
88
+ return {
89
+ path: 'template-instant',
90
+ taskKind: 'web-build',
91
+ fastPath: 'template-instant',
92
+ confidence: 1,
93
+ reason: 'trivial html page (regex)',
94
+ source: 'regex-trivial',
95
+ };
96
+ }
97
+ const regexTask = inferAgentTaskType(stripped);
98
+ if (!taskRequiresWorkspaceChanges(stripped) && (regexTask === 'analysis' || regexTask === 'debugging')) {
99
+ return {
100
+ path: 'analysis-only',
101
+ taskKind: regexTask,
102
+ confidence: 0.95,
103
+ reason: 'read-only analysis (regex)',
104
+ source: 'regex',
105
+ };
106
+ }
107
+ if (hasGameIntent(stripped) && regexTask === 'game-build') {
108
+ return {
109
+ path: 'v3-agent',
110
+ taskKind: 'game-build',
111
+ confidence: 0.95,
112
+ reason: 'game build (regex)',
113
+ source: 'regex',
114
+ };
115
+ }
116
+ const selfHosted = api.getSelfHostedModelsApiUrl();
117
+ const tryLlm = shouldUseBalancedRouter(stripped, regexTask)
118
+ && await isFastInferenceRouterAvailable(selfHosted);
119
+ if (tryLlm) {
120
+ const llm = await routeWithBalanced4b(stripped, selfHosted);
121
+ if (llm.ok && llm.parsed) {
122
+ const merged = mergeLlmRoute(regexTask, stripped, llm.parsed);
123
+ return {
124
+ ...merged,
125
+ routerLatencyMs: llm.latencyMs,
126
+ routerModel: llm.model,
127
+ };
128
+ }
129
+ }
130
+ return {
131
+ path: 'v3-agent',
132
+ taskKind: regexTask,
133
+ confidence: 0.8,
134
+ reason: 'default v3 agent (regex)',
135
+ source: 'regex',
136
+ };
137
+ }