attocode 0.2.2 → 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 (152) hide show
  1. package/CHANGELOG.md +103 -3
  2. package/dist/src/agent.d.ts +6 -0
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +504 -49
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/cli.d.ts.map +1 -1
  7. package/dist/src/cli.js +23 -2
  8. package/dist/src/cli.js.map +1 -1
  9. package/dist/src/core/protocol/types.d.ts +8 -8
  10. package/dist/src/defaults.d.ts +6 -1
  11. package/dist/src/defaults.d.ts.map +1 -1
  12. package/dist/src/defaults.js +36 -2
  13. package/dist/src/defaults.js.map +1 -1
  14. package/dist/src/integrations/agent-registry.d.ts +11 -0
  15. package/dist/src/integrations/agent-registry.d.ts.map +1 -1
  16. package/dist/src/integrations/agent-registry.js.map +1 -1
  17. package/dist/src/integrations/auto-compaction.d.ts.map +1 -1
  18. package/dist/src/integrations/auto-compaction.js +5 -1
  19. package/dist/src/integrations/auto-compaction.js.map +1 -1
  20. package/dist/src/integrations/bash-policy.d.ts +33 -0
  21. package/dist/src/integrations/bash-policy.d.ts.map +1 -0
  22. package/dist/src/integrations/bash-policy.js +142 -0
  23. package/dist/src/integrations/bash-policy.js.map +1 -0
  24. package/dist/src/integrations/codebase-context.d.ts +5 -0
  25. package/dist/src/integrations/codebase-context.d.ts.map +1 -1
  26. package/dist/src/integrations/codebase-context.js +33 -0
  27. package/dist/src/integrations/codebase-context.js.map +1 -1
  28. package/dist/src/integrations/delegation-protocol.js +2 -2
  29. package/dist/src/integrations/delegation-protocol.js.map +1 -1
  30. package/dist/src/integrations/economics.d.ts +42 -0
  31. package/dist/src/integrations/economics.d.ts.map +1 -1
  32. package/dist/src/integrations/economics.js +130 -14
  33. package/dist/src/integrations/economics.js.map +1 -1
  34. package/dist/src/integrations/hierarchical-config.d.ts.map +1 -1
  35. package/dist/src/integrations/hierarchical-config.js +17 -0
  36. package/dist/src/integrations/hierarchical-config.js.map +1 -1
  37. package/dist/src/integrations/index.d.ts +3 -1
  38. package/dist/src/integrations/index.d.ts.map +1 -1
  39. package/dist/src/integrations/index.js +3 -1
  40. package/dist/src/integrations/index.js.map +1 -1
  41. package/dist/src/integrations/policy-engine.d.ts +55 -0
  42. package/dist/src/integrations/policy-engine.d.ts.map +1 -0
  43. package/dist/src/integrations/policy-engine.js +247 -0
  44. package/dist/src/integrations/policy-engine.js.map +1 -0
  45. package/dist/src/integrations/safety.d.ts +5 -4
  46. package/dist/src/integrations/safety.d.ts.map +1 -1
  47. package/dist/src/integrations/safety.js +32 -7
  48. package/dist/src/integrations/safety.js.map +1 -1
  49. package/dist/src/integrations/sandbox/basic.d.ts +7 -0
  50. package/dist/src/integrations/sandbox/basic.d.ts.map +1 -1
  51. package/dist/src/integrations/sandbox/basic.js +27 -2
  52. package/dist/src/integrations/sandbox/basic.js.map +1 -1
  53. package/dist/src/integrations/sandbox/index.d.ts +6 -0
  54. package/dist/src/integrations/sandbox/index.d.ts.map +1 -1
  55. package/dist/src/integrations/sandbox/index.js +3 -0
  56. package/dist/src/integrations/sandbox/index.js.map +1 -1
  57. package/dist/src/integrations/sandbox/landlock.d.ts.map +1 -1
  58. package/dist/src/integrations/sandbox/landlock.js +3 -0
  59. package/dist/src/integrations/sandbox/landlock.js.map +1 -1
  60. package/dist/src/integrations/self-improvement.d.ts.map +1 -1
  61. package/dist/src/integrations/self-improvement.js +12 -0
  62. package/dist/src/integrations/self-improvement.js.map +1 -1
  63. package/dist/src/integrations/smart-decomposer.d.ts +18 -1
  64. package/dist/src/integrations/smart-decomposer.d.ts.map +1 -1
  65. package/dist/src/integrations/smart-decomposer.js +72 -0
  66. package/dist/src/integrations/smart-decomposer.js.map +1 -1
  67. package/dist/src/integrations/swarm/index.d.ts +1 -1
  68. package/dist/src/integrations/swarm/index.d.ts.map +1 -1
  69. package/dist/src/integrations/swarm/index.js.map +1 -1
  70. package/dist/src/integrations/swarm/model-selector.d.ts +15 -0
  71. package/dist/src/integrations/swarm/model-selector.d.ts.map +1 -1
  72. package/dist/src/integrations/swarm/model-selector.js +99 -20
  73. package/dist/src/integrations/swarm/model-selector.js.map +1 -1
  74. package/dist/src/integrations/swarm/swarm-budget.d.ts +4 -0
  75. package/dist/src/integrations/swarm/swarm-budget.d.ts.map +1 -1
  76. package/dist/src/integrations/swarm/swarm-budget.js +6 -0
  77. package/dist/src/integrations/swarm/swarm-budget.js.map +1 -1
  78. package/dist/src/integrations/swarm/swarm-config-loader.d.ts.map +1 -1
  79. package/dist/src/integrations/swarm/swarm-config-loader.js +154 -7
  80. package/dist/src/integrations/swarm/swarm-config-loader.js.map +1 -1
  81. package/dist/src/integrations/swarm/swarm-event-bridge.d.ts +12 -1
  82. package/dist/src/integrations/swarm/swarm-event-bridge.d.ts.map +1 -1
  83. package/dist/src/integrations/swarm/swarm-event-bridge.js +170 -23
  84. package/dist/src/integrations/swarm/swarm-event-bridge.js.map +1 -1
  85. package/dist/src/integrations/swarm/swarm-events.d.ts +55 -1
  86. package/dist/src/integrations/swarm/swarm-events.d.ts.map +1 -1
  87. package/dist/src/integrations/swarm/swarm-events.js +22 -5
  88. package/dist/src/integrations/swarm/swarm-events.js.map +1 -1
  89. package/dist/src/integrations/swarm/swarm-orchestrator.d.ts +124 -8
  90. package/dist/src/integrations/swarm/swarm-orchestrator.d.ts.map +1 -1
  91. package/dist/src/integrations/swarm/swarm-orchestrator.js +1668 -96
  92. package/dist/src/integrations/swarm/swarm-orchestrator.js.map +1 -1
  93. package/dist/src/integrations/swarm/swarm-quality-gate.d.ts +83 -2
  94. package/dist/src/integrations/swarm/swarm-quality-gate.d.ts.map +1 -1
  95. package/dist/src/integrations/swarm/swarm-quality-gate.js +278 -19
  96. package/dist/src/integrations/swarm/swarm-quality-gate.js.map +1 -1
  97. package/dist/src/integrations/swarm/task-queue.d.ts +44 -0
  98. package/dist/src/integrations/swarm/task-queue.d.ts.map +1 -1
  99. package/dist/src/integrations/swarm/task-queue.js +274 -11
  100. package/dist/src/integrations/swarm/task-queue.js.map +1 -1
  101. package/dist/src/integrations/swarm/types.d.ts +210 -13
  102. package/dist/src/integrations/swarm/types.d.ts.map +1 -1
  103. package/dist/src/integrations/swarm/types.js +61 -8
  104. package/dist/src/integrations/swarm/types.js.map +1 -1
  105. package/dist/src/integrations/swarm/worker-pool.d.ts +11 -4
  106. package/dist/src/integrations/swarm/worker-pool.d.ts.map +1 -1
  107. package/dist/src/integrations/swarm/worker-pool.js +173 -43
  108. package/dist/src/integrations/swarm/worker-pool.js.map +1 -1
  109. package/dist/src/integrations/tool-recommendation.d.ts +7 -4
  110. package/dist/src/integrations/tool-recommendation.d.ts.map +1 -1
  111. package/dist/src/integrations/tool-recommendation.js +58 -5
  112. package/dist/src/integrations/tool-recommendation.js.map +1 -1
  113. package/dist/src/integrations/work-log.js +4 -4
  114. package/dist/src/integrations/work-log.js.map +1 -1
  115. package/dist/src/main.js +26 -1
  116. package/dist/src/main.js.map +1 -1
  117. package/dist/src/modes/repl.d.ts.map +1 -1
  118. package/dist/src/modes/repl.js +10 -4
  119. package/dist/src/modes/repl.js.map +1 -1
  120. package/dist/src/modes/tui.d.ts.map +1 -1
  121. package/dist/src/modes/tui.js +5 -0
  122. package/dist/src/modes/tui.js.map +1 -1
  123. package/dist/src/modes.d.ts.map +1 -1
  124. package/dist/src/modes.js +4 -27
  125. package/dist/src/modes.js.map +1 -1
  126. package/dist/src/tools/agent.d.ts.map +1 -1
  127. package/dist/src/tools/agent.js +11 -2
  128. package/dist/src/tools/agent.js.map +1 -1
  129. package/dist/src/tools/bash.d.ts +3 -3
  130. package/dist/src/tools/coercion.d.ts +6 -0
  131. package/dist/src/tools/coercion.d.ts.map +1 -1
  132. package/dist/src/tools/coercion.js +13 -0
  133. package/dist/src/tools/coercion.js.map +1 -1
  134. package/dist/src/tools/file.d.ts +2 -2
  135. package/dist/src/tools/file.js +2 -2
  136. package/dist/src/tools/file.js.map +1 -1
  137. package/dist/src/tools/permission.d.ts.map +1 -1
  138. package/dist/src/tools/permission.js +4 -111
  139. package/dist/src/tools/permission.js.map +1 -1
  140. package/dist/src/tracing/trace-collector.d.ts +167 -0
  141. package/dist/src/tracing/trace-collector.d.ts.map +1 -1
  142. package/dist/src/tracing/trace-collector.js +137 -0
  143. package/dist/src/tracing/trace-collector.js.map +1 -1
  144. package/dist/src/tracing/types.d.ts +105 -1
  145. package/dist/src/tracing/types.d.ts.map +1 -1
  146. package/dist/src/tracing/types.js.map +1 -1
  147. package/dist/src/tui/app.d.ts.map +1 -1
  148. package/dist/src/tui/app.js +34 -5
  149. package/dist/src/tui/app.js.map +1 -1
  150. package/dist/src/types.d.ts +71 -0
  151. package/dist/src/types.d.ts.map +1 -1
  152. package/package.json +1 -1
