@compilr-dev/cli 0.7.3 → 0.7.5

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 (46) 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/tutorial-registry.js +2 -0
  34. package/dist/ui/overlay/data/tutorials/projects/interactive-flows.d.ts +9 -0
  35. package/dist/ui/overlay/data/tutorials/projects/interactive-flows.js +94 -0
  36. package/dist/ui/overlay/data/tutorials/projects/new-project.js +56 -20
  37. package/dist/ui/overlay/impl/interactive-flow-overlay-v2.d.ts +79 -0
  38. package/dist/ui/overlay/impl/interactive-flow-overlay-v2.js +580 -0
  39. package/dist/ui/overlay/impl/new-overlay-v2.d.ts +32 -0
  40. package/dist/ui/overlay/impl/new-overlay-v2.js +305 -66
  41. package/dist/ui/overlay/impl/propose-alternatives-overlay-v2.d.ts +53 -0
  42. package/dist/ui/overlay/impl/propose-alternatives-overlay-v2.js +326 -0
  43. package/dist/ui/overlay/index.d.ts +2 -0
  44. package/dist/ui/overlay/index.js +2 -0
  45. package/dist/ui/tool-formatters.js +61 -3
  46. package/package.json +3 -3
@@ -5,15 +5,30 @@
5
5
  * The deps object is SharedState passed by reference, so live mutations
6
6
  * (e.g. team set after REPL starts) work exactly as before.
7
7
  */
8
- import { registerDelegateHandler, registerDelegateBackgroundHandler, registerHandoffHandler, } from '../shared-handlers.js';
8
+ import { registerDelegateHandler, registerDelegateBackgroundHandler, registerHandoffHandler, registerConsultHandler, } from '../shared-handlers.js';
9
9
  import { getDelegationTracker } from '../multi-agent/delegation-tracker.js';
10
+ import { buildHandoffTaskMessage, buildConsultQuestionMessage, } from '@compilr-dev/sdk';
10
11
  import chalk from 'chalk';
11
12
  import { truncate } from '../ui/base/index.js';
12
13
  // ---------------------------------------------------------------------------
13
14
  // Factory
14
15
  // ---------------------------------------------------------------------------
