@metabob/minibob 0.1.2

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 (174) hide show
  1. package/ARCHITECTURE.md +255 -0
  2. package/CHANGELOG.md +112 -0
  3. package/README.md +380 -0
  4. package/bin/minibob.js +36 -0
  5. package/dist/acp-gossip.d.ts +72 -0
  6. package/dist/acp-gossip.d.ts.map +1 -0
  7. package/dist/acp-gossip.js +156 -0
  8. package/dist/acp-gossip.js.map +1 -0
  9. package/dist/acp.d.ts +62 -0
  10. package/dist/acp.d.ts.map +1 -0
  11. package/dist/acp.js +292 -0
  12. package/dist/acp.js.map +1 -0
  13. package/dist/activity.d.ts +157 -0
  14. package/dist/activity.d.ts.map +1 -0
  15. package/dist/activity.js +518 -0
  16. package/dist/activity.js.map +1 -0
  17. package/dist/agent-runtime.d.ts +104 -0
  18. package/dist/agent-runtime.d.ts.map +1 -0
  19. package/dist/boredom.d.ts +125 -0
  20. package/dist/boredom.d.ts.map +1 -0
  21. package/dist/boredom.js +244 -0
  22. package/dist/boredom.js.map +1 -0
  23. package/dist/cli/acp-server.d.ts +23 -0
  24. package/dist/cli/acp-server.d.ts.map +1 -0
  25. package/dist/cli/burrow.d.ts +26 -0
  26. package/dist/cli/burrow.d.ts.map +1 -0
  27. package/dist/cli/doctor.d.ts +22 -0
  28. package/dist/cli/doctor.d.ts.map +1 -0
  29. package/dist/cli/goal.d.ts +22 -0
  30. package/dist/cli/goal.d.ts.map +1 -0
  31. package/dist/cli/index.d.ts +47 -0
  32. package/dist/cli/index.d.ts.map +1 -0
  33. package/dist/cli/instance-registry.d.ts +78 -0
  34. package/dist/cli/instance-registry.d.ts.map +1 -0
  35. package/dist/cli/observe.d.ts +35 -0
  36. package/dist/cli/observe.d.ts.map +1 -0
  37. package/dist/cli/vessel.d.ts +14 -0
  38. package/dist/cli/vessel.d.ts.map +1 -0
  39. package/dist/composition-observer.d.ts +96 -0
  40. package/dist/composition-observer.d.ts.map +1 -0
  41. package/dist/config.d.ts +36 -0
  42. package/dist/config.d.ts.map +1 -0
  43. package/dist/config.js +128 -0
  44. package/dist/config.js.map +1 -0
  45. package/dist/docker/Dockerfile +35 -0
  46. package/dist/environment.d.ts +72 -0
  47. package/dist/environment.d.ts.map +1 -0
  48. package/dist/environment.js +142 -0
  49. package/dist/environment.js.map +1 -0
  50. package/dist/goal-processor.d.ts +165 -0
  51. package/dist/goal-processor.d.ts.map +1 -0
  52. package/dist/helm/minibob-cluster/Chart.yaml +13 -0
  53. package/dist/helm/minibob-cluster/templates/_helpers.tpl +60 -0
  54. package/dist/helm/minibob-cluster/templates/configmap.yaml +11 -0
  55. package/dist/helm/minibob-cluster/templates/deployment.yaml +108 -0
  56. package/dist/helm/minibob-cluster/templates/secret.yaml +10 -0
  57. package/dist/helm/minibob-cluster/templates/service.yaml +37 -0
  58. package/dist/helm/minibob-cluster/values-local.yaml +41 -0
  59. package/dist/helm/minibob-cluster/values-production.yaml +57 -0
  60. package/dist/helm/minibob-cluster/values-testing-cluster.yaml +43 -0
  61. package/dist/helm/minibob-cluster/values.yaml +127 -0
  62. package/dist/improviser.d.ts +74 -0
  63. package/dist/improviser.d.ts.map +1 -0
  64. package/dist/impulse-filter.d.ts +74 -0
  65. package/dist/impulse-filter.d.ts.map +1 -0
  66. package/dist/impulse.d.ts +92 -0
  67. package/dist/impulse.d.ts.map +1 -0
  68. package/dist/impulse.js +234 -0
  69. package/dist/impulse.js.map +1 -0
  70. package/dist/lib.d.ts +29 -0
  71. package/dist/lib.d.ts.map +1 -0
  72. package/dist/lib.js +18561 -0
  73. package/dist/lib.js.map +98 -0
  74. package/dist/lifecycle-hooks.d.ts +99 -0
  75. package/dist/lifecycle-hooks.d.ts.map +1 -0
  76. package/dist/lifecycle-hooks.js +135 -0
  77. package/dist/lifecycle-hooks.js.map +1 -0
  78. package/dist/llm.d.ts +31 -0
  79. package/dist/llm.d.ts.map +1 -0
  80. package/dist/llm.js +349 -0
  81. package/dist/llm.js.map +1 -0
  82. package/dist/mcp-activity-bridge.d.ts +66 -0
  83. package/dist/mcp-activity-bridge.d.ts.map +1 -0
  84. package/dist/mcp-activity-bridge.js +126 -0
  85. package/dist/mcp-activity-bridge.js.map +1 -0
  86. package/dist/mcp.d.ts +216 -0
  87. package/dist/mcp.d.ts.map +1 -0
  88. package/dist/mcp.js +292 -0
  89. package/dist/mcp.js.map +1 -0
  90. package/dist/memory-agent.d.ts +92 -0
  91. package/dist/memory-agent.d.ts.map +1 -0
  92. package/dist/memory-agent.js +277 -0
  93. package/dist/memory-agent.js.map +1 -0
  94. package/dist/runtime-mapping.d.ts +97 -0
  95. package/dist/runtime-mapping.d.ts.map +1 -0
  96. package/dist/search-first-executor.d.ts +113 -0
  97. package/dist/search-first-executor.d.ts.map +1 -0
  98. package/dist/session.d.ts +48 -0
  99. package/dist/session.d.ts.map +1 -0
  100. package/dist/template-extractor.d.ts +9 -0
  101. package/dist/template-extractor.d.ts.map +1 -0
  102. package/dist/template-generator.d.ts +12 -0
  103. package/dist/template-generator.d.ts.map +1 -0
  104. package/dist/tools.d.ts +58 -0
  105. package/dist/tools.d.ts.map +1 -0
  106. package/dist/tools.js +771 -0
  107. package/dist/tools.js.map +1 -0
  108. package/dist/types.d.ts +503 -0
  109. package/dist/types.d.ts.map +1 -0
  110. package/dist/types.js +8 -0
  111. package/dist/types.js.map +1 -0
  112. package/dist/understanding/analyzer.d.ts +55 -0
  113. package/dist/understanding/analyzer.d.ts.map +1 -0
  114. package/dist/understanding/explorer.d.ts +73 -0
  115. package/dist/understanding/explorer.d.ts.map +1 -0
  116. package/dist/understanding/index.d.ts +7 -0
  117. package/dist/understanding/index.d.ts.map +1 -0
  118. package/dist/understanding/types.d.ts +136 -0
  119. package/dist/understanding/types.d.ts.map +1 -0
  120. package/dist/validation.d.ts +29 -0
  121. package/dist/validation.d.ts.map +1 -0
  122. package/dist/validation.js +106 -0
  123. package/dist/validation.js.map +1 -0
  124. package/dist/vessel-bootstrap.d.ts +190 -0
  125. package/dist/vessel-bootstrap.d.ts.map +1 -0
  126. package/dist/vessel-registry.d.ts +229 -0
  127. package/dist/vessel-registry.d.ts.map +1 -0
  128. package/index.ts +1329 -0
  129. package/package.json +54 -0
  130. package/src/acp-gossip.ts +193 -0
  131. package/src/acp.ts +362 -0
  132. package/src/activity.ts +1464 -0
  133. package/src/agent-runtime.ts +365 -0
  134. package/src/boredom.ts +423 -0
  135. package/src/cli/acp-server.ts +377 -0
  136. package/src/cli/burrow.ts +896 -0
  137. package/src/cli/doctor.ts +526 -0
  138. package/src/cli/goal.ts +224 -0
  139. package/src/cli/index.ts +147 -0
  140. package/src/cli/instance-registry.ts +271 -0
  141. package/src/cli/observe.ts +682 -0
  142. package/src/cli/vessel.ts +287 -0
  143. package/src/components/SystemOverview.tsx +331 -0
  144. package/src/composition-observer.ts +449 -0
  145. package/src/config.ts +172 -0
  146. package/src/environment.ts +167 -0
  147. package/src/goal-processor.ts +654 -0
  148. package/src/improviser.ts +591 -0
  149. package/src/impulse-filter.ts +273 -0
  150. package/src/impulse.ts +311 -0
  151. package/src/lib.ts +147 -0
  152. package/src/lifecycle-hooks.ts +181 -0
  153. package/src/llm.ts +434 -0
  154. package/src/mcp-activity-bridge.ts +158 -0
  155. package/src/mcp.ts +747 -0
  156. package/src/memory-agent.ts +316 -0
  157. package/src/runtime-mapping.ts +527 -0
  158. package/src/search-first-executor.ts +666 -0
  159. package/src/session.ts +141 -0
  160. package/src/template-extractor.ts +256 -0
  161. package/src/template-generator.ts +130 -0
  162. package/src/tools.ts +924 -0
  163. package/src/types.ts +497 -0
  164. package/src/understanding/analyzer.ts +354 -0
  165. package/src/understanding/explorer.ts +488 -0
  166. package/src/understanding/index.ts +27 -0
  167. package/src/understanding/types.ts +153 -0
  168. package/src/validation.ts +125 -0
  169. package/src/vessel-bootstrap.ts +440 -0
  170. package/src/vessel-registry.ts +621 -0
  171. package/templates/core/edit-file.json +85 -0
  172. package/templates/understanding/diagnose-problem.json +32 -0
  173. package/templates/understanding/explore-codebase-v2.json +57 -0
  174. package/templates/understanding/explore-codebase.json +37 -0
