@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,147 @@
1
+ /**
2
+ * consensus/index.ts — Public orchestrator for the consensus layer.
3
+ *
4
+ * F-039 — wraps `ByzantineConsensus` behind a stable factory API and
5
+ * re-exports every type/utility callers need. This is the surface the
6
+ * MCP tool (`consensus_propose`) and any future dashboard wiring
7
+ * consume.
8
+ *
9
+ * The orchestrator is intentionally minimal — it's a thin facade over
10
+ * `ByzantineConsensus` plus the type re-exports. Anything fancier
11
+ * (proposer scheduling, fault dashboards, etc.) belongs in `queen.ts`
12
+ * or a future `dashboard.ts`.
13
+ */
14
+
15
+ import { ByzantineConsensus, type ByzantineConsensusOpts } from "./byzantine.js";
16
+ import {
17
+ DEFAULT_MAX_FAULTS,
18
+ DEFAULT_QUORUM,
19
+ type CommitResult,
20
+ type ConsensusStatus,
21
+ type Decision,
22
+ type ProposeResult,
23
+ type ProposalSnapshot,
24
+ type Vote,
25
+ type VoteResult,
26
+ type ViewChangeResult,
27
+ } from "./types.js";
28
+
29
+ export interface CreateConsensusOpts extends Omit<ByzantineConsensusOpts, "localAgentId" | "peers"> {
30
+ localAgentId: string;
31
+ peers: string[];
32
+ }
33
+
34
+ export interface ConsensusHandle {
35
+ /** Identity of the local agent (vote short-circuits on its own calls). */
36
+ readonly localAgentId: string;
37
+ /** Read-only snapshot of every proposal the instance has tracked. */
38
+ status(): ConsensusStatus;
39
+ /** Snapshot of a single proposal, or `undefined` if unknown. */
40
+ getProposal(proposalId: string): ProposalSnapshot | undefined;
41
+ /** Initiate pre-prepare for a new proposal. Re-submitting the same
42
+ * payload under the same view returns the cached proposalId. */
43
+ propose(payload: unknown): ProposeResult;
44
+ /** Cast a vote for a peer during the `prepare` phase. */
45
+ castVote(proposalId: string, agentId: string, decision: Decision, signature?: string): VoteResult;
46
+ /** Transport-friendly entry point — `onPrepare(proposalId, vote)`. */
47
+ onPrepare(proposalId: string, vote: Vote): VoteResult;
48
+ /** Force-commit a proposal (admin override; rare). */
49
+ commit(proposalId: string): CommitResult;
50
+ /** Trigger a view-change; returns the new proposer + view number. */
51
+ viewChange(reason: string, proposalId?: string): ViewChangeResult;
52
+ /** Head of the round-robin proposer schedule. */
53
+ getCurrentProposer(): string;
54
+ /** Current view number (incremented on every view-change). */
55
+ getViewNumber(): number;
56
+ /** Quorum threshold in effect. */
57
+ getQuorum(): number;
58
+ /** Peer roster (read-only copy). */
59
+ getPeers(): string[];
60
+ }
61
+
62
+ /**
63
+ * Build a `ConsensusHandle` for a given local agent + peer roster.
64
+ * The returned object is the public surface used by the MCP tool and
65
+ * any dashboard wiring.
66
+ */
67
+ export function createConsensus(opts: CreateConsensusOpts): ConsensusHandle {
68
+ const instance = new ByzantineConsensus(opts);
69
+ return {
70
+ localAgentId: opts.localAgentId,
71
+ status: () => instance.status(),
72
+ getProposal: (id) => instance.getProposal(id),
73
+ propose: (payload) => instance.propose(payload),
74
+ castVote: (proposalId, agentId, decision, signature) =>
75
+ instance.castVote(proposalId, agentId, decision, signature),
76
+ onPrepare: (proposalId, vote) => instance.onPrepare(proposalId, vote),
77
+ commit: (proposalId) => instance.commit(proposalId),
78
+ viewChange: (reason, proposalId) => instance.viewChange(reason, proposalId),
79
+ getCurrentProposer: () => instance.getCurrentProposer(),
80
+ getViewNumber: () => instance.getViewNumber(),
81
+ getQuorum: () => instance.getQuorum(),
82
+ getPeers: () => instance.getPeers(),
83
+ };
84
+ }
85
+
86
+ // ─── Re-exports ────────────────────────────────────────────────────────
87
+
88
+ export { ByzantineConsensus, type ByzantineConsensusOpts } from "./byzantine.js";
89
+ export { QueenCoordinator, type QueenCoordinatorOpts } from "./queen.js";
90
+ export {
91
+ DEFAULT_MAX_FAULTS,
92
+ DEFAULT_PEER_COUNT,
93
+ DEFAULT_QUORUM,
94
+ PHASE_ORDER,
95
+ } from "./types.js";
96
+ export type {
97
+ CommitResult,
98
+ ConsensusOpts,
99
+ ConsensusStatus,
100
+ Decision,
101
+ Phase,
102
+ ProposeResult,
103
+ Proposal,
104
+ ProposalSnapshot,
105
+ ProposalStatus,
106
+ Vote,
107
+ VoteResult,
108
+ ViewChangeResult,
109
+ } from "./types.js";
110
+
111
+ // ─── Process-wide singleton ───────────────────────────────────────────
112
+
113
+ /**
114
+ * The shared consensus singleton used by the MCP tool. Initialized
115
+ * with the Bizar default 5-peer roster (odin / frigg / vor / mimir /
116
+ * heimdall — the same five Norse agents F-033 routes between) and a
117
+ * deterministic seed so MCP-driven consensus is reproducible across
118
+ * restarts. Tests should call `createConsensus()` with their own
119
+ * `localAgentId` + `peers` to get an isolated instance.
120
+ *
121
+ * The roster here is intentional — it maps to the existing 5-agent
122
+ * surface F-033 already exposes, so a single MCP call can drive the
123
+ * same set of agents the orchestrator routes to.
124
+ */
125
+ const DEFAULT_PEERS: readonly string[] = ["odin", "frigg", "vor", "mimir", "heimdall"];
126
+ const DEFAULT_LOCAL_AGENT = "odin";
127
+ const DEFAULT_PROPOSER_SEED = "bizar-f039-default";
128
+
129
+ let _shared: ConsensusHandle | null = null;
130
+
131
+ export function getSharedConsensus(): ConsensusHandle {
132
+ if (_shared) return _shared;
133
+ _shared = createConsensus({
134
+ localAgentId: DEFAULT_LOCAL_AGENT,
135
+ peers: [...DEFAULT_PEERS],
136
+ quorum: DEFAULT_QUORUM,
137
+ maxFaults: DEFAULT_MAX_FAULTS,
138
+ proposerSeed: DEFAULT_PROPOSER_SEED,
139
+ historyLimit: 50,
140
+ });
141
+ return _shared;
142
+ }
143
+
144
+ /** Reset the singleton — test/dev only. */
145
+ export function resetSharedConsensus(): void {
146
+ _shared = null;
147
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * consensus/queen.ts — Proposer election (QueenCoordinator).
3
+ *
4
+ * F-039 — thin port of ruflo's capability-scored QueenCoordinator
5
+ * (v3/@claude-flow/swarm/src/coordination/queen-coordinator.ts) for
6
+ * the consensus layer's proposer selection. Bizar needs a deterministic,
7
+ * fault-aware round-robin; we don't port the full ReasoningBank pattern
8
+ * matching because Bizar's review steps don't have agent capability
9
+ * vectors — they have peer rosters + a fault log.
10
+ *
11
+ * Algorithm: round-robin weighted by recent fault count. A peer with
12
+ * N faults is skipped N times before becoming proposer again. With
13
+ * `proposerSeed`, the initial head is computed deterministically so
14
+ * tests can pin it.
15
+ */
16
+
17
+ export interface QueenCoordinatorOpts {
18
+ /** Full peer roster, in the order round-robin walks them. Required. */
19
+ peers: string[];
20
+ /** Maximum Byzantine faults the cluster can tolerate (used as the
21
+ * skip-multiplier floor). Defaults to 1. */
22
+ maxFaults?: number;
23
+ /** Optional seed — when set, the initial head is `peers[seedHash % peers.length]`. */
24
+ seed?: string;
25
+ }
26
+
27
+ /**
28
+ * Tracks per-peer fault counts and walks the peer list in
29
+ * round-robin order, skipping any peer whose `faultCount` is greater
30
+ * than the candidate's expected count at that position.
31
+ *
32
+ * Concretely: if peer X has faultCount 2, the next 2 round-robin
33
+ * positions that would land on X skip it instead and the proposer
34
+ * becomes the peer at the next non-skipped position. After 2
35
+ * `advance()` calls (or `selectProposer` calls that consumed a skip),
36
+ * X is eligible again.
37
+ *
38
+ * The fault penalty decays as the round-robin walks — this keeps
39
+ * the cluster moving even when one peer has accumulated many faults.
40
+ */
41
+ export class QueenCoordinator {
42
+ private readonly peers: readonly string[];
43
+ /** Maximum Byzantine faults the cluster is sized for (used as the
44
+ * skip-multiplier floor — `recordFault` adds a skip of size
45
+ * `maxFaults` so the worst peer is skipped `maxFaults` rounds). */
46
+ private readonly maxFaults: number;
47
+ /** Per-peer cumulative fault count. */
48
+ private readonly faults: Map<string, number> = new Map();
49
+ /** Per-peer remaining-skip count (decremented on every advance). */
50
+ private readonly skips: Map<string, number> = new Map();
51
+ /** Current head of the round-robin walk. */
52
+ private cursor = 0;
53
+
54
+ constructor(opts: QueenCoordinatorOpts) {
55
+ if (!opts || !Array.isArray(opts.peers) || opts.peers.length === 0) {
56
+ throw new Error("QueenCoordinator: `peers` is required and must be non-empty");
57
+ }
58
+ const seen = new Set<string>();
59
+ for (const p of opts.peers) {
60
+ if (typeof p !== "string" || p.length === 0) {
61
+ throw new Error("QueenCoordinator: every peer id must be a non-empty string");
62
+ }
63
+ if (seen.has(p)) {
64
+ throw new Error(`QueenCoordinator: duplicate peer id "${p}"`);
65
+ }
66
+ seen.add(p);
67
+ this.faults.set(p, 0);
68
+ this.skips.set(p, 0);
69
+ }
70
+ this.peers = [...opts.peers];
71
+ this.maxFaults = Math.max(0, opts.maxFaults ?? 1);
72
+ if (opts.seed) {
73
+ this.cursor = this.seedCursor(opts.seed);
74
+ }
75
+ }
76
+
77
+ /** Current proposer (head of the round-robin walk). */
78
+ currentProposer(): string {
79
+ return this.peers[this.cursor % this.peers.length] as string;
80
+ }
81
+
82
+ /** Record a fault against a peer (the proposer just voted `no`, or
83
+ * missed its pre-prepare window). Increments both the cumulative
84
+ * fault log and the per-peer skip counter — the peer is then
85
+ * skipped for `maxFaults` round-robin steps before becoming
86
+ * proposer-eligible again. With `maxFaults = 1` (the F-039
87
+ * default), one fault skips the peer for one round. */
88
+ recordFault(peerId: string): void {
89
+ if (!this.faults.has(peerId)) {
90
+ throw new Error(`recordFault: unknown peer "${peerId}"`);
91
+ }
92
+ this.faults.set(peerId, (this.faults.get(peerId) ?? 0) + 1);
93
+ this.skips.set(peerId, (this.skips.get(peerId) ?? 0) + this.maxFaults);
94
+ }
95
+
96
+ /**
97
+ * Walk one step forward. Skips any peer whose remaining-skip count
98
+ * is > 0 (decrementing the counter as it skips). Returns the new
99
+ * proposer.
100
+ */
101
+ advance(): string {
102
+ const n = this.peers.length;
103
+ let steps = 0;
104
+ while (steps < n) {
105
+ this.cursor = (this.cursor + 1) % n;
106
+ const candidate = this.peers[this.cursor] as string;
107
+ const skip = this.skips.get(candidate) ?? 0;
108
+ if (skip > 0) {
109
+ this.skips.set(candidate, skip - 1);
110
+ steps++;
111
+ continue;
112
+ }
113
+ return candidate;
114
+ }
115
+ // Everyone had skip > 0 — we walked the full circle and decremented
116
+ // everyone. Just return where we landed.
117
+ return this.peers[this.cursor] as string;
118
+ }
119
+
120
+ /**
121
+ * Equivalent to `selectProposer(peers, recentFaults)` from the F-039
122
+ * spec — but stateful, because we own the fault log. Returns the
123
+ * current proposer without advancing.
124
+ */
125
+ selectProposer(): string {
126
+ // If the current proposer has a non-zero skip, walk forward to find
127
+ // the next eligible peer.
128
+ const n = this.peers.length;
129
+ let probe = this.cursor;
130
+ for (let i = 0; i < n; i++) {
131
+ const candidate = this.peers[probe] as string;
132
+ const skip = this.skips.get(candidate) ?? 0;
133
+ if (skip === 0) return candidate;
134
+ probe = (probe + 1) % n;
135
+ }
136
+ // All peers have skip > 0 — same fallback as advance().
137
+ return this.peers[this.cursor] as string;
138
+ }
139
+
140
+ /** Cumulative fault counts (read-only snapshot). */
141
+ faultCounts(): Record<string, number> {
142
+ return Object.fromEntries(this.faults);
143
+ }
144
+
145
+ /** Number of peers currently under a skip penalty. */
146
+ pendingSkipCount(): number {
147
+ let n = 0;
148
+ for (const v of this.skips.values()) if (v > 0) n++;
149
+ return n;
150
+ }
151
+
152
+ /** Test/dev helper: drop the cursor and fault counts. */
153
+ reset(): void {
154
+ this.cursor = 0;
155
+ for (const k of this.faults.keys()) this.faults.set(k, 0);
156
+ for (const k of this.skips.keys()) this.skips.set(k, 0);
157
+ }
158
+
159
+ // ─── Helpers ────────────────────────────────────────────────────────
160
+
161
+ /**
162
+ * Stable hash of an optional seed string → integer cursor in
163
+ * `[0, peers.length)`. Lets a test caller pin the initial proposer
164
+ * for replay safety (F-039 determinism rule).
165
+ */
166
+ private seedCursor(seed: string): number {
167
+ let h = 5381;
168
+ for (let i = 0; i < seed.length; i++) {
169
+ h = ((h << 5) + h + seed.charCodeAt(i)) >>> 0;
170
+ }
171
+ return h % this.peers.length;
172
+ }
173
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * consensus/types.ts — Shared type definitions for the in-process
3
+ * Byzantine consensus layer.
4
+ *
5
+ * F-039 — thin port of ruflo's `v3/@claude-flow/swarm/src/consensus/byzantine.ts`
6
+ * for Bizar's review/decision steps. We carry the full PBFT phase
7
+ * vocabulary (pre-prepare / prepare / commit / reply) and a 3-of-5
8
+ * simple-majority quorum — enough surface for Bizar to gate
9
+ * multi-agent review decisions without the operational cost of full
10
+ * PBFT (no transport, no crypto signatures, no checkpoint protocol).
11
+ *
12
+ * Determinism rule: quorum math is purely set-based. The only place
13
+ * randomness (or ordering) enters is the optional round-robin
14
+ * proposer election in `queen.ts`, and even there the caller can
15
+ * pass an explicit seed for replay safety.
16
+ */
17
+
18
+ // ─── Phase vocabulary ──────────────────────────────────────────────────
19
+
20
+ /**
21
+ * PBFT-style phase identifiers. Each proposal walks
22
+ * `pre-prepare → prepare → commit → reply` in order.
23
+ */
24
+ export type Phase =
25
+ | "pre-prepare"
26
+ | "prepare"
27
+ | "commit"
28
+ | "reply";
29
+
30
+ export const PHASE_ORDER: readonly Phase[] = [
31
+ "pre-prepare",
32
+ "prepare",
33
+ "commit",
34
+ "reply",
35
+ ] as const;
36
+
37
+ /** Decision a peer casts during the `prepare` phase. */
38
+ export type Decision = "yes" | "no" | "abstain";
39
+
40
+ // ─── Messages ──────────────────────────────────────────────────────────
41
+
42
+ /**
43
+ * A vote cast by a peer during the `prepare` phase. The signature
44
+ * field is a free-form string — we don't enforce cryptographic
45
+ * signatures in the thin port (no transport), but callers that
46
+ * wire in a transport can stuff an HMAC in here without changing
47
+ * the shape.
48
+ */
49
+ export interface Vote {
50
+ agentId: string;
51
+ phase: Extract<Phase, "prepare">;
52
+ decision: Decision;
53
+ signature?: string;
54
+ /** Wall-clock timestamp at which the peer emitted the vote. */
55
+ timestamp: string;
56
+ }
57
+
58
+ /**
59
+ * A proposal is the unit of agreement. `proposer` is the agent that
60
+ * initiated pre-prepare; `quorum` is the number of matching `yes`
61
+ * votes required for commit (3-of-5 simple majority by default).
62
+ */
63
+ export interface Proposal {
64
+ id: string;
65
+ payload: unknown;
66
+ proposer: string;
67
+ quorum: number;
68
+ /** Total peer count this proposal was negotiated against (incl. proposer). */
69
+ peerCount: number;
70
+ /** Current phase, drives which transitions are legal. */
71
+ phase: Phase;
72
+ /** Set of agentIds that have voted `yes` in `prepare`. */
73
+ approvals: Set<string>;
74
+ /** Set of agentIds that have voted `no` in `prepare`. */
75
+ rejections: Set<string>;
76
+ /** Set of agentIds that have voted `abstain`. */
77
+ abstentions: Set<string>;
78
+ /** Free-form reason for `view-change` (set if status === 'view-change'). */
79
+ viewChangeReason?: string;
80
+ /** Set when the proposal is committed. */
81
+ committedAt?: string;
82
+ /** Set when the proposal is finalized (reply phase). */
83
+ finalizedAt?: string;
84
+ /** Wall-clock timestamp at which the proposer started pre-prepare. */
85
+ createdAt: string;
86
+ }
87
+
88
+ export type ProposalStatus =
89
+ | "pending"
90
+ | "pre-prepare"
91
+ | "prepare"
92
+ | "commit"
93
+ | "reply"
94
+ | "view-change"
95
+ | "committed"
96
+ | "rejected"
97
+ | "expired";
98
+
99
+ /** A snapshot of a proposal for the public `status()` query. */
100
+ export interface ProposalSnapshot {
101
+ id: string;
102
+ status: ProposalStatus;
103
+ phase: Phase;
104
+ proposer: string;
105
+ quorum: number;
106
+ peerCount: number;
107
+ approvals: number;
108
+ rejections: number;
109
+ abstentions: number;
110
+ createdAt: string;
111
+ committedAt?: string;
112
+ finalizedAt?: string;
113
+ viewChangeReason?: string;
114
+ payload: unknown;
115
+ }
116
+
117
+ // ─── Quorum ────────────────────────────────────────────────────────────
118
+
119
+ /** 3-of-5 simple majority quorum, per the F-039 spec. */
120
+ export const DEFAULT_QUORUM = 3;
121
+ export const DEFAULT_PEER_COUNT = 5;
122
+
123
+ /** The default fault tolerance — at most 1 Byzantine peer per cluster. */
124
+ export const DEFAULT_MAX_FAULTS = 1;
125
+
126
+ // ─── Consensus status (returned by `status()`) ─────────────────────────
127
+
128
+ export interface ConsensusStatus {
129
+ /** All proposals the instance has processed, in arrival order. */
130
+ proposals: ProposalSnapshot[];
131
+ /** Current proposer (head of the round-robin schedule, weighted by faults). */
132
+ currentProposer: string;
133
+ /** View number — increments on every view-change. */
134
+ viewNumber: number;
135
+ /** Total proposals ever committed (status === 'committed'). */
136
+ committed: number;
137
+ /** Total proposals ever rejected (status === 'rejected'). */
138
+ rejected: number;
139
+ /** Total proposals ever expired (status === 'expired'). */
140
+ expired: number;
141
+ /** Total view-changes triggered by a faulty proposer. */
142
+ viewChanges: number;
143
+ }
144
+
145
+ // ─── Constructor / orchestrator options ─────────────────────────────────
146
+
147
+ export interface ConsensusOpts {
148
+ /** Local agent id — votes cast by `localAgentId` are short-circuited
149
+ * to `prepare`. Required. */
150
+ localAgentId: string;
151
+ /** Full peer roster (including `localAgentId`). Order matters for
152
+ * round-robin proposer election. Required. */
153
+ peers: string[];
154
+ /** Quorum threshold for `prepare → commit`. Defaults to 3. */
155
+ quorum?: number;
156
+ /** Maximum Byzantine faults the cluster is sized for. Defaults to 1. */
157
+ maxFaults?: number;
158
+ /** Wall-clock timeout (ms) after which a pending proposal expires. */
159
+ /** Optional seed for round-robin proposer election — makes proposer
160
+ * selection deterministic for tests. If absent, round-robin walks
161
+ * the peer list in the order given. */
162
+ proposerSeed?: string;
163
+ /** A clock function for deterministic timestamps in tests. */
164
+ now?: () => Date;
165
+ }
166
+
167
+ // ─── Public result shapes ──────────────────────────────────────────────
168
+
169
+ export interface ProposeResult {
170
+ proposalId: string;
171
+ status: ProposalStatus;
172
+ phase: Phase;
173
+ }
174
+
175
+ export interface VoteResult {
176
+ proposalId: string;
177
+ status: ProposalStatus;
178
+ phase: Phase;
179
+ approvals: number;
180
+ rejections: number;
181
+ abstentions: number;
182
+ committed: boolean;
183
+ }
184
+
185
+ export interface CommitResult {
186
+ proposalId: string;
187
+ status: ProposalStatus;
188
+ committedAt: string;
189
+ finalizedAt: string;
190
+ approvals: number;
191
+ }
192
+
193
+ export interface ViewChangeResult {
194
+ previousProposer: string;
195
+ newProposer: string;
196
+ viewNumber: number;
197
+ reason: string;
198
+ proposalId: string;
199
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * federation/audit.ts — AuditService: NDJSON log of every
3
+ * federation decision.
4
+ *
5
+ * F-038 — ported from ruflo
6
+ * `v3/@claude-flow/plugin-agent-federation/src/domain/services/audit-service.ts`
7
+ * stripped to the parts the skeleton needs:
8
+ *
9
+ * - record(envelope, decision) appends ONE NDJSON line to
10
+ * `.harness/federation-audit.log`.
11
+ * - When the file exceeds 10 MB, it rotates to
12
+ * `.harness/federation-audit.log.1` (single generation).
13
+ * - tail() reads the last N lines for the `federation_status` MCP tool.
14
+ * - size() returns the on-disk file size for the status payload.
15
+ *
16
+ * Writes are synchronous (fs.appendFileSync) — the skeleton is
17
+ * in-process and not throughput-bound; an async path would only
18
+ * complicate testing.
19
+ *
20
+ * The `.harness/` directory is created on first write. The audit
21
+ * log file is added to `.gitignore` by the Makefile/install step.
22
+ */
23
+
24
+ import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, renameSync, statSync } from "node:fs";
25
+ import { dirname } from "node:path";
26
+
27
+ /** Default audit log path — colocated with the rest of `.harness/`. */
28
+ export const DEFAULT_AUDIT_PATH = ".harness/federation-audit.log";
29
+
30
+ /** Rotate when the file exceeds this size. */
31
+ const ROTATION_THRESHOLD_BYTES = 10 * 1024 * 1024; // 10 MB
32
+
33
+ /** Tail size cap — protects against reading gigabytes into memory. */
34
+ const TAIL_MAX_BYTES = 1 * 1024 * 1024; // 1 MB
35
+
36
+ /** Decisions recorded by the orchestrator — matches the schema the
37
+ * `federation_status` MCP tool reads back. */
38
+ export type AuditDecision =
39
+ | { allowed: true; reason: string }
40
+ | { allowed: false; reason: string };
41
+
42
+ export interface AuditEntry {
43
+ readonly ts: string;
44
+ readonly envelopeId: string;
45
+ readonly sourceNodeId: string;
46
+ readonly targetNodeId: string;
47
+ readonly messageType: string;
48
+ readonly nonce: string;
49
+ readonly allowed: boolean;
50
+ readonly reason: string;
51
+ readonly layer: "trust" | "policy" | "hmac" | "budget" | "received" | "sent";
52
+ readonly nodeId: string;
53
+ }
54
+
55
+ export interface AuditServiceOpts {
56
+ /** Override the audit file path (defaults to .harness/federation-audit.log). */
57
+ readonly path?: string;
58
+ /** Local nodeId stamped into every entry. */
59
+ readonly nodeId: string;
60
+ }
61
+
62
+ export class AuditService {
63
+ private readonly path: string;
64
+ private readonly nodeId: string;
65
+ private rotated = false;
66
+
67
+ constructor(opts: AuditServiceOpts) {
68
+ if (!opts || typeof opts.nodeId !== "string" || opts.nodeId.length === 0) {
69
+ throw new Error("AuditService: nodeId is required");
70
+ }
71
+ this.path = opts.path ?? DEFAULT_AUDIT_PATH;
72
+ this.nodeId = opts.nodeId;
73
+ }
74
+
75
+ getPath(): string {
76
+ return this.path;
77
+ }
78
+
79
+ /** Append an NDJSON line. Triggers rotation if the file would
80
+ * exceed the threshold after the write. */
81
+ record(entry: Omit<AuditEntry, "nodeId">): void {
82
+ const full: AuditEntry = { ...entry, nodeId: this.nodeId };
83
+ const dir = dirname(this.path);
84
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
85
+
86
+ // Pre-write rotation check: if appending would exceed the
87
+ // threshold AND we haven't already rotated this session, rotate
88
+ // first. We keep a single generation (`.log.1`); older logs
89
+ // are the operator's responsibility (or a separate cron).
90
+ if (!this.rotated && existsSync(this.path)) {
91
+ try {
92
+ const size = statSync(this.path).size;
93
+ if (size + 256 > ROTATION_THRESHOLD_BYTES) {
94
+ this.rotate();
95
+ }
96
+ } catch {
97
+ // stat failed (file gone between check and write) — fall through.
98
+ }
99
+ }
100
+
101
+ appendFileSync(this.path, JSON.stringify(full) + "\n", "utf-8");
102
+ }
103
+
104
+ /** Rotate the current log to `.log.1`. Subsequent writes go to a
105
+ * fresh `.log` file. Idempotent within a single service lifetime. */
106
+ rotate(): void {
107
+ const target = `${this.path}.1`;
108
+ if (existsSync(this.path)) {
109
+ try { renameSync(this.path, target); }
110
+ catch { /* ignore — best-effort */ }
111
+ }
112
+ this.rotated = true;
113
+ }
114
+
115
+ /** On-disk size in bytes. 0 if file missing. */
116
+ size(): number {
117
+ try {
118
+ return existsSync(this.path) ? statSync(this.path).size : 0;
119
+ } catch {
120
+ return 0;
121
+ }
122
+ }
123
+
124
+ /** Read the last `n` entries from the log. Bounded by TAIL_MAX_BYTES. */
125
+ tail(n: number = 50): AuditEntry[] {
126
+ if (!existsSync(this.path)) return [];
127
+ let raw: string;
128
+ try {
129
+ const stats = statSync(this.path);
130
+ const start = Math.max(0, stats.size - TAIL_MAX_BYTES);
131
+ const buffer = Buffer.alloc(stats.size - start);
132
+ // Read the last TAIL_MAX_BYTES via a small open/read/close dance.
133
+ const handle = openSync(this.path, "r");
134
+ try { readSync(handle, buffer, 0, buffer.length, start); }
135
+ finally { closeSync(handle); }
136
+ raw = buffer.toString("utf-8");
137
+ } catch {
138
+ raw = readFileSync(this.path, "utf-8");
139
+ }
140
+ const lines = raw.split("\n").filter((l) => l.length > 0);
141
+ const slice = lines.slice(-n);
142
+ const out: AuditEntry[] = [];
143
+ for (const line of slice) {
144
+ try { out.push(JSON.parse(line) as AuditEntry); }
145
+ catch { /* malformed line — skip */ }
146
+ }
147
+ return out;
148
+ }
149
+
150
+ /** Wipe the audit log. Test-only helper. */
151
+ reset(): void {
152
+ this.rotated = false;
153
+ try { if (existsSync(this.path)) renameSync(this.path, `${this.path}.deleted`); }
154
+ catch { /* ignore */ }
155
+ }
156
+ }