attocode 0.1.5 → 0.1.7

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 (187) hide show
  1. package/CHANGELOG.md +27 -2
  2. package/README.md +47 -0
  3. package/dist/src/adapters.d.ts +21 -1
  4. package/dist/src/adapters.d.ts.map +1 -1
  5. package/dist/src/adapters.js +29 -1
  6. package/dist/src/adapters.js.map +1 -1
  7. package/dist/src/agent.d.ts +69 -3
  8. package/dist/src/agent.d.ts.map +1 -1
  9. package/dist/src/agent.js +692 -42
  10. package/dist/src/agent.js.map +1 -1
  11. package/dist/src/commands/agents-commands.d.ts.map +1 -1
  12. package/dist/src/commands/agents-commands.js +18 -4
  13. package/dist/src/commands/agents-commands.js.map +1 -1
  14. package/dist/src/commands/handler.d.ts.map +1 -1
  15. package/dist/src/commands/handler.js +120 -5
  16. package/dist/src/commands/handler.js.map +1 -1
  17. package/dist/src/core/index.d.ts +1 -6
  18. package/dist/src/core/index.d.ts.map +1 -1
  19. package/dist/src/core/index.js +4 -7
  20. package/dist/src/core/index.js.map +1 -1
  21. package/dist/src/defaults.d.ts +31 -1
  22. package/dist/src/defaults.d.ts.map +1 -1
  23. package/dist/src/defaults.js +52 -2
  24. package/dist/src/defaults.js.map +1 -1
  25. package/dist/src/integrations/agent-registry.d.ts.map +1 -1
  26. package/dist/src/integrations/agent-registry.js +12 -5
  27. package/dist/src/integrations/agent-registry.js.map +1 -1
  28. package/dist/src/integrations/codebase-context.d.ts +163 -0
  29. package/dist/src/integrations/codebase-context.d.ts.map +1 -1
  30. package/dist/src/integrations/codebase-context.js +462 -0
  31. package/dist/src/integrations/codebase-context.js.map +1 -1
  32. package/dist/src/integrations/economics.js +2 -2
  33. package/dist/src/integrations/economics.js.map +1 -1
  34. package/dist/src/integrations/file-change-tracker.d.ts +1 -0
  35. package/dist/src/integrations/file-change-tracker.d.ts.map +1 -1
  36. package/dist/src/integrations/file-change-tracker.js +30 -12
  37. package/dist/src/integrations/file-change-tracker.js.map +1 -1
  38. package/dist/src/integrations/graph-visualization.d.ts +72 -0
  39. package/dist/src/integrations/graph-visualization.d.ts.map +1 -0
  40. package/dist/src/integrations/graph-visualization.js +383 -0
  41. package/dist/src/integrations/graph-visualization.js.map +1 -0
  42. package/dist/src/integrations/index.d.ts +3 -1
  43. package/dist/src/integrations/index.d.ts.map +1 -1
  44. package/dist/src/integrations/index.js +4 -0
  45. package/dist/src/integrations/index.js.map +1 -1
  46. package/dist/src/integrations/memory.d.ts +8 -0
  47. package/dist/src/integrations/memory.d.ts.map +1 -1
  48. package/dist/src/integrations/memory.js +23 -0
  49. package/dist/src/integrations/memory.js.map +1 -1
  50. package/dist/src/integrations/pending-plan.d.ts +26 -1
  51. package/dist/src/integrations/pending-plan.d.ts.map +1 -1
  52. package/dist/src/integrations/pending-plan.js +131 -3
  53. package/dist/src/integrations/pending-plan.js.map +1 -1
  54. package/dist/src/integrations/planning.d.ts +19 -0
  55. package/dist/src/integrations/planning.d.ts.map +1 -1
  56. package/dist/src/integrations/planning.js +78 -4
  57. package/dist/src/integrations/planning.js.map +1 -1
  58. package/dist/src/integrations/safety.d.ts +4 -0
  59. package/dist/src/integrations/safety.d.ts.map +1 -1
  60. package/dist/src/integrations/safety.js +46 -7
  61. package/dist/src/integrations/safety.js.map +1 -1
  62. package/dist/src/integrations/sqlite-store.d.ts.map +1 -1
  63. package/dist/src/integrations/sqlite-store.js +24 -2
  64. package/dist/src/integrations/sqlite-store.js.map +1 -1
  65. package/dist/src/integrations/task-manager.d.ts +132 -0
  66. package/dist/src/integrations/task-manager.d.ts.map +1 -0
  67. package/dist/src/integrations/task-manager.js +309 -0
  68. package/dist/src/integrations/task-manager.js.map +1 -0
  69. package/dist/src/modes/tui.d.ts.map +1 -1
  70. package/dist/src/modes/tui.js +9 -0
  71. package/dist/src/modes/tui.js.map +1 -1
  72. package/dist/src/modes.d.ts +30 -0
  73. package/dist/src/modes.d.ts.map +1 -1
  74. package/dist/src/modes.js +130 -2
  75. package/dist/src/modes.js.map +1 -1
  76. package/dist/src/providers/adapters/openai.d.ts +46 -2
  77. package/dist/src/providers/adapters/openai.d.ts.map +1 -1
  78. package/dist/src/providers/adapters/openai.js +221 -21
  79. package/dist/src/providers/adapters/openai.js.map +1 -1
  80. package/dist/src/providers/llm-resilience.d.ts +8 -0
  81. package/dist/src/providers/llm-resilience.d.ts.map +1 -1
  82. package/dist/src/providers/llm-resilience.js +36 -0
  83. package/dist/src/providers/llm-resilience.js.map +1 -1
  84. package/dist/src/tools/agent.d.ts +38 -1
  85. package/dist/src/tools/agent.d.ts.map +1 -1
  86. package/dist/src/tools/agent.js +152 -2
  87. package/dist/src/tools/agent.js.map +1 -1
  88. package/dist/src/tools/file.d.ts.map +1 -1
  89. package/dist/src/tools/file.js +17 -3
  90. package/dist/src/tools/file.js.map +1 -1
  91. package/dist/src/tools/tasks.d.ts +32 -0
  92. package/dist/src/tools/tasks.d.ts.map +1 -0
  93. package/dist/src/tools/tasks.js +334 -0
  94. package/dist/src/tools/tasks.js.map +1 -0
  95. package/dist/src/tracing/trace-collector.d.ts +81 -0
  96. package/dist/src/tracing/trace-collector.d.ts.map +1 -1
  97. package/dist/src/tracing/trace-collector.js +216 -4
  98. package/dist/src/tracing/trace-collector.js.map +1 -1
  99. package/dist/src/tracing/types.d.ts +8 -0
  100. package/dist/src/tracing/types.d.ts.map +1 -1
  101. package/dist/src/tracing/types.js.map +1 -1
  102. package/dist/src/tui/app.d.ts.map +1 -1
  103. package/dist/src/tui/app.js +503 -90
  104. package/dist/src/tui/app.js.map +1 -1
  105. package/dist/src/tui/components/ActiveAgentsPanel.d.ts +45 -0
  106. package/dist/src/tui/components/ActiveAgentsPanel.d.ts.map +1 -0
  107. package/dist/src/tui/components/ActiveAgentsPanel.js +121 -0
  108. package/dist/src/tui/components/ActiveAgentsPanel.js.map +1 -0
  109. package/dist/src/tui/components/CollapsibleDiffView.d.ts +49 -0
  110. package/dist/src/tui/components/CollapsibleDiffView.d.ts.map +1 -0
  111. package/dist/src/tui/components/CollapsibleDiffView.js +302 -0
  112. package/dist/src/tui/components/CollapsibleDiffView.js.map +1 -0
  113. package/dist/src/tui/components/DiffView.d.ts +55 -0
  114. package/dist/src/tui/components/DiffView.d.ts.map +1 -0
  115. package/dist/src/tui/components/DiffView.js +356 -0
  116. package/dist/src/tui/components/DiffView.js.map +1 -0
  117. package/dist/src/tui/components/ErrorBoundary.d.ts +63 -0
  118. package/dist/src/tui/components/ErrorBoundary.d.ts.map +1 -0
  119. package/dist/src/tui/components/ErrorBoundary.js +88 -0
  120. package/dist/src/tui/components/ErrorBoundary.js.map +1 -0
  121. package/dist/src/tui/components/FileChangeSummary.d.ts +48 -0
  122. package/dist/src/tui/components/FileChangeSummary.d.ts.map +1 -0
  123. package/dist/src/tui/components/FileChangeSummary.js +152 -0
  124. package/dist/src/tui/components/FileChangeSummary.js.map +1 -0
  125. package/dist/src/tui/components/SideBySideDiff.d.ts +62 -0
  126. package/dist/src/tui/components/SideBySideDiff.d.ts.map +1 -0
  127. package/dist/src/tui/components/SideBySideDiff.js +320 -0
  128. package/dist/src/tui/components/SideBySideDiff.js.map +1 -0
  129. package/dist/src/tui/components/SyntaxText.d.ts +47 -0
  130. package/dist/src/tui/components/SyntaxText.d.ts.map +1 -0
  131. package/dist/src/tui/components/SyntaxText.js +43 -0
  132. package/dist/src/tui/components/SyntaxText.js.map +1 -0
  133. package/dist/src/tui/components/TasksPanel.d.ts +25 -0
  134. package/dist/src/tui/components/TasksPanel.d.ts.map +1 -0
  135. package/dist/src/tui/components/TasksPanel.js +101 -0
  136. package/dist/src/tui/components/TasksPanel.js.map +1 -0
  137. package/dist/src/tui/components/index.d.ts +8 -0
  138. package/dist/src/tui/components/index.d.ts.map +1 -1
  139. package/dist/src/tui/components/index.js +13 -0
  140. package/dist/src/tui/components/index.js.map +1 -1
  141. package/dist/src/tui/hooks/index.d.ts +7 -0
  142. package/dist/src/tui/hooks/index.d.ts.map +1 -0
  143. package/dist/src/tui/hooks/index.js +7 -0
  144. package/dist/src/tui/hooks/index.js.map +1 -0
  145. package/dist/src/tui/hooks/useMessagePruning.d.ts +114 -0
  146. package/dist/src/tui/hooks/useMessagePruning.d.ts.map +1 -0
  147. package/dist/src/tui/hooks/useMessagePruning.js +127 -0
  148. package/dist/src/tui/hooks/useMessagePruning.js.map +1 -0
  149. package/dist/src/tui/index.d.ts +3 -0
  150. package/dist/src/tui/index.d.ts.map +1 -1
  151. package/dist/src/tui/index.js +9 -0
  152. package/dist/src/tui/index.js.map +1 -1
  153. package/dist/src/tui/syntax/index.d.ts +12 -0
  154. package/dist/src/tui/syntax/index.d.ts.map +1 -0
  155. package/dist/src/tui/syntax/index.js +14 -0
  156. package/dist/src/tui/syntax/index.js.map +1 -0
  157. package/dist/src/tui/syntax/languages/bash.d.ts +8 -0
  158. package/dist/src/tui/syntax/languages/bash.d.ts.map +1 -0
  159. package/dist/src/tui/syntax/languages/bash.js +296 -0
  160. package/dist/src/tui/syntax/languages/bash.js.map +1 -0
  161. package/dist/src/tui/syntax/languages/javascript.d.ts +8 -0
  162. package/dist/src/tui/syntax/languages/javascript.d.ts.map +1 -0
  163. package/dist/src/tui/syntax/languages/javascript.js +253 -0
  164. package/dist/src/tui/syntax/languages/javascript.js.map +1 -0
  165. package/dist/src/tui/syntax/languages/json.d.ts +8 -0
  166. package/dist/src/tui/syntax/languages/json.d.ts.map +1 -0
  167. package/dist/src/tui/syntax/languages/json.js +112 -0
  168. package/dist/src/tui/syntax/languages/json.js.map +1 -0
  169. package/dist/src/tui/syntax/languages/python.d.ts +8 -0
  170. package/dist/src/tui/syntax/languages/python.d.ts.map +1 -0
  171. package/dist/src/tui/syntax/languages/python.js +232 -0
  172. package/dist/src/tui/syntax/languages/python.js.map +1 -0
  173. package/dist/src/tui/syntax/lexer.d.ts +51 -0
  174. package/dist/src/tui/syntax/lexer.d.ts.map +1 -0
  175. package/dist/src/tui/syntax/lexer.js +131 -0
  176. package/dist/src/tui/syntax/lexer.js.map +1 -0
  177. package/dist/src/tui/utils/index.d.ts +7 -0
  178. package/dist/src/tui/utils/index.d.ts.map +1 -0
  179. package/dist/src/tui/utils/index.js +7 -0
  180. package/dist/src/tui/utils/index.js.map +1 -0
  181. package/dist/src/tui/utils/keyboard.d.ts +123 -0
  182. package/dist/src/tui/utils/keyboard.d.ts.map +1 -0
  183. package/dist/src/tui/utils/keyboard.js +185 -0
  184. package/dist/src/tui/utils/keyboard.js.map +1 -0
  185. package/dist/src/types.d.ts +73 -0
  186. package/dist/src/types.d.ts.map +1 -1
  187. package/package.json +4 -1
