@polderlabs/bizar 6.3.0 → 7.0.0

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 (245) hide show
  1. package/.claude/hooks/sessionend-recall.mjs +3 -3
  2. package/.claude/hooks/worker-suggest.mjs +110 -0
  3. package/.claude/settings.json +9 -0
  4. package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js → EnvVarsSection-CW6fWKds.js} +3 -3
  5. package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js.map → EnvVarsSection-CW6fWKds.js.map} +1 -1
  6. package/bizar-dash/dist/assets/MobileChat-D7zRhU9K.js +1 -0
  7. package/bizar-dash/dist/assets/{MobileChat-BJrqwVDd.js.map → MobileChat-D7zRhU9K.js.map} +1 -1
  8. package/bizar-dash/dist/assets/MobileSettings-K-sNsn_r.js +1 -0
  9. package/bizar-dash/dist/assets/{MobileSettings-CEQNJNLJ.js.map → MobileSettings-K-sNsn_r.js.map} +1 -1
  10. package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js → Toast-WTxc3m2g.js} +1 -1
  11. package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js.map → Toast-WTxc3m2g.js.map} +1 -1
  12. package/bizar-dash/dist/assets/flow-BpU0-gK2.js +1 -0
  13. package/bizar-dash/dist/assets/flow-BpU0-gK2.js.map +1 -0
  14. package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js +1 -0
  15. package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js.map +1 -0
  16. package/bizar-dash/dist/assets/{icons-Do5N2jmX.js → icons-B0AKnYI3.js} +98 -103
  17. package/bizar-dash/dist/assets/icons-B0AKnYI3.js.map +1 -0
  18. package/bizar-dash/dist/assets/main-C-Md3DaY.css +1 -0
  19. package/bizar-dash/dist/assets/main-CO8fIxQK.js +18 -0
  20. package/bizar-dash/dist/assets/main-CO8fIxQK.js.map +1 -0
  21. package/bizar-dash/dist/assets/markdown-w2bDDkAH.js +1 -0
  22. package/bizar-dash/dist/assets/{markdown-tOLaD6nm.js.map → markdown-w2bDDkAH.js.map} +1 -1
  23. package/bizar-dash/dist/assets/mobile-DXYCA0hC.js +1 -0
  24. package/bizar-dash/dist/assets/{mobile-DYCHcUpq.js.map → mobile-DXYCA0hC.js.map} +1 -1
  25. package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js +2 -0
  26. package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js.map +1 -0
  27. package/bizar-dash/dist/assets/react-vendor-BnuhLJ6X.css +1 -0
  28. package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js +46 -0
  29. package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js.map +1 -0
  30. package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js +2 -0
  31. package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js.map +1 -0
  32. package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js +45 -0
  33. package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js.map +1 -0
  34. package/bizar-dash/dist/index.html +12 -10
  35. package/bizar-dash/dist/mobile.html +7 -4
  36. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  37. package/bizar-dash/src/server/api.mjs +11 -0
  38. package/bizar-dash/src/server/memory-consolidator.mjs +218 -0
  39. package/bizar-dash/src/server/memory-distillation.mjs +296 -0
  40. package/bizar-dash/src/server/routes/distill.mjs +129 -0
  41. package/bizar-dash/src/server/routes/goal-planner.mjs +236 -0
  42. package/bizar-dash/src/web/App.tsx +4 -0
  43. package/bizar-dash/src/web/components/Tag.tsx +18 -5
  44. package/bizar-dash/src/web/components/Topbar.tsx +3 -0
  45. package/bizar-dash/src/web/components/agents/CommunicationLog.tsx +126 -0
  46. package/bizar-dash/src/web/components/agents/DependencyGraph.tsx +163 -0
  47. package/bizar-dash/src/web/components/agents/QualityGates.tsx +123 -0
  48. package/bizar-dash/src/web/components/agents/RealTimeEventLog.tsx +127 -0
  49. package/bizar-dash/src/web/components/agents/RoutingDecisions.test.tsx +148 -0
  50. package/bizar-dash/src/web/components/agents/RoutingDecisions.tsx +160 -0
  51. package/bizar-dash/src/web/components/goals/GoalInput.tsx +86 -0
  52. package/bizar-dash/src/web/components/goals/PlanVisualization.tsx +111 -0
  53. package/bizar-dash/src/web/lib/goapPlanner.ts +614 -0
  54. package/bizar-dash/src/web/lib/types.ts +25 -0
  55. package/bizar-dash/src/web/styles/agents-redesign.css +311 -0
  56. package/bizar-dash/src/web/styles/main.css +251 -0
  57. package/bizar-dash/src/web/styles/tasks-redesign.css +231 -0
  58. package/bizar-dash/src/web/ui/controls/Button.tsx +77 -0
  59. package/bizar-dash/src/web/ui/controls/Checkbox.tsx +95 -0
  60. package/bizar-dash/src/web/ui/controls/IconButton.tsx +66 -0
  61. package/bizar-dash/src/web/ui/controls/Kbd.tsx +24 -0
  62. package/bizar-dash/src/web/ui/controls/NumberInput.tsx +77 -0
  63. package/bizar-dash/src/web/ui/controls/RadioGroup.tsx +109 -0
  64. package/bizar-dash/src/web/ui/controls/SearchInput.tsx +91 -0
  65. package/bizar-dash/src/web/ui/controls/Select.tsx +92 -0
  66. package/bizar-dash/src/web/ui/controls/Slider.tsx +70 -0
  67. package/bizar-dash/src/web/ui/controls/TextInput.tsx +67 -0
  68. package/bizar-dash/src/web/ui/controls/Toggle.tsx +79 -0
  69. package/bizar-dash/src/web/ui/controls/controls.css +454 -0
  70. package/bizar-dash/src/web/ui/controls/index.ts +33 -0
  71. package/bizar-dash/src/web/ui/data/BarChart.tsx +60 -0
  72. package/bizar-dash/src/web/ui/data/DataTable.tsx +216 -0
  73. package/bizar-dash/src/web/ui/data/EmptyState.tsx +60 -0
  74. package/bizar-dash/src/web/ui/data/ErrorState.tsx +56 -0
  75. package/bizar-dash/src/web/ui/data/KeyValueList.tsx +106 -0
  76. package/bizar-dash/src/web/ui/data/LoadingState.tsx +65 -0
  77. package/bizar-dash/src/web/ui/data/Sparkline.tsx +87 -0
  78. package/bizar-dash/src/web/ui/data/StatTile.tsx +87 -0
  79. package/bizar-dash/src/web/ui/data/data.css +535 -0
  80. package/bizar-dash/src/web/ui/data/index.ts +31 -0
  81. package/bizar-dash/src/web/ui/feedback/Badge.tsx +49 -0
  82. package/bizar-dash/src/web/ui/feedback/Dialog.tsx +209 -0
  83. package/bizar-dash/src/web/ui/feedback/ProgressBar.tsx +69 -0
  84. package/bizar-dash/src/web/ui/feedback/StatusDot.tsx +50 -0
  85. package/bizar-dash/src/web/ui/feedback/Toast.tsx +200 -0
  86. package/bizar-dash/src/web/ui/feedback/Tooltip.tsx +126 -0
  87. package/bizar-dash/src/web/ui/feedback/feedback.css +314 -0
  88. package/bizar-dash/src/web/ui/feedback/index.ts +43 -0
  89. package/bizar-dash/src/web/ui/index.ts +56 -0
  90. package/bizar-dash/src/web/ui/layout/AppShell.tsx +51 -0
  91. package/bizar-dash/src/web/ui/layout/Breadcrumbs.tsx +97 -0
  92. package/bizar-dash/src/web/ui/layout/Panel.tsx +73 -0
  93. package/bizar-dash/src/web/ui/layout/PanelHeader.tsx +39 -0
  94. package/bizar-dash/src/web/ui/layout/Sidebar.tsx +131 -0
  95. package/bizar-dash/src/web/ui/layout/Tabs.tsx +133 -0
  96. package/bizar-dash/src/web/ui/layout/Topbar.tsx +42 -0
  97. package/bizar-dash/src/web/ui/layout/index.ts +33 -0
  98. package/bizar-dash/src/web/ui/layout/layout.css +413 -0
  99. package/bizar-dash/src/web/ui/navigation/CommandPalette.tsx +199 -0
  100. package/bizar-dash/src/web/ui/navigation/NavGroup.tsx +95 -0
  101. package/bizar-dash/src/web/ui/navigation/NavLink.tsx +93 -0
  102. package/bizar-dash/src/web/ui/navigation/index.ts +17 -0
  103. package/bizar-dash/src/web/ui/navigation/navigation.css +343 -0
  104. package/bizar-dash/src/web/ui/primitives/Box.tsx +91 -0
  105. package/bizar-dash/src/web/ui/primitives/Grid.tsx +49 -0
  106. package/bizar-dash/src/web/ui/primitives/Inline.tsx +55 -0
  107. package/bizar-dash/src/web/ui/primitives/Separator.tsx +46 -0
  108. package/bizar-dash/src/web/ui/primitives/Stack.tsx +54 -0
  109. package/bizar-dash/src/web/ui/primitives/VisuallyHidden.tsx +26 -0
  110. package/bizar-dash/src/web/ui/primitives/index.ts +23 -0
  111. package/bizar-dash/src/web/ui/primitives/primitives.css +183 -0
  112. package/bizar-dash/src/web/ui/styles/globals.css +86 -0
  113. package/bizar-dash/src/web/ui/styles/reset.css +125 -0
  114. package/bizar-dash/src/web/ui/styles/tokens.css +195 -0
  115. package/bizar-dash/src/web/ui/theme/ThemeProvider.tsx +142 -0
  116. package/bizar-dash/src/web/ui/theme/tokens.ts +131 -0
  117. package/bizar-dash/src/web/ui/theme/useTheme.ts +22 -0
  118. package/bizar-dash/src/web/ui/utils/cx.ts +42 -0
  119. package/bizar-dash/src/web/views/Agents.tsx +506 -266
  120. package/bizar-dash/src/web/views/GoalPlanner.tsx +158 -0
  121. package/bizar-dash/src/web/views/Overview.tsx +996 -448
  122. package/bizar-dash/src/web/views/Tasks.tsx +277 -273
  123. package/bizar-dash/tests/communication-log.test.tsx +67 -0
  124. package/bizar-dash/tests/dep-graph.test.tsx +40 -0
  125. package/bizar-dash/tests/event-log.test.tsx +61 -0
  126. package/bizar-dash/tests/goal-input.test.tsx +72 -0
  127. package/bizar-dash/tests/goap-planner.test.ts +68 -0
  128. package/bizar-dash/tests/quality-gates.test.tsx +59 -0
  129. package/bizar-dash/tests/setup.ts +31 -1
  130. package/bizar-dash/tests/ui/controls/Button.test.tsx +50 -0
  131. package/bizar-dash/tests/ui/controls/Checkbox.test.tsx +44 -0
  132. package/bizar-dash/tests/ui/controls/IconButton.test.tsx +37 -0
  133. package/bizar-dash/tests/ui/controls/Kbd.test.tsx +39 -0
  134. package/bizar-dash/tests/ui/controls/NumberInput.test.tsx +56 -0
  135. package/bizar-dash/tests/ui/controls/RadioGroup.test.tsx +58 -0
  136. package/bizar-dash/tests/ui/controls/SearchInput.test.tsx +52 -0
  137. package/bizar-dash/tests/ui/controls/Select.test.tsx +66 -0
  138. package/bizar-dash/tests/ui/controls/Slider.test.tsx +41 -0
  139. package/bizar-dash/tests/ui/controls/TextInput.test.tsx +44 -0
  140. package/bizar-dash/tests/ui/controls/Toggle.test.tsx +39 -0
  141. package/bizar-dash/tests/ui/data/BarChart.test.tsx +55 -0
  142. package/bizar-dash/tests/ui/data/DataTable.test.tsx +112 -0
  143. package/bizar-dash/tests/ui/data/EmptyState.test.tsx +57 -0
  144. package/bizar-dash/tests/ui/data/ErrorState.test.tsx +47 -0
  145. package/bizar-dash/tests/ui/data/KeyValueList.test.tsx +59 -0
  146. package/bizar-dash/tests/ui/data/LoadingState.test.tsx +37 -0
  147. package/bizar-dash/tests/ui/data/Sparkline.test.tsx +56 -0
  148. package/bizar-dash/tests/ui/data/StatTile.test.tsx +69 -0
  149. package/bizar-dash/tests/ui/feedback/Badge.test.tsx +41 -0
  150. package/bizar-dash/tests/ui/feedback/Dialog.test.tsx +99 -0
  151. package/bizar-dash/tests/ui/feedback/ProgressBar.test.tsx +66 -0
  152. package/bizar-dash/tests/ui/feedback/StatusDot.test.tsx +41 -0
  153. package/bizar-dash/tests/ui/feedback/Toast.test.tsx +124 -0
  154. package/bizar-dash/tests/ui/feedback/Tooltip.test.tsx +102 -0
  155. package/bizar-dash/tests/ui/layout/AppShell.test.tsx +50 -0
  156. package/bizar-dash/tests/ui/layout/Breadcrumbs.test.tsx +85 -0
  157. package/bizar-dash/tests/ui/layout/Panel.test.tsx +52 -0
  158. package/bizar-dash/tests/ui/layout/PanelHeader.test.tsx +32 -0
  159. package/bizar-dash/tests/ui/layout/Sidebar.test.tsx +95 -0
  160. package/bizar-dash/tests/ui/layout/Tabs.test.tsx +87 -0
  161. package/bizar-dash/tests/ui/layout/Topbar.test.tsx +41 -0
  162. package/bizar-dash/tests/ui/navigation/CommandPalette.test.tsx +105 -0
  163. package/bizar-dash/tests/ui/navigation/NavGroup.test.tsx +55 -0
  164. package/bizar-dash/tests/ui/navigation/NavLink.test.tsx +61 -0
  165. package/bizar-dash/tests/ui/primitives/Box.test.tsx +49 -0
  166. package/bizar-dash/tests/ui/primitives/Grid.test.tsx +37 -0
  167. package/bizar-dash/tests/ui/primitives/Inline.test.tsx +33 -0
  168. package/bizar-dash/tests/ui/primitives/Separator.test.tsx +34 -0
  169. package/bizar-dash/tests/ui/primitives/Stack.test.tsx +45 -0
  170. package/bizar-dash/tests/ui/primitives/VisuallyHidden.test.tsx +27 -0
  171. package/bizar-dash/tests/views/Agents.test.tsx +359 -0
  172. package/bizar-dash/tests/views/Overview.test.tsx +413 -0
  173. package/bizar-dash/tests/views/Tasks.test.tsx +250 -0
  174. package/bizar-dash/vitest.config.ts +3 -1
  175. package/cli/__tests__/cost-gate.test.mjs +301 -0
  176. package/cli/__tests__/feature-list-bridge.test.mjs +371 -0
  177. package/cli/bin.mjs +38 -0
  178. package/cli/commands/claim.mjs +282 -0
  179. package/cli/commands/cost.mjs +279 -0
  180. package/cli/cost-gate.mjs +501 -0
  181. package/cli/feature-list-bridge.mjs +400 -0
  182. package/cli/memory-constants.mjs +17 -0
  183. package/cli/worker-dispatcher.mjs +259 -0
  184. package/cli/worker-dispatcher.test.mjs +204 -0
  185. package/config/trigger-patterns.json +186 -0
  186. package/package.json +2 -2
  187. package/packages/sdk/package.json +1 -1
  188. package/packages/sdk/src/agent-registry.ts +304 -0
  189. package/packages/sdk/src/consensus/byzantine.ts +587 -0
  190. package/packages/sdk/src/consensus/index.ts +147 -0
  191. package/packages/sdk/src/consensus/queen.ts +173 -0
  192. package/packages/sdk/src/consensus/types.ts +199 -0
  193. package/packages/sdk/src/federation/audit.ts +156 -0
  194. package/packages/sdk/src/federation/budget.ts +269 -0
  195. package/packages/sdk/src/federation/envelope.ts +144 -0
  196. package/packages/sdk/src/federation/hmac.ts +160 -0
  197. package/packages/sdk/src/federation/index.ts +443 -0
  198. package/packages/sdk/src/federation/pii.ts +241 -0
  199. package/packages/sdk/src/federation/policy.ts +112 -0
  200. package/packages/sdk/src/federation/trust.ts +146 -0
  201. package/packages/sdk/src/index.ts +67 -0
  202. package/packages/sdk/src/mcp/server.ts +351 -1
  203. package/packages/sdk/src/router/codemod-intent.ts +125 -0
  204. package/packages/sdk/src/router/index.ts +247 -0
  205. package/packages/sdk/src/router/memory-distillation-shim.mjs +18 -0
  206. package/packages/sdk/src/router/memory-distillation.ts +249 -0
  207. package/packages/sdk/src/router/model-router.ts +235 -0
  208. package/packages/sdk/src/router/q-learning-router.ts +304 -0
  209. package/packages/sdk/src/swarm-topology.ts +279 -0
  210. package/packages/sdk/tests/agent-registry.test.ts +285 -0
  211. package/packages/sdk/tests/consensus.test.ts +376 -0
  212. package/packages/sdk/tests/federation/audit.test.ts +164 -0
  213. package/packages/sdk/tests/federation/budget.test.ts +189 -0
  214. package/packages/sdk/tests/federation/envelope.test.ts +120 -0
  215. package/packages/sdk/tests/federation/hmac.test.ts +194 -0
  216. package/packages/sdk/tests/federation/orchestrator.test.ts +176 -0
  217. package/packages/sdk/tests/federation/pii.test.ts +106 -0
  218. package/packages/sdk/tests/federation/policy.test.ts +140 -0
  219. package/packages/sdk/tests/federation/trust.test.ts +124 -0
  220. package/packages/sdk/tests/mcp-tools.test.ts +204 -0
  221. package/packages/sdk/tests/memory-distillation.test.mjs +186 -0
  222. package/packages/sdk/tests/model-router.test.mjs +108 -0
  223. package/packages/sdk/tests/q-learning-router.test.mjs +100 -0
  224. package/packages/sdk/tests/router-orchestrator.test.mjs +172 -0
  225. package/packages/sdk/tests/router.test.mjs +91 -0
  226. package/packages/sdk/tests/sdk.test.mjs +26 -2
  227. package/packages/sdk/tests/swarm-topology.test.ts +258 -0
  228. package/scripts/bh-full-e2e.mjs +11 -15
  229. package/bizar-dash/dist/assets/MobileChat-BJrqwVDd.js +0 -1
  230. package/bizar-dash/dist/assets/MobileSettings-CEQNJNLJ.js +0 -1
  231. package/bizar-dash/dist/assets/icons-Do5N2jmX.js.map +0 -1
  232. package/bizar-dash/dist/assets/main-IvfQAOfy.js +0 -18
  233. package/bizar-dash/dist/assets/main-IvfQAOfy.js.map +0 -1
  234. package/bizar-dash/dist/assets/main-ietCEg_R.css +0 -1
  235. package/bizar-dash/dist/assets/markdown-tOLaD6nm.js +0 -1
  236. package/bizar-dash/dist/assets/mobile-DYCHcUpq.js +0 -1
  237. package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js +0 -2
  238. package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js.map +0 -1
  239. package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js +0 -40
  240. package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js.map +0 -1
  241. package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js +0 -2
  242. package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js.map +0 -1
  243. package/bizar-dash/dist/assets/vendor-C843201K.js +0 -29
  244. package/bizar-dash/dist/assets/vendor-C843201K.js.map +0 -1
  245. package/packages/sdk/package-lock.json +0 -32
