@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,163 @@
1
+ // src/web/components/agents/DependencyGraph.tsx — DAG of plan step dependencies.
2
+ //
3
+ // v6.4.0 — F-036 (Goal Planner UI). Uses @xyflow/react (already in
4
+ // Bizar deps) to render the plan's step graph. Falls back to a
5
+ // hand-rolled table when no plan is supplied.
6
+ import { useMemo } from 'react';
7
+ import {
8
+ ReactFlow,
9
+ Background,
10
+ Controls,
11
+ MiniMap,
12
+ type Node,
13
+ type Edge,
14
+ type NodeProps,
15
+ Handle,
16
+ Position,
17
+ } from '@xyflow/react';
18
+ import '@xyflow/react/dist/style.css';
19
+ import { GitBranch } from 'lucide-react';
20
+ import { Card, CardTitle, CardMeta } from '../Card';
21
+ import { Tag } from '../Tag';
22
+ import type { Plan } from '../../lib/goapPlanner';
23
+
24
+ interface Props {
25
+ plan: Plan | null;
26
+ }
27
+
28
+ function StepNode({ data }: NodeProps<Node<{ label: string; action: string; agent: string }>>) {
29
+ return (
30
+ <div className={`dep-node is-${(data as { action?: string }).action || 'research'}`}>
31
+ <Handle type="target" position={Position.Left} className="dep-handle" />
32
+ <div className="dep-node-action">{(data as { action?: string }).action}</div>
33
+ <div className="dep-node-label">{(data as { label?: string }).label}</div>
34
+ <div className="dep-node-agent">@{(data as { agent?: string }).agent}</div>
35
+ <Handle type="source" position={Position.Right} className="dep-handle" />
36
+ </div>
37
+ );
38
+ }
39
+
40
+ const nodeTypes = { step: StepNode };
41
+
42
+ export function DependencyGraph({ plan }: Props) {
43
+ const nodes: Node[] = useMemo(() => {
44
+ if (!plan) return [];
45
+ const n = plan.steps.length;
46
+ // Lay out nodes in a horizontal flow based on dep depth.
47
+ const depths = new Map<string, number>();
48
+ const computeDepth = (id: string, stack = new Set<string>()): number => {
49
+ const cached = depths.get(id);
50
+ if (cached !== undefined) return cached;
51
+ if (stack.has(id)) return 0;
52
+ stack.add(id);
53
+ const step = plan.steps.find((s) => s.id === id);
54
+ if (!step || step.deps.length === 0) {
55
+ depths.set(id, 0);
56
+ stack.delete(id);
57
+ return 0;
58
+ }
59
+ let max = 0;
60
+ for (const dep of step.deps) {
61
+ const d = computeDepth(dep, stack);
62
+ if (d > max) max = d;
63
+ }
64
+ depths.set(id, max + 1);
65
+ stack.delete(id);
66
+ return max + 1;
67
+ };
68
+ for (const s of plan.steps) computeDepth(s.id);
69
+
70
+ const byDepth = new Map<number, string[]>();
71
+ for (const s of plan.steps) {
72
+ const d = depths.get(s.id) ?? 0;
73
+ const arr = byDepth.get(d) || [];
74
+ arr.push(s.id);
75
+ byDepth.set(d, arr);
76
+ }
77
+ const nodesOut: Node[] = [];
78
+ for (const [depth, ids] of byDepth) {
79
+ const total = ids.length;
80
+ ids.forEach((id, i) => {
81
+ const step = plan.steps.find((s) => s.id === id);
82
+ if (!step) return;
83
+ const xOffset = 60 + depth * 220;
84
+ const yOffset = 40 + (i - (total - 1) / 2) * 90;
85
+ nodesOut.push({
86
+ id: step.id,
87
+ type: 'step',
88
+ position: { x: xOffset, y: yOffset },
89
+ data: {
90
+ label: step.title,
91
+ action: step.action,
92
+ agent: step.agent,
93
+ },
94
+ });
95
+ });
96
+ }
97
+ void n;
98
+ return nodesOut;
99
+ }, [plan]);
100
+
101
+ const edges: Edge[] = useMemo(() => {
102
+ if (!plan) return [];
103
+ return plan.steps.flatMap((step) =>
104
+ step.deps.map((dep) => ({
105
+ id: `${dep}->${step.id}`,
106
+ source: dep,
107
+ target: step.id,
108
+ animated: true,
109
+ })),
110
+ );
111
+ }, [plan]);
112
+
113
+ if (!plan) {
114
+ return (
115
+ <Card className="dep-graph" data-testid="dep-graph">
116
+ <CardTitle>
117
+ <GitBranch size={14} aria-hidden /> Dependency graph
118
+ </CardTitle>
119
+ <CardMeta>Submit a goal to render the dependency DAG.</CardMeta>
120
+ </Card>
121
+ );
122
+ }
123
+
124
+ if (plan.steps.length === 0) {
125
+ return (
126
+ <Card className="dep-graph" data-testid="dep-graph">
127
+ <CardTitle>
128
+ <GitBranch size={14} aria-hidden /> Dependency graph
129
+ </CardTitle>
130
+ <CardMeta>Empty plan — nothing to render.</CardMeta>
131
+ </Card>
132
+ );
133
+ }
134
+
135
+ return (
136
+ <Card className="dep-graph" data-testid="dep-graph">
137
+ <div className="dep-graph-header">
138
+ <CardTitle>
139
+ <GitBranch size={14} aria-hidden /> Dependency graph
140
+ </CardTitle>
141
+ <Tag variant="info">{plan.steps.length} nodes</Tag>
142
+ <Tag variant="accent">{edges.length} edges</Tag>
143
+ </div>
144
+ <CardMeta>Plan step DAG · {plan.goal.slice(0, 80)}</CardMeta>
145
+ <div className="dep-graph-canvas" data-testid="dep-graph-canvas">
146
+ <ReactFlow
147
+ nodes={nodes}
148
+ edges={edges}
149
+ nodeTypes={nodeTypes}
150
+ fitView
151
+ fitViewOptions={{ padding: 0.15 }}
152
+ proOptions={{ hideAttribution: true }}
153
+ >
154
+ <Background gap={16} size={1} />
155
+ <Controls showInteractive={false} />
156
+ <MiniMap pannable zoomable />
157
+ </ReactFlow>
158
+ </div>
159
+ </Card>
160
+ );
161
+ }
162
+
163
+ export default DependencyGraph;
@@ -0,0 +1,123 @@
1
+ // src/web/components/agents/QualityGates.tsx — pass/fail rubric display.
2
+ //
3
+ // v6.4.0 — F-036 (Goal Planner UI). Scores the four L09 rubric
4
+ // dimensions (Correctness / Arch compliance / Test coverage /
5
+ // Verification evidence) against the live plan. Each gate turns
6
+ // green when the supplied metric meets the threshold documented in
7
+ // `templates/evaluator-rubric.md`.
8
+ import { Shield, CheckCircle2, XCircle, AlertTriangle } from 'lucide-react';
9
+ import { Card, CardTitle, CardMeta } from '../Card';
10
+ import { Tag } from '../Tag';
11
+ import type { Plan } from '../../lib/goapPlanner';
12
+
13
+ interface Metrics {
14
+ /** 0 or 1 — whether `make check` is green. */
15
+ compileCheck: boolean;
16
+ /** Percentage of touched tests passing. */
17
+ testCoverage: number;
18
+ /** Percentage of arch-rules passing. */
19
+ archScore: number;
20
+ /** Verification evidence (commit hash or test output) present. */
21
+ evidencePresent: boolean;
22
+ }
23
+
24
+ interface GateRow {
25
+ name: string;
26
+ status: 'passed' | 'warning' | 'failed';
27
+ value?: number | boolean;
28
+ threshold?: number | boolean;
29
+ detail: string;
30
+ }
31
+
32
+ interface Props {
33
+ plan: Plan | null;
34
+ metrics: Metrics;
35
+ }
36
+
37
+ function evaluate(metrics: Metrics): GateRow[] {
38
+ const compile: GateRow = metrics.compileCheck
39
+ ? { name: 'Compile check', status: 'passed', value: true, threshold: true, detail: 'make check is green.' }
40
+ : { name: 'Compile check', status: 'failed', value: false, threshold: true, detail: 'Typecheck error — fix before marking passing.' };
41
+
42
+ const coverage: GateRow = metrics.testCoverage >= 80
43
+ ? { name: 'Test coverage', status: 'passed', value: metrics.testCoverage, threshold: 80, detail: 'Coverage meets the B-grade threshold.' }
44
+ : metrics.testCoverage >= 60
45
+ ? { name: 'Test coverage', status: 'warning', value: metrics.testCoverage, threshold: 80, detail: 'Coverage is below the B-grade threshold.' }
46
+ : { name: 'Test coverage', status: 'failed', value: metrics.testCoverage, threshold: 80, detail: 'Coverage is critical.' };
47
+
48
+ const arch: GateRow = metrics.archScore >= 90
49
+ ? { name: 'Arch compliance', status: 'passed', value: metrics.archScore, threshold: 90, detail: 'All non-trivial arch-rules satisfied.' }
50
+ : metrics.archScore >= 70
51
+ ? { name: 'Arch compliance', status: 'warning', value: metrics.archScore, threshold: 90, detail: 'Minor arch-rule violations.' }
52
+ : { name: 'Arch compliance', status: 'failed', value: metrics.archScore, threshold: 90, detail: 'Multiple arch-rule violations.' };
53
+
54
+ const evidence: GateRow = metrics.evidencePresent
55
+ ? { name: 'Verification evidence', status: 'passed', value: true, threshold: true, detail: 'Test output + commit hash captured.' }
56
+ : { name: 'Verification evidence', status: 'failed', value: false, threshold: true, detail: 'No evidence recorded yet.' };
57
+
58
+ return [compile, coverage, arch, evidence];
59
+ }
60
+
61
+ function GateIcon({ status }: { status: GateRow['status'] }) {
62
+ if (status === 'passed') return <CheckCircle2 size={14} className="qg-icon is-pass" />;
63
+ if (status === 'warning') return <AlertTriangle size={14} className="qg-icon is-warn" />;
64
+ return <XCircle size={14} className="qg-icon is-fail" />;
65
+ }
66
+
67
+ function GateTag({ status }: { status: GateRow['status'] }) {
68
+ if (status === 'passed') return <Tag variant="success">passed</Tag>;
69
+ if (status === 'warning') return <Tag variant="warning">warning</Tag>;
70
+ return <Tag variant="error">failed</Tag>;
71
+ }
72
+
73
+ export function QualityGates({ plan, metrics }: Props) {
74
+ const gates = evaluate(metrics);
75
+ const passing = gates.filter((g) => g.status === 'passed').length;
76
+ return (
77
+ <Card className="qg-panel" data-testid="quality-gates">
78
+ <div className="qg-header">
79
+ <CardTitle>
80
+ <Shield size={14} aria-hidden /> Quality gates
81
+ </CardTitle>
82
+ <Tag variant={passing === gates.length ? 'success' : passing > 0 ? 'warning' : 'error'}>
83
+ {passing}/{gates.length} passing
84
+ </Tag>
85
+ </div>
86
+ <CardMeta>
87
+ L09 rubric (Correctness / Arch compliance / Test coverage / Verification evidence) —{' '}
88
+ {plan ? `evaluating against plan with ${plan.steps.length} steps.` : 'awaiting plan.'}
89
+ </CardMeta>
90
+ <ul className="qg-list" data-testid="quality-gates-list">
91
+ {gates.map((gate) => (
92
+ <li
93
+ key={gate.name}
94
+ className={`qg-row is-${gate.status}`}
95
+ data-testid="quality-gates-row"
96
+ data-status={gate.status}
97
+ >
98
+ <div className="qg-row-head">
99
+ <GateIcon status={gate.status} />
100
+ <span className="qg-name">{gate.name}</span>
101
+ <GateTag status={gate.status} />
102
+ </div>
103
+ <div className="qg-row-body">
104
+ <span className="qg-detail">{gate.detail}</span>
105
+ {typeof gate.value === 'number' && (
106
+ <span className="qg-threshold">
107
+ current: {gate.value}% · threshold: {gate.threshold}%
108
+ </span>
109
+ )}
110
+ {typeof gate.value === 'boolean' && (
111
+ <span className="qg-threshold">
112
+ current: {String(gate.value)} · threshold: {String(gate.threshold)}
113
+ </span>
114
+ )}
115
+ </div>
116
+ </li>
117
+ ))}
118
+ </ul>
119
+ </Card>
120
+ );
121
+ }
122
+
123
+ export default QualityGates;
@@ -0,0 +1,127 @@
1
+ // src/web/components/agents/RealTimeEventLog.tsx — streaming event ticker.
2
+ //
3
+ // v6.4.0 — F-036 (Goal Planner UI). Subscribes to ws.ts for ALL
4
+ // events and renders a color-coded ticker. Bounded ring buffer so
5
+ // a runaway server can't fill memory.
6
+ import { useEffect, useMemo, useState } from 'react';
7
+ import { Activity } from 'lucide-react';
8
+ import { Ws } from '../../lib/ws';
9
+ import { Card, CardTitle, CardMeta } from '../Card';
10
+ import { Tag } from '../Tag';
11
+
12
+ const MAX_ROWS = 500;
13
+
14
+ type EventRow = {
15
+ id: string;
16
+ ts: number;
17
+ type: string;
18
+ severity: 'info' | 'success' | 'warning' | 'error' | 'control';
19
+ preview: string;
20
+ };
21
+
22
+ function severityFor(type: string): EventRow['severity'] {
23
+ if (type.startsWith('goal:')) return 'success';
24
+ if (type.startsWith('task:')) return 'info';
25
+ if (type.includes('error') || type.includes('fail')) return 'error';
26
+ if (type.includes('stuck') || type.includes('blocked') || type.includes('warn')) return 'warning';
27
+ if (type === 'ping' || type === 'pong' || type === 'refresh') return 'control';
28
+ return 'info';
29
+ }
30
+
31
+ function previewOf(type: string, payload: Record<string, unknown> | undefined): string {
32
+ if (!payload) return type;
33
+ const keys = Object.keys(payload).slice(0, 3);
34
+ return keys.map((k) => `${k}=${shortVal(payload[k])}`).join(' ') || type;
35
+ }
36
+
37
+ function shortVal(v: unknown): string {
38
+ if (v == null) return 'null';
39
+ if (typeof v === 'string') return v.length > 20 ? `"${v.slice(0, 20)}…"` : `"${v}"`;
40
+ if (typeof v === 'number' || typeof v === 'boolean') return String(v);
41
+ if (Array.isArray(v)) return `[${v.length}]`;
42
+ if (typeof v === 'object') return `{${Object.keys(v).length}}`;
43
+ return String(v);
44
+ }
45
+
46
+ export function RealTimeEventLog() {
47
+ const [rows, setRows] = useState<EventRow[]>([]);
48
+
49
+ useEffect(() => {
50
+ const ws = new Ws();
51
+ const off = ws.on((msg) => {
52
+ const m = msg as { type?: string; ts?: number } & Record<string, unknown>;
53
+ const t = m.type;
54
+ if (!t) return;
55
+ const ts = Number(m.ts) || Date.now();
56
+ const row: EventRow = {
57
+ id: `${ts}-${Math.random().toString(36).slice(2, 7)}`,
58
+ ts,
59
+ type: t,
60
+ severity: severityFor(t),
61
+ preview: previewOf(t, m as Record<string, unknown>),
62
+ };
63
+ setRows((prev) => {
64
+ const next = [...prev, row];
65
+ return next.length > MAX_ROWS ? next.slice(next.length - MAX_ROWS) : next;
66
+ });
67
+ });
68
+ return () => {
69
+ off();
70
+ ws.close();
71
+ };
72
+ }, []);
73
+
74
+ const summary = useMemo(() => {
75
+ const counts: Record<EventRow['severity'], number> = {
76
+ info: 0,
77
+ success: 0,
78
+ warning: 0,
79
+ error: 0,
80
+ control: 0,
81
+ };
82
+ for (const r of rows) counts[r.severity] += 1;
83
+ return counts;
84
+ }, [rows]);
85
+
86
+ return (
87
+ <Card className="event-log" data-testid="event-log">
88
+ <div className="event-log-header">
89
+ <CardTitle>
90
+ <Activity size={14} aria-hidden /> Real-time events
91
+ </CardTitle>
92
+ <Tag variant="neutral" data-testid="event-log-count">{rows.length}</Tag>
93
+ </div>
94
+ <CardMeta>Live WebSocket event ticker · bounded to {MAX_ROWS} rows</CardMeta>
95
+ <div className="event-log-summary" data-testid="event-log-summary">
96
+ <Tag variant="info">info {summary.info}</Tag>
97
+ <Tag variant="success">success {summary.success}</Tag>
98
+ <Tag variant="warning">warning {summary.warning}</Tag>
99
+ <Tag variant="error">error {summary.error}</Tag>
100
+ <Tag variant="neutral">control {summary.control}</Tag>
101
+ </div>
102
+ <ul className="event-log-list" data-testid="event-log-list">
103
+ {rows.length === 0 && (
104
+ <li className="event-log-empty" data-testid="event-log-empty">
105
+ No events yet. Events will stream here as the server emits them.
106
+ </li>
107
+ )}
108
+ {rows.slice().reverse().map((row) => (
109
+ <li
110
+ key={row.id}
111
+ className={`event-log-row is-${row.severity}`}
112
+ data-testid="event-log-row"
113
+ data-severity={row.severity}
114
+ >
115
+ <time className="event-log-time" dateTime={new Date(row.ts).toISOString()}>
116
+ {new Date(row.ts).toLocaleTimeString()}
117
+ </time>
118
+ <code className="event-log-type">{row.type}</code>
119
+ <span className="event-log-preview">{row.preview}</span>
120
+ </li>
121
+ ))}
122
+ </ul>
123
+ </Card>
124
+ );
125
+ }
126
+
127
+ export default RealTimeEventLog;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * src/web/components/agents/RoutingDecisions.test.tsx
3
+ *
4
+ * F-035 — vitest tests for the routing-decisions panel.
5
+ * 3 tests required by the F-035 layers[] spec:
6
+ * 1. renders the empty state when no decisions have been emitted
7
+ * 2. renders a single decision via the `initial` prop
8
+ * 3. renders multiple decisions via the WS subscription path
9
+ *
10
+ * The `subscribe` prop receives a `(msg: WsMessage) => void` handler
11
+ * and returns an unsubscribe function. We capture the handler and
12
+ * drive it directly — no real WebSocket needed.
13
+ */
14
+
15
+ import { describe, it, expect, vi } from 'vitest';
16
+ import { render, screen, fireEvent, act, waitFor } from '@testing-library/react';
17
+ import React from 'react';
18
+ import { RoutingDecisions } from './RoutingDecisions';
19
+ import type { RoutingDecision, WsMessage } from '../../lib/types';
20
+
21
+ const dec = (id: string, tier: RoutingDecision['tier'], model: string, extras: Partial<RoutingDecision> = {}): RoutingDecision => ({
22
+ id,
23
+ ts: 1_700_000_000_000 + Number(id.replace(/\D/g, '') || 0) * 1000,
24
+ tier,
25
+ model,
26
+ task: `task-${id}`,
27
+ costUsd: tier === 'CODEMOD' ? 0 : 0.01,
28
+ latencyMs: 250,
29
+ ...extras,
30
+ });
31
+
32
+ describe('RoutingDecisions', () => {
33
+ it('renders the empty state when no decisions have been emitted', () => {
34
+ render(<RoutingDecisions />);
35
+ const empty = screen.getByTestId('routing-decisions-empty');
36
+ expect(empty).toBeTruthy();
37
+ expect(empty.textContent).toMatch(/No routing decisions yet/i);
38
+ // Confirm table is NOT rendered in empty state.
39
+ expect(screen.queryByRole('table')).toBeNull();
40
+ });
41
+
42
+ it('renders a single decision via the initial prop', () => {
43
+ const one = dec('1', 'CODEMOD', 'codemod:var-to-const');
44
+ render(<RoutingDecisions initial={[one]} />);
45
+
46
+ // Tier badge with the right class
47
+ const row = screen.getByTestId(`routing-row-${one.id}`);
48
+ expect(row).toBeTruthy();
49
+ const badge = row.querySelector('.routing-tier-codemod');
50
+ expect(badge).toBeTruthy();
51
+ expect(badge?.textContent).toBe('CODEMOD');
52
+
53
+ // Model + task + cost columns
54
+ expect(row.querySelector('.routing-model')?.textContent).toBe('codemod:var-to-const');
55
+ expect(row.querySelector('.routing-task')?.textContent).toBe('task-1');
56
+ expect(row.querySelector('.routing-cost')?.textContent).toBe('$0');
57
+ // latency = 250ms → "250ms"
58
+ expect(row.querySelector('.routing-latency')?.textContent).toBe('250ms');
59
+ });
60
+
61
+ it('renders multiple decisions and filters by tier', () => {
62
+ const list: RoutingDecision[] = [
63
+ dec('1', 'CODEMOD', 'codemod:remove-console', { costUsd: 0 }),
64
+ dec('2', 'TIER1', 'haiku', { costUsd: 0.0008 }),
65
+ dec('3', 'TIER2', 'sonnet', { costUsd: 0.012 }),
66
+ dec('4', 'TIER3', 'opus', { costUsd: 0.05 }),
67
+ ];
68
+
69
+ const { rerender } = render(<RoutingDecisions initial={list} />);
70
+ // All four rows present
71
+ for (const d of list) {
72
+ expect(screen.getByTestId(`routing-row-${d.id}`)).toBeTruthy();
73
+ }
74
+
75
+ // Tier filter — only TIER2 row survives
76
+ rerender(<RoutingDecisions initial={list} filterTier="TIER2" />);
77
+ expect(screen.getByTestId('routing-row-3')).toBeTruthy();
78
+ expect(screen.queryByTestId('routing-row-1')).toBeNull();
79
+ expect(screen.queryByTestId('routing-row-2')).toBeNull();
80
+ expect(screen.queryByTestId('routing-row-4')).toBeNull();
81
+
82
+ // Subscribed WS append test — drive a `routing:decision` through
83
+ // the captured handler.
84
+ let capturedHandler: ((msg: WsMessage) => void) | null = null;
85
+ const subscribe = (handler: (msg: WsMessage) => void) => {
86
+ capturedHandler = handler;
87
+ return () => { capturedHandler = null; };
88
+ };
89
+ const appended: RoutingDecision[] = [];
90
+ const onAppend = (d: RoutingDecision) => appended.push(d);
91
+
92
+ rerender(
93
+ <RoutingDecisions
94
+ initial={list}
95
+ subscribe={subscribe}
96
+ onAppend={onAppend}
97
+ filterTier="ALL"
98
+ />,
99
+ );
100
+ expect(capturedHandler).toBeTruthy();
101
+ const fresh = dec('5', 'TIER3', 'opus-2', { costUsd: 0.07, latencyMs: 1200 });
102
+ // Drive the handler inside act() so React commits the setDecisions.
103
+ act(() => {
104
+ capturedHandler!({ type: 'routing:decision', decision: fresh });
105
+ });
106
+ expect(screen.getByTestId('routing-row-5')).toBeTruthy();
107
+ expect(appended).toHaveLength(1);
108
+ expect(appended[0].id).toBe('5');
109
+
110
+ // Malformed message should be ignored (no `routing:decision` type).
111
+ capturedHandler!({ type: 'tasks:change', task: {} as never });
112
+ expect(screen.queryByTestId('routing-row-')).toBeNull();
113
+
114
+ // Empty filterTier = 'ALL' shows everything
115
+ rerender(<RoutingDecisions initial={list} filterTier="ALL" />);
116
+ for (const d of list) {
117
+ expect(screen.getByTestId(`routing-row-${d.id}`)).toBeTruthy();
118
+ }
119
+ });
120
+
121
+ it('respects maxRows by pruning oldest decisions', () => {
122
+ // The component treats `initial` as most-recent-first (same
123
+ // convention as the WS stream). Build the list newest-first so
124
+ // maxRows=3 keeps the last 3 items from the array (the newest).
125
+ const list: RoutingDecision[] = [
126
+ dec('5', 'TIER1', 'haiku'),
127
+ dec('4', 'TIER1', 'haiku'),
128
+ dec('3', 'TIER1', 'haiku'),
129
+ dec('2', 'TIER1', 'haiku'),
130
+ dec('1', 'TIER1', 'haiku'),
131
+ ];
132
+ render(<RoutingDecisions initial={list} maxRows={3} />);
133
+ // Only 3 of 5 should be visible (most-recent first)
134
+ expect(screen.getByTestId('routing-row-5')).toBeTruthy();
135
+ expect(screen.getByTestId('routing-row-4')).toBeTruthy();
136
+ expect(screen.getByTestId('routing-row-3')).toBeTruthy();
137
+ expect(screen.queryByTestId('routing-row-2')).toBeNull();
138
+ expect(screen.queryByTestId('routing-row-1')).toBeNull();
139
+ });
140
+
141
+ it('compact prop toggles a different class', () => {
142
+ const one = dec('1', 'CODEMOD', 'codemod:add-logging');
143
+ const { container, rerender } = render(<RoutingDecisions initial={[one]} />);
144
+ expect(container.querySelector('.routing-decisions-compact')).toBeNull();
145
+ rerender(<RoutingDecisions initial={[one]} compact />);
146
+ expect(container.querySelector('.routing-decisions-compact')).toBeTruthy();
147
+ });
148
+ });