package/dist/src/agent.js CHANGED
@@ -18,17 +18,19 @@
18
18
  * - Execution Policies (Lesson 23)
19
19
  * - Thread Management (Lesson 24)
20
20
  */
21
- import { buildConfig, isFeatureEnabled, getEnabledFeatures, } from './defaults.js';
22
- import { createModeManager, formatModeList, parseMode, } from './modes.js';
21
+ import { buildConfig, isFeatureEnabled, getEnabledFeatures, getSubagentTimeout, getSubagentMaxIterations, } from './defaults.js';
22
+ import { createModeManager, formatModeList, parseMode, calculateTaskSimilarity, SUBAGENT_PLAN_MODE_ADDITION, } from './modes.js';
23
23
  import { createLSPFileTools, } from './agent-tools/index.js';
24
- import { HookManager, MemoryManager, PlanningManager, ObservabilityManager, SafetyManager, RoutingManager, MultiAgentManager, ReActManager, ExecutionPolicyManager, ThreadManager, RulesManager, DEFAULT_RULE_SOURCES, ExecutionEconomicsManager, STANDARD_BUDGET, AgentRegistry, filterToolsForAgent, formatAgentList, createCancellationManager, isCancellationError, createTimeoutToken, createLinkedToken, race, createResourceManager, createLSPManager, createSemanticCacheManager, createSkillManager, formatSkillList, createContextEngineering, stableStringify, createCodebaseContext, buildContextFromChunks, createPendingPlanManager, createInteractivePlanner, createRecursiveContext, createLearningStore, createCompactor, createAutoCompactionManager, createFileChangeTracker, createCapabilitiesRegistry, } from './integrations/index.js';
24
+ import { HookManager, MemoryManager, PlanningManager, ObservabilityManager, SafetyManager, RoutingManager, MultiAgentManager, ReActManager, ExecutionPolicyManager, ThreadManager, RulesManager, DEFAULT_RULE_SOURCES, ExecutionEconomicsManager, STANDARD_BUDGET, AgentRegistry, filterToolsForAgent, formatAgentList, createCancellationManager, isCancellationError, createTimeoutToken, createLinkedToken, race, createResourceManager, createLSPManager, createSemanticCacheManager, createSkillManager, formatSkillList, createContextEngineering, stableStringify, createCodebaseContext, buildContextFromChunks, createPendingPlanManager, createInteractivePlanner, createRecursiveContext, createLearningStore, createCompactor, createAutoCompactionManager, createFileChangeTracker, createCapabilitiesRegistry, createSharedBlackboard, createTaskManager, } from './integrations/index.js';
25
25
  // Lesson 26: Tracing & Evaluation integration
26
26
  import { createTraceCollector } from './tracing/trace-collector.js';
27
27
  // Model registry for context window limits
28
28
  import { modelRegistry } from './costs/index.js';
29
29
  import { getModelContextLength } from './integrations/openrouter-pricing.js';
30
- // Spawn agent tool for LLM-driven subagent delegation
31
- import { createBoundSpawnAgentTool } from './tools/agent.js';
30
+ // Spawn agent tools for LLM-driven subagent delegation
31
+ import { createBoundSpawnAgentTool, createBoundSpawnAgentsParallelTool, } from './tools/agent.js';
32
+ // Task tools for Claude Code-style task management
33
+ import { createTaskTools, } from './tools/tasks.js';
32
34
  // =============================================================================
33
35
  // PRODUCTION AGENT
34
36
  // =============================================================================
@@ -71,6 +73,17 @@ export class ProductionAgent {
71
73
  fileChangeTracker = null;
72
74
  capabilitiesRegistry = null;
73
75
  toolResolver = null;
76
+ blackboard = null;
77
+ taskManager = null;
78
+ // Duplicate spawn prevention - tracks recently spawned tasks to prevent doom loops
79
+ // Map<taskKey, { timestamp: number; result: string; queuedChanges: number }>
80
+ spawnedTasks = new Map();
81
+ static SPAWN_DEDUP_WINDOW_MS = 60000; // 60 seconds
82
+ // Parent iteration tracking for total budget calculation
83
+ parentIterations = 0;
84
+ // External cancellation token (for subagent timeout propagation)
85
+ // When set, the agent will check this token in addition to its own cancellation manager
86
+ externalCancellationToken = null;
74
87
  // Initialization tracking
75
88
  initPromises = [];
76
89
  initComplete = false;
@@ -106,6 +119,18 @@ export class ProductionAgent {
106
119
  this.modeManager = createModeManager(this.config.tools);
107
120
  // Initialize pending plan manager for plan mode write interception
108
121
  this.pendingPlanManager = createPendingPlanManager();
122
+ // Shared Blackboard - enables coordination between parallel subagents
123
+ // Subagents inherit parent's blackboard; parent agents create their own
124
+ if (userConfig.blackboard) {
125
+ this.blackboard = userConfig.blackboard;
126
+ }
127
+ else if (this.config.subagent !== false) {
128
+ this.blackboard = createSharedBlackboard({
129
+ maxFindings: 500,
130
+ defaultClaimTTL: 120000, // 2 minutes for file claims
131
+ deduplicateFindings: true,
132
+ });
133
+ }
109
134
  // Initialize enabled features
110
135
  this.initializeFeatures();
111
136
  }
@@ -215,8 +240,25 @@ export class ProductionAgent {
215
240
  console.warn('[ProductionAgent] Failed to load user agents:', err);
216
241
  }));