@@ -0,0 +1,279 @@
1
+ /**
2
+ * swarm-topology.ts — in-process swarm coordination topology registry.
3
+ *
4
+ * F-032 — ported from ruflo `v3/mcp/tools/swarm-tools.ts` `swarm/init`
5
+ * (line 389). Tracks swarm definitions and the agents assigned to them.
6
+ * No event bus — purely a registry the MCP tools can read/write.
7
+ *
8
+ * The `default` swarm is created lazily on first `initSwarm` call so
9
+ * consumers always have at least one swarm to inspect.
10
+ *
11
+ * Persistence is opt-in: pass `{ persistPath }` to the constructor (or
12
+ * to the `swarmTopologyRegistry` singleton via its mutator) and the
13
+ * registry writes a JSON snapshot to disk after every mutation.
14
+ */
15
+
16
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
17
+ import { dirname } from "node:path";
18
+
19
+ export type SwarmTopology =
20
+ | "hierarchical"
21
+ | "mesh"
22
+ | "adaptive"
23
+ | "collective"
24
+ | "hierarchical-mesh";
25
+
26
+ export const SWARM_TOPOLOGIES: readonly SwarmTopology[] = [
27
+ "hierarchical",
28
+ "mesh",
29
+ "adaptive",
30
+ "collective",
31
+ "hierarchical-mesh",
32
+ ] as const;
33
+
34
+ export const DEFAULT_TOPOLOGY: SwarmTopology = "hierarchical-mesh";
35
+ export const DEFAULT_MAX_AGENTS = 15;
36
+ export const MIN_MAX_AGENTS = 1;
37
+ export const MAX_MAX_AGENTS = 1000;
38
+ export const DEFAULT_SWARM_ID = "default";
39
+
40
+ export interface SwarmRecord {
41
+ swarmId: string;
42
+ topology: SwarmTopology;
43
+ maxAgents: number;
44
+ metadata?: Record<string, unknown>;
45
+ agentIds: string[];
46
+ createdAt: string;
47
+ decommissionedAt?: string;
48
+ }
49
+
50
+ export interface InitSwarmInput {
51
+ swarmId?: string;
52
+ topology?: SwarmTopology;
53
+ maxAgents?: number;
54
+ metadata?: Record<string, unknown>;
55
+ }
56
+
57
+ export interface InitSwarmResult {
58
+ swarmId: string;
59
+ topology: SwarmTopology;
60
+ maxAgents: number;
61
+ createdAt: string;
62
+ metadata?: Record<string, unknown>;
63
+ }
64
+
65
+ export interface SwarmTopologyRegistryOpts {
66
+ /**
67
+ * When set, the registry loads from this file on construction (if it
68
+ * exists) and writes a JSON snapshot after every mutation. The parent
69
+ * directory is created on demand. Path resolution is the caller's
70
+ * responsibility — the registry treats the value as final.
71
+ */
72
+ persistPath?: string;
73
+ }
74
+
75
+ interface PersistShape {
76
+ version: 1;
77
+ swarms: SwarmRecord[];
78
+ }
79
+
80
+ function clampMaxAgents(n: unknown): number {
81
+ const v = typeof n === "number" && Number.isFinite(n) ? Math.floor(n) : DEFAULT_MAX_AGENTS;
82
+ if (v < MIN_MAX_AGENTS) return MIN_MAX_AGENTS;
83
+ if (v > MAX_MAX_AGENTS) return MAX_MAX_AGENTS;
84
+ return v;
85
+ }
86
+
87
+ function normalizeTopology(t: unknown): SwarmTopology {
88
+ if (typeof t === "string" && (SWARM_TOPOLOGIES as readonly string[]).includes(t)) {
89
+ return t as SwarmTopology;
90
+ }
91
+ return DEFAULT_TOPOLOGY;
92
+ }
93
+
94
+ export class SwarmTopologyRegistry {
95
+ private readonly swarms: Map<string, SwarmRecord> = new Map();
96
+ private defaultSeeded = false;
97
+ private persistPath?: string;
98
+
99
+ constructor(opts: SwarmTopologyRegistryOpts = {}) {
100
+ this.persistPath = opts.persistPath;
101
+ if (this.persistPath) this.loadFromDisk();
102
+ }
103
+
104
+ /**
105
+ * Configure (or clear) the persistence path after construction.
106
+ */
107
+ setPersistPath(path: string | undefined): void {
108
+ this.persistPath = path;
109
+ if (path) this.loadFromDisk();
110
+ }
111
+
112
+ private nextSwarmId(): string {
113
+ let i = 1;
114
+ while (this.swarms.has(`swarm-${i}`)) i++;
115
+ return `swarm-${i}`;
116
+ }
117
+
118
+ private ensureDefault(): void {
119
+ if (this.defaultSeeded) return;
120
+ this.defaultSeeded = true;
121
+ if (!this.swarms.has(DEFAULT_SWARM_ID)) {
122
+ const createdAt = new Date().toISOString();
123
+ this.swarms.set(DEFAULT_SWARM_ID, {
124
+ swarmId: DEFAULT_SWARM_ID,
125
+ topology: DEFAULT_TOPOLOGY,
126
+ maxAgents: DEFAULT_MAX_AGENTS,
127
+ agentIds: [],
128
+ createdAt,
129
+ });
130
+ }
131
+ }
132
+
133
+ initSwarm(input: InitSwarmInput = {}): InitSwarmResult {
134
+ this.ensureDefault();
135
+
136
+ let swarmId = input.swarmId?.trim();
137
+ if (!swarmId) swarmId = this.nextSwarmId();
138
+ if (this.swarms.has(swarmId)) {
139
+ throw new Error(`initSwarm: swarm already exists: ${swarmId}`);
140
+ }
141
+
142
+ const topology = normalizeTopology(input.topology);
143
+ const maxAgents = clampMaxAgents(input.maxAgents);
144
+ const createdAt = new Date().toISOString();
145
+ const record: SwarmRecord = {
146
+ swarmId,
147
+ topology,
148
+ maxAgents,
149
+ metadata: input.metadata,
150
+ agentIds: [],
151
+ createdAt,
152
+ };
153
+ this.swarms.set(swarmId, record);
154
+ this.persist();
155
+ return { swarmId, topology, maxAgents, createdAt, metadata: input.metadata };
156
+ }
157
+
158
+ getSwarm(swarmId: string): SwarmRecord | undefined {
159
+ return this.swarms.get(swarmId);
160
+ }
161
+
162
+ listSwarms(): SwarmRecord[] {
163
+ return Array.from(this.swarms.values());
164
+ }
165
+
166
+ recordAgentInSwarm(swarmId: string, agentId: string): SwarmRecord {
167
+ const record = this.swarms.get(swarmId);
168
+ if (!record) {
169
+ throw new Error(`recordAgentInSwarm: swarm not found: ${swarmId}`);
170
+ }
171
+ if (record.decommissionedAt) {
172
+ throw new Error(`recordAgentInSwarm: swarm is decommissioned: ${swarmId}`);
173
+ }
174
+ if (record.agentIds.length >= record.maxAgents && !record.agentIds.includes(agentId)) {
175
+ throw new Error(
176
+ `recordAgentInSwarm: swarm ${swarmId} is at maxAgents (${record.maxAgents})`,
177
+ );
178
+ }
179
+ if (!record.agentIds.includes(agentId)) {
180
+ record.agentIds.push(agentId);
181
+ }
182
+ this.persist();
183
+ return record;
184
+ }
185
+
186
+ decommissionSwarm(swarmId: string): SwarmRecord {
187
+ if (swarmId === DEFAULT_SWARM_ID) {
188
+ throw new Error("decommissionSwarm: cannot decommission the default swarm");
189
+ }
190
+ const record = this.swarms.get(swarmId);
191
+ if (!record) {
192
+ throw new Error(`decommissionSwarm: swarm not found: ${swarmId}`);
193
+ }
194
+ if (record.decommissionedAt) return record;
195
+ record.decommissionedAt = new Date().toISOString();
196
+ this.persist();
197
+ return record;
198
+ }
199
+
200
+ /**
201
+ * Test/dev helper: drop everything.
202
+ */
203
+ reset(): void {
204
+ this.swarms.clear();
205
+ this.defaultSeeded = false;
206
+ if (this.persistPath && existsSync(this.persistPath)) {
207
+ try { writeFileSync(this.persistPath, JSON.stringify({ version: 1, swarms: [] }, null, 2), "utf-8"); }
208
+ catch { /* best-effort */ }
209
+ }
210
+ }
211
+
212
+ // ── Persistence ───────────────────────────────────────────────────────
213
+
214
+ private loadFromDisk(): void {
215
+ if (!this.persistPath || !existsSync(this.persistPath)) return;
216
+ try {
217
+ const raw = readFileSync(this.persistPath, "utf-8");
218
+ const data = JSON.parse(raw) as Partial<PersistShape>;
219
+ if (Array.isArray(data.swarms)) {
220
+ for (const s of data.swarms) {
221
+ if (!s || typeof s.swarmId !== "string") continue;
222
+ this.swarms.set(s.swarmId, s);
223
+ if (s.swarmId === DEFAULT_SWARM_ID) this.defaultSeeded = true;
224
+ }
225
+ }
226
+ } catch {
227
+ // Corrupt or partial file — start fresh.
228
+ }
229
+ }
230
+
231
+ private persist(): void {
232
+ if (!this.persistPath) return;
233
+ try {
234
+ mkdirSync(dirname(this.persistPath), { recursive: true });
235
+ const data: PersistShape = {
236
+ version: 1,
237
+ swarms: Array.from(this.swarms.values()),
238
+ };
239
+ writeFileSync(this.persistPath, JSON.stringify(data, null, 2), "utf-8");
240
+ } catch {
241
+ // Persistence is best-effort.
242
+ }
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Process-wide singleton used by MCP tools. Persistence defaults to
248
+ * `.harness/topology.json` in the current working directory.
249
+ */
250
+ const _sharedRegistry = new SwarmTopologyRegistry({
251
+ persistPath: ".harness/topology.json",
252
+ });
253
+
254
+ export function initSwarm(input: InitSwarmInput = {}): InitSwarmResult {
255
+ return _sharedRegistry.initSwarm(input);
256
+ }
257
+
258
+ export function getSwarm(swarmId: string): SwarmRecord | undefined {
259
+ return _sharedRegistry.getSwarm(swarmId);
260
+ }
261
+
262
+ export function listSwarms(): SwarmRecord[] {
263
+ return _sharedRegistry.listSwarms();
264
+ }
265
+
266
+ export function recordAgentInSwarm(swarmId: string, agentId: string): SwarmRecord {
267
+ return _sharedRegistry.recordAgentInSwarm(swarmId, agentId);
268
+ }
269
+
270
+ export function decommissionSwarm(swarmId: string): SwarmRecord {
271
+ return _sharedRegistry.decommissionSwarm(swarmId);
272
+ }
273
+
274
+ /**
275
+ * Process-wide singleton used by MCP tools. Tests should construct
276
+ * their own `new SwarmTopologyRegistry()` to get an isolated, non-
277
+ * persistent instance.
278
+ */
279
+ export const swarmTopologyRegistry = _sharedRegistry;
@@ -0,0 +1,285 @@
1
+ /**
2
+ * agent-registry.test.ts — unit tests for the in-process agent registry.
3
+ *
4
+ * F-032 — covers registerAgent, listAgents, terminateAgent, getAgent,
5
+ * the heartbeat stub, the type allowlist, and the singleton-vs-class
6
+ * contract.
7
+ */
8
+
9
+ import { describe, test, expect, beforeEach, afterEach } from "vitest";
10
+ import { mkdtempSync, rmSync, existsSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ import {
14
+ BizarAgentRegistry,
15
+ bizarAgentRegistry,
16
+ AGENT_TYPES,
17
+ isAllowedAgentType,
18
+ type AgentRecord,
19
+ } from "../src/agent-registry.js";
20
+
21
+ describe("BizarAgentRegistry — class API", () => {
22
+ let reg: BizarAgentRegistry;
23
+
24
+ beforeEach(() => {
25
+ reg = new BizarAgentRegistry();
26
+ });
27
+
28
+ test("registerAgent returns { agentId, status: 'active' } and a uuid-suffixed id", () => {
29
+ const r = reg.registerAgent({ type: "coder" });
30
+ expect(r.status).toBe("active");
31
+ // crypto.randomUUID() format: agent-<8-4-4-4-12 hex>
32
+ expect(r.agentId).toMatch(/^agent-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
33
+ });
34
+
35
+ test("each register call yields a unique id", () => {
36
+ const ids = new Set<string>();
37
+ for (let i = 0; i < 25; i++) {
38
+ ids.add(reg.registerAgent({ type: "coder" }).agentId);
39
+ }
40
+ expect(ids.size).toBe(25);
41
+ });
42
+
43
+ test("getAgent returns the same record the register call produced", () => {
44
+ const { agentId } = reg.registerAgent({ type: "researcher", name: "Ada" });
45
+ const got: AgentRecord | undefined = reg.getAgent(agentId);
46
+ expect(got).toBeDefined();
47
+ expect(got?.type).toBe("researcher");
48
+ expect(got?.name).toBe("Ada");
49
+ expect(got?.status).toBe("active");
50
+ expect(got?.taskCount).toBe(0);
51
+ expect(typeof got?.createdAt).toBe("string");
52
+ expect(got?.createdAt).toBe(got?.lastHeartbeatAt);
53
+ });
54
+
55
+ test("listAgents returns { agents, total } with default filters", () => {
56
+ reg.registerAgent({ type: "coder" });
57
+ reg.registerAgent({ type: "reviewer" });
58
+ const out = reg.listAgents();
59
+ expect(out.total).toBe(2);
60
+ expect(out.agents).toHaveLength(2);
61
+ expect(out.agents.every((a) => a.status === "active")).toBe(true);
62
+ });
63
+
64
+ test("listAgents filters by type", () => {
65
+ reg.registerAgent({ type: "coder" });
66
+ reg.registerAgent({ type: "coder" });
67
+ reg.registerAgent({ type: "reviewer" });
68
+ const coders = reg.listAgents({ type: "coder" });
69
+ expect(coders.total).toBe(2);
70
+ expect(coders.agents.every((a) => a.type === "coder")).toBe(true);
71
+ });
72
+
73
+ test("listAgents supports limit + offset", () => {
74
+ for (let i = 0; i < 10; i++) reg.registerAgent({ type: "coder" });
75
+ const page1 = reg.listAgents({ limit: 3, offset: 0 });
76
+ const page2 = reg.listAgents({ limit: 3, offset: 3 });
77
+ expect(page1.agents).toHaveLength(3);
78
+ expect(page2.agents).toHaveLength(3);
79
+ expect(new Set([...page1.agents, ...page2.agents].map((a) => a.agentId)).size).toBe(6);
80
+ });
81
+
82
+ test("terminateAgent flips status to terminated and stamps terminatedAt", () => {
83
+ const { agentId } = reg.registerAgent({ type: "coder" });
84
+ const r = reg.terminateAgent({ agentId, reason: "shutdown", graceful: true });
85
+ expect(r.status).toBe("terminated");
86
+ expect(r.agentId).toBe(agentId);
87
+ expect(typeof r.terminatedAt).toBe("string");
88
+ const record = reg.getAgent(agentId);
89
+ expect(record?.status).toBe("terminated");
90
+ expect(record?.terminationReason).toBe("shutdown");
91
+ expect(record?.gracefulTermination).toBe(true);
92
+ expect(record?.terminatedAt).toBe(r.terminatedAt);
93
+ });
94
+
95
+ test("listAgents({ status: 'terminated' }) only shows terminated", () => {
96
+ const a = reg.registerAgent({ type: "coder" });
97
+ reg.registerAgent({ type: "coder" });
98
+ reg.terminateAgent({ agentId: a.agentId, reason: "x" });
99
+ const out = reg.listAgents({ status: "terminated" });
100
+ expect(out.total).toBe(1);
101
+ expect(out.agents[0]?.agentId).toBe(a.agentId);
102
+ });
103
+
104
+ test("listAgents({ status: 'all' }) returns both", () => {
105
+ const a = reg.registerAgent({ type: "coder" });
106
+ reg.registerAgent({ type: "coder" });
107
+ reg.terminateAgent({ agentId: a.agentId, reason: "x" });
108
+ const out = reg.listAgents({ status: "all" });
109
+ expect(out.total).toBe(2);
110
+ });
111
+
112
+ test("terminateAgent throws on unknown id", () => {
113
+ expect(() => reg.terminateAgent({ agentId: "agent-nope", reason: "x" }))
114
+ .toThrow(/not found/);
115
+ });
116
+
117
+ test("registerAgent throws on empty type", () => {
118
+ expect(() => reg.registerAgent({ type: "" })).toThrow(/type/);
119
+ });
120
+
121
+ test("heartbeat refreshes lastHeartbeatAt for active agents", async () => {
122
+ const { agentId } = reg.registerAgent({ type: "coder" });
123
+ const before = reg.getAgent(agentId)?.lastHeartbeatAt;
124
+ await new Promise((r) => setTimeout(r, 5));
125
+ expect(reg.heartbeat(agentId)).toBe(true);
126
+ const after = reg.getAgent(agentId)?.lastHeartbeatAt;
127
+ expect(after).not.toBe(before);
128
+ expect(after).toBeDefined();
129
+ });
130
+
131
+ test("heartbeat returns false for unknown or terminated agents", () => {
132
+ expect(reg.heartbeat("agent-missing")).toBe(false);
133
+ const { agentId } = reg.registerAgent({ type: "coder" });
134
+ reg.terminateAgent({ agentId, reason: "x" });
135
+ expect(reg.heartbeat(agentId)).toBe(false);
136
+ });
137
+
138
+ test("incrementTaskCount bumps taskCount and returns the new value", () => {
139
+ const { agentId } = reg.registerAgent({ type: "coder" });
140
+ expect(reg.incrementTaskCount(agentId)).toBe(1);
141
+ expect(reg.incrementTaskCount(agentId, 4)).toBe(5);
142
+ expect(reg.getAgent(agentId)?.taskCount).toBe(5);
143
+ });
144
+
145
+ test("reset() clears the registry", () => {
146
+ reg.registerAgent({ type: "coder" });
147
+ expect(reg.size()).toBe(1);
148
+ reg.reset();
149
+ expect(reg.size()).toBe(0);
150
+ });
151
+ });
152
+
153
+ describe("BizarAgentRegistry — type allowlist", () => {
154
+ let reg: BizarAgentRegistry;
155
+ beforeEach(() => { reg = new BizarAgentRegistry(); });
156
+
157
+ test("AGENT_TYPES contains the 9 Bizar typed agents", () => {
158
+ expect(AGENT_TYPES).toEqual([
159
+ "coder",
160
+ "tester",
161
+ "reviewer",
162
+ "system-architect",
163
+ "planner",
164
+ "researcher",
165
+ "performance-engineer",
166
+ "security-auditor",
167
+ "memory-specialist",
168
+ ]);
169
+ });
170
+
171
+ test("isAllowedAgentType returns true for known and false for unknown", () => {
172
+ for (const t of AGENT_TYPES) expect(isAllowedAgentType(t)).toBe(true);
173
+ expect(isAllowedAgentType("unknown")).toBe(false);
174
+ expect(isAllowedAgentType("")).toBe(false);
175
+ expect(isAllowedAgentType(undefined)).toBe(false);
176
+ expect(isAllowedAgentType(42)).toBe(false);
177
+ });
178
+
179
+ test("registerAgent accepts each allowed type", () => {
180
+ for (const t of AGENT_TYPES) {
181
+ const r = reg.registerAgent({ type: t });
182
+ expect(reg.getAgent(r.agentId)?.type).toBe(t);
183
+ }
184
+ expect(reg.size()).toBe(AGENT_TYPES.length);
185
+ });
186
+
187
+ test("registerAgent rejects unknown types with a descriptive message", () => {
188
+ expect(() => reg.registerAgent({ type: "lawyer" as unknown as "coder" }))
189
+ .toThrow(/must be one of/);
190
+ expect(() => reg.registerAgent({ type: "random-string" as unknown as "coder" }))
191
+ .toThrow(/got: random-string/);
192
+ });
193
+ });
194
+
195
+ describe("BizarAgentRegistry — persistence", () => {
196
+ let dir: string;
197
+ let persistPath: string;
198
+
199
+ beforeEach(() => {
200
+ dir = mkdtempSync(join(tmpdir(), "agent-registry-"));
201
+ persistPath = join(dir, "agents.json");
202
+ });
203
+
204
+ afterEach(() => {
205
+ if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
206
+ });
207
+
208
+ test("a fresh registry with persistPath starts empty when file is missing", () => {
209
+ const reg = new BizarAgentRegistry({ persistPath });
210
+ expect(reg.size()).toBe(0);
211
+ });
212
+
213
+ test("mutations write a snapshot to disk after each call", () => {
214
+ const reg = new BizarAgentRegistry({ persistPath });
215
+ const { agentId } = reg.registerAgent({ type: "coder" });
216
+ expect(existsSync(persistPath)).toBe(true);
217
+ reg.terminateAgent({ agentId, reason: "x" });
218
+ const stored = JSON.parse(require("node:fs").readFileSync(persistPath, "utf-8")) as { agents: AgentRecord[] };
219
+ expect(stored.agents).toHaveLength(1);
220
+ expect(stored.agents[0]?.status).toBe("terminated");
221
+ expect(stored.agents[0]?.agentId).toBe(agentId);
222
+ });
223
+
224
+ test("a new registry reading the same path picks up persisted agents", () => {
225
+ const reg1 = new BizarAgentRegistry({ persistPath });
226
+ const { agentId } = reg1.registerAgent({ type: "reviewer", name: "R1" });
227
+ expect(reg1.size()).toBe(1);
228
+
229
+ const reg2 = new BizarAgentRegistry({ persistPath });
230
+ expect(reg2.size()).toBe(1);
231
+ const got = reg2.getAgent(agentId);
232
+ expect(got?.type).toBe("reviewer");
233
+ expect(got?.name).toBe("R1");
234
+ expect(got?.status).toBe("active");
235
+ });
236
+
237
+ test("setPersistPath switches the persistence target at runtime", () => {
238
+ const reg = new BizarAgentRegistry();
239
+ reg.setPersistPath(persistPath);
240
+ reg.registerAgent({ type: "planner" });
241
+ expect(existsSync(persistPath)).toBe(true);
242
+
243
+ const other = join(dir, "agents-other.json");
244
+ reg.setPersistPath(other);
245
+ reg.registerAgent({ type: "tester" });
246
+ expect(existsSync(other)).toBe(true);
247
+ });
248
+
249
+ test("a corrupt persistence file does not crash the registry", () => {
250
+ require("node:fs").writeFileSync(persistPath, "{not json", "utf-8");
251
+ const reg = new BizarAgentRegistry({ persistPath });
252
+ expect(reg.size()).toBe(0);
253
+ reg.registerAgent({ type: "coder" });
254
+ expect(reg.size()).toBe(1);
255
+ });
256
+
257
+ test("reset() wipes the persisted file when configured", () => {
258
+ const reg = new BizarAgentRegistry({ persistPath });
259
+ reg.registerAgent({ type: "coder" });
260
+ expect(existsSync(persistPath)).toBe(true);
261
+ reg.reset();
262
+ const stored = JSON.parse(require("node:fs").readFileSync(persistPath, "utf-8")) as { agents: AgentRecord[] };
263
+ expect(stored.agents).toEqual([]);
264
+ });
265
+ });
266
+
267
+ describe("BizarAgentRegistry — singleton", () => {
268
+ test("bizarAgentRegistry is an instance of BizarAgentRegistry", () => {
269
+ expect(bizarAgentRegistry).toBeInstanceOf(BizarAgentRegistry);
270
+ });
271
+
272
+ test("register on singleton is visible via getAgent on the same singleton", () => {
273
+ const r = bizarAgentRegistry.registerAgent({ type: "memory-specialist" });
274
+ expect(bizarAgentRegistry.getAgent(r.agentId)?.type).toBe("memory-specialist");
275
+ // cleanup so we don't pollute later tests
276
+ bizarAgentRegistry.terminateAgent({ agentId: r.agentId, reason: "cleanup" });
277
+ });
278
+
279
+ test("singleton has a persistPath configured", () => {
280
+ // The singleton writes to .harness/agents.json by default. We
281
+ // verify the internal field is set without depending on the
282
+ // exact path resolution at the CWD level.
283
+ expect((bizarAgentRegistry as unknown as { persistPath?: string }).persistPath).toBeDefined();
284
+ });
285
+ });