@compilr-dev/cli 0.7.4 → 0.7.6

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 (43) hide show
  1. package/dist/agent.d.ts +7 -2
  2. package/dist/agent.js +55 -39
  3. package/dist/commands-v2/handlers/project.js +65 -2
  4. package/dist/commands-v2/handlers/settings.js +18 -26
  5. package/dist/compilr-diff-companion.vsix +0 -0
  6. package/dist/db/schema.d.ts +1 -1
  7. package/dist/episodes/index.d.ts +13 -13
  8. package/dist/episodes/index.js +13 -14
  9. package/dist/guide/cli-guide-entries.js +54 -2
  10. package/dist/handlers/delegation-handlers.js +228 -50
  11. package/dist/handlers/interactive-flow-handlers.d.ts +26 -0
  12. package/dist/handlers/interactive-flow-handlers.js +61 -0
  13. package/dist/handlers/propose-alternatives-handlers.d.ts +36 -0
  14. package/dist/handlers/propose-alternatives-handlers.js +65 -0
  15. package/dist/index.js +13 -2
  16. package/dist/repl-v2.d.ts +66 -0
  17. package/dist/repl-v2.js +382 -53
  18. package/dist/shared-handlers.d.ts +57 -5
  19. package/dist/shared-handlers.js +50 -0
  20. package/dist/tools/consult.d.ts +14 -0
  21. package/dist/tools/consult.js +73 -0
  22. package/dist/tools/delegate.d.ts +12 -6
  23. package/dist/tools/delegate.js +35 -19
  24. package/dist/tools/interactive-flow.d.ts +13 -0
  25. package/dist/tools/interactive-flow.js +19 -0
  26. package/dist/tools/platform-adapter.d.ts +14 -2
  27. package/dist/tools/platform-adapter.js +16 -4
  28. package/dist/tools/propose-alternatives.d.ts +13 -0
  29. package/dist/tools/propose-alternatives.js +19 -0
  30. package/dist/tools.d.ts +22 -98
  31. package/dist/tools.js +27 -382
  32. package/dist/ui/markdown-renderer.js +26 -7
  33. package/dist/ui/overlay/data/tutorials/projects/new-project.js +56 -20
  34. package/dist/ui/overlay/impl/interactive-flow-overlay-v2.d.ts +79 -0
  35. package/dist/ui/overlay/impl/interactive-flow-overlay-v2.js +580 -0
  36. package/dist/ui/overlay/impl/new-overlay-v2.d.ts +32 -0
  37. package/dist/ui/overlay/impl/new-overlay-v2.js +305 -66
  38. package/dist/ui/overlay/impl/propose-alternatives-overlay-v2.d.ts +53 -0
  39. package/dist/ui/overlay/impl/propose-alternatives-overlay-v2.js +326 -0
  40. package/dist/ui/overlay/index.d.ts +2 -0
  41. package/dist/ui/overlay/index.js +2 -0
  42. package/dist/ui/tool-formatters.js +61 -3
  43. package/package.json +2 -2