217
242
  // Register spawn_agent tool so LLM can delegate to subagents
218
- const boundSpawnTool = createBoundSpawnAgentTool((name, task) => this.spawnAgent(name, task));
243
+ const boundSpawnTool = createBoundSpawnAgentTool((name, task, constraints) => this.spawnAgent(name, task, constraints));
219
244
  this.tools.set(boundSpawnTool.name, boundSpawnTool);
245
+ // Register spawn_agents_parallel tool for parallel subagent execution
246
+ const boundParallelSpawnTool = createBoundSpawnAgentsParallelTool((tasks) => this.spawnAgentsParallel(tasks));
247
+ this.tools.set(boundParallelSpawnTool.name, boundParallelSpawnTool);
248
+ // Task Manager - Claude Code-style task system for coordination
249
+ this.taskManager = createTaskManager();
250
+ // Forward task events
251
+ this.taskManager.on('task.created', (data) => {
252
+ this.emit({ type: 'task.created', task: data.task });
253
+ });
254
+ this.taskManager.on('task.updated', (data) => {
255
+ this.emit({ type: 'task.updated', task: data.task });
256
+ });
257
+ // Register task tools
258
+ const taskTools = createTaskTools(this.taskManager);
259
+ for (const tool of taskTools) {
260
+ this.tools.set(tool.name, tool);
261
+ }
220
262
  // Cancellation Support
221
263
  if (isFeatureEnabled(this.config.cancellation)) {
222
264
  this.cancellation = createCancellationManager();
@@ -313,6 +355,11 @@ export class ProductionAgent {
313
355
  cacheResults: true,
314
356
  cacheTTL: 5 * 60 * 1000, // 5 minutes
315
357
  });
358
+ // Connect LSP manager to codebase context for enhanced code selection
359
+ // This enables LSP-based relevance boosting (Phase 4.1)
360
+ if (this.lspManager) {
361
+ this.codebaseContext.setLSPManager(this.lspManager);
362
+ }
316
363
  }
317
364
  // Forward context engineering events
318
365
  this.contextEngineering.on(event => {
@@ -792,9 +839,13 @@ export class ProductionAgent {
792
839
  this.observability?.logger?.warn('Plan task failed', { taskId: currentTask.id });
793
840
  // Continue with other tasks if possible
794
841
  }
795
- // Check iteration limit
796
- if (this.state.iteration >= this.config.maxIterations) {
797
- this.observability?.logger?.warn('Max iterations reached');
842
+ // Check iteration limit (using total iterations to account for parent)
843
+ if (this.getTotalIterations() >= this.config.maxIterations) {
844
+ this.observability?.logger?.warn('Max iterations reached', {
845
+ iteration: this.state.iteration,
846
+ parentIterations: this.parentIterations,
847
+ total: this.getTotalIterations(),
848
+ });
798
849
  break;
799
850
  }
800
851
  }
@@ -832,10 +883,16 @@ export class ProductionAgent {
832
883
  });
833
884
  // =======================================================================
834
885
  // CANCELLATION CHECK
886
+ // Checks both internal cancellation (ESC key) and external cancellation
887
+ // (parent timeout when this agent is a subagent)
835
888
  // =======================================================================
836
889
  if (this.cancellation?.isCancelled) {
837
890
  this.cancellation.token.throwIfCancellationRequested();
838
891
  }
892
+ // Also check external cancellation token (from parent when spawned as subagent)
893
+ if (this.externalCancellationToken?.isCancellationRequested) {
894
+ this.externalCancellationToken.throwIfCancellationRequested();
895
+ }
839
896
  // =======================================================================
840
897
  // RESOURCE CHECK - system resource limits
841
898
  // =======================================================================
@@ -945,7 +1002,11 @@ export class ProductionAgent {
945
1002
  });
946
1003
  // Emit appropriate event
947
1004
  if (budgetCheck.budgetType === 'iterations') {
948
- this.emit({ type: 'error', error: `Max iterations reached (${this.state.iteration})` });
1005
+ const totalIter = this.getTotalIterations();
1006
+ const iterMsg = this.parentIterations > 0
1007
+ ? `${this.state.iteration} + ${this.parentIterations} parent = ${totalIter}`
1008
+ : `${this.state.iteration}`;
1009
+ this.emit({ type: 'error', error: `Max iterations reached (${iterMsg})` });
949
1010
  }
