@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,449 @@
1
+ /**
2
+ * Composition Observer - Track and visualize activity composition
3
+ *
4
+ * Provides real-time observation of which activities call which,
5
+ * detects cycles, tracks performance, and generates reports.
6
+ */
7
+
8
+ import type { ExecutorConfig } from './activity'
9
+
10
+ // ============================================================================
11
+ // TYPES
12
+ // ============================================================================
13
+
14
+ export interface CompositionEvent {
15
+ timestamp: number
16
+ parent: string
17
+ child: string
18
+ depth: number
19
+ reason?: string
20
+ status?: 'started' | 'completed' | 'failed' | 'blocked'
21
+ duration?: number
22
+ error?: string
23
+ }
24
+
25
+ export interface ActivityMetrics {
26
+ activityId: string
27
+ calls: number
28
+ successes: number
29
+ failures: number
30
+ blocked: number
31
+ totalDuration: number
32
+ avgDuration: number
33
+ successRate: number
34
+ }
35
+
36
+ export interface CompositionGraph {
37
+ nodes: Map<string, { calls: number; depths: Set<number> }>
38
+ edges: Map<string, { count: number; successes: number }>
39
+ }
40
+
41
+ // ============================================================================
42
+ // COMPOSITION OBSERVER
43
+ // ============================================================================
44
+
45
+ export class CompositionObserver {
46
+ private events: CompositionEvent[] = []
47
+ private callStack: string[] = []
48
+ private activityMetrics = new Map<string, {
49
+ calls: number
50
+ successes: number
51
+ failures: number
52
+ blocked: number
53
+ totalDuration: number
54
+ }>()
55
+ private startTimes = new Map<string, number>()
56
+ private onEventCallback?: (event: CompositionEvent) => void
57
+
58
+ /**
59
+ * Create enhanced executor config with composition tracking
60
+ */
61
+ wrapConfig(config: ExecutorConfig): ExecutorConfig {
62
+ const originalOnActivityExecute = config.onActivityExecute
63
+
64
+ return {
65
+ ...config,
66
+ onActivityExecute: async (templateId, variables, reason) => {
67
+ const parent = this.callStack[this.callStack.length - 1] || 'ROOT'
68
+ const depth = this.callStack.length
69
+ const eventId = `${templateId}_${Date.now()}`
70
+
71
+ // Record start event
72
+ const startEvent: CompositionEvent = {
73
+ timestamp: Date.now(),
74
+ parent,
75
+ child: templateId,
76
+ depth,
77
+ reason,
78
+ status: 'started'
79
+ }
80
+ this.recordEvent(startEvent)
81
+
82
+ // Initialize metrics
83
+ if (!this.activityMetrics.has(templateId)) {
84
+ this.activityMetrics.set(templateId, {
85
+ calls: 0,
86
+ successes: 0,
87
+ failures: 0,
88
+ blocked: 0,
89
+ totalDuration: 0
90
+ })
91
+ }
92
+
93
+ const metrics = this.activityMetrics.get(templateId)!
94
+ metrics.calls++
95
+
96
+ // Track start time
97
+ this.startTimes.set(eventId, Date.now())
98
+
99
+ // Check for cycle (before calling original handler)
100
+ const callStack = (config.activityCallStack ?? [])
101
+ if (callStack.includes(templateId)) {
102
+ const cycleChain = [...callStack, templateId].join(' → ')
103
+
104
+ const blockedEvent: CompositionEvent = {
105
+ timestamp: Date.now(),
106
+ parent,
107
+ child: templateId,
108
+ depth,
109
+ reason,
110
+ status: 'blocked',
111
+ error: `Cycle detected: ${cycleChain}`
112
+ }
113
+ this.recordEvent(blockedEvent)
114
+ metrics.blocked++
115
+
116
+ // Return blocked response
117
+ return {
118
+ id: `blocked_cycle_${Date.now()}`,
119
+ templateId,
120
+ status: "failed" as const,
121
+ error: `Cycle detected: ${cycleChain}`,
122
+ }
123
+ }
124
+
125
+ // Update call stack
126
+ this.callStack.push(templateId)
127
+
128
+ try {
129
+ // Call original handler or default implementation
130
+ const result = originalOnActivityExecute
131
+ ? await originalOnActivityExecute(templateId, variables, reason)
132
+ : { id: `result_${Date.now()}`, templateId, status: 'completed' as const }
133
+
134
+ // Calculate duration
135
+ const startTime = this.startTimes.get(eventId)
136
+ const duration = startTime ? Date.now() - startTime : 0
137
+ this.startTimes.delete(eventId)
138
+
139
+ // Update metrics
140
+ metrics.totalDuration += duration
141
+ if (result.status === 'completed') {
142
+ metrics.successes++
143
+ } else {
144
+ metrics.failures++
145
+ }
146
+
147
+ // Record completion event
148
+ const completeEvent: CompositionEvent = {
149
+ timestamp: Date.now(),
150
+ parent,
151
+ child: templateId,
152
+ depth,
153
+ reason,
154
+ status: result.status === 'completed' ? 'completed' : 'failed',
155
+ duration,
156
+ error: result.status === 'failed' ? result.error : undefined
157
+ }
158
+ this.recordEvent(completeEvent)
159
+
160
+ return result
161
+ } catch (error) {
162
+ // Calculate duration
163
+ const startTime = this.startTimes.get(eventId)
164
+ const duration = startTime ? Date.now() - startTime : 0
165
+ this.startTimes.delete(eventId)
166
+
167
+ // Update metrics
168
+ metrics.totalDuration += duration
169
+ metrics.failures++
170
+
171
+ // Record failure event
172
+ const failureEvent: CompositionEvent = {
173
+ timestamp: Date.now(),
174
+ parent,
175
+ child: templateId,
176
+ depth,
177
+ reason,
178
+ status: 'failed',
179
+ duration,
180
+ error: error instanceof Error ? error.message : String(error)
181
+ }
182
+ this.recordEvent(failureEvent)
183
+
184
+ throw error
185
+ } finally {
186
+ // Pop call stack
187
+ this.callStack.pop()
188
+ }
189
+ }
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Register callback for real-time event notifications
195
+ */
196
+ onEvent(callback: (event: CompositionEvent) => void): void {
197
+ this.onEventCallback = callback
198
+ }
199
+
200
+ /**
201
+ * Record composition event
202
+ */
203
+ private recordEvent(event: CompositionEvent): void {
204
+ this.events.push(event)
205
+ if (this.onEventCallback) {
206
+ this.onEventCallback(event)
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Get all recorded events
212
+ */
213
+ getEvents(): CompositionEvent[] {
214
+ return [...this.events]
215
+ }
216
+
217
+ /**
218
+ * Get current call stack
219
+ */
220
+ getCurrentCallStack(): string[] {
221
+ return [...this.callStack]
222
+ }
223
+
224
+ /**
225
+ * Get activity metrics
226
+ */
227
+ getActivityMetrics(): ActivityMetrics[] {
228
+ return Array.from(this.activityMetrics.entries()).map(([activityId, metrics]) => ({
229
+ activityId,
230
+ calls: metrics.calls,
231
+ successes: metrics.successes,
232
+ failures: metrics.failures,
233
+ blocked: metrics.blocked,
234
+ totalDuration: metrics.totalDuration,
235
+ avgDuration: metrics.calls > 0 ? Math.round(metrics.totalDuration / metrics.calls) : 0,
236
+ successRate: metrics.calls > 0 ? metrics.successes / metrics.calls : 0
237
+ })).sort((a, b) => b.calls - a.calls)
238
+ }
239
+
240
+ /**
241
+ * Build composition graph
242
+ */
243
+ getCompositionGraph(): CompositionGraph {
244
+ const nodes = new Map<string, { calls: number; depths: Set<number> }>()
245
+ const edges = new Map<string, { count: number; successes: number }>()
246
+
247
+ for (const event of this.events) {
248
+ if (event.status !== 'started') continue
249
+
250
+ // Track nodes
251
+ if (!nodes.has(event.child)) {
252
+ nodes.set(event.child, { calls: 0, depths: new Set() })
253
+ }
254
+ const node = nodes.get(event.child)!
255
+ node.calls++
256
+ node.depths.add(event.depth)
257
+
258
+ // Track edges
259
+ const edgeKey = `${event.parent} → ${event.child}`
260
+ if (!edges.has(edgeKey)) {
261
+ edges.set(edgeKey, { count: 0, successes: 0 })
262
+ }
263
+ const edge = edges.get(edgeKey)!
264
+ edge.count++
265
+
266
+ // Find corresponding completion event
267
+ const completionEvent = this.events.find(e =>
268
+ e.child === event.child &&
269
+ e.timestamp > event.timestamp &&
270
+ e.status === 'completed'
271
+ )
272
+ if (completionEvent) {
273
+ edge.successes++
274
+ }
275
+ }
276
+
277
+ return { nodes, edges }
278
+ }
279
+
280
+ /**
281
+ * Detect cycles in recorded events
282
+ */
283
+ detectCycles(): Array<{ cycle: string[]; occurrences: number }> {
284
+ const cycles = new Map<string, number>()
285
+
286
+ for (const event of this.events) {
287
+ if (event.status === 'blocked' && event.error?.includes('Cycle detected:')) {
288
+ const match = event.error.match(/Cycle detected: (.+)/)
289
+ if (match) {
290
+ const cycle = match[1].split(' → ')
291
+ const key = cycle.join('→')
292
+ cycles.set(key, (cycles.get(key) ?? 0) + 1)
293
+ }
294
+ }
295
+ }
296
+
297
+ return Array.from(cycles.entries())
298
+ .map(([cycle, occurrences]) => ({
299
+ cycle: cycle.split('→'),
300
+ occurrences
301
+ }))
302
+ .sort((a, b) => b.occurrences - a.occurrences)
303
+ }
304
+
305
+ /**
306
+ * Generate text report
307
+ */
308
+ generateReport(): string {
309
+ const lines: string[] = []
310
+
311
+ lines.push('═'.repeat(80))
312
+ lines.push('📊 COMPOSITION REPORT')
313
+ lines.push('═'.repeat(80))
314
+
315
+ // Summary
316
+ lines.push('')
317
+ lines.push('SUMMARY:')
318
+ lines.push(` Total Events: ${this.events.length}`)
319
+ lines.push(` Total Activities: ${this.activityMetrics.size}`)
320
+
321
+ const totalCalls = Array.from(this.activityMetrics.values()).reduce((sum, m) => sum + m.calls, 0)
322
+ const totalSuccesses = Array.from(this.activityMetrics.values()).reduce((sum, m) => sum + m.successes, 0)
323
+ const totalFailures = Array.from(this.activityMetrics.values()).reduce((sum, m) => sum + m.failures, 0)
324
+ const totalBlocked = Array.from(this.activityMetrics.values()).reduce((sum, m) => sum + m.blocked, 0)
325
+
326
+ lines.push(` Total Calls: ${totalCalls}`)
327
+ lines.push(` Successes: ${totalSuccesses} (${((totalSuccesses / totalCalls) * 100).toFixed(0)}%)`)
328
+ lines.push(` Failures: ${totalFailures} (${((totalFailures / totalCalls) * 100).toFixed(0)}%)`)
329
+ lines.push(` Blocked: ${totalBlocked} (${((totalBlocked / totalCalls) * 100).toFixed(0)}%)`)
330
+
331
+ // Activity Metrics
332
+ lines.push('')
333
+ lines.push('─'.repeat(80))
334
+ lines.push('ACTIVITY METRICS:')
335
+ lines.push('─'.repeat(80))
336
+
337
+ for (const metrics of this.getActivityMetrics()) {
338
+ lines.push('')
339
+ lines.push(`${metrics.activityId}:`)
340
+ lines.push(` Calls: ${metrics.calls}`)
341
+ lines.push(` Successes: ${metrics.successes} (${(metrics.successRate * 100).toFixed(0)}%)`)
342
+ lines.push(` Failures: ${metrics.failures}`)
343
+ if (metrics.blocked > 0) {
344
+ lines.push(` Blocked: ${metrics.blocked}`)
345
+ }
346
+ lines.push(` Avg Duration: ${metrics.avgDuration}ms`)
347
+ lines.push(` Total Duration: ${metrics.totalDuration}ms`)
348
+ }
349
+
350
+ // Composition Graph
351
+ lines.push('')
352
+ lines.push('─'.repeat(80))
353
+ lines.push('COMPOSITION GRAPH:')
354
+ lines.push('─'.repeat(80))
355
+
356
+ const graph = this.getCompositionGraph()
357
+
358
+ lines.push('')
359
+ lines.push('Nodes (Activities):')
360
+ for (const [activity, data] of graph.nodes.entries()) {
361
+ lines.push(` ${activity}`)
362
+ lines.push(` Calls: ${data.calls}`)
363
+ lines.push(` Depths: ${Array.from(data.depths).sort((a, b) => a - b).join(', ')}`)
364
+ }
365
+
366
+ lines.push('')
367
+ lines.push('Edges (Compositions):')
368
+ for (const [edge, data] of graph.edges.entries()) {
369
+ const successRate = data.count > 0 ? (data.successes / data.count * 100).toFixed(0) : '0'
370
+ lines.push(` ${edge}`)
371
+ lines.push(` Count: ${data.count}`)
372
+ lines.push(` Success Rate: ${successRate}%`)
373
+ }
374
+
375
+ // Cycles
376
+ const cycles = this.detectCycles()
377
+ if (cycles.length > 0) {
378
+ lines.push('')
379
+ lines.push('─'.repeat(80))
380
+ lines.push('⚠️ CYCLES DETECTED:')
381
+ lines.push('─'.repeat(80))
382
+
383
+ for (const { cycle, occurrences } of cycles) {
384
+ lines.push(` ${cycle.join(' → ')} (${occurrences}x)`)
385
+ }
386
+ }
387
+
388
+ lines.push('')
389
+ lines.push('═'.repeat(80))
390
+
391
+ return lines.join('\n')
392
+ }
393
+
394
+ /**
395
+ * Generate ASCII tree visualization
396
+ */
397
+ generateTree(): string {
398
+ const lines: string[] = []
399
+
400
+ // Group events by parent
401
+ const tree = new Map<string, string[]>()
402
+ for (const event of this.events) {
403
+ if (event.status === 'started') {
404
+ if (!tree.has(event.parent)) {
405
+ tree.set(event.parent, [])
406
+ }
407
+ tree.get(event.parent)!.push(event.child)
408
+ }
409
+ }
410
+
411
+ // Build tree starting from ROOT
412
+ const buildBranch = (node: string, prefix: string, isLast: boolean): void => {
413
+ const marker = isLast ? '└─' : '├─'
414
+ lines.push(`${prefix}${marker} ${node}`)
415
+
416
+ const children = tree.get(node) ?? []
417
+ const newPrefix = prefix + (isLast ? ' ' : '│ ')
418
+
419
+ children.forEach((child, index) => {
420
+ buildBranch(child, newPrefix, index === children.length - 1)
421
+ })
422
+ }
423
+
424
+ lines.push('ROOT')
425
+ const rootChildren = tree.get('ROOT') ?? []
426
+ rootChildren.forEach((child, index) => {
427
+ buildBranch(child, '', index === rootChildren.length - 1)
428
+ })
429
+
430
+ return lines.join('\n')
431
+ }
432
+
433
+ /**
434
+ * Clear all recorded data
435
+ */
436
+ clear(): void {
437
+ this.events = []
438
+ this.callStack = []
439
+ this.activityMetrics.clear()
440
+ this.startTimes.clear()
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Create a new composition observer
446
+ */
447
+ export function createCompositionObserver(): CompositionObserver {
448
+ return new CompositionObserver()
449
+ }
package/src/config.ts ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * minibob Configuration
3
+ *
4
+ * Loads configuration from environment variables and optional config file.
5
+ */
6
+
7
+ import type { MinibobConfig, VesselManifest } from "./types"
8
+ import type { RuntimeEnvironment } from "./environment"
9
+
10
+ // =============================================================================
11
+ // DEFAULT CONFIGURATION
12
+ // =============================================================================
13
+
14
+ const defaultConfig: MinibobConfig = {
15
+ port: 8080,
16
+ host: "0.0.0.0",
17
+ provider: "anthropic",
18
+ model: "claude-sonnet-4-20250514",
19
+ apiKey: "",
20
+ vessels: {
21
+ metabob: {
22
+ type: "http",
23
+ endpoint: process.env.MINIBOB_MCP_ENDPOINT || "http://api.minibob.local",
24
+ capabilities: ["activities", "impulses", "executions", "thompson-sampling"],
25
+ },
26
+ },
27
+ templatesDir: "./templates",
28
+ workingDirectory: process.cwd(),
29
+ autoCommit: false,
30
+ }
31
+
32
+ // =============================================================================
33
+ // CONFIGURATION LOADING
34
+ // =============================================================================
35
+
36
+ /**
37
+ * Safe wrapper for Bun.file() with better error handling
38
+ */
39
+ function safeReadFile(path: string): ReturnType<typeof Bun.file> {
40
+ if (!path || typeof path !== 'string') {
41
+ throw new Error(`Invalid file path: expected string, got ${typeof path}`)
42
+ }
43
+ try {
44
+ return Bun.file(path)
45
+ } catch (error) {
46
+ throw new Error(`Failed to create file handle for '${path}': ${error instanceof Error ? error.message : String(error)}`)
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Load configuration from environment and optional file
52
+ */
53
+ export async function loadConfig(configPath?: string): Promise<MinibobConfig> {
54
+ let fileConfig: Partial<MinibobConfig> = {}
55
+
56
+ // Try to load config file
57
+ if (configPath) {
58
+ const file = safeReadFile(configPath)
59
+ if (await file.exists()) {
60
+ fileConfig = await file.json()
61
+ }
62
+ } else {
63
+ // Try default locations
64
+ for (const path of ["minibob.json", ".minibob.json", "opencode.json"]) {
65
+ const file = safeReadFile(path)
66
+ if (await file.exists()) {
67
+ fileConfig = await file.json()
68
+ break
69
+ }
70
+ }
71
+ }
72
+
73
+ // Merge with environment variables
74
+ const config: MinibobConfig = {
75
+ ...defaultConfig,
76
+ ...fileConfig,
77
+ port: parseInt(process.env.MINIBOB_PORT ?? String(fileConfig.port ?? defaultConfig.port)),
78
+ host: process.env.MINIBOB_HOST ?? fileConfig.host ?? defaultConfig.host,
79
+ provider: (process.env.MINIBOB_PROVIDER ?? fileConfig.provider ?? defaultConfig.provider) as "anthropic" | "openai",
80
+ model: process.env.MINIBOB_MODEL ?? fileConfig.model ?? defaultConfig.model,
81
+ apiKey: process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY ?? fileConfig.apiKey ?? "",
82
+ workingDirectory: process.env.MINIBOB_WORKDIR ?? fileConfig.workingDirectory ?? process.cwd(),
83
+ templatesDir: process.env.MINIBOB_TEMPLATES ?? fileConfig.templatesDir ?? defaultConfig.templatesDir,
84
+ autoCommit: process.env.MINIBOB_AUTO_COMMIT === "true" || fileConfig.autoCommit || false,
85
+ }
86
+
87
+ // Validate required fields
88
+ if (!config.apiKey) {
89
+ console.warn("Warning: No API key configured. Set ANTHROPIC_API_KEY or OPENAI_API_KEY.")
90
+ }
91
+
92
+ return config
93
+ }
94
+
95
+ // =============================================================================
96
+ // VESSEL MANIFEST
97
+ // =============================================================================
98
+
99
+ /**
100
+ * Runtime context for dynamic manifest generation
101
+ */
102
+ export interface RuntimeContext {
103
+ environment: RuntimeEnvironment
104
+ clusterMode: boolean
105
+ peerCount: number
106
+ boredomEnabled: boolean
107
+ acpGossipEnabled: boolean
108
+ backendAvailable: boolean
109
+ }
110
+
111
+ /**
112
+ * Generate vessel manifest with dynamic capabilities based on runtime context
113
+ *
114
+ * Capabilities are determined by detected environment:
115
+ * - Base: activities, impulses, git (always enabled)
116
+ * - acp-gossip: Only in cluster mode (3+ pods)
117
+ * - boredom: Only in cluster mode with backend available
118
+ */
119
+ export function generateManifest(config: MinibobConfig, runtime?: RuntimeContext): VesselManifest {
120
+ const capabilities = ["activities", "impulses", "git"]
121
+
122
+ // Add dynamic capabilities based on runtime context
123
+ if (runtime?.acpGossipEnabled) {
124
+ capabilities.push("acp-gossip")
125
+ }
126
+ if (runtime?.boredomEnabled) {
127
+ capabilities.push("boredom")
128
+ }
129
+
130
+ // Always expose basic ACP endpoint for delegation
131
+ capabilities.push("acp")
132
+
133
+ return {
134
+ id: "minibob",
135
+ name: "minibob - Minimal Vessel",
136
+ version: "0.1.0",
137
+ capabilities,
138
+ tools: [
139
+ "bash",
140
+ "read",
141
+ "write",
142
+ "edit",
143
+ "glob",
144
+ "grep",
145
+ "list",
146
+ "git",
147
+ ],
148
+ acpEndpoint: `http://${config.host}:${config.port}/acp`,
149
+ metadata: runtime ? {
150
+ environment: runtime.environment,
151
+ clusterMode: runtime.clusterMode,
152
+ peerCount: runtime.peerCount,
153
+ backendAvailable: runtime.backendAvailable,
154
+ } : undefined
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Get configuration summary for logging
160
+ */
161
+ export function configSummary(config: MinibobConfig): string {
162
+ return `
163
+ minibob Configuration:
164
+ Port: ${config.port}
165
+ Provider: ${config.provider}
166
+ Model: ${config.model}
167
+ Working Directory: ${config.workingDirectory}
168
+ Templates: ${config.templatesDir}
169
+ Auto-Commit: ${config.autoCommit}
170
+ API Key: ${config.apiKey ? "***" + config.apiKey.slice(-4) : "(not set)"}
171
+ `.trim()
172
+ }