@synergenius/flow-weaver-pack-weaver 0.9.62 → 0.9.78

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 (162) hide show
  1. package/dist/ai-chat-provider.d.ts +12 -0
  2. package/dist/ai-chat-provider.d.ts.map +1 -1
  3. package/dist/ai-chat-provider.js +173 -19
  4. package/dist/ai-chat-provider.js.map +1 -1
  5. package/dist/bot/agent-loop.d.ts +20 -0
  6. package/dist/bot/agent-loop.d.ts.map +1 -0
  7. package/dist/bot/agent-loop.js +331 -0
  8. package/dist/bot/agent-loop.js.map +1 -0
  9. package/dist/bot/ai-router.d.ts +19 -0
  10. package/dist/bot/ai-router.d.ts.map +1 -0
  11. package/dist/bot/ai-router.js +104 -0
  12. package/dist/bot/ai-router.js.map +1 -0
  13. package/dist/bot/bot-registry.js +2 -2
  14. package/dist/bot/bot-registry.js.map +1 -1
  15. package/dist/bot/conversation-store.d.ts +1 -0
  16. package/dist/bot/conversation-store.d.ts.map +1 -1
  17. package/dist/bot/conversation-store.js.map +1 -1
  18. package/dist/bot/improve-loop.js.map +1 -1
  19. package/dist/bot/instance-manager.d.ts +31 -0
  20. package/dist/bot/instance-manager.d.ts.map +1 -0
  21. package/dist/bot/instance-manager.js +115 -0
  22. package/dist/bot/instance-manager.js.map +1 -0
  23. package/dist/bot/orchestrator.d.ts +36 -0
  24. package/dist/bot/orchestrator.d.ts.map +1 -0
  25. package/dist/bot/orchestrator.js +176 -0
  26. package/dist/bot/orchestrator.js.map +1 -0
  27. package/dist/bot/profile-store.d.ts +36 -0
  28. package/dist/bot/profile-store.d.ts.map +1 -0
  29. package/dist/bot/profile-store.js +208 -0
  30. package/dist/bot/profile-store.js.map +1 -0
  31. package/dist/bot/profile-types.d.ts +126 -0
  32. package/dist/bot/profile-types.d.ts.map +1 -0
  33. package/dist/bot/profile-types.js +7 -0
  34. package/dist/bot/profile-types.js.map +1 -0
  35. package/dist/bot/session-state.d.ts +25 -0
  36. package/dist/bot/session-state.d.ts.map +1 -0
  37. package/dist/bot/session-state.js +110 -0
  38. package/dist/bot/session-state.js.map +1 -0
  39. package/dist/bot/swarm-controller.d.ts +37 -21
  40. package/dist/bot/swarm-controller.d.ts.map +1 -1
  41. package/dist/bot/swarm-controller.js +344 -163
  42. package/dist/bot/swarm-controller.js.map +1 -1
  43. package/dist/bot/task-prompt-builder.d.ts +2 -1
  44. package/dist/bot/task-prompt-builder.d.ts.map +1 -1
  45. package/dist/bot/task-prompt-builder.js +33 -10
  46. package/dist/bot/task-prompt-builder.js.map +1 -1
  47. package/dist/bot/task-queue.d.ts +46 -0
  48. package/dist/bot/task-queue.d.ts.map +1 -0
  49. package/dist/bot/task-queue.js +237 -0
  50. package/dist/bot/task-queue.js.map +1 -0
  51. package/dist/bot/task-store.d.ts +1 -6
  52. package/dist/bot/task-store.d.ts.map +1 -1
  53. package/dist/bot/task-store.js +27 -78
  54. package/dist/bot/task-store.js.map +1 -1
  55. package/dist/bot/task-types.d.ts +8 -4
  56. package/dist/bot/task-types.d.ts.map +1 -1
  57. package/dist/cli-handlers.d.ts.map +1 -1
  58. package/dist/cli-handlers.js +2 -3
  59. package/dist/cli-handlers.js.map +1 -1
  60. package/dist/cli.d.ts +3 -0
  61. package/dist/cli.d.ts.map +1 -0
  62. package/dist/cli.js +749 -0
  63. package/dist/cli.js.map +1 -0
  64. package/dist/docs/docs/weaver-bot-usage.md +35 -18
  65. package/dist/docs/docs/weaver-config.md +20 -0
  66. package/dist/docs/docs/weaver-task-queue.md +31 -19
  67. package/dist/docs/weaver-config.md +15 -9
  68. package/dist/mcp-tools.d.ts +17 -0
  69. package/dist/mcp-tools.d.ts.map +1 -1
  70. package/dist/mcp-tools.js +98 -232
  71. package/dist/mcp-tools.js.map +1 -1
  72. package/dist/node-types/orchestrator-dispatch.d.ts +17 -0
  73. package/dist/node-types/orchestrator-dispatch.d.ts.map +1 -0
  74. package/dist/node-types/orchestrator-dispatch.js +63 -0
  75. package/dist/node-types/orchestrator-dispatch.js.map +1 -0
  76. package/dist/node-types/orchestrator-load-state.d.ts +16 -0
  77. package/dist/node-types/orchestrator-load-state.d.ts.map +1 -0
  78. package/dist/node-types/orchestrator-load-state.js +60 -0
  79. package/dist/node-types/orchestrator-load-state.js.map +1 -0
  80. package/dist/node-types/orchestrator-route.d.ts +16 -0
  81. package/dist/node-types/orchestrator-route.d.ts.map +1 -0
  82. package/dist/node-types/orchestrator-route.js +28 -0
  83. package/dist/node-types/orchestrator-route.js.map +1 -0
  84. package/dist/node-types/receive-task.d.ts +2 -3
  85. package/dist/node-types/receive-task.d.ts.map +1 -1
  86. package/dist/node-types/receive-task.js +3 -28
  87. package/dist/node-types/receive-task.js.map +1 -1
  88. package/dist/templates/weaver-template.d.ts +11 -0
  89. package/dist/templates/weaver-template.d.ts.map +1 -0
  90. package/dist/templates/weaver-template.js +53 -0
  91. package/dist/templates/weaver-template.js.map +1 -0
  92. package/dist/ui/bot-constants.d.ts +14 -0
  93. package/dist/ui/bot-constants.d.ts.map +1 -0
  94. package/dist/ui/bot-constants.js +189 -0
  95. package/dist/ui/bot-constants.js.map +1 -0
  96. package/dist/ui/bot-panel.js +51 -90
  97. package/dist/ui/bot-slot-card.js +87 -122
  98. package/dist/ui/budget-bar.js +5 -3
  99. package/dist/ui/chat-task-result.js +4 -7
  100. package/dist/ui/decision-log.js +136 -0
  101. package/dist/ui/profile-card.js +158 -0
  102. package/dist/ui/profile-editor.js +597 -0
  103. package/dist/ui/swarm-controls.js +36 -27
  104. package/dist/ui/swarm-dashboard.js +2034 -736
  105. package/dist/ui/task-create-form.js +39 -116
  106. package/dist/ui/task-detail-view.js +490 -239
  107. package/dist/ui/task-pool-list.js +69 -94
  108. package/dist/workflows/orchestrator.d.ts +21 -0
  109. package/dist/workflows/orchestrator.d.ts.map +1 -0
  110. package/dist/workflows/orchestrator.js +281 -0
  111. package/dist/workflows/orchestrator.js.map +1 -0
  112. package/dist/workflows/weaver-bot-session.d.ts +65 -0
  113. package/dist/workflows/weaver-bot-session.d.ts.map +1 -0
  114. package/dist/workflows/weaver-bot-session.js +68 -0
  115. package/dist/workflows/weaver-bot-session.js.map +1 -0
  116. package/dist/workflows/weaver.d.ts +24 -0
  117. package/dist/workflows/weaver.d.ts.map +1 -0
  118. package/dist/workflows/weaver.js +28 -0
  119. package/dist/workflows/weaver.js.map +1 -0
  120. package/flowweaver.manifest.json +253 -66
  121. package/package.json +1 -1
  122. package/src/ai-chat-provider.ts +184 -18
  123. package/src/bot/ai-router.ts +132 -0
  124. package/src/bot/bot-registry.ts +2 -2
  125. package/src/bot/conversation-store.ts +2 -1
  126. package/src/bot/improve-loop.ts +6 -6
  127. package/src/bot/instance-manager.ts +128 -0
  128. package/src/bot/orchestrator.ts +244 -0
  129. package/src/bot/profile-store.ts +225 -0
  130. package/src/bot/profile-types.ts +141 -0
  131. package/src/bot/swarm-controller.ts +385 -186
  132. package/src/bot/task-prompt-builder.ts +37 -6
  133. package/src/bot/task-store.ts +28 -89
  134. package/src/bot/task-types.ts +10 -4
  135. package/src/cli-handlers.ts +2 -3
  136. package/src/docs/weaver-bot-usage.md +35 -18
  137. package/src/docs/weaver-config.md +20 -0
  138. package/src/docs/weaver-task-queue.md +31 -19
  139. package/src/mcp-tools.ts +129 -320
  140. package/src/node-types/orchestrator-dispatch.ts +71 -0
  141. package/src/node-types/orchestrator-load-state.ts +66 -0
  142. package/src/node-types/orchestrator-route.ts +33 -0
  143. package/src/node-types/receive-task.ts +3 -26
  144. package/src/ui/bot-constants.ts +192 -0
  145. package/src/ui/bot-panel.tsx +55 -79
  146. package/src/ui/bot-slot-card.tsx +69 -117
  147. package/src/ui/budget-bar.tsx +5 -3
  148. package/src/ui/chat-task-result.tsx +6 -9
  149. package/src/ui/decision-log.tsx +148 -0
  150. package/src/ui/profile-card.tsx +157 -0
  151. package/src/ui/profile-editor.tsx +384 -0
  152. package/src/ui/swarm-controls.tsx +35 -31
  153. package/src/ui/swarm-dashboard.tsx +409 -80
  154. package/src/ui/task-create-form.tsx +29 -119
  155. package/src/ui/task-detail-view.tsx +461 -215
  156. package/src/ui/task-pool-list.tsx +74 -95
  157. package/src/workflows/orchestrator.ts +302 -0
  158. package/dist/docs/weaver-bot-usage.md +0 -34
  159. package/dist/docs/weaver-genesis.md +0 -32
  160. package/dist/docs/weaver-task-queue.md +0 -34
  161. package/src/bot/error-guide.ts +0 -4
  162. package/src/bot/retry-utils.ts +0 -4