950
1011
  else {
951
1012
  this.emit({ type: 'error', error: budgetCheck.reason || 'Budget exceeded' });
@@ -963,8 +1024,13 @@ export class ProductionAgent {
963
1024
  }
964
1025
  else {
965
1026
  // Fallback to simple iteration check if economics not available
966
- if (this.state.iteration >= this.config.maxIterations) {
967
- this.observability?.logger?.warn('Max iterations reached');
1027
+ // Use getTotalIterations() to account for parent iterations (subagent hierarchy)
1028
+ if (this.getTotalIterations() >= this.config.maxIterations) {
1029
+ this.observability?.logger?.warn('Max iterations reached', {
1030
+ iteration: this.state.iteration,
1031
+ parentIterations: this.parentIterations,
1032
+ total: this.getTotalIterations(),
1033
+ });
968
1034
  break;
969
1035
  }
970
1036
  }
@@ -1167,6 +1233,15 @@ export class ProductionAgent {
1167
1233
  messages.push(assistantMessage);
1168
1234
  this.state.messages.push(assistantMessage);
1169
1235
  lastResponse = response.content;
1236
+ // In plan mode: capture exploration findings as we go (not just at the end)
1237
+ // This ensures we collect context from exploration iterations before writes are queued
1238
+ if (this.modeManager.getMode() === 'plan' && response.content && response.content.length > 50) {
1239
+ const hasReadOnlyTools = response.toolCalls?.every(tc => ['read_file', 'list_files', 'glob', 'grep', 'search', 'mcp_'].some(prefix => tc.name.startsWith(prefix) || tc.name === prefix));
1240
+ // Capture substantive exploration content (not just "let me read..." responses)
1241
+ if (hasReadOnlyTools && !response.content.match(/^(Let me|I'll|I will|I need to|First,)/i)) {
1242
+ this.pendingPlanManager.appendExplorationFinding(response.content.slice(0, 1000));
1243
+ }
1244
+ }
1170
1245
  // Check for tool calls
1171
1246
  // When forceTextOnly is set (max iterations reached), ignore any tool calls
1172
1247
  const hasToolCalls = response.toolCalls && response.toolCalls.length > 0;
@@ -1182,6 +1257,14 @@ export class ProductionAgent {
1182
1257
  // The model has "consumed" the tool outputs and produced a response,
1183
1258
  // so we can replace verbose outputs with compact summaries
1184
1259
  this.compactToolOutputs();
1260
+ // In plan mode: capture exploration summary from the final response
1261
+ // This provides context for what was learned during exploration before proposing changes
1262
+ if (this.modeManager.getMode() === 'plan' && this.pendingPlanManager.hasPendingPlan()) {
1263
+ const explorationContent = response.content || '';
1264
+ if (explorationContent.length > 0) {
1265
+ this.pendingPlanManager.setExplorationSummary(explorationContent);
1266
+ }
1267
+ }
1185
1268
  // Final validation: warn if response is still empty after all retries
1186
1269
  if (!response.content || response.content.length === 0) {
1187
1270
  this.observability?.logger?.error('Agent finished with empty response after all retries', {
@@ -1683,11 +1766,8 @@ export class ProductionAgent {
1683
1766
  // =====================================================================
1684
1767
  // In plan mode, intercept write operations and queue them as proposed changes
1685
1768
  if (this.modeManager.shouldInterceptTool(toolCall.name, toolCall.arguments)) {
1686
- // Extract reason from context - use last assistant message or generate one
1687
- const lastAssistantMsg = [...this.state.messages].reverse().find(m => m.role === 'assistant');
1688
- const reason = typeof lastAssistantMsg?.content === 'string'
1689
- ? lastAssistantMsg.content.slice(0, 200)
1690
- : `Proposed change: ${toolCall.name}`;
1769
+ // Extract contextual reasoning instead of simple truncation
1770
+ const reason = this.extractChangeReasoning(toolCall, this.state.messages);
1691
1771
  // Start a new plan if needed
1692
1772
  if (!this.pendingPlanManager.hasPendingPlan()) {
1693
1773
  const lastUserMsg = [...this.state.messages].reverse().find(m => m.role === 'user');
@@ -1823,10 +1903,45 @@ export class ProductionAgent {
1823
1903
  if (process.env.DEBUG && toolCall.name.startsWith('mcp_') && wasPreloaded) {
1824
1904
  console.log(` ✓ Using pre-loaded MCP tool: ${toolCall.name}`);
1825
1905
  }
1906
+ // =====================================================================
1907
+ // BLACKBOARD FILE COORDINATION (Parallel Subagent Support)
1908
+ // =====================================================================
1909
+ // Claim file resources before write operations to prevent conflicts
1910
+ if (this.blackboard && (toolCall.name === 'write_file' || toolCall.name === 'edit_file')) {
1911
+ const args = toolCall.arguments;
1912
+ const filePath = String(args.path || args.file_path || '');
1913
+ if (filePath) {
1914
+ const agentId = this.config.systemPrompt?.slice(0, 50) || 'agent';
1915
+ const claimed = this.blackboard.claim(filePath, agentId, 'write', {
1916
+ ttl: 60000, // 1 minute claim
1917
+ intent: `${toolCall.name}: ${filePath}`,
1918
+ });
1919
+ if (!claimed) {
1920
+ const existingClaim = this.blackboard.getClaim(filePath);
1921
+ throw new Error(`File "${filePath}" is being edited by another agent (${existingClaim?.agentId || 'unknown'}). ` +
1922
+ `Wait for the other agent to complete or choose a different file.`);
1923
+ }
1924
+ }
1925
+ }
1826
1926
  // Execute tool (with sandbox if available)
1827
1927
  let result;
1828
1928
  if (this.safety?.sandbox) {
1829
- result = await this.safety.sandbox.executeWithLimits(() => tool.execute(toolCall.arguments));
1929
+ // CRITICAL: spawn_agent and spawn_agents_parallel need MUCH longer timeouts
1930
+ // The default 60s sandbox timeout would kill subagents prematurely
1931
+ // Subagents may run for minutes (per their own timeout config)
1932
+ const isSpawnAgent = toolCall.name === 'spawn_agent';
1933
+ const isSpawnParallel = toolCall.name === 'spawn_agents_parallel';
1934
+ const isSubagentTool = isSpawnAgent || isSpawnParallel;
1935
+ const subagentConfig = this.config.subagent;
1936
+ const hasSubagentConfig = subagentConfig !== false && subagentConfig !== undefined;
1937
+ const subagentTimeout = hasSubagentConfig
1938
+ ? subagentConfig.defaultTimeout ?? 600000 // 10 min default
1939
+ : 600000;
1940
+ // Use subagent timeout + buffer for spawn tools, default for others
1941
+ // For spawn_agents_parallel, multiply by number of agents (they run in parallel,
1942
+ // but the total wall-clock time should still allow the slowest agent to complete)
1943
+ const toolTimeout = isSubagentTool ? subagentTimeout + 30000 : undefined;
1944
+ result = await this.safety.sandbox.executeWithLimits(() => tool.execute(toolCall.arguments), toolTimeout);
1830
1945
  }
1831
1946
  else {
1832
1947
  result = await tool.execute(toolCall.arguments);
@@ -1859,6 +1974,15 @@ export class ProductionAgent {
1859
1974
  callId: toolCall.id,
1860
1975
  result,
1861
1976
  });
1977
+ // Release blackboard claim after successful file write
1978
+ if (this.blackboard && (toolCall.name === 'write_file' || toolCall.name === 'edit_file')) {
1979
+ const args = toolCall.arguments;
1980
+ const filePath = String(args.path || args.file_path || '');
1981
+ if (filePath) {
1982
+ const agentId = this.config.systemPrompt?.slice(0, 50) || 'agent';
1983
+ this.blackboard.release(filePath, agentId);
1984
+ }
1985
+ }
1862
1986
  this.observability?.tracer?.endSpan(spanId);
1863
1987
  }
1864
1988
  catch (err) {
@@ -1895,8 +2019,32 @@ export class ProductionAgent {
1895
2019
  }
1896
2020
  return results;
1897
2021
  }
2022
+ /**
2023
+ * Get recently modified file paths from the file change tracker.
2024
+ * Returns paths of files modified in this session (not undone).
2025
+ */
2026
+ getRecentlyModifiedFiles(limit = 5) {
2027
+ if (!this.fileChangeTracker)
2028
+ return [];
2029
+ try {
2030
+ const changes = this.fileChangeTracker.getChanges();
2031
+ const recentFiles = new Set();
2032
+ // Iterate in reverse to get most recent first
2033
+ for (let i = changes.length - 1; i >= 0 && recentFiles.size < limit; i--) {
2034
+ const change = changes[i];
2035
+ if (!change.isUndone) {
2036
+ recentFiles.add(change.filePath);
2037
+ }
2038
+ }
2039
+ return Array.from(recentFiles);
2040
+ }
2041
+ catch {
2042
+ return [];
2043
+ }
2044
+ }
1898
2045
  /**
1899
2046
  * Select relevant code synchronously using cached repo analysis.
2047
+ * Uses LSP-enhanced selection when available to boost related files.
1900
2048
  * Returns empty result if analysis hasn't been run yet.
1901
2049
  */
1902
2050
  selectRelevantCodeSync(task, maxTokens) {
@@ -1907,6 +2055,30 @@ export class ProductionAgent {
1907
2055
  if (!repoMap) {
1908
2056
  return { chunks: [], totalTokens: 0 };
1909
2057
  }
2058
+ // Get recently modified files for LSP-enhanced selection
2059
+ const recentFiles = this.getRecentlyModifiedFiles();
2060
+ const priorityFileSet = new Set(recentFiles);
2061
+ // LSP-related files (files that reference or are referenced by recent files)
2062
+ const lspRelatedFiles = new Set();
2063
+ if (this.codebaseContext.hasActiveLSP() && recentFiles.length > 0) {
2064
+ // Use dependency graph as a synchronous proxy for LSP relationships
2065
+ for (const file of recentFiles) {
2066
+ // Files that this file depends on
2067
+ const deps = repoMap.dependencyGraph.get(file);
2068
+ if (deps) {
2069
+ for (const dep of deps) {
2070
+ lspRelatedFiles.add(dep);
2071
+ }
2072
+ }
2073
+ // Files that depend on this file
2074
+ const reverseDeps = repoMap.reverseDependencyGraph.get(file);
2075
+ if (reverseDeps) {
2076
+ for (const dep of reverseDeps) {
2077
+ lspRelatedFiles.add(dep);
2078
+ }
2079
+ }
2080
+ }
2081
+ }
1910
2082
  // Get all chunks and score by relevance
1911
2083
  const allChunks = Array.from(repoMap.chunks.values());
1912
2084
  const taskLower = task.toLowerCase();
@@ -1930,13 +2102,22 @@ export class ProductionAgent {
1930
2102
  }
1931
2103
  }
1932
2104
  // Combine with base importance
1933
- const combinedScore = chunk.importance * 0.4 + Math.min(relevance, 1) * 0.6;
2105
+ let combinedScore = chunk.importance * 0.4 + Math.min(relevance, 1) * 0.6;
2106
+ // Boost recently modified files (highest priority)
2107
+ if (priorityFileSet.has(chunk.filePath)) {
2108
+ combinedScore = Math.min(1.0, combinedScore + 0.4);
2109
+ }
2110
+ // Boost LSP-related files (files connected to recent edits)
2111
+ else if (lspRelatedFiles.has(chunk.filePath)) {
2112
+ combinedScore = Math.min(1.0, combinedScore + 0.25);
2113
+ }
1934
2114
  return { chunk, score: combinedScore };
1935
2115
  });
1936
2116
  // Sort by score and select within budget
1937
2117
  scored.sort((a, b) => b.score - a.score);
1938
2118
  const selected = [];
1939
2119
  let totalTokens = 0;
2120
+ const boostedFiles = [];
1940
2121
  for (const { chunk, score } of scored) {
1941
2122
  if (score < 0.1)
1942
2123
  continue; // Skip very low relevance
@@ -1949,8 +2130,16 @@ export class ProductionAgent {
1949
2130
  importance: score,
1950
2131
  });
1951
2132
  totalTokens += chunk.tokenCount;
2133
+ // Track which files were boosted by LSP/dependency relationships
2134
+ if (lspRelatedFiles.has(chunk.filePath)) {
2135
+ boostedFiles.push(chunk.filePath);
2136
+ }
1952
2137
  }
1953
- return { chunks: selected, totalTokens };
2138
+ return {
2139
+ chunks: selected,
2140
+ totalTokens,
2141
+ lspBoostedFiles: boostedFiles.length > 0 ? boostedFiles : undefined,
2142
+ };
1954
2143
  }
1955
2144
  /**
1956
2145
  * Analyze the codebase (async). Call this once at startup for optimal performance.
@@ -2032,6 +2221,57 @@ export class ProductionAgent {
2032
2221
  // Generic
2033
2222
  return `Args: ${JSON.stringify(args).slice(0, 100)}...`;
2034
2223
  }
2224
+ /**
2225
+ * Extract contextual reasoning for a proposed change in plan mode.
2226
+ * Looks at recent assistant messages to find relevant explanation.
2227
+ * Returns a more complete reason than simple truncation.
2228
+ */
2229
+ extractChangeReasoning(toolCall, messages) {
2230
+ // Get last few assistant messages (most recent first)
2231
+ const assistantMsgs = messages
2232
+ .filter(m => m.role === 'assistant' && typeof m.content === 'string')
2233
+ .slice(-3)
2234
+ .reverse();
2235
+ if (assistantMsgs.length === 0) {
2236
+ return `Proposed change: ${toolCall.name}`;
2237
+ }
2238
+ // Use the most recent assistant message
2239
+ const lastMsg = assistantMsgs[0];
2240
+ const content = lastMsg.content;
2241
+ // For spawn_agent, the task itself is usually the reason
2242
+ if (toolCall.name === 'spawn_agent') {
2243
+ const args = toolCall.arguments;
2244
+ const task = String(args.task || args.prompt || args.goal || '');
2245
+ if (task.length > 0) {
2246
+ // Use first paragraph or 500 chars of task as reason
2247
+ const firstPara = task.split(/\n\n/)[0];
2248
+ return firstPara.length > 500 ? firstPara.slice(0, 500) + '...' : firstPara;
2249
+ }
2250
+ }
2251
+ // For file operations, look for context about the file
2252
+ if (['write_file', 'edit_file'].includes(toolCall.name)) {
2253
+ const args = toolCall.arguments;
2254
+ const path = String(args.path || args.file_path || '');
2255
+ // Look for mentions of this file in the assistant's explanation
2256
+ if (path && content.toLowerCase().includes(path.toLowerCase().split('/').pop() || '')) {
2257
+ // Extract the sentence(s) mentioning this file
2258
+ const sentences = content.split(/[.!?\n]+/).filter(s => s.toLowerCase().includes(path.toLowerCase().split('/').pop() || ''));
2259
+ if (sentences.length > 0) {
2260
+ const relevant = sentences.slice(0, 2).join('. ').trim();
2261
+ return relevant.length > 500 ? relevant.slice(0, 500) + '...' : relevant;
2262
+ }
2263
+ }
2264
+ }
2265
+ // Fallback: use first 500 chars instead of 200
2266
+ // Look for the first meaningful paragraph/section
2267
+ const paragraphs = content.split(/\n\n+/).filter(p => p.trim().length > 20);
2268
+ if (paragraphs.length > 0) {
2269
+ const firstPara = paragraphs[0].trim();
2270
+ return firstPara.length > 500 ? firstPara.slice(0, 500) + '...' : firstPara;
2271
+ }
2272
+ // Ultimate fallback
2273
+ return content.length > 500 ? content.slice(0, 500) + '...' : content;
2274
+ }
2035
2275
  /**
2036
2276
  * Update memory statistics.
2037
2277
  * Memory stats are retrieved via memory manager, not stored in state.
@@ -2084,6 +2324,13 @@ export class ProductionAgent {
2084
2324
  getTraceCollector() {
2085
2325
  return this.traceCollector;
2086
2326
  }
2327
+ /**
2328
+ * Set a trace collector for this agent.
2329
+ * Used for subagents to share the parent's trace collector (with subagent context).
2330
+ */
2331
+ setTraceCollector(collector) {
2332
+ this.traceCollector = collector;
2333
+ }
2087
2334
  /**
2088
2335
  * Get the learning store for cross-session learning.
2089
2336
  * Returns null if learning store is not enabled.
@@ -2603,6 +2850,11 @@ export class ProductionAgent {
2603
2850
  if (!this.threadManager) {
2604
2851
  throw new Error('Thread management not enabled. Enable it in config to use createCheckpoint()');
2605
2852
  }
2853
+ // CRITICAL: Sync current state.messages to threadManager before checkpoint
2854
+ // The run() method adds messages directly to this.state.messages but doesn't sync
2855
+ // to threadManager, so thread.messages would be empty without this sync
2856
+ const thread = this.threadManager.getActiveThread();
2857
+ thread.messages = [...this.state.messages];
2606
2858
  const checkpoint = this.threadManager.createCheckpoint({
2607
2859
  label,
2608
2860
  agentState: this.state,
@@ -2777,8 +3029,12 @@ export class ProductionAgent {
2777
3029
  /**
2778
3030
  * Spawn an agent to execute a task.
2779
3031
  * Returns the result when the agent completes.
3032
+ *
3033
+ * @param agentName - Name of the agent to spawn (researcher, coder, etc.)
3034
+ * @param task - The task description for the agent
3035
+ * @param constraints - Optional constraints to keep the subagent focused
2780
3036
  */
2781
- async spawnAgent(agentName, task) {
3037
+ async spawnAgent(agentName, task, constraints) {
2782
3038
  if (!this.agentRegistry) {
2783
3039
  return {
2784
3040
  success: false,
@@ -2794,6 +3050,65 @@ export class ProductionAgent {
2794
3050
  metrics: { tokens: 0, duration: 0, toolCalls: 0 },
2795
3051
  };
2796
3052
  }
3053
+ // DUPLICATE SPAWN PREVENTION with SEMANTIC SIMILARITY
3054
+ // First try exact string match, then check semantic similarity for similar tasks
3055
+ const SEMANTIC_SIMILARITY_THRESHOLD = 0.75; // 75% similarity = duplicate
3056
+ const taskKey = `${agentName}:${task.slice(0, 150).toLowerCase().replace(/\s+/g, ' ').trim()}`;
3057
+ const now = Date.now();
3058
+ // Clean up old entries (older than dedup window)
3059
+ for (const [key, entry] of this.spawnedTasks.entries()) {
3060
+ if (now - entry.timestamp > ProductionAgent.SPAWN_DEDUP_WINDOW_MS) {
3061
+ this.spawnedTasks.delete(key);
3062
+ }
3063
+ }
3064
+ // Check for exact match first
3065
+ let existingMatch = this.spawnedTasks.get(taskKey);
3066
+ let matchType = 'exact';
3067
+ // If no exact match, check for semantic similarity among same agent's tasks
3068
+ if (!existingMatch) {
3069
+ for (const [key, entry] of this.spawnedTasks.entries()) {
3070
+ // Only compare tasks from the same agent type
3071
+ if (!key.startsWith(`${agentName}:`))
3072
+ continue;
3073
+ if (now - entry.timestamp >= ProductionAgent.SPAWN_DEDUP_WINDOW_MS)
3074
+ continue;
3075
+ // Extract the task portion from the key
3076
+ const existingTask = key.slice(agentName.length + 1);
3077
+ const similarity = calculateTaskSimilarity(task, existingTask);
3078
+ if (similarity >= SEMANTIC_SIMILARITY_THRESHOLD) {
3079
+ existingMatch = entry;
3080
+ matchType = 'semantic';
3081
+ this.observability?.logger?.debug('Semantic duplicate detected', {
3082
+ agent: agentName,
3083
+ newTask: task.slice(0, 80),
3084
+ existingTask: existingTask.slice(0, 80),
3085
+ similarity: (similarity * 100).toFixed(1) + '%',
3086
+ });
3087
+ break;
3088
+ }
3089
+ }
3090
+ }
3091
+ if (existingMatch && now - existingMatch.timestamp < ProductionAgent.SPAWN_DEDUP_WINDOW_MS) {
3092
+ // Same or semantically similar task spawned within the dedup window
3093
+ this.observability?.logger?.warn('Duplicate spawn prevented', {
3094
+ agent: agentName,
3095
+ task: task.slice(0, 100),
3096
+ matchType,
3097
+ originalTimestamp: existingMatch.timestamp,
3098
+ elapsedMs: now - existingMatch.timestamp,
3099
+ });
3100
+ const duplicateMessage = `[DUPLICATE SPAWN PREVENTED${matchType === 'semantic' ? ' - SEMANTIC MATCH' : ''}]\n` +
3101
+ `This task was already spawned ${Math.round((now - existingMatch.timestamp) / 1000)}s ago.\n` +
3102
+ `${existingMatch.queuedChanges > 0
3103
+ ? `The previous spawn queued ${existingMatch.queuedChanges} change(s) to the pending plan.\n` +
3104
+ `These changes are already in your plan - do NOT spawn again.\n`
3105
+ : ''}Previous result summary:\n${existingMatch.result.slice(0, 500)}`;
3106
+ return {
3107
+ success: true, // Mark as success since original task completed
3108
+ output: duplicateMessage,
3109
+ metrics: { tokens: 0, duration: 0, toolCalls: 0 },
3110
+ };
3111
+ }
2797
3112
  this.emit({ type: 'agent.spawn', agentId: `spawn-${Date.now()}`, name: agentName, task });
2798
3113
  this.observability?.logger?.info('Spawning agent', { name: agentName, task });
2799
3114
  const startTime = Date.now();
@@ -2807,16 +3122,88 @@ export class ProductionAgent {
2807
3122
  const resolvedModel = (agentDef.model && agentDef.model.includes('/'))
2808
3123
  ? agentDef.model
2809
3124
  : this.config.model;
2810
- // Get subagent config with defaults
2811
- // Note: subagent config is SubagentConfig | false from buildConfig
3125
+ // Get subagent config with agent-type-specific timeouts and iteration limits
3126
+ // Uses dynamic configuration based on agent type (researcher needs more time than reviewer)
2812
3127
  const subagentConfig = this.config.subagent;
2813
3128
  const hasSubagentConfig = subagentConfig !== false && subagentConfig !== undefined;
2814
- const defaultMaxIterations = hasSubagentConfig
2815
- ? subagentConfig.defaultMaxIterations ?? 10
2816
- : 10;
2817
- const subagentTimeout = hasSubagentConfig
2818
- ? subagentConfig.defaultTimeout ?? 120000
2819
- : 120000;
3129
+ // Agent-type-specific timeout: researchers get 5min, reviewers get 2min, etc.
3130
+ const agentTypeTimeout = getSubagentTimeout(agentName);
3131
+ const configTimeout = hasSubagentConfig
3132
+ ? subagentConfig.defaultTimeout
3133
+ : undefined;
3134
+ const subagentTimeout = configTimeout ?? agentTypeTimeout;
3135
+ // Agent-type-specific iteration limit: researchers get 25, documenters get 10, etc.
3136
+ const agentTypeMaxIter = getSubagentMaxIterations(agentName);
3137
+ const configMaxIter = hasSubagentConfig
3138
+ ? subagentConfig.defaultMaxIterations
3139
+ : undefined;
3140
+ const defaultMaxIterations = agentDef.maxIterations ?? configMaxIter ?? agentTypeMaxIter;
3141
+ // BLACKBOARD CONTEXT INJECTION
3142
+ // Gather relevant context from the blackboard for the subagent
3143
+ let blackboardContext = '';
3144
+ const parentAgentId = `parent-${Date.now()}`;
3145
+ if (this.blackboard) {
3146
+ // Post parent's exploration context before spawning
3147
+ this.blackboard.post(parentAgentId, {
3148
+ topic: 'spawn.parent_context',
3149
+ content: `Parent spawning ${agentName} for task: ${task.slice(0, 200)}`,
3150
+ type: 'progress',
3151
+ confidence: 1,
3152
+ metadata: { agentName, taskPreview: task.slice(0, 100) },
3153
+ });
3154
+ // Gather recent findings that might help the subagent
3155
+ const recentFindings = this.blackboard.query({
3156
+ limit: 5,
3157
+ types: ['discovery', 'analysis', 'progress'],
3158
+ minConfidence: 0.7,
3159
+ });
3160
+ if (recentFindings.length > 0) {
3161
+ const findingsSummary = recentFindings
3162
+ .map(f => `- [${f.agentId}] ${f.topic}: ${f.content.slice(0, 150)}${f.content.length > 150 ? '...' : ''}`)
3163
+ .join('\n');
3164
+ blackboardContext = `\n\n**BLACKBOARD CONTEXT (from parent/sibling agents):**\n${findingsSummary}\n`;
3165
+ }
3166
+ }
3167
+ // Check for files already being modified in parent's pending plan
3168
+ const currentPlan = this.pendingPlanManager.getPendingPlan();
3169
+ if (currentPlan && currentPlan.proposedChanges.length > 0) {
3170
+ const pendingFiles = currentPlan.proposedChanges
3171
+ .filter((c) => c.tool === 'write_file' || c.tool === 'edit_file')
3172
+ .map((c) => c.args.path || c.args.file_path)
3173
+ .filter(Boolean);
3174
+ if (pendingFiles.length > 0) {
3175
+ blackboardContext += `\n**FILES ALREADY IN PENDING PLAN (do not duplicate):**\n${pendingFiles.slice(0, 10).join('\n')}\n`;
3176
+ }
3177
+ }
3178
+ // CONSTRAINT INJECTION
3179
+ // Add constraints to the subagent's context if provided
3180
+ let constraintContext = '';
3181
+ if (constraints) {
3182
+ const constraintParts = [];
3183
+ if (constraints.focusAreas && constraints.focusAreas.length > 0) {
3184
+ constraintParts.push(`**FOCUS AREAS (limit exploration to these paths):**\n${constraints.focusAreas.map(a => ` - ${a}`).join('\n')}`);
3185
+ }
3186
+ if (constraints.excludeAreas && constraints.excludeAreas.length > 0) {
3187
+ constraintParts.push(`**EXCLUDED AREAS (do NOT explore these):**\n${constraints.excludeAreas.map(a => ` - ${a}`).join('\n')}`);
3188
+ }
3189
+ if (constraints.requiredDeliverables && constraints.requiredDeliverables.length > 0) {
3190
+ constraintParts.push(`**REQUIRED DELIVERABLES (you must produce these):**\n${constraints.requiredDeliverables.map(d => ` - ${d}`).join('\n')}`);
3191
+ }
3192
+ if (constraints.maxTokens) {
3193
+ constraintParts.push(`**TOKEN BUDGET:** ${constraints.maxTokens} tokens maximum`);
3194
+ }
3195
+ if (constraints.timeboxMinutes) {
3196
+ constraintParts.push(`**TIME LIMIT:** ${constraints.timeboxMinutes} minutes (soft limit - wrap up if approaching)`);
3197
+ }
3198
+ if (constraintParts.length > 0) {
3199
+ constraintContext = `\n\n**EXECUTION CONSTRAINTS:**\n${constraintParts.join('\n\n')}\n`;
3200
+ }
3201
+ }
3202
+ // Build subagent system prompt with subagent-specific plan mode addition
3203
+ const parentMode = this.getMode();
3204
+ const subagentSystemPrompt = parentMode === 'plan'
3205
+ ? `${agentDef.systemPrompt}\n\n${SUBAGENT_PLAN_MODE_ADDITION}${blackboardContext}${constraintContext}`
3206
+ : `${agentDef.systemPrompt}${blackboardContext}${constraintContext}`;
2820
3207
  // Create a sub-agent with the agent's config
2821
3208
  const subAgent = new ProductionAgent({
2822
3209
  provider: this.provider,
@@ -2825,7 +3212,7 @@ export class ProductionAgent {
2825
3212
  toolResolver: this.toolResolver || undefined,
2826
3213
  // Pass MCP tool summaries so subagent knows what tools are available
2827
3214
  mcpToolSummaries: this.config.mcpToolSummaries,
2828
- systemPrompt: agentDef.systemPrompt,
3215
+ systemPrompt: subagentSystemPrompt,
2829
3216
  model: resolvedModel,
2830
3217
  maxIterations: agentDef.maxIterations || defaultMaxIterations,
2831
3218
  // Inherit some features but keep subagent simpler
@@ -2843,7 +3230,31 @@ export class ProductionAgent {
2843
3230
  builtIn: { logging: false, timing: false, metrics: false },
2844
3231
  custom: [],
2845
3232
  },
3233
+ // Share parent's blackboard for coordination between parallel subagents
3234
+ blackboard: this.blackboard || undefined,
2846
3235
  });
3236
+ // CRITICAL: Subagent inherits parent's mode
3237
+ // This ensures that if parent is in plan mode:
3238
+ // - Subagent's read operations execute immediately (visible exploration)
3239
+ // - Subagent's write operations get queued in the subagent's pending plan
3240
+ // - User maintains control over what actually gets written
3241
+ if (parentMode !== 'build') {
3242
+ subAgent.setMode(parentMode);
3243
+ }
3244
+ // Pass parent's iteration count to subagent for accurate budget tracking
3245
+ // This prevents subagents from consuming excessive iterations when parent already used many
3246
+ subAgent.setParentIterations(this.getTotalIterations());
3247
+ // UNIFIED TRACING: Share parent's trace collector with subagent context
3248
+ // This ensures all subagent events are written to the same trace file as the parent,
3249
+ // tagged with subagent context for proper aggregation in /trace output
3250
+ if (this.traceCollector) {
3251
+ const subagentTraceView = this.traceCollector.createSubagentView({
3252
+ parentSessionId: this.traceCollector.getSessionId() || 'unknown',
3253
+ agentType: agentName,
3254
+ spawnedAtIteration: this.state.iteration,
3255
+ });
3256
+ subAgent.setTraceCollector(subagentTraceView);
3257
+ }
2847
3258
  // Forward events from subagent with context
2848
3259
  subAgent.subscribe(event => {
2849
3260
  // Tag event with subagent source so TUI can display it properly
@@ -2857,20 +3268,77 @@ export class ProductionAgent {
2857
3268
  const effectiveSource = parentSource
2858
3269
  ? createLinkedToken(parentSource, timeoutSource)
2859
3270
  : timeoutSource;
3271
+ // CRITICAL: Pass the cancellation token to the subagent so it can check and stop
3272
+ // gracefully when timeout fires. Without this, the subagent continues running as
3273
+ // a "zombie" even after race() returns with a timeout error.
3274
+ subAgent.setExternalCancellation(effectiveSource.token);
2860
3275
  try {
2861
3276
  // Run the task with cancellation propagation from parent
2862
3277
  const result = await race(subAgent.run(task), effectiveSource.token);
2863
3278
  const duration = Date.now() - startTime;
2864
- const spawnResult = {
3279
+ // BEFORE cleanup - extract subagent's pending plan and merge into parent's plan
3280
+ // This ensures that when a subagent in plan mode queues writes, they bubble up to the parent
3281
+ let queuedChangeSummary = '';
3282
+ let queuedChangesCount = 0;
3283
+ if (subAgent.hasPendingPlan()) {
3284
+ const subPlan = subAgent.getPendingPlan();
3285
+ if (subPlan && subPlan.proposedChanges.length > 0) {
3286
+ queuedChangesCount = subPlan.proposedChanges.length;
3287
+ // Emit event for TUI to display
3288
+ this.emit({
3289
+ type: 'agent.pending_plan',
3290
+ agentId: agentName,
3291
+ changes: subPlan.proposedChanges,
3292
+ });
3293
+ // Build detailed summary of what was queued for the return message
3294
+ // This prevents the "doom loop" where parent doesn't know what subagent did
3295
+ const changeSummaries = subPlan.proposedChanges.map(c => {
3296
+ if (c.tool === 'write_file' || c.tool === 'edit_file') {
3297
+ const path = c.args.path || c.args.file_path || '(unknown file)';
3298
+ return ` - [${c.tool}] ${path}: ${c.reason}`;
3299
+ }
3300
+ else if (c.tool === 'bash') {
3301
+ const cmd = String(c.args.command || '').slice(0, 60);
3302
+ return ` - [bash] ${cmd}${String(c.args.command || '').length > 60 ? '...' : ''}: ${c.reason}`;
3303
+ }
3304
+ return ` - [${c.tool}]: ${c.reason}`;
3305
+ });
3306
+ queuedChangeSummary = `\n\n[PLAN MODE - CHANGES QUEUED TO PARENT]\n` +
3307
+ `The following ${subPlan.proposedChanges.length} change(s) have been queued in the parent's pending plan:\n` +
3308
+ changeSummaries.join('\n') + '\n' +
3309
+ `\nThese changes are now in YOUR pending plan. The task for this subagent is COMPLETE.\n` +
3310
+ `Do NOT spawn another agent for the same task - the changes are already queued.\n` +
3311
+ `Use /show-plan to see all pending changes, /approve to execute them.`;
3312
+ // Merge into parent's pending plan with subagent context
3313
+ for (const change of subPlan.proposedChanges) {
3314
+ this.pendingPlanManager.addProposedChange(change.tool, { ...change.args, _fromSubagent: agentName }, `[${agentName}] ${change.reason}`, change.toolCallId);
3315
+ }
3316
+ }
3317
+ // Also merge exploration summary if available
3318
+ if (subPlan?.explorationSummary) {
3319
+ this.pendingPlanManager.appendExplorationFinding(`[${agentName}] ${subPlan.explorationSummary}`);
3320
+ }
3321
+ }
3322
+ // If subagent queued changes, override output with informative message
3323
+ // This is critical to prevent doom loops where parent doesn't understand what happened
3324
+ const finalOutput = queuedChangeSummary
3325
+ ? (result.response || '') + queuedChangeSummary
3326
+ : (result.response || result.error || '');
3327
+ const spawnResultFinal = {
2865
3328
  success: result.success,
2866
- output: result.response || result.error || '',
3329
+ output: finalOutput,
2867
3330
  metrics: {
2868
3331
  tokens: result.metrics.totalTokens,
2869
3332
  duration,
2870
3333
  toolCalls: result.metrics.toolCalls,
2871
3334
  },
2872
3335
  };
2873
- this.emit({ type: 'agent.complete', agentId: agentName, success: result.success });
3336
+ this.emit({
3337
+ type: 'agent.complete',
3338
+ agentId: agentName,
3339
+ success: result.success,
3340
+ output: finalOutput.slice(0, 500), // Include output preview
3341
+ });
2874
3342
  // Enhanced tracing: Record subagent completion
2875
3343
  this.traceCollector?.record({
2876
3344
  type: 'subagent.link',
@@ -2898,7 +3366,14 @@ export class ProductionAgent {
2898
3366
  },
2899
3367
  });
2900
3368
  await subAgent.cleanup();
2901
- return spawnResult;
3369
+ // Cache result for duplicate spawn prevention
3370
+ // Use the same taskKey from the dedup check above
3371
+ this.spawnedTasks.set(taskKey, {
3372
+ timestamp: Date.now(),
3373
+ result: finalOutput,
3374
+ queuedChanges: queuedChangesCount,
3375
+ });
3376
+ return spawnResultFinal;
2902
3377
  }
2903
3378
  catch (err) {
2904
3379
  // Handle cancellation (user ESC or timeout) for cleaner error messages
@@ -2909,6 +3384,55 @@ export class ProductionAgent {
2909
3384
  ? 'User cancelled'
2910
3385
  : `Timed out after ${subagentTimeout}ms`;
2911
3386
  this.emit({ type: 'agent.error', agentId: agentName, error: reason });
3387
+ // =======================================================================
3388
+ // PRESERVE PARTIAL RESULTS
3389
+ // Instead of discarding all work, capture whatever the subagent produced
3390
+ // before timeout. This prevents the "zombie agent" problem where tokens
3391
+ // are consumed but results are lost.
3392
+ // =======================================================================
3393
+ const subagentState = subAgent.getState();
3394
+ const subagentMetrics = subAgent.getMetrics();
3395
+ // Extract partial response from the last assistant message
3396
+ const assistantMessages = subagentState.messages.filter(m => m.role === 'assistant');
3397
+ const lastAssistantMsg = assistantMessages[assistantMessages.length - 1];
3398
+ const partialResponse = typeof lastAssistantMsg?.content === 'string'
3399
+ ? lastAssistantMsg.content
3400
+ : '';
3401
+ // Extract pending plan before cleanup (even on cancellation, preserve any queued work)
3402
+ let cancelledQueuedSummary = '';
3403
+ if (subAgent.hasPendingPlan()) {
3404
+ const subPlan = subAgent.getPendingPlan();
3405
+ if (subPlan && subPlan.proposedChanges.length > 0) {
3406
+ this.emit({
3407
+ type: 'agent.pending_plan',
3408
+ agentId: agentName,
3409
+ changes: subPlan.proposedChanges,
3410
+ });
3411
+ // Build summary of changes that were queued before cancellation
3412
+ const changeSummaries = subPlan.proposedChanges.map(c => {
3413
+ if (c.tool === 'write_file' || c.tool === 'edit_file') {
3414
+ const path = c.args.path || c.args.file_path || '(unknown file)';
3415
+ return ` - [${c.tool}] ${path}: ${c.reason}`;
3416
+ }
3417
+ else if (c.tool === 'bash') {
3418
+ const cmd = String(c.args.command || '').slice(0, 60);
3419
+ return ` - [bash] ${cmd}...: ${c.reason}`;
3420
+ }
3421
+ return ` - [${c.tool}]: ${c.reason}`;
3422
+ });
3423
+ cancelledQueuedSummary = `\n\n[PLAN MODE - CHANGES QUEUED BEFORE CANCELLATION]\n` +
3424
+ `${subPlan.proposedChanges.length} change(s) were queued to the parent plan:\n` +
3425
+ changeSummaries.join('\n') + '\n' +
3426
+ `These changes are preserved in your pending plan.`;
3427
+ for (const change of subPlan.proposedChanges) {
3428
+ this.pendingPlanManager.addProposedChange(change.tool, { ...change.args, _fromSubagent: agentName }, `[${agentName}] ${change.reason}`, change.toolCallId);
3429
+ }
3430
+ }
3431
+ // Also preserve exploration summary
3432
+ if (subPlan?.explorationSummary) {
3433
+ this.pendingPlanManager.appendExplorationFinding(`[${agentName}] ${subPlan.explorationSummary}`);
3434
+ }
3435
+ }
2912
3436
  // Try to cleanup the subagent gracefully
2913
3437
  try {
2914
3438
  await subAgent.cleanup();
@@ -2916,13 +3440,50 @@ export class ProductionAgent {
2916
3440
  catch {
2917
3441
  // Ignore cleanup errors on cancellation
2918
3442
  }
2919
- const output = isUserCancellation
3443
+ // Build output message with partial results
3444
+ const baseOutput = isUserCancellation
2920
3445
  ? `Subagent '${agentName}' was cancelled by user.`
2921
- : `Subagent '${agentName}' timed out after ${Math.round(subagentTimeout / 1000)}s. The task may be too complex or the model may be slow.`;
3446
+ : `Subagent '${agentName}' timed out after ${Math.round(subagentTimeout / 1000)}s.`;
3447
+ // Include partial response if we have one
3448
+ const partialResultSection = partialResponse
3449
+ ? `\n\n[PARTIAL RESULTS BEFORE TIMEOUT]\n${partialResponse.slice(0, 2000)}${partialResponse.length > 2000 ? '...(truncated)' : ''}`
3450
+ : '';
3451
+ // Enhanced tracing: Record subagent timeout with partial results
3452
+ this.traceCollector?.record({
3453
+ type: 'subagent.link',
3454
+ data: {
3455
+ parentSessionId: this.traceCollector.getSessionId() || 'unknown',
3456
+ childSessionId,
3457
+ childTraceId,
3458
+ childConfig: {
3459
+ agentType: agentName,
3460
+ model: resolvedModel || 'default',
3461
+ task,
3462
+ tools: agentTools.map(t => t.name),
3463
+ },
3464
+ spawnContext: {
3465
+ reason: `Delegated task: ${task.slice(0, 100)}`,
3466
+ expectedOutcome: agentDef.description,
3467
+ parentIteration: this.state.iteration,
3468
+ },
3469
+ result: {
3470
+ success: false,
3471
+ summary: `[TIMEOUT] ${baseOutput}\n${partialResponse.slice(0, 200)}`,
3472
+ tokensUsed: subagentMetrics.totalTokens,
3473
+ durationMs: duration,
3474
+ },
3475
+ },
3476
+ });
2922
3477
  return {
2923
3478
  success: false,
2924
- output,
2925
- metrics: { tokens: 0, duration, toolCalls: 0 },
3479
+ output: baseOutput + partialResultSection + cancelledQueuedSummary,
3480
+ // IMPORTANT: Use actual metrics instead of zeros
3481
+ // This ensures accurate token tracking in /trace output
3482
+ metrics: {
3483
+ tokens: subagentMetrics.totalTokens,
3484
+ duration,
3485
+ toolCalls: subagentMetrics.toolCalls,
3486
+ },
2926
3487
  };
2927
3488
  }
2928
3489
  throw err; // Re-throw non-cancellation errors
@@ -2943,6 +3504,33 @@ export class ProductionAgent {
2943
3504
  };
2944
3505
  }
2945
3506
  }
3507
+ /**
3508
+ * Spawn multiple agents in parallel to work on independent tasks.
3509
+ * Uses the shared blackboard for coordination and conflict prevention.
3510
+ */
3511
+ async spawnAgentsParallel(tasks) {
3512
+ // Emit start event for TUI visibility
3513
+ this.emit({
3514
+ type: 'parallel.spawn.start',
3515
+ count: tasks.length,
3516
+ agents: tasks.map(t => t.agent),
3517
+ });
3518
+ // Execute all tasks in parallel
3519
+ const promises = tasks.map(({ agent, task }) => this.spawnAgent(agent, task));
3520
+ const results = await Promise.all(promises);
3521
+ // Emit completion event
3522
+ this.emit({
3523
+ type: 'parallel.spawn.complete',
3524
+ count: tasks.length,
3525
+ successCount: results.filter(r => r.success).length,
3526
+ results: results.map((r, i) => ({
3527
+ agent: tasks[i].agent,
3528
+ success: r.success,
3529
+ tokens: r.metrics?.tokens || 0,
3530
+ })),
3531
+ });
3532
+ return results;
3533
+ }
2946
3534
  /**
2947
3535
  * Get a formatted list of available agents.
2948
3536
  */
@@ -3277,6 +3865,37 @@ If the task is a simple question or doesn't need specialized handling, set bestA
3277
3865
  this.emit({ type: 'mode.changed', from: this.getMode(), to: parsed });
3278
3866
  }
3279
3867
  }
3868
+ /**
3869
+ * Set the parent's iteration count for total budget tracking.
3870
+ * When this agent is a subagent, the parent passes its iteration count
3871
+ * so the subagent can account for total iterations across the hierarchy.
3872
+ */
3873
+ setParentIterations(count) {
3874
+ this.parentIterations = count;
3875
+ }
3876
+ /**
3877
+ * Set an external cancellation token for this agent.
3878
+ * Used when spawning subagents to propagate parent timeout/cancellation.
3879
+ * The agent will check this token in its main loop and stop gracefully
3880
+ * when cancellation is requested, preserving partial results.
3881
+ */
3882
+ setExternalCancellation(token) {
3883
+ this.externalCancellationToken = token;
3884
+ }
3885
+ /**
3886
+ * Check if external cancellation has been requested.
3887
+ * Returns true if the external token signals cancellation.
3888
+ */
3889
+ isExternallyCancelled() {
3890
+ return this.externalCancellationToken?.isCancellationRequested ?? false;
3891
+ }
3892
+ /**
3893
+ * Get total iterations (this agent + parent).
3894
+ * Used for accurate budget tracking across subagent hierarchies.
3895
+ */
3896
+ getTotalIterations() {
3897
+ return this.state.iteration + this.parentIterations;
3898
+ }
3280
3899
  /**
3281
3900
  * Cycle to the next mode (for Tab key).
3282
3901
  */
@@ -3351,20 +3970,21 @@ If the task is a simple question or doesn't need specialized handling, set bestA
3351
3970
  /**
3352
3971
  * Approve the pending plan and execute the changes.
3353
3972
  * @param count - If provided, only approve first N changes
3354
- * @returns Result of executing the approved changes
3973
+ * @returns Result of executing the approved changes, including tool outputs
3355
3974
  */
3356
3975
  async approvePlan(count) {
3357
3976
  const result = this.pendingPlanManager.approve(count);
3358
3977
  if (result.changes.length === 0) {
3359
- return { success: true, executed: 0, errors: [] };
3978
+ return { success: true, executed: 0, errors: [], results: [] };
3360
3979
  }
3361
3980
  // Switch to build mode for execution
3362
3981
  const previousMode = this.getMode();
3363
3982
  this.setMode('build');
3364
3983
  this.emit({ type: 'plan.approved', changeCount: result.changes.length });
3365
3984
  const errors = [];
3985
+ const results = [];
3366
3986
  let executed = 0;
3367
- // Execute each change
3987
+ // Execute each change and CAPTURE results
3368
3988
  for (let i = 0; i < result.changes.length; i++) {
3369
3989
  const change = result.changes[i];
3370
3990
  this.emit({ type: 'plan.executing', changeIndex: i, totalChanges: result.changes.length });
@@ -3372,14 +3992,37 @@ If the task is a simple question or doesn't need specialized handling, set bestA
3372
3992
  const tool = this.tools.get(change.tool);
3373
3993
  if (!tool) {
3374
3994
  errors.push(`Unknown tool: ${change.tool}`);
3995
+ this.emit({
3996
+ type: 'plan.change.complete',
3997
+ changeIndex: i,
3998
+ tool: change.tool,
3999
+ result: null,
4000
+ error: `Unknown tool: ${change.tool}`,
4001
+ });
3375
4002
  continue;
3376
4003
  }
3377
- await tool.execute(change.args);
4004
+ // CRITICAL: Capture tool result instead of discarding it
4005
+ const toolResult = await tool.execute(change.args);
4006
+ results.push({ tool: change.tool, output: toolResult });
3378
4007
  executed++;
4008
+ // Emit result for TUI display
4009
+ this.emit({
4010
+ type: 'plan.change.complete',
4011
+ changeIndex: i,
4012
+ tool: change.tool,
4013
+ result: toolResult,
4014
+ });
3379
4015
  }
3380
4016
  catch (err) {
3381
4017
  const error = err instanceof Error ? err.message : String(err);
3382
4018
  errors.push(`${change.tool}: ${error}`);
4019
+ this.emit({
4020
+ type: 'plan.change.complete',
4021
+ changeIndex: i,
4022
+ tool: change.tool,
4023
+ result: null,
4024
+ error,
4025
+ });
3383
4026
  }
3384
4027
  }
3385
4028
  // Restore previous mode if it wasn't build
@@ -3390,6 +4033,7 @@ If the task is a simple question or doesn't need specialized handling, set bestA
3390
4033
  success: errors.length === 0,
3391
4034
  executed,
3392
4035
  errors,
4036
+ results,
3393
4037
  };
3394
4038
  }
3395
4039
  /**
@@ -3426,6 +4070,12 @@ If the task is a simple question or doesn't need specialized handling, set bestA
3426
4070
  getAgentRegistry() {
3427
4071
  return this.agentRegistry;
3428
4072
  }
4073
+ /**
4074
+ * Get the task manager instance for task tracking.
4075
+ */
4076
+ getTaskManager() {
4077
+ return this.taskManager;
4078
+ }
3429
4079
  /**
3430
4080
  * Get all loaded skills.
3431
4081
  */