attocode 0.2.1 → 0.2.3

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 (219) hide show
  1. package/CHANGELOG.md +191 -1
  2. package/README.md +7 -0
  3. package/dist/src/adapters.d.ts +6 -1
  4. package/dist/src/adapters.d.ts.map +1 -1
  5. package/dist/src/adapters.js +8 -1
  6. package/dist/src/adapters.js.map +1 -1
  7. package/dist/src/agent.d.ts +41 -4
  8. package/dist/src/agent.d.ts.map +1 -1
  9. package/dist/src/agent.js +846 -75
  10. package/dist/src/agent.js.map +1 -1
  11. package/dist/src/cli.d.ts.map +1 -1
  12. package/dist/src/cli.js +23 -2
  13. package/dist/src/cli.js.map +1 -1
  14. package/dist/src/core/protocol/types.d.ts +8 -8
  15. package/dist/src/defaults.d.ts +7 -2
  16. package/dist/src/defaults.d.ts.map +1 -1
  17. package/dist/src/defaults.js +38 -2
  18. package/dist/src/defaults.js.map +1 -1
  19. package/dist/src/integrations/agent-registry.d.ts +13 -0
  20. package/dist/src/integrations/agent-registry.d.ts.map +1 -1
  21. package/dist/src/integrations/agent-registry.js.map +1 -1
  22. package/dist/src/integrations/async-subagent.d.ts +135 -0
  23. package/dist/src/integrations/async-subagent.d.ts.map +1 -0
  24. package/dist/src/integrations/async-subagent.js +213 -0
  25. package/dist/src/integrations/async-subagent.js.map +1 -0
  26. package/dist/src/integrations/auto-checkpoint.d.ts +98 -0
  27. package/dist/src/integrations/auto-checkpoint.d.ts.map +1 -0
  28. package/dist/src/integrations/auto-checkpoint.js +252 -0
  29. package/dist/src/integrations/auto-checkpoint.js.map +1 -0
  30. package/dist/src/integrations/auto-compaction.d.ts.map +1 -1
  31. package/dist/src/integrations/auto-compaction.js +5 -1
  32. package/dist/src/integrations/auto-compaction.js.map +1 -1
  33. package/dist/src/integrations/bash-policy.d.ts +33 -0
  34. package/dist/src/integrations/bash-policy.d.ts.map +1 -0
  35. package/dist/src/integrations/bash-policy.js +142 -0
  36. package/dist/src/integrations/bash-policy.js.map +1 -0
  37. package/dist/src/integrations/codebase-context.d.ts +5 -0
  38. package/dist/src/integrations/codebase-context.d.ts.map +1 -1
  39. package/dist/src/integrations/codebase-context.js +33 -0
  40. package/dist/src/integrations/codebase-context.js.map +1 -1
  41. package/dist/src/integrations/complexity-classifier.d.ts +86 -0
  42. package/dist/src/integrations/complexity-classifier.d.ts.map +1 -0
  43. package/dist/src/integrations/complexity-classifier.js +233 -0
  44. package/dist/src/integrations/complexity-classifier.js.map +1 -0
  45. package/dist/src/integrations/delegation-protocol.d.ts +86 -0
  46. package/dist/src/integrations/delegation-protocol.d.ts.map +1 -0
  47. package/dist/src/integrations/delegation-protocol.js +127 -0
  48. package/dist/src/integrations/delegation-protocol.js.map +1 -0
  49. package/dist/src/integrations/dynamic-budget.d.ts +81 -0
  50. package/dist/src/integrations/dynamic-budget.d.ts.map +1 -0
  51. package/dist/src/integrations/dynamic-budget.js +151 -0
  52. package/dist/src/integrations/dynamic-budget.js.map +1 -0
  53. package/dist/src/integrations/economics.d.ts +86 -1
  54. package/dist/src/integrations/economics.d.ts.map +1 -1
  55. package/dist/src/integrations/economics.js +306 -11
  56. package/dist/src/integrations/economics.js.map +1 -1
  57. package/dist/src/integrations/environment-facts.d.ts +52 -0
  58. package/dist/src/integrations/environment-facts.d.ts.map +1 -0
  59. package/dist/src/integrations/environment-facts.js +84 -0
  60. package/dist/src/integrations/environment-facts.js.map +1 -0
  61. package/dist/src/integrations/hierarchical-config.d.ts.map +1 -1
  62. package/dist/src/integrations/hierarchical-config.js +17 -0
  63. package/dist/src/integrations/hierarchical-config.js.map +1 -1
  64. package/dist/src/integrations/index.d.ts +19 -2
  65. package/dist/src/integrations/index.d.ts.map +1 -1
  66. package/dist/src/integrations/index.js +34 -2
  67. package/dist/src/integrations/index.js.map +1 -1
  68. package/dist/src/integrations/injection-budget.d.ts +71 -0
  69. package/dist/src/integrations/injection-budget.d.ts.map +1 -0
  70. package/dist/src/integrations/injection-budget.js +136 -0
  71. package/dist/src/integrations/injection-budget.js.map +1 -0
  72. package/dist/src/integrations/mcp-client.d.ts.map +1 -1
  73. package/dist/src/integrations/mcp-client.js +14 -0
  74. package/dist/src/integrations/mcp-client.js.map +1 -1
  75. package/dist/src/integrations/mcp-custom-tools.d.ts +102 -0
  76. package/dist/src/integrations/mcp-custom-tools.d.ts.map +1 -0
  77. package/dist/src/integrations/mcp-custom-tools.js +232 -0
  78. package/dist/src/integrations/mcp-custom-tools.js.map +1 -0
  79. package/dist/src/integrations/mcp-tool-validator.d.ts +60 -0
  80. package/dist/src/integrations/mcp-tool-validator.d.ts.map +1 -0
  81. package/dist/src/integrations/mcp-tool-validator.js +141 -0
  82. package/dist/src/integrations/mcp-tool-validator.js.map +1 -0
  83. package/dist/src/integrations/policy-engine.d.ts +55 -0
  84. package/dist/src/integrations/policy-engine.d.ts.map +1 -0
  85. package/dist/src/integrations/policy-engine.js +247 -0
  86. package/dist/src/integrations/policy-engine.js.map +1 -0
  87. package/dist/src/integrations/safety.d.ts +5 -4
  88. package/dist/src/integrations/safety.d.ts.map +1 -1
  89. package/dist/src/integrations/safety.js +32 -7
  90. package/dist/src/integrations/safety.js.map +1 -1
  91. package/dist/src/integrations/sandbox/basic.d.ts +7 -0
  92. package/dist/src/integrations/sandbox/basic.d.ts.map +1 -1
  93. package/dist/src/integrations/sandbox/basic.js +27 -2
  94. package/dist/src/integrations/sandbox/basic.js.map +1 -1
  95. package/dist/src/integrations/sandbox/index.d.ts +6 -0
  96. package/dist/src/integrations/sandbox/index.d.ts.map +1 -1
  97. package/dist/src/integrations/sandbox/index.js +3 -0
  98. package/dist/src/integrations/sandbox/index.js.map +1 -1
  99. package/dist/src/integrations/sandbox/landlock.d.ts.map +1 -1
  100. package/dist/src/integrations/sandbox/landlock.js +3 -0
  101. package/dist/src/integrations/sandbox/landlock.js.map +1 -1
  102. package/dist/src/integrations/self-improvement.d.ts +90 -0
  103. package/dist/src/integrations/self-improvement.d.ts.map +1 -0
  104. package/dist/src/integrations/self-improvement.js +229 -0
  105. package/dist/src/integrations/self-improvement.js.map +1 -0
  106. package/dist/src/integrations/smart-decomposer.d.ts +22 -1
  107. package/dist/src/integrations/smart-decomposer.d.ts.map +1 -1
  108. package/dist/src/integrations/smart-decomposer.js +127 -28
  109. package/dist/src/integrations/smart-decomposer.js.map +1 -1
  110. package/dist/src/integrations/subagent-output-store.d.ts +91 -0
  111. package/dist/src/integrations/subagent-output-store.d.ts.map +1 -0
  112. package/dist/src/integrations/subagent-output-store.js +257 -0
  113. package/dist/src/integrations/subagent-output-store.js.map +1 -0
  114. package/dist/src/integrations/swarm/index.d.ts +2 -2
  115. package/dist/src/integrations/swarm/index.d.ts.map +1 -1
  116. package/dist/src/integrations/swarm/index.js +1 -1
  117. package/dist/src/integrations/swarm/index.js.map +1 -1
  118. package/dist/src/integrations/swarm/model-selector.d.ts +16 -0
  119. package/dist/src/integrations/swarm/model-selector.d.ts.map +1 -1
  120. package/dist/src/integrations/swarm/model-selector.js +123 -10
  121. package/dist/src/integrations/swarm/model-selector.js.map +1 -1
  122. package/dist/src/integrations/swarm/swarm-budget.d.ts +4 -0
  123. package/dist/src/integrations/swarm/swarm-budget.d.ts.map +1 -1
  124. package/dist/src/integrations/swarm/swarm-budget.js +6 -0
  125. package/dist/src/integrations/swarm/swarm-budget.js.map +1 -1
  126. package/dist/src/integrations/swarm/swarm-config-loader.d.ts +10 -1
  127. package/dist/src/integrations/swarm/swarm-config-loader.d.ts.map +1 -1
  128. package/dist/src/integrations/swarm/swarm-config-loader.js +226 -13
  129. package/dist/src/integrations/swarm/swarm-config-loader.js.map +1 -1
  130. package/dist/src/integrations/swarm/swarm-event-bridge.d.ts +12 -1
  131. package/dist/src/integrations/swarm/swarm-event-bridge.d.ts.map +1 -1
  132. package/dist/src/integrations/swarm/swarm-event-bridge.js +178 -9
  133. package/dist/src/integrations/swarm/swarm-event-bridge.js.map +1 -1
  134. package/dist/src/integrations/swarm/swarm-events.d.ts +66 -1
  135. package/dist/src/integrations/swarm/swarm-events.d.ts.map +1 -1
  136. package/dist/src/integrations/swarm/swarm-events.js +26 -5
  137. package/dist/src/integrations/swarm/swarm-events.js.map +1 -1
  138. package/dist/src/integrations/swarm/swarm-orchestrator.d.ts +127 -0
  139. package/dist/src/integrations/swarm/swarm-orchestrator.d.ts.map +1 -1
  140. package/dist/src/integrations/swarm/swarm-orchestrator.js +1842 -47
  141. package/dist/src/integrations/swarm/swarm-orchestrator.js.map +1 -1
  142. package/dist/src/integrations/swarm/swarm-quality-gate.d.ts +91 -3
  143. package/dist/src/integrations/swarm/swarm-quality-gate.d.ts.map +1 -1
  144. package/dist/src/integrations/swarm/swarm-quality-gate.js +395 -19
  145. package/dist/src/integrations/swarm/swarm-quality-gate.js.map +1 -1
  146. package/dist/src/integrations/swarm/task-queue.d.ts +55 -1
  147. package/dist/src/integrations/swarm/task-queue.d.ts.map +1 -1
  148. package/dist/src/integrations/swarm/task-queue.js +389 -16
  149. package/dist/src/integrations/swarm/task-queue.js.map +1 -1
  150. package/dist/src/integrations/swarm/types.d.ts +247 -11
  151. package/dist/src/integrations/swarm/types.d.ts.map +1 -1
  152. package/dist/src/integrations/swarm/types.js +67 -9
  153. package/dist/src/integrations/swarm/types.js.map +1 -1
  154. package/dist/src/integrations/swarm/worker-pool.d.ts +18 -5
  155. package/dist/src/integrations/swarm/worker-pool.d.ts.map +1 -1
  156. package/dist/src/integrations/swarm/worker-pool.js +236 -34
  157. package/dist/src/integrations/swarm/worker-pool.js.map +1 -1
  158. package/dist/src/integrations/thinking-strategy.d.ts +52 -0
  159. package/dist/src/integrations/thinking-strategy.d.ts.map +1 -0
  160. package/dist/src/integrations/thinking-strategy.js +129 -0
  161. package/dist/src/integrations/thinking-strategy.js.map +1 -0
  162. package/dist/src/integrations/tool-recommendation.d.ts +61 -0
  163. package/dist/src/integrations/tool-recommendation.d.ts.map +1 -0
  164. package/dist/src/integrations/tool-recommendation.js +268 -0
  165. package/dist/src/integrations/tool-recommendation.js.map +1 -0
  166. package/dist/src/integrations/verification-gate.d.ts +80 -0
  167. package/dist/src/integrations/verification-gate.d.ts.map +1 -0
  168. package/dist/src/integrations/verification-gate.js +146 -0
  169. package/dist/src/integrations/verification-gate.js.map +1 -0
  170. package/dist/src/integrations/work-log.d.ts +87 -0
  171. package/dist/src/integrations/work-log.d.ts.map +1 -0
  172. package/dist/src/integrations/work-log.js +275 -0
  173. package/dist/src/integrations/work-log.js.map +1 -0
  174. package/dist/src/main.js +31 -5
  175. package/dist/src/main.js.map +1 -1
  176. package/dist/src/modes/repl.d.ts.map +1 -1
  177. package/dist/src/modes/repl.js +10 -4
  178. package/dist/src/modes/repl.js.map +1 -1
  179. package/dist/src/modes/tui.d.ts.map +1 -1
  180. package/dist/src/modes/tui.js +5 -0
  181. package/dist/src/modes/tui.js.map +1 -1
  182. package/dist/src/modes.d.ts +6 -0
  183. package/dist/src/modes.d.ts.map +1 -1
  184. package/dist/src/modes.js +69 -21
  185. package/dist/src/modes.js.map +1 -1
  186. package/dist/src/tools/agent.d.ts.map +1 -1
  187. package/dist/src/tools/agent.js +11 -2
  188. package/dist/src/tools/agent.js.map +1 -1
  189. package/dist/src/tools/bash.d.ts +9 -3
  190. package/dist/src/tools/bash.d.ts.map +1 -1
  191. package/dist/src/tools/bash.js +12 -0
  192. package/dist/src/tools/bash.js.map +1 -1
  193. package/dist/src/tools/coercion.d.ts +6 -0
  194. package/dist/src/tools/coercion.d.ts.map +1 -1
  195. package/dist/src/tools/coercion.js +13 -0
  196. package/dist/src/tools/coercion.js.map +1 -1
  197. package/dist/src/tools/file.d.ts +2 -2
  198. package/dist/src/tools/file.js +2 -2
  199. package/dist/src/tools/file.js.map +1 -1
  200. package/dist/src/tools/permission.d.ts.map +1 -1
  201. package/dist/src/tools/permission.js +4 -111
  202. package/dist/src/tools/permission.js.map +1 -1
  203. package/dist/src/tools/standard.d.ts +17 -1
  204. package/dist/src/tools/standard.d.ts.map +1 -1
  205. package/dist/src/tools/standard.js +64 -11
  206. package/dist/src/tools/standard.js.map +1 -1
  207. package/dist/src/tracing/trace-collector.d.ts +167 -0
  208. package/dist/src/tracing/trace-collector.d.ts.map +1 -1
  209. package/dist/src/tracing/trace-collector.js +137 -0
  210. package/dist/src/tracing/trace-collector.js.map +1 -1
  211. package/dist/src/tracing/types.d.ts +105 -1
  212. package/dist/src/tracing/types.d.ts.map +1 -1
  213. package/dist/src/tracing/types.js.map +1 -1
  214. package/dist/src/tui/app.d.ts.map +1 -1
  215. package/dist/src/tui/app.js +34 -5
  216. package/dist/src/tui/app.js.map +1 -1
  217. package/dist/src/types.d.ts +89 -0
  218. package/dist/src/types.d.ts.map +1 -1
  219. package/package.json +6 -2
