@synergenius/flow-weaver-pack-weaver 0.9.7 → 0.9.9

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 (224) hide show
  1. package/dist/bot/agent-loop.d.ts +20 -0
  2. package/dist/bot/agent-loop.d.ts.map +1 -0
  3. package/dist/bot/agent-loop.js +331 -0
  4. package/dist/bot/agent-loop.js.map +1 -0
  5. package/dist/bot/agent-provider.d.ts.map +1 -1
  6. package/dist/bot/agent-provider.js +3 -2
  7. package/dist/bot/agent-provider.js.map +1 -1
  8. package/dist/bot/approvals.js +17 -8
  9. package/dist/bot/approvals.js.map +1 -1
  10. package/dist/bot/assistant-core.d.ts +17 -0
  11. package/dist/bot/assistant-core.d.ts.map +1 -1
  12. package/dist/bot/assistant-core.js +418 -60
  13. package/dist/bot/assistant-core.js.map +1 -1
  14. package/dist/bot/assistant-tools.d.ts +1 -1
  15. package/dist/bot/assistant-tools.d.ts.map +1 -1
  16. package/dist/bot/assistant-tools.js +283 -9
  17. package/dist/bot/assistant-tools.js.map +1 -1
  18. package/dist/bot/bot-agent-channel.d.ts.map +1 -1
  19. package/dist/bot/bot-agent-channel.js +2 -0
  20. package/dist/bot/bot-agent-channel.js.map +1 -1
  21. package/dist/bot/bot-manager.d.ts +4 -0
  22. package/dist/bot/bot-manager.d.ts.map +1 -1
  23. package/dist/bot/bot-manager.js +72 -27
  24. package/dist/bot/bot-manager.js.map +1 -1
  25. package/dist/bot/conversation-store.d.ts +6 -5
  26. package/dist/bot/conversation-store.d.ts.map +1 -1
  27. package/dist/bot/conversation-store.js +98 -42
  28. package/dist/bot/conversation-store.js.map +1 -1
  29. package/dist/bot/cost-store.d.ts +3 -0
  30. package/dist/bot/cost-store.d.ts.map +1 -1
  31. package/dist/bot/cost-store.js +21 -10
  32. package/dist/bot/cost-store.js.map +1 -1
  33. package/dist/bot/cost-tracker.d.ts.map +1 -1
  34. package/dist/bot/cost-tracker.js +14 -1
  35. package/dist/bot/cost-tracker.js.map +1 -1
  36. package/dist/bot/cron-parser.d.ts.map +1 -1
  37. package/dist/bot/cron-parser.js +2 -0
  38. package/dist/bot/cron-parser.js.map +1 -1
  39. package/dist/bot/cron-scheduler.d.ts.map +1 -1
  40. package/dist/bot/cron-scheduler.js +1 -0
  41. package/dist/bot/cron-scheduler.js.map +1 -1
  42. package/dist/bot/device-connection.d.ts +13 -0
  43. package/dist/bot/device-connection.d.ts.map +1 -0
  44. package/dist/bot/device-connection.js +102 -0
  45. package/dist/bot/device-connection.js.map +1 -0
  46. package/dist/bot/error-classifier.d.ts.map +1 -1
  47. package/dist/bot/error-classifier.js +5 -0
  48. package/dist/bot/error-classifier.js.map +1 -1
  49. package/dist/bot/file-lock.d.ts.map +1 -1
  50. package/dist/bot/file-lock.js +13 -3
  51. package/dist/bot/file-lock.js.map +1 -1
  52. package/dist/bot/file-watcher.d.ts.map +1 -1
  53. package/dist/bot/file-watcher.js +1 -0
  54. package/dist/bot/file-watcher.js.map +1 -1
  55. package/dist/bot/genesis-prompt-context.d.ts +5 -0
  56. package/dist/bot/genesis-prompt-context.d.ts.map +1 -1
  57. package/dist/bot/genesis-prompt-context.js +55 -0
  58. package/dist/bot/genesis-prompt-context.js.map +1 -1
  59. package/dist/bot/genesis-store.d.ts +4 -0
  60. package/dist/bot/genesis-store.d.ts.map +1 -1
  61. package/dist/bot/genesis-store.js +79 -12
  62. package/dist/bot/genesis-store.js.map +1 -1
  63. package/dist/bot/improve-loop.d.ts +46 -0
  64. package/dist/bot/improve-loop.d.ts.map +1 -0
  65. package/dist/bot/improve-loop.js +592 -0
  66. package/dist/bot/improve-loop.js.map +1 -0
  67. package/dist/bot/insight-engine.d.ts +12 -0
  68. package/dist/bot/insight-engine.d.ts.map +1 -0
  69. package/dist/bot/insight-engine.js +256 -0
  70. package/dist/bot/insight-engine.js.map +1 -0
  71. package/dist/bot/knowledge-store.d.ts.map +1 -1
  72. package/dist/bot/knowledge-store.js +4 -1
  73. package/dist/bot/knowledge-store.js.map +1 -1
  74. package/dist/bot/pipeline-runner.d.ts.map +1 -1
  75. package/dist/bot/pipeline-runner.js +12 -4
  76. package/dist/bot/pipeline-runner.js.map +1 -1
  77. package/dist/bot/project-model.d.ts +25 -0
  78. package/dist/bot/project-model.d.ts.map +1 -0
  79. package/dist/bot/project-model.js +372 -0
  80. package/dist/bot/project-model.js.map +1 -0
  81. package/dist/bot/response-formatter.js +2 -3
  82. package/dist/bot/response-formatter.js.map +1 -1
  83. package/dist/bot/run-store.d.ts.map +1 -1
  84. package/dist/bot/run-store.js +10 -2
  85. package/dist/bot/run-store.js.map +1 -1
  86. package/dist/bot/safe-path.d.ts +1 -1
  87. package/dist/bot/safe-path.d.ts.map +1 -1
  88. package/dist/bot/safe-path.js +20 -1
  89. package/dist/bot/safe-path.js.map +1 -1
  90. package/dist/bot/safety.d.ts +10 -2
  91. package/dist/bot/safety.d.ts.map +1 -1
  92. package/dist/bot/safety.js +45 -2
  93. package/dist/bot/safety.js.map +1 -1
  94. package/dist/bot/session-state.d.ts +4 -0
  95. package/dist/bot/session-state.d.ts.map +1 -1
  96. package/dist/bot/session-state.js +52 -9
  97. package/dist/bot/session-state.js.map +1 -1
  98. package/dist/bot/slash-commands.d.ts.map +1 -1
  99. package/dist/bot/slash-commands.js +109 -3
  100. package/dist/bot/slash-commands.js.map +1 -1
  101. package/dist/bot/steering-engine.d.ts +67 -0
  102. package/dist/bot/steering-engine.d.ts.map +1 -0
  103. package/dist/bot/steering-engine.js +198 -0
  104. package/dist/bot/steering-engine.js.map +1 -0
  105. package/dist/bot/step-executor.d.ts.map +1 -1
  106. package/dist/bot/step-executor.js +62 -25
  107. package/dist/bot/step-executor.js.map +1 -1
  108. package/dist/bot/system-prompt.d.ts.map +1 -1
  109. package/dist/bot/system-prompt.js +5 -2
  110. package/dist/bot/system-prompt.js.map +1 -1
  111. package/dist/bot/task-queue.d.ts +6 -1
  112. package/dist/bot/task-queue.d.ts.map +1 -1
  113. package/dist/bot/task-queue.js +43 -4
  114. package/dist/bot/task-queue.js.map +1 -1
  115. package/dist/bot/tool-registry.d.ts +1 -1
  116. package/dist/bot/tool-registry.d.ts.map +1 -1
  117. package/dist/bot/tool-registry.js +65 -4
  118. package/dist/bot/tool-registry.js.map +1 -1
  119. package/dist/bot/trust-calculator.d.ts +34 -0
  120. package/dist/bot/trust-calculator.d.ts.map +1 -0
  121. package/dist/bot/trust-calculator.js +67 -0
  122. package/dist/bot/trust-calculator.js.map +1 -0
  123. package/dist/bot/types.d.ts +97 -0
  124. package/dist/bot/types.d.ts.map +1 -1
  125. package/dist/bot/update-checker.d.ts +21 -0
  126. package/dist/bot/update-checker.d.ts.map +1 -0
  127. package/dist/bot/update-checker.js +129 -0
  128. package/dist/bot/update-checker.js.map +1 -0
  129. package/dist/bot/weaver-tools.d.ts.map +1 -1
  130. package/dist/bot/weaver-tools.js +11 -4
  131. package/dist/bot/weaver-tools.js.map +1 -1
  132. package/dist/cli-bridge.d.ts +2 -0
  133. package/dist/cli-bridge.d.ts.map +1 -1
  134. package/dist/cli-bridge.js +3 -1
  135. package/dist/cli-bridge.js.map +1 -1
  136. package/dist/cli-handlers.d.ts +10 -1
  137. package/dist/cli-handlers.d.ts.map +1 -1
  138. package/dist/cli-handlers.js +141 -24
  139. package/dist/cli-handlers.js.map +1 -1
  140. package/dist/cli.d.ts +3 -0
  141. package/dist/cli.d.ts.map +1 -0
  142. package/dist/cli.js +749 -0
  143. package/dist/cli.js.map +1 -0
  144. package/dist/docs/weaver-config.md +15 -9
  145. package/dist/handlers/on-execution-completed.d.ts +11 -0
  146. package/dist/handlers/on-execution-completed.d.ts.map +1 -0
  147. package/dist/handlers/on-execution-completed.js +25 -0
  148. package/dist/handlers/on-execution-completed.js.map +1 -0
  149. package/dist/mcp-tools.d.ts.map +1 -1
  150. package/dist/mcp-tools.js +33 -0
  151. package/dist/mcp-tools.js.map +1 -1
  152. package/dist/node-types/genesis-approve.d.ts.map +1 -1
  153. package/dist/node-types/genesis-approve.js +28 -3
  154. package/dist/node-types/genesis-approve.js.map +1 -1
  155. package/dist/node-types/genesis-observe.d.ts.map +1 -1
  156. package/dist/node-types/genesis-observe.js +23 -13
  157. package/dist/node-types/genesis-observe.js.map +1 -1
  158. package/dist/node-types/genesis-propose.d.ts.map +1 -1
  159. package/dist/node-types/genesis-propose.js +8 -0
  160. package/dist/node-types/genesis-propose.js.map +1 -1
  161. package/dist/node-types/genesis-update-history.d.ts.map +1 -1
  162. package/dist/node-types/genesis-update-history.js +13 -0
  163. package/dist/node-types/genesis-update-history.js.map +1 -1
  164. package/dist/templates/weaver-template.d.ts +11 -0
  165. package/dist/templates/weaver-template.d.ts.map +1 -0
  166. package/dist/templates/weaver-template.js +53 -0
  167. package/dist/templates/weaver-template.js.map +1 -0
  168. package/dist/workflows/weaver-bot-session.d.ts +65 -0
  169. package/dist/workflows/weaver-bot-session.d.ts.map +1 -0
  170. package/dist/workflows/weaver-bot-session.js +68 -0
  171. package/dist/workflows/weaver-bot-session.js.map +1 -0
  172. package/dist/workflows/weaver.d.ts +24 -0
  173. package/dist/workflows/weaver.d.ts.map +1 -0
  174. package/dist/workflows/weaver.js +28 -0
  175. package/dist/workflows/weaver.js.map +1 -0
  176. package/flowweaver.manifest.json +28 -1
  177. package/package.json +6 -3
  178. package/src/bot/agent-provider.ts +3 -2
  179. package/src/bot/approvals.ts +16 -8
  180. package/src/bot/assistant-core.ts +420 -63
  181. package/src/bot/assistant-tools.ts +291 -9
  182. package/src/bot/bot-agent-channel.ts +2 -0
  183. package/src/bot/bot-manager.ts +70 -29
  184. package/src/bot/conversation-store.ts +87 -42
  185. package/src/bot/cost-store.ts +20 -9
  186. package/src/bot/cost-tracker.ts +13 -1
  187. package/src/bot/cron-parser.ts +1 -0
  188. package/src/bot/cron-scheduler.ts +1 -0
  189. package/src/bot/device-connection.ts +102 -0
  190. package/src/bot/error-classifier.ts +5 -0
  191. package/src/bot/file-lock.ts +12 -2
  192. package/src/bot/file-watcher.ts +1 -0
  193. package/src/bot/genesis-prompt-context.ts +61 -0
  194. package/src/bot/genesis-store.ts +68 -16
  195. package/src/bot/improve-loop.ts +651 -0
  196. package/src/bot/insight-engine.ts +273 -0
  197. package/src/bot/knowledge-store.ts +4 -1
  198. package/src/bot/pipeline-runner.ts +11 -6
  199. package/src/bot/project-model.ts +404 -0
  200. package/src/bot/response-formatter.ts +2 -3
  201. package/src/bot/run-store.ts +5 -2
  202. package/src/bot/safe-path.ts +20 -1
  203. package/src/bot/safety.ts +57 -3
  204. package/src/bot/session-state.ts +47 -7
  205. package/src/bot/slash-commands.ts +111 -3
  206. package/src/bot/steering-engine.ts +233 -0
  207. package/src/bot/step-executor.ts +66 -26
  208. package/src/bot/system-prompt.ts +5 -2
  209. package/src/bot/task-queue.ts +40 -4
  210. package/src/bot/tool-registry.ts +67 -5
  211. package/src/bot/trust-calculator.ts +87 -0
  212. package/src/bot/types.ts +104 -0
  213. package/src/bot/update-checker.ts +138 -0
  214. package/src/bot/weaver-tools.ts +10 -4
  215. package/src/cli-bridge.ts +4 -1
  216. package/src/cli-handlers.ts +150 -29
  217. package/src/handlers/on-execution-completed.ts +30 -0
  218. package/src/mcp-tools.ts +38 -0
  219. package/src/node-types/genesis-approve.ts +28 -3
  220. package/src/node-types/genesis-observe.ts +23 -12
  221. package/src/node-types/genesis-propose.ts +8 -0
  222. package/src/node-types/genesis-update-history.ts +12 -0
  223. package/src/ui/evolution-panel.tsx +96 -0
  224. package/src/ui/insights-widget.tsx +77 -0