package/dist/src/agent.js CHANGED
@@ -21,11 +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, createDynamicBudgetPool, createPendingPlanManager, createInteractivePlanner, createRecursiveContext, createLearningStore, createCompactor, createAutoCompactionManager, createFileChangeTracker, createCapabilitiesRegistry, createSharedBlackboard, createTaskManager, createSwarmOrchestrator, createThrottledProvider, FREE_TIER_THROTTLE, PAID_TIER_THROTTLE, createWorkLog, createVerificationGate,
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
25
  // Phase 2: Orchestration
26
26
  classifyComplexity, getScalingGuidance, buildDelegationPrompt, createMinimalDelegationSpec, getSubagentQualityPrompt, ToolRecommendationEngine, createToolRecommendationEngine, createInjectionBudgetManager,
27
27
  // Phase 3: Advanced
28
28
  getThinkingSystemPrompt, createSelfImprovementProtocol, createSubagentOutputStore, createSerperSearchTool, getEnvironmentFacts, formatFactsBlock, createAutoCheckpointManager, createSubagentSupervisor, createSubagentHandle, } from './integrations/index.js';
29
+ import { mergeApprovalScopeWithProfile, resolvePolicyProfile, } from './integrations/policy-engine.js';
29
30
  // Lesson 26: Tracing & Evaluation integration