package/dist/src/agent.js CHANGED
@@ -21,7 +21,12 @@
21
21
  import { buildConfig, isFeatureEnabled, getEnabledFeatures, getSubagentTimeout, getSubagentMaxIterations, } from './defaults.js';
22
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, SUBAGENT_BUDGET, TIMEOUT_WRAPUP_PROMPT, AgentRegistry, filterToolsForAgent, formatAgentList, createCancellationManager, isCancellationError, createLinkedToken, createGracefulTimeout, race, createResourceManager, createLSPManager, createSemanticCacheManager, createSkillManager, formatSkillList, createContextEngineering, stableStringify, createCodebaseContext, buildContextFromChunks, createSharedFileCache, createBudgetPool, createPendingPlanManager, createInteractivePlanner, createRecursiveContext, createLearningStore, createCompactor, createAutoCompactionManager, createFileChangeTracker, createCapabilitiesRegistry, createSharedBlackboard, createTaskManager, createSwarmOrchestrator, createThrottledProvider, FREE_TIER_THROTTLE, PAID_TIER_THROTTLE, } from './integrations/index.js';
24
+ import { HookManager, MemoryManager, PlanningManager, ObservabilityManager, SafetyManager, RoutingManager, MultiAgentManager, ReActManager, ExecutionPolicyManager, ThreadManager, RulesManager, DEFAULT_RULE_SOURCES, ExecutionEconomicsManager, STANDARD_BUDGET, SUBAGENT_BUDGET, TIMEOUT_WRAPUP_PROMPT, AgentRegistry, filterToolsForAgent, formatAgentList, createCancellationManager, isCancellationError, createLinkedToken, createGracefulTimeout, race, createResourceManager, createLSPManager, createSemanticCacheManager, createSkillManager, formatSkillList, createContextEngineering, stableStringify, createCodebaseContext, buildContextFromChunks, generateLightweightRepoMap, createSharedFileCache, createBudgetPool, createDynamicBudgetPool, createPendingPlanManager, createInteractivePlanner, createRecursiveContext, createLearningStore, createCompactor, createAutoCompactionManager, createFileChangeTracker, createCapabilitiesRegistry, createSharedBlackboard, createTaskManager, createSwarmOrchestrator, createThrottledProvider, FREE_TIER_THROTTLE, PAID_TIER_THROTTLE, createWorkLog, createVerificationGate,
25
+ // Phase 2: Orchestration
26
+ classifyComplexity, getScalingGuidance, buildDelegationPrompt, createMinimalDelegationSpec, getSubagentQualityPrompt, ToolRecommendationEngine, createToolRecommendationEngine, createInjectionBudgetManager,
27
+ // Phase 3: Advanced
28
+ getThinkingSystemPrompt, createSelfImprovementProtocol, createSubagentOutputStore, createSerperSearchTool, getEnvironmentFacts, formatFactsBlock, createAutoCheckpointManager, createSubagentSupervisor, createSubagentHandle, } from './integrations/index.js';
29
+ import { mergeApprovalScopeWithProfile, resolvePolicyProfile, } from './integrations/policy-engine.js';
25
30
  // Lesson 26: Tracing & Evaluation integration
26
31
  import { createTraceCollector } from './tracing/trace-collector.js';
27
32
  // Model registry for context window limits
@@ -43,31 +48,107 @@ export const PARALLELIZABLE_TOOLS = new Set([
43
48
  'search_code', 'get_file_info',
44
49
  ]);
45
50
  /**
46
- * Groups consecutive tool calls into batches for parallel/sequential execution.
47
- * Consecutive parallelizable tools form a single parallel batch.
48
- * Non-parallelizable tools break the sequence, starting a new batch.
51
+ * Tools that can run in parallel IF they target different files.
52
+ * write_file and edit_file on different paths are safe to parallelize.
49
53
  */
