@compilr-dev/cli 0.5.2 → 0.5.4

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.
@@ -356,7 +356,7 @@ export async function loadAndRestoreSession(projectId, agent, mode = 'auto') {
356
356
  * @param teamOptions - Optional team switching options
357
357
  * @returns Object with switch info, or null if no switch occurred
358
358
  */
359
- export async function handleProjectSwitch(oldProjectId, agent, teamOptions) {
359
+ export async function handleProjectSwitch(oldProjectId, agent, teamOptions, agentOptions) {
360
360
  const newProject = getCurrentProject();
361
361
  const newProjectId = newProject?.id ?? null;
362
362
  // No switch occurred
@@ -427,14 +427,24 @@ export async function handleProjectSwitch(oldProjectId, agent, teamOptions) {
427
427
  messageCount = messages.length;
428
428
  }
429
429
  }
430
- // Update agent's anchors for the new project
431
- if (agent?.clearAnchors && agent.addAnchor) {
432
- // Clear existing persistent anchors
433
- agent.clearAnchors({ scope: 'persistent' });
434
- // Add new project's anchors
430
+ }
431
+ catch {
432
+ // Context refresh is best-effort
433
+ }
434
+ // Recreate default agent if agentOptions provided — the new agent gets
435
+ // the fresh system prompt (project name, COMPILR.md, guided context).
436
+ let newAgent;
437
+ if (agentOptions) {
438
+ try {
439
+ // 1. Update liveProjectState in index.ts so agentFactory reads fresh values
440
+ await agentOptions.updateProjectState?.(newProjectId);
441
+ // 2. Create fresh default agent with new project's system prompt
442
+ newAgent = await agentOptions.agentFactory({});
443
+ // 3. Transfer anchors to the NEW agent
444
+ newAgent.clearAnchors({ scope: 'persistent' });
435
445
  const newAnchors = await getProjectAnchors(newProjectId);
436
446
  for (const anchor of newAnchors) {
437
- agent.addAnchor({
447
+ newAgent.addAnchor({
438
448
  id: anchor.id,
439
449
  content: anchor.content,
440
450
  priority: anchor.priority,
@@ -442,18 +452,45 @@ export async function handleProjectSwitch(oldProjectId, agent, teamOptions) {
442
452
  tags: anchor.tags,
443
453
  });
444
454
  }
455
+ // 4. Set history on the NEW agent (not the old one)
456
+ if (messages.length > 0) {
457
+ await newAgent.setHistory(messages, { turnCount: messageCount });
458
+ }
459
+ }
460
+ catch {
461
+ // If agent recreation fails, fall back to updating the old agent
462
+ newAgent = undefined;
445
463
  }
446
464
  }
447
- catch {
448
- // Context refresh is best-effort
449
- }
450
- // Set the updated history (with context message if any)
451
- if (agent) {
452
- if (messages.length > 0) {
453
- await agent.setHistory(messages, { turnCount: messageCount });
465
+ // If no new agent was created, update the old agent as before
466
+ if (!newAgent) {
467
+ try {
468
+ // Update old agent's anchors
469
+ if (agent?.clearAnchors && agent.addAnchor) {
470
+ agent.clearAnchors({ scope: 'persistent' });
471
+ const newAnchors = await getProjectAnchors(newProjectId);
472
+ for (const anchor of newAnchors) {
473
+ agent.addAnchor({
474
+ id: anchor.id,
475
+ content: anchor.content,
476
+ priority: anchor.priority,
477
+ scope: anchor.scope,
478
+ tags: anchor.tags,
479
+ });
480
+ }
481
+ }
454
482
  }
455
- else {
456
- agent.clearHistory();
483
+ catch {
484
+ // Context refresh is best-effort
485
+ }
486
+ // Set the updated history on the old agent
487
+ if (agent) {
488
+ if (messages.length > 0) {
489
+ await agent.setHistory(messages, { turnCount: messageCount });
490
+ }
491
+ else {
492
+ agent.clearHistory();
493
+ }
457
494
  }
458
495
  }
459
496
  // Handle team switching if team options provided
@@ -496,6 +533,7 @@ export async function handleProjectSwitch(oldProjectId, agent, teamOptions) {
496
533
  contextRefreshed,
497
534
  contextMessage,
498
535
  newTeam,
536
+ newAgent,
499
537
  };
500
538
  }
501
539
  /**
@@ -43,6 +43,11 @@ export interface CommandContextV2 {
43
43
  * Used when switching projects to create new team.
44
44
  */
45
45
  agentFactory?: (config: Record<string, unknown>, systemPromptAddition?: string, useMinimalSystemPrompt?: boolean, noTools?: boolean) => Promise<Agent>;
46
+ /**
47
+ * Update live project state so agentFactory creates agents with fresh context.
48
+ * Called during project switch before recreating the default agent.
49
+ */
50
+ updateProjectState?: (projectId: number | null) => Promise<void>;
46
51
  /**
47
52
  * Replace the current agent with a restored one.
48
53
  * Used by /resume command to restore from a saved session.
Binary file
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ process.env.FORCE_COLOR = '3';
11
11
  import { perf, flushStartupPerf } from './utils/startup-perf.js';
12
12
  perf('imports-start');
13
13
  import { createAgent } from './agent.js';
14
- import { getModelForTier } from './models/index.js';
14
+ import { getModelForTier, getProviderMetadata } from './models/index.js';
15
15
  import { ReplV2 } from './repl-v2.js';
16
16
  import { AgentTeam, setActiveSharedContext } from './multi-agent/index.js';
17
17
  import { getStyles } from './themes/index.js';
@@ -320,34 +320,48 @@ async function main() {
320
320
  }
321
321
  }
322
322
  }
323
+ // ==========================================================================
324
+ // Mutable project state — closures read these live so agentFactory
325
+ // always sees the current project context (not stale startup values).
326
+ // ==========================================================================
327
+ const liveProjectState = {
328
+ dbProject: null,
329
+ projectContext: undefined,
330
+ guidedModeContext: undefined,
331
+ };
323
332
  // Load project memory (COMPILR.md or CLAUDE.md)
324
333
  perf('project-memory-start');
325
334
  const projectMemory = loadProjectMemory();
326
- let projectContext;
327
335
  if (projectMemory.found && projectMemory.filePath) {
328
- projectContext = projectMemory.content;
336
+ liveProjectState.projectContext = projectMemory.content;
329
337
  }
330
338
  // ==========================================================================
331
339
  // Load guided mode context from database (if project uses guided mode)
332
340
  // ==========================================================================
333
341
  perf('project-db-start');
334
- let guidedModeContext;
335
342
  // Determine which project to load based on projectStartup setting
336
343
  const projectStartupMode = getProjectStartupMode();
337
- let dbProject = null;
338
344
  if (projectStartupMode === 'last') {
339
345
  // Try to load the last opened project
340
346
  const lastProjectId = getLastProjectId();
341
347
  if (lastProjectId !== null) {
342
- dbProject = projectRepository.getById(lastProjectId);
348
+ liveProjectState.dbProject = projectRepository.getById(lastProjectId);
343
349
  // If last project no longer exists (deleted), don't auto-load any project
344
350
  }
345
351
  // If no lastProjectId remembered, don't auto-load any project
346
352
  }
347
353
  // else projectStartupMode === 'off': don't auto-load any project
348
- if (dbProject) {
354
+ if (liveProjectState.dbProject) {
355
+ const dbProject = liveProjectState.dbProject;
349
356
  // Set as active project for tools to use
350
357
  setActiveProject({ id: dbProject.id, name: dbProject.name, displayName: dbProject.displayName, path: dbProject.path });
358
+ // Reload project memory from the DB project's path (not CWD)
359
+ // The initial loadProjectMemory() above uses process.cwd() which may
360
+ // load the wrong COMPILR.md/CLAUDE.md (e.g., workspace-level instead of project-specific)
361
+ if (dbProject.path) {
362
+ const memory = loadProjectMemory({ cwd: dbProject.path });
363
+ liveProjectState.projectContext = memory.found ? memory.content : undefined;
364
+ }
351
365
  // If in guided mode, generate context
352
366
  if (shouldInjectGuidedContext(dbProject)) {
353
367
  // Get work item counts
@@ -376,7 +390,7 @@ async function main() {
376
390
  inProgressCount: statusCounts.in_progress,
377
391
  completedCount: statusCounts.completed,
378
392
  };
379
- guidedModeContext = getFullGuidedPrompt(contextOptions);
393
+ liveProjectState.guidedModeContext = getFullGuidedPrompt(contextOptions);
380
394
  }
381
395
  }
382
396
  // ==========================================================================
@@ -386,8 +400,8 @@ async function main() {
386
400
  // Load global anchors (always)
387
401
  const globalAnchors = getGlobalAnchorManager().getAll({ scope: 'persistent' });
388
402
  // Load project-specific anchors (if in a tracked project)
389
- const projectAnchors = dbProject
390
- ? getAnchorManager(String(dbProject.id)).getAll({ scope: 'persistent' })
403
+ const projectAnchors = liveProjectState.dbProject
404
+ ? getAnchorManager(String(liveProjectState.dbProject.id)).getAll({ scope: 'persistent' })
391
405
  : [];
392
406
  // Combine into persistedAnchors array for agent
393
407
  const persistedAnchors = [
@@ -404,7 +418,7 @@ async function main() {
404
418
  priority: a.priority,
405
419
  scope: a.scope,
406
420
  tags: a.tags,
407
- projectId: String(dbProject?.id),
421
+ projectId: String(liveProjectState.dbProject?.id),
408
422
  })),
409
423
  ];
410
424
  // ==========================================================================
@@ -463,8 +477,8 @@ async function main() {
463
477
  // ==========================================================================
464
478
  perf('mcp-init-fire');
465
479
  // Check if there are MCP servers to connect — fire the promise but don't await yet
466
- const mcpServerCount = loadMCPConfig(dbProject?.path).length;
467
- const mcpInitPromise = mcpServerCount > 0 ? initializeMCP(dbProject?.path) : null;
480
+ const mcpServerCount = loadMCPConfig(liveProjectState.dbProject?.path).length;
481
+ const mcpInitPromise = mcpServerCount > 0 ? initializeMCP(liveProjectState.dbProject?.path) : null;
468
482
  // Mutable ref for MCP tools — read by team agent factory when creating new agents.
469
483
  // Updated when background MCP init completes.
470
484
  let mcpToolsRef = [];
@@ -472,9 +486,10 @@ async function main() {
472
486
  // Episode Recorder Setup (work history tracking)
473
487
  // ==========================================================================
474
488
  const createEpisodeRecorder = (agentId) => {
475
- if (!dbProject)
489
+ if (!liveProjectState.dbProject)
476
490
  return undefined;
477
- const episodesPath = path.join(getSessionsPath(), `project-${String(dbProject.id)}`, 'episodes.json');
491
+ const projectId = liveProjectState.dbProject.id;
492
+ const episodesPath = path.join(getSessionsPath(), `project-${String(projectId)}`, 'episodes.json');
478
493
  const store = new FileEpisodeStore({ filePath: episodesPath });
479
494
  // Set global store for recall_work tool access
480
495
  setGlobalEpisodeStore(store);
@@ -486,7 +501,7 @@ async function main() {
486
501
  onBatchFlushed: () => {
487
502
  updateWorkSummaryAnchor({
488
503
  store,
489
- projectId: String(dbProject.id),
504
+ projectId: String(projectId),
490
505
  getAnchorManager,
491
506
  });
492
507
  },
@@ -500,16 +515,16 @@ async function main() {
500
515
  model,
501
516
  minimal: options.minimal,
502
517
  quiet: true,
503
- enableMetaTools: !options.minimal, // Use meta-tools for token optimization
518
+ enableMetaTools: !options.minimal && getProviderMetadata(provider).supportsMetaTools,
504
519
  onPermissionRequest,
505
520
  onIterationLimitReached,
506
521
  onSuggest: (event) => {
507
522
  // Store suggestion to be applied after agent finishes
508
523
  sharedState.pendingSuggestion = event.action;
509
524
  },
510
- projectName: dbProject?.displayName,
511
- projectContext,
512
- guidedModeContext,
525
+ projectName: liveProjectState.dbProject?.displayName,
526
+ projectContext: liveProjectState.projectContext,
527
+ guidedModeContext: liveProjectState.guidedModeContext,
513
528
  // Anchors - library handles re-injection on every LLM call
514
529
  enableAnchors: true,
515
530
  persistedAnchors: persistedAnchors.length > 0 ? persistedAnchors : undefined,
@@ -537,9 +552,11 @@ async function main() {
537
552
  episodeRecorder: defaultEpisodeRecorder,
538
553
  });
539
554
  perf('create-agent-done');
555
+ // Mutable ref so grantSession and onDefaultAgentSwapped closures track the current default agent
556
+ let defaultAgentRef = agent;
540
557
  // Bind grantSession for the permission handler to use
541
558
  sharedState.grantSession = (toolName) => {
542
- agent.grantSessionPermission(toolName);
559
+ defaultAgentRef.grantSessionPermission(toolName);
543
560
  };
544
561
  // ==========================================================================
545
562
  // Create AgentTeam with the default agent (or restore from persistence)
@@ -554,15 +571,15 @@ async function main() {
554
571
  provider,
555
572
  model: agentModel,
556
573
  minimal: options.minimal,
557
- enableMetaTools: !options.minimal && !noTools, // Disable meta-tools if noTools is set
574
+ enableMetaTools: !options.minimal && !noTools && getProviderMetadata(provider).supportsMetaTools,
558
575
  onPermissionRequest,
559
576
  onIterationLimitReached,
560
577
  onSuggest: (event) => {
561
578
  sharedState.pendingSuggestion = event.action;
562
579
  },
563
- projectName: dbProject?.displayName,
564
- projectContext,
565
- guidedModeContext,
580
+ projectName: liveProjectState.dbProject?.displayName,
581
+ projectContext: liveProjectState.projectContext,
582
+ guidedModeContext: liveProjectState.guidedModeContext,
566
583
  enableAnchors: true,
567
584
  persistedAnchors: persistedAnchors.length > 0 ? persistedAnchors : undefined,
568
585
  enableGuardrails: true,
@@ -605,11 +622,11 @@ async function main() {
605
622
  // Set the active shared context for activity recording
606
623
  setActiveSharedContext(team.sharedContext);
607
624
  // Populate shared context with actual project info so agents know the project name
608
- if (dbProject) {
625
+ if (liveProjectState.dbProject) {
609
626
  team.sharedContext.setProject({
610
- id: dbProject.id,
611
- name: dbProject.displayName,
612
- path: dbProject.path,
627
+ id: liveProjectState.dbProject.id,
628
+ name: liveProjectState.dbProject.displayName,
629
+ path: liveProjectState.dbProject.path,
613
630
  });
614
631
  }
615
632
  // ==========================================================================
@@ -636,7 +653,7 @@ async function main() {
636
653
  provider,
637
654
  version: VERSION,
638
655
  updateAvailable,
639
- projectName: dbProject?.displayName ?? process.cwd().split('/').pop(),
656
+ projectName: liveProjectState.dbProject?.displayName ?? process.cwd().split('/').pop(),
640
657
  // Set up V2 permission overlay callback when UI is ready
641
658
  onUIReady: (showOverlay) => {
642
659
  sharedState.showPermissionOverlayV2 = showOverlay;
@@ -705,6 +722,58 @@ async function main() {
705
722
  onTeamReplaced: (newTeam) => {
706
723
  sharedState.team = newTeam;
707
724
  },
725
+ // Recreate default agent on project switch — updates liveProjectState
726
+ // so agentFactory reads fresh project context for the new agent.
727
+ onProjectStateUpdate: (projectId) => {
728
+ if (projectId !== null) {
729
+ liveProjectState.dbProject = projectRepository.getById(projectId);
730
+ if (liveProjectState.dbProject?.path) {
731
+ const memory = loadProjectMemory({ cwd: liveProjectState.dbProject.path });
732
+ liveProjectState.projectContext = memory.found ? memory.content : undefined;
733
+ }
734
+ else {
735
+ liveProjectState.projectContext = undefined;
736
+ }
737
+ if (liveProjectState.dbProject && shouldInjectGuidedContext(liveProjectState.dbProject)) {
738
+ const statusCounts = workItemRepository.getStatusCounts(liveProjectState.dbProject.id);
739
+ let currentItem = null;
740
+ if (liveProjectState.dbProject.currentItemId) {
741
+ currentItem = workItemRepository.getByItemId(liveProjectState.dbProject.id, liveProjectState.dbProject.currentItemId);
742
+ }
743
+ else {
744
+ const inProgressItems = workItemRepository.query({
745
+ project_id: liveProjectState.dbProject.id,
746
+ status: 'in_progress',
747
+ limit: 1,
748
+ });
749
+ if (inProgressItems.items.length > 0) {
750
+ currentItem = inProgressItems.items[0];
751
+ }
752
+ }
753
+ const contextOptions = {
754
+ project: liveProjectState.dbProject,
755
+ currentItem,
756
+ backlogCount: statusCounts.backlog,
757
+ inProgressCount: statusCounts.in_progress,
758
+ completedCount: statusCounts.completed,
759
+ };
760
+ liveProjectState.guidedModeContext = getFullGuidedPrompt(contextOptions);
761
+ }
762
+ else {
763
+ liveProjectState.guidedModeContext = undefined;
764
+ }
765
+ }
766
+ else {
767
+ liveProjectState.dbProject = null;
768
+ liveProjectState.projectContext = undefined;
769
+ liveProjectState.guidedModeContext = undefined;
770
+ }
771
+ return Promise.resolve();
772
+ },
773
+ // Keep defaultAgentRef in sync when project switch creates a new default agent
774
+ onDefaultAgentSwapped: (newAgent) => {
775
+ defaultAgentRef = newAgent;
776
+ },
708
777
  // Set session prefix for VS Code diff IPC after session registration
709
778
  onSessionRegistered: (sessionId) => {
710
779
  sharedState.sessionPrefix = sessionId.slice(0, 8);
@@ -9,11 +9,13 @@ import type { ProviderKey } from '../utils/credentials.js';
9
9
  import { type ProviderMetadata as SdkProviderMetadata } from '@compilr-dev/sdk';
10
10
  export { type OthersProviderModel, TOGETHER_MODELS, GROQ_MODELS, FIREWORKS_MODELS, PERPLEXITY_MODELS, OPENROUTER_MODELS, getModelsForOthersProvider, } from '@compilr-dev/sdk';
11
11
  /**
12
- * CLI-extended provider metadata — adds credentialKey for encrypted storage.
12
+ * CLI-extended provider metadata — adds credentialKey and capability flags.
13
13
  */
14
14
  export interface ProviderMetadata extends SdkProviderMetadata {
15
15
  /** Credential key for API key storage (CLI-specific) */
16
16
  credentialKey: ProviderKey;
17
+ /** Whether this provider handles the meta-tools indirection pattern well */
18
+ supportsMetaTools: boolean;
17
19
  }
18
20
  /**
19
21
  * CLI-extended provider metadata registry.
@@ -22,6 +22,14 @@ const CREDENTIAL_KEYS = {
22
22
  openrouter: 'openrouter',
23
23
  custom: 'openai', // Custom uses openai-compatible format by default
24
24
  };
25
+ /**
26
+ * Meta-tools capability: which providers handle the indirection pattern well.
27
+ * When false, all tools are registered directly (higher token cost, but reliable).
28
+ */
29
+ const SUPPORTS_META_TOOLS = {
30
+ claude: true,
31
+ gemini: true,
32
+ };
25
33
  /**
26
34
  * CLI-extended provider metadata registry.
27
35
  * Merges SDK metadata with CLI credential keys.
@@ -33,6 +41,7 @@ export const PROVIDER_METADATA = (() => {
33
41
  result[providerType] = {
34
42
  ...sdkMeta,
35
43
  credentialKey: CREDENTIAL_KEYS[providerType],
44
+ supportsMetaTools: SUPPORTS_META_TOOLS[providerType] ?? false,
36
45
  };
37
46
  }
38
47
  return result;
package/dist/repl-v2.d.ts CHANGED
@@ -160,6 +160,16 @@ export interface ReplV2Options {
160
160
  * see the restored team (with all agents) instead of the initial one.
161
161
  */
162
162
  onTeamReplaced?: (newTeam: AgentTeam) => void;
163
+ /**
164
+ * Callback to update live project state in index.ts before recreating the default agent.
165
+ * Updates liveProjectState (COMPILR.md, guided context) so agentFactory sees fresh values.
166
+ */
167
+ onProjectStateUpdate?: (projectId: number | null) => Promise<void>;
168
+ /**
169
+ * Callback invoked after the default agent is swapped on project switch.
170
+ * Allows index.ts to update defaultAgentRef for grantSession.
171
+ */
172
+ onDefaultAgentSwapped?: (newAgent: Agent) => void;
163
173
  }
164
174
  /**
165
175
  * Options for switchAgent().
@@ -220,6 +230,8 @@ export declare class ReplV2 {
220
230
  private readonly onBackgroundDelegationReady?;
221
231
  private readonly onSessionRegistered?;
222
232
  private readonly onTeamReplaced?;
233
+ private readonly onProjectStateUpdate?;
234
+ private readonly onDefaultAgentSwapped?;
223
235
  private readonly authOnly;
224
236
  private restoredSessionInfo;
225
237
  private currentExecutingBackgroundAgentId;
package/dist/repl-v2.js CHANGED
@@ -19,7 +19,7 @@ import { initSessionRegistry, getSessionRegistry, setActiveTerminalSessionId, ge
19
19
  import { initFileLockManager } from './multi-agent/file-locks.js';
20
20
  import { initNotificationManager, getNotificationManager } from './multi-agent/notification-manager.js';
21
21
  import { getSessionsPath } from './settings/paths.js';
22
- import { setMetaToolFilter } from './tools.js';
22
+ import { setMetaToolFilter, isMetaToolSilent, todoStore } from './tools.js';
23
23
  import { getCurrentProject } from './tools/project-db.js';
24
24
  import { TerminalUI } from './ui/terminal-ui.js';
25
25
  import * as terminal from './ui/terminal.js';
@@ -197,6 +197,8 @@ export class ReplV2 {
197
197
  onBackgroundDelegationReady;
198
198
  onSessionRegistered;
199
199
  onTeamReplaced;
200
+ onProjectStateUpdate;
201
+ onDefaultAgentSwapped;
200
202
  authOnly;
201
203
  restoredSessionInfo = null;
202
204
  // Phase 3b: Track which background agent is currently executing (for permission routing)
@@ -242,6 +244,8 @@ export class ReplV2 {
242
244
  this.onBackgroundDelegationReady = options.onBackgroundDelegationReady;
243
245
  this.onSessionRegistered = options.onSessionRegistered;
244
246
  this.onTeamReplaced = options.onTeamReplaced;
247
+ this.onProjectStateUpdate = options.onProjectStateUpdate;
248
+ this.onDefaultAgentSwapped = options.onDefaultAgentSwapped;
245
249
  this.authOnly = options.authOnly ?? false;
246
250
  }
247
251
  /**
@@ -734,6 +738,7 @@ export class ReplV2 {
734
738
  */
735
739
  restoreAgent(newAgent) {
736
740
  this.agent = newAgent;
741
+ this.onDefaultAgentSwapped?.(newAgent);
737
742
  }
738
743
  /**
739
744
  * Get the current agent instance.
@@ -1667,6 +1672,7 @@ export class ReplV2 {
1667
1672
  agent: this.agent,
1668
1673
  team: this.team,
1669
1674
  agentFactory: this.agentFactory,
1675
+ updateProjectState: this.onProjectStateUpdate,
1670
1676
  setTeam: (newTeam) => {
1671
1677
  const oldTeam = this.team;
1672
1678
  this.team = newTeam;
@@ -2351,7 +2357,7 @@ export class ReplV2 {
2351
2357
  this.ui.addBashCommand(event.toolUseId, command);
2352
2358
  }
2353
2359
  // Update spinner with tool name (skip silent tools)
2354
- if (!this.agent?.isToolSilent(event.name)) {
2360
+ if (!this.agent?.isToolSilent(event.name) && !isMetaToolSilent(event.name)) {
2355
2361
  if (event.name === 'task' && event.toolUseId) {
2356
2362
  const subagentType = typeof lastToolInput.subagent_type === 'string'
2357
2363
  ? lastToolInput.subagent_type
@@ -2428,30 +2434,37 @@ export class ReplV2 {
2428
2434
  this.ui.setCurrentTool(null);
2429
2435
  continue;
2430
2436
  }
2431
- // Handle todo_write - update UI state
2432
- if (toolName === 'todo_write' && toolInput && Array.isArray(toolInput.todos)) {
2433
- const todos = toolInput.todos.map((t) => {
2434
- const content = typeof t.content === 'string' ? t.content :
2435
- typeof t.title === 'string' ? t.title :
2436
- typeof t.task === 'string' ? t.task :
2437
- typeof t.description === 'string' ? t.description :
2438
- typeof t.text === 'string' ? t.text : 'Untitled task';
2439
- const status = (typeof t.status === 'string' && ['pending', 'in_progress', 'completed'].includes(t.status)
2440
- ? t.status
2441
- : 'pending');
2442
- const activeForm = typeof t.activeForm === 'string' ? t.activeForm : undefined;
2443
- const owner = typeof t.owner === 'string' ? t.owner : undefined;
2444
- const blockedBy = Array.isArray(t.blockedBy)
2445
- ? t.blockedBy.filter((n) => typeof n === 'number')
2446
- : undefined;
2447
- return { content, status, activeForm, owner, blockedBy };
2437
+ // Handle todo_write - update UI state from todoStore
2438
+ // (store is already updated by the tool's execute handler, regardless of
2439
+ // whether the tool was called directly or via the meta-registry fallback)
2440
+ if (toolName === 'todo_write') {
2441
+ // Get todos in creation order (getAll() sorts by status which breaks
2442
+ // task numbering the footer renderer handles status sorting itself)
2443
+ const storeTodos = todoStore.getAll()
2444
+ .sort((a, b) => {
2445
+ const aNum = parseInt(a.id.replace('todo-', ''), 10);
2446
+ const bNum = parseInt(b.id.replace('todo-', ''), 10);
2447
+ return aNum - bNum;
2448
2448
  });
2449
- this.ui.setTodos(todos);
2449
+ const allDone = storeTodos.length > 0 && storeTodos.every((t) => t.status === 'completed');
2450
+ if (allDone) {
2451
+ this.ui.setTodos([]);
2452
+ }
2453
+ else {
2454
+ this.ui.setTodos(storeTodos.map((t) => ({
2455
+ content: t.content,
2456
+ status: t.status,
2457
+ activeForm: t.activeForm,
2458
+ owner: t.owner,
2459
+ blockedBy: t.blockedBy,
2460
+ })));
2461
+ }
2450
2462
  this.ui.setCurrentTool(null);
2451
2463
  continue;
2452
2464
  }
2453
2465
  // Skip silent tools (no output needed)
2454
- if (this.agent?.isToolSilent(toolName)) {
2466
+ // Check both direct registry and meta-registry for the silent flag
2467
+ if (this.agent?.isToolSilent(toolName) || isMetaToolSilent(toolName)) {
2455
2468
  this.ui.setCurrentTool(null);
2456
2469
  continue;
2457
2470
  }
@@ -3245,13 +3258,12 @@ export class ReplV2 {
3245
3258
  if (metrics.toolCalls > 0) {
3246
3259
  parts.push(`${String(metrics.toolCalls)} tool${metrics.toolCalls > 1 ? 's' : ''}`);
3247
3260
  }
3248
- // DEBUG: Show payload debug info (chars sent to provider)
3261
+ // DEBUG: Show payload debug info (token estimates sent to provider)
3249
3262
  if (metrics.debugPayload) {
3250
- const { systemChars, contentsChars, toolsChars } = metrics.debugPayload;
3251
- const totalChars = systemChars + contentsChars + toolsChars;
3252
- const estTokens = Math.ceil(totalChars / 4);
3253
- // Format: [dbg 2calls sent:123k (~31k est) sys:10k cont:100k tools:13k]
3254
- parts.push(`[dbg ${String(metrics.apiCalls)}calls sent:${formatTokens(totalChars)} (~${formatTokens(estTokens)} est) sys:${formatTokens(systemChars)} cont:${formatTokens(contentsChars)} tools:${formatTokens(toolsChars)}]`);
3263
+ const { systemTokens, contentsTokens, toolsTokens } = metrics.debugPayload;
3264
+ const totalTokens = systemTokens + contentsTokens + toolsTokens;
3265
+ // Format: [dbg 2calls sent:31k sys:10k cont:8k tools:13k]
3266
+ parts.push(`[dbg ${String(metrics.apiCalls)}calls sent:${formatTokens(totalTokens)} sys:${formatTokens(systemTokens)} cont:${formatTokens(contentsTokens)} tools:${formatTokens(toolsTokens)}]`);
3255
3267
  }
3256
3268
  // Print as muted info line
3257
3269
  this.ui.print({ type: 'turn-summary', parts });
@@ -102,6 +102,10 @@ export declare const TOOL_NAMES: {
102
102
  export type ToolName = (typeof TOOL_NAMES)[keyof typeof TOOL_NAMES];
103
103
  /** Array of all direct tool names (always available) */
104
104
  export declare const DIRECT_TOOL_NAMES: readonly ToolName[];
105
+ /** Hot tools — used in >50% of turns, always declared as direct schemas */
106
+ export declare const HOT_TOOL_NAMES: readonly ToolName[];
107
+ /** Warm tools — used occasionally, moved to meta-registry in hot-tools mode */
108
+ export declare const WARM_TOOL_NAMES: readonly ToolName[];
105
109
  /** Array of all meta-tool names (tool discovery) */
106
110
  export declare const META_TOOL_NAMES: readonly ToolName[];
107
111
  /** Array of all meta-registry tool names (accessed via use_tool) */
@@ -142,6 +142,30 @@ export const DIRECT_TOOL_NAMES = [
142
142
  TOOL_NAMES.TASK,
143
143
  TOOL_NAMES.SUGGEST,
144
144
  ];
145
+ /** Hot tools — used in >50% of turns, always declared as direct schemas */
146
+ export const HOT_TOOL_NAMES = [
147
+ TOOL_NAMES.READ_FILE,
148
+ TOOL_NAMES.WRITE_FILE,
149
+ TOOL_NAMES.EDIT,
150
+ TOOL_NAMES.BASH,
151
+ TOOL_NAMES.GREP,
152
+ TOOL_NAMES.GLOB,
153
+ TOOL_NAMES.ASK_USER,
154
+ ];
155
+ /** Warm tools — used occasionally, moved to meta-registry in hot-tools mode */
156
+ export const WARM_TOOL_NAMES = [
157
+ TOOL_NAMES.BASH_OUTPUT,
158
+ TOOL_NAMES.KILL_SHELL,
159
+ TOOL_NAMES.TODO_WRITE,
160
+ TOOL_NAMES.TODO_READ,
161
+ TOOL_NAMES.TODO_CLAIM,
162
+ TOOL_NAMES.TODO_HANDOFF,
163
+ TOOL_NAMES.ASK_USER_SIMPLE,
164
+ TOOL_NAMES.DELEGATE,
165
+ TOOL_NAMES.DELEGATE_BACKGROUND,
166
+ TOOL_NAMES.HANDOFF,
167
+ TOOL_NAMES.COMPILR_GUIDE,
168
+ ];
145
169
  /** Array of all meta-tool names (tool discovery) */
146
170
  export const META_TOOL_NAMES = [
147
171
  TOOL_NAMES.LIST_TOOLS,