@@ -0,0 +1,1464 @@
1
+ /**
2
+ * minibob Activity Executor
3
+ *
4
+ * Executes activity templates with impulse injection and tool calling.
5
+ */
6
+
7
+ import type {
8
+ ActivityTemplate,
9
+ ActivityExecution,
10
+ ActivityTask,
11
+ TaskResult,
12
+ Impulse,
13
+ Message,
14
+ ToolResult,
15
+ ToolHandler,
16
+ ExecutedTask,
17
+ } from "./types"
18
+ import { createLLMClient, type LLMClient } from "./llm"
19
+ import { createToolHandlers, getAllToolDefinitions, type ToolHandlerOptions, type ToolDefinition } from "./tools"
20
+ import {
21
+ createImpulse,
22
+ loadImpulses,
23
+ formatImpulsesForContext,
24
+ storeActivityOutput,
25
+ getImpulseStore,
26
+ } from "./impulse"
27
+ import { getMCPClient, isMCPEnabled } from "./mcp"
28
+ import {
29
+ filterImpulsesByRelevance,
30
+ calculateSavings,
31
+ estimateImpulseTokens,
32
+ generateFilteringSummary,
33
+ } from "./impulse-filter"
34
+ import { assembleTemplateFromExecution } from "./template-generator"
35
+
36
+ /**
37
+ * Safe wrapper for Bun.file() with better error handling
38
+ */
39
+ function safeReadFile(path: string, context?: string): ReturnType<typeof Bun.file> {
40
+ if (!path || typeof path !== 'string') {
41
+ const stack = new Error().stack
42
+ throw new Error(`[activity.ts] Invalid file path: expected string, got ${typeof path}${context ? ` (context: ${context})` : ''}\nStack: ${stack}`)
43
+ }
44
+ try {
45
+ return Bun.file(path)
46
+ } catch (error) {
47
+ throw new Error(`Failed to create file handle for '${path}': ${error instanceof Error ? error.message : String(error)}`)
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Substitute {{impulse:id}} placeholders with impulse content
53
+ */
54
+ async function substituteImpulses(template: string, _impulseIds: string[]): Promise<string> {
55
+ let result = template
56
+
57
+ // Find all {{impulse:id}} patterns
58
+ const impulsePattern = /{{impulse:([a-zA-Z0-9_-]+)}}/g
59
+ const matches = Array.from(template.matchAll(impulsePattern))
60
+
61
+ if (matches.length === 0) {
62
+ return result
63
+ }
64
+
65
+ // Load all referenced impulses
66
+ const referencedIds = matches.map(m => m[1]).filter((id): id is string => id !== undefined)
67
+ const impulses = await loadImpulses(referencedIds)
68
+
69
+ // Format impulses for context
70
+ const formatted = formatImpulsesForContext(impulses)
71
+
72
+ // Replace each placeholder
73
+ for (const match of matches) {
74
+ const impulseId = match[1]
75
+ const impulseContent = impulses.find(imp => imp.id === impulseId)
76
+
77
+ if (impulseContent) {
78
+ // Use formatted content (respects budget)
79
+ const content = typeof impulseContent.content === 'string'
80
+ ? impulseContent.content
81
+ : JSON.stringify(impulseContent.content, null, 2)
82
+ result = result.replace(match[0], content)
83
+ } else {
84
+ // Impulse not found, leave placeholder or use empty
85
+ console.warn(`Impulse not found: ${impulseId}`)
86
+ result = result.replace(match[0], `[impulse:${impulseId} not found]`)
87
+ }
88
+ }
89
+
90
+ return result
91
+ }
92
+
93
+ // =============================================================================
94
+ // ACTIVITY EXECUTOR
95
+ // =============================================================================
96
+
97
+ export interface ExecutorConfig {
98
+ provider: "anthropic" | "openai"
99
+ apiKey: string
100
+ model: string
101
+ workingDirectory: string
102
+ systemPrompt?: string
103
+ // Activity management callbacks (for autonomous trailblazing)
104
+ onSearchActivities?: (category?: string, verbose?: boolean) => Promise<{ count: number; activities: unknown[] }>
105
+ onCreateActivity?: (params: {
106
+ goalDescription: string
107
+ templateName: string
108
+ category: string
109
+ variables: Record<string, unknown>
110
+ }) => Promise<{ templateId: string }>
111
+ /**
112
+ * Callback when an activity is executed (for composition tracking)
113
+ */
114
+ onActivityExecute?: (templateId: string, variables: Record<string, unknown>, reason?: string) => Promise<ActivityExecution>
115
+ /**
116
+ * Custom domain-specific tools to expose to the LLM in addition to built-in tools.
117
+ * The host application (e.g. PerspectiveMinibobAdapter) injects these so that
118
+ * activity templates can call Perspective-specific database / API operations.
119
+ */
120
+ customTools?: ToolHandlerOptions["customTools"]
121
+ /**
122
+ * Maximum nesting depth for nested activity execution.
123
+ * Prevents runaway recursion when activities call other activities.
124
+ * Default: 3
125
+ */
126
+ maxNestingDepth?: number
127
+ /**
128
+ * Activity call stack for cycle detection.
129
+ * Tracks the chain of activity IDs from root to current execution.
130
+ * Used to detect and prevent circular activity calls (A → B → A).
131
+ *
132
+ * Example: ["process-goal", "explore-codebase", "read-file"]
133
+ *
134
+ * Internal use only - populated automatically during composition.
135
+ */
136
+ activityCallStack?: string[]
137
+ }
138
+
139
+ export interface ExecuteOptions {
140
+ template: ActivityTemplate
141
+ variables: Record<string, unknown>
142
+ reason?: string
143
+ impulses?: Impulse[] // Context from previous executions
144
+ onTaskStart?: (taskId: string) => void
145
+ onTaskComplete?: (taskId: string, result: TaskResult) => void
146
+ // Parent activity context for composition tracking
147
+ parentActivityId?: string
148
+ parentExecutionId?: string
149
+ goalContext?: string
150
+ }
151
+
152
+ // =============================================================================
153
+ // PHASE 1.8: State Capture Utilities for Debugging-as-Activity
154
+ // =============================================================================
155
+
156
+ /**
157
+ * Capture input state before task execution
158
+ */
159
+ async function captureInputState(
160
+ workingDirectory: string,
161
+ impulseIds: string[],
162
+ variables: Record<string, unknown>
163
+ ): Promise<{
164
+ filesAvailable: string[]
165
+ environment: Record<string, string>
166
+ impulses: string[]
167
+ variables: Record<string, unknown>
168
+ }> {
169
+ // List files in working directory (shallow, don't recurse into node_modules, etc.)
170
+ let filesAvailable: string[] = []
171
+ try {
172
+ const glob = new Bun.Glob("**/*")
173
+ const entries = await Array.fromAsync(glob.scan({ cwd: workingDirectory }))
174
+ filesAvailable = entries.filter(f => !f.includes("node_modules") && !f.startsWith(".") && !f.startsWith("dist"))
175
+ } catch (error) {
176
+ console.warn("[State Capture] Failed to list files:", error instanceof Error ? error.message : String(error))
177
+ }
178
+
179
+ // Capture relevant environment variables (filter sensitive ones)
180
+ const environment: Record<string, string> = {}
181
+ const envVarsToCapture = [
182
+ "NODE_ENV",
183
+ "PATH",
184
+ "HOME",
185
+ "USER",
186
+ "PWD",
187
+ "SHELL",
188
+ // Add more as needed
189
+ ]
190
+ for (const key of envVarsToCapture) {
191
+ const value = process.env[key]
192
+ if (value) {
193
+ environment[key] = value
194
+ }
195
+ }
196
+
197
+ return {
198
+ filesAvailable,
199
+ environment,
200
+ impulses: impulseIds,
201
+ variables: JSON.parse(JSON.stringify(variables)), // Deep clone
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Capture output state after task execution
207
+ */
208
+ async function captureOutputState(
209
+ workingDirectory: string,
210
+ beforeFiles: string[],
211
+ toolCalls: Array<{ toolName: string; params: any; result: ToolResult }>
212
+ ): Promise<{
213
+ filesModified: string[]
214
+ filesCreated: string[]
215
+ filesDeleted: string[]
216
+ exitCode?: number
217
+ stderr?: string
218
+ }> {
219
+ // List files after execution
220
+ let afterFiles: string[] = []
221
+ try {
222
+ const glob = new Bun.Glob("**/*")
223
+ const entries = await Array.fromAsync(glob.scan({ cwd: workingDirectory }))
224
+ afterFiles = entries.filter(f => !f.includes("node_modules") && !f.startsWith(".") && !f.startsWith("dist"))
225
+ } catch (error) {
226
+ console.warn("[State Capture] Failed to list files after execution:", error instanceof Error ? error.message : String(error))
227
+ }
228
+
229
+ // Detect created, modified, deleted files
230
+ const beforeSet = new Set(beforeFiles)
231
+ const afterSet = new Set(afterFiles)
232
+
233
+ const filesCreated = afterFiles.filter(f => !beforeSet.has(f))
234
+ const filesDeleted = beforeFiles.filter(f => !afterSet.has(f))
235
+
236
+ // Modified files: files that existed before and after (tool calls track actual modifications)
237
+ const filesModified: string[] = []
238
+ for (const call of toolCalls) {
239
+ if (call.toolName === "write" || call.toolName === "edit") {
240
+ const filePath = call.params.filePath as string
241
+ if (filePath && beforeSet.has(filePath)) {
242
+ filesModified.push(filePath)
243
+ }
244
+ }
245
+ }
246
+
247
+ // Extract exit code and stderr from bash tool calls
248
+ let exitCode: number | undefined
249
+ let stderr: string | undefined
250
+
251
+ for (const call of toolCalls) {
252
+ if (call.toolName === "bash" && call.result.metadata) {
253
+ if (call.result.metadata.exitCode !== undefined) {
254
+ exitCode = call.result.metadata.exitCode as number
255
+ }
256
+ if (call.result.metadata.stderr) {
257
+ stderr = call.result.metadata.stderr as string
258
+ }
259
+ }
260
+ }
261
+
262
+ return {
263
+ filesModified: Array.from(new Set(filesModified)), // Deduplicate
264
+ filesCreated,
265
+ filesDeleted,
266
+ exitCode,
267
+ stderr,
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Capture lightweight file hashes for state transition tracking
273
+ */
274
+ async function captureFileHashes(
275
+ workingDirectory: string,
276
+ files: string[]
277
+ ): Promise<Record<string, string>> {
278
+ const hashes: Record<string, string> = {}
279
+
280
+ for (const file of files.slice(0, 50)) { // Limit to 50 files to avoid overhead
281
+ try {
282
+ const fullPath = `${workingDirectory}/${file}`
283
+ const fileContent = await safeReadFile(fullPath).text()
284
+ const hash = Bun.hash(fileContent).toString(16)
285
+ hashes[file] = hash
286
+ } catch (error) {
287
+ // File might have been deleted or is not readable
288
+ hashes[file] = "ERROR"
289
+ }
290
+ }
291
+
292
+ return hashes
293
+ }
294
+
295
+ /**
296
+ * Activity executor for running activity templates
297
+ */
298
+ export class ActivityExecutor {
299
+ private llm: LLMClient
300
+ private toolHandlers: ReturnType<typeof createToolHandlers>
301
+ private config: ExecutorConfig
302
+ private customToolDefinitions: ToolDefinition[]
303
+ private currentActivityId?: string
304
+ private currentExecutionId?: string
305
+ private currentGoalContext?: string
306
+ private toolCallRecords: Array<{ toolName: string; params: any; result: ToolResult; timestamp: number }> = []
307
+
308
+ /** Public accessor for LLM client (needed by GoalProcessor for improvisation fallback) */
309
+ get llmClient(): LLMClient {
310
+ return this.llm
311
+ }
312
+
313
+ constructor(config: ExecutorConfig) {
314
+ this.config = config
315
+ this.llm = createLLMClient(config.provider, config.apiKey)
316
+ // Collect custom tool definitions for passing to getAllToolDefinitions later
317
+ this.customToolDefinitions = Object.values(config.customTools ?? {}).map(({ definition }) => definition)
318
+ this.toolHandlers = createToolHandlers({
319
+ workingDirectory: config.workingDirectory,
320
+ onActivityExecute: config.onActivityExecute ?? (async (templateId, variables, reason) => {
321
+ // Default implementation if no custom callback provided
322
+ // Get current call stack (or initialize)
323
+ const callStack = config.activityCallStack ?? []
324
+
325
+ // CYCLE DETECTION: Check if this activity is already in the call stack
326
+ if (callStack.includes(templateId)) {
327
+ const cycleChain = [...callStack, templateId].join(' → ')
328
+ console.error(`[Activity] Cycle detected: ${cycleChain}`)
329
+ return {
330
+ id: `blocked_cycle_${Date.now()}`,
331
+ templateId,
332
+ status: "failed",
333
+ error: `Cycle detected: ${cycleChain}`,
334
+ }
335
+ }
336
+
337
+ // DEPTH CHECK: Verify we haven't exceeded maximum depth
338
+ if (callStack.length >= (config.maxNestingDepth ?? 3)) {
339
+ const depthChain = [...callStack, templateId].join(' → ')
340
+ console.warn(`[Activity] Max nesting depth reached: ${depthChain}`)
341
+ return {
342
+ id: `blocked_depth_${Date.now()}`,
343
+ templateId,
344
+ status: "failed",
345
+ error: `Maximum nesting depth reached (${callStack.length + 1} > ${config.maxNestingDepth ?? 3})`,
346
+ }
347
+ }
348
+
349
+ // CONTEXT ISOLATION: Nested activities execute with minimal context
350
+ // to prevent token accumulation across nesting levels
351
+ const template = await loadTemplateFromMCPOrLocal(templateId)
352
+
353
+ // Create isolated config - only essential settings, no accumulated state
354
+ const isolatedConfig: ExecutorConfig = {
355
+ workingDirectory: config.workingDirectory,
356
+ model: config.model,
357
+ provider: config.provider,
358
+ apiKey: config.apiKey,
359
+ systemPrompt: config.systemPrompt,
360
+ // Don't inherit parent's custom tools that might have accumulated state
361
+ customTools: {},
362
+ // Wire callbacks for further nesting (but depth-limited)
363
+ onSearchActivities: config.onSearchActivities,
364
+ onCreateActivity: config.onCreateActivity,
365
+ onActivityExecute: config.onActivityExecute, // Pass observer callback for composition tracking
366
+ // IMPORTANT: Pass updated call stack for cycle detection
367
+ activityCallStack: [...callStack, templateId],
368
+ // Adjust max depth based on current depth
369
+ maxNestingDepth: (config.maxNestingDepth ?? 3) - callStack.length,
370
+ }
371
+
372
+ const nestedExecutor = new ActivityExecutor(isolatedConfig)
373
+ const result = await nestedExecutor.execute({
374
+ template,
375
+ // Only pass essential variables, not accumulated context
376
+ variables: variables ?? {},
377
+ reason,
378
+ parentActivityId: this.currentActivityId,
379
+ parentExecutionId: this.currentExecutionId,
380
+ // Only pass a brief summary of goal context, not full accumulated context
381
+ goalContext: reason ? `Parent goal: ${reason.substring(0, 200)}` : undefined,
382
+ })
383
+
384
+ // Record composition if MCP is enabled
385
+ if (isMCPEnabled() && this.currentActivityId && this.currentExecutionId) {
386
+ const mcp = getMCPClient()
387
+ if (mcp) {
388
+ await mcp.recordComposition({
389
+ parentActivityId: this.currentActivityId,
390
+ childActivityId: template.id,
391
+ executionId: this.currentExecutionId,
392
+ goalContext: reason ? reason.substring(0, 200) : undefined,
393
+ success: result.status === "completed",
394
+ })
395
+ }
396
+ }
397
+
398
+ // Return summarized result to prevent full trace accumulation
399
+ return {
400
+ id: result.id,
401
+ templateId: result.templateId,
402
+ status: result.status,
403
+ summary: result.status === "completed"
404
+ ? `Activity "${templateId}" completed successfully`
405
+ : `Activity "${templateId}" failed: ${result.error?.substring(0, 200) ?? "unknown error"}`,
406
+ }
407
+ }),
408
+ onSearchActivities: config.onSearchActivities, // Wire activity search callback
409
+ onCreateActivity: config.onCreateActivity, // Wire activity creation callback
410
+ onImpulseCreate: (impulse) => {
411
+ // Create impulse
412
+ createImpulse({
413
+ id: impulse.id,
414
+ pointer: impulse.type === "memo" && impulse.content
415
+ ? { type: "memo", content: impulse.content }
416
+ : impulse.type === "file" && impulse.content
417
+ ? { type: "file", path: impulse.content }
418
+ : { type: "custom", resolver: impulse.type, data: {} },
419
+ budget: impulse.budget,
420
+ priority: impulse.priority as "critical" | "high" | "medium" | "low",
421
+ })
422
+ },
423
+ customTools: config.customTools,
424
+ })
425
+ }
426
+
427
+ /**
428
+ * Execute an activity template
429
+ */
430
+ async execute(options: ExecuteOptions): Promise<ActivityExecution> {
431
+ const { template, variables, reason, impulses, onTaskStart, onTaskComplete, parentActivityId, parentExecutionId, goalContext } = options
432
+ const activityId = `act_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
433
+
434
+ // Store context for composition tracking
435
+ this.currentActivityId = template.id
436
+ this.currentExecutionId = activityId
437
+ this.currentGoalContext = goalContext || reason
438
+
439
+ const execution: ActivityExecution = {
440
+ id: activityId,
441
+ templateId: template.id,
442
+ status: "executing",
443
+ variables,
444
+ impulses: impulses || [], // Use provided impulses or empty array
445
+ taskResults: [],
446
+ startedAt: Date.now(),
447
+ // Initialize execution trace for debugging and ribosome pattern
448
+ executionTrace: {
449
+ tasks: [],
450
+ impulsesCreated: [],
451
+ filesModified: [],
452
+ goalContext: goalContext ? {
453
+ goal: goalContext,
454
+ intent: reason || "",
455
+ context: variables
456
+ } : undefined,
457
+ },
458
+ }
459
+
460
+ console.log(`[Activity] Starting: ${template.name} (${activityId})`)
461
+ if (reason) {
462
+ console.log(`[Activity] Reason: ${reason}`)
463
+ }
464
+ if (parentActivityId) {
465
+ console.log(`[Activity] Parent: ${parentActivityId}`)
466
+ }
467
+
468
+ // Register template to backend if MCP is enabled (ensure variant tracking)
469
+ if (isMCPEnabled()) {
470
+ const mcp = getMCPClient()
471
+ if (mcp) {
472
+ console.log(`[Activity] Registering template variant: ${template.id}`)
473
+ await mcp.registerTemplate(template)
474
+ }
475
+ }
476
+
477
+ try {
478
+ // Step 1: Create impulses from context requirements
479
+ const impulses = await this.createImpulsesFromRequirements(activityId, template, variables)
480
+ execution.impulses = impulses
481
+
482
+ // Step 2: Sort tasks by dependencies
483
+ const sortedTasks = this.topologicalSort(template.tasks)
484
+
485
+ // Step 3: Execute tasks in order
486
+ let totalInputTokens = 0
487
+ let totalOutputTokens = 0
488
+
489
+ for (const task of sortedTasks) {
490
+ onTaskStart?.(task.id)
491
+ console.log(`[Task] Executing: ${task.id} - ${task.description}`)
492
+
493
+ const result = await this.executeTask(activityId, task, variables, impulses, undefined, template.id)
494
+ execution.taskResults.push(result)
495
+
496
+ // Add executed task to trace for dashboard display
497
+ if (execution.executionTrace) {
498
+ const executedTask: ExecutedTask = {
499
+ id: task.id,
500
+ description: task.description,
501
+ actualPrompt: result.metadata?.actualPrompt || "",
502
+ toolCalls: result.metadata?.toolCalls || [],
503
+ response: result.output || "",
504
+ result: {
505
+ status: result.status === "completed" ? "success" :
506
+ result.status === "failed" ? "failure" : "partial",
507
+ error: result.error,
508
+ metadata: result.metadata
509
+ },
510
+ inputState: result.metadata?.inputState,
511
+ outputState: result.metadata?.outputState,
512
+ stateTransition: result.metadata?.stateTransition,
513
+ }
514
+ execution.executionTrace.tasks.push(executedTask)
515
+
516
+ // Track files modified across all tasks
517
+ if (result.metadata?.outputState?.filesModified) {
518
+ for (const file of result.metadata.outputState.filesModified) {
519
+ if (!execution.executionTrace.filesModified.includes(file)) {
520
+ execution.executionTrace.filesModified.push(file)
521
+ }
522
+ }
523
+ }
524
+ if (result.metadata?.outputState?.filesCreated) {
525
+ for (const file of result.metadata.outputState.filesCreated) {
526
+ if (!execution.executionTrace.filesModified.includes(file)) {
527
+ execution.executionTrace.filesModified.push(file)
528
+ }
529
+ }
530
+ }
531
+ }
532
+
533
+ if (result.tokens) {
534
+ totalInputTokens += result.tokens.input
535
+ totalOutputTokens += result.tokens.output
536
+ }
537
+
538
+ // Create output impulses if task succeeded
539
+ if (result.status === "completed" && task.outputImpulses && task.outputImpulses.length > 0) {
540
+ for (const impulseId of task.outputImpulses) {
541
+ try {
542
+ createImpulse({
543
+ id: impulseId,
544
+ pointer: {
545
+ type: "memo",
546
+ content: result.output || ""
547
+ },
548
+ budget: 5000,
549
+ priority: "medium"
550
+ })
551
+ console.log(`[Activity] Created output impulse: ${impulseId}`)
552
+ } catch (error) {
553
+ console.error(`[Activity] Failed to create impulse ${impulseId}:`, error instanceof Error ? error.message : String(error))
554
+ }
555
+ }
556
+ }
557
+
558
+ onTaskComplete?.(task.id, result)
559
+
560
+ if (result.status === "failed") {
561
+ console.error(`[Task] Failed: ${task.id} - ${result.error}`)
562
+
563
+ // Check retry policy
564
+ if (task.retry && task.retry.maxAttempts > 1) {
565
+ let retryCount = 1
566
+ while (retryCount < task.retry.maxAttempts && result.status === "failed") {
567
+ console.log(`[Task] Retrying ${task.id} (attempt ${retryCount + 1}/${task.retry.maxAttempts})`)
568
+ const retryResult = await this.executeTask(activityId, task, variables, impulses, result.error, template.id)
569
+ execution.taskResults[execution.taskResults.length - 1] = retryResult
570
+
571
+ // Update executed task in trace with retry result
572
+ if (execution.executionTrace && execution.executionTrace.tasks.length > 0) {
573
+ const executedTask: ExecutedTask = {
574
+ id: task.id,
575
+ description: task.description,
576
+ actualPrompt: retryResult.metadata?.actualPrompt || "",
577
+ toolCalls: retryResult.metadata?.toolCalls || [],
578
+ response: retryResult.output || "",
579
+ result: {
580
+ status: retryResult.status === "completed" ? "success" :
581
+ retryResult.status === "failed" ? "failure" : "partial",
582
+ error: retryResult.error,
583
+ metadata: retryResult.metadata
584
+ },
585
+ inputState: retryResult.metadata?.inputState,
586
+ outputState: retryResult.metadata?.outputState,
587
+ stateTransition: retryResult.metadata?.stateTransition,
588
+ }
589
+ execution.executionTrace.tasks[execution.executionTrace.tasks.length - 1] = executedTask
590
+ }
591
+
592
+ if (retryResult.tokens) {
593
+ totalInputTokens += retryResult.tokens.input
594
+ totalOutputTokens += retryResult.tokens.output
595
+ }
596
+
597
+ if (retryResult.status === "completed") break
598
+ retryCount++
599
+ }
600
+ }
601
+
602
+ // If still failed after retries, abort
603
+ if (execution.taskResults[execution.taskResults.length - 1]?.status === "failed") {
604
+ execution.status = "failed"
605
+ break
606
+ }
607
+ }
608
+ }
609
+
610
+ // Step 4: Complete execution
611
+ execution.completedAt = Date.now()
612
+ if (execution.status !== "failed") {
613
+ execution.status = "completed"
614
+ }
615
+
616
+ execution.metrics = {
617
+ duration: execution.completedAt - execution.startedAt,
618
+ cost: this.estimateCost(totalInputTokens, totalOutputTokens),
619
+ totalTokens: { input: totalInputTokens, output: totalOutputTokens },
620
+ }
621
+
622
+ console.log(`[Activity] Completed: ${execution.status} in ${execution.metrics.duration}ms`)
623
+
624
+ // Report execution to MCP backend if enabled
625
+ if (isMCPEnabled()) {
626
+ const mcp = getMCPClient()
627
+ if (mcp) {
628
+ console.log(`[Activity] Reporting execution to MCP backend...`)
629
+ const reported = await mcp.reportExecution(execution)
630
+ if (reported) {
631
+ console.log(`[Activity] ✓ Execution reported to backend`)
632
+ } else {
633
+ console.warn(`[Activity] ⚠ Failed to report execution to backend`)
634
+ }
635
+
636
+ // Store execution trace for debugging-as-activity
637
+ if (execution.executionTrace) {
638
+ console.log(`[Activity] Storing execution trace...`)
639
+ const traceStored = await mcp.storeExecutionTrace(execution)
640
+ if (traceStored) {
641
+ console.log(`[Activity] ✓ Execution trace stored: ${execution.id}`)
642
+ } else {
643
+ console.warn(`[Activity] ⚠ Failed to store execution trace`)
644
+ }
645
+ }
646
+
647
+ // Report tool usage patterns
648
+ if (this.toolCallRecords.length > 0) {
649
+ console.log(`[Activity] Reporting ${this.toolCallRecords.length} tool usage records...`)
650
+ const activitySucceeded = execution.status === "completed"
651
+
652
+ for (const record of this.toolCallRecords) {
653
+ await mcp.recordToolUsage({
654
+ toolName: record.toolName,
655
+ activityVariantId: activityId,
656
+ taskId: undefined, // Could be enhanced to track per-task
657
+ executionId: execution.id,
658
+ toolSucceeded: record.result.success,
659
+ activitySucceeded,
660
+ paramsComplexity: record.params ? JSON.stringify(record.params).length : 0,
661
+ }).catch((error) => {
662
+ console.warn(`[Activity] ⚠ Failed to report tool usage for ${record.toolName}:`, error)
663
+ })
664
+ }
665
+
666
+ console.log(`[Activity] ✓ Tool usage patterns reported`)
667
+ }
668
+
669
+ // RIBOSOME: Extract reusable template from successful execution
670
+ // Only extract if:
671
+ // 1. Execution was successful
672
+ // 2. Has execution trace (detailed task history)
673
+ // 3. Not already a ribosome-generated template (avoid infinite loop)
674
+ if (
675
+ execution.status === "completed" &&
676
+ execution.executionTrace &&
677
+ template.metadata?.author !== "ribosome"
678
+ ) {
679
+ try {
680
+ console.log(`[Ribosome] Extracting template from successful execution...`)
681
+
682
+ // Infer category from original template or default to "tool"
683
+ const category = template.category || "tool"
684
+
685
+ // Generate a descriptive name from the goal or template name
686
+ const goalDescription = execution.executionTrace.goalContext?.goal || template.name || "unnamed"
687
+ const shortGoal = goalDescription.substring(0, 50).replace(/[^a-zA-Z0-9]/g, "-").toLowerCase()
688
+ const templateName = `learned-${shortGoal}-${Date.now().toString(36)}`
689
+
690
+ // Call ribosome to extract template
691
+ const extractedTemplate = assembleTemplateFromExecution(
692
+ execution,
693
+ templateName,
694
+ category as "feature" | "bugfix" | "refactor" | "tool" | "infrastructure"
695
+ )
696
+
697
+ console.log(`[Ribosome] Template extracted: ${extractedTemplate.name}`)
698
+
699
+ // Register with backend for Thompson Sampling
700
+ const registered = await mcp.registerTemplate(extractedTemplate)
701
+ if (registered) {
702
+ console.log(`[Ribosome] ✓ Template registered: ${extractedTemplate.id}`)
703
+
704
+ // Enqueue for future testing via boredom queue (low priority)
705
+ try {
706
+ const enqueueResponse = await fetch(`${(mcp as any).endpoint}/v2/activities/boredom/enqueue`, {
707
+ method: "POST",
708
+ headers: { "Content-Type": "application/json" },
709
+ body: JSON.stringify({
710
+ templateId: extractedTemplate.id,
711
+ priority: "low",
712
+ reason: `Ribosome-extracted from ${execution.id}`,
713
+ variables: {},
714
+ }),
715
+ })
716
+
717
+ if (enqueueResponse.ok) {
718
+ console.log(`[Ribosome] ✓ Template enqueued for testing`)
719
+ }
720
+ } catch (enqueueError) {
721
+ // Non-fatal: template is registered, just not queued for immediate testing
722
+ console.log(`[Ribosome] Template registered but not enqueued for testing`)
723
+ }
724
+ } else {
725
+ console.log(`[Ribosome] Template already exists or registration failed`)
726
+ }
727
+ } catch (ribosomeError) {
728
+ // Non-fatal: execution succeeded, ribosome extraction is bonus
729
+ console.log(`[Ribosome] Could not extract template:`, ribosomeError)
730
+ }
731
+ }
732
+ }
733
+ }
734
+
735
+ } catch (error) {
736
+ execution.status = "failed"
737
+ execution.completedAt = Date.now()
738
+ console.error(`[Activity] Error:`, error)
739
+
740
+ // Still try to report failure to backend
741
+ if (isMCPEnabled()) {
742
+ const mcp = getMCPClient()
743
+ if (mcp) {
744
+ await mcp.reportExecution(execution).catch(() => {
745
+ // Ignore reporting errors on failure
746
+ })
747
+ }
748
+ }
749
+ }
750
+
751
+ return execution
752
+ }
753
+
754
+ /**
755
+ * Get current executor state for TUI display
756
+ * Returns real-time information about what MiniBob is doing
757
+ */
758
+ getState(): {
759
+ currentActivityId: string | undefined
760
+ currentExecutionId: string | undefined
761
+ currentGoalContext: string | undefined
762
+ toolCallRecords: Array<{ toolName: string; params: any; result: ToolResult; timestamp: number }>
763
+ workingDirectory: string
764
+ } {
765
+ return {
766
+ currentActivityId: this.currentActivityId,
767
+ currentExecutionId: this.currentExecutionId,
768
+ currentGoalContext: this.currentGoalContext,
769
+ toolCallRecords: this.toolCallRecords.slice(-10), // Last 10 tool calls
770
+ workingDirectory: this.config.workingDirectory,
771
+ }
772
+ }
773
+
774
+ /**
775
+ * Create impulses from template context requirements
776
+ */
777
+ private async createImpulsesFromRequirements(
778
+ activityId: string,
779
+ template: ActivityTemplate,
780
+ variables: Record<string, unknown>
781
+ ): Promise<Impulse[]> {
782
+ const impulses: Impulse[] = []
783
+
784
+ if (!template.contextRequirements) {
785
+ return impulses
786
+ }
787
+
788
+ for (const req of template.contextRequirements) {
789
+ // Interpolate source with variables
790
+ const source = this.interpolate(req.source, variables)
791
+
792
+ let impulse: Impulse
793
+
794
+ switch (req.type) {
795
+ case "file":
796
+ impulse = createImpulse({
797
+ id: req.id,
798
+ pointer: { type: "file", path: source },
799
+ budget: req.budget,
800
+ priority: req.priority,
801
+ })
802
+ break
803
+
804
+ case "glob":
805
+ // Resolve glob to files and create impulses for each
806
+ // For simplicity, create a single impulse that will list files
807
+ impulse = createImpulse({
808
+ id: req.id,
809
+ pointer: {
810
+ type: "custom",
811
+ resolver: "glob",
812
+ data: { pattern: source, cwd: this.config.workingDirectory },
813
+ },
814
+ budget: req.budget,
815
+ priority: req.priority,
816
+ })
817
+ break
818
+
819
+ case "memo":
820
+ impulse = createImpulse({
821
+ id: req.id,
822
+ pointer: { type: "memo", content: source },
823
+ budget: req.budget,
824
+ priority: req.priority,
825
+ })
826
+ break
827
+
828
+ default:
829
+ impulse = createImpulse({
830
+ id: req.id,
831
+ pointer: {
832
+ type: "custom",
833
+ resolver: req.type,
834
+ data: { source },
835
+ },
836
+ budget: req.budget,
837
+ priority: req.priority,
838
+ })
839
+ }
840
+
841
+ impulses.push(impulse)
842
+ }
843
+
844
+ // Register glob resolver if not already registered
845
+ getImpulseStore().registerResolver("glob", async (data) => {
846
+ const pattern = data.pattern as string
847
+ const cwd = (data.cwd as string) ?? this.config.workingDirectory
848
+ const glob = new Bun.Glob(pattern)
849
+ const files: string[] = []
850
+ for await (const file of glob.scan({ cwd })) {
851
+ files.push(file)
852
+ if (files.length >= 50) break
853
+ }
854
+ return files.join("\n")
855
+ })
856
+
857
+ return impulses
858
+ }
859
+
860
+ /**
861
+ * Execute a single task
862
+ */
863
+ private async executeTask(
864
+ activityId: string,
865
+ task: ActivityTask,
866
+ variables: Record<string, unknown>,
867
+ impulses: Impulse[],
868
+ lastError?: string,
869
+ templateId?: string
870
+ ): Promise<TaskResult> {
871
+ const startedAt = Date.now()
872
+
873
+ // Phase 1.8: Declare variables outside try block for catch block access
874
+ const taskImpulseIds = task.impulseReferences ?? impulses.map((i) => i.id)
875
+ let impulsesToLoad = taskImpulseIds
876
+
877
+ // Phase 1.8+: Declare state tracking variables outside try block
878
+ let inputState: any
879
+ let beforeHashes: Record<string, string> = {}
880
+ let outputState: any
881
+ let afterHashes: Record<string, string> = {}
882
+ let stateTransition: any
883
+ let prompt = ""
884
+
885
+ try {
886
+ // Phase 1.8: Intelligent impulse filtering
887
+ let loadedImpulses: Impulse[]
888
+ let filteringSummary: ReturnType<typeof generateFilteringSummary> | undefined
889
+
890
+ // Query relevance metrics if MCP is enabled
891
+ if (isMCPEnabled() && templateId && taskImpulseIds.length > 0) {
892
+ const mcp = getMCPClient()
893
+ if (mcp) {
894
+ try {
895
+ // Query relevance metrics from backend
896
+ const metrics = await mcp.queryImpulseRelevance({
897
+ activityVariantId: templateId,
898
+ impulseIds: taskImpulseIds,
899
+ })
900
+
901
+ // Filter impulses based on learned relevance
902
+ const filterResult = filterImpulsesByRelevance(taskImpulseIds, metrics)
903
+ impulsesToLoad = filterResult.toLoad
904
+
905
+ // Calculate and log savings
906
+ if (filterResult.toSkip.length > 0) {
907
+ const impulseStore = getImpulseStore()
908
+ const tokenSizes = new Map<string, number>()
909
+
910
+ for (const impulseId of taskImpulseIds) {
911
+ const impulse = impulseStore.get(impulseId)
912
+ if (impulse) {
913
+ tokenSizes.set(impulseId, estimateImpulseTokens(impulse))
914
+ }
915
+ }
916
+
917
+ const savings = calculateSavings(filterResult.toSkip, tokenSizes)
918
+ filteringSummary = generateFilteringSummary(filterResult, savings)
919
+
920
+ console.log(`[Impulse Filter] Task ${task.id}:`)
921
+ console.log(` - Original: ${taskImpulseIds.length} impulses`)
922
+ console.log(` - Loaded: ${filterResult.toLoad.length} impulses`)
923
+ console.log(` - Skipped: ${filterResult.toSkip.length} impulses`)
924
+ console.log(` - Saved: ~${savings.tokensSaved} tokens (~$${savings.costSaved.toFixed(4)})`)
925
+ }
926
+ } catch (error) {
927
+ // Fallback to loading all impulses if filtering fails
928
+ console.warn(`[Impulse Filter] Failed to filter impulses, loading all:`, error instanceof Error ? error.message : String(error))
929
+ impulsesToLoad = taskImpulseIds
930
+ }
931
+ }
932
+ }
933
+
934
+ // Load filtered impulses
935
+ loadedImpulses = await loadImpulses(impulsesToLoad)
936
+
937
+ // Phase 1.8: Capture input state BEFORE task execution
938
+ inputState = await captureInputState(
939
+ this.config.workingDirectory,
940
+ impulsesToLoad,
941
+ variables
942
+ )
943
+
944
+ // Capture file hashes for state transition tracking
945
+ beforeHashes = await captureFileHashes(
946
+ this.config.workingDirectory,
947
+ inputState.filesAvailable
948
+ )
949
+
950
+ // Build prompt with impulse substitution first
951
+ prompt = await substituteImpulses(task.prompt.template, impulsesToLoad)
952
+ prompt = this.interpolate(prompt, variables)
953
+
954
+ // Add impulse context
955
+ const impulseContext = formatImpulsesForContext(loadedImpulses)
956
+ if (impulseContext) {
957
+ prompt = `${impulseContext}\n\n${prompt}`
958
+ }
959
+
960
+ // Add error context if retrying
961
+ if (lastError) {
962
+ prompt = `Previous attempt failed with error:\n${lastError}\n\nPlease try again, addressing the error.\n\n${prompt}`
963
+ }
964
+
965
+ // Build messages
966
+ const messages: Message[] = [
967
+ {
968
+ role: "system",
969
+ content: this.config.systemPrompt ?? this.getDefaultSystemPrompt(),
970
+ },
971
+ {
972
+ role: "user",
973
+ content: prompt,
974
+ },
975
+ ]
976
+
977
+ // Clear previous tool call records for this task
978
+ this.toolCallRecords = []
979
+
980
+ // Wrap tool handlers to capture tool execution data
981
+ const wrappedHandlers: Record<string, ToolHandler> = {}
982
+ for (const [toolName, handler] of Object.entries(this.toolHandlers)) {
983
+ wrappedHandlers[toolName] = async (params: any): Promise<ToolResult> => {
984
+ const toolResult = await handler(params)
985
+
986
+ // Record tool call for impulse creation
987
+ this.toolCallRecords.push({
988
+ toolName,
989
+ params,
990
+ result: toolResult,
991
+ timestamp: Date.now(),
992
+ })
993
+
994
+ return toolResult
995
+ }
996
+ }
997
+
998
+ // Execute with tool calling (using wrapped handlers)
999
+ const result = await this.llm.completeWithTools(
1000
+ {
1001
+ model: this.config.model,
1002
+ messages,
1003
+ tools: getAllToolDefinitions(this.customToolDefinitions),
1004
+ maxTokens: task.prompt.maxTokens ?? 4096,
1005
+ },
1006
+ wrappedHandlers
1007
+ )
1008
+
1009
+ // Create impulses from tool calls
1010
+ for (const record of this.toolCallRecords) {
1011
+ if (record.result.success && record.result.output) {
1012
+ const impulseId = `tool:${record.toolName}:${task.id}:${record.timestamp}`
1013
+
1014
+ createImpulse({
1015
+ id: impulseId,
1016
+ pointer: {
1017
+ type: "memo", // Store tool output as memo
1018
+ content: record.result.output,
1019
+ },
1020
+ budget: Math.min(Math.ceil(record.result.output.length / 4), 2000), // Estimate tokens
1021
+ priority: "medium",
1022
+ tags: [`tool:${record.toolName}`, `activity:${activityId}`, `task:${task.id}`],
1023
+ })
1024
+
1025
+ console.log(`[Impulse] Created from tool call: ${impulseId}`)
1026
+ }
1027
+ }
1028
+
1029
+ // Store output for potential downstream tasks
1030
+ storeActivityOutput(activityId, task.id, result.content)
1031
+
1032
+ // Phase 1.8: Capture output state AFTER task execution
1033
+ outputState = await captureOutputState(
1034
+ this.config.workingDirectory,
1035
+ inputState.filesAvailable,
1036
+ this.toolCallRecords
1037
+ )
1038
+
1039
+ // Capture file hashes for state transition tracking
1040
+ afterHashes = await captureFileHashes(
1041
+ this.config.workingDirectory,
1042
+ outputState.filesCreated.concat(outputState.filesModified)
1043
+ )
1044
+
1045
+ stateTransition = {
1046
+ before: beforeHashes,
1047
+ after: afterHashes,
1048
+ workingDirectory: this.config.workingDirectory,
1049
+ }
1050
+
1051
+ // Run validation if specified
1052
+ if (task.validation) {
1053
+ const validationResult = await this.runValidation(task.validation, result.content)
1054
+ if (!validationResult.success) {
1055
+ // Phase 1.8: Record impulse relevance (validation failed)
1056
+ await this.recordImpulseRelevance(templateId, taskImpulseIds, impulsesToLoad, false)
1057
+
1058
+ return {
1059
+ taskId: task.id,
1060
+ status: "failed",
1061
+ output: result.content,
1062
+ error: `Validation failed: ${validationResult.error}`,
1063
+ startedAt,
1064
+ completedAt: Date.now(),
1065
+ tokens: { input: result.usage.inputTokens, output: result.usage.outputTokens },
1066
+ // Phase 1.8+: Include state even for validation failures (helps debugging)
1067
+ metadata: {
1068
+ inputState,
1069
+ outputState,
1070
+ stateTransition,
1071
+ toolCalls: this.toolCallRecords.map((r, index) => ({
1072
+ id: `tool:${r.toolName}:${task.id}:${r.timestamp}`,
1073
+ name: r.toolName,
1074
+ arguments: r.params,
1075
+ result: {
1076
+ success: r.result.success,
1077
+ output: r.result.output,
1078
+ error: r.result.error,
1079
+ metadata: r.result.metadata
1080
+ }
1081
+ })),
1082
+ actualPrompt: prompt,
1083
+ },
1084
+ }
1085
+ }
1086
+ }
1087
+
1088
+ // Phase 1.8: Record impulse relevance (success)
1089
+ await this.recordImpulseRelevance(templateId, taskImpulseIds, impulsesToLoad, true)
1090
+
1091
+ return {
1092
+ taskId: task.id,
1093
+ status: "completed",
1094
+ output: result.content,
1095
+ startedAt,
1096
+ completedAt: Date.now(),
1097
+ tokens: { input: result.usage.inputTokens, output: result.usage.outputTokens },
1098
+ // Phase 1.8+: Include state tracking for ribosome
1099
+ metadata: {
1100
+ inputState,
1101
+ outputState,
1102
+ stateTransition,
1103
+ toolCalls: this.toolCallRecords.map((r, index) => ({
1104
+ id: `tool:${r.toolName}:${task.id}:${r.timestamp}`,
1105
+ name: r.toolName,
1106
+ arguments: r.params,
1107
+ result: {
1108
+ success: r.result.success,
1109
+ output: r.result.output,
1110
+ error: r.result.error,
1111
+ metadata: r.result.metadata
1112
+ }
1113
+ })),
1114
+ actualPrompt: prompt,
1115
+ },
1116
+ }
1117
+ } catch (error) {
1118
+ // Phase 1.8: Record impulse relevance (execution failed)
1119
+ await this.recordImpulseRelevance(templateId, taskImpulseIds, impulsesToLoad, false)
1120
+
1121
+ // Phase 1.8+: Try to capture output state even on error
1122
+ let errorOutputState
1123
+ let errorAfterHashes
1124
+ try {
1125
+ // Only capture state if inputState was initialized (error occurred after state capture)
1126
+ if (inputState && inputState.filesAvailable) {
1127
+ errorOutputState = await captureOutputState(
1128
+ this.config.workingDirectory,
1129
+ inputState.filesAvailable,
1130
+ this.toolCallRecords
1131
+ )
1132
+ errorAfterHashes = await captureFileHashes(
1133
+ this.config.workingDirectory,
1134
+ errorOutputState.filesCreated.concat(errorOutputState.filesModified)
1135
+ )
1136
+ }
1137
+ } catch (stateError) {
1138
+ // State capture failed - not critical, continue
1139
+ console.warn(`[State Capture] Failed during error handling:`, stateError)
1140
+ }
1141
+
1142
+ return {
1143
+ taskId: task.id,
1144
+ status: "failed",
1145
+ error: error instanceof Error ? error.message : String(error),
1146
+ startedAt,
1147
+ completedAt: Date.now(),
1148
+ // Phase 1.8+: Include partial state (very helpful for debugging failures)
1149
+ metadata: errorOutputState ? {
1150
+ inputState,
1151
+ outputState: errorOutputState,
1152
+ stateTransition: {
1153
+ before: beforeHashes,
1154
+ after: errorAfterHashes || {},
1155
+ workingDirectory: this.config.workingDirectory,
1156
+ },
1157
+ toolCalls: this.toolCallRecords.map((r, index) => ({
1158
+ id: `tool:${r.toolName}:${task.id}:${r.timestamp}`,
1159
+ name: r.toolName,
1160
+ arguments: r.params,
1161
+ result: {
1162
+ success: r.result.success,
1163
+ output: r.result.output,
1164
+ error: r.result.error,
1165
+ metadata: r.result.metadata
1166
+ }
1167
+ })),
1168
+ } : undefined,
1169
+ }
1170
+ }
1171
+ }
1172
+
1173
+ /**
1174
+ * Phase 1.8: Record impulse relevance metrics to backend
1175
+ */
1176
+ private async recordImpulseRelevance(
1177
+ templateId: string | undefined,
1178
+ allImpulseIds: string[],
1179
+ loadedImpulseIds: string[],
1180
+ executionSucceeded: boolean
1181
+ ): Promise<void> {
1182
+ if (!isMCPEnabled() || !templateId || allImpulseIds.length === 0) {
1183
+ return
1184
+ }
1185
+
1186
+ const mcp = getMCPClient()
1187
+ if (!mcp) return
1188
+
1189
+ try {
1190
+ // Record relevance for each impulse
1191
+ for (const impulseId of allImpulseIds) {
1192
+ const wasLoaded = loadedImpulseIds.includes(impulseId)
1193
+ await mcp.recordImpulseRelevance({
1194
+ impulseId,
1195
+ activityVariantId: templateId,
1196
+ wasLoaded,
1197
+ executionSucceeded,
1198
+ })
1199
+ }
1200
+ } catch (error) {
1201
+ // Non-blocking: log error but don't fail task
1202
+ console.warn(`[Impulse Filter] Failed to record relevance:`, error instanceof Error ? error.message : String(error))
1203
+ }
1204
+ }
1205
+
1206
+ /**
1207
+ * Run validation checks
1208
+ */
1209
+ private async runValidation(validation: ActivityTask["validation"], taskOutput?: string): Promise<{ success: boolean; error?: string }> {
1210
+ if (!validation) return { success: true }
1211
+
1212
+ // Check required files exist
1213
+ if (validation.requiredFiles) {
1214
+ for (const filePath of validation.requiredFiles) {
1215
+ const file = safeReadFile(filePath)
1216
+ if (!(await file.exists())) {
1217
+ return { success: false, error: `Required file missing: ${filePath}` }
1218
+ }
1219
+ }
1220
+ }
1221
+
1222
+ // Check required patterns
1223
+ if (validation.requiredPatterns) {
1224
+ for (const patternSpec of validation.requiredPatterns) {
1225
+ // Support both string patterns (checked in task output) and { file, pattern } objects
1226
+ if (typeof patternSpec === 'string') {
1227
+ // Simple string pattern - check in task output
1228
+ if (!taskOutput) {
1229
+ return { success: false, error: `Cannot validate pattern "${patternSpec}": task output not available` }
1230
+ }
1231
+ const regex = new RegExp(patternSpec)
1232
+ if (!regex.test(taskOutput)) {
1233
+ return { success: false, error: `Required pattern not found in task output: ${patternSpec}` }
1234
+ }
1235
+ } else {
1236
+ // Object format { file, pattern } - check in specific file
1237
+ const { file: filePath, pattern } = patternSpec
1238
+ const file = safeReadFile(filePath)
1239
+ if (!(await file.exists())) {
1240
+ return { success: false, error: `File missing for pattern check: ${filePath}` }
1241
+ }
1242
+ const content = await file.text()
1243
+ const regex = new RegExp(pattern)
1244
+ if (!regex.test(content)) {
1245
+ return { success: false, error: `Pattern not found in ${filePath}: ${pattern}` }
1246
+ }
1247
+ }
1248
+ }
1249
+ }
1250
+
1251
+ // Check forbidden patterns
1252
+ if (validation.forbiddenPatterns) {
1253
+ for (const { file: filePath, pattern } of validation.forbiddenPatterns) {
1254
+ const file = safeReadFile(filePath)
1255
+ if (await file.exists()) {
1256
+ const content = await file.text()
1257
+ const regex = new RegExp(pattern)
1258
+ if (regex.test(content)) {
1259
+ return { success: false, error: `Forbidden pattern found in ${filePath}: ${pattern}` }
1260
+ }
1261
+ }
1262
+ }
1263
+ }
1264
+
1265
+ // Run validation commands
1266
+ if (validation.commands) {
1267
+ for (const { command, expectedOutput } of validation.commands) {
1268
+ const bashHandler = this.toolHandlers.bash
1269
+ if (!bashHandler) {
1270
+ return { success: false, error: "bash tool handler not available" }
1271
+ }
1272
+ const result = await bashHandler({ command })
1273
+ if (!result.success) {
1274
+ return { success: false, error: `Validation command failed: ${command}` }
1275
+ }
1276
+ if (expectedOutput && !result.output?.includes(expectedOutput)) {
1277
+ return { success: false, error: `Unexpected output from: ${command}` }
1278
+ }
1279
+ }
1280
+ }
1281
+
1282
+ return { success: true }
1283
+ }
1284
+
1285
+ /**
1286
+ * Topological sort for task dependencies
1287
+ */
1288
+ private topologicalSort(tasks: ActivityTask[]): ActivityTask[] {
1289
+ const taskMap = new Map(tasks.map((t) => [t.id, t]))
1290
+ const visited = new Set<string>()
1291
+ const result: ActivityTask[] = []
1292
+
1293
+ const visit = (taskId: string): void => {
1294
+ if (visited.has(taskId)) return
1295
+ visited.add(taskId)
1296
+
1297
+ const task = taskMap.get(taskId)
1298
+ if (!task) return
1299
+
1300
+ for (const depId of task.dependencies ?? []) {
1301
+ visit(depId)
1302
+ }
1303
+
1304
+ result.push(task)
1305
+ }
1306
+
1307
+ for (const task of tasks) {
1308
+ visit(task.id)
1309
+ }
1310
+
1311
+ return result
1312
+ }
1313
+
1314
+ /**
1315
+ * Interpolate variables into template string
1316
+ */
1317
+ private interpolate(template: string, variables: Record<string, unknown>): string {
1318
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
1319
+ const value = variables[key]
1320
+ if (value === undefined) return `{{${key}}}`
1321
+ if (typeof value === "object") return JSON.stringify(value, null, 2)
1322
+ return String(value)
1323
+ })
1324
+ }
1325
+
1326
+ /**
1327
+ * Estimate cost based on token usage
1328
+ */
1329
+ private estimateCost(inputTokens: number, outputTokens: number): number {
1330
+ // Approximate costs for Claude 3.5 Sonnet
1331
+ const inputCostPerMillion = 3.0
1332
+ const outputCostPerMillion = 15.0
1333
+
1334
+ return (
1335
+ (inputTokens / 1_000_000) * inputCostPerMillion +
1336
+ (outputTokens / 1_000_000) * outputCostPerMillion
1337
+ )
1338
+ }
1339
+
1340
+ /**
1341
+ * Get default system prompt
1342
+ *
1343
+ * ARCHITECTURE: Enforces activity-first constraint per corrected trailblazing architecture.
1344
+ * MiniBob must search for existing activities and create new ones for non-trivial tasks.
1345
+ */
1346
+ private getDefaultSystemPrompt(): string {
1347
+ return `You are minibob, an autonomous vessel that creates and composes activities instead of using direct tool calls.
1348
+
1349
+ **CORE PRINCIPLE**: You learn by creating reusable activities, not by executing one-off tool calls.
1350
+
1351
+ **CONSTRAINTS - ACTIVITY-FIRST APPROACH**:
1352
+
1353
+ For NON-TRIVIAL tasks (anything beyond simple reads/status checks):
1354
+ 1. search_activities() to find existing templates
1355
+ 2. If match found: Execute the activity
1356
+ 3. If no match: create_activity_goal_seeking() to create new template
1357
+ 4. Record your reasoning for the choice
1358
+
1359
+ For TRIVIAL tasks (simple operations that don't benefit from templating):
1360
+ - Simple file reads (read a single file to check content)
1361
+ - Basic git status checks (git status, git diff)
1362
+ - Simple list operations (ls, find single file)
1363
+ ✅ Direct tool use allowed
1364
+
1365
+ **EXAMPLES**:
1366
+
1367
+ Non-Trivial (MUST use activities):
1368
+ ❌ "Fix all TypeScript errors" → search_activities → create if no match
1369
+ ❌ "Add authentication" → search_activities → create if no match
1370
+ ❌ "Refactor component" → search_activities → create if no match
1371
+ ❌ "Deploy to production" → search_activities → create if no match
1372
+
1373
+ Trivial (Direct tools OK):
1374
+ ✅ "Read src/index.ts to see imports" → read tool
1375
+ ✅ "Check git status" → bash git status
1376
+ ✅ "List files in src/" → bash ls src/
1377
+
1378
+ **WORKFLOW**:
1379
+ 1. Analyze task complexity
1380
+ 2. If non-trivial: search_activities() first
1381
+ - Found match? Execute it
1382
+ - No match? create_activity_goal_seeking()
1383
+ 3. If trivial: Use direct tools (read, bash, git)
1384
+ 4. Always explain your reasoning
1385
+
1386
+ **GUIDELINES**:
1387
+ - Prefer activity composition over direct execution
1388
+ - Create reusable templates from complex workflows
1389
+ - Search before creating (avoid duplicate activities)
1390
+ - Report activity execution results clearly
1391
+ - When stuck, ask specific targeted questions
1392
+
1393
+ Be autonomous, learn from execution, and build reusable knowledge.`
1394
+ }
1395
+ }
1396
+
1397
+ /**
1398
+ * Load an activity template from a JSON file
1399
+ */
1400
+ export async function loadTemplate(path: string): Promise<ActivityTemplate> {
1401
+ const file = safeReadFile(path)
1402
+ if (!(await file.exists())) {
1403
+ throw new Error(`Template not found: ${path}`)
1404
+ }
1405
+ const content = await file.json()
1406
+ return content as ActivityTemplate
1407
+ }
1408
+
1409
+ /**
1410
+ * Load activity template from MCP backend or local file
1411
+ *
1412
+ * If templateIdOrPath looks like a file path (contains / or .json), load from file.
1413
+ * Otherwise, try to load from MCP backend first, then fall back to local templates/ directory.
1414
+ */
1415
+ export async function loadTemplateFromMCPOrLocal(templateIdOrPath: string): Promise<ActivityTemplate> {
1416
+ // Check if it's a file path
1417
+ if (templateIdOrPath.includes("/") || templateIdOrPath.endsWith(".json")) {
1418
+ return loadTemplate(templateIdOrPath)
1419
+ }
1420
+
1421
+ // Try MCP first if enabled
1422
+ if (isMCPEnabled()) {
1423
+ const mcp = getMCPClient()
1424
+ if (mcp) {
1425
+ console.log(`[Activity] Fetching template "${templateIdOrPath}" from MCP backend...`)
1426
+ const template = await mcp.getActivityTemplate(templateIdOrPath)
1427
+ if (template) {
1428
+ console.log(`[Activity] ✓ Template loaded from MCP backend`)
1429
+ return template
1430
+ }
1431
+ console.log(`[Activity] Template not found in MCP, trying local...`)
1432
+ }
1433
+ }
1434
+
1435
+ // Fall back to local templates directory
1436
+ const localPath = `templates/${templateIdOrPath}.json`
1437
+ return loadTemplate(localPath)
1438
+ }
1439
+
1440
+ /**
1441
+ * Create executor and run activity
1442
+ */
1443
+ export async function runActivity(
1444
+ config: ExecutorConfig,
1445
+ templatePath: string,
1446
+ variables: Record<string, unknown>,
1447
+ reason?: string
1448
+ ): Promise<ActivityExecution> {
1449
+ const template = await loadTemplate(templatePath)
1450
+ const executor = new ActivityExecutor(config)
1451
+ return executor.execute({ template, variables, reason })
1452
+ }
1453
+
1454
+ /**
1455
+ * Format execution duration in human-readable format
1456
+ * @param ms - Duration in milliseconds
1457
+ * @returns Formatted string (e.g., "1.2s" or "345ms")
1458
+ */
1459
+ export function formatDuration(ms: number): string {
1460
+ if (ms >= 1000) {
1461
+ return `${(ms / 1000).toFixed(1)}s`
1462
+ }
1463
+ return `${Math.round(ms)}ms`
1464
+ }