50
- export function groupToolCallsIntoBatches(toolCalls, isParallelizable = (tc) => PARALLELIZABLE_TOOLS.has(tc.name)) {
54
+ export const CONDITIONALLY_PARALLEL_TOOLS = new Set([
55
+ 'write_file', 'edit_file',
56
+ ]);
57
+ /**
58
+ * Extract the target file path from a tool call's arguments.
59
+ * Returns null if no file path can be determined.
60
+ */
61
+ export function extractToolFilePath(toolCall) {
62
+ // Check common argument patterns
63
+ const args = toolCall;
64
+ for (const key of ['path', 'file_path', 'filename', 'file']) {
65
+ if (typeof args[key] === 'string')
66
+ return args[key];
67
+ }
68
+ // Check nested args object
69
+ if (args.args && typeof args.args === 'object') {
70
+ const nested = args.args;
71
+ for (const key of ['path', 'file_path', 'filename', 'file']) {
72
+ if (typeof nested[key] === 'string')
73
+ return nested[key];
74
+ }
75
+ }
76
+ // Check input object (common in structured tool calls)
77
+ if (args.input && typeof args.input === 'object') {
78
+ const input = args.input;
79
+ for (const key of ['path', 'file_path', 'filename', 'file']) {
80
+ if (typeof input[key] === 'string')
81
+ return input[key];
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+ /**
87
+ * Check if a conditionally-parallel tool call conflicts with any tool
88
+ * in the current accumulator (same file path).
89
+ */
90
+ function hasFileConflict(toolCall, accumulator) {
91
+ const path = extractToolFilePath(toolCall);
92
+ if (!path)
93
+ return true; // Can't determine path → assume conflict
94
+ for (const existing of accumulator) {
95
+ const existingPath = extractToolFilePath(existing);
96
+ if (existingPath === path)
97
+ return true; // Same file → conflict
98
+ }
99
+ return false;
100
+ }
101
+ /**
102
+ * Groups tool calls into batches for parallel/sequential execution.
103
+ * Uses accumulate-and-flush: parallelizable tools accumulate until a
104
+ * non-parallelizable tool flushes them as a batch. This produces optimal
105
+ * batching even for non-consecutive parallelizable tools.
106
+ *
107
+ * Enhanced with conditional parallelism: write_file/edit_file on
108
+ * DIFFERENT files can be batched together for parallel execution.
109
+ *
110
+ * Example: [read1, read2, write, read3, grep] → [[read1, read2], [write], [read3, grep]]
111
+ * (Previous algorithm produced 4 batches; this produces 3)
112
+ *
113
+ * Enhanced: [write_a, write_b, write_a] → [[write_a, write_b], [write_a]]
114
+ * (Different files parallelized, same file sequential)
115
+ */
116
+ export function groupToolCallsIntoBatches(toolCalls, isParallelizable = (tc) => PARALLELIZABLE_TOOLS.has(tc.name), isConditionallyParallel = (tc) => CONDITIONALLY_PARALLEL_TOOLS.has(tc.name)) {
117
+ if (toolCalls.length === 0)
118
+ return [];
51
119
  const batches = [];
52
- let currentBatch = [];
53
- let currentIsParallel = false;
120
+ let parallelAccum = [];
54
121
  for (const toolCall of toolCalls) {
55
- const isParallel = isParallelizable(toolCall);
56
- if (batches.length === 0 && currentBatch.length === 0) {
57
- currentBatch.push(toolCall);
58
- currentIsParallel = isParallel;
122
+ if (isParallelizable(toolCall)) {
123
+ parallelAccum.push(toolCall);
59
124
  }
60
- else if (isParallel && currentIsParallel) {
61
- currentBatch.push(toolCall);
125
+ else if (isConditionallyParallel(toolCall)) {
126
+ // Can parallelize if no file conflict with existing accumulator
127
+ if (!hasFileConflict(toolCall, parallelAccum)) {
128
+ parallelAccum.push(toolCall);
129
+ }
130
+ else {
131
+ // Conflict: flush current batch, start new one with this tool
132
+ if (parallelAccum.length > 0) {
133
+ batches.push(parallelAccum);
134
+ parallelAccum = [];
135
+ }
136
+ parallelAccum.push(toolCall);
137
+ }
62
138
  }
63
139
  else {
64
- batches.push(currentBatch);
65
- currentBatch = [toolCall];
66
- currentIsParallel = isParallel;
140
+ // Flush any accumulated parallel tools as a single batch
141
+ if (parallelAccum.length > 0) {
142
+ batches.push(parallelAccum);
143
+ parallelAccum = [];
144
+ }
145
+ // Non-parallelizable tool gets its own batch
146
+ batches.push([toolCall]);
67
147
  }
68
148
  }
69
- if (currentBatch.length > 0) {
70
- batches.push(currentBatch);
149
+ // Flush remaining parallel tools
150
+ if (parallelAccum.length > 0) {
151
+ batches.push(parallelAccum);
71
152
  }
72
153
  return batches;
73
154
  }
@@ -99,6 +180,7 @@ export class ProductionAgent {
99
180
  skillManager = null;
100
181
  contextEngineering = null;
101
182
  codebaseContext = null;
183
+ codebaseAnalysisTriggered = false;
102
184
  traceCollector = null;
103
185
  modeManager;
104
186
  pendingPlanManager;
@@ -117,6 +199,15 @@ export class ProductionAgent {
117
199
  taskManager = null;
118
200
  store = null;
119
201
  swarmOrchestrator = null;
202
+ workLog = null;
203
+ verificationGate = null;
204
+ // Phase 2-4 integration modules
205
+ injectionBudget = null;
206
+ selfImprovement = null;
207
+ subagentOutputStore = null;
208
+ autoCheckpointManager = null;
209
+ toolRecommendation = null;
210
+ lastComplexityAssessment = null;
120
211
  // Duplicate spawn prevention - tracks recently spawned tasks to prevent doom loops
121
212
  // Map<taskKey, { timestamp: number; result: string; queuedChanges: number }>
122
213
  spawnedTasks = new Map();
@@ -132,6 +223,9 @@ export class ProductionAgent {
132
223
  // Cacheable system prompt blocks for prompt caching (Improvement P1)
133
224
  // When set, callLLM() will inject these as structured content with cache_control markers
134
225
  cacheableSystemBlocks = null;
226
+ // Pre-compaction agentic turn: when true, the agent gets one more LLM turn
227
+ // to summarize its state before compaction clears the context.
228
+ compactionPending = false;
135
229
  // Initialization tracking
136
230
  initPromises = [];
137
231
  initComplete = false;
@@ -250,7 +344,29 @@ export class ProductionAgent {
250
344
  }
251
345
  // Safety (Sandbox + Human-in-Loop)
252
346
  if (isFeatureEnabled(this.config.sandbox) || isFeatureEnabled(this.config.humanInLoop)) {
253
- this.safety = new SafetyManager(isFeatureEnabled(this.config.sandbox) ? this.config.sandbox : false, isFeatureEnabled(this.config.humanInLoop) ? this.config.humanInLoop : false);
347
+ this.safety = new SafetyManager(isFeatureEnabled(this.config.sandbox) ? this.config.sandbox : false, isFeatureEnabled(this.config.humanInLoop) ? this.config.humanInLoop : false, isFeatureEnabled(this.config.policyEngine) ? this.config.policyEngine : false);
348
+ }
349
+ if (isFeatureEnabled(this.config.policyEngine)) {
350
+ const rootPolicy = resolvePolicyProfile({
351
+ policyEngine: this.config.policyEngine,
352
+ sandboxConfig: isFeatureEnabled(this.config.sandbox) ? this.config.sandbox : undefined,
353
+ });
354
+ this.emit({
355
+ type: 'policy.profile.resolved',
356
+ profile: rootPolicy.profileName,
357
+ context: 'root',
358
+ selectionSource: rootPolicy.metadata.selectionSource,
359
+ usedLegacyMappings: rootPolicy.metadata.usedLegacyMappings,
360
+ legacySources: rootPolicy.metadata.legacyMappingSources,
361
+ });
362
+ if (rootPolicy.metadata.usedLegacyMappings) {
363
+ this.emit({
364
+ type: 'policy.legacy.fallback.used',
365
+ profile: rootPolicy.profileName,
366
+ sources: rootPolicy.metadata.legacyMappingSources,
367
+ warnings: rootPolicy.metadata.warnings,
368
+ });
369
+ }
254
370
  }
255
371
  // Routing
256
372
  if (isFeatureEnabled(this.config.routing)) {
@@ -311,6 +427,19 @@ export class ProductionAgent {
311
427
  maxIterations: this.config.maxIterations,
312
428
  targetIterations: Math.min(baseBudget.targetIterations ?? 20, this.config.maxIterations),
313
429
  });
430
+ // Work Log - compaction-resilient summary of agent work
431
+ // Always enabled - minimal overhead and critical for long-running tasks
432
+ this.workLog = createWorkLog();
433
+ // Verification Gate - opt-in completion verification
434
+ if (this.config.verificationCriteria) {
435
+ this.verificationGate = createVerificationGate(this.config.verificationCriteria);
436
+ }
437
+ // Phase 2-4: Orchestration & Advanced modules (always enabled, lightweight)
438
+ this.injectionBudget = createInjectionBudgetManager();
439
+ this.selfImprovement = createSelfImprovementProtocol(undefined, this.learningStore ?? undefined);
440
+ this.subagentOutputStore = createSubagentOutputStore({ persistToFile: false });
441
+ this.autoCheckpointManager = createAutoCheckpointManager({ enabled: true });
442
+ this.toolRecommendation = createToolRecommendationEngine();
314
443
  // Agent Registry - always enabled for subagent support
315
444
  this.agentRegistry = new AgentRegistry();
316
445
  // Load user agents asynchronously - tracked for ensureReady()
@@ -341,6 +470,15 @@ export class ProductionAgent {
341
470
  for (const tool of taskTools) {
342
471
  this.tools.set(tool.name, tool);
343
472
  }
473
+ // Built-in web search (Serper API) — gracefully handles missing API key
474
+ const serperCustomTool = createSerperSearchTool();
475
+ this.tools.set('web_search', {
476
+ name: serperCustomTool.name,
477
+ description: serperCustomTool.description,
478
+ parameters: serperCustomTool.inputSchema,
479
+ execute: serperCustomTool.execute,
480
+ dangerLevel: 'safe',
481
+ });
344
482
  // Swarm Mode (experimental)
345
483
  if (this.config.swarm) {
346
484
  const swarmConfig = this.config.swarm;
@@ -832,11 +970,19 @@ export class ProductionAgent {
832
970
  else {
833
971
  // Single-task mode (backward compatibility) - start session with task
834
972
  const traceSessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
835
- await this.traceCollector?.startSession(traceSessionId, task, this.config.model || 'default', {});
973
+ const sessionMetadata = {};
974
+ if (this.swarmOrchestrator) {
975
+ sessionMetadata.swarm = true;
976
+ }
977
+ await this.traceCollector?.startSession(traceSessionId, task, this.config.model || 'default', sessionMetadata);
836
978
  }
837
979
  try {
838
980
  // Check for cancellation before starting
839
981
  cancellationToken?.throwIfCancellationRequested();
982
+ // Classify task complexity for scaling guidance
983
+ this.lastComplexityAssessment = classifyComplexity(task, {
984
+ hasActivePlan: !!this.state.plan,
985
+ });
840
986
  // Check if swarm mode should handle this task
841
987
  if (this.swarmOrchestrator) {
842
988
  const swarmResult = await this.runSwarm(task);
@@ -987,6 +1133,161 @@ export class ProductionAgent {
987
1133
  const { SwarmEventBridge } = await import('./integrations/swarm/swarm-event-bridge.js');
988
1134
  const bridge = new SwarmEventBridge({ outputDir: '.agent/swarm-live' });
989
1135
  const unsubBridge = bridge.attach(this.swarmOrchestrator);
1136
+ // Bridge swarm events into JSONL trace pipeline
1137
+ const traceCollector = this.traceCollector;
1138
+ let unsubTrace;
1139
+ if (traceCollector) {
1140
+ unsubTrace = this.swarmOrchestrator.subscribe(event => {
1141
+ switch (event.type) {
1142
+ case 'swarm.start':
1143
+ traceCollector.record({
1144
+ type: 'swarm.start',
1145
+ data: { taskCount: event.taskCount, config: event.config },
1146
+ });
1147
+ break;
1148
+ case 'swarm.tasks.loaded':
1149
+ traceCollector.record({
1150
+ type: 'swarm.decomposition',
1151
+ data: {
1152
+ tasks: event.tasks.map(t => ({
1153
+ id: t.id,
1154
+ description: t.description.slice(0, 200),
1155
+ type: t.type,
1156
+ wave: t.wave,
1157
+ deps: t.dependencies,
1158
+ })),
1159
+ totalWaves: Math.max(...event.tasks.map(t => t.wave), 0) + 1,
1160
+ },
1161
+ });
1162
+ break;
1163
+ case 'swarm.wave.start':
1164
+ traceCollector.record({
1165
+ type: 'swarm.wave',
1166
+ data: { phase: 'start', wave: event.wave, taskCount: event.taskCount },
1167
+ });
1168
+ break;
1169
+ case 'swarm.wave.complete':
1170
+ traceCollector.record({
1171
+ type: 'swarm.wave',
1172
+ data: {
1173
+ phase: 'complete',
1174
+ wave: event.wave,
1175
+ taskCount: event.completed + event.failed + (event.skipped ?? 0),
1176
+ completed: event.completed,
1177
+ failed: event.failed,
1178
+ },
1179
+ });
1180
+ break;
1181
+ case 'swarm.task.dispatched':
1182
+ traceCollector.record({
1183
+ type: 'swarm.task',
1184
+ data: { phase: 'dispatched', taskId: event.taskId, model: event.model },
1185
+ });
1186
+ break;
1187
+ case 'swarm.task.completed':
1188
+ traceCollector.record({
1189
+ type: 'swarm.task',
1190
+ data: {
1191
+ phase: 'completed',
1192
+ taskId: event.taskId,
1193
+ tokensUsed: event.tokensUsed,
1194
+ costUsed: event.costUsed,
1195
+ qualityScore: event.qualityScore,
1196
+ },
1197
+ });
1198
+ break;
1199
+ case 'swarm.task.failed':
1200
+ traceCollector.record({
1201
+ type: 'swarm.task',
1202
+ data: { phase: 'failed', taskId: event.taskId, error: event.error },
1203
+ });
1204
+ break;
1205
+ case 'swarm.task.skipped':
1206
+ traceCollector.record({
1207
+ type: 'swarm.task',
1208
+ data: { phase: 'skipped', taskId: event.taskId, reason: event.reason },
1209
+ });
1210
+ break;
1211
+ case 'swarm.quality.rejected':
1212
+ traceCollector.record({
1213
+ type: 'swarm.quality',
1214
+ data: { taskId: event.taskId, score: event.score, feedback: event.feedback },
1215
+ });
1216
+ break;
1217
+ case 'swarm.budget.update':
1218
+ traceCollector.record({
1219
+ type: 'swarm.budget',
1220
+ data: {
1221
+ tokensUsed: event.tokensUsed,
1222
+ tokensTotal: event.tokensTotal,
1223
+ costUsed: event.costUsed,
1224
+ costTotal: event.costTotal,
1225
+ },
1226
+ });
1227
+ break;
1228
+ case 'swarm.verify.start':
1229
+ traceCollector.record({
1230
+ type: 'swarm.verification',
1231
+ data: { phase: 'start', description: `${event.stepCount} verification steps` },
1232
+ });
1233
+ break;
1234
+ case 'swarm.verify.step':
1235
+ traceCollector.record({
1236
+ type: 'swarm.verification',
1237
+ data: {
1238
+ phase: 'step',
1239
+ stepIndex: event.stepIndex,
1240
+ description: event.description,
1241
+ passed: event.passed,
1242
+ },
1243
+ });
1244
+ break;
1245
+ case 'swarm.verify.complete':
1246
+ traceCollector.record({
1247
+ type: 'swarm.verification',
1248
+ data: {
1249
+ phase: 'complete',
1250
+ passed: event.result.passed,
1251
+ summary: event.result.summary,
1252
+ },
1253
+ });
1254
+ break;
1255
+ case 'swarm.orchestrator.llm':
1256
+ traceCollector.record({
1257
+ type: 'swarm.orchestrator.llm',
1258
+ data: { model: event.model, purpose: event.purpose, tokens: event.tokens, cost: event.cost },
1259
+ });
1260
+ break;
1261
+ case 'swarm.wave.allFailed':
1262
+ traceCollector.record({
1263
+ type: 'swarm.wave.allFailed',
1264
+ data: { wave: event.wave },
1265
+ });
1266
+ break;
1267
+ case 'swarm.phase.progress':
1268
+ traceCollector.record({
1269
+ type: 'swarm.phase.progress',
1270
+ data: { phase: event.phase, message: event.message },
1271
+ });
1272
+ break;
1273
+ case 'swarm.complete':
1274
+ traceCollector.record({
1275
+ type: 'swarm.complete',
1276
+ data: {
1277
+ stats: {
1278
+ totalTasks: event.stats.totalTasks,
1279
+ completedTasks: event.stats.completedTasks,
1280
+ failedTasks: event.stats.failedTasks,
1281
+ totalTokens: event.stats.totalTokens,
1282
+ totalCost: event.stats.totalCost,
1283
+ totalDuration: event.stats.totalDurationMs,
1284
+ },
1285
+ },
1286
+ });
1287
+ break;
1288
+ }
1289
+ });
1290
+ }
990
1291
  try {
991
1292
  const result = await this.swarmOrchestrator.execute(task);
992
1293
  // Populate task DAG for dashboard after execution
@@ -1001,6 +1302,7 @@ export class ProductionAgent {
1001
1302
  return result;
1002
1303
  }
1003
1304
  finally {
1305
+ unsubTrace?.();
1004
1306
  unsubBridge();
1005
1307
  bridge.close();
1006
1308
  unsubSwarm();
@@ -1119,6 +1421,14 @@ export class ProductionAgent {
1119
1421
  content: `[CONTEXT REDUCED: Earlier messages were removed to stay within budget. Conversation continues from recent context.]`,
1120
1422
  });
1121
1423
  messages.push(...recentMessages);
1424
+ // Inject work log after emergency truncation to prevent amnesia
1425
+ if (this.workLog?.hasContent()) {
1426
+ const workLogMessage = {
1427
+ role: 'user',
1428
+ content: this.workLog.toCompactString(),
1429
+ };
1430
+ messages.push(workLogMessage);
1431
+ }
1122
1432
  // Update state messages too
1123
1433
  this.state.messages.length = 0;
1124
1434
  this.state.messages.push(...messages);
@@ -1299,6 +1609,35 @@ export class ProductionAgent {
1299
1609
  }
1300
1610
  }
1301
1611
  // =====================================================================
1612
+ // INJECTION BUDGET ANALYSIS (Phase 2 - monitoring mode)
1613
+ // Collects stats on context injections without gating; logs when
1614
+ // budget would have dropped items. Validates system before enabling gating.
1615
+ // =====================================================================
1616
+ if (this.injectionBudget) {
1617
+ const proposals = [];
1618
+ if (budgetInjectedPrompt) {
1619
+ proposals.push({ name: 'budget_warning', priority: 0, maxTokens: 500, content: budgetInjectedPrompt });
1620
+ }
1621
+ // Approximate recitation content (actual injection handled above)
1622
+ if (this.contextEngineering) {
1623
+ const failureCtx = this.contextEngineering.getFailureContext(5);
1624
+ if (failureCtx) {
1625
+ proposals.push({ name: 'failure_context', priority: 2, maxTokens: 300, content: failureCtx });
1626
+ }
1627
+ }
1628
+ if (proposals.length > 0) {
1629
+ const accepted = this.injectionBudget.allocate(proposals);
1630
+ const stats = this.injectionBudget.getLastStats();
1631
+ if (stats && stats.droppedNames.length > 0 && process.env.DEBUG) {
1632
+ console.log(`[injection-budget] Would drop: ${stats.droppedNames.join(', ')} (${stats.proposedTokens} proposed, ${stats.acceptedTokens} accepted)`);
1633
+ }
1634
+ // Log total injection overhead for observability
1635
+ if (stats && process.env.DEBUG_LLM) {
1636
+ console.log(`[injection-budget] Iteration ${this.state.iteration}: ${accepted.length}/${proposals.length} injections, ~${stats.acceptedTokens} tokens`);
1637
+ }
1638
+ }
1639
+ }
1640
+ // =====================================================================
1302
1641
  // RESILIENT LLM CALL: Empty response retries + max_tokens continuation
1303
1642
  // =====================================================================
1304
1643
  // Get resilience config
@@ -1594,6 +1933,24 @@ export class ProductionAgent {
1594
1933
  });
1595
1934
  incompleteActionRetries = 0;
1596
1935
  }
1936
+ // Verification gate: if criteria not met, nudge agent to verify before completing
1937
+ if (this.verificationGate && !forceTextOnly) {
1938
+ const vResult = this.verificationGate.check();
1939
+ if (!vResult.satisfied && !vResult.forceAllow && vResult.nudge) {
1940
+ // Inject nudge and continue the loop
1941
+ const nudgeMessage = {
1942
+ role: 'user',
1943
+ content: vResult.nudge,
1944
+ };
1945
+ messages.push(nudgeMessage);
1946
+ this.state.messages.push(nudgeMessage);
1947
+ this.observability?.logger?.info('Verification gate nudge', {
1948
+ missing: vResult.missing,
1949
+ nudgeCount: this.verificationGate.getState().nudgeCount,
1950
+ });
1951
+ continue;
1952
+ }
1953
+ }
1597
1954
  // No tool calls (or forced to ignore), agent is done - compact tool outputs to save context
1598
1955
  // The model has "consumed" the tool outputs and produced a response,
1599
1956
  // so we can replace verbose outputs with compact summaries
@@ -1630,12 +1987,33 @@ export class ProductionAgent {
1630
1987
  // Execute tool calls (we know toolCalls is defined here due to the check above)
1631
1988
  const toolCalls = response.toolCalls;
1632
1989
  const toolResults = await this.executeToolCalls(toolCalls);
1633
- // Record tool calls for economics/progress tracking
1990
+ // Record tool calls for economics/progress tracking + work log
1634
1991
  for (let i = 0; i < toolCalls.length; i++) {
1635
1992
  const toolCall = toolCalls[i];
1636
1993
  const result = toolResults[i];
1637
1994
  executedToolNames.add(toolCall.name);
1638
1995
  this.economics?.recordToolCall(toolCall.name, toolCall.arguments, result?.result);
1996
+ // Record in work log for compaction resilience
1997
+ const toolOutput = result?.result && typeof result.result === 'object' && 'output' in result.result
1998
+ ? String(result.result.output)
1999
+ : typeof result?.result === 'string' ? result.result : undefined;
2000
+ this.workLog?.recordToolExecution(toolCall.name, toolCall.arguments, toolOutput);
2001
+ // Record in verification gate
2002
+ if (this.verificationGate) {
2003
+ if (toolCall.name === 'bash') {
2004
+ const toolRes = result?.result;
2005
+ const output = toolRes && typeof toolRes === 'object' && 'output' in toolRes
2006
+ ? String(toolRes.output)
2007
+ : typeof toolRes === 'string' ? toolRes : '';
2008
+ const exitCode = toolRes && typeof toolRes === 'object' && toolRes.metadata
2009
+ ? toolRes.metadata.exitCode ?? null
2010
+ : null;
2011
+ this.verificationGate.recordBashExecution(String(toolCall.arguments.command || ''), output, exitCode);
2012
+ }
2013
+ if (['write_file', 'edit_file'].includes(toolCall.name)) {
2014
+ this.verificationGate.recordFileChange();
2015
+ }
2016
+ }
1639
2017
  }
1640
2018
  // Add tool results to messages (with truncation and proactive budget management)
1641
2019
  const MAX_TOOL_OUTPUT_CHARS = 8000; // ~2000 tokens max per tool output
@@ -1652,11 +2030,100 @@ export class ProductionAgent {
1652
2030
  });
1653
2031
  // Handle compaction result
1654
2032
  if (compactionResult.status === 'compacted' && compactionResult.compactedMessages) {
1655
- // Replace messages with compacted version
1656
- messages.length = 0;
1657
- messages.push(...compactionResult.compactedMessages);
1658
- this.state.messages.length = 0;
1659
- this.state.messages.push(...compactionResult.compactedMessages);
2033
+ // ─── Pre-compaction agentic turn ───────────────────────────────
2034
+ // Give the agent one LLM turn to summarize critical state before
2035
+ // compaction clears the context. On the first trigger we inject a
2036
+ // system message and skip compaction; on the next trigger (the
2037
+ // agent has already responded) we proceed with actual compaction.
2038
+ if (!this.compactionPending) {
2039
+ this.compactionPending = true;
2040
+ const preCompactionMsg = {
2041
+ role: 'user',
2042
+ content: '[SYSTEM] Context compaction is imminent. Summarize your current progress, key findings, and next steps into a single concise message. This will be preserved after compaction.',
2043
+ };
2044
+ messages.push(preCompactionMsg);
2045
+ this.state.messages.push(preCompactionMsg);
2046
+ this.observability?.logger?.info('Pre-compaction agentic turn: injected summary request');
2047
+ // Skip compaction this iteration — let the agent respond first
2048
+ // (continue to tool result processing below)
2049
+ }
2050
+ else {
2051
+ // Agent has had its chance to summarize — now compact for real
2052
+ this.compactionPending = false;
2053
+ // Pre-compaction checkpoint: save full state before discarding
2054
+ try {
2055
+ this.autoCheckpoint(true); // force=true bypasses frequency check
2056
+ }
2057
+ catch {
2058
+ // Non-critical — don't block compaction
2059
+ }
2060
+ // Replace messages with compacted version
2061
+ messages.length = 0;
2062
+ messages.push(...compactionResult.compactedMessages);
2063
+ this.state.messages.length = 0;
2064
+ this.state.messages.push(...compactionResult.compactedMessages);
2065
+ // Inject work log after compaction to prevent amnesia
2066
+ if (this.workLog?.hasContent()) {
2067
+ const workLogMessage = {
2068
+ role: 'user',
2069
+ content: this.workLog.toCompactString(),
2070
+ };
2071
+ messages.push(workLogMessage);
2072
+ this.state.messages.push(workLogMessage);
2073
+ }
2074
+ // Context recovery: re-inject critical state after compaction
2075
+ const recoveryParts = [];
2076
+ // Goals
2077
+ if (this.store) {
2078
+ const goalsSummary = this.store.getGoalsSummary();
2079
+ if (goalsSummary && goalsSummary !== 'No active goals.' && goalsSummary !== 'Goals feature not available.') {
2080
+ recoveryParts.push(goalsSummary);
2081
+ }
2082
+ }
2083
+ // Junctures (last 5 key moments)
2084
+ if (this.store) {
2085
+ const juncturesSummary = this.store.getJuncturesSummary(undefined, 5);
2086
+ if (juncturesSummary) {
2087
+ recoveryParts.push(juncturesSummary);
2088
+ }
2089
+ }
2090
+ // Learnings from past patterns
2091
+ if (this.learningStore) {
2092
+ const learnings = this.learningStore.getLearningContext({ maxLearnings: 3 });
2093
+ if (learnings) {
2094
+ recoveryParts.push(learnings);
2095
+ }
2096
+ }
2097
+ if (recoveryParts.length > 0) {
2098
+ const recoveryMessage = {
2099
+ role: 'user',
2100
+ content: `[CONTEXT RECOVERY — Re-injected after compaction]\n\n${recoveryParts.join('\n\n')}`,
2101
+ };
2102
+ messages.push(recoveryMessage);
2103
+ this.state.messages.push(recoveryMessage);
2104
+ }
2105
+ // Emit compaction event for observability
2106
+ const compactionTokensAfter = this.estimateContextTokens(messages);
2107
+ const compactionRecoveryInjected = recoveryParts.length > 0;
2108
+ const compactionEvent = {
2109
+ type: 'context.compacted',
2110
+ tokensBefore: currentContextTokens,
2111
+ tokensAfter: compactionTokensAfter,
2112
+ recoveryInjected: compactionRecoveryInjected,
2113
+ };
2114
+ this.emit(compactionEvent);
2115
+ // Record to trace collector for JSONL output
2116
+ if (this.traceCollector) {
2117
+ this.traceCollector.record({
2118
+ type: 'context.compacted',
2119
+ data: {
2120
+ tokensBefore: currentContextTokens,
2121
+ tokensAfter: compactionTokensAfter,
2122
+ recoveryInjected: compactionRecoveryInjected,
2123
+ },
2124
+ });
2125
+ }
2126
+ }
1660
2127
  }
1661
2128
  else if (compactionResult.status === 'hard_limit') {
1662
2129
  // Hard limit reached - this is serious, emit error
@@ -1679,6 +2146,13 @@ export class ProductionAgent {
1679
2146
  currentTokens: currentUsage.tokens,
1680
2147
  maxTokens: budget.maxTokens,
1681
2148
  });
2149
+ // Also checkpoint before fallback compaction
2150
+ try {
2151
+ this.autoCheckpoint(true);
2152
+ }
2153
+ catch {
2154
+ // Non-critical
2155
+ }
1682
2156
  this.compactToolOutputs();
1683
2157
  }
1684
2158
  }
@@ -1688,8 +2162,10 @@ export class ProductionAgent {
1688
2162
  const sourceToolName = toolCallNameById.get(result.callId);
1689
2163
  const isExpensiveResult = sourceToolName === 'spawn_agent' || sourceToolName === 'spawn_agents_parallel';
1690
2164
  // Truncate long outputs to save context
1691
- if (content.length > MAX_TOOL_OUTPUT_CHARS) {
1692
- content = content.slice(0, MAX_TOOL_OUTPUT_CHARS) + `\n\n... [truncated ${content.length - MAX_TOOL_OUTPUT_CHARS} chars]`;
2165
+ // Use larger limit for subagent results to preserve critical context
2166
+ const effectiveMaxChars = isExpensiveResult ? MAX_TOOL_OUTPUT_CHARS * 2 : MAX_TOOL_OUTPUT_CHARS;
2167
+ if (content.length > effectiveMaxChars) {
2168
+ content = content.slice(0, effectiveMaxChars) + `\n\n... [truncated ${content.length - effectiveMaxChars} chars]`;
1693
2169
  }
1694
2170
  // =======================================================================
1695
2171
  // ESTIMATE if adding this result would exceed budget
@@ -1813,10 +2289,14 @@ export class ProductionAgent {
1813
2289
  const reservedTokens = 10500;
1814
2290
  const maxContextTokens = (this.config.maxContextTokens ?? 80000) - reservedTokens;
1815
2291
  const codebaseBudget = Math.min(maxContextTokens * 0.3, 15000); // Up to 30% or 15K tokens
1816
- try {
1817
- // Use synchronous cache if available, otherwise skip
1818
- const repoMap = this.codebaseContext.getRepoMap();
1819
- if (repoMap) {
2292
+ const repoMap = this.codebaseContext.getRepoMap();
2293
+ // Lazy: trigger analysis on first system prompt build, ready by next turn
2294
+ if (!repoMap && !this.codebaseAnalysisTriggered) {
2295
+ this.codebaseAnalysisTriggered = true;
2296
+ this.codebaseContext.analyze().catch(() => { });
2297
+ }
2298
+ if (repoMap) {
2299
+ try {
1820
2300
  const selection = this.selectRelevantCodeSync(task, codebaseBudget);
1821
2301
  if (selection.chunks.length > 0) {
1822
2302
  codebaseContextStr = buildContextFromChunks(selection.chunks, {
@@ -1825,10 +2305,14 @@ export class ProductionAgent {
1825
2305
  maxTotalTokens: codebaseBudget,
1826
2306
  });
1827
2307
  }
2308
+ else {
2309
+ // Fallback: lightweight repo map when task-specific selection finds nothing
2310
+ codebaseContextStr = generateLightweightRepoMap(repoMap, codebaseBudget);
2311
+ }
2312
+ }
2313
+ catch {
2314
+ // Selection error — skip
1828
2315
  }
1829
- }
1830
- catch {
1831
- // Codebase analysis not ready yet - skip for this call
1832
2316
  }
1833
2317
  }
1834
2318
  // Build tool descriptions
@@ -1851,12 +2335,25 @@ export class ProductionAgent {
1851
2335
  }
1852
2336
  }
1853
2337
  // Build system prompt using cache-aware builder if available (Trick P)
1854
- // Combine memory, learnings, and codebase context
1855
- const combinedContext = [
2338
+ // Combine memory, learnings, codebase context, and environment facts
2339
+ const combinedContextParts = [
2340
+ // Environment facts — temporal/platform grounding (prevents stale date hallucinations)
2341
+ formatFactsBlock(getEnvironmentFacts()),
1856
2342
  ...(memoryContext.length > 0 ? memoryContext : []),
1857
2343
  ...(learningsContext ? [learningsContext] : []),
1858
2344
  ...(codebaseContextStr ? [`\n## Relevant Code\n${codebaseContextStr}`] : []),
1859
- ].join('\n');
2345
+ ];
2346
+ // Inject thinking directives and scaling guidance for non-simple tasks
2347
+ if (this.lastComplexityAssessment) {
2348
+ const thinkingPrompt = getThinkingSystemPrompt(this.lastComplexityAssessment.tier);
2349
+ if (thinkingPrompt) {
2350
+ combinedContextParts.push(thinkingPrompt);
2351
+ }
2352
+ if (this.lastComplexityAssessment.tier !== 'simple') {
2353
+ combinedContextParts.push(getScalingGuidance(this.lastComplexityAssessment));
2354
+ }
2355
+ }
2356
+ const combinedContext = combinedContextParts.join('\n');
1860
2357
  const promptOptions = {
1861
2358
  rules: rulesContent + (skillsPrompt ? '\n\n' + skillsPrompt : ''),
1862
2359
  tools: toolDescriptions,
@@ -1996,6 +2493,8 @@ export class ProductionAgent {
1996
2493
  },
1997
2494
  },
1998
2495
  });
2496
+ // Pause duration budget during LLM call - network time shouldn't count against agent
2497
+ this.economics?.pauseDuration();
1999
2498
  try {
2000
2499
  let response;
2001
2500
  let actualModel = model;
@@ -2135,6 +2634,10 @@ export class ProductionAgent {
2135
2634
  this.observability?.tracer?.endSpan(spanId);
2136
2635
  throw error;
2137
2636
  }
2637
+ finally {
2638
+ // Resume duration budget after LLM call completes (success or failure)
2639
+ this.economics?.resumeDuration();
2640
+ }
2138
2641
  }
2139
2642
  /**
2140
2643
  * Execute an async callback while excluding wall-clock wait time from duration budgeting.
@@ -2287,6 +2790,12 @@ export class ProductionAgent {
2287
2790
  });
2288
2791
  // Handle forbidden policy - always block
2289
2792
  if (evaluation.policy === 'forbidden') {
2793
+ this.emit({
2794
+ type: 'policy.tool.blocked',
2795
+ tool: toolCall.name,
2796
+ phase: 'enforced',
2797
+ reason: `Forbidden by execution policy: ${evaluation.reason}`,
2798
+ });
2290
2799
  throw new Error(`Forbidden by policy: ${evaluation.reason}`);
2291
2800
  }
2292
2801
  // Handle prompt policy - requires approval
@@ -2329,6 +2838,21 @@ export class ProductionAgent {
2329
2838
  const safety = this.safety;
2330
2839
  const validation = await this.withPausedDuration(() => safety.validateAndApprove(toolCall, `Executing tool: ${toolCall.name}`, { skipHumanApproval: policyApprovedByUser }));
2331
2840
  if (!validation.allowed) {
2841
+ this.emit({
2842
+ type: 'policy.tool.blocked',
2843
+ tool: toolCall.name,
2844
+ phase: 'enforced',
2845
+ reason: validation.reason || 'Blocked by safety manager',
2846
+ });
2847
+ if (toolCall.name === 'bash') {
2848
+ const args = toolCall.arguments;
2849
+ this.emit({
2850
+ type: 'policy.bash.blocked',
2851
+ phase: 'enforced',
2852
+ command: String(args.command || args.cmd || ''),
2853
+ reason: validation.reason || 'Blocked by safety manager',
2854
+ });
2855
+ }
2332
2856
  throw new Error(`Tool call blocked: ${validation.reason}`);
2333
2857
  }
2334
2858
  }
@@ -2467,6 +2991,8 @@ export class ProductionAgent {
2467
2991
  this.blackboard.release(filePath, agentId);
2468
2992
  }
2469
2993
  }
2994
+ // Self-improvement: record success pattern
2995
+ this.selfImprovement?.recordSuccess(toolCall.name, toolCall.arguments, typeof result === 'string' ? result.slice(0, 200) : JSON.stringify(result).slice(0, 200));
2470
2996
  this.observability?.tracer?.endSpan(spanId);
2471
2997
  return { callId: toolCall.id, result };
2472
2998
  }
@@ -2494,6 +3020,12 @@ export class ProductionAgent {
2494
3020
  error,
2495
3021
  intent: `Execute tool ${toolCall.name}`,
2496
3022
  });
3023
+ // Self-improvement: enhance error message with diagnosis for better LLM recovery
3024
+ if (this.selfImprovement) {
3025
+ const enhanced = this.selfImprovement.enhanceErrorMessage(toolCall.name, error.message, toolCall.arguments);
3026
+ this.emit({ type: 'tool.blocked', tool: toolCall.name, reason: enhanced });
3027
+ return { callId: toolCall.id, result: `Error: ${enhanced}`, error: enhanced };
3028
+ }
2497
3029
  this.emit({ type: 'tool.blocked', tool: toolCall.name, reason: error.message });
2498
3030
  return { callId: toolCall.id, result: `Error: ${error.message}`, error: error.message };
2499
3031
  }
@@ -3381,6 +3913,12 @@ export class ProductionAgent {
3381
3913
  return null;
3382
3914
  return this.economics.getProgress();
3383
3915
  }
3916
+ /**
3917
+ * Get actual file paths modified during this agent's session.
3918
+ */
3919
+ getModifiedFilePaths() {
3920
+ return this.economics?.getModifiedFilePaths() ?? [];
3921
+ }
3384
3922
  /**
3385
3923
  * Extend the budget limits.
3386
3924
  */
@@ -3522,6 +4060,19 @@ export class ProductionAgent {
3522
4060
  }
3523
4061
  // Create the checkpoint
3524
4062
  const label = `auto-iter-${this.state.iteration}`;
4063
+ // Supplementary: also save to AutoCheckpointManager (file-based)
4064
+ if (this.autoCheckpointManager) {
4065
+ try {
4066
+ this.autoCheckpointManager.save({
4067
+ label,
4068
+ sessionId: this.agentId,
4069
+ iteration: this.state.iteration,
4070
+ });
4071
+ }
4072
+ catch {
4073
+ // Non-critical — don't fail the main checkpoint path
4074
+ }
4075
+ }
3525
4076
  return this.createCheckpoint(label);
3526
4077
  }
3527
4078
  // =========================================================================
@@ -3675,7 +4226,75 @@ export class ProductionAgent {
3675
4226
  let workerResultId;
3676
4227
  try {
3677
4228
  // Filter tools for this agent
3678
- const agentTools = filterToolsForAgent(agentDef, Array.from(this.tools.values()));
4229
+ let agentTools = filterToolsForAgent(agentDef, Array.from(this.tools.values()));
4230
+ // Resolve policy profile FIRST so we know which tools the policy allows.
4231
+ // This must happen before the recommendation filter so policy-allowed tools
4232
+ // are preserved through the recommendation pruning step.
4233
+ const inferredTaskType = agentDef.taskType ?? ToolRecommendationEngine.inferTaskType(agentName);
4234
+ const policyResolution = resolvePolicyProfile({
4235
+ policyEngine: this.config.policyEngine,
4236
+ requestedProfile: agentDef.policyProfile,
4237
+ swarmConfig: isSwarmWorker && this.config.swarm && typeof this.config.swarm === 'object'
4238
+ ? this.config.swarm
4239
+ : undefined,
4240
+ taskType: inferredTaskType,
4241
+ isSwarmWorker,
4242
+ sandboxConfig: this.config.sandbox && typeof this.config.sandbox === 'object'
4243
+ ? this.config.sandbox
4244
+ : undefined,
4245
+ });
4246
+ this.emit({
4247
+ type: 'policy.profile.resolved',
4248
+ profile: policyResolution.profileName,
4249
+ context: isSwarmWorker ? 'swarm' : 'subagent',
4250
+ selectionSource: policyResolution.metadata.selectionSource,
4251
+ usedLegacyMappings: policyResolution.metadata.usedLegacyMappings,
4252
+ legacySources: policyResolution.metadata.legacyMappingSources,
4253
+ });
4254
+ if (policyResolution.metadata.usedLegacyMappings) {
4255
+ this.emit({
4256
+ type: 'policy.legacy.fallback.used',
4257
+ profile: policyResolution.profileName,
4258
+ sources: policyResolution.metadata.legacyMappingSources,
4259
+ warnings: policyResolution.metadata.warnings,
4260
+ });
4261
+ this.observability?.logger?.warn('Policy legacy mappings used', {
4262
+ agent: agentName,
4263
+ profile: policyResolution.profileName,
4264
+ sources: policyResolution.metadata.legacyMappingSources,
4265
+ });
4266
+ }
4267
+ // Apply tool recommendations to improve subagent focus (only for large tool sets)
4268
+ if (this.toolRecommendation && agentTools.length > 15) {
4269
+ const taskType = ToolRecommendationEngine.inferTaskType(agentName);
4270
+ const recommendations = this.toolRecommendation.recommendTools(task, taskType, agentTools.map(t => t.name));
4271
+ if (recommendations.length > 0) {
4272
+ const recommendedNames = new Set(recommendations.map(r => r.toolName));
4273
+ // Always keep spawn tools even if not recommended
4274
+ const alwaysKeep = new Set(['spawn_agent', 'spawn_agents_parallel']);
4275
+ // Also keep tools that the resolved policy profile explicitly allows.
4276
+ // This prevents the recommendation engine from stripping tools that the
4277
+ // security policy says the worker should have.
4278
+ if (policyResolution.profile.allowedTools) {
4279
+ for (const t of policyResolution.profile.allowedTools)
4280
+ alwaysKeep.add(t);
4281
+ }
4282
+ agentTools = agentTools.filter(t => recommendedNames.has(t.name) || alwaysKeep.has(t.name));
4283
+ }
4284
+ }
4285
+ // Enforce unified tool policy at spawn-time so denied tools are never exposed.
4286
+ if (policyResolution.profile.toolAccessMode === 'whitelist' && policyResolution.profile.allowedTools) {
4287
+ const allowed = new Set(policyResolution.profile.allowedTools);
4288
+ agentTools = agentTools.filter(t => allowed.has(t.name));
4289
+ }
4290
+ else if (policyResolution.profile.deniedTools && policyResolution.profile.deniedTools.length > 0) {
4291
+ const denied = new Set(policyResolution.profile.deniedTools);
4292
+ agentTools = agentTools.filter(t => !denied.has(t.name));
4293
+ }
4294
+ // Fail fast if tool filtering resulted in zero tools — the worker can't do anything
4295
+ if (agentTools.length === 0) {
4296
+ throw new Error(`Worker '${agentName}' has zero available tools after filtering. Check toolAccessMode and policy profile '${policyResolution.profileName}'.`);
4297
+ }
3679
4298
  // Resolve model - abstract tiers (fast/balanced/quality) should use parent's model
3680
4299
  // Only use agentDef.model if it's an actual model ID (contains '/')
3681
4300
  const resolvedModel = (agentDef.model && agentDef.model.includes('/'))
@@ -3698,7 +4317,8 @@ export class ProductionAgent {
3698
4317
  // Precedence: per-type config > per-type default > global config > hardcoded fallback
3699
4318
  const subagentConfig = this.config.subagent;
3700
4319
  const hasSubagentConfig = subagentConfig !== false && subagentConfig !== undefined;
3701
- // Timeout precedence: per-type config override > agent-type default > global config default
4320
+ // Timeout precedence: agentDef.timeout > per-type config > agent-type default > global config default
4321
+ // agentDef.timeout is set by worker-pool for swarm workers, giving them precise timeout control
3702
4322
  const agentTypeTimeout = getSubagentTimeout(agentName);
3703
4323
  const rawPerTypeTimeout = hasSubagentConfig
3704
4324
  ? subagentConfig.timeouts?.[agentName]
@@ -3708,9 +4328,10 @@ export class ProductionAgent {
3708
4328
  : undefined;
3709
4329
  // Validate: reject negative, NaN, or non-finite timeout values
3710
4330
  const isValidTimeout = (v) => v !== undefined && Number.isFinite(v) && v > 0;
4331
+ const agentDefTimeout = isValidTimeout(agentDef.timeout) ? agentDef.timeout : undefined;
3711
4332
  const perTypeConfigTimeout = isValidTimeout(rawPerTypeTimeout) ? rawPerTypeTimeout : undefined;
3712
4333
  const globalConfigTimeout = isValidTimeout(rawGlobalTimeout) ? rawGlobalTimeout : undefined;
3713
- const subagentTimeout = perTypeConfigTimeout ?? agentTypeTimeout ?? globalConfigTimeout ?? 300000;
4334
+ const subagentTimeout = agentDefTimeout ?? perTypeConfigTimeout ?? agentTypeTimeout ?? globalConfigTimeout ?? 300000;
3714
4335
  // Iteration precedence: per-type config override > agent-type default > global config default
3715
4336
  const agentTypeMaxIter = getSubagentMaxIterations(agentName);
3716
4337
  const rawPerTypeMaxIter = hasSubagentConfig
@@ -3767,14 +4388,29 @@ export class ProductionAgent {
3767
4388
  // BUDGET AWARENESS: Always inject so subagent understands its limits
3768
4389
  const subagentBudgetTokens = constraints?.maxTokens ?? SUBAGENT_BUDGET.maxTokens ?? 100000;
3769
4390
  const subagentBudgetMinutes = Math.round((SUBAGENT_BUDGET.maxDuration ?? 240000) / 60000);
3770
- constraintParts.push(`**RESOURCE AWARENESS (CRITICAL):**\n` +
3771
- `- Token budget: ~${(subagentBudgetTokens / 1000).toFixed(0)}k tokens\n` +
3772
- `- Time limit: ~${subagentBudgetMinutes} minutes\n` +
3773
- `- You will receive warnings at 70% usage. When warned, WRAP UP immediately.\n` +
3774
- `- Do not explore indefinitely - be focused and efficient.\n` +
3775
- `- If approaching limits, summarize findings and return.\n` +
3776
- `- **STRUCTURED WRAPUP:** When told to wrap up, respond with ONLY this JSON (no tool calls):\n` +
3777
- ` {"findings":[...], "actionsTaken":[...], "failures":[...], "remainingWork":[...], "suggestedNextSteps":[...]}`);
4391
+ if (isSwarmWorker) {
4392
+ // V8: Minimal resource awareness for swarm workers — removes budget/time
4393
+ // messaging entirely to prevent cheap models from bail-out anxiety.
4394
+ // The economics system handles budget warnings via system messages when needed.
4395
+ // Wrapup JSON format is ONLY injected when requestWrapup() is called.
4396
+ constraintParts.push(`**Execution Mode:** You are a focused worker agent.\n` +
4397
+ `- Complete your assigned task using tool calls.\n` +
4398
+ `- Your FIRST action must be a tool call (read_file, write_file, edit_file, grep, glob, etc.).\n` +
4399
+ `- To create files use write_file. To modify files use edit_file. Do NOT use bash for file operations.\n` +
4400
+ `- You will receive a system message if you need to wrap up. Until then, work normally.\n` +
4401
+ `- Do NOT produce summaries or reports — produce CODE and FILE CHANGES.`);
4402
+ }
4403
+ else {
4404
+ // Original RESOURCE AWARENESS text for regular subagents
4405
+ constraintParts.push(`**RESOURCE AWARENESS (CRITICAL):**\n` +
4406
+ `- Token budget: ~${(subagentBudgetTokens / 1000).toFixed(0)}k tokens\n` +
4407
+ `- Time limit: ~${subagentBudgetMinutes} minutes\n` +
4408
+ `- You will receive warnings at 70% usage. When warned, WRAP UP immediately.\n` +
4409
+ `- Do not explore indefinitely - be focused and efficient.\n` +
4410
+ `- If approaching limits, summarize findings and return.\n` +
4411
+ `- **STRUCTURED WRAPUP:** When told to wrap up, respond with ONLY this JSON (no tool calls):\n` +
4412
+ ` {"findings":[...], "actionsTaken":[...], "failures":[...], "remainingWork":[...], "suggestedNextSteps":[...]}`);
4413
+ }
3778
4414
  if (constraints) {
3779
4415
  if (constraints.focusAreas && constraints.focusAreas.length > 0) {
3780
4416
  constraintParts.push(`**FOCUS AREAS (limit exploration to these paths):**\n${constraints.focusAreas.map(a => ` - ${a}`).join('\n')}`);
@@ -3790,14 +4426,36 @@ export class ProductionAgent {
3790
4426
  }
3791
4427
  }
3792
4428
  const constraintContext = `\n\n**EXECUTION CONSTRAINTS:**\n${constraintParts.join('\n\n')}\n`;
4429
+ // Build delegation-enhanced system prompt
4430
+ let delegationContext = '';
4431
+ if (this.lastComplexityAssessment && this.lastComplexityAssessment.tier !== 'simple') {
4432
+ const spec = createMinimalDelegationSpec(task, agentName);
4433
+ delegationContext = '\n\n' + buildDelegationPrompt(spec);
4434
+ }
4435
+ // Quality self-assessment prompt for subagent
4436
+ const qualityPrompt = '\n\n' + getSubagentQualityPrompt();
3793
4437
  // Build subagent system prompt with subagent-specific plan mode addition
3794
4438
  const parentMode = this.getMode();
3795
4439
  const subagentSystemPrompt = parentMode === 'plan'
3796
- ? `${agentDef.systemPrompt}\n\n${SUBAGENT_PLAN_MODE_ADDITION}${blackboardContext}${constraintContext}`
3797
- : `${agentDef.systemPrompt}${blackboardContext}${constraintContext}`;
4440
+ ? `${agentDef.systemPrompt}\n\n${SUBAGENT_PLAN_MODE_ADDITION}${blackboardContext}${constraintContext}${delegationContext}${qualityPrompt}`
4441
+ : `${agentDef.systemPrompt}${blackboardContext}${constraintContext}${delegationContext}${qualityPrompt}`;
3798
4442
  // Allocate budget from pool (or use default) — track allocation ID for release later
3799
4443
  const pooledBudget = this.getSubagentBudget(agentName, constraints);
3800
4444
  const poolAllocationId = pooledBudget.allocationId;
4445
+ const deniedByProfile = new Set(policyResolution.profile.deniedTools ?? []);
4446
+ const policyToolPolicies = {};
4447
+ for (const toolName of deniedByProfile) {
4448
+ policyToolPolicies[toolName] = {
4449
+ policy: 'forbidden',
4450
+ reason: `Denied by policy profile '${policyResolution.profileName}'`,
4451
+ };
4452
+ }
4453
+ if ((policyResolution.profile.bashMode ?? 'full') === 'disabled') {
4454
+ policyToolPolicies.bash = {
4455
+ policy: 'forbidden',
4456
+ reason: `Bash is disabled by policy profile '${policyResolution.profileName}'`,
4457
+ };
4458
+ }
3801
4459
  // Create a sub-agent with the agent's config
3802
4460
  // Use SUBAGENT_BUDGET to constrain resource usage (prevents runaway token consumption)
3803
4461
  const subAgent = new ProductionAgent({
@@ -3829,14 +4487,56 @@ export class ProductionAgent {
3829
4487
  // Lower context window for subagents so percentage-based compaction triggers earlier
3830
4488
  maxContextTokens: 80000,
3831
4489
  observability: this.config.observability,
3832
- sandbox: this.config.sandbox,
4490
+ sandbox: (() => {
4491
+ const swarm = this.config.swarm;
4492
+ const extraCmds = swarm && typeof swarm === 'object' && swarm.permissions?.additionalAllowedCommands;
4493
+ const baseSbx = this.config.sandbox;
4494
+ if (baseSbx && typeof baseSbx === 'object') {
4495
+ const sbx = baseSbx;
4496
+ const allowedCommands = extraCmds
4497
+ ? [...(sbx.allowedCommands || []), ...extraCmds]
4498
+ : sbx.allowedCommands;
4499
+ return {
4500
+ ...sbx,
4501
+ allowedCommands,
4502
+ bashMode: policyResolution.profile.bashMode ?? sbx.bashMode,
4503
+ bashWriteProtection: policyResolution.profile.bashWriteProtection ?? sbx.bashWriteProtection,
4504
+ blockFileCreationViaBash: (policyResolution.profile.bashWriteProtection ?? 'off') === 'block_file_mutation'
4505
+ ? true
4506
+ : sbx.blockFileCreationViaBash,
4507
+ };
4508
+ }
4509
+ return baseSbx;
4510
+ })(),
3833
4511
  humanInLoop: this.config.humanInLoop,
3834
4512
  // Subagents get 'allow' as default policy since they're already
3835
4513
  // constrained to their registered tool set. The parent's 'prompt'
3836
4514
  // policy can't work without humanInLoop.
3837
- executionPolicy: this.config.executionPolicy
3838
- ? { ...this.config.executionPolicy, defaultPolicy: 'allow' }
3839
- : this.config.executionPolicy,
4515
+ executionPolicy: (() => {
4516
+ const hasPolicyOverrides = Object.keys(policyToolPolicies).length > 0;
4517
+ if (this.config.executionPolicy) {
4518
+ return {
4519
+ ...this.config.executionPolicy,
4520
+ defaultPolicy: 'allow',
4521
+ toolPolicies: {
4522
+ ...(this.config.executionPolicy.toolPolicies ?? {}),
4523
+ ...policyToolPolicies,
4524
+ },
4525
+ };
4526
+ }
4527
+ if (hasPolicyOverrides) {
4528
+ return {
4529
+ enabled: true,
4530
+ defaultPolicy: 'allow',
4531
+ toolPolicies: policyToolPolicies,
4532
+ intentAware: false,
4533
+ };
4534
+ }
4535
+ return this.config.executionPolicy;
4536
+ })(),
4537
+ policyEngine: this.config.policyEngine
4538
+ ? { ...this.config.policyEngine, defaultProfile: policyResolution.profileName }
4539
+ : this.config.policyEngine,
3840
4540
  threads: false,
3841
4541
  // Disable hooks console output in subagents - parent handles event display
3842
4542
  hooks: this.config.hooks === false ? false : {
@@ -3852,7 +4552,10 @@ export class ProductionAgent {
3852
4552
  fileCache: this.fileCache || undefined,
3853
4553
  // CONSTRAINED BUDGET: Use pooled budget when available, falling back to SUBAGENT_BUDGET
3854
4554
  // Pooled budget ensures total tree cost stays bounded by parent's budget
3855
- budget: pooledBudget.budget,
4555
+ // Merge economicsTuning from agent definition so swarm workers get custom thresholds
4556
+ budget: agentDef.economicsTuning
4557
+ ? { ...pooledBudget.budget, tuning: agentDef.economicsTuning }
4558
+ : pooledBudget.budget,
3856
4559
  });
3857
4560
  // CRITICAL: Subagent inherits parent's mode
3858
4561
  // This ensures that if parent is in plan mode:
@@ -3865,14 +4568,35 @@ export class ProductionAgent {
3865
4568
  // APPROVAL BATCHING (Improvement P6): Set approval scope for subagents
3866
4569
  // Read-only tools are auto-approved; write tools get scoped approval
3867
4570
  // This reduces interruptions from ~8 per session to ~1-2
3868
- subAgent.setApprovalScope({
3869
- autoApprove: ['read_file', 'list_files', 'glob', 'grep', 'show_file_history', 'show_session_changes'],
3870
- scopedApprove: {
4571
+ // Swarm permissions from config override defaults when present
4572
+ const swarmPerms = this.config.swarm && typeof this.config.swarm === 'object'
4573
+ ? this.config.swarm.permissions : undefined;
4574
+ const baseAutoApprove = ['read_file', 'list_files', 'glob', 'grep', 'show_file_history', 'show_session_changes'];
4575
+ const baseScopedApprove = isSwarmWorker
4576
+ ? {
3871
4577
  write_file: { paths: ['src/', 'tests/', 'tools/'] },
3872
4578
  edit_file: { paths: ['src/', 'tests/', 'tools/'] },
3873
- },
3874
- requireApproval: ['bash', 'delete_file'],
3875
- });
4579
+ bash: { paths: ['src/', 'tests/', 'tools/'] },
4580
+ }
4581
+ : {
4582
+ write_file: { paths: ['src/', 'tests/', 'tools/'] },
4583
+ edit_file: { paths: ['src/', 'tests/', 'tools/'] },
4584
+ };
4585
+ const baseRequireApproval = isSwarmWorker ? ['delete_file'] : ['bash', 'delete_file'];
4586
+ const mergedScope = mergeApprovalScopeWithProfile({
4587
+ autoApprove: swarmPerms?.autoApprove
4588
+ ? [...new Set([...baseAutoApprove, ...swarmPerms.autoApprove])]
4589
+ : baseAutoApprove,
4590
+ scopedApprove: swarmPerms?.scopedApprove
4591
+ ? { ...baseScopedApprove, ...swarmPerms.scopedApprove }
4592
+ : baseScopedApprove,
4593
+ // requireApproval: full replacement (not merge) — user may want to REMOVE
4594
+ // tools like 'bash' to let workers run freely
4595
+ requireApproval: swarmPerms?.requireApproval
4596
+ ? swarmPerms.requireApproval
4597
+ : baseRequireApproval,
4598
+ }, policyResolution.profile);
4599
+ subAgent.setApprovalScope(mergedScope);
3876
4600
  // Pass parent's iteration count to subagent for accurate budget tracking
3877
4601
  // This prevents subagents from consuming excessive iterations when parent already used many
3878
4602
  subAgent.setParentIterations(this.getTotalIterations());
@@ -3893,7 +4617,7 @@ export class ProductionAgent {
3893
4617
  // 1. Normal operation: progress extends idle timer
3894
4618
  // 2. Wrapup phase: 30s before hard kill, wrapup callback fires → forceTextOnly
3895
4619
  // 3. Hard kill: race() throws CancellationError after wrapup window
3896
- const IDLE_TIMEOUT = 120000; // 2 minutes without progress = timeout
4620
+ const IDLE_TIMEOUT = agentDef.idleTimeout ?? 120000; // Configurable idle timeout (default: 2 min)
3897
4621
  let WRAPUP_WINDOW = 30000;
3898
4622
  let IDLE_CHECK_INTERVAL = 5000;
3899
4623
  if (this.config.subagent) {
@@ -3996,6 +4720,8 @@ export class ProductionAgent {
3996
4720
  : (result.response || result.error || '');
3997
4721
  // Parse structured closure report from agent's response (if it produced one)
3998
4722
  const structured = parseStructuredClosureReport(result.response || '', 'completed');
4723
+ // Extract real file paths from subagent's economics tracker (before cleanup)
4724
+ const subagentFilePaths = subAgent.getModifiedFilePaths();
3999
4725
  const spawnResultFinal = {
4000
4726
  success: result.success,
4001
4727
  output: finalOutput,
@@ -4005,7 +4731,27 @@ export class ProductionAgent {
4005
4731
  toolCalls: result.metrics.toolCalls,
4006
4732
  },
4007
4733
  structured,
4734
+ filesModified: subagentFilePaths,
4008
4735
  };
4736
+ // Save full output to subagent output store (avoids telephone problem)
4737
+ if (this.subagentOutputStore) {
4738
+ const outputEntry = {
4739
+ id: agentId,
4740
+ agentId,
4741
+ agentName,
4742
+ task,
4743
+ fullOutput: finalOutput,
4744
+ structured,
4745
+ filesModified: subagentFilePaths,
4746
+ filesCreated: [],
4747
+ timestamp: new Date(),
4748
+ tokensUsed: result.metrics.totalTokens,
4749
+ durationMs: duration,
4750
+ };
4751
+ const storeId = this.subagentOutputStore.save(outputEntry);
4752
+ // Attach reference so downstream consumers can retrieve full output
4753
+ spawnResultFinal.outputStoreId = storeId;
4754
+ }
4009
4755
  if (workerResultId && this.store?.hasWorkerResultsFeature()) {
4010
4756
  try {
4011
4757
  this.store.completeWorkerResult(workerResultId, {
@@ -4146,6 +4892,8 @@ export class ProductionAgent {
4146
4892
  this.pendingPlanManager.appendExplorationFinding(`[${agentName}] ${subPlan.explorationSummary}`);
4147
4893
  }
4148
4894
  }
4895
+ // Extract real file paths from subagent's economics tracker (before cleanup)
4896
+ const subagentFilePaths = subAgent.getModifiedFilePaths();
4149
4897
  // Unsubscribe from subagent events and cleanup gracefully
4150
4898
  unsubSubAgent();
4151
4899
  try {
@@ -4213,6 +4961,7 @@ export class ProductionAgent {
4213
4961
  toolCalls: subagentMetrics.toolCalls,
4214
4962
  },
4215
4963
  structured,
4964
+ filesModified: subagentFilePaths,
4216
4965
  };
4217
4966
  }
4218
4967
  throw err; // Re-throw non-cancellation errors
@@ -4310,23 +5059,34 @@ export class ProductionAgent {
4310
5059
  count: tasks.length,
4311
5060
  agents: tasks.map(t => t.agent),
4312
5061
  });
4313
- // Pre-divide budget pool equally to prevent first-come starvation.
4314
- // Temporarily lower maxPerChild so each spawnAgent's normal reserve() call
4315
- // gets an equal share instead of racing for the full maxPerChild allocation.
5062
+ // Use DynamicBudgetPool for parallel spawns (prevents child starvation,
5063
+ // enables priority-based allocation). Falls back to regular pool for single tasks.
4316
5064
  let settled;
5065
+ const originalPool = this.budgetPool;
5066
+ // SubagentSupervisor for unified monitoring of concurrent subagents
5067
+ const supervisor = tasks.length > 1 ? createSubagentSupervisor() : null;
4317
5068
  if (this.budgetPool && tasks.length > 1) {
5069
+ // Swap to DynamicBudgetPool for this parallel batch
4318
5070
  const poolStats = this.budgetPool.getStats();
4319
- // equalShare is always ≤ remaining ≤ totalTokens ≤ originalMaxPerChild
4320
- // (guaranteed by createBudgetPool capping maxPerChild to poolTokens)
4321
- // so we don't need Math.min(equalShare, originalMaxPerChild) here.
4322
- const equalShare = Math.floor(poolStats.tokensRemaining / tasks.length);
4323
- this.budgetPool.setMaxPerChild(equalShare);
5071
+ const dynamicPool = createDynamicBudgetPool(poolStats.tokensRemaining, 0.1);
5072
+ dynamicPool.setExpectedChildren(tasks.length);
5073
+ // Temporarily replace the budget pool so spawnAgent's reserve() uses the dynamic one
5074
+ this.budgetPool = dynamicPool;
4324
5075
  try {
4325
- const promises = tasks.map(({ agent, task }) => this.spawnAgent(agent, task));
5076
+ const promises = tasks.map(({ agent, task }) => {
5077
+ const spawnPromise = this.spawnAgent(agent, task);
5078
+ // Register with supervisor for monitoring
5079
+ if (supervisor) {
5080
+ const handle = createSubagentHandle(`parallel-${agent}-${Date.now()}`, agent, task, spawnPromise, {});
5081
+ supervisor.add(handle);
5082
+ }
5083
+ return spawnPromise;
5084
+ });
4326
5085
  settled = await Promise.allSettled(promises);
4327
5086
  }
4328
5087
  finally {
4329
- this.budgetPool.resetMaxPerChild();
5088
+ this.budgetPool = originalPool;
5089
+ supervisor?.stop();
4330
5090
  }
4331
5091
  }
4332
5092
  else {
@@ -5041,8 +5801,19 @@ If the task is a simple question or doesn't need specialized handling, set bestA
5041
5801
  this.unsubscribers = [];
5042
5802
  // Flush trace collector before cleanup
5043
5803
  await this.traceCollector?.flush();
5044
- // Clear blackboard (releases file claim locks)
5045
- this.blackboard?.clear();
5804
+ // Per-agent blackboard cleanup: release only this agent's claims and subscriptions
5805
+ // so parallel siblings don't lose their data. Only root agent clears everything.
5806
+ if (this.blackboard) {
5807
+ if (this.parentIterations > 0 && this.agentId) {
5808
+ // Subagent: release only our claims and subscriptions
5809
+ this.blackboard.releaseAll(this.agentId);
5810
+ this.blackboard.unsubscribeAgent(this.agentId);
5811
+ }
5812
+ else {
5813
+ // Root agent: full clear
5814
+ this.blackboard.clear();
5815
+ }
5816
+ }
5046
5817
  // Wait for any pending init before cleanup
5047
5818
  if (this.initPromises.length > 0) {
5048
5819
  try {