15
16
  export function registerDelegationHandlers(deps) {
16
- // Register delegate handler for multi-agent delegation
17
+ // Register delegate handler for multi-agent delegation.
18
+ //
19
+ // Behavior (per multi-agent-tools-taxonomy-spec.md §2):
20
+ // - Coordinator → specialist, foreground, SYNC response.
21
+ // - Approval overlay shown (preserved UX).
22
+ // - On approval, specialist runs the task INLINE and the response is
23
+ // captured + returned to the coordinator as the tool result. The
24
+ // active speaker does NOT permanently switch — coordinator stays in
25
+ // charge of the conversation.
26
+ // - Specialist's history IS persisted (delegate is the "persistent" sibling
27
+ // of consult — see consult handler below for the transient version).
28
+ //
29
+ // Previously (pre-spec): set `pendingDelegation` so the REPL post-turn loop
30
+ // would switch active agent permanently. That was effectively
31
+ // "handoff with approval" and didn't match the intended semantics.
17
32
  registerDelegateHandler(async (input) => {
18
33
  // Wait for event loop to process any pending tool_end events
19
34
  await new Promise((resolve) => setImmediate(resolve));
@@ -28,8 +43,8 @@ export function registerDelegationHandlers(deps) {
28
43
  };
29
44
  }
30
45
  // Validate target agent exists
31
- const targetAgent = deps.team.get(input.agentId);
32
- if (!targetAgent) {
46
+ const targetTeamAgent = deps.team.get(input.agentId);
47
+ if (!targetTeamAgent) {
33
48
  return {
34
49
  success: false,
35
50
  declined: false,
@@ -44,63 +59,74 @@ export function registerDelegationHandlers(deps) {
44
59
  error: 'Only the coordinator can delegate tasks. Use $<agent> to switch agents manually.',
45
60
  };
46
61
  }
47
- // V2 mode: use TerminalUI overlay for approval
48
- if (deps.showDelegateOverlayV2) {
49
- const result = await deps.showDelegateOverlayV2({
50
- agentId: input.agentId,
51
- agentDisplayName: targetAgent.displayName,
52
- task: input.task,
53
- reason: input.reason,
54
- });
55
- if (!result.approved) {
56
- return {
57
- success: false,
58
- declined: true,
59
- };
60
- }
61
- // Set pending delegation - REPL will handle the switch after agent finishes
62
- deps.pendingDelegation = {
63
- agentId: input.agentId,
64
- task: input.task,
65
- };
62
+ // Ask for user approval — V2 overlay first, ask_user_simple fallback
63
+ const approval = await askForDelegationApproval({
64
+ deps,
65
+ agentId: input.agentId,
66
+ displayName: targetTeamAgent.displayName,
67
+ task: input.task,
68
+ reason: input.reason,
69
+ });
70
+ if (approval.kind === 'no-ui') {
66
71
  return {
67
- success: true,
72
+ success: false,
68
73
  declined: false,
74
+ error: 'Delegation UI not available. Ensure you are running in interactive mode.',
69
75
  };
70
76
  }
71
- // Fallback: no V2 overlay available, use simple ask_user_simple handler
72
- const askHandler = deps.showAskUserSimpleOverlayV2;
73
- if (askHandler) {
74
- const question = input.reason
75
- ? `Delegate to $${input.agentId} (${targetAgent.displayName})?\n${chalk.dim(`Reason: ${input.reason}`)}\n${chalk.dim(`Task: ${truncate(input.task, 100)}`)}`
76
- : `Delegate to $${input.agentId} (${targetAgent.displayName})?\n${chalk.dim(`Task: ${truncate(input.task, 100)}`)}`;
77
- const result = await askHandler({
78
- question,
79
- options: ['Yes', 'No'],
80
- allowCustom: false,
81
- });
82
- if (result.skipped || result.answer === 'No') {
77
+ if (approval.kind === 'declined') {
78
+ return { success: false, declined: true };
79
+ }
80
+ // Approved run the specialist inline. Same pattern as consult below,
81
+ // minus the history snapshot/restore (delegate is persistent — the
82
+ // specialist remembers the work).
83
+ if (!targetTeamAgent.agent) {
84
+ try {
85
+ await deps.team.initializeAll();
86
+ }
87
+ catch (initErr) {
83
88
  return {
84
89
  success: false,
85
- declined: true,
90
+ declined: false,
91
+ error: `Failed to initialize $${input.agentId}: ${initErr instanceof Error ? initErr.message : String(initErr)}`,
86
92
  };
87
93
  }
88
- // Set pending delegation
89
- deps.pendingDelegation = {
90
- agentId: input.agentId,
91
- task: input.task,
94
+ }
95
+ const targetAgent = targetTeamAgent.agent;
96
+ if (!targetAgent) {
97
+ return {
98
+ success: false,
99
+ declined: false,
100
+ error: `Agent '$${input.agentId}' could not be initialized.`,
92
101
  };
102
+ }
103
+ // Build the delegated task message. Mirrors the delegate_background
104
+ // preamble's SCOPE block so the specialist knows it's a delegation.
105
+ const taskMessage = buildDelegateTaskMessage({
106
+ task: input.task,
107
+ reason: input.reason,
108
+ });
109
+ try {
110
+ const runResult = await targetAgent.run(taskMessage);
111
+ // Truncate very long responses to keep the coordinator's context lean.
112
+ const response = runResult.response.length > 2000
113
+ ? runResult.response.slice(0, 2000) + '\n…[truncated to 2000 chars]'
114
+ : runResult.response;
93
115
  return {
94
116
  success: true,
95
117
  declined: false,
118
+ response,
119
+ agentId: input.agentId,
120
+ };
121
+ }
122
+ catch (err) {
123
+ return {
124
+ success: false,
125
+ declined: false,
126
+ agentId: input.agentId,
127
+ error: `Delegate failed: ${err instanceof Error ? err.message : String(err)}`,
96
128
  };
97
129
  }
98
- // No handler available
99
- return {
100
- success: false,
101
- declined: false,
102
- error: 'Delegation UI not available. Ensure you are running in interactive mode.',
103
- };
104
130
  });
105
131
  // Register delegate_background handler for coordinator mode
106
132
  registerDelegateBackgroundHandler(async (input) => {
@@ -277,15 +303,167 @@ export function registerDelegationHandlers(deps) {
277
303
  }
278
304
  // Record the handoff for one-hop tracking
279
305
  deps.team.recordHandoff(input.agentId, sourceAgentId);
280
- // Set pending delegation - REPL will handle the switch after agent finishes
281
- // Reuses the same pendingDelegation mechanism as delegate
306
+ // Set pending delegation - REPL will handle the switch after agent finishes.
307
+ // Reuses the same pendingDelegation mechanism as delegate. We dispatch the
308
+ // task via `buildHandoffTaskMessage` (SDK helper) so the target receives
309
+ // the canonical `[Handoff from $arch]\n\nTask: …` wrapper — identical
310
+ // wording to what the Desktop sends on handoff, regardless of host.
311
+ const intent = {
312
+ // CLI has a single REPL session — the conversation key is implicit.
313
+ // SDK takes this as opaque, so any stable string is fine.
314
+ conversationId: 'cli',
315
+ sourceAgentId,
316
+ targetAgentId: input.agentId,
317
+ task: input.task,
318
+ reason: input.reason,
319
+ };
282
320
  deps.pendingDelegation = {
283
321
  agentId: input.agentId,
284
- task: input.task,
322
+ task: buildHandoffTaskMessage(intent),
285
323
  };
286
324
  return {
287
325
  success: true,
288
326
  declined: false,
289
327
  };
290
328
  });
329
+ // ---------------------------------------------------------------------------
330
+ // consult handler — specialist-to-specialist sync borrow
331
+ //
332
+ // Unlike delegate/handoff (which set `pendingDelegation` so the REPL can
333
+ // run the target agent on the NEXT turn), consult must run the target
334
+ // INLINE during the borrower's current turn and return the answer as the
335
+ // tool's output. The borrower's run never yields control.
336
+ //
337
+ // Transient design: snapshot the target's history, run the synthesised
338
+ // question, restore the history. The target's chat is untouched.
339
+ //
340
+ // Errors are returned as `{ answer: '', error: ... }` so the borrower's
341
+ // LLM sees them as tool output and can decide what to do.
342
+ // ---------------------------------------------------------------------------
343
+ registerConsultHandler(async (input) => {
344
+ // Yield to event loop (consistent with the other handlers in this file).
345
+ await new Promise((resolve) => setImmediate(resolve));
346
+ if (!deps.team) {
347
+ return {
348
+ answer: '',
349
+ agentId: input.agentId,
350
+ error: 'Multi-agent team not initialized. Consult requires an active team.',
351
+ };
352
+ }
353
+ const targetTeamAgent = deps.team.get(input.agentId);
354
+ if (!targetTeamAgent) {
355
+ return {
356
+ answer: '',
357
+ agentId: input.agentId,
358
+ error: `Agent '${input.agentId}' not found in team.`,
359
+ };
360
+ }
361
+ // Auto-initialize the target if it hasn't been used yet. This used
362
+ // to be an error ("switch to them once first") but the friction was
363
+ // wrong — a coordinator asking the user to manually warm up every
364
+ // specialist before consult is unreasonable. team.initializeAll()
365
+ // is the SDK's lazy-init entry point; it skips already-initialized
366
+ // agents, so subsequent consults pay only the first-time cost.
367
+ if (!targetTeamAgent.agent) {
368
+ try {
369
+ await deps.team.initializeAll();
370
+ }
371
+ catch (initErr) {
372
+ return {
373
+ answer: '',
374
+ agentId: input.agentId,
375
+ error: `Failed to initialize $${input.agentId}: ${initErr instanceof Error ? initErr.message : String(initErr)}`,
376
+ };
377
+ }
378
+ }
379
+ const targetAgent = targetTeamAgent.agent;
380
+ if (!targetAgent) {
381
+ return {
382
+ answer: '',
383
+ agentId: input.agentId,
384
+ error: `Agent '$${input.agentId}' could not be initialized.`,
385
+ };
386
+ }
387
+ const sourceAgentId = deps.team.activeAgentId;
388
+ const consultMessage = buildConsultQuestionMessage({
389
+ sourceAgentId,
390
+ question: input.question,
391
+ context: input.context,
392
+ });
393
+ // Snapshot the target's history so the consult is transient — the
394
+ // target's actual chat shouldn't grow a record of consults received.
395
+ const historySnapshot = targetAgent.getHistory();
396
+ try {
397
+ // Run inline. No onEvent forwarder → the target's tool calls and
398
+ // streaming text don't render to the REPL (we want the source agent's
399
+ // voice foregrounded; the answer comes back as a tool result).
400
+ const runResult = await targetAgent.run(consultMessage);
401
+ return { answer: runResult.response, agentId: input.agentId };
402
+ }
403
+ catch (err) {
404
+ return {
405
+ answer: '',
406
+ agentId: input.agentId,
407
+ error: `Consult failed: ${err instanceof Error ? err.message : String(err)}`,
408
+ };
409
+ }
410
+ finally {
411
+ // Restore history regardless of success/failure — even a partial run
412
+ // mutates the target's message log, and we promised it'd be transient.
413
+ try {
414
+ await targetAgent.setHistory(historySnapshot);
415
+ }
416
+ catch {
417
+ // History restore is best-effort; if it fails the next message the
418
+ // user sends to the target will see the consult artefacts. Logging
419
+ // happens via the agent's own error path.
420
+ }
421
+ }
422
+ });
423
+ }
424
+ /**
425
+ * Show the delegation approval overlay (V2 if available, ask_user_simple
426
+ * fallback otherwise). Centralises the dual-path logic that the old
427
+ * delegate handler inlined twice.
428
+ */
429
+ async function askForDelegationApproval(args) {
430
+ const { deps, agentId, displayName, task, reason } = args;
431
+ // V2 mode: TerminalUI overlay
432
+ if (deps.showDelegateOverlayV2) {
433
+ const result = await deps.showDelegateOverlayV2({
434
+ agentId,
435
+ agentDisplayName: displayName,
436
+ task,
437
+ reason,
438
+ });
439
+ return result.approved ? { kind: 'approved' } : { kind: 'declined' };
440
+ }
441
+ // Fallback: ask_user_simple
442
+ if (deps.showAskUserSimpleOverlayV2) {
443
+ const question = reason
444
+ ? `Delegate to $${agentId} (${displayName})?\n${chalk.dim(`Reason: ${reason}`)}\n${chalk.dim(`Task: ${truncate(task, 100)}`)}`
445
+ : `Delegate to $${agentId} (${displayName})?\n${chalk.dim(`Task: ${truncate(task, 100)}`)}`;
446
+ const result = await deps.showAskUserSimpleOverlayV2({
447
+ question,
448
+ options: ['Yes', 'No'],
449
+ allowCustom: false,
450
+ });
451
+ if (result.skipped || result.answer === 'No')
452
+ return { kind: 'declined' };
453
+ return { kind: 'approved' };
454
+ }
455
+ return { kind: 'no-ui' };
456
+ }
457
+ /**
458
+ * Build the delegated task message handed to the specialist. Mirrors the
459
+ * delegate_background preamble's SCOPE block so the specialist knows it
460
+ * was a delegation (versus a direct turn from the user or a consult).
461
+ */
462
+ function buildDelegateTaskMessage(args) {
463
+ const { task, reason } = args;
464
+ const lines = ['[DELEGATION from coordinator]'];
465
+ if (reason)
466
+ lines.push(`Reason: ${reason}`);
467
+ lines.push('', 'SCOPE: Focus ONLY on the task below. Do NOT pick up, start, or ask', 'about other tasks. Report your final result when done.', '', 'Task:', task);
468
+ return lines.join('\n');
291
469
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * build_interactive_flow handler factory — mirrors ask-user-handlers.ts.
3
+ *
4
+ * Registers a handler that:
5
+ * 1. Waits for any pending tool_end events to drain
6
+ * 2. Flushes accumulated agent text so the wizard doesn't crowd the
7
+ * conversation log
8
+ * 3. For background agents, enqueues the request via the pending-
9
+ * requests manager (user services it through /pending)
10
+ * 4. For foreground agents, shows InteractiveFlowOverlayV2
11
+ *
12
+ * Spec: project-docs/00-requirements/compilr-dev-cli/interactive-flow-cli-spec.md
13
+ */
14
+ import { type InteractiveFlowResult, type Flow } from '../shared-handlers.js';
15
+ export interface InteractiveFlowHandlerDeps {
16
+ flushTextBuffer?: () => void;
17
+ getActiveBackgroundAgentId?: () => string | null;
18
+ isBackgroundAgent?: (agentId?: string) => boolean;
19
+ getBackgroundAgentInfo?: (agentId: string) => {
20
+ mascot: string;
21
+ } | null;
22
+ showInteractiveFlowOverlayV2?: (options: {
23
+ flow: Flow;
24
+ }) => Promise<InteractiveFlowResult>;
25
+ }
26
+ export declare function registerInteractiveFlowHandlers(deps: InteractiveFlowHandlerDeps): void;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * build_interactive_flow handler factory — mirrors ask-user-handlers.ts.
3
+ *
4
+ * Registers a handler that:
5
+ * 1. Waits for any pending tool_end events to drain
6
+ * 2. Flushes accumulated agent text so the wizard doesn't crowd the
7
+ * conversation log
8
+ * 3. For background agents, enqueues the request via the pending-
9
+ * requests manager (user services it through /pending)
10
+ * 4. For foreground agents, shows InteractiveFlowOverlayV2
11
+ *
12
+ * Spec: project-docs/00-requirements/compilr-dev-cli/interactive-flow-cli-spec.md
13
+ */
14
+ import { registerInteractiveFlowHandler, } from '../shared-handlers.js';
15
+ import { getPendingRequestsManager } from '../multi-agent/index.js';
16
+ // ---------------------------------------------------------------------------
17
+ // Factory
18
+ // ---------------------------------------------------------------------------
19
+ export function registerInteractiveFlowHandlers(deps) {
20
+ registerInteractiveFlowHandler(async (input) => {
21
+ // Wait for any pending tool_end events to drain
22
+ await new Promise((resolve) => setImmediate(resolve));
23
+ // Flush accumulated text BEFORE showing the overlay
24
+ deps.flushTextBuffer?.();
25
+ // Background-agent routing — enqueue via pending-requests manager
26
+ const backgroundAgentId = deps.getActiveBackgroundAgentId?.();
27
+ if (backgroundAgentId && deps.isBackgroundAgent?.(backgroundAgentId)) {
28
+ const agentInfo = deps.getBackgroundAgentInfo?.(backgroundAgentId);
29
+ const pendingManager = getPendingRequestsManager();
30
+ try {
31
+ const response = await pendingManager.enqueue({
32
+ agentId: backgroundAgentId,
33
+ mascot: agentInfo?.mascot ?? '[•_•]',
34
+ type: 'question',
35
+ prompt: input.flow.title,
36
+ context: {
37
+ toolType: 'build_interactive_flow',
38
+ originalInput: input,
39
+ },
40
+ });
41
+ return response.overlayResult;
42
+ }
43
+ catch {
44
+ // Request was cancelled — return aborted result
45
+ return {
46
+ completed: false,
47
+ path: [],
48
+ answers: {},
49
+ answerLabels: {},
50
+ abortReason: 'unknown',
51
+ durationMs: 0,
52
+ };
53
+ }
54
+ }
55
+ // Foreground — show the overlay
56
+ if (!deps.showInteractiveFlowOverlayV2) {
57
+ throw new Error('build_interactive_flow overlay not initialized');
58
+ }
59
+ return deps.showInteractiveFlowOverlayV2({ flow: input.flow });
60
+ });
61
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * propose_alternatives handler factory — mirrors ask-user-handlers.ts.
3
+ *
4
+ * Registers a handler that:
5
+ * 1. Waits for any pending tool_end events to drain
6
+ * 2. Flushes accumulated agent text so the overlay doesn't crowd the
7
+ * conversation log
8
+ * 3. For background agents, enqueues the request via the pending-
9
+ * requests manager (user services it through /pending)
10
+ * 4. For foreground agents, shows ProposeAlternativesOverlayV2
11
+ *
12
+ * The deps object is SharedState passed by reference, so live overlay
13
+ * callbacks wired after REPL starts work as expected.
14
+ *
15
+ * Spec: project-docs/00-requirements/compilr-dev-cli/propose-alternatives-spec.md
16
+ */
17
+ import { type Alternative } from '../shared-handlers.js';
18
+ export interface ProposeAlternativesHandlerDeps {
19
+ flushTextBuffer?: () => void;
20
+ getActiveBackgroundAgentId?: () => string | null;
21
+ isBackgroundAgent?: (agentId?: string) => boolean;
22
+ getBackgroundAgentInfo?: (agentId: string) => {
23
+ mascot: string;
24
+ } | null;
25
+ showProposeAlternativesOverlayV2?: (options: {
26
+ question: string;
27
+ context?: string;
28
+ alternatives: Alternative[];
29
+ }) => Promise<{
30
+ chosenIndex: number;
31
+ chosenTitle: string;
32
+ notes?: string;
33
+ rejected: boolean;
34
+ }>;
35
+ }
36
+ export declare function registerProposeAlternativesHandlers(deps: ProposeAlternativesHandlerDeps): void;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * propose_alternatives handler factory — mirrors ask-user-handlers.ts.
3
+ *
4
+ * Registers a handler that:
5
+ * 1. Waits for any pending tool_end events to drain
6
+ * 2. Flushes accumulated agent text so the overlay doesn't crowd the
7
+ * conversation log
8
+ * 3. For background agents, enqueues the request via the pending-
9
+ * requests manager (user services it through /pending)
10
+ * 4. For foreground agents, shows ProposeAlternativesOverlayV2
11
+ *
12
+ * The deps object is SharedState passed by reference, so live overlay
13
+ * callbacks wired after REPL starts work as expected.
14
+ *
15
+ * Spec: project-docs/00-requirements/compilr-dev-cli/propose-alternatives-spec.md
16
+ */
17
+ import { registerProposeAlternativesHandler, } from '../shared-handlers.js';
18
+ import { getPendingRequestsManager } from '../multi-agent/index.js';
19
+ // ---------------------------------------------------------------------------
20
+ // Factory
21
+ // ---------------------------------------------------------------------------
22
+ export function registerProposeAlternativesHandlers(deps) {
23
+ registerProposeAlternativesHandler(async (input) => {
24
+ // Wait for any pending tool_end events to drain
25
+ await new Promise((resolve) => setImmediate(resolve));
26
+ // Flush accumulated text BEFORE showing the overlay
27
+ deps.flushTextBuffer?.();
28
+ // Background-agent routing — enqueue via pending-requests manager
29
+ const backgroundAgentId = deps.getActiveBackgroundAgentId?.();
30
+ if (backgroundAgentId && deps.isBackgroundAgent?.(backgroundAgentId)) {
31
+ const agentInfo = deps.getBackgroundAgentInfo?.(backgroundAgentId);
32
+ const pendingManager = getPendingRequestsManager();
33
+ try {
34
+ const response = await pendingManager.enqueue({
35
+ agentId: backgroundAgentId,
36
+ mascot: agentInfo?.mascot ?? '[•_•]',
37
+ type: 'question',
38
+ prompt: input.question,
39
+ context: {
40
+ toolType: 'propose_alternatives',
41
+ originalInput: input,
42
+ },
43
+ });
44
+ return response.overlayResult;
45
+ }
46
+ catch {
47
+ // Request was cancelled — fall back to a rejected result
48
+ return {
49
+ chosenIndex: 0,
50
+ chosenTitle: input.alternatives[0]?.title ?? '',
51
+ rejected: true,
52
+ };
53
+ }
54
+ }
55
+ // Foreground — show the overlay
56
+ if (!deps.showProposeAlternativesOverlayV2) {
57
+ throw new Error('propose_alternatives overlay not initialized');
58
+ }
59
+ return deps.showProposeAlternativesOverlayV2({
60
+ question: input.question,
61
+ context: input.context,
62
+ alternatives: input.alternatives,
63
+ });
64
+ });
65
+ }
package/dist/index.js CHANGED
@@ -36,8 +36,9 @@ import { cleanupStaleDiffs } from './ui/vscode-diff-ipc.js';
36
36
  import { createPermissionHandler } from './handlers/permission-handler.js';
37
37
  import { registerDelegationHandlers } from './handlers/delegation-handlers.js';
38
38
  import { registerAskUserHandlers } from './handlers/ask-user-handlers.js';
39
- import { EpisodeRecorder, FileEpisodeStore, setGlobalEpisodeStore } from './episodes/index.js';
40
- import { updateWorkSummaryAnchor } from './episodes/work-summary-anchor.js';
39
+ import { registerProposeAlternativesHandlers } from './handlers/propose-alternatives-handlers.js';
40
+ import { registerInteractiveFlowHandlers } from './handlers/interactive-flow-handlers.js';
41
+ import { EpisodeRecorder, FileEpisodeStore, setGlobalEpisodeStore, updateWorkSummaryAnchor, } from './episodes/index.js';
41
42
  import { getSessionsPath } from './settings/paths.js';
42
43
  import * as path from 'path';
43
44
  perf('imports-done');
@@ -447,6 +448,8 @@ async function main() {
447
448
  const onPermissionRequest = createPermissionHandler(sharedState);
448
449
  // Register ask_user and ask_user_simple handlers
449
450
  registerAskUserHandlers(sharedState);
451
+ registerProposeAlternativesHandlers(sharedState);
452
+ registerInteractiveFlowHandlers(sharedState);
450
453
  // Register delegate, delegate_background, and handoff handlers for multi-agent delegation
451
454
  registerDelegationHandlers(sharedState);
452
455
  // Create iteration limit handler that shows overlay
@@ -712,6 +715,14 @@ async function main() {
712
715
  onAskUserReady: (showOverlay) => {
713
716
  sharedState.showAskUserOverlayV2 = showOverlay;
714
717
  },
718
+ // Set up propose_alternatives V2 overlay callback
719
+ onProposeAlternativesReady: (showOverlay) => {
720
+ sharedState.showProposeAlternativesOverlayV2 = showOverlay;
721
+ },
722
+ // Set up build_interactive_flow V2 overlay callback
723
+ onInteractiveFlowReady: (showOverlay) => {
724
+ sharedState.showInteractiveFlowOverlayV2 = showOverlay;
725
+ },
715
726
  // Set up iteration_limit V2 overlay callback
716
727
  onIterationLimitReady: (showOverlay) => {
717
728
  sharedState.showIterationLimitOverlayV2 = showOverlay;
package/dist/repl-v2.d.ts CHANGED
@@ -15,6 +15,8 @@ import { type PermissionResult, type PermissionOverlayV2Options } from './ui/ove
15
15
  import { type AskUserSimpleOptionsV2, type AskUserSimpleResultV2 } from './ui/overlay/impl/ask-user-simple-overlay-v2.js';
16
16
  import { type GuardrailConfirmOptionsV2, type GuardrailConfirmResultV2 } from './ui/overlay/impl/guardrail-overlay-v2.js';
17
17
  import { type AskUserOptionsV2, type AskUserResultV2 } from './ui/overlay/impl/ask-user-overlay-v2.js';
18
+ import { type ProposeAlternativesOptionsV2, type ProposeAlternativesResultV2 } from './ui/overlay/impl/propose-alternatives-overlay-v2.js';
19
+ import { type InteractiveFlowOptionsV2, type InteractiveFlowResultV2 } from './ui/overlay/impl/interactive-flow-overlay-v2.js';
18
20
  import { type IterationLimitOptionsV2, type IterationLimitResultV2 } from './ui/overlay/impl/iteration-limit-overlay-v2.js';
19
21
  export interface DelegateOverlayOptions {
20
22
  agentId: string;
@@ -70,6 +72,16 @@ export interface ReplV2Options {
70
72
  * Allows index.ts to use V2 overlay for multi-question forms.
71
73
  */
72
74
  onAskUserReady?: (showOverlay: (options: AskUserOptionsV2) => Promise<AskUserResultV2>) => void;
75
+ /**
76
+ * Callback invoked when propose_alternatives overlay is ready.
77
+ * Allows index.ts to wire showProposeAlternativesOverlayV2 on sharedState.
78
+ */
79
+ onProposeAlternativesReady?: (showOverlay: (options: ProposeAlternativesOptionsV2) => Promise<ProposeAlternativesResultV2>) => void;
80
+ /**
81
+ * Callback invoked when build_interactive_flow overlay is ready.
82
+ * Allows index.ts to wire showInteractiveFlowOverlayV2 on sharedState.
83
+ */
84
+ onInteractiveFlowReady?: (showOverlay: (options: InteractiveFlowOptionsV2) => Promise<InteractiveFlowResultV2>) => void;
73
85
  /**
74
86
  * Callback invoked when iteration_limit overlay is ready.
75
87
  * Allows index.ts to use V2 overlay for iteration limit prompts.
@@ -238,6 +250,8 @@ export declare class ReplV2 {
238
250
  private readonly onAskUserSimpleReady?;
239
251
  private readonly onGuardrailReady?;
240
252
  private readonly onAskUserReady?;
253
+ private readonly onProposeAlternativesReady?;
254
+ private readonly onInteractiveFlowReady?;
241
255
  private readonly onIterationLimitReady?;
242
256
  private readonly onAgentFinish?;
243
257
  private readonly onSuggestionReady?;
@@ -264,6 +278,7 @@ export declare class ReplV2 {
264
278
  private sessionRequests;
265
279
  private textAccumulator;
266
280
  private readonly bashAbortControllers;
281
+ private readonly specialistToolsRegistered;
267
282
  constructor(options?: ReplV2Options);
268
283
  /**
269
284
  * Flush accumulated agent text to the UI.
@@ -354,6 +369,57 @@ export declare class ReplV2 {
354
369
  * Callers handle their own messaging (different styles/wording per context).
355
370
  */
356
371
  private switchAgent;
372
+ /**
373
+ * Register multi-agent tools (consult always, handoff for specialists)
374
+ * on the agent's underlying Agent using SDK factories.
375
+ *
376
+ * Tool grants:
377
+ * - **consult**: everyone (coordinator AND specialists). Lightweight
378
+ * "ask another agent a focused question" tool. Originally specced as
379
+ * specialist-only, but empirical evidence (session 220) showed the
380
+ * coordinator's LLM grabs the wrong tool (e.g. `suggest`) when the
381
+ * user says "consult $pm" because no tool by that name was in its
382
+ * registry. delegate is too heavyweight for a quick question.
383
+ * - **handoff**: specialists only. Coordinators delegate, they don't
384
+ * pass the baton. $default → specialist already has delegate.
385
+ * - **delegate / delegate_background**: coordinator only, via the
386
+ * existing CLI tool-array path. Not touched here.
387
+ *
388
+ * Why lazy: the SDK factories need `team` + `currentAgentId` at
389
+ * construction time. The CLI's tool array is built at module load
390
+ * when neither is known.
391
+ *
392
+ * Dedupe via `specialistToolsRegistered` set — tools are registered
393
+ * once per agent for the lifetime of the REPL. Mirrors Desktop's
394
+ * pattern in `agent-manager.ts:registerDelegateToolsIfNeeded`.
395
+ *
396
+ * Called from:
397
+ * - `switchAgent` (every agent switch — covers specialists).
398
+ * - `restoreTeamForPrefix` (team restoration — covers $default at
399
+ * startup, since the coordinator is never switchTo'd from outside).
400
+ */
401
+ /**
402
+ * Called whenever `this.team` is reassigned (project switch, team
403
+ * restoration). The SDK's createConsultTool factory captures the `team`
404
+ * argument in its closure at construction time, so a tool registered
405
+ * against the old team has a stale roster view. We must:
406
+ * 1. Unregister the stale consult/handoff tools from $default's
407
+ * registry (Agent.registerTools throws on duplicate-name, it
408
+ * does not replace).
409
+ * 2. Re-register fresh tools bound to the new team.
410
+ *
411
+ * $default's underlying Agent is preserved across team swaps (the new
412
+ * team is set as default via setDefaultAgent), so its tool registry
413
+ * accumulates stale tools. Specialists' Agents are fresh after a swap,
414
+ * so they don't have stale tools to clean up — clearing the dedupe set
415
+ * is enough; they'll re-register on demand via switchAgent.
416
+ *
417
+ * Without this, `consult($pm)` from $default reports "no specialists
418
+ * available" because the closure references the fresh-startup team
419
+ * (which only had $default) instead of the restored/built team.
420
+ */
421
+ private rebindMultiAgentToolsAfterTeamSwap;
422
+ private registerMultiAgentTools;
357
423
  /**
358
424
  * Bring a background agent to foreground.
359
425
  * Validates the agent has a background session, then delegates to switchAgent().