@@ -26,6 +26,7 @@ import { TaskStore } from './bot/task-store.js';
26
26
  import { SwarmController } from './bot/swarm-controller.js';
27
27
  import { SwarmEventLog } from './bot/swarm-event-log.js';
28
28
  import type { CreateTaskInput, TaskFilter } from './bot/task-types.js';
29
+ import type { CreateProfileInput } from './bot/profile-types.js';
29
30
 
30
31
  interface AiChatToolContext {
31
32
  workspacePath: string;
@@ -145,8 +146,8 @@ const toolHandlers: Record<
145
146
  const state = controller.getStatus();
146
147
  return JSON.stringify({
147
148
  swarmStatus: state.status,
148
- activeBots: Object.values(state.bots).filter(b => b.status === 'executing').length,
149
- totalBots: Object.keys(state.bots).length,
149
+ activeBots: Object.values(state.instances).filter(i => i.status === 'executing').length,
150
+ totalBots: Object.keys(state.instances).length,
150
151
  tasksCompleted: state.tasksCompleted,
151
152
  tasksFailed: state.tasksFailed,
152
153
  totalTokensUsed: state.totalTokensUsed,
@@ -318,7 +319,6 @@ const toolHandlers: Record<
318
319
  const input: CreateTaskInput = {
319
320
  title: args.title as string,
320
321
  description: (args.description as string) ?? '',
321
- assignedBots: args.assignedBots as string[] | undefined,
322
322
  priority: args.priority as number | undefined,
323
323
  parentId: args.parentId as string | undefined,
324
324
  dependsOn: args.dependsOn as string[] | undefined,
@@ -327,6 +327,8 @@ const toolHandlers: Record<
327
327
  timeoutMs: args.timeoutMs as number | undefined,
328
328
  maxAttempts: args.maxAttempts as number | undefined,
329
329
  createdBy: 'ai',
330
+ assignedProfile: args.assignedProfile as string | undefined,
331
+ complexity: args.complexity as CreateTaskInput['complexity'],
330
332
  subtasks: args.subtasks as CreateTaskInput['subtasks'],
331
333
  };
332
334
  const task = await store.create(input);
@@ -430,8 +432,153 @@ const toolHandlers: Record<
430
432
  const done = controller.getStatus().status === 'idle';
431
433
  return JSON.stringify({ events, done });
432
434
  },
435
+
436
+ // ---------------------------------------------------------------------------
437
+ // Profile CRUD tools
438
+ // ---------------------------------------------------------------------------
439
+
440
+ async fw_weaver_profile_list(_args: Record<string, unknown>, ctx: AiChatToolContext) {
441
+ const controller = SwarmController.getInstance(ctx.workspacePath);
442
+ const profileStore = controller.getProfileStore();
443
+ const instanceManager = controller.getInstanceManager();
444
+ const profiles = profileStore.list();
445
+ const enriched = profiles.map(p => ({
446
+ ...p,
447
+ activeInstances: instanceManager.listByProfile(p.id).filter(i => i.status === 'executing').length,
448
+ idleInstances: instanceManager.findIdle(p.id).length,
449
+ totalInstances: instanceManager.listByProfile(p.id).length,
450
+ }));
451
+ return JSON.stringify(enriched, null, 2);
452
+ },
453
+
454
+ async fw_weaver_profile_create(args: Record<string, unknown>, ctx: AiChatToolContext) {
455
+ const controller = SwarmController.getInstance(ctx.workspacePath);
456
+ const profileStore = controller.getProfileStore();
457
+ const input: CreateProfileInput = {
458
+ name: args.name as string,
459
+ botId: args.botId as string,
460
+ capabilities: args.capabilities as CreateProfileInput['capabilities'],
461
+ preferences: {
462
+ costStrategy: (args.costStrategy as 'frugal' | 'balanced' | 'performance') ?? 'balanced',
463
+ maxCostPerRun: args.maxCostPerRun as number | undefined,
464
+ maxCostPerTask: args.maxCostPerTask as number | undefined,
465
+ requireApproval: args.requireApproval as boolean | undefined,
466
+ instructions: args.instructions as string | undefined,
467
+ },
468
+ maxInstances: args.maxInstances as number | undefined,
469
+ description: args.description as string | undefined,
470
+ icon: args.icon as string | undefined,
471
+ color: args.color as string | undefined,
472
+ };
473
+ const profile = profileStore.create(input);
474
+ return JSON.stringify(profile, null, 2);
475
+ },
476
+
477
+ async fw_weaver_profile_update(args: Record<string, unknown>, ctx: AiChatToolContext) {
478
+ const controller = SwarmController.getInstance(ctx.workspacePath);
479
+ const profileStore = controller.getProfileStore();
480
+ const { id, ...rest } = args;
481
+
482
+ // Build a typed patch from the remaining args
483
+ const patch: Record<string, unknown> = {};
484
+ if (rest.name !== undefined) patch.name = rest.name;
485
+ if (rest.description !== undefined) patch.description = rest.description;
486
+ if (rest.icon !== undefined) patch.icon = rest.icon;
487
+ if (rest.color !== undefined) patch.color = rest.color;
488
+ if (rest.capabilities !== undefined) patch.capabilities = rest.capabilities;
489
+ if (rest.maxInstances !== undefined) patch.maxInstances = rest.maxInstances;
490
+ if (rest.minInstances !== undefined) patch.minInstances = rest.minInstances;
491
+ if (rest.costStrategy !== undefined || rest.maxCostPerRun !== undefined || rest.maxCostPerTask !== undefined || rest.requireApproval !== undefined || rest.instructions !== undefined) {
492
+ patch.preferences = {
493
+ ...(rest.costStrategy !== undefined && { costStrategy: rest.costStrategy }),
494
+ ...(rest.maxCostPerRun !== undefined && { maxCostPerRun: rest.maxCostPerRun }),
495
+ ...(rest.maxCostPerTask !== undefined && { maxCostPerTask: rest.maxCostPerTask }),
496
+ ...(rest.requireApproval !== undefined && { requireApproval: rest.requireApproval }),
497
+ ...(rest.instructions !== undefined && { instructions: rest.instructions }),
498
+ };
499
+ }
500
+
501
+ const profile = profileStore.update(id as string, patch);
502
+ return JSON.stringify(profile, null, 2);
503
+ },
504
+
505
+ async fw_weaver_profile_delete(args: Record<string, unknown>, ctx: AiChatToolContext) {
506
+ const controller = SwarmController.getInstance(ctx.workspacePath);
507
+ const profileStore = controller.getProfileStore();
508
+ const deleted = profileStore.delete(args.id as string);
509
+ return JSON.stringify({ deleted });
510
+ },
511
+
512
+ // ---------------------------------------------------------------------------
513
+ // Orchestrator tools
514
+ // ---------------------------------------------------------------------------
515
+
516
+ async fw_weaver_orchestrator_status(_args: Record<string, unknown>, ctx: AiChatToolContext) {
517
+ const controller = SwarmController.getInstance(ctx.workspacePath);
518
+ const { decisions, stats } = controller.getOrchestratorStatus();
519
+ const instanceManager = controller.getInstanceManager();
520
+ const allInstances = instanceManager.listAll();
521
+ return JSON.stringify({
522
+ recentDecisions: decisions.slice(-20),
523
+ routingStats: stats,
524
+ activeInstances: allInstances.filter(i => i.status === 'executing').length,
525
+ totalInstances: allInstances.length,
526
+ }, null, 2);
527
+ },
528
+
529
+ async fw_weaver_orchestrator_hint(args: Record<string, unknown>, ctx: AiChatToolContext) {
530
+ const hint = args.hint as string;
531
+ if (!hint) return JSON.stringify({ error: 'hint is required' });
532
+
533
+ const weaverDir = path.join(ctx.workspacePath, '.weaver');
534
+ fs.mkdirSync(weaverDir, { recursive: true });
535
+ const hintsPath = path.join(weaverDir, 'orchestrator-hints.json');
536
+
537
+ let hints: Array<{ hint: string; timestamp: number }> = [];
538
+ try {
539
+ if (fs.existsSync(hintsPath)) {
540
+ const raw = fs.readFileSync(hintsPath, 'utf-8');
541
+ const parsed = JSON.parse(raw);
542
+ if (Array.isArray(parsed)) hints = parsed;
543
+ }
544
+ } catch { /* corrupt file — start fresh */ }
545
+
546
+ hints.push({ hint, timestamp: Date.now() });
547
+ fs.writeFileSync(hintsPath, JSON.stringify(hints, null, 2), 'utf-8');
548
+ return JSON.stringify({ stored: true });
549
+ },
433
550
  };
434
551
 
552
+ // ---------------------------------------------------------------------------
553
+ // Shared handler — used by both ai-chat-provider and mcp-tools
554
+ // ---------------------------------------------------------------------------
555
+
556
+ /**
557
+ * Execute a weaver tool by name. Shared core for ai-chat-provider and MCP.
558
+ *
559
+ * @param toolName One of the fw_weaver_* tool names
560
+ * @param args Tool arguments (as parsed from the caller)
561
+ * @param projectDir Workspace / project directory
562
+ * @returns The JSON-serialised result string
563
+ */
564
+ export async function handleWeaverTool(
565
+ toolName: string,
566
+ args: Record<string, unknown>,
567
+ projectDir: string,
568
+ ): Promise<{ result: string; isError: boolean }> {
569
+ const handler = toolHandlers[toolName];
570
+ if (!handler) {
571
+ return { result: `Unknown weaver tool: ${toolName}`, isError: true };
572
+ }
573
+ try {
574
+ const ctx: AiChatToolContext = { workspacePath: projectDir, userId: 'mcp' };
575
+ const result = await handler(args, ctx);
576
+ return { result, isError: false };
577
+ } catch (err) {
578
+ return { result: err instanceof Error ? err.message : String(err), isError: true };
579
+ }
580
+ }
581
+
435
582
  // ---------------------------------------------------------------------------
436
583
  // Provider implementation
437
584
  // ---------------------------------------------------------------------------
@@ -442,16 +589,7 @@ export default {
442
589
  args: Record<string, unknown>,
443
590
  context: AiChatToolContext,
444
591
  ): Promise<AiChatToolResult> {
445
- const handler = toolHandlers[name];
446
- if (!handler) {
447
- return { result: `Unknown weaver tool: ${name}`, isError: true };
448
- }
449
- try {
450
- const result = await handler(args, context);
451
- return { result, isError: false };
452
- } catch (err) {
453
- return { result: err instanceof Error ? err.message : String(err), isError: true };
454
- }
592
+ return handleWeaverTool(name, args, context.workspacePath);
455
593
  },
456
594
 
457
595
  async getSystemPromptSections(
@@ -482,7 +620,7 @@ export default {
482
620
  content: `Beyond standard Flow Weaver tools, you have swarm-based task and bot management:
483
621
 
484
622
  **Task Management:**
485
- - \`fw_weaver_task_create\`: Create tasks (with optional subtasks via \`subtasks\` array, \`^prev\` shorthand for dependency chaining, \`assignedBots\` to target specific bots)
623
+ - \`fw_weaver_task_create\`: Create tasks (with optional subtasks via \`subtasks\` array, \`^prev\` shorthand for dependency chaining, \`assignedProfile\` to target specific profiles)
486
624
  - \`fw_weaver_task_list\`: List tasks (filter by status, parentId, botId)
487
625
  - \`fw_weaver_task_get\`: Get task details including subtasks and context
488
626
  - \`fw_weaver_task_update\`: Update task fields (title, description, priority, status)
@@ -512,7 +650,15 @@ export default {
512
650
  - \`fw_weaver_validate_bot\`: Validate a bot's file and export
513
651
  - \`fw_weaver_eject_bot\`: Mark a bot as ejected with a new file path
514
652
 
515
- **Workflow:** To delegate work, create tasks with \`fw_weaver_task_create\`, then start the swarm with \`fw_weaver_swarm_start\`. Bots will automatically claim and execute tasks. Use \`fw_weaver_steer\` with a \`botId\` to control individual bots.
653
+ **Profiles & Orchestrator:**
654
+ - \`fw_weaver_profile_list\`: List all bot profiles with capabilities, instances, and routing info
655
+ - \`fw_weaver_profile_create\`: Create a new bot profile with free-form capabilities (name + description pairs) and preferences (costStrategy, maxCostPerRun, maxCostPerTask, requireApproval, instructions)
656
+ - \`fw_weaver_profile_update\`: Update a bot profile's capabilities, preferences, or scaling config
657
+ - \`fw_weaver_profile_delete\`: Delete a bot profile
658
+ - \`fw_weaver_orchestrator_status\`: Get orchestrator routing status — recent decisions, stats, active instances
659
+ - \`fw_weaver_orchestrator_hint\`: Provide a routing hint to influence orchestrator decisions
660
+
661
+ **Workflow:** To delegate work, create tasks with \`fw_weaver_task_create\`, then start the swarm with \`fw_weaver_swarm_start\`. The orchestrator automatically routes tasks to the best available bot instance based on free-form capability matching. Use \`fw_weaver_steer\` with a \`botId\` to control individual bots. Use \`fw_weaver_profile_create\` to create specialized profiles with capabilities (name + description pairs) and preferences (costStrategy, maxCostPerRun, instructions), then route tasks via \`assignedProfile\`.
516
662
 
517
663
  Proactively offer these when relevant.`,
518
664
  priority: 15,
@@ -574,24 +720,44 @@ Proactively offer these when relevant.`,
574
720
  sections.push({
575
721
  id: 'registered-bots',
576
722
  title: 'Registered Bots',
577
- content: `The user has registered bot workflows available for swarm execution. To assign work to a bot, use \`fw_weaver_task_create\` with \`assignedBots: ["${bots[0]!.id}"]\` do NOT use the old \`fw_weaver_bot\` tool (it has been removed).\n\n${botLines}`,
723
+ content: `The user has registered bot workflows available for swarm execution. To assign work, use \`fw_weaver_task_create\` with \`assignedProfile\` to route tasks to the right profile.\n\n${botLines}`,
578
724
  priority: 25,
579
725
  });
580
726
  }