package/dist/repl-v2.js CHANGED
@@ -20,7 +20,7 @@ import { initSessionRegistry, getSessionRegistry, setActiveTerminalSessionId, ge
20
20
  import { initFileLockManager } from './multi-agent/file-locks.js';
21
21
  import { initNotificationManager, getNotificationManager } from './multi-agent/notification-manager.js';
22
22
  import { getSessionsPath } from './settings/paths.js';
23
- import { setMetaToolFilter, isMetaToolSilent, todoStore } from './tools.js';
23
+ import { todoStore } from './tools.js';
24
24
  import { getCurrentProject } from './tools/project-db.js';
25
25
  import { TerminalUI } from './ui/terminal-ui.js';
26
26
  import * as terminal from './ui/terminal.js';
@@ -31,7 +31,8 @@ import { renderMascotWithLogo } from './ui/mascot/renderer.js';
31
31
  import { getStartupHighlights } from './changelog/index.js';
32
32
  import { registerCommands, executeCommand, allCommands, getAutocompleteCommands, setSkillCommandLoader, saveCurrentSession, saveCurrentTeam, loadProjectSession, archiveCurrentSession, convertMessagesToItems, } from './commands-v2/index.js';
33
33
  import { getCustomCommandRegistry } from './commands/custom-registry.js';
34
- import { getPlanModePrompt } from '@compilr-dev/sdk';
34
+ import { getPlanModePrompt, createHandoffTool, createConsultTool, createDelegateTool, createDelegateBackgroundTool, } from '@compilr-dev/sdk';
35
+ import { getHandoffHandler, getConsultHandler, getDelegateHandler, getDelegateBackgroundHandler, } from './shared-handlers.js';
35
36
  import { planRepository } from './db/repositories/index.js';
36
37
  import { PlanApprovalOverlayV2, } from './ui/overlay/impl/plan-approval-overlay-v2.js';
37
38
  import { PermissionOverlayV2, } from './ui/overlay/impl/permission-overlay-v2.js';
@@ -40,6 +41,8 @@ import { truncate } from './ui/base/index.js';
40
41
  import { AskUserSimpleOverlayV2, } from './ui/overlay/impl/ask-user-simple-overlay-v2.js';
41
42
  import { GuardrailOverlayV2, } from './ui/overlay/impl/guardrail-overlay-v2.js';
42
43
  import { AskUserOverlayV2, } from './ui/overlay/impl/ask-user-overlay-v2.js';
44
+ import { ProposeAlternativesOverlayV2, } from './ui/overlay/impl/propose-alternatives-overlay-v2.js';
45
+ import { InteractiveFlowOverlayV2, } from './ui/overlay/impl/interactive-flow-overlay-v2.js';
43
46
  import { IterationLimitOverlayV2, } from './ui/overlay/impl/iteration-limit-overlay-v2.js';
44
47
  import { OnboardingWizardOverlayV2, } from './ui/overlay/impl/onboarding-wizard-overlay-v2.js';
45
48
  import { LoginOverlayV2, } from './ui/overlay/impl/login-overlay-v2.js';
@@ -68,9 +71,10 @@ const MAX_QUEUE_SIZE = 10;
68
71
  const DEBUG_LOG_FILE = '/tmp/compilr-agent-debug.log';
69
72
  const DEBUG_ENABLED = process.env.DEBUG_AGENT_REQUESTS === '1';
70
73
  let debugRequestCount = 0;
71
- // Import tool registry functions for debug logging
72
- import { getDirectTools, getMetaTools, getToolStats } from './tools.js';
73
- import { getRegisteredMetaTools } from './tools/meta-tools.js';
74
+ // Debug logging fetches tool definitions live from the agent (the SDK's
75
+ // internal meta-registry is fed via the agent's capability config and
76
+ // isn't separately accessible from the CLI; the CLI's old parallel
77
+ // `MetaToolsRegistry` was removed in audit TODO-15).
74
78
  /**
75
79
  * Write debug info to log file before agent request.
76
80
  * Enable with: DEBUG_AGENT_REQUESTS=1 compilr
@@ -87,26 +91,18 @@ function debugLogAgentRequest(agent, message) {
87
91
  const systemPromptTokens = estimateTokens(state.systemPrompt);
88
92
  const messagesJson = JSON.stringify(state.messages);
89
93
  const messagesTokens = estimateTokens(messagesJson);
90
- // Get tool definitions for logging
91
- const directTools = getDirectTools();
92
- const metaTools = getMetaTools();
93
- const metaRegistryTools = getRegisteredMetaTools();
94
- const toolStats = getToolStats();
95
- // Calculate tool definition sizes
96
- const directToolsJson = JSON.stringify(directTools.map(t => ({
97
- name: t.definition.name,
98
- description: t.definition.description,
99
- inputSchema: t.definition.inputSchema,
100
- })));
101
- const metaToolsJson = JSON.stringify(metaTools.map(t => ({
102
- name: t.definition.name,
103
- description: t.definition.description,
104
- inputSchema: t.definition.inputSchema,
94
+ // Tool definitions come live from the agent. The SDK's internal
95
+ // meta-registry isn't separately accessible — its tools are routed
96
+ // transparently via use_tool() and `get_tool_info` already lives in
97
+ // the agent's direct list (so it's counted here).
98
+ const directTools = agent.getToolDefinitions();
99
+ const directToolsJson = JSON.stringify(directTools.map(d => ({
100
+ name: d.name,
101
+ description: d.description,
102
+ inputSchema: d.inputSchema,
105
103
  })));
106
104
  const directToolsTokens = estimateTokens(directToolsJson);
107
- const metaToolsTokens = estimateTokens(metaToolsJson);
108
- const totalToolTokens = directToolsTokens + metaToolsTokens;
109
- const totalEstimate = systemPromptTokens + messagesTokens + totalToolTokens;
105
+ const totalEstimate = systemPromptTokens + messagesTokens + directToolsTokens;
110
106
  const logEntry = `
111
107
  ################################################################################
112
108
  # REQUEST #${String(debugRequestCount)} @ ${timestamp}
@@ -120,13 +116,9 @@ CONTEXT SIZES:
120
116
  - Messages: ${messagesJson.length.toLocaleString()} chars (~${messagesTokens.toLocaleString()} tokens)
121
117
  - Message Count: ${String(state.messages.length)}
122
118
  - Direct Tools: ${directToolsJson.length.toLocaleString()} chars (~${directToolsTokens.toLocaleString()} tokens) [${String(directTools.length)} tools]
123
- - Meta Tools: ${metaToolsJson.length.toLocaleString()} chars (~${metaToolsTokens.toLocaleString()} tokens) [${String(metaTools.length)} tools]
124
- - Meta Registry: ${String(metaRegistryTools.length)} tools (accessed via use_tool)
125
119
  --------------------------------------------------------------------------------
126
120
  ESTIMATED TOTAL: ~${totalEstimate.toLocaleString()} tokens
127
- (prompt: ${systemPromptTokens.toLocaleString()} + messages: ${messagesTokens.toLocaleString()} + tools: ${totalToolTokens.toLocaleString()})
128
-
129
- Tool Stats: Direct=${String(toolStats.directTools)}, MetaRegistry=${String(toolStats.metaRegistryTools)}, Savings=~${String(toolStats.tokenSavings)}
121
+ (prompt: ${systemPromptTokens.toLocaleString()} + messages: ${messagesTokens.toLocaleString()} + tools: ${directToolsTokens.toLocaleString()})
130
122
 
131
123
  ================================================================================
132
124
  FULL SYSTEM PROMPT (${systemPromptChars.toLocaleString()} chars):
@@ -136,17 +128,7 @@ ${state.systemPrompt}
136
128
  ================================================================================
137
129
  DIRECT TOOLS (${String(directTools.length)} tools, ~${directToolsTokens.toLocaleString()} tokens):
138
130
  ================================================================================
139
- ${directTools.map(t => `- ${t.definition.name}: ${t.definition.description.slice(0, 100)}...`).join('\n')}
140
-
141
- ================================================================================
142
- META TOOLS (${String(metaTools.length)} tools, ~${metaToolsTokens.toLocaleString()} tokens):
143
- ================================================================================
144
- ${metaTools.map(t => `- ${t.definition.name}: ${t.definition.description.slice(0, 100)}...`).join('\n')}
145
-
146
- ================================================================================
147
- META REGISTRY TOOLS (${String(metaRegistryTools.length)} tools, accessed via use_tool):
148
- ================================================================================
149
- ${metaRegistryTools.map(t => `- ${t.definition.name}`).join('\n')}
131
+ ${directTools.map(d => `- ${d.name}: ${d.description.slice(0, 100)}...`).join('\n')}
150
132
 
151
133
  ================================================================================
152
134
  MESSAGES HISTORY (${String(state.messages.length)} messages):
@@ -156,7 +138,7 @@ ${JSON.stringify(state.messages, null, 2)}
156
138
  `;
157
139
  appendFileSync(DEBUG_LOG_FILE, logEntry);
158
140
  log.debug({ component: 'repl', requestNum: debugRequestCount, file: DEBUG_LOG_FILE }, 'Debug request logged');
159
- log.debug({ component: 'repl', systemPromptTokens, messagesTokens, totalToolTokens, totalEstimate }, 'Token estimate');
141
+ log.debug({ component: 'repl', systemPromptTokens, messagesTokens, directToolsTokens, totalEstimate }, 'Token estimate');
160
142
  }
161
143
  catch (error) {
162
144
  log.error({ component: 'repl', err: error }, 'Failed to log agent request');
@@ -187,6 +169,8 @@ export class ReplV2 {
187
169
  onAskUserSimpleReady;
188
170
  onGuardrailReady;
189
171
  onAskUserReady;
172
+ onProposeAlternativesReady;
173
+ onInteractiveFlowReady;
190
174
  onIterationLimitReady;
191
175
  onAgentFinish;
192
176
  onSuggestionReady;
@@ -222,6 +206,11 @@ export class ReplV2 {
222
206
  // Bash abort controllers for Ctrl+B backgrounding feature
223
207
  // Maps toolUseId -> AbortController for each running bash command
224
208
  bashAbortControllers = new Map();
209
+ // Dedupe set for specialist multi-agent tool registration (handoff/consult).
210
+ // Keyed by agentId — once we've registered these tools on a specialist
211
+ // agent's underlying Agent instance we don't do it again on subsequent
212
+ // switches. Mirrors Desktop's `delegateToolsRegistered` set.
213
+ specialistToolsRegistered = new Set();
225
214
  constructor(options = {}) {
226
215
  this.agent = options.agent;
227
216
  this.team = options.team;
@@ -236,6 +225,8 @@ export class ReplV2 {
236
225
  this.onAskUserSimpleReady = options.onAskUserSimpleReady;
237
226
  this.onGuardrailReady = options.onGuardrailReady;
238
227
  this.onAskUserReady = options.onAskUserReady;
228
+ this.onProposeAlternativesReady = options.onProposeAlternativesReady;
229
+ this.onInteractiveFlowReady = options.onInteractiveFlowReady;
239
230
  this.onIterationLimitReady = options.onIterationLimitReady;
240
231
  this.onAgentFinish = options.onAgentFinish;
241
232
  this.onSuggestionReady = options.onSuggestionReady;
@@ -485,8 +476,16 @@ export class ReplV2 {
485
476
  if (teamAgent.agent) {
486
477
  this.agent = teamAgent.agent;
487
478
  }
488
- // 4. Update meta-tool filter
489
- setMetaToolFilter(teamAgent.toolFilter ?? null);
479
+ // 3b. Register multi-agent tools (consult always, handoff for
480
+ // specialists) using SDK factories. Coordinator path also fires here
481
+ // when a user explicitly switches to $default; the startup path
482
+ // covers it via restoreTeamForPrefix.
483
+ this.registerMultiAgentTools(teamAgent);
484
+ // Tool filtering for the new agent is already in effect — each team
485
+ // agent gets its own capability profile baked in at factory time via
486
+ // `agent.ts:467` (`capabilities.profile`). No runtime filter change
487
+ // needed here (audit TODO-15 removed the dead `setMetaToolFilter`
488
+ // call that used to live at this spot).
490
489
  // 5. Update footer UI
491
490
  if (agentId !== 'default') {
492
491
  this.ui.setActiveTeamAgent({
@@ -550,6 +549,171 @@ export class ReplV2 {
550
549
  demotedAgentId,
551
550
  };
552
551
  }
552
+ /**
553
+ * Register multi-agent tools (consult always, handoff for specialists)
554
+ * on the agent's underlying Agent using SDK factories.
555
+ *
556
+ * Tool grants:
557
+ * - **consult**: everyone (coordinator AND specialists). Lightweight
558
+ * "ask another agent a focused question" tool. Originally specced as
559
+ * specialist-only, but empirical evidence (session 220) showed the
560
+ * coordinator's LLM grabs the wrong tool (e.g. `suggest`) when the
561
+ * user says "consult $pm" because no tool by that name was in its
562
+ * registry. delegate is too heavyweight for a quick question.
563
+ * - **handoff**: specialists only. Coordinators delegate, they don't
564
+ * pass the baton. $default → specialist already has delegate.
565
+ * - **delegate / delegate_background**: coordinator only, via the
566
+ * existing CLI tool-array path. Not touched here.
567
+ *
568
+ * Why lazy: the SDK factories need `team` + `currentAgentId` at
569
+ * construction time. The CLI's tool array is built at module load
570
+ * when neither is known.
571
+ *
572
+ * Dedupe via `specialistToolsRegistered` set — tools are registered
573
+ * once per agent for the lifetime of the REPL. Mirrors Desktop's
574
+ * pattern in `agent-manager.ts:registerDelegateToolsIfNeeded`.
575
+ *
576
+ * Called from:
577
+ * - `switchAgent` (every agent switch — covers specialists).
578
+ * - `restoreTeamForPrefix` (team restoration — covers $default at
579
+ * startup, since the coordinator is never switchTo'd from outside).
580
+ */
581
+ /**
582
+ * Called whenever `this.team` is reassigned (project switch, team
583
+ * restoration). The SDK's createConsultTool factory captures the `team`
584
+ * argument in its closure at construction time, so a tool registered
585
+ * against the old team has a stale roster view. We must:
586
+ * 1. Unregister the stale consult/handoff tools from $default's
587
+ * registry (Agent.registerTools throws on duplicate-name, it
588
+ * does not replace).
589
+ * 2. Re-register fresh tools bound to the new team.
590
+ *
591
+ * $default's underlying Agent is preserved across team swaps (the new
592
+ * team is set as default via setDefaultAgent), so its tool registry
593
+ * accumulates stale tools. Specialists' Agents are fresh after a swap,
594
+ * so they don't have stale tools to clean up — clearing the dedupe set
595
+ * is enough; they'll re-register on demand via switchAgent.
596
+ *
597
+ * Without this, `consult($pm)` from $default reports "no specialists
598
+ * available" because the closure references the fresh-startup team
599
+ * (which only had $default) instead of the restored/built team.
600
+ */
601
+ rebindMultiAgentToolsAfterTeamSwap() {
602
+ this.specialistToolsRegistered.clear();
603
+ const defaultTeamAgent = this.team?.get('default');
604
+ if (defaultTeamAgent?.agent) {
605
+ // Cast to access the unregister method — it's on
606
+ // DefaultToolRegistry but not on the public ToolRegistry interface.
607
+ const registry = defaultTeamAgent.agent.getToolRegistry();
608
+ // Unregister every tool registerMultiAgentTools may have added to
609
+ // the coordinator on a previous call. Re-registration would fail
610
+ // with "Tool already registered" otherwise.
611
+ registry.unregister?.('consult');
612
+ registry.unregister?.('handoff');
613
+ registry.unregister?.('delegate');
614
+ registry.unregister?.('delegate_background');
615
+ this.registerMultiAgentTools(defaultTeamAgent);
616
+ }
617
+ }
618
+ registerMultiAgentTools(teamAgent) {
619
+ if (!teamAgent.agent)
620
+ return;
621
+ if (!this.team)
622
+ return;
623
+ if (this.specialistToolsRegistered.has(teamAgent.id))
624
+ return;
625
+ const team = this.team;
626
+ const currentAgentId = teamAgent.id;
627
+ const tools = [];
628
+ // Consult — available to everyone.
629
+ tools.push(createConsultTool({
630
+ team,
631
+ currentAgentId,
632
+ onConsult: async (targetId, question, context) => {
633
+ const handler = getConsultHandler();
634
+ if (!handler) {
635
+ return {
636
+ answer: '',
637
+ error: 'Consult handler not initialised. This shouldn\'t happen in normal use.',
638
+ };
639
+ }
640
+ const result = await handler({ agentId: targetId, question, context });
641
+ return { answer: result.answer, error: result.error };
642
+ },
643
+ }));
644
+ // Handoff — specialists only (per multi-agent-tools-taxonomy-spec.md §2:
645
+ // handoff is the specialist's vocabulary for passing the baton).
646
+ if (teamAgent.id !== 'default') {
647
+ tools.push(createHandoffTool({
648
+ team,
649
+ currentAgentId,
650
+ onHandoff: async (targetId, task, reason) => {
651
+ const handler = getHandoffHandler();
652
+ if (!handler) {
653
+ return {
654
+ success: false,
655
+ error: 'Handoff handler not initialised. This shouldn\'t happen in normal use.',
656
+ };
657
+ }
658
+ const result = await handler({ agentId: targetId, task, reason });
659
+ return {
660
+ success: result.success,
661
+ declined: result.declined,
662
+ error: result.error,
663
+ };
664
+ },
665
+ }));
666
+ }
667
+ // Delegate + delegate_background — coordinator only (per taxonomy spec
668
+ // §2: coordinators push work down, specialists pass the baton). Built
669
+ // via the SDK factories (same pattern as consult/handoff above) and
670
+ // bridged to the existing shared-handlers registry where the actual
671
+ // approval-overlay + specialist-run code lives.
672
+ if (teamAgent.id === 'default') {
673
+ tools.push(createDelegateTool({
674
+ team,
675
+ currentAgentId,
676
+ onDelegate: async (targetId, task, reason) => {
677
+ const handler = getDelegateHandler();
678
+ if (!handler) {
679
+ return {
680
+ success: false,
681
+ error: "Delegate handler not initialised. This shouldn't happen in normal use.",
682
+ };
683
+ }
684
+ const result = await handler({ agentId: targetId, task, reason });
685
+ return {
686
+ success: result.success,
687
+ declined: result.declined,
688
+ response: result.response,
689
+ error: result.error,
690
+ };
691
+ },
692
+ }));
693
+ tools.push(createDelegateBackgroundTool({
694
+ team,
695
+ currentAgentId,
696
+ onDelegateBackground: async (args) => {
697
+ const handler = getDelegateBackgroundHandler();
698
+ if (!handler) {
699
+ return {
700
+ delegationId: '',
701
+ status: 'running',
702
+ error: "delegate_background handler not initialised. This shouldn't happen in normal use.",
703
+ };
704
+ }
705
+ return await handler({
706
+ agentId: args.targetAgentId,
707
+ task: args.task,
708
+ todoIndex: args.todoIndex,
709
+ expectedOutput: args.expectedOutput,
710
+ });
711
+ },
712
+ }));
713
+ }
714
+ teamAgent.agent.registerTools(tools);
715
+ this.specialistToolsRegistered.add(teamAgent.id);
716
+ }
553
717
  /**
554
718
  * Bring a background agent to foreground.
555
719
  * Validates the agent has a background session, then delegates to switchAgent().
@@ -798,6 +962,16 @@ export class ReplV2 {
798
962
  displayName: a.displayName,
799
963
  mascot: a.mascot,
800
964
  })));
965
+ // Register multi-agent tools on $default so it has consult available
966
+ // from the first message — covers brand-new projects where the team
967
+ // has only $default and restoreTeamForPrefix early-returns before
968
+ // hitting its registration block (size <= 1 check).
969
+ // Dedupe via specialistToolsRegistered set, so this is safe to call
970
+ // alongside the restoreTeamForPrefix path.
971
+ const defaultTeamAgent = this.team.get('default');
972
+ if (defaultTeamAgent) {
973
+ this.registerMultiAgentTools(defaultTeamAgent);
974
+ }
801
975
  }
802
976
  }
803
977
  catch {
@@ -885,6 +1059,37 @@ export class ReplV2 {
885
1059
  return result ?? { answers: {}, skipped: options.questions.map((q) => q.id) };
886
1060
  });
887
1061
  }
1062
+ // Provide the propose_alternatives overlay function to the caller
1063
+ if (this.onProposeAlternativesReady) {
1064
+ const ui = this.ui;
1065
+ this.onProposeAlternativesReady(async (options) => {
1066
+ const overlay = new ProposeAlternativesOverlayV2(options);
1067
+ const result = await ui.showOverlay(overlay);
1068
+ // No-overlay-result (Esc to dismiss overlay outside our key handler)
1069
+ // → treat as rejected so the agent gets a defined shape.
1070
+ return (result ?? {
1071
+ chosenIndex: 0,
1072
+ chosenTitle: options.alternatives[0]?.title ?? '',
1073
+ rejected: true,
1074
+ });
1075
+ });
1076
+ }
1077
+ // Provide the build_interactive_flow overlay function to the caller
1078
+ if (this.onInteractiveFlowReady) {
1079
+ const ui = this.ui;
1080
+ this.onInteractiveFlowReady(async (options) => {
1081
+ const overlay = new InteractiveFlowOverlayV2(options);
1082
+ const result = await ui.showOverlay(overlay);
1083
+ return (result ?? {
1084
+ completed: false,
1085
+ path: [],
1086
+ answers: {},
1087
+ answerLabels: {},
1088
+ abortReason: 'unknown',
1089
+ durationMs: 0,
1090
+ });
1091
+ });
1092
+ }
888
1093
  // Provide the iteration_limit overlay function to the caller
889
1094
  if (this.onIterationLimitReady) {
890
1095
  const ui = this.ui;
@@ -1218,6 +1423,26 @@ export class ReplV2 {
1218
1423
  scope: 'session',
1219
1424
  });
1220
1425
  }
1426
+ // Rebind multi-agent tools to the freshly restored team. Without this,
1427
+ // any consult tool previously registered on $default (in start()) is
1428
+ // bound to the pre-restoration team and reports a stale roster.
1429
+ this.rebindMultiAgentToolsAfterTeamSwap();
1430
+ // Update the footer to show the restored active agent's badge.
1431
+ // start() set the footer to the pre-restore default during its sync
1432
+ // path (line ~1437) — handleStartupSequence then ran asynchronously
1433
+ // and got here. If the team was saved with a specialist active, the
1434
+ // footer would otherwise still show "no specialist active" (blank).
1435
+ const restoredActive = persistedTeam.getActive();
1436
+ if (restoredActive.id !== 'default') {
1437
+ this.ui.setActiveTeamAgent({
1438
+ id: restoredActive.id,
1439
+ displayName: restoredActive.displayName,
1440
+ mascot: restoredActive.mascot,
1441
+ });
1442
+ }
1443
+ else {
1444
+ this.ui.setActiveTeamAgent(null);
1445
+ }
1221
1446
  // Sync roster to session registry
1222
1447
  const registry = getSessionRegistry();
1223
1448
  if (registry) {
@@ -1745,6 +1970,9 @@ export class ReplV2 {
1745
1970
  mascot: agent.mascot,
1746
1971
  }));
1747
1972
  });
1973
+ // Rebind multi-agent tools so consult/handoff closures point at
1974
+ // the new team's roster instead of the pre-swap team.
1975
+ this.rebindMultiAgentToolsAfterTeamSwap();
1748
1976
  return oldTeam;
1749
1977
  },
1750
1978
  restoreAgent: (newAgent) => {
@@ -2312,6 +2540,13 @@ export class ReplV2 {
2312
2540
  this.textAccumulator = '';
2313
2541
  let lastToolInput = null;
2314
2542
  let pendingDiffLines;
2543
+ // Track what was actually rendered to the user this turn. Some models
2544
+ // (Gemini Flash Lite, occasionally) end a turn with zero text output,
2545
+ // which looks like a hang/failure on the terminal. We surface a
2546
+ // diagnostic in that case so the user knows the turn completed even
2547
+ // though nothing was said.
2548
+ let hasRenderedAgentText = false;
2549
+ let hasToolCalls = false;
2315
2550
  // Stream agent events (agent is guaranteed to exist in this method)
2316
2551
  const agent = this.agent;
2317
2552
  if (!agent)
@@ -2411,9 +2646,11 @@ export class ReplV2 {
2411
2646
  agentId: activeAgentId,
2412
2647
  });
2413
2648
  this.textAccumulator = '';
2649
+ hasRenderedAgentText = true;
2414
2650
  }
2415
2651
  // Track tool calls for per-turn metrics
2416
2652
  this.ui.incrementToolCalls();
2653
+ hasToolCalls = true;
2417
2654
  // Record tool usage for telemetry
2418
2655
  if (event.name) {
2419
2656
  getAuthManager().recordTool(event.name);
@@ -2455,7 +2692,7 @@ export class ReplV2 {
2455
2692
  this.ui.addBashCommand(event.toolUseId, command);
2456
2693
  }
2457
2694
  // Update spinner with tool name (skip silent tools)
2458
- if (!this.agent?.isToolSilent(event.name) && !isMetaToolSilent(event.name)) {
2695
+ if (!this.agent?.isToolSilent(event.name)) {
2459
2696
  if (event.name === 'task' && event.toolUseId) {
2460
2697
  const subagentType = typeof lastToolInput.subagent_type === 'string'
2461
2698
  ? lastToolInput.subagent_type
@@ -2532,10 +2769,12 @@ export class ReplV2 {
2532
2769
  this.ui.setCurrentTool(null);
2533
2770
  continue;
2534
2771
  }
2535
- // Handle todo_write - update UI state from todoStore
2536
- // (store is already updated by the tool's execute handler, regardless of
2537
- // whether the tool was called directly or via the meta-registry fallback)
2538
- if (toolName === 'todo_write') {
2772
+ // Handle todo_write / todo_claim / todo_handoff all three
2773
+ // mutate the same store, so the UI's todos snapshot needs to
2774
+ // refresh on any of them. Previously only todo_write did this,
2775
+ // so the owner badge (@agent-id) silently never appeared
2776
+ // after a claim/handoff even though the renderer supports it.
2777
+ if (toolName === 'todo_write' || toolName === 'todo_claim' || toolName === 'todo_handoff') {
2539
2778
  // Get todos in creation order (getAll() sorts by status which breaks
2540
2779
  // task numbering — the footer renderer handles status sorting itself)
2541
2780
  const storeTodos = todoStore.getAll()
@@ -2560,9 +2799,10 @@ export class ReplV2 {
2560
2799
  this.ui.setCurrentTool(null);
2561
2800
  continue;
2562
2801
  }
2563
- // Skip silent tools (no output needed)
2564
- // Check both direct registry and meta-registry for the silent flag
2565
- if (this.agent?.isToolSilent(toolName) || isMetaToolSilent(toolName)) {
2802
+ // Skip silent tools (no output needed). The agent's
2803
+ // tool registry is the single source of truth post-TODO-15
2804
+ // (the CLI's parallel MetaToolsRegistry was dead).
2805
+ if (this.agent?.isToolSilent(toolName)) {
2566
2806
  this.ui.setCurrentTool(null);
2567
2807
  continue;
2568
2808
  }
@@ -2611,7 +2851,7 @@ export class ReplV2 {
2611
2851
  this.ui.print({
2612
2852
  type: 'tool-result',
2613
2853
  name: innerToolName,
2614
- params: this.formatToolParams(innerArgs),
2854
+ params: this.formatToolParams(innerArgs, innerToolName),
2615
2855
  summary,
2616
2856
  content: content || undefined,
2617
2857
  diffLines: innerDiffLines,
@@ -2686,7 +2926,7 @@ export class ReplV2 {
2686
2926
  this.ui.print({
2687
2927
  type: 'tool-result',
2688
2928
  name: toolName,
2689
- params: this.formatToolParams(toolInput),
2929
+ params: this.formatToolParams(toolInput, toolName),
2690
2930
  summary,
2691
2931
  content: content || undefined,
2692
2932
  diffLines,
@@ -2763,6 +3003,21 @@ export class ReplV2 {
2763
3003
  // Flush remaining text (PLAN_READY marker detection removed — plan_submit tool handles approval)
2764
3004
  if (this.textAccumulator.trim()) {
2765
3005
  this.ui.print({ type: 'agent-text', text: this.textAccumulator.trim(), expression: this.getActiveAgentMascot(), agentId: activeAgentId });
3006
+ hasRenderedAgentText = true;
3007
+ }
3008
+ // Diagnostic for silent turns — some models occasionally finish a
3009
+ // turn without emitting any user-facing text. Without this, the
3010
+ // terminal just shows the metrics line under the user's input
3011
+ // (e.g. `── 1s · ↓ 20.4k ↑ 15 ──`) and the user is left guessing
3012
+ // whether the agent crashed, is still thinking, or just decided
3013
+ // not to respond. Surface a clear status line instead.
3014
+ if (!hasRenderedAgentText && !signal.aborted) {
3015
+ this.ui.print({
3016
+ type: 'info',
3017
+ message: hasToolCalls
3018
+ ? '(no follow-up text after tool calls — the tool result above is the response)'
3019
+ : '(silent turn — the model produced no output. Re-ask, or try a different model.)',
3020
+ });
2766
3021
  }
2767
3022
  // Print per-turn summary before stopping agent
2768
3023
  this.printTurnSummary();
@@ -3163,7 +3418,7 @@ export class ReplV2 {
3163
3418
  // Silently fail - session/team saving is best-effort
3164
3419
  }
3165
3420
  }
3166
- formatToolParams(input) {
3421
+ formatToolParams(input, toolName) {
3167
3422
  if (!input)
3168
3423
  return '';
3169
3424
  // Format common params
@@ -3175,6 +3430,80 @@ export class ReplV2 {
3175
3430
  return String(input.pattern);
3176
3431
  if ('command' in input)
3177
3432
  return truncate(String(input.command), 50);
3433
+ // ask_user: show the question headers (or first-question-text fallback)
3434
+ // instead of the raw JSON array. Some providers (Gemini Flash via
3435
+ // OpenAI compat, etc.) ship the questions field as a JSON-stringified
3436
+ // string rather than a parsed array, so try parsing if needed.
3437
+ if (toolName === 'ask_user') {
3438
+ const rawQuestions = input.questions;
3439
+ let questions = null;
3440
+ if (Array.isArray(rawQuestions)) {
3441
+ questions = rawQuestions;
3442
+ }
3443
+ else if (typeof rawQuestions === 'string') {
3444
+ try {
3445
+ const parsed = JSON.parse(rawQuestions);
3446
+ if (Array.isArray(parsed)) {
3447
+ questions = parsed;
3448
+ }
3449
+ }
3450
+ catch {
3451
+ /* fall through to default */
3452
+ }
3453
+ }
3454
+ if (questions) {
3455
+ const headers = questions.map((q) => {
3456
+ if (typeof q.header === 'string' && q.header.length > 0)
3457
+ return q.header;
3458
+ if (typeof q.question === 'string')
3459
+ return q.question;
3460
+ return '?';
3461
+ });
3462
+ return truncate(headers.join(', '), 50);
3463
+ }
3464
+ }
3465
+ // build_interactive_flow: show flow.title (same JSON-string fallback)
3466
+ if (toolName === 'build_interactive_flow') {
3467
+ let flow = input.flow;
3468
+ if (typeof flow === 'string') {
3469
+ try {
3470
+ flow = JSON.parse(flow);
3471
+ }
3472
+ catch {
3473
+ /* fall through */
3474
+ }
3475
+ }
3476
+ if (flow && typeof flow === 'object' && 'title' in flow) {
3477
+ const f = flow;
3478
+ if (typeof f.title === 'string') {
3479
+ return truncate(f.title, 50);
3480
+ }
3481
+ }
3482
+ }
3483
+ // propose_alternatives: show alternative titles. Same defensive JSON
3484
+ // parse as ask_user above — some providers JSON-stringify nested arrays.
3485
+ if (toolName === 'propose_alternatives') {
3486
+ const raw = input.alternatives;
3487
+ let alternatives = null;
3488
+ if (Array.isArray(raw)) {
3489
+ alternatives = raw;
3490
+ }
3491
+ else if (typeof raw === 'string') {
3492
+ try {
3493
+ const parsed = JSON.parse(raw);
3494
+ if (Array.isArray(parsed)) {
3495
+ alternatives = parsed;
3496
+ }
3497
+ }
3498
+ catch {
3499
+ /* fall through to default */
3500
+ }
3501
+ }
3502
+ if (alternatives) {
3503
+ const titles = alternatives.map((a) => typeof a.title === 'string' ? a.title : '?');
3504
+ return truncate(titles.join(', '), 50);
3505
+ }
3506
+ }
3178
3507
  // Default: show first param
3179
3508
  const keys = Object.keys(input);
3180
3509
  if (keys.length > 0) {