30
31
  import { createTraceCollector } from './tracing/trace-collector.js';
31
32
  // Model registry for context window limits
@@ -179,6 +180,7 @@ export class ProductionAgent {
179
180
  skillManager = null;
180
181
  contextEngineering = null;
181
182
  codebaseContext = null;
183
+ codebaseAnalysisTriggered = false;
182
184
  traceCollector = null;
183
185
  modeManager;
184
186
  pendingPlanManager;
@@ -221,6 +223,9 @@ export class ProductionAgent {
221
223
  // Cacheable system prompt blocks for prompt caching (Improvement P1)
222
224
  // When set, callLLM() will inject these as structured content with cache_control markers
223
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;
224
229
  // Initialization tracking
225
230
  initPromises = [];
226
231
  initComplete = false;
@@ -339,7 +344,29 @@ export class ProductionAgent {
339
344
  }
340
345
  // Safety (Sandbox + Human-in-Loop)
341
346
  if (isFeatureEnabled(this.config.sandbox) || isFeatureEnabled(this.config.humanInLoop)) {
342
- 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
+ }
343
370
  }
344
371
  // Routing
345
372
  if (isFeatureEnabled(this.config.routing)) {
@@ -943,7 +970,11 @@ export class ProductionAgent {
943
970
  else {
944
971
  // Single-task mode (backward compatibility) - start session with task
945
972
  const traceSessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
946
- 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);
947
978
  }
948
979
  try {
949
980
  // Check for cancellation before starting
@@ -1102,6 +1133,161 @@ export class ProductionAgent {
1102
1133
  const { SwarmEventBridge } = await import('./integrations/swarm/swarm-event-bridge.js');
1103
1134
  const bridge = new SwarmEventBridge({ outputDir: '.agent/swarm-live' });
1104
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
+ }
1105
1291
  try {
1106
1292
  const result = await this.swarmOrchestrator.execute(task);
1107
1293
  // Populate task DAG for dashboard after execution
@@ -1116,6 +1302,7 @@ export class ProductionAgent {
1116
1302
  return result;
1117
1303
  }
1118
1304
  finally {
1305
+ unsubTrace?.();
1119
1306
  unsubBridge();
1120
1307
  bridge.close();
1121
1308
  unsubSwarm();
@@ -1843,19 +2030,99 @@ export class ProductionAgent {
1843
2030
  });
1844
2031
  // Handle compaction result
1845
2032
  if (compactionResult.status === 'compacted' && compactionResult.compactedMessages) {
1846
- // Replace messages with compacted version
1847
- messages.length = 0;
1848
- messages.push(...compactionResult.compactedMessages);
1849
- this.state.messages.length = 0;
1850
- this.state.messages.push(...compactionResult.compactedMessages);
1851
- // Inject work log after compaction to prevent amnesia
1852
- if (this.workLog?.hasContent()) {
1853
- const workLogMessage = {
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 = {
1854
2041
  role: 'user',
1855
- content: this.workLog.toCompactString(),
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,
1856
2113
  };
1857
- messages.push(workLogMessage);
1858
- this.state.messages.push(workLogMessage);
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
+ }
1859
2126
  }
1860
2127
  }
1861
2128
  else if (compactionResult.status === 'hard_limit') {
@@ -1879,6 +2146,13 @@ export class ProductionAgent {
1879
2146
  currentTokens: currentUsage.tokens,
1880
2147
  maxTokens: budget.maxTokens,
1881
2148
  });
2149
+ // Also checkpoint before fallback compaction
2150
+ try {
2151
+ this.autoCheckpoint(true);
2152
+ }
2153
+ catch {
2154
+ // Non-critical
2155
+ }
1882
2156
  this.compactToolOutputs();
1883
2157
  }
1884
2158
  }
@@ -1888,8 +2162,10 @@ export class ProductionAgent {
1888
2162
  const sourceToolName = toolCallNameById.get(result.callId);
1889
2163
  const isExpensiveResult = sourceToolName === 'spawn_agent' || sourceToolName === 'spawn_agents_parallel';
1890
2164
  // Truncate long outputs to save context
1891
- if (content.length > MAX_TOOL_OUTPUT_CHARS) {
1892
- 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]`;
1893
2169
  }
1894
2170
  // =======================================================================
1895
2171
  // ESTIMATE if adding this result would exceed budget
@@ -2013,10 +2289,14 @@ export class ProductionAgent {
2013
2289
  const reservedTokens = 10500;
2014
2290
  const maxContextTokens = (this.config.maxContextTokens ?? 80000) - reservedTokens;
2015
2291
  const codebaseBudget = Math.min(maxContextTokens * 0.3, 15000); // Up to 30% or 15K tokens
2016
- try {
2017
- // Use synchronous cache if available, otherwise skip
2018
- const repoMap = this.codebaseContext.getRepoMap();
2019
- 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 {
2020
2300
  const selection = this.selectRelevantCodeSync(task, codebaseBudget);
2021
2301
  if (selection.chunks.length > 0) {
2022
2302
  codebaseContextStr = buildContextFromChunks(selection.chunks, {
@@ -2025,10 +2305,14 @@ export class ProductionAgent {
2025
2305
  maxTotalTokens: codebaseBudget,
2026
2306
  });
2027
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
2028
2315
  }
2029
- }
2030
- catch {
2031
- // Codebase analysis not ready yet - skip for this call
2032
2316
  }
2033
2317
  }
2034
2318
  // Build tool descriptions
@@ -2506,6 +2790,12 @@ export class ProductionAgent {
2506
2790
  });
2507
2791
  // Handle forbidden policy - always block
2508
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
+ });
2509
2799
  throw new Error(`Forbidden by policy: ${evaluation.reason}`);
2510
2800
  }
2511
2801
  // Handle prompt policy - requires approval
@@ -2548,6 +2838,21 @@ export class ProductionAgent {
2548
2838
  const safety = this.safety;
2549
2839
  const validation = await this.withPausedDuration(() => safety.validateAndApprove(toolCall, `Executing tool: ${toolCall.name}`, { skipHumanApproval: policyApprovedByUser }));
2550
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
+ }
2551
2856
  throw new Error(`Tool call blocked: ${validation.reason}`);
2552
2857
  }
2553
2858
  }
@@ -3608,6 +3913,12 @@ export class ProductionAgent {
3608
3913
  return null;
3609
3914
  return this.economics.getProgress();
3610
3915
  }
3916
+ /**
3917
+ * Get actual file paths modified during this agent's session.
3918
+ */
3919
+ getModifiedFilePaths() {
3920
+ return this.economics?.getModifiedFilePaths() ?? [];
3921
+ }
3611
3922
  /**
3612
3923
  * Extend the budget limits.
3613
3924
  */
@@ -3916,6 +4227,43 @@ export class ProductionAgent {
3916
4227
  try {
3917
4228
  // Filter tools for this agent
3918
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
+ }
3919
4267
  // Apply tool recommendations to improve subagent focus (only for large tool sets)
3920
4268
  if (this.toolRecommendation && agentTools.length > 15) {
3921
4269
  const taskType = ToolRecommendationEngine.inferTaskType(agentName);
@@ -3924,9 +4272,29 @@ export class ProductionAgent {
3924
4272
  const recommendedNames = new Set(recommendations.map(r => r.toolName));
3925
4273
  // Always keep spawn tools even if not recommended
3926
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
+ }
3927
4282
  agentTools = agentTools.filter(t => recommendedNames.has(t.name) || alwaysKeep.has(t.name));
3928
4283
  }
3929
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
+ }
3930
4298
  // Resolve model - abstract tiers (fast/balanced/quality) should use parent's model
3931
4299
  // Only use agentDef.model if it's an actual model ID (contains '/')
3932
4300
  const resolvedModel = (agentDef.model && agentDef.model.includes('/'))
@@ -3949,7 +4317,8 @@ export class ProductionAgent {
3949
4317
  // Precedence: per-type config > per-type default > global config > hardcoded fallback
3950
4318
  const subagentConfig = this.config.subagent;
3951
4319
  const hasSubagentConfig = subagentConfig !== false && subagentConfig !== undefined;
3952
- // 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
3953
4322
  const agentTypeTimeout = getSubagentTimeout(agentName);
3954
4323
  const rawPerTypeTimeout = hasSubagentConfig
3955
4324
  ? subagentConfig.timeouts?.[agentName]
@@ -3959,9 +4328,10 @@ export class ProductionAgent {
3959
4328
  : undefined;
3960
4329
  // Validate: reject negative, NaN, or non-finite timeout values
3961
4330
  const isValidTimeout = (v) => v !== undefined && Number.isFinite(v) && v > 0;
4331
+ const agentDefTimeout = isValidTimeout(agentDef.timeout) ? agentDef.timeout : undefined;
3962
4332
  const perTypeConfigTimeout = isValidTimeout(rawPerTypeTimeout) ? rawPerTypeTimeout : undefined;
3963
4333
  const globalConfigTimeout = isValidTimeout(rawGlobalTimeout) ? rawGlobalTimeout : undefined;
3964
- const subagentTimeout = perTypeConfigTimeout ?? agentTypeTimeout ?? globalConfigTimeout ?? 300000;
4334
+ const subagentTimeout = agentDefTimeout ?? perTypeConfigTimeout ?? agentTypeTimeout ?? globalConfigTimeout ?? 300000;
3965
4335
  // Iteration precedence: per-type config override > agent-type default > global config default
3966
4336
  const agentTypeMaxIter = getSubagentMaxIterations(agentName);
3967
4337
  const rawPerTypeMaxIter = hasSubagentConfig
@@ -4019,17 +4389,16 @@ export class ProductionAgent {
4019
4389
  const subagentBudgetTokens = constraints?.maxTokens ?? SUBAGENT_BUDGET.maxTokens ?? 100000;
4020
4390
  const subagentBudgetMinutes = Math.round((SUBAGENT_BUDGET.maxDuration ?? 240000) / 60000);
4021
4391
  if (isSwarmWorker) {
4022
- // V6: Calmer resource awareness for swarm workers — prevents weaker models
4023
- // from confabulating budget warnings and wrapping up without doing work
4024
- constraintParts.push(`**Resource Info:**\n` +
4025
- `- Token budget: ~${(subagentBudgetTokens / 1000).toFixed(0)}k tokens (you have plenty)\n` +
4026
- `- Time limit: ~${subagentBudgetMinutes} minutes\n` +
4027
- `- Focus on completing your task. Do NOT wrap up prematurely.\n` +
4028
- `- You will receive a system warning IF you approach budget limits. Until then, work normally.\n` +
4029
- `- **IMPORTANT:** Budget warnings come from the SYSTEM, not from your own assessment. ` +
4030
- `Do not preemptively claim budget issues.\n` +
4031
- `- **STRUCTURED WRAPUP:** When told to wrap up, respond with ONLY this JSON (no tool calls):\n` +
4032
- ` {"findings":[...], "actionsTaken":[...], "failures":[...], "remainingWork":[...], "suggestedNextSteps":[...]}`);
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.`);
4033
4402
  }
4034
4403
  else {
4035
4404
  // Original RESOURCE AWARENESS text for regular subagents
@@ -4073,6 +4442,20 @@ export class ProductionAgent {
4073
4442
  // Allocate budget from pool (or use default) — track allocation ID for release later
4074
4443
  const pooledBudget = this.getSubagentBudget(agentName, constraints);
4075
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
+ }
4076
4459
  // Create a sub-agent with the agent's config
4077
4460
  // Use SUBAGENT_BUDGET to constrain resource usage (prevents runaway token consumption)
4078
4461
  const subAgent = new ProductionAgent({
@@ -4104,14 +4487,56 @@ export class ProductionAgent {
4104
4487
  // Lower context window for subagents so percentage-based compaction triggers earlier
4105
4488
  maxContextTokens: 80000,
4106
4489
  observability: this.config.observability,
4107
- 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
+ })(),
4108
4511
  humanInLoop: this.config.humanInLoop,
4109
4512
  // Subagents get 'allow' as default policy since they're already
4110
4513
  // constrained to their registered tool set. The parent's 'prompt'
4111
4514
  // policy can't work without humanInLoop.
4112
- executionPolicy: this.config.executionPolicy
4113
- ? { ...this.config.executionPolicy, defaultPolicy: 'allow' }
4114
- : 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,
4115
4540
  threads: false,
4116
4541
  // Disable hooks console output in subagents - parent handles event display
4117
4542
  hooks: this.config.hooks === false ? false : {
@@ -4127,7 +4552,10 @@ export class ProductionAgent {
4127
4552
  fileCache: this.fileCache || undefined,
4128
4553
  // CONSTRAINED BUDGET: Use pooled budget when available, falling back to SUBAGENT_BUDGET
4129
4554
  // Pooled budget ensures total tree cost stays bounded by parent's budget
4130
- 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,
4131
4559
  });
4132
4560
  // CRITICAL: Subagent inherits parent's mode
4133
4561
  // This ensures that if parent is in plan mode:
@@ -4140,14 +4568,35 @@ export class ProductionAgent {
4140
4568
  // APPROVAL BATCHING (Improvement P6): Set approval scope for subagents
4141
4569
  // Read-only tools are auto-approved; write tools get scoped approval
4142
4570
  // This reduces interruptions from ~8 per session to ~1-2
4143
- subAgent.setApprovalScope({
4144
- autoApprove: ['read_file', 'list_files', 'glob', 'grep', 'show_file_history', 'show_session_changes'],
4145
- 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
+ ? {
4146
4577
  write_file: { paths: ['src/', 'tests/', 'tools/'] },
4147
4578
  edit_file: { paths: ['src/', 'tests/', 'tools/'] },
4148
- },
4149
- requireApproval: ['bash', 'delete_file'],
4150
- });
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);
4151
4600
  // Pass parent's iteration count to subagent for accurate budget tracking
4152
4601
  // This prevents subagents from consuming excessive iterations when parent already used many
4153
4602
  subAgent.setParentIterations(this.getTotalIterations());
@@ -4168,7 +4617,7 @@ export class ProductionAgent {
4168
4617
  // 1. Normal operation: progress extends idle timer
4169
4618
  // 2. Wrapup phase: 30s before hard kill, wrapup callback fires → forceTextOnly
4170
4619
  // 3. Hard kill: race() throws CancellationError after wrapup window
4171
- const IDLE_TIMEOUT = 120000; // 2 minutes without progress = timeout
4620
+ const IDLE_TIMEOUT = agentDef.idleTimeout ?? 120000; // Configurable idle timeout (default: 2 min)
4172
4621
  let WRAPUP_WINDOW = 30000;
4173
4622
  let IDLE_CHECK_INTERVAL = 5000;
4174
4623
  if (this.config.subagent) {
@@ -4271,6 +4720,8 @@ export class ProductionAgent {
4271
4720
  : (result.response || result.error || '');
4272
4721
  // Parse structured closure report from agent's response (if it produced one)
4273
4722
  const structured = parseStructuredClosureReport(result.response || '', 'completed');
4723
+ // Extract real file paths from subagent's economics tracker (before cleanup)
4724
+ const subagentFilePaths = subAgent.getModifiedFilePaths();
4274
4725
  const spawnResultFinal = {
4275
4726
  success: result.success,
4276
4727
  output: finalOutput,
@@ -4280,6 +4731,7 @@ export class ProductionAgent {
4280
4731
  toolCalls: result.metrics.toolCalls,
4281
4732
  },
4282
4733
  structured,
4734
+ filesModified: subagentFilePaths,
4283
4735
  };
4284
4736
  // Save full output to subagent output store (avoids telephone problem)
4285
4737
  if (this.subagentOutputStore) {
@@ -4290,7 +4742,7 @@ export class ProductionAgent {
4290
4742
  task,
4291
4743
  fullOutput: finalOutput,
4292
4744
  structured,
4293
- filesModified: [],
4745
+ filesModified: subagentFilePaths,
4294
4746
  filesCreated: [],
4295
4747
  timestamp: new Date(),
4296
4748
  tokensUsed: result.metrics.totalTokens,
@@ -4440,6 +4892,8 @@ export class ProductionAgent {
4440
4892
  this.pendingPlanManager.appendExplorationFinding(`[${agentName}] ${subPlan.explorationSummary}`);
4441
4893
  }
4442
4894
  }
4895
+ // Extract real file paths from subagent's economics tracker (before cleanup)
4896
+ const subagentFilePaths = subAgent.getModifiedFilePaths();
4443
4897
  // Unsubscribe from subagent events and cleanup gracefully
4444
4898
  unsubSubAgent();
4445
4899
  try {
@@ -4507,6 +4961,7 @@ export class ProductionAgent {
4507
4961
  toolCalls: subagentMetrics.toolCalls,
4508
4962
  },
4509
4963
  structured,
4964
+ filesModified: subagentFilePaths,
4510
4965
  };
4511
4966
  }
4512
4967
  throw err; // Re-throw non-cancellation errors