@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
package/index.ts ADDED
@@ -0,0 +1,1329 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * minibob - Minimal Vessel for the Process-of-Becoming
4
+ *
5
+ * Just describe what you want. MiniBob figures out how to do it.
6
+ *
7
+ * Primary usage:
8
+ * minibob goal "Fix the login bug"
9
+ * minibob goal "Add a logout button"
10
+ * minibob goal "Optimize slow queries"
11
+ *
12
+ * MiniBob automatically:
13
+ * 1. Searches for proven solutions (existing templates)
14
+ * 2. Adapts to your context (impulse interpolation)
15
+ * 3. Recovers from failures (adaptive retry & trailblazing)
16
+ * 4. Improvises when needed (creative exploration)
17
+ * 5. Learns which approaches work (Thompson Sampling)
18
+ *
19
+ * Advanced usage:
20
+ * minibob run <template> # Execute specific template
21
+ * minibob improvise <goal> # Pure improvisation
22
+ * minibob understand <path> # Codebase exploration
23
+ * minibob diagnose <problem> # Problem diagnosis
24
+ *
25
+ * For local development:
26
+ * bun run index.ts [command] # Run from source
27
+ */
28
+
29
+ import { loadConfig, generateManifest, configSummary, type RuntimeContext } from "./src/config"
30
+ import { ActivityExecutor, loadTemplate, loadTemplateFromMCPOrLocal, type ExecutorConfig } from "./src/activity"
31
+ import { handleACPRequest, type ACPServerConfig } from "./src/acp"
32
+ import { initializeMCP, type MCPClient } from "./src/mcp"
33
+ import { initializeBoredom, startBoredom, markBoredomActivity } from "./src/boredom"
34
+ import { validateRunActivityRequest, validateRequestSize, ValidationException } from "./src/validation"
35
+ import { MCPActivityBridge } from "./src/mcp-activity-bridge"
36
+ import { ActivityCompositionService } from "./activity_composition_service"
37
+ import { GoalProcessor } from "./src/goal-processor"
38
+ import { SearchFirstExecutor } from "./src/search-first-executor"
39
+ import { CodeExplorer, ApplicationAnalyzer } from "./src/understanding"
40
+
41
+ // CLI command imports
42
+ import { burrow } from "./src/cli/burrow"
43
+ import { observe } from "./src/cli/observe"
44
+ import { goal } from "./src/cli/goal"
45
+ import { doctor } from "./src/cli/doctor"
46
+ import { launchACPServer } from "./src/cli/acp-server"
47
+ // Idealization is handled within vessel-bootstrap, not as a separate CLI command
48
+ import { registerInstance, deregisterInstance, updateInstanceStatus, generateInstanceId } from "./src/cli/instance-registry"
49
+ import { vessel as vesselCommand } from "./src/cli/vessel"
50
+
51
+ // Vessel registry and runtime mapping
52
+ import {
53
+ registerVessel,
54
+ registerVesselWithSync,
55
+ recordActivityExecution,
56
+ fetchVesselFromBackend,
57
+ getVessel,
58
+ getVesselByPath,
59
+ listVessels,
60
+ updateVesselActivities,
61
+ addVesselActivity,
62
+ updateVesselMapping,
63
+ recordVesselComposition,
64
+ deregisterVessel,
65
+ vesselRegistry,
66
+ type Vessel,
67
+ type VesselIdentity,
68
+ type VesselActivity,
69
+ type VesselImpulse,
70
+ type VesselHook,
71
+ type VesselTool,
72
+ type VesselComposition,
73
+ type RuntimeMapping,
74
+ type SourceLocation,
75
+ type InferredIntent,
76
+ } from "./src/vessel-registry"
77
+
78
+ import {
79
+ analyzeCodebase as analyzeCodebaseMapping,
80
+ mapTraceToSource,
81
+ inferIntent,
82
+ runtimeMapping,
83
+ type TraceToSourceMapping,
84
+ type CodebaseAnalysis,
85
+ type FunctionInfo,
86
+ type ClassInfo,
87
+ type ModuleInfo,
88
+ type DocumentationInfo,
89
+ } from "./src/runtime-mapping"
90
+
91
+ // Re-export for library usage
92
+ export {
93
+ // Vessel registry
94
+ registerVessel,
95
+ getVessel,
96
+ getVesselByPath,
97
+ listVessels,
98
+ updateVesselActivities,
99
+ addVesselActivity,
100
+ updateVesselMapping,
101
+ recordVesselComposition,
102
+ deregisterVessel,
103
+ vesselRegistry,
104
+ // Runtime mapping
105
+ analyzeCodebaseMapping,
106
+ mapTraceToSource,
107
+ inferIntent,
108
+ runtimeMapping,
109
+ // CLI commands
110
+ burrow,
111
+ observe,
112
+ goal,
113
+ doctor,
114
+ launchACPServer,
115
+ vesselCommand,
116
+ }
117
+
118
+ // Re-export AgentRuntime for library usage
119
+ export { AgentRuntime } from "./src/agent-runtime"
120
+
121
+ // Re-export types
122
+ export type {
123
+ Vessel,
124
+ VesselIdentity,
125
+ VesselActivity,
126
+ VesselImpulse,
127
+ VesselHook,
128
+ VesselTool,
129
+ VesselComposition,
130
+ RuntimeMapping,
131
+ SourceLocation,
132
+ InferredIntent,
133
+ TraceToSourceMapping,
134
+ CodebaseAnalysis,
135
+ FunctionInfo,
136
+ ClassInfo,
137
+ ModuleInfo,
138
+ DocumentationInfo,
139
+ }
140
+
141
+ // =============================================================================
142
+ // CLI ARGUMENT PARSING
143
+ // =============================================================================
144
+
145
+ const args = process.argv.slice(2)
146
+ const command = args[0]
147
+
148
+ // =============================================================================
149
+ // HELP COMMAND
150
+ // =============================================================================
151
+
152
+ if (command === "--help" || command === "-h") {
153
+ console.log(`
154
+ minibob - Minimal Vessel for the Process-of-Becoming
155
+
156
+ Just describe what you want. MiniBob figures out how to do it.
157
+
158
+ USAGE:
159
+ minibob goal "<description>" # Recommended: Describe what you want
160
+ minibob [command] [options] # Advanced: Direct command access
161
+
162
+ PRIMARY COMMAND:
163
+ goal <description> Achieve a goal (recommended approach)
164
+ MiniBob will:
165
+ 1. Try routine activities (learned patterns)
166
+ 2. Recover from issues (adaptive retry)
167
+ 3. Improvise solutions (creative exploration)
168
+
169
+ Example: minibob goal "Fix the login bug"
170
+ Example: minibob goal "Add a logout button"
171
+
172
+ UTILITY COMMANDS:
173
+ doctor Health check and configuration
174
+ observe View running minibob instances
175
+ vessel <subcommand> Manage vessel registry (list, info, remove)
176
+ burrow [path] Inject minibob into a codebase
177
+ (no command) Start the HTTP/ACP server
178
+ acp Launch standalone ACP server
179
+
180
+ ADVANCED COMMANDS (Direct Execution):
181
+ run <template> Execute a specific activity template
182
+ Example: minibob run templates/fix-bug.json
183
+
184
+ improvise <goal> Pure improvisation (no template search)
185
+ Example: minibob improvise "Create HTTP server"
186
+
187
+ understand [path] Explore and analyze codebase structure
188
+ Example: minibob understand ./my-app
189
+
190
+ diagnose <problem> Diagnose application problems
191
+ Example: minibob diagnose "API returns 500"
192
+
193
+ --help Show this help message
194
+
195
+ HOW MINIBOB WORKS:
196
+ When you use 'goal', MiniBob automatically:
197
+ • Searches for proven solutions (existing templates)
198
+ • Adapts to your specific context (impulse interpolation)
199
+ • Recovers from failures (trailblazing new variants)
200
+ • Creates new templates from successful improvisations
201
+ • Learns which approaches work best (Thompson Sampling)
202
+
203
+ You don't choose the method - MiniBob does. Just describe the goal.
204
+
205
+ ENVIRONMENT VARIABLES:
206
+ ANTHROPIC_API_KEY API key for Claude (required)
207
+ OPENAI_API_KEY API key for OpenAI (if using OpenAI provider)
208
+ MINIBOB_MCP_ENDPOINT Backend API endpoint (default: http://localhost:8080)
209
+ MINIBOB_PORT Server port (default: 8080)
210
+ MINIBOB_HOST Server host (default: 0.0.0.0)
211
+ MINIBOB_PROVIDER LLM provider: anthropic or openai (default: anthropic)
212
+ MINIBOB_MODEL Model to use (default: claude-sonnet-4-20250514)
213
+ MINIBOB_WORKDIR Working directory (default: current directory)
214
+ MINIBOB_TEMPLATES Templates directory (default: ./templates)
215
+ MINIBOB_AUTO_COMMIT Auto-commit changes: true or false (default: false)
216
+
217
+ QUICK START:
218
+ # Install
219
+ npm install -g @metabob/minibob
220
+
221
+ # Set API key
222
+ export ANTHROPIC_API_KEY="sk-ant-..."
223
+
224
+ # Achieve a goal
225
+ minibob goal "Add error handling to the API"
226
+
227
+ # That's it! MiniBob handles the rest.
228
+
229
+ EXAMPLES:
230
+ # Goal-first (recommended)
231
+ minibob goal "Fix the broken test suite"
232
+ minibob goal "Add dark mode to the dashboard"
233
+ minibob goal "Optimize database queries"
234
+ minibob goal "Debug why the API is slow"
235
+
236
+ # Configuration
237
+ minibob doctor # Check setup
238
+ minibob observe --scan # View running instances
239
+
240
+ # Advanced (direct execution)
241
+ minibob run templates/fix-bug.json --var bug="login timeout"
242
+ minibob improvise "Create a countdown timer"
243
+ minibob understand ./my-project components
244
+ minibob diagnose "dashboard crashes on load"
245
+
246
+ DEVELOPMENT:
247
+ # Run from source (contributors)
248
+ bun run index.ts goal "your goal here"
249
+ `)
250
+ process.exit(0)
251
+ }
252
+
253
+ // =============================================================================
254
+ // RUN ACTIVITY COMMAND
255
+ // =============================================================================
256
+
257
+ if (command === "run") {
258
+ const templatePath = args[1]
259
+ if (!templatePath) {
260
+ console.error("Error: Template path required")
261
+ console.error("Usage: minibob run <template.json> [--var key=value...]")
262
+ process.exit(1)
263
+ }
264
+
265
+ // Parse variables from command line
266
+ const variables: Record<string, unknown> = {}
267
+ for (let i = 2; i < args.length; i++) {
268
+ const arg = args[i]
269
+ if (arg?.startsWith("--var")) {
270
+ const [, varArg] = arg.split("=", 1)
271
+ if (varArg) {
272
+ const eqIndex = varArg.indexOf("=")
273
+ if (eqIndex > 0) {
274
+ const key = varArg.slice(0, eqIndex)
275
+ const value = varArg.slice(eqIndex + 1)
276
+ variables[key] = value
277
+ }
278
+ }
279
+ } else if (arg && args[i - 1] === "--var") {
280
+ const eqIndex = arg.indexOf("=")
281
+ if (eqIndex > 0) {
282
+ const key = arg.slice(0, eqIndex)
283
+ const value = arg.slice(eqIndex + 1)
284
+ variables[key] = value
285
+ }
286
+ }
287
+ }
288
+
289
+ // Find reason argument
290
+ const reasonIndex = args.indexOf("--reason")
291
+ const reason = reasonIndex > -1 ? args[reasonIndex + 1] : undefined
292
+
293
+ // Run the activity
294
+ runActivity(templatePath, variables, reason)
295
+ .then((result) => {
296
+ console.log("\n=== Activity Result ===")
297
+ console.log(`Status: ${result.status}`)
298
+ console.log(`Duration: ${result.metrics?.duration}ms`)
299
+ console.log(`Tokens: ${result.metrics?.totalTokens.input} in / ${result.metrics?.totalTokens.output} out`)
300
+ console.log(`Cost: $${result.metrics?.cost.toFixed(4)}`)
301
+
302
+ if (result.status === "failed") {
303
+ const failedTask = result.taskResults.find((t) => t.status === "failed")
304
+ console.error(`\nFailed task: ${failedTask?.taskId}`)
305
+ console.error(`Error: ${failedTask?.error}`)
306
+ process.exit(1)
307
+ }
308
+ })
309
+ .catch((error) => {
310
+ console.error("Error running activity:", error)
311
+ process.exit(1)
312
+ })
313
+ }
314
+
315
+ // =============================================================================
316
+ // IMPROVISE COMMAND
317
+ // =============================================================================
318
+
319
+ else if (command === "improvise") {
320
+ const goal = args.slice(1).join(" ")
321
+ if (!goal) {
322
+ console.error("Error: Goal description required")
323
+ console.error('Usage: minibob improvise "your goal here"')
324
+ process.exit(1)
325
+ }
326
+
327
+ runImprovise(goal)
328
+ .then(() => {
329
+ process.exit(0)
330
+ })
331
+ .catch((error) => {
332
+ console.error("Error during improvisation:", error)
333
+ process.exit(1)
334
+ })
335
+ }
336
+
337
+ // =============================================================================
338
+ // UNDERSTAND COMMAND
339
+ // =============================================================================
340
+
341
+ else if (command === "understand") {
342
+ const rootPath = args[1] || process.cwd()
343
+ const focus = args[2] || "architecture"
344
+
345
+ runUnderstand(rootPath, focus)
346
+ .then(() => {
347
+ process.exit(0)
348
+ })
349
+ .catch((error) => {
350
+ console.error("Error understanding codebase:", error)
351
+ process.exit(1)
352
+ })
353
+ }
354
+
355
+ // =============================================================================
356
+ // DIAGNOSE COMMAND
357
+ // =============================================================================
358
+
359
+ else if (command === "diagnose") {
360
+ if (args.length < 2) {
361
+ console.error("Error: Problem description required")
362
+ console.error('Usage: minibob diagnose "description of problem"')
363
+ process.exit(1)
364
+ }
365
+
366
+ const problem = args.slice(1).join(" ")
367
+
368
+ runDiagnose(problem)
369
+ .then(() => {
370
+ process.exit(0)
371
+ })
372
+ .catch((error) => {
373
+ console.error("Error diagnosing problem:", error)
374
+ process.exit(1)
375
+ })
376
+ }
377
+
378
+ // =============================================================================
379
+ // GOAL COMMAND (NEW)
380
+ // =============================================================================
381
+
382
+ else if (command === "goal") {
383
+ goal(args.slice(1))
384
+ .then(() => {
385
+ process.exit(0)
386
+ })
387
+ .catch((error) => {
388
+ console.error("Error executing goal:", error)
389
+ process.exit(1)
390
+ })
391
+ }
392
+
393
+ // =============================================================================
394
+ // BURROW COMMAND (NEW)
395
+ // =============================================================================
396
+
397
+ else if (command === "burrow") {
398
+ burrow(args.slice(1))
399
+ .then(() => {
400
+ process.exit(0)
401
+ })
402
+ .catch((error) => {
403
+ console.error("Error during burrow:", error)
404
+ process.exit(1)
405
+ })
406
+ }
407
+
408
+ // =============================================================================
409
+ // OBSERVE COMMAND (NEW)
410
+ // =============================================================================
411
+
412
+ else if (command === "observe") {
413
+ observe(args.slice(1))
414
+ .then(() => {
415
+ process.exit(0)
416
+ })
417
+ .catch((error) => {
418
+ console.error("Error observing:", error)
419
+ process.exit(1)
420
+ })
421
+ }
422
+
423
+ // =============================================================================
424
+ // DOCTOR COMMAND (NEW)
425
+ // =============================================================================
426
+
427
+ else if (command === "doctor") {
428
+ doctor(args.slice(1))
429
+ .then(() => {
430
+ process.exit(0)
431
+ })
432
+ .catch((error) => {
433
+ console.error("Error running doctor:", error)
434
+ process.exit(1)
435
+ })
436
+ }
437
+
438
+ // =============================================================================
439
+ // ACP SERVER COMMAND (NEW)
440
+ // =============================================================================
441
+
442
+ else if (command === "acp") {
443
+ launchACPServer(args.slice(1))
444
+ .then(() => {
445
+ // ACP server runs until terminated
446
+ })
447
+ .catch((error) => {
448
+ console.error("Error starting ACP server:", error)
449
+ process.exit(1)
450
+ })
451
+ }
452
+
453
+ // =============================================================================
454
+ // VESSEL COMMAND (NEW)
455
+ // =============================================================================
456
+
457
+ else if (command === "vessel") {
458
+ vesselCommand(args.slice(1))
459
+ .then(() => {
460
+ process.exit(0)
461
+ })
462
+ .catch((error) => {
463
+ console.error("Error with vessel command:", error)
464
+ process.exit(1)
465
+ })
466
+ }
467
+
468
+ else {
469
+ // =============================================================================
470
+ // START SERVER (default)
471
+ // =============================================================================
472
+ startServer()
473
+ }
474
+
475
+ // =============================================================================
476
+ // ACTIVITY RUNNER
477
+ // =============================================================================
478
+
479
+ async function runActivity(
480
+ templatePath: string,
481
+ variables: Record<string, unknown>,
482
+ reason?: string
483
+ ) {
484
+ const config = await loadConfig()
485
+ console.log(configSummary(config))
486
+
487
+ // Initialize agent runtime with backend integration
488
+ const { AgentRuntime } = await import("./src/agent-runtime")
489
+ const runtime = await AgentRuntime.initialize({
490
+ config,
491
+ mode: "cli",
492
+ enableHeartbeat: false, // No heartbeat for one-shot CLI commands
493
+ })
494
+
495
+ console.log(`\nRunning activity: ${templatePath}`)
496
+ console.log(`Variables: ${JSON.stringify(variables)}`)
497
+ if (reason) console.log(`Reason: ${reason}`)
498
+
499
+ try {
500
+ // Load template from MCP or local
501
+ const template = await loadTemplateFromMCPOrLocal(templatePath)
502
+ const executorConfig: ExecutorConfig = {
503
+ provider: config.provider,
504
+ apiKey: config.apiKey,
505
+ model: config.model,
506
+ workingDirectory: config.workingDirectory,
507
+ // Wire MCP activity callbacks for autonomous trailblazing
508
+ onSearchActivities: MCPActivityBridge.isAvailable() ? MCPActivityBridge.searchActivities : undefined,
509
+ onCreateActivity: MCPActivityBridge.isAvailable() ? MCPActivityBridge.createActivity : undefined,
510
+ }
511
+
512
+ const executor = new ActivityExecutor(executorConfig)
513
+
514
+ // Track activity
515
+ runtime.setCurrentActivity({
516
+ id: template.id,
517
+ name: template.name,
518
+ startedAt: Date.now()
519
+ })
520
+
521
+ const result = await executor.execute({
522
+ template,
523
+ variables,
524
+ reason,
525
+ onTaskStart: (taskId) => {
526
+ console.log(`\n>>> Starting task: ${taskId}`)
527
+ runtime.setCurrentActivity({
528
+ id: template.id,
529
+ name: template.name,
530
+ task: taskId,
531
+ startedAt: Date.now()
532
+ })
533
+ },
534
+ onTaskComplete: (taskId, taskResult) => {
535
+ const status = taskResult.status === "completed" ? "✓" : "✗"
536
+ console.log(`${status} Completed task: ${taskId}`)
537
+ },
538
+ })
539
+
540
+ // Report execution to backend
541
+ await runtime.reportExecution(result)
542
+ runtime.clearCurrentActivity()
543
+
544
+ return result
545
+ } finally {
546
+ // Clean shutdown
547
+ await runtime.shutdown()
548
+ }
549
+ }
550
+
551
+ // =============================================================================
552
+ // UNDERSTAND COMMAND RUNNER
553
+ // =============================================================================
554
+
555
+ async function runUnderstand(rootPath: string, focus: string) {
556
+ const config = await loadConfig()
557
+ console.log(configSummary(config))
558
+
559
+ // Initialize MCP if configured (for potential backend features)
560
+ if (config.vessels.metabob) {
561
+ const mcpEndpoint = config.vessels.metabob.endpoint
562
+ console.log(`\nInitializing MCP client: ${mcpEndpoint}`)
563
+ await initializeMCP({ endpoint: mcpEndpoint, apiKey: config.apiKey }, true)
564
+ }
565
+
566
+ console.log(`\n=== Understanding Codebase ===`)
567
+ console.log(`Path: ${rootPath}`)
568
+ console.log(`Focus: ${focus}\n`)
569
+
570
+ // 1. Fast static exploration
571
+ const explorer = new CodeExplorer(config.workingDirectory)
572
+ console.log('Exploring codebase structure...')
573
+ const structure = await explorer.explore(rootPath)
574
+
575
+ console.log(`\n📊 Codebase Overview:`)
576
+ console.log(` Total Files: ${structure.totalFiles}`)
577
+ console.log(` Total Lines of Code: ${structure.totalLoc.toLocaleString()}`)
578
+ console.log(` Total Size: ${(structure.totalSize / 1024).toFixed(2)} KB`)
579
+ console.log(`\n📁 File Types:`)
580
+ for (const [ext, count] of Object.entries(structure.filesByType).sort((a, b) => b[1] - a[1]).slice(0, 10)) {
581
+ console.log(` .${ext}: ${count} files`)
582
+ }
583
+ console.log(`\n🔧 Frameworks:`)
584
+ if (structure.dependencies.frameworks.length > 0) {
585
+ for (const framework of structure.dependencies.frameworks) {
586
+ console.log(` - ${framework}`)
587
+ }
588
+ } else {
589
+ console.log(` (none detected)`)
590
+ }
591
+ console.log(`\n🚀 Entry Points:`)
592
+ for (const entry of structure.entryPoints) {
593
+ console.log(` - ${entry}`)
594
+ }
595
+
596
+ // 2. LLM-powered semantic analysis
597
+ console.log(`\n🤖 Performing semantic analysis with LLM...`)
598
+
599
+ // Use the target path as working directory for file reads
600
+ const analyzePath = rootPath.startsWith('/') ? rootPath : `${config.workingDirectory}/${rootPath}`
601
+
602
+ const executorConfig: ExecutorConfig = {
603
+ provider: config.provider,
604
+ apiKey: config.apiKey,
605
+ model: config.model,
606
+ workingDirectory: analyzePath,
607
+ }
608
+
609
+ const executor = new ActivityExecutor(executorConfig)
610
+ const analyzer = new ApplicationAnalyzer(executor, explorer)
611
+
612
+ const analysis = await analyzer.analyze({ rootPath, focus: focus as any, depth: 'medium' })
613
+
614
+ console.log(`\n📝 Analysis Results:\n`)
615
+ console.log(`Architecture Pattern: ${analysis.architecture.pattern}`)
616
+ console.log(`Description: ${analysis.architecture.description}`)
617
+
618
+ if (analysis.architecture.layers.length > 0) {
619
+ console.log(`\nArchitectural Layers:`)
620
+ for (const layer of analysis.architecture.layers) {
621
+ console.log(` - ${layer}`)
622
+ }
623
+ }
624
+
625
+ if (analysis.components.keyModules.length > 0) {
626
+ console.log(`\nKey Modules:`)
627
+ for (const module of analysis.components.keyModules) {
628
+ console.log(` - ${module.name}: ${module.purpose}`)
629
+ }
630
+ }
631
+
632
+ console.log(`\n✅ Understanding complete!\n`)
633
+ }
634
+
635
+ // =============================================================================
636
+ // DIAGNOSE COMMAND RUNNER
637
+ // =============================================================================
638
+
639
+ async function runDiagnose(problem: string) {
640
+ const config = await loadConfig()
641
+ console.log(configSummary(config))
642
+
643
+ // Initialize MCP if configured
644
+ if (config.vessels.metabob) {
645
+ const mcpEndpoint = config.vessels.metabob.endpoint
646
+ console.log(`\nInitializing MCP client: ${mcpEndpoint}`)
647
+ await initializeMCP({ endpoint: mcpEndpoint, apiKey: config.apiKey }, true)
648
+ }
649
+
650
+ console.log(`\n=== Diagnosing Problem ===`)
651
+ console.log(`Problem: ${problem}\n`)
652
+
653
+ const executorConfig: ExecutorConfig = {
654
+ provider: config.provider,
655
+ apiKey: config.apiKey,
656
+ model: config.model,
657
+ workingDirectory: config.workingDirectory,
658
+ }
659
+
660
+ const executor = new ActivityExecutor(executorConfig)
661
+ const explorer = new CodeExplorer(config.workingDirectory)
662
+ const analyzer = new ApplicationAnalyzer(executor, explorer)
663
+
664
+ console.log('🔍 Analyzing problem...')
665
+ const diagnosis = await analyzer.diagnose(problem)
666
+
667
+ console.log(`\n📊 Diagnosis Results:\n`)
668
+ console.log(`Confidence: ${diagnosis.confidence.toUpperCase()}`)
669
+ console.log(`\nRoot Cause:`)
670
+ console.log(` ${diagnosis.rootCause}`)
671
+
672
+ if (diagnosis.symptoms.length > 0) {
673
+ console.log(`\nSymptoms:`)
674
+ for (const symptom of diagnosis.symptoms) {
675
+ console.log(` - ${symptom}`)
676
+ }
677
+ }
678
+
679
+ if (diagnosis.affectedFiles.length > 0) {
680
+ console.log(`\nAffected Files:`)
681
+ for (const file of diagnosis.affectedFiles) {
682
+ console.log(` - ${file}`)
683
+ }
684
+ }
685
+
686
+ console.log(`\nRecommended Fix:`)
687
+ console.log(` ${diagnosis.recommendedFix}`)
688
+
689
+ if (diagnosis.validationSteps.length > 0) {
690
+ console.log(`\nValidation Steps:`)
691
+ for (const step of diagnosis.validationSteps) {
692
+ console.log(` - ${step}`)
693
+ }
694
+ }
695
+
696
+ console.log(`\n✅ Diagnosis complete!\n`)
697
+ }
698
+
699
+ // =============================================================================
700
+ // IMPROVISE COMMAND RUNNER
701
+ // =============================================================================
702
+
703
+ async function runImprovise(goal: string) {
704
+ const config = await loadConfig()
705
+ console.log(configSummary(config))
706
+
707
+ // Initialize agent runtime with backend integration
708
+ const { AgentRuntime } = await import("./src/agent-runtime")
709
+ const runtime = await AgentRuntime.initialize({
710
+ config,
711
+ mode: "cli",
712
+ enableHeartbeat: false,
713
+ })
714
+
715
+ console.log(`\n🎭 Improvising toward goal: ${goal}`)
716
+ console.log('Recording all steps...\n')
717
+
718
+ try {
719
+ const { GoalImproviser } = await import('./src/improviser')
720
+ const improviser = new GoalImproviser({
721
+ provider: config.provider,
722
+ apiKey: config.apiKey,
723
+ model: config.model,
724
+ workingDirectory: config.workingDirectory
725
+ })
726
+
727
+ // Track activity
728
+ runtime.setCurrentActivity({
729
+ id: `improvise-${Date.now()}`,
730
+ name: 'Pure Improvisation',
731
+ task: goal,
732
+ startedAt: Date.now()
733
+ })
734
+
735
+ const trace = await improviser.improvise(goal)
736
+ runtime.clearCurrentActivity()
737
+
738
+ // Display results
739
+ console.log(`\n${'='.repeat(60)}`)
740
+ console.log(`Goal: ${goal}`)
741
+ console.log(`Status: ${trace.outcome.goal_achieved ? '✅ Achieved' : '❌ Not achieved'}`)
742
+ console.log(`Steps: ${trace.steps.length}`)
743
+ console.log(`Duration: ${trace.outcome.total_duration_ms}ms`)
744
+ console.log(`Cost: $${trace.outcome.total_cost.toFixed(4)}`)
745
+ console.log(`${'='.repeat(60)}\n`)
746
+
747
+ // Show steps summary
748
+ console.log('Steps taken:')
749
+ trace.steps.forEach(step => {
750
+ const paramsPreview = JSON.stringify(step.params).substring(0, 50)
751
+ console.log(` ${step.step}. ${step.thought}`)
752
+ console.log(` → ${step.action}(${paramsPreview}${paramsPreview.length >= 50 ? '...' : ''})`)
753
+ console.log(` ${step.result.success ? '✓' : '✗'} ${step.result.success ? 'Success' : 'Failed'} (${step.duration_ms}ms)`)
754
+ })
755
+
756
+ // Extract template if successful
757
+ if (trace.outcome.goal_achieved) {
758
+ console.log('\n🧬 Extracting template from successful improvisation...')
759
+
760
+ const { extractTemplateFromImprovisation } = await import('./src/template-extractor')
761
+ const template = await extractTemplateFromImprovisation(trace)
762
+
763
+ console.log(`✅ Template created: ${template.id}`)
764
+ console.log(` Name: ${template.name}`)
765
+ console.log(` Tasks: ${template.tasks.length}`)
766
+
767
+ // Register with backend if available
768
+ const { getMCPClient, isMCPEnabled } = await import('./src/mcp')
769
+ if (isMCPEnabled()) {
770
+ try {
771
+ const mcp = getMCPClient()
772
+ // Store the template - assuming there's a method for this
773
+ // For now, just log that we would register it
774
+ console.log(` Would register with backend (not yet implemented)`)
775
+ } catch (error) {
776
+ console.log(` Backend registration skipped: ${error}`)
777
+ }
778
+ }
779
+
780
+ // TODO: Review and create variants
781
+ // This requires the template-reviewer module which we'll implement next
782
+ console.log('\n💡 Template extraction complete!')
783
+ console.log(' Next: Create variants through review (not yet implemented)')
784
+ } else {
785
+ console.log('\n❌ Goal not achieved - no template extracted')
786
+ if (trace.outcome.error) {
787
+ console.log(` Error: ${trace.outcome.error}`)
788
+ }
789
+ }
790
+
791
+ console.log(`\n✅ Improvisation complete!\n`)
792
+ } finally {
793
+ // Clean shutdown
794
+ await runtime.shutdown()
795
+ }
796
+ }
797
+
798
+ // =============================================================================
799
+ // HTTP SERVER
800
+ // =============================================================================
801
+
802
+ async function startServer() {
803
+ const config = await loadConfig()
804
+ console.log(configSummary(config))
805
+
806
+ // =============================================================================
807
+ // ENVIRONMENT AUTO-DETECTION
808
+ // =============================================================================
809
+
810
+ console.log("\n=== Environment Detection ===")
811
+ const { detectCompleteEnvironment } = await import("./src/environment")
812
+
813
+ const mcpEndpoint = config.vessels.metabob?.endpoint
814
+ const envInfo = await detectCompleteEnvironment(mcpEndpoint)
815
+
816
+ console.log(`Environment: ${envInfo.environment}`)
817
+ console.log(`Cluster Mode: ${envInfo.clusterMode} (${envInfo.peerCount} peer(s))`)
818
+ console.log(`Backend Available: ${envInfo.backendAvailable}`)
819
+ console.log("=============================\n")
820
+
821
+ // Build runtime context for dynamic manifest
822
+ const runtime: RuntimeContext = {
823
+ environment: envInfo.environment,
824
+ clusterMode: envInfo.clusterMode,
825
+ peerCount: envInfo.peerCount,
826
+ boredomEnabled: false,
827
+ acpGossipEnabled: false,
828
+ backendAvailable: envInfo.backendAvailable,
829
+ }
830
+
831
+ // =============================================================================
832
+ // CONDITIONAL MCP INITIALIZATION
833
+ // =============================================================================
834
+
835
+ let mcp: MCPClient | null = null
836
+ if (config.vessels.metabob && envInfo.backendAvailable) {
837
+ console.log(`Initializing MCP client: ${mcpEndpoint}`)
838
+ mcp = await initializeMCP({ endpoint: mcpEndpoint!, apiKey: config.apiKey }, false)
839
+
840
+ if (mcp) {
841
+ // Register this vessel with the backend
842
+ const tempManifest = generateManifest(config, runtime)
843
+ await mcp.registerVessel({
844
+ id: tempManifest.id,
845
+ name: tempManifest.name,
846
+ version: tempManifest.version,
847
+ capabilities: tempManifest.capabilities,
848
+ endpoint: tempManifest.acpEndpoint,
849
+ })
850
+ console.log("✓ Registered with backend")
851
+ }
852
+ } else if (config.vessels.metabob && !envInfo.backendAvailable) {
853
+ console.log("Backend unavailable, running in local mode")
854
+ }
855
+
856
+ // =============================================================================
857
+ // CONDITIONAL BOREDOM INITIALIZATION (Cluster Mode Only)
858
+ // =============================================================================
859
+
860
+ if (envInfo.clusterMode && mcp) {
861
+ console.log("\nInitializing boredom task executor (cluster mode)...")
862
+ const executorConfig: ExecutorConfig = {
863
+ provider: config.provider,
864
+ apiKey: config.apiKey,
865
+ model: config.model,
866
+ workingDirectory: config.workingDirectory,
867
+ }
868
+
869
+ initializeBoredom({
870
+ pollInterval: 30000, // Poll every 30 seconds
871
+ idleThreshold: 60000, // Consider idle after 60 seconds
872
+ // LLM config for goal-based execution via SearchFirstExecutor
873
+ llmConfig: {
874
+ provider: config.provider,
875
+ apiKey: config.apiKey,
876
+ model: config.model,
877
+ workingDirectory: config.workingDirectory,
878
+ },
879
+ // Template-based execution callback (legacy mode)
880
+ onExecuteTask: async (template, variables, reason) => {
881
+ // Create executor with MCP activity callbacks for boredom tasks
882
+ const boredomExecutorConfig = {
883
+ ...executorConfig,
884
+ onSearchActivities: MCPActivityBridge.isAvailable() ? MCPActivityBridge.searchActivities : undefined,
885
+ onCreateActivity: MCPActivityBridge.isAvailable() ? MCPActivityBridge.createActivity : undefined,
886
+ }
887
+ const executor = new ActivityExecutor(boredomExecutorConfig)
888
+ return executor.execute({ template, variables, reason })
889
+ },
890
+ })
891
+
892
+ // Start boredom executor with cluster mode flag
893
+ startBoredom(envInfo.clusterMode)
894
+ runtime.boredomEnabled = true
895
+ console.log("✓ Boredom task executor started")
896
+ } else if (!envInfo.clusterMode) {
897
+ console.log("Boredom disabled: Not in cluster mode")
898
+ } else if (!mcp) {
899
+ console.log("Boredom disabled: Backend unavailable")
900
+ }
901
+
902
+ // =============================================================================
903
+ // TODO: ACP GOSSIP (Cluster Mode Only)
904
+ // =============================================================================
905
+
906
+ if (envInfo.clusterMode) {
907
+ console.log("\n[TODO] ACP Gossip protocol would be initialized here")
908
+ console.log(" - DNS peer discovery")
909
+ console.log(" - Periodic health broadcasts")
910
+ runtime.acpGossipEnabled = true
911
+ }
912
+
913
+ // Generate final manifest with runtime capabilities
914
+ const manifest = generateManifest(config, runtime)
915
+ const acpConfig: ACPServerConfig = {
916
+ provider: config.provider,
917
+ apiKey: config.apiKey,
918
+ model: config.model,
919
+ workingDirectory: config.workingDirectory,
920
+ }
921
+
922
+ // =============================================================================
923
+ // INSTANCE STATE TRACKING (for observe command)
924
+ // =============================================================================
925
+ const instanceId = generateInstanceId()
926
+ const startTime = Date.now()
927
+ let activitiesExecuted = 0
928
+ let totalCost = 0
929
+ let currentActivity: {
930
+ id: string
931
+ name?: string
932
+ task?: string
933
+ startedAt: number
934
+ } | null = null
935
+
936
+ // Register instance
937
+ await registerInstance({
938
+ id: instanceId,
939
+ pid: process.pid,
940
+ port: config.port,
941
+ host: config.host,
942
+ startTime,
943
+ workingDirectory: config.workingDirectory,
944
+ status: 'idle',
945
+ capabilities: manifest.capabilities,
946
+ })
947
+ console.log(`✓ Registered instance: ${instanceId}`)
948
+
949
+ const server = Bun.serve({
950
+ port: config.port,
951
+ hostname: config.host,
952
+
953
+ fetch(request) {
954
+ const url = new URL(request.url)
955
+ const path = url.pathname
956
+
957
+ // Health check
958
+ if (path === "/health") {
959
+ return new Response(JSON.stringify({ status: "ok", vessel: "minibob" }), {
960
+ headers: { "Content-Type": "application/json" },
961
+ })
962
+ }
963
+
964
+ // Status endpoint (for observe command)
965
+ if (path === "/status") {
966
+ const status = currentActivity ? 'executing' : (runtime.boredomEnabled ? 'boredom' : 'idle')
967
+ return new Response(JSON.stringify({
968
+ id: instanceId,
969
+ status,
970
+ currentActivity: currentActivity ? {
971
+ id: currentActivity.id,
972
+ name: currentActivity.name,
973
+ task: currentActivity.task,
974
+ progress: 0.5, // TODO: Track actual progress
975
+ } : undefined,
976
+ metrics: {
977
+ activitiesExecuted,
978
+ uptime: Date.now() - startTime,
979
+ totalCost,
980
+ },
981
+ config: {
982
+ workingDirectory: config.workingDirectory,
983
+ model: config.model,
984
+ port: config.port,
985
+ },
986
+ }), {
987
+ headers: { "Content-Type": "application/json" },
988
+ })
989
+ }
990
+
991
+ // Vessel manifest/config
992
+ if (path === "/config" || path === "/manifest") {
993
+ return new Response(JSON.stringify(manifest), {
994
+ headers: { "Content-Type": "application/json" },
995
+ })
996
+ }
997
+
998
+ // ACP endpoint
999
+ if (path === "/acp" || path === "/acp/stream") {
1000
+ if (request.method === "POST") {
1001
+ return handleACPRequest(acpConfig, request)
1002
+ }
1003
+ // WebSocket upgrade would happen here
1004
+ return new Response("ACP endpoint - use POST or WebSocket", {
1005
+ status: 400,
1006
+ })
1007
+ }
1008
+
1009
+ // Run activity endpoint
1010
+ if (path === "/run" && request.method === "POST") {
1011
+ markBoredomActivity() // Mark activity to prevent boredom tasks during user requests
1012
+ return handleRunActivity(request, config)
1013
+ }
1014
+
1015
+ // List templates
1016
+ if (path === "/templates" && request.method === "GET") {
1017
+ return handleListTemplates(config.templatesDir)
1018
+ }
1019
+
1020
+ // Goal-seeking endpoint
1021
+ if (path === "/goal" && request.method === "POST") {
1022
+ markBoredomActivity() // Mark activity to prevent boredom tasks during user requests
1023
+ return handleGoal(request, config)
1024
+ }
1025
+
1026
+ // Search-first goal endpoint (experimental)
1027
+ if (path === "/goal/search-first" && request.method === "POST") {
1028
+ markBoredomActivity()
1029
+ return handleSearchFirstGoal(request, config)
1030
+ }
1031
+
1032
+ // 404
1033
+ return new Response(JSON.stringify({
1034
+ error: "Not found",
1035
+ endpoints: [
1036
+ "GET /health",
1037
+ "GET /status",
1038
+ "GET /config",
1039
+ "POST /acp",
1040
+ "POST /run",
1041
+ "POST /goal",
1042
+ "POST /goal/search-first",
1043
+ "GET /templates",
1044
+ ],
1045
+ }), {
1046
+ status: 404,
1047
+ headers: { "Content-Type": "application/json" },
1048
+ })
1049
+ },
1050
+ })
1051
+
1052
+ console.log(`\nminibob server running at http://${config.host}:${config.port}`)
1053
+ console.log(`\nEndpoints:`)
1054
+ console.log(` GET /health - Health check`)
1055
+ console.log(` GET /status - Instance status (for observe)`)
1056
+ console.log(` GET /config - Vessel manifest`)
1057
+ console.log(` POST /acp - ACP protocol handler`)
1058
+ console.log(` POST /run - Run activity template`)
1059
+ console.log(` POST /goal - Goal-seeking execution`)
1060
+ console.log(` GET /templates - List available templates`)
1061
+
1062
+ // =============================================================================
1063
+ // HEARTBEAT TO BACKEND (for centralized observation)
1064
+ // =============================================================================
1065
+ let heartbeatInterval: ReturnType<typeof setInterval> | null = null
1066
+
1067
+ const sendHeartbeat = async () => {
1068
+ if (!envInfo.backendAvailable) return
1069
+
1070
+ try {
1071
+ const heartbeatUrl = `${mcpEndpoint}/v2/vessels/heartbeat`
1072
+ const status = currentActivity ? 'executing' : (runtime.boredomEnabled ? 'bored' : 'idle')
1073
+
1074
+ await fetch(heartbeatUrl, {
1075
+ method: 'POST',
1076
+ headers: { 'Content-Type': 'application/json' },
1077
+ body: JSON.stringify({
1078
+ pod_name: instanceId,
1079
+ namespace: 'local', // Use 'local' for non-k8s environments
1080
+ status,
1081
+ current_activity: currentActivity ? {
1082
+ variant_id: currentActivity.id,
1083
+ activity_id: currentActivity.id,
1084
+ variant_name: currentActivity.name || currentActivity.id,
1085
+ started_at: new Date(currentActivity.startedAt).toISOString(),
1086
+ current_task: currentActivity.task,
1087
+ } : null,
1088
+ metrics: {
1089
+ executions_completed: activitiesExecuted,
1090
+ total_cost_usd: totalCost,
1091
+ uptime_seconds: Math.floor((Date.now() - startTime) / 1000),
1092
+ },
1093
+ }),
1094
+ signal: AbortSignal.timeout(5000),
1095
+ })
1096
+ } catch (error) {
1097
+ // Silently ignore heartbeat failures - backend might be temporarily unavailable
1098
+ }
1099
+ }
1100
+
1101
+ // Send initial heartbeat and start interval
1102
+ if (envInfo.backendAvailable) {
1103
+ sendHeartbeat()
1104
+ heartbeatInterval = setInterval(sendHeartbeat, 30000) // Every 30 seconds
1105
+ console.log("✓ Heartbeat reporting started (30s interval)")
1106
+ }
1107
+
1108
+ // RELIABILITY: Graceful shutdown on SIGTERM/SIGINT
1109
+ let isShuttingDown = false
1110
+ const gracefulShutdown = async (signal: string) => {
1111
+ if (isShuttingDown) return
1112
+ isShuttingDown = true
1113
+
1114
+ console.log(`\n\nReceived ${signal}, starting graceful shutdown...`)
1115
+
1116
+ // Stop heartbeat
1117
+ if (heartbeatInterval) {
1118
+ clearInterval(heartbeatInterval)
1119
+ }
1120
+
1121
+ // Deregister instance
1122
+ try {
1123
+ await deregisterInstance(instanceId)
1124
+ console.log("✓ Deregistered instance")
1125
+ } catch (error) {
1126
+ console.error("Error deregistering instance:", error)
1127
+ }
1128
+
1129
+ // Stop accepting new requests
1130
+ try {
1131
+ server.stop()
1132
+ console.log("✓ Stopped accepting new requests")
1133
+ } catch (error) {
1134
+ console.error("Error stopping server:", error)
1135
+ }
1136
+
1137
+ // Give in-flight requests time to complete (5 seconds)
1138
+ await new Promise((resolve) => setTimeout(resolve, 5000))
1139
+
1140
+ console.log("✓ Graceful shutdown complete")
1141
+ process.exit(0)
1142
+ }
1143
+
1144
+ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"))
1145
+ process.on("SIGINT", () => gracefulShutdown("SIGINT"))
1146
+ }
1147
+
1148
+ // =============================================================================
1149
+ // REQUEST HANDLERS
1150
+ // =============================================================================
1151
+
1152
+ async function handleRunActivity(request: Request, config: Awaited<ReturnType<typeof loadConfig>>) {
1153
+ try {
1154
+ // SECURITY: Validate request size to prevent DoS
1155
+ const contentLength = request.headers.get("Content-Length")
1156
+ validateRequestSize(contentLength, 10 * 1024 * 1024) // 10MB limit
1157
+
1158
+ const body = await request.json()
1159
+
1160
+ // SECURITY: Validate input schema
1161
+ const { template: templatePath, variables, reason } = validateRunActivityRequest(body)
1162
+
1163
+ const template = await loadTemplateFromMCPOrLocal(templatePath)
1164
+ const executorConfig: ExecutorConfig = {
1165
+ provider: config.provider,
1166
+ apiKey: config.apiKey,
1167
+ model: config.model,
1168
+ workingDirectory: config.workingDirectory,
1169
+ // Wire MCP activity callbacks for autonomous trailblazing
1170
+ onSearchActivities: MCPActivityBridge.isAvailable() ? MCPActivityBridge.searchActivities : undefined,
1171
+ onCreateActivity: MCPActivityBridge.isAvailable() ? MCPActivityBridge.createActivity : undefined,
1172
+ }
1173
+
1174
+ const executor = new ActivityExecutor(executorConfig)
1175
+ const result = await executor.execute({
1176
+ template,
1177
+ variables,
1178
+ reason,
1179
+ })
1180
+
1181
+ return new Response(JSON.stringify(result), {
1182
+ headers: { "Content-Type": "application/json" },
1183
+ })
1184
+ } catch (error) {
1185
+ // Return appropriate status code for validation errors
1186
+ const isValidationError = error instanceof ValidationException
1187
+ return new Response(JSON.stringify({
1188
+ error: error instanceof Error ? error.message : String(error),
1189
+ ...(isValidationError && { validationErrors: (error as ValidationException).errors }),
1190
+ }), {
1191
+ status: isValidationError ? 400 : 500,
1192
+ headers: { "Content-Type": "application/json" },
1193
+ })
1194
+ }
1195
+ }
1196
+
1197
+ async function handleGoal(request: Request, config: Awaited<ReturnType<typeof loadConfig>>) {
1198
+ try {
1199
+ // SECURITY: Validate request size to prevent DoS
1200
+ const contentLength = request.headers.get("Content-Length")
1201
+ validateRequestSize(contentLength, 10 * 1024 * 1024) // 10MB limit
1202
+
1203
+ const body = await request.json() as Record<string, unknown>
1204
+
1205
+ // Parse goal request
1206
+ const goal = body.goal as string
1207
+ const context = (body.context as Record<string, unknown>) || {}
1208
+ const impulseRefs = (body.impulseRefs as string[]) || []
1209
+ const maxActivities = (body.maxActivities as number) || 5
1210
+ const maxCost = (body.maxCost as number) || 10.0
1211
+
1212
+ if (!goal || typeof goal !== 'string') {
1213
+ throw new ValidationException([
1214
+ { field: 'goal', message: 'Must be a non-empty string' }
1215
+ ])
1216
+ }
1217
+
1218
+ console.log(`[Goal] Received: ${goal}`)
1219
+ console.log(`[Goal] Context:`, context)
1220
+ console.log(`[Goal] Impulse refs:`, impulseRefs)
1221
+ console.log(`[Goal] Max activities: ${maxActivities}, Max cost: $${maxCost}`)
1222
+
1223
+ // Create executor with MCP activity callbacks
1224
+ const executorConfig: ExecutorConfig = {
1225
+ provider: config.provider,
1226
+ apiKey: config.apiKey,
1227
+ model: config.model,
1228
+ workingDirectory: config.workingDirectory,
1229
+ onSearchActivities: MCPActivityBridge.isAvailable() ? MCPActivityBridge.searchActivities : undefined,
1230
+ onCreateActivity: MCPActivityBridge.isAvailable() ? MCPActivityBridge.createActivity : undefined,
1231
+ }
1232
+
1233
+ const executor = new ActivityExecutor(executorConfig)
1234
+ const goalProcessor = new GoalProcessor({
1235
+ workingDirectory: config.workingDirectory,
1236
+ executor,
1237
+ })
1238
+
1239
+ // Execute goal-seeking (GoalProcessor signature is just: message, context, options)
1240
+ const result = await goalProcessor.executeGoal(goal, context, {
1241
+ maxActivities,
1242
+ maxCost,
1243
+ })
1244
+
1245
+ return new Response(JSON.stringify(result), {
1246
+ headers: { "Content-Type": "application/json" },
1247
+ })
1248
+ } catch (error) {
1249
+ // Return appropriate status code for validation errors
1250
+ const isValidationError = error instanceof ValidationException
1251
+ return new Response(JSON.stringify({
1252
+ error: error instanceof Error ? error.message : String(error),
1253
+ ...(isValidationError && { validationErrors: (error as ValidationException).errors }),
1254
+ }), {
1255
+ status: isValidationError ? 400 : 500,
1256
+ headers: { "Content-Type": "application/json" },
1257
+ })
1258
+ }
1259
+ }
1260
+
1261
+ /**
1262
+ * Handle search-first goal execution
1263
+ * Uses step-by-step decomposition with activity reuse
1264
+ */
1265
+ async function handleSearchFirstGoal(request: Request, config: Awaited<ReturnType<typeof loadConfig>>) {
1266
+ try {
1267
+ const contentLength = request.headers.get("Content-Length")
1268
+ validateRequestSize(contentLength, 10 * 1024 * 1024)
1269
+
1270
+ const body = await request.json() as Record<string, unknown>
1271
+
1272
+ const goal = body.goal as string
1273
+ const context = (body.context as Record<string, unknown>) || {}
1274
+
1275
+ if (!goal || typeof goal !== 'string') {
1276
+ throw new ValidationException([
1277
+ { field: 'goal', message: 'Must be a non-empty string' }
1278
+ ])
1279
+ }
1280
+
1281
+ console.log(`[SearchFirst] Received goal: ${goal}`)
1282
+
1283
+ const executor = new SearchFirstExecutor({
1284
+ provider: config.provider,
1285
+ apiKey: config.apiKey,
1286
+ model: config.model,
1287
+ workingDirectory: config.workingDirectory,
1288
+ maxSteps: 5,
1289
+ maxTokensPerStep: 4096,
1290
+ })
1291
+
1292
+ const result = await executor.execute(goal, context)
1293
+
1294
+ return new Response(JSON.stringify(result), {
1295
+ headers: { "Content-Type": "application/json" },
1296
+ })
1297
+ } catch (error) {
1298
+ const isValidationError = error instanceof ValidationException
1299
+ return new Response(JSON.stringify({
1300
+ error: error instanceof Error ? error.message : String(error),
1301
+ ...(isValidationError && { validationErrors: (error as ValidationException).errors }),
1302
+ }), {
1303
+ status: isValidationError ? 400 : 500,
1304
+ headers: { "Content-Type": "application/json" },
1305
+ })
1306
+ }
1307
+ }
1308
+
1309
+ async function handleListTemplates(templatesDir: string) {
1310
+ try {
1311
+ const glob = new Bun.Glob("**/*.json")
1312
+ const templates: string[] = []
1313
+
1314
+ for await (const file of glob.scan({ cwd: templatesDir })) {
1315
+ templates.push(file)
1316
+ }
1317
+
1318
+ return new Response(JSON.stringify({ templates }), {
1319
+ headers: { "Content-Type": "application/json" },
1320
+ })
1321
+ } catch (error) {
1322
+ return new Response(JSON.stringify({
1323
+ templates: [],
1324
+ error: error instanceof Error ? error.message : String(error),
1325
+ }), {
1326
+ headers: { "Content-Type": "application/json" },
1327
+ })
1328
+ }
1329
+ }