581
727
  } catch { /* non-fatal */ }
582
728
  }
583
729
 
730
+ // Bot profiles — capabilities, scaling, routing info
731
+ if (context.workspacePath) {
732
+ try {
733
+ const controller = SwarmController.getInstance(context.workspacePath);
734
+ const profileStore = controller.getProfileStore();
735
+ const profiles = profileStore.list();
736
+ if (profiles.length > 0) {
737
+ const profileLines = profiles.map(p =>
738
+ `- ${p.name} (${p.id}): capabilities=[${p.capabilities.map(c => c.name).join(', ')}], maxInstances=${p.maxInstances}, costStrategy=${p.preferences.costStrategy}`,
739
+ ).join('\n');
740
+ sections.push({
741
+ id: 'bot-profiles',
742
+ title: 'Bot Profiles',
743
+ content: `${profileLines}\n\nUse fw_weaver_profile_create to create new profiles. Use fw_weaver_task_create with assignedProfile to route tasks to the right profiles.`,
744
+ priority: 22,
745
+ });
746
+ }
747
+ } catch { /* non-fatal */ }
748
+ }
749
+
584
750
  // Swarm status — include if swarm is active
585
751
  if (context.workspacePath) {
586
752
  try {
587
753
  const controller = SwarmController.getInstance(context.workspacePath);
588
754
  const swarmState = controller.getStatus();
589
755
  if (swarmState.status !== 'idle') {
590
- const activeBots = Object.values(swarmState.bots).filter(b => b.status === 'executing').length;
756
+ const activeInstances = Object.values(swarmState.instances).filter(i => i.status === 'executing').length;
591
757
  sections.push({
592
758
  id: 'swarm-status',
593
759
  title: 'Swarm Status',
594
- content: `The swarm is currently **${swarmState.status}** with ${activeBots} active bot(s) out of ${Object.keys(swarmState.bots).length} total. Tasks completed: ${swarmState.tasksCompleted}, failed: ${swarmState.tasksFailed}. Total cost: $${swarmState.totalCost.toFixed(4)}, tokens: ${swarmState.totalTokensUsed}.`,
760
+ content: `The swarm is currently **${swarmState.status}** with ${activeInstances} active instance(s) out of ${Object.keys(swarmState.instances).length} total. Tasks completed: ${swarmState.tasksCompleted}, failed: ${swarmState.tasksFailed}. Total cost: $${swarmState.totalCost.toFixed(4)}, tokens: ${swarmState.totalTokensUsed}.`,
595
761
  priority: 12,
596
762
  });
597
763
  }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * AIRouterImpl — LLM-powered routing for ambiguous profile selection.
3
+ *
4
+ * When multiple bot profiles match a task, the orchestrator delegates
5
+ * to this router to pick the best one.
6
+ * Uses the project's configured AI provider via the callAI infrastructure.
7
+ */
8
+
9
+ import type { AIRouter, AIRouterResult } from './orchestrator.js';
10
+ import type { BotProfile, OrchestratorInput } from './profile-types.js';
11
+ import type { ProviderInfo } from './types.js';
12
+ import { callAI, parseJsonResponse } from './ai-client.js';
13
+ import { resolveProviderConfig, detectProvider } from './agent-provider.js';
14
+
15
+ type Task = OrchestratorInput['pendingTasks'][number];
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Provider resolution — map BotProviderConfig.name to ProviderInfo.type
19
+ // ---------------------------------------------------------------------------
20
+
21
+ const NAME_TO_TYPE: Record<string, ProviderInfo['type']> = {
22
+ anthropic: 'anthropic',
23
+ 'claude-cli': 'claude-cli',
24
+ 'copilot-cli': 'copilot-cli',
25
+ platform: 'platform',
26
+ };
27
+
28
+ function resolveProviderInfo(projectDir: string): Pick<ProviderInfo, 'type' | 'apiKey' | 'model' | 'maxTokens'> {
29
+ // Try reading .weaver.json from the project directory
30
+ let providerInput: string | { name: string; model?: string; maxTokens?: number } = 'auto';
31
+ try {
32
+ const fs = require('node:fs');
33
+ const path = require('node:path');
34
+ const configPath = path.join(projectDir, '.weaver.json');
35
+ if (fs.existsSync(configPath)) {
36
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
37
+ if (config.provider) providerInput = config.provider;
38
+ }
39
+ } catch { /* use auto detection */ }
40
+
41
+ const resolved = resolveProviderConfig(providerInput);
42
+ const type = NAME_TO_TYPE[resolved.name] ?? 'claude-cli';
43
+
44
+ return {
45
+ type,
46
+ model: resolved.model,
47
+ maxTokens: resolved.maxTokens,
48
+ apiKey: type === 'anthropic' ? process.env.ANTHROPIC_API_KEY : undefined,
49
+ };
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // AIRouterImpl
54
+ // ---------------------------------------------------------------------------
55
+
56
+ const SYSTEM_PROMPT =
57
+ 'You are a task routing orchestrator. Given a task and candidate bot profiles, ' +
58
+ 'select the best profile to handle the task. Respond with ONLY valid JSON: ' +
59
+ '{ "profileId": "...", "reason": "...", "confidence": 0.0-1.0 }';
60
+
61
+ export class AIRouterImpl implements AIRouter {
62
+ private projectDir: string;
63
+
64
+ constructor(projectDir: string) {
65
+ this.projectDir = projectDir;
66
+ }
67
+
68
+ async route(task: Task, candidates: BotProfile[]): Promise<AIRouterResult> {
69
+ const prompt = this._buildPrompt(task, candidates);
70
+ const providerInfo = resolveProviderInfo(this.projectDir);
71
+
72
+ // Use a low max-tokens since this is just a routing decision
73
+ const response = await callAI(providerInfo, SYSTEM_PROMPT, prompt, 256);
74
+
75
+ const result = parseJsonResponse(response) as {
76
+ profileId?: string;
77
+ reason?: string;
78
+ confidence?: number;
79
+ };
80
+
81
+ if (!result.profileId) {
82
+ throw new Error('AI router returned no profileId');
83
+ }
84
+
85
+ return {
86
+ profileId: result.profileId,
87
+ reason: result.reason || 'AI-selected',
88
+ confidence: typeof result.confidence === 'number' ? result.confidence : 0.5,
89
+ };
90
+ }
91
+
92
+ /** @internal — exposed for testing */
93
+ _buildPrompt(task: Task, candidates: BotProfile[]): string {
94
+ const lines = [
95
+ `Task: "${task.title}"`,
96
+ task.description ? `Description: ${task.description}` : '',
97
+ task.complexity ? `Complexity: ${task.complexity}` : '',
98
+ '',
99
+ 'Candidate profiles:',
100
+ ...candidates.map((p) => {
101
+ const parts = [`- ${p.id} (${p.name}):`];
102
+ if (p.capabilities.length > 0) {
103
+ parts.push(' Capabilities:');
104
+ for (const cap of p.capabilities) {
105
+ parts.push(` - ${cap.name}: ${cap.description}`);
106
+ }
107
+ }
108
+ parts.push(` Cost strategy: ${p.preferences.costStrategy}`);
109
+ if (p.preferences.instructions) {
110
+ parts.push(` Instructions: ${p.preferences.instructions}`);
111
+ }
112
+ parts.push(` maxInstances=${p.maxInstances}`);
113
+ return parts.join('\n');
114
+ }),
115
+ ];
116
+
117
+ // Add previous attempt info if available
118
+ const failures = task.context.runSummaries.filter((r) => r.outcome !== 'success');
119
+ if (failures.length > 0) {
120
+ lines.push(
121
+ '',
122
+ `Previous attempts: ${task.context.runSummaries.length} (${failures.length} failed)`,
123
+ );
124
+ for (const f of failures.slice(-3)) {
125
+ lines.push(` - ${f.botId}: ${f.outcome}`);
126
+ }
127
+ }
128
+
129
+ lines.push('', 'Select the best profile. Respond with JSON only.');
130
+ return lines.filter(Boolean).join('\n');
131
+ }
132
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Bot registry — manages .fw/bots.json in the user's workspace.
2
+ * Bot registry — manages .weaver/bots.json in the user's workspace.
3
3
  * Reads, writes, validates bot registrations.
4
4
  */
5
5
  import * as fs from 'node:fs';
@@ -19,7 +19,7 @@ export interface BotRegistration {
19
19
  ejected?: boolean;
20
20
  }
21
21
 
22
- const BOTS_FILE = '.fw/bots.json';
22
+ const BOTS_FILE = '.weaver/bots.json';
23
23
 
24
24
  export class BotRegistry {
25
25
  private _cache: BotRegistration[] | null = null;
@@ -26,6 +26,7 @@ export interface ConversationRecord {
26
26
  createdAt: number;
27
27
  lastMessageAt: number;
28
28
  botIds: string[];
29
+ cloudConversationId?: string;
29
30
  }
30
31
 
31
32
  interface StoredMessage {
@@ -221,7 +222,7 @@ export class ConversationStore {
221
222
  },
222
223
  body: JSON.stringify({
223
224
  message: `[Synced from CLI] ${message}`,
224
- conversationId: (conversation as any).cloudConversationId,
225
+ conversationId: conversation.cloudConversationId,
225
226
  }),
226
227
  }).catch(() => {}); // fire-and-forget
227
228
  } catch { /* sync not available */ }
@@ -526,9 +526,9 @@ function emptyResult(startedAt: string, branch: string, worktreePath: string, re
526
526
 
527
527
  // --- Helpers ---
528
528
 
529
- let cachedProvider: unknown = null;
530
- let cachedTools: unknown[] = [];
531
- let cachedExecutor: unknown = null;
529
+ let cachedProvider: import('@synergenius/flow-weaver/agent').AgentProvider | null = null;
530
+ let cachedTools: import('@synergenius/flow-weaver/agent').ToolDefinition[] = [];
531
+ let cachedExecutor: import('@synergenius/flow-weaver/agent').ToolExecutor | null = null;
532
532
 
533
533
  async function runAssistantInDir(worktreeDir: string, message: string, _conversationId: string, steeringEngine?: import('./steering-engine.js').SteeringEngine): Promise<string> {
534
534
  const originalCwd = process.cwd();
@@ -562,9 +562,9 @@ async function runAssistantInDir(worktreeDir: string, message: string, _conversa
562
562
  const toolCalls: Array<{ name: string; isError: boolean }> = [];
563
563
 
564
564
  const result = await runAgentLoop(
565
- cachedProvider as any,
566
- cachedTools as any,
567
- cachedExecutor as any,
565
+ cachedProvider!,
566
+ cachedTools,
567
+ cachedExecutor!,
568
568
  [{ role: 'user' as const, content: message }],
569
569
  {
570
570
  maxIterations: 20,
@@ -0,0 +1,128 @@
1
+ /**
2
+ * InstanceManager — manages in-memory bot instances (workers) per profile.
3
+ * Instances represent running workers that don't survive process restart.
4
+ */
5
+
6
+ import type { BotProfile, BotInstance } from './profile-types.js';
7
+
8
+ export class InstanceManager {
9
+ private instances = new Map<string, BotInstance>();
10
+
11
+ /** Spawn a new instance for the given profile. Throws if maxInstances reached. */
12
+ spawn(profile: BotProfile): BotInstance {
13
+ const existing = this.listByProfile(profile.id);
14
+ if (existing.length >= profile.maxInstances) {
15
+ throw new Error(
16
+ `Cannot spawn: profile "${profile.id}" already at max instances (${profile.maxInstances})`,
17
+ );
18
+ }
19
+
20
+ const usedIndices = new Set(existing.map((i) => i.index));
21
+ let index = 0;
22
+ while (usedIndices.has(index)) index++;
23
+
24
+ const instance: BotInstance = {
25
+ instanceId: `${profile.id}-${index}`,
26
+ profileId: profile.id,
27
+ index,
28
+ status: 'idle',
29
+ tokensUsed: 0,
30
+ cost: 0,
31
+ tasksCompleted: 0,
32
+ tasksFailed: 0,
33
+ };
34
+
35
+ this.instances.set(instance.instanceId, instance);
36
+ return { ...instance };
37
+ }
38
+
39
+ /** Get instance by ID (returns a copy), or null if not found. */
40
+ get(instanceId: string): BotInstance | null {
41
+ const inst = this.instances.get(instanceId);
42
+ return inst ? { ...inst } : null;
43
+ }
44
+
45
+ /** Return copies of all instances. */
46
+ listAll(): BotInstance[] {
47
+ return [...this.instances.values()].map((i) => ({ ...i }));
48
+ }
49
+
50
+ /** Return copies of instances belonging to a specific profile. */
51
+ listByProfile(profileId: string): BotInstance[] {
52
+ return [...this.instances.values()]
53
+ .filter((i) => i.profileId === profileId)
54
+ .map((i) => ({ ...i }));
55
+ }
56
+
57
+ /** Return idle instances for a given profile. */
58
+ findIdle(profileId: string): BotInstance[] {
59
+ return [...this.instances.values()]
60
+ .filter((i) => i.profileId === profileId && i.status === 'idle')
61
+ .map((i) => ({ ...i }));
62
+ }
63
+
64
+ /** Mark an instance as executing a specific task/run. */
65
+ markExecuting(instanceId: string, taskId: string, runId: string): void {
66
+ const inst = this.instances.get(instanceId);
67
+ if (!inst) throw new Error(`Instance not found: ${instanceId}`);
68
+ inst.status = 'executing';
69
+ inst.currentTaskId = taskId;
70
+ inst.currentRunId = runId;
71
+ inst.startedAt = new Date().toISOString();
72
+ }
73
+
74
+ /** Mark an instance as idle after task completion/failure. */
75
+ markIdle(instanceId: string, success: boolean): void {
76
+ const inst = this.instances.get(instanceId);
77
+ if (!inst) throw new Error(`Instance not found: ${instanceId}`);
78
+ inst.status = 'idle';
79
+ inst.currentTaskId = undefined;
80
+ inst.currentRunId = undefined;
81
+ inst.startedAt = undefined;
82
+ if (success) {
83
+ inst.tasksCompleted++;
84
+ } else {
85
+ inst.tasksFailed++;
86
+ }
87
+ }
88
+
89
+ /** Record token/cost usage for an instance. */
90
+ recordUsage(instanceId: string, tokens: number, cost: number): void {
91
+ const inst = this.instances.get(instanceId);
92
+ if (!inst) throw new Error(`Instance not found: ${instanceId}`);
93
+ inst.tokensUsed += tokens;
94
+ inst.cost += cost;
95
+ }
96
+
97
+ /** Stop and remove an instance. Returns true if it existed. */
98
+ stop(instanceId: string): boolean {
99
+ return this.instances.delete(instanceId);
100
+ }
101
+
102
+ /** Stop and remove all instances. */
103
+ stopAll(): void {
104
+ this.instances.clear();
105
+ }
106
+
107
+ /** Scale instances for a profile to the target count. Clamps to [0, maxInstances]. */
108
+ scaleTo(profile: BotProfile, target: number): void {
109
+ const clamped = Math.max(0, Math.min(target, profile.maxInstances));
110
+ const current = this.listByProfile(profile.id);
111
+
112
+ if (current.length < clamped) {
113
+ // Spawn more
114
+ const toSpawn = clamped - current.length;
115
+ for (let i = 0; i < toSpawn; i++) {
116
+ this.spawn(profile);
117
+ }
118
+ } else if (current.length > clamped) {
119
+ // Stop idle instances first
120
+ const idle = current.filter((i) => i.status === 'idle');
121
+ const toStop = current.length - clamped;
122
+ const stopped = idle.slice(0, toStop);
123
+ for (const inst of stopped) {
124
+ this.stop(inst.instanceId);
125
+ }
126
+ }
127
+ }
128
+ }