@@ -22,7 +22,7 @@ function getManager(): BotManager {
22
22
  return manager;
23
23
  }
24
24
 
25
- export function createAssistantExecutor(projectDir: string): ToolExecutor {
25
+ export function createAssistantExecutor(projectDir: string, steeringEngine?: import('./steering-engine.js').SteeringEngine): ToolExecutor {
26
26
  const mgr = getManager();
27
27
 
28
28
  return async (name: string, args: Record<string, unknown>) => {
@@ -38,7 +38,7 @@ export function createAssistantExecutor(projectDir: string): ToolExecutor {
38
38
  deadline: args.deadline as string | undefined,
39
39
  branch: args.branch as string | undefined,
40
40
  });
41
- return { result: JSON.stringify(bot), isError: false };
41
+ return { result: `Bot "${botName}" started (pid ${bot.pid}). Add tasks with queue_add or check status with bot_status.`, isError: false };
42
42
  }
43
43
  case 'bot_list': {
44
44
  const bots = mgr.list();
@@ -65,7 +65,7 @@ export function createAssistantExecutor(projectDir: string): ToolExecutor {
65
65
  if (failedTasks.length > 0) {
66
66
  result += `\nFailed tasks:\n`;
67
67
  for (const t of failedTasks) {
68
- result += ` - ${t.instruction.slice(0, 80)}\n`;
68
+ result += ` - ${t.instruction.slice(0, 120)}\n`;
69
69
  }
70
70
  }
71
71
  return { result, isError: false };
@@ -95,7 +95,7 @@ export function createAssistantExecutor(projectDir: string): ToolExecutor {
95
95
  targets: args.targets as string[] | undefined,
96
96
  priority: 0,
97
97
  });
98
- if (duplicate) return { result: `Task already queued (${id}).`, isError: false };
98
+ if (duplicate) return { result: `Skipped: similar task already exists (${id}).`, isError: false };
99
99
  return { result: `Added task ${id} to "${args.bot}" queue.`, isError: false };
100
100
  }
101
101
  case 'queue_add_batch': {
@@ -113,7 +113,7 @@ export function createAssistantExecutor(projectDir: string): ToolExecutor {
113
113
  const queue = mgr.getQueue(String(args.bot));
114
114
  const tasks = await queue.list();
115
115
  if (tasks.length === 0) return { result: 'Queue is empty.', isError: false };
116
- const lines = tasks.map(t => `[${t.status}] ${t.instruction.slice(0, 70)}`);
116
+ const lines = tasks.map(t => `[${t.status}] ${t.instruction.slice(0, 120)}`);
117
117
  return { result: lines.join('\n'), isError: false };
118
118
  }
119
119
  case 'queue_retry': {
@@ -172,8 +172,9 @@ export function createAssistantExecutor(projectDir: string): ToolExecutor {
172
172
  case 'run_shell': {
173
173
  const cmd = String(args.command);
174
174
  // Safety: block destructive commands
175
- if (isBlockedCommand(cmd)) {
176
- return { result: `Blocked: "${cmd}" is not allowed.`, isError: true };
175
+ const blockedPattern = isBlockedCommand(cmd);
176
+ if (blockedPattern) {
177
+ return { result: `Blocked: command matches disallowed pattern "${blockedPattern}".`, isError: true };
177
178
  }
178
179
  const output = execFileSync('sh', ['-c', cmd], {
179
180
  encoding: 'utf-8', cwd: projectDir, timeout: 30_000, stdio: ['pipe', 'pipe', 'pipe'],
@@ -181,6 +182,86 @@ export function createAssistantExecutor(projectDir: string): ToolExecutor {
181
182
  return { result: output.trim().slice(0, 5000) || '(no output)', isError: false };
182
183
  }
183
184
 
185
+ case 'write_file': {
186
+ const filePath = path.isAbsolute(String(args.file)) ? String(args.file) : path.resolve(projectDir, String(args.file));
187
+ // Safety: must be within project directory
188
+ if (!filePath.startsWith(projectDir)) {
189
+ steeringEngine?.recordEvent('file_write_blocked');
190
+ return { result: 'Blocked: cannot write files outside project directory.', isError: true };
191
+ }
192
+ const content = String(args.content);
193
+ if (!content.trim()) {
194
+ steeringEngine?.recordEvent('file_write_blocked');
195
+ return { result: 'Blocked: cannot write empty file.', isError: true };
196
+ }
197
+ // Check shrink protection for existing files
198
+ if (fs.existsSync(filePath)) {
199
+ const existing = fs.readFileSync(filePath, 'utf-8');
200
+ if (existing.length > 0 && content.length < existing.length * 0.5) {
201
+ steeringEngine?.recordEvent('file_write_blocked');
202
+ return { result: `Blocked: write would shrink file by more than 50% (${existing.length} -> ${content.length} chars).`, isError: true };
203
+ }
204
+ }
205
+ const dir = path.dirname(filePath);
206
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
207
+ fs.writeFileSync(filePath, content, 'utf-8');
208
+ steeringEngine?.recordEvent('file_write');
209
+ return { result: `Wrote ${content.length} chars to ${path.relative(projectDir, filePath)}`, isError: false };
210
+ }
211
+ case 'patch_file': {
212
+ const filePath = path.isAbsolute(String(args.file)) ? String(args.file) : path.resolve(projectDir, String(args.file));
213
+ if (!filePath.startsWith(projectDir)) {
214
+ return { result: 'Blocked: cannot patch files outside project directory.', isError: true };
215
+ }
216
+ if (!fs.existsSync(filePath)) {
217
+ return { result: `File not found: ${args.file}`, isError: true };
218
+ }
219
+ let content = fs.readFileSync(filePath, 'utf-8');
220
+ const patches = args.patches as Array<{ find: string; replace: string }>;
221
+ let applied = 0;
222
+ for (const p of patches) {
223
+ if (content.includes(p.find)) {
224
+ content = content.replace(p.find, p.replace);
225
+ applied++;
226
+ }
227
+ }
228
+ if (applied === 0) {
229
+ steeringEngine?.recordEvent('file_patch_blocked');
230
+ return { result: `No patches matched in ${args.file}. Check exact strings.`, isError: true };
231
+ }
232
+ fs.writeFileSync(filePath, content, 'utf-8');
233
+ steeringEngine?.recordEvent('file_patch');
234
+ return { result: `Applied ${applied}/${patches.length} patches to ${path.relative(projectDir, filePath)}`, isError: false };
235
+ }
236
+ case 'tsc_check': {
237
+ try {
238
+ const output = execFileSync('npx', ['tsc', '--noEmit'], {
239
+ encoding: 'utf-8', cwd: projectDir, timeout: 60_000, stdio: ['pipe', 'pipe', 'pipe'],
240
+ });
241
+ steeringEngine?.recordEvent('build_pass');
242
+ return { result: output.trim() || 'TypeScript check passed — no errors.', isError: false };
243
+ } catch (tscErr: any) {
244
+ steeringEngine?.recordEvent('build_fail');
245
+ const msg = tscErr instanceof Error ? tscErr.message : String(tscErr);
246
+ return { result: msg.length > 500 ? msg.slice(0, 497) + '...' : msg, isError: true };
247
+ }
248
+ }
249
+ case 'run_tests': {
250
+ const testArgs = ['vitest', 'run'];
251
+ if (args.pattern) testArgs.push(String(args.pattern));
252
+ try {
253
+ const output = execFileSync('npx', testArgs, {
254
+ encoding: 'utf-8', cwd: projectDir, timeout: 120_000, stdio: ['pipe', 'pipe', 'pipe'],
255
+ });
256
+ steeringEngine?.recordEvent('test_pass');
257
+ return { result: output.trim().slice(-3000) || 'Tests completed.', isError: false };
258
+ } catch (testErr: any) {
259
+ steeringEngine?.recordEvent('test_fail');
260
+ const msg = testErr instanceof Error ? testErr.message : String(testErr);
261
+ return { result: msg.length > 3000 ? msg.slice(-3000) : msg, isError: true };
262
+ }
263
+ }
264
+
184
265
  // Conversation management
185
266
  case 'conversation_list': {
186
267
  const { ConversationStore } = await import('./conversation-store.js');
@@ -200,7 +281,7 @@ export function createAssistantExecutor(projectDir: string): ToolExecutor {
200
281
  const cStore = new ConversationStore();
201
282
  const existing = cStore.get(String(args.id));
202
283
  if (!existing) return { result: `Conversation "${args.id}" not found.`, isError: true };
203
- cStore.delete(String(args.id));
284
+ await cStore.delete(String(args.id));
204
285
  return { result: `Deleted conversation "${args.id}" (${existing.title || 'untitled'}).`, isError: false };
205
286
  }
206
287
  case 'conversation_summary': {
@@ -307,12 +388,213 @@ export function createAssistantExecutor(projectDir: string): ToolExecutor {
307
388
  return { result: entries.map(e => `${e.key}: ${e.value}`).join('\n'), isError: false };
308
389
  }
309
390
 
391
+ // Overseer tools
392
+ case 'project_health': {
393
+ const { ProjectModelStore } = await import('./project-model.js');
394
+ const pms = new ProjectModelStore(projectDir);
395
+ const model = await pms.getOrBuild();
396
+ const lines: string[] = [
397
+ `Overall health: ${model.health.overall}/100`,
398
+ '',
399
+ ];
400
+ if (model.health.workflows.length > 0) {
401
+ lines.push('Workflows:');
402
+ for (const w of model.health.workflows) {
403
+ lines.push(` ${w.file}: ${w.score}/100 (${w.totalRuns} runs, ${Math.round(w.successRate * 100)}% success, trend: ${w.trend})`);
404
+ }
405
+ lines.push('');
406
+ }
407
+ if (model.bots.length > 0) {
408
+ lines.push('Bots:');
409
+ for (const b of model.bots) {
410
+ lines.push(` ${b.name}: ${b.ejected ? 'ejected' : 'pack'} (${b.totalTasksRun} tasks, ${Math.round(b.successRate * 100)}% success)`);
411
+ }
412
+ lines.push('');
413
+ }
414
+ if (model.failurePatterns.length > 0) {
415
+ lines.push('Failure patterns:');
416
+ for (const p of model.failurePatterns.slice(0, 5)) {
417
+ lines.push(` ${p.pattern} (${p.occurrences}x, ${p.transient ? 'transient' : 'persistent'})`);
418
+ }
419
+ lines.push('');
420
+ }
421
+ lines.push(`Cost (7d): $${model.cost.last7Days.toFixed(2)} (trend: ${model.cost.trend})`);
422
+ lines.push(`Trust: phase ${model.trust.phase} (score ${model.trust.score})`);
423
+ return { result: lines.join('\n'), isError: false };
424
+ }
425
+
426
+ case 'project_insights': {
427
+ const { ProjectModelStore } = await import('./project-model.js');
428
+ const { InsightEngine } = await import('./insight-engine.js');
429
+ const pms = new ProjectModelStore(projectDir);
430
+ const model = await pms.getOrBuild();
431
+ const engine = new InsightEngine();
432
+ const insights = engine.analyze(model);
433
+ const limit = (args.limit as number) ?? 5;
434
+ if (insights.length === 0) return { result: 'No actionable insights right now. Keep running workflows to build up data.', isError: false };
435
+ const lines = insights.slice(0, limit).map(i =>
436
+ `[${i.severity}] ${i.title} (confidence: ${Math.round(i.confidence * 100)}%)\n ${i.description}${i.suggestion ? `\n Suggestion: ${i.suggestion}` : ''}`
437
+ );
438
+ return { result: lines.join('\n\n'), isError: false };
439
+ }
440
+
441
+ case 'evolution_status': {
442
+ const { ProjectModelStore } = await import('./project-model.js');
443
+ const pms = new ProjectModelStore(projectDir);
444
+ const model = await pms.getOrBuild();
445
+ const evo = model.evolution;
446
+ if (evo.totalCycles === 0) return { result: 'No genesis cycles recorded yet. Use /genesis or genesis_propose to start evolving bot workflows.', isError: false };
447
+ const lines = [
448
+ `Total cycles: ${evo.totalCycles} (${Math.round(evo.successRate * 100)}% success rate)`,
449
+ '',
450
+ 'Operation effectiveness:',
451
+ ...Object.entries(evo.byOperationType).map(([op, stats]) =>
452
+ ` ${op}: ${Math.round(stats.effectiveness * 100)}% effective (${stats.applied}/${stats.proposed} applied)`
453
+ ),
454
+ '',
455
+ 'Recent cycles:',
456
+ ...evo.recentCycles.slice(-5).map(c =>
457
+ ` ${c.id} [${c.outcome}] ${c.proposal?.summary ?? 'no proposal'}`
458
+ ),
459
+ ];
460
+ return { result: lines.join('\n'), isError: false };
461
+ }
462
+
463
+ case 'improve_status': {
464
+ const summaryDir = path.join(os.homedir(), '.weaver', 'improve');
465
+ if (!fs.existsSync(summaryDir)) return { result: 'No improve runs found. Start one with: weaver improve', isError: false };
466
+ const files = fs.readdirSync(summaryDir).filter(f => f.endsWith('.json')).sort().reverse();
467
+ if (files.length === 0) return { result: 'No improve runs found.', isError: false };
468
+ const latest = JSON.parse(fs.readFileSync(path.join(summaryDir, files[0]!), 'utf-8'));
469
+ const duration = Math.round((new Date(latest.finishedAt).getTime() - new Date(latest.startedAt).getTime()) / 1000);
470
+ const lines: string[] = [
471
+ `Improve Run: ${latest.reason}`,
472
+ `Branch: ${latest.branch}`,
473
+ `Duration: ${duration}s`,
474
+ `Successes: ${latest.successes} Failures: ${latest.failures} Skips: ${latest.skips}`,
475
+ '',
476
+ ];
477
+ for (const cy of latest.cycles) {
478
+ const icon = cy.outcome === 'success' ? '✓' : cy.outcome === 'failure' ? '✗' : '○';
479
+ lines.push(`${icon} Cycle ${cy.cycle}: [${cy.outcome}] ${cy.description.slice(0, 70)}`);
480
+ if (cy.commitHash) lines.push(` Commit: ${cy.commitHash}`);
481
+ }
482
+ // Check if worktree is active (run in progress)
483
+ try {
484
+ const worktrees = execFileSync('git', ['worktree', 'list'], { encoding: 'utf-8', cwd: projectDir });
485
+ if (worktrees.includes('weaver-improve')) {
486
+ lines.push('', 'LIVE: improve worktree active — run in progress');
487
+ }
488
+ } catch { /* git not available */ }
489
+ return { result: lines.join('\n'), isError: false };
490
+ }
491
+
492
+ case 'genesis_propose': {
493
+ const { ProjectModelStore } = await import('./project-model.js');
494
+ const pms = new ProjectModelStore(projectDir);
495
+ const model = await pms.getOrBuild();
496
+
497
+ // Check if genesis config exists, create default if not
498
+ const genesisDir = path.join(projectDir, '.genesis');
499
+ const configPath = path.join(genesisDir, 'config.json');
500
+ if (!fs.existsSync(configPath)) {
501
+ fs.mkdirSync(genesisDir, { recursive: true });
502
+ const defaultConfig = {
503
+ intent: args.focus ? String(args.focus) : 'Improve bot workflow reliability and efficiency',
504
+ focus: args.focus ? [String(args.focus)] : [],
505
+ constraints: [],
506
+ approvalThreshold: 'MINOR',
507
+ budgetPerCycle: (args.budget as number) ?? 3,
508
+ stabilize: false,
509
+ targetWorkflow: '',
510
+ maxCyclesPerRun: 1,
511
+ };
512
+
513
+ // Find bot workflow — check ejected first, then pack
514
+ const botName = String(args.bot ?? 'weaver-bot');
515
+ const ejectedPath = path.join(projectDir, '.fw', 'bots', botName, 'weaver-bot.ts');
516
+ const packPath = path.resolve(projectDir, 'node_modules', '@synergenius', 'flow-weaver-pack-weaver', 'src', 'workflows', 'weaver-bot.ts');
517
+
518
+ if (fs.existsSync(ejectedPath)) {
519
+ defaultConfig.targetWorkflow = ejectedPath;
520
+ } else if (fs.existsSync(packPath)) {
521
+ // Auto-eject: copy from pack to project
522
+ const ejectDir = path.join(projectDir, '.fw', 'bots', botName);
523
+ fs.mkdirSync(ejectDir, { recursive: true });
524
+ fs.copyFileSync(packPath, path.join(ejectDir, 'weaver-bot.ts'));
525
+ defaultConfig.targetWorkflow = path.join(ejectDir, 'weaver-bot.ts');
526
+ // Also copy node types needed
527
+ } else {
528
+ return { result: `Could not find bot workflow for "${botName}". Ensure the weaver pack is installed.`, isError: true };
529
+ }
530
+
531
+ fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), 'utf-8');
532
+ }
533
+
534
+ // Build insight context
535
+ let insightSummary = '';
536
+ try {
537
+ const { getGenesisInsightContext } = await import('./genesis-prompt-context.js');
538
+ insightSummary = await getGenesisInsightContext(projectDir);
539
+ } catch { /* insights not available */ }
540
+
541
+ // Build proposal summary from model data
542
+ const lines: string[] = ['Genesis Proposal Context:', ''];
543
+ lines.push(`Project health: ${model.health.overall}/100`);
544
+ lines.push(`Trust phase: ${model.trust.phase}`);
545
+ if (model.failurePatterns.length > 0) {
546
+ lines.push('');
547
+ lines.push('Key failure patterns:');
548
+ for (const p of model.failurePatterns.slice(0, 3)) {
549
+ lines.push(` - ${p.pattern} (${p.occurrences}x, ${p.transient ? 'transient' : 'persistent'})`);
550
+ }
551
+ }
552
+ if (model.bots.length > 0) {
553
+ lines.push('');
554
+ lines.push('Bot performance:');
555
+ for (const b of model.bots) {
556
+ lines.push(` - ${b.name}: ${Math.round(b.successRate * 100)}% success (${b.totalTasksRun} tasks, ${b.ejected ? 'ejected' : 'pack'})`);
557
+ }
558
+ }
559
+ if (insightSummary) {
560
+ lines.push('');
561
+ lines.push(insightSummary);
562
+ }
563
+ lines.push('');
564
+ lines.push('To generate and apply a full proposal, run: flow-weaver weaver genesis');
565
+ lines.push('Or use genesis_apply after reviewing this context.');
566
+
567
+ return { result: lines.join('\n'), isError: false };
568
+ }
569
+
570
+ case 'genesis_apply': {
571
+ const proposalId = String(args.proposal_id);
572
+
573
+ // Run genesis via CLI
574
+ try {
575
+ const output = execFileSync('npx', ['flow-weaver', 'weaver', 'genesis', '--max-cycles', '1'], {
576
+ encoding: 'utf-8', cwd: projectDir, timeout: 120_000, stdio: ['pipe', 'pipe', 'pipe'],
577
+ });
578
+
579
+ // Invalidate project model cache
580
+ try {
581
+ const { ProjectModelStore } = await import('./project-model.js');
582
+ new ProjectModelStore(projectDir).invalidate();
583
+ } catch { /* non-fatal */ }
584
+
585
+ return { result: output.trim() || 'Genesis cycle completed.', isError: false };
586
+ } catch (err: any) {
587
+ return { result: `Genesis cycle failed: ${(err.message ?? '').slice(0, 500)}`, isError: true };
588
+ }
589
+ }
590
+
310
591
  default:
311
592
  return { result: `Unknown tool: ${name}`, isError: true };
312
593
  }
313
594
  } catch (err: unknown) {
595
+ steeringEngine?.recordEvent('tool_error');
314
596
  const msg = err instanceof Error ? err.message : String(err);
315
- return { result: msg.slice(0, 500), isError: true };
597
+ return { result: msg.length > 500 ? msg.slice(0, 497) + '...' : msg, isError: true };
316
598
  }
317
599
  };
318
600
  }
@@ -72,6 +72,7 @@ export class BotAgentChannel {
72
72
  if (this.provider.decideWithTools) {
73
73
  return this.provider.decideWithTools({ ...agentRequest, tools });
74
74
  }
75
+ console.warn('[BotAgentChannel] Provider does not support decideWithTools — falling back to decide (tool results will be dropped)');
75
76
  const result = await this.provider.decide(agentRequest);
76
77
  return { result };
77
78
  }
@@ -85,6 +86,7 @@ export class BotAgentChannel {
85
86
  yield* this.provider.stream(agentRequest);
86
87
  return;
87
88
  }
89
+ console.warn('[BotAgentChannel] Provider does not support stream — falling back to decide (response will be non-streaming)');
88
90
  const result = await this.provider.decide(agentRequest);
89
91
  yield { type: 'text', text: JSON.stringify(result) };
90
92
  yield { type: 'done' };
@@ -32,7 +32,7 @@ export interface SpawnOpts {
32
32
  const BOTS_DIR = path.join(os.homedir(), '.weaver', 'bots');
33
33
 
34
34
  export class BotManager {
35
- private bots = new Map<string, { meta: ManagedBot; process: ChildProcess }>();
35
+ private bots = new Map<string, { meta: ManagedBot; process: ChildProcess | null }>();
36
36
 
37
37
  constructor() {
38
38
  // Ensure base dir exists
@@ -42,21 +42,33 @@ export class BotManager {
42
42
  const cleanup = () => this.cleanup();
43
43
  process.on('exit', cleanup);
44
44
  process.on('SIGTERM', cleanup);
45
+ process.on('SIGINT', cleanup);
45
46
  }
46
47
 
47
48
  spawn(name: string, opts: SpawnOpts): ManagedBot {
48
49
  if (this.bots.has(name)) {
49
- throw new Error(`Bot "${name}" already exists. Stop it first or use a different name.`);
50
+ throw new Error(`Bot "${name}" already exists. Use bot_stop("${name}") first, or choose a different name.`);
50
51
  }
51
52
 
52
53
  const botDir = path.join(BOTS_DIR, name);
53
54
  fs.mkdirSync(botDir, { recursive: true });
54
55
 
55
56
  // Create git branch if specified (keeps main clean for overnight runs)
57
+ // WARNING: checkout -B mutates the user's working directory. Prefer git worktree
58
+ // for isolation when the project supports it.
56
59
  if (opts.branch) {
57
60
  try {
58
- execFileSync('git', ['checkout', '-B', opts.branch], { cwd: opts.projectDir, encoding: 'utf-8', stdio: 'pipe' });
59
- } catch { /* branch may already exist */ }
61
+ // Try worktree first for isolation (does not mutate the user's working dir)
62
+ const worktreePath = path.join(botDir, 'worktree');
63
+ execFileSync('git', ['worktree', 'add', '-B', opts.branch, worktreePath], { cwd: opts.projectDir, encoding: 'utf-8', stdio: 'pipe' });
64
+ opts.projectDir = worktreePath;
65
+ } catch {
66
+ // Worktree unavailable or failed — fall back to checkout but warn
67
+ process.stderr.write(`[weaver] WARNING: "git checkout -B ${opts.branch}" will switch the working directory in ${opts.projectDir}. Use git worktree for isolation.\n`);
68
+ try {
69
+ execFileSync('git', ['checkout', '-B', opts.branch], { cwd: opts.projectDir, encoding: 'utf-8', stdio: 'pipe' });
70
+ } catch { /* branch may already exist */ }
71
+ }
60
72
  }
61
73
 
62
74
  const logPath = path.join(botDir, 'output.log');
@@ -83,12 +95,19 @@ export class BotManager {
83
95
  WEAVER_STEERING_DIR: botDir,
84
96
  };
85
97
 
86
- const child = spawn(cmd, args, {
87
- cwd: opts.projectDir,
88
- env,
89
- stdio: ['ignore', 'pipe', 'pipe'],
90
- detached: false,
91
- });
98
+ let child: ChildProcess;
99
+ try {
100
+ child = spawn(cmd, args, {
101
+ cwd: opts.projectDir,
102
+ env,
103
+ stdio: ['ignore', 'pipe', 'pipe'],
104
+ detached: false,
105
+ });
106
+ } catch (err) {
107
+ // Clean up logStream on spawn failure to prevent fd leak
108
+ logStream.destroy();
109
+ throw err;
110
+ }
92
111
 
93
112
  // Capture output to log file
94
113
  child.stdout?.pipe(logStream);
@@ -135,7 +154,7 @@ export class BotManager {
135
154
  }
136
155
 
137
156
  get(name: string): ManagedBot | null {
138
- const bot = this.bots.get(name);
157
+ const bot = this.resolve(name);
139
158
  if (!bot) return null;
140
159
  // Health check on access
141
160
  if (bot.meta.status === 'running' && !this.isAlive(bot.meta)) {
@@ -154,13 +173,13 @@ export class BotManager {
154
173
  }
155
174
 
156
175
  getQueue(name: string): TaskQueue {
157
- const bot = this.bots.get(name);
176
+ const bot = this.resolve(name);
158
177
  if (!bot) throw new Error(`Bot "${name}" not found.`);
159
178
  return new TaskQueue(bot.meta.botDir);
160
179
  }
161
180
 
162
181
  getSteering(name: string): SteeringController {
163
- const bot = this.bots.get(name);
182
+ const bot = this.resolve(name);
164
183
  if (!bot) throw new Error(`Bot "${name}" not found.`);
165
184
  return new SteeringController(bot.meta.botDir);
166
185
  }
@@ -178,26 +197,30 @@ export class BotManager {
178
197
  }
179
198
 
180
199
  stop(name: string): void {
181
- const bot = this.bots.get(name);
200
+ const bot = this.resolve(name);
182
201
  if (!bot) throw new Error(`Bot "${name}" not found.`);
183
- // Send SIGTERM for graceful shutdown
184
- if (bot.process.pid && !bot.process.killed) {
185
- bot.process.kill('SIGTERM');
186
- }
202
+ this.sendSignal(bot, 'SIGTERM');
187
203
  bot.meta.status = 'stopped';
188
204
  }
189
205
 
190
206
  kill(name: string): void {
191
- const bot = this.bots.get(name);
207
+ const bot = this.resolve(name);
192
208
  if (!bot) throw new Error(`Bot "${name}" not found.`);
193
- if (bot.process.pid && !bot.process.killed) {
194
- bot.process.kill('SIGKILL');
195
- }
209
+ this.sendSignal(bot, 'SIGKILL');
196
210
  bot.meta.status = 'stopped';
197
211
  }
198
212
 
213
+ /** Send a signal to a bot, using the process handle if available, otherwise the PID from meta. */
214
+ private sendSignal(bot: { meta: ManagedBot; process: ChildProcess | null }, signal: NodeJS.Signals): void {
215
+ if (bot.process?.pid && !bot.process.killed) {
216
+ bot.process.kill(signal);
217
+ } else if (bot.meta.pid > 0) {
218
+ try { process.kill(bot.meta.pid, signal); } catch { /* process already gone */ }
219
+ }
220
+ }
221
+
199
222
  logs(name: string, lines = 50): string {
200
- const bot = this.bots.get(name);
223
+ const bot = this.resolve(name);
201
224
  if (!bot) throw new Error(`Bot "${name}" not found.`);
202
225
  const logPath = path.join(bot.meta.botDir, 'output.log');
203
226
  if (!fs.existsSync(logPath)) return '(no logs yet)';
@@ -208,7 +231,7 @@ export class BotManager {
208
231
 
209
232
  cleanup(): void {
210
233
  for (const [, bot] of this.bots) {
211
- if (bot.process.pid && !bot.process.killed) {
234
+ if (bot.process?.pid && !bot.process.killed) {
212
235
  try { bot.process.kill('SIGTERM'); } catch (err) {
213
236
  if (process.env.WEAVER_VERBOSE) process.stderr.write(`[weaver] SIGTERM failed for bot: ${err}\n`);
214
237
  }
@@ -216,6 +239,16 @@ export class BotManager {
216
239
  }
217
240
  }
218
241
 
242
+ /** Look up a bot by name, lazily discovering bots from disk if not in memory. */
243
+ private resolve(name: string): { meta: ManagedBot; process: ChildProcess | null } | undefined {
244
+ let bot = this.bots.get(name);
245
+ if (!bot) {
246
+ this.discoverExistingBots();
247
+ bot = this.bots.get(name);
248
+ }
249
+ return bot;
250
+ }
251
+
219
252
  /** Discover bots from disk that were spawned by a previous assistant session. */
220
253
  private discoverExistingBots(): void {
221
254
  if (!fs.existsSync(BOTS_DIR)) return;
@@ -235,7 +268,7 @@ export class BotManager {
235
268
  }
236
269
  }
237
270
  // Store without a process handle (can only steer via file, not kill directly)
238
- this.bots.set(name, { meta, process: null as unknown as ChildProcess });
271
+ this.bots.set(name, { meta, process: null });
239
272
  } catch { /* corrupt meta */ }
240
273
  }
241
274
  }
@@ -277,15 +310,23 @@ function wrapWithSleepInhibitor(command: string, args: string[]): { cmd: string;
277
310
  export function sendDesktopNotification(title: string, message: string): void {
278
311
  try {
279
312
  switch (process.platform) {
280
- case 'darwin':
281
- execFileSync('osascript', ['-e', `display notification "${message}" with title "${title}"`], { stdio: 'ignore' });
313
+ case 'darwin': {
314
+ // Escape backslashes and double-quotes for AppleScript string literals
315
+ const safeTitle = title.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
316
+ const safeMessage = message.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
317
+ execFileSync('osascript', ['-e', `display notification "${safeMessage}" with title "${safeTitle}"`], { stdio: 'ignore' });
282
318
  break;
319
+ }
283
320
  case 'linux':
284
321
  execFileSync('notify-send', [title, message], { stdio: 'ignore' });
285
322
  break;
286
- case 'win32':
287
- execFileSync('powershell', ['-Command', `[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('${message}','${title}')`], { stdio: 'ignore' });
323
+ case 'win32': {
324
+ // Escape single-quotes for PowerShell single-quoted strings (double them)
325
+ const psTitle = title.replace(/'/g, "''");
326
+ const psMessage = message.replace(/'/g, "''");
327
+ execFileSync('powershell', ['-Command', `[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('${psMessage}','${psTitle}')`], { stdio: 'ignore' });
288
328
  break;
329
+ }
289
330
  }
290
331
  } catch {
291
332
  // Non-fatal — notification is best-effort