@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,400 @@
1
+ /**
2
+ * cli/feature-list-bridge.mjs
3
+ *
4
+ * F-035 (MetaHarness) — wraps `feature_list.json` with claim semantics.
5
+ *
6
+ * Source-of-truth port from
7
+ * ruflo/v3/@claude-flow/cli/src/services/claim-service.ts (ClaimService,
8
+ * ADR-016) — issues.ts is the CLI surface we mirror.
9
+ *
10
+ * Status state machine:
11
+ * unclaimed -> claimed
12
+ * claimed -> active (start work)
13
+ * active -> paused
14
+ * paused -> active
15
+ * active -> handoff-pending (initiated)
16
+ * handoff-pending -> claimed (accepted by recipient)
17
+ * active -> blocked (caller marked a blocker)
18
+ * blocked -> active
19
+ * {claimed,active,paused,blocked,handoff-pending}
20
+ * -> completed
21
+ * {claimed,active,...} -> unclaimed (release)
22
+ *
23
+ * Steal reasons (per spec): overloaded | stale | blocked-timeout | voluntary
24
+ *
25
+ * Persistence model: we ADD the following fields on each feature entry —
26
+ * claimant : { type: 'human'|'agent', id, name }
27
+ * claimStatus : ClaimStatus
28
+ * claimedAt : ISO timestamp
29
+ * claimHistory : Array<{ event, claimant?, ts, reason? }>
30
+ *
31
+ * We MUST NOT modify the existing `vcr` / `passing` semantics of
32
+ * feature_list.json — claims are additive fields only.
33
+ *
34
+ * File writes are atomic via the standard `writeFileSync` to .tmp +
35
+ * `renameSync` pattern.
36
+ */
37
+
38
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, statSync, copyFileSync } from 'node:fs';
39
+ import { dirname, join, resolve } from 'node:path';
40
+ import { randomUUID } from 'node:crypto';
41
+
42
+ // ── Constants ────────────────────────────────────────────────────────
43
+
44
+ export const CLAIM_STATUSES = Object.freeze([
45
+ 'unclaimed',
46
+ 'claimed',
47
+ 'active',
48
+ 'paused',
49
+ 'handoff-pending',
50
+ 'blocked',
51
+ 'completed',
52
+ ]);
53
+
54
+ export const STEAL_REASONS = Object.freeze([
55
+ 'overloaded',
56
+ 'stale',
57
+ 'blocked-timeout',
58
+ 'voluntary',
59
+ ]);
60
+
61
+ // Transitions the bridge will allow without complaint. Anything else
62
+ // emits a typed error.
63
+ const ALLOWED_TRANSITIONS = {
64
+ unclaimed: new Set(['claimed']),
65
+ claimed: new Set(['active', 'unclaimed', 'completed', 'handoff-pending']),
66
+ active: new Set(['paused', 'blocked', 'completed', 'handoff-pending', 'unclaimed']),
67
+ paused: new Set(['active', 'unclaimed', 'completed']),
68
+ 'handoff-pending': new Set(['claimed', 'unclaimed']),
69
+ blocked: new Set(['active', 'unclaimed']),
70
+ completed: new Set([]),
71
+ };
72
+
73
+ // ── Errors ───────────────────────────────────────────────────────────
74
+
75
+ export class ClaimError extends Error {
76
+ constructor(code, message) {
77
+ super(message || code);
78
+ this.name = 'ClaimError';
79
+ this.code = code;
80
+ }
81
+ }
82
+
83
+ // ── Helpers ──────────────────────────────────────────────────────────
84
+
85
+ function nowIso() {
86
+ return new Date().toISOString();
87
+ }
88
+
89
+ function readJsonSafe(file) {
90
+ try {
91
+ if (!existsSync(file)) return null;
92
+ const txt = readFileSync(file, 'utf8');
93
+ return JSON.parse(txt);
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ function writeJsonAtomic(file, data) {
100
+ mkdirSync(dirname(file), { recursive: true });
101
+ const tmp = `${file}.tmp.${randomUUID()}`;
102
+ writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
103
+ renameSync(tmp, file);
104
+ }
105
+
106
+ function appendHistory(feature, event, extra) {
107
+ const prev = Array.isArray(feature.claimHistory) ? feature.claimHistory : [];
108
+ feature.claimHistory = [...prev, { event, ts: nowIso(), ...(extra || {}) }];
109
+ }
110
+
111
+ function setClaim(feature, status, claimant, reason) {
112
+ feature.claimant = claimant || null;
113
+ feature.claimStatus = status;
114
+ if (status === 'claimed' || status === 'active' || status === 'paused' || status === 'handoff-pending' || status === 'blocked') {
115
+ if (!feature.claimedAt) feature.claimedAt = nowIso();
116
+ }
117
+ if (status === 'unclaimed' || status === 'completed') {
118
+ // Keep claimedAt as historical record.
119
+ }
120
+ if (reason) feature.claimReason = reason;
121
+ }
122
+
123
+ function assertStatus(s) {
124
+ if (!CLAIM_STATUSES.includes(s)) {
125
+ throw new ClaimError('UNKNOWN_STATUS', `Unknown claim status: ${s}`);
126
+ }
127
+ }
128
+
129
+ function assertReason(r) {
130
+ if (!STEAL_REASONS.includes(r)) {
131
+ throw new ClaimError('UNKNOWN_STEAL_REASON', `Unknown steal reason: ${r}`);
132
+ }
133
+ }
134
+
135
+ // ── FeatureListBridge ────────────────────────────────────────────────
136
+
137
+ export class FeatureListBridge {
138
+ /**
139
+ * @param {object|string} opts Either a path string or `{ filePath, backup }`.
140
+ * - filePath: path to feature_list.json (default: ./feature_list.json)
141
+ * - backup : write a .bak copy before each write (default: true)
142
+ */
143
+ constructor(opts = {}) {
144
+ if (typeof opts === 'string') {
145
+ this.filePath = resolve(opts);
146
+ } else {
147
+ this.filePath = resolve(opts.filePath || 'feature_list.json');
148
+ this.backup = opts.backup !== false;
149
+ }
150
+ }
151
+
152
+ /** Load the feature list from disk. Returns the parsed JSON. */
153
+ load() {
154
+ const data = readJsonSafe(this.filePath);
155
+ if (!data) {
156
+ throw new ClaimError('NO_FILE', `feature_list.json not found at ${this.filePath}`);
157
+ }
158
+ if (!Array.isArray(data.features)) {
159
+ throw new ClaimError('SCHEMA', 'feature_list.json missing top-level features[]');
160
+ }
161
+ return data;
162
+ }
163
+
164
+ /** Save + atomic temp-rename. Creates a .bak if `backup=true`. */
165
+ save(data, { reason = 'update' } = {}) {
166
+ if (this.backup && existsSync(this.filePath)) {
167
+ try { copyFileSync(this.filePath, `${this.filePath}.bak`); } catch { /* best-effort */ }
168
+ }
169
+ writeJsonAtomic(this.filePath, data);
170
+ return { ok: true, path: this.filePath, reason };
171
+ }
172
+
173
+ _find(data, featureId) {
174
+ const idx = data.features.findIndex((f) => f.id === featureId);
175
+ if (idx === -1) {
176
+ throw new ClaimError('NOT_FOUND', `Feature ${featureId} not in feature_list.json`);
177
+ }
178
+ return idx;
179
+ }
180
+
181
+ _getOrThrow(data, featureId) {
182
+ return data.features[this._find(data, featureId)];
183
+ }
184
+
185
+ // ── claim ─────────────────────────────────────────────────────────
186
+
187
+ /**
188
+ * @param {string} featureId
189
+ * @param {{type:'human'|'agent', id:string, name?:string}} claimant
190
+ * @param {string} reason
191
+ * @returns {{ ok: true, feature: object }}
192
+ */
193
+ claim(featureId, claimant, reason) {
194
+ if (!claimant || (claimant.type !== 'human' && claimant.type !== 'agent')) {
195
+ throw new ClaimError('BAD_CLAIMANT', 'claimant must be {type:"human"|"agent", id, name?}');
196
+ }
197
+ const data = this.load();
198
+ const feature = this._getOrThrow(data, featureId);
199
+ const prev = feature.claimant || null;
200
+ const prevStatus = feature.claimStatus || 'unclaimed';
201
+
202
+ if (prevStatus !== 'unclaimed' && prevStatus !== 'completed') {
203
+ throw new ClaimError(
204
+ 'ALREADY_CLAIMED',
205
+ `Feature ${featureId} is ${prevStatus} by ${prev?.name || prev?.id || 'unknown'} — release or steal first.`,
206
+ );
207
+ }
208
+
209
+ feature.claimant = claimant;
210
+ feature.claimStatus = 'claimed';
211
+ feature.claimedAt = nowIso();
212
+ if (reason) feature.claimReason = reason;
213
+ appendHistory(feature, 'claim', { claimant, reason });
214
+
215
+ this.save(data, { reason: `claim:${featureId}` });
216
+ return { ok: true, feature, previousClaimant: prev };
217
+ }
218
+
219
+ // ── release ───────────────────────────────────────────────────────
220
+
221
+ /**
222
+ * @param {string} featureId
223
+ * @param {{id:string}} claimant expected to match the current claimant
224
+ * @param {string} reason
225
+ */
226
+ release(featureId, claimant, reason) {
227
+ const data = this.load();
228
+ const feature = this._getOrThrow(data, featureId);
229
+ const prevStatus = feature.claimStatus || 'unclaimed';
230
+ if (prevStatus === 'unclaimed' || prevStatus === 'completed') {
231
+ throw new ClaimError(
232
+ 'NOT_CLAIMED',
233
+ `Feature ${featureId} is ${prevStatus}; nothing to release.`,
234
+ );
235
+ }
236
+ if (claimant && feature.claimant && feature.claimant.id !== claimant.id) {
237
+ throw new ClaimError(
238
+ 'WRONG_CLAIMANT',
239
+ `Feature ${featureId} is claimed by ${feature.claimant.name || feature.claimant.id}, not ${claimant.name || claimant.id}.`,
240
+ );
241
+ }
242
+ const prev = feature.claimant;
243
+ feature.claimStatus = 'unclaimed';
244
+ appendHistory(feature, 'release', { releasedBy: claimant, reason });
245
+ // Keep claimant for historical record (the entry's `claimHistory`
246
+ // captures who held it); but flip `claimant` to null so further
247
+ // list({status: 'claimed'}) filters don't pick it up accidentally.
248
+ feature.claimant = null;
249
+
250
+ this.save(data, { reason: `release:${featureId}` });
251
+ return { ok: true, feature, previousClaimant: prev };
252
+ }
253
+
254
+ // ── handoff ───────────────────────────────────────────────────────
255
+
256
+ /**
257
+ * Hand off the feature from one claimant to another.
258
+ * The recipient must be a valid claimant shape.
259
+ */
260
+ handoff(featureId, fromClaimant, toClaimant, reason) {
261
+ if (!toClaimant || (toClaimant.type !== 'human' && toClaimant.type !== 'agent')) {
262
+ throw new ClaimError('BAD_CLAIMANT', 'toClaimant must be {type:"human"|"agent", id, name?}');
263
+ }
264
+ const data = this.load();
265
+ const feature = this._getOrThrow(data, featureId);
266
+ const prevStatus = feature.claimStatus || 'unclaimed';
267
+ if (prevStatus === 'unclaimed' || prevStatus === 'completed') {
268
+ throw new ClaimError('NOT_CLAIMED', `Feature ${featureId} is ${prevStatus}; cannot handoff.`);
269
+ }
270
+ if (fromClaimant && feature.claimant && feature.claimant.id !== fromClaimant.id) {
271
+ throw new ClaimError(
272
+ 'WRONG_CLAIMANT',
273
+ `Feature ${featureId} is held by ${feature.claimant.name || feature.claimant.id}, not ${fromClaimant.name || fromClaimant.id}.`,
274
+ );
275
+ }
276
+
277
+ appendHistory(feature, 'handoff-initiated', { from: feature.claimant, to: toClaimant, reason });
278
+ feature.handoffTo = toClaimant;
279
+ feature.claimStatus = 'handoff-pending';
280
+
281
+ // Synchronous accept per the simplest semantics — the CLI flag
282
+ // `bizar claim handoff` accepts immediately. An async workflow
283
+ // would split this into handoff-initiate + handoff-accept, but
284
+ // v1 keeps it atomic.
285
+ feature.claimant = toClaimant;
286
+ feature.claimStatus = 'claimed';
287
+ feature.claimedAt = nowIso();
288
+ appendHistory(feature, 'handoff-accepted', { by: toClaimant, reason });
289
+ delete feature.handoffTo;
290
+
291
+ this.save(data, { reason: `handoff:${featureId}` });
292
+ return { ok: true, feature };
293
+ }
294
+
295
+ // ── steal ─────────────────────────────────────────────────────────
296
+
297
+ /**
298
+ * Steal a feature from the current claimant. Requires a `stealReason`
299
+ * (one of STEAL_REASONS). Records the event in `claimHistory`.
300
+ */
301
+ steal(featureId, newClaimant, reason) {
302
+ assertReason(reason);
303
+ if (!newClaimant || (newClaimant.type !== 'human' && newClaimant.type !== 'agent')) {
304
+ throw new ClaimError('BAD_CLAIMANT', 'newClaimant must be {type:"human"|"agent", id, name?}');
305
+ }
306
+ const data = this.load();
307
+ const feature = this._getOrThrow(data, featureId);
308
+ const prevStatus = feature.claimStatus || 'unclaimed';
309
+ if (prevStatus === 'unclaimed' || prevStatus === 'completed') {
310
+ throw new ClaimError('NOT_CLAIMED', `Feature ${featureId} is ${prevStatus}; nothing to steal.`);
311
+ }
312
+ const prev = feature.claimant;
313
+ appendHistory(feature, 'steal', { stolenFrom: prev, stolenBy: newClaimant, reason });
314
+ feature.claimant = newClaimant;
315
+ feature.claimStatus = 'claimed';
316
+ feature.claimedAt = nowIso();
317
+ feature.claimReason = reason;
318
+
319
+ this.save(data, { reason: `steal:${featureId}` });
320
+ return { ok: true, feature, previousClaimant: prev, stealReason: reason };
321
+ }
322
+
323
+ // ── transition ────────────────────────────────────────────────────
324
+
325
+ /**
326
+ * Generic status transition (active / paused / blocked / completed).
327
+ * Used by the CLI to mark a claim actively-in-progress, paused,
328
+ * blocked, or completed.
329
+ */
330
+ transition(featureId, newStatus, claimant, reason) {
331
+ assertStatus(newStatus);
332
+ const data = this.load();
333
+ const feature = this._getOrThrow(data, featureId);
334
+ const prevStatus = feature.claimStatus || 'unclaimed';
335
+ if (newStatus === prevStatus) {
336
+ return { ok: true, feature, unchanged: true };
337
+ }
338
+ const allowed = ALLOWED_TRANSITIONS[prevStatus] || new Set();
339
+ if (!allowed.has(newStatus)) {
340
+ throw new ClaimError(
341
+ 'BAD_TRANSITION',
342
+ `Cannot transition ${featureId}: ${prevStatus} → ${newStatus} not allowed.`,
343
+ );
344
+ }
345
+ if (claimant && feature.claimant && feature.claimant.id !== claimant.id) {
346
+ throw new ClaimError(
347
+ 'WRONG_CLAIMANT',
348
+ `Feature ${featureId} is held by ${feature.claimant.name || feature.claimant.id}, not ${claimant.name || claimant.id}.`,
349
+ );
350
+ }
351
+ appendHistory(feature, 'transition', { from: prevStatus, to: newStatus, by: claimant, reason });
352
+ feature.claimStatus = newStatus;
353
+
354
+ this.save(data, { reason: `transition:${featureId}:${newStatus}` });
355
+ return { ok: true, feature };
356
+ }
357
+
358
+ // ── list ──────────────────────────────────────────────────────────
359
+
360
+ /**
361
+ * List features, optionally filtered by status or claimant.
362
+ * @param {{status?:string|string[], claimantId?:string}} filters
363
+ */
364
+ list(filters = {}) {
365
+ const data = this.load();
366
+ let out = data.features.slice();
367
+ if (filters.status) {
368
+ const statuses = Array.isArray(filters.status) ? filters.status : [filters.status];
369
+ out = out.filter((f) => statuses.includes(f.claimStatus || 'unclaimed'));
370
+ }
371
+ if (filters.claimantId) {
372
+ out = out.filter((f) => f.claimant && f.claimant.id === filters.claimantId);
373
+ }
374
+ if (filters.claimedByHuman != null) {
375
+ out = out.filter((f) => Boolean(f.claimant) === Boolean(filters.claimedByHuman) && (!f.claimant || f.claimant.type === 'human'));
376
+ }
377
+ if (filters.claimedByAgent != null) {
378
+ out = out.filter((f) => Boolean(f.claimant) && f.claimant.type === 'agent');
379
+ }
380
+ return out;
381
+ }
382
+
383
+ /** Get a single feature with its full claim history. */
384
+ get(featureId) {
385
+ const data = this.load();
386
+ return this._getOrThrow(data, featureId);
387
+ }
388
+ }
389
+
390
+ // ── Path helper ──────────────────────────────────────────────────────
391
+
392
+ import { cwd } from 'node:process';
393
+
394
+ /** Default path under cwd / feature_list.json. */
395
+ export function defaultFeatureListPath() {
396
+ if (process.env.BIZAR_FEATURE_LIST && process.env.BIZAR_FEATURE_LIST.trim()) {
397
+ return resolve(process.env.BIZAR_FEATURE_LIST);
398
+ }
399
+ return join(cwd(), 'feature_list.json');
400
+ }
@@ -19,6 +19,23 @@ export const VALID_TYPES = [
19
19
  'task_summary',
20
20
  'session_summary',
21
21
  'user_preference',
22
+ // v6.4.0 — F-033 ReasoningBank distillation pipeline (ADR-174).
23
+ // `pattern` is emitted by `bizar-dash/src/server/memory-consolidator.mjs`
24
+ // when a distillation run produces a promoted pattern (oracle:test-exec
25
+ // or judge:fable tier, ≥1 contributing entry). Patterns are searchable
26
+ // and re-feed into subsequent distillation runs as plain inputs.
27
+ 'pattern',
28
+ ];
29
+
30
+ export const VALID_PROVENANCE_TIERS = [
31
+ // Tier-1 (oracle) — observed via test execution. Eligible for promotion.
32
+ 'oracle:test-exec',
33
+ // Tier-2 (proxy) — structural inference only. NOT eligible for
34
+ // promotion; written to the vault but `promoted: false`.
35
+ 'proxy:structural',
36
+ // Tier-3 (judge:fable) — cost-bounded LLM-judge path; requires
37
+ // `BIZAR_DISTILL_BUDGET_USD > 0`. Out of scope in the $0 default.
38
+ 'judge:fable',
22
39
  ];
23
40
 
24
41
  export const VALID_STATUSES = [
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cli/worker-dispatcher.mjs
4
+ *
5
+ * Bizar Background Workers — trigger-pattern dispatcher.
6
+ *
7
+ * Reads `config/trigger-patterns.json` (cached for the lifetime of the
8
+ * process) and matches user prompts against each worker's `regex` array.
9
+ * Returns up to `opts.maxSuggestions` (default 3) weighted matches, sorted
10
+ * by weight desc.
11
+ *
12
+ * The hook at `.claude/hooks/worker-suggest.mjs` calls `dispatch()` on
13
+ * every `UserPromptSubmit` event. Pure JS, no external deps; safe to
14
+ * import from either Node or Bun.
15
+ *
16
+ * Public API
17
+ * ──────────
18
+ * dispatch(text, { maxSuggestions }?) -> Array<{
19
+ * workerId: string,
20
+ * weight: number,
21
+ * skill: string | null,
22
+ * agent: string | null,
23
+ * description: string | undefined,
24
+ * matchedPattern: string, // the literal regex string that hit
25
+ * matchedIndex: number // the prompt char-offset of the match
26
+ * }>
27
+ *
28
+ * listWorkers() -> string[] // all worker ids (even with no regex)
29
+ *
30
+ * resetCache() -> void // force re-read of the JSON on next dispatch
31
+ *
32
+ * loadPatterns({ configPath? }?) -> object // explicit loader for tests
33
+ *
34
+ * Honesty note: ruflo's equivalent worker bodies are stubs
35
+ * (`await new Promise(r => setTimeout(r, …))`); this port is just a
36
+ * dispatcher — actual work happens in Bizar skills/agents invoked
37
+ * separately by the model.
38
+ */
39
+ 'use strict';
40
+
41
+ import { readFileSync, existsSync } from 'node:fs';
42
+ import { dirname, resolve, join } from 'node:path';
43
+ import { fileURLToPath } from 'node:url';
44
+
45
+ const __dirname = dirname(fileURLToPath(import.meta.url));
46
+
47
+ // ── Config path resolution ──────────────────────────────────────────────────
48
+
49
+ /**
50
+ * Locate `config/trigger-patterns.json`. Resolution order:
51
+ * 1. opts.configPath (explicit, used by tests)
52
+ * 2. BIZAR_TRIGGER_PATTERNS env var
53
+ * 3. <projectRoot>/config/trigger-patterns.json (projectRoot = parent of cli/)
54
+ */
55
+ export function defaultConfigPath() {
56
+ if (process.env.BIZAR_TRIGGER_PATTERNS) {
57
+ return process.env.BIZAR_TRIGGER_PATTERNS;
58
+ }
59
+ const projectRoot = resolve(__dirname, '..');
60
+ return join(projectRoot, 'config', 'trigger-patterns.json');
61
+ }
62
+
63
+ // ── Cached patterns + pre-compiled regexes ──────────────────────────────────
64
+
65
+ /** @type {Array<CompiledWorker> | null} */
66
+ let _cache = null;
67
+ /** @type {string | null} */
68
+ let _cachePath = null;
69
+
70
+ /**
71
+ * @typedef CompiledWorker
72
+ * @property {string} id
73
+ * @property {number} weight
74
+ * @property {string|null} skill
75
+ * @property {string|null} agent
76
+ * @property {string|undefined} description
77
+ * @property {Array<{ pattern: string, regex: RegExp }>} compiled
78
+ */
79
+
80
+ /**
81
+ * Load + compile the patterns file. Cached on first call.
82
+ * @param {{ configPath?: string }=} opts
83
+ * @returns {Array<CompiledWorker>}
84
+ */
85
+ export function loadPatterns(opts = {}) {
86
+ const path = opts.configPath || defaultConfigPath();
87
+ if (_cache && _cachePath === path) return _cache;
88
+
89
+ if (!existsSync(path)) {
90
+ // Empty dispatcher — no patterns means no suggestions, but the API
91
+ // remains available (so a missing config never throws at the hook).
92
+ _cache = [];
93
+ _cachePath = path;
94
+ return _cache;
95
+ }
96
+
97
+ let raw;
98
+ try {
99
+ raw = JSON.parse(readFileSync(path, 'utf8'));
100
+ } catch (err) {
101
+ // Invalid JSON is a config bug, not a runtime bug — log + fall back
102
+ // to an empty dispatcher so the hook still exits 0.
103
+ process.stderr.write(
104
+ `[bizar.workers] WARN: failed to parse ${path}: ${
105
+ err && err.message ? err.message : String(err)
106
+ }\n`,
107
+ );
108
+ _cache = [];
109
+ _cachePath = path;
110
+ return _cache;
111
+ }
112
+
113
+ const workers = Array.isArray(raw && raw.workers) ? raw.workers : [];
114
+
115
+ /** @type {Array<CompiledWorker>} */
116
+ const compiled = [];
117
+ for (const w of workers) {
118
+ if (!w || typeof w.id !== 'string') continue;
119
+ const regexSources = Array.isArray(w.regex) ? w.regex : [];
120
+ /** @type {Array<{ pattern: string, regex: RegExp }>} */
121
+ const compiled_ = [];
122
+ for (const src of regexSources) {
123
+ if (typeof src !== 'string' || src.length === 0) continue;
124
+ try {
125
+ compiled_.push({ pattern: src, regex: new RegExp(src, 'i') });
126
+ } catch {
127
+ // Skip invalid regex sources; never throw from the hook path.
128
+ }
129
+ }
130
+ compiled.push({
131
+ id: w.id,
132
+ weight: typeof w.weight === 'number' ? w.weight : 0.5,
133
+ skill: typeof w.skill === 'string' ? w.skill : null,
134
+ agent: typeof w.agent === 'string' ? w.agent : null,
135
+ description: typeof w.description === 'string' ? w.description : undefined,
136
+ compiled: compiled_,
137
+ });
138
+ }
139
+
140
+ _cache = compiled;
141
+ _cachePath = path;
142
+ return _cache;
143
+ }
144
+
145
+ /** Force re-read of the patterns JSON on the next `dispatch()` call. */
146
+ export function resetCache() {
147
+ _cache = null;
148
+ _cachePath = null;
149
+ }
150
+
151
+ // ── Public API ──────────────────────────────────────────────────────────────
152
+
153
+ const DEFAULT_MAX_SUGGESTIONS = 3;
154
+
155
+ /**
156
+ * Match a prompt against all worker patterns and return ranked suggestions.
157
+ *
158
+ * @param {string} text
159
+ * @param {{ maxSuggestions?: number, configPath?: string }=} opts
160
+ * @returns {Array<{
161
+ * workerId: string,
162
+ * weight: number,
163
+ * skill: string|null,
164
+ * agent: string|null,
165
+ * description: string|undefined,
166
+ * matchedPattern: string,
167
+ * matchedIndex: number
168
+ * }>}
169
+ */
170
+ export function dispatch(text, opts = {}) {
171
+ const out = [];
172
+ const source = typeof text === 'string' ? text : '';
173
+ if (source.length === 0) return out;
174
+
175
+ const workers = loadPatterns(
176
+ opts.configPath ? { configPath: opts.configPath } : {},
177
+ );
178
+ if (workers.length === 0) return out;
179
+
180
+ const max = Number.isInteger(opts.maxSuggestions)
181
+ ? Math.max(0, opts.maxSuggestions)
182
+ : DEFAULT_MAX_SUGGESTIONS;
183
+
184
+ for (const w of workers) {
185
+ let bestIndex = -1;
186
+ let bestPattern = null;
187
+ for (const c of w.compiled) {
188
+ const m = c.regex.exec(source);
189
+ if (m && m.index >= 0) {
190
+ if (bestIndex === -1 || m.index < bestIndex) {
191
+ bestIndex = m.index;
192
+ bestPattern = c.pattern;
193
+ }
194
+ }
195
+ }
196
+ if (bestPattern !== null) {
197
+ out.push({
198
+ workerId: w.id,
199
+ weight: w.weight,
200
+ skill: w.skill,
201
+ agent: w.agent,
202
+ description: w.description,
203
+ matchedPattern: bestPattern,
204
+ matchedIndex: bestIndex,
205
+ });
206
+ }
207
+ }
208
+
209
+ out.sort((a, b) => {
210
+ if (b.weight !== a.weight) return b.weight - a.weight;
211
+ // Tie-break deterministically by workerId so the output is stable
212
+ // across runs (useful for tests + logs).
213
+ return a.workerId.localeCompare(b.workerId);
214
+ });
215
+
216
+ return max === 0 ? [] : out.slice(0, max);
217
+ }
218
+
219
+ /**
220
+ * Return all worker ids declared in the patterns file. Workers with no
221
+ * regex are returned too — they simply never surface as suggestions.
222
+ *
223
+ * @param {{ configPath?: string }=} opts
224
+ * @returns {string[]}
225
+ */
226
+ export function listWorkers(opts = {}) {
227
+ const workers = loadPatterns(
228
+ opts.configPath ? { configPath: opts.configPath } : {},
229
+ );
230
+ return workers.map((w) => w.id);
231
+ }
232
+
233
+ // Direct CLI entrypoint for dry-runs / debugging.
234
+ // node cli/worker-dispatcher.mjs "your prompt here" [--max N] [--list]
235
+ if (
236
+ process.argv[1] &&
237
+ fileURLToPath(import.meta.url) === process.argv[1]
238
+ ) {
239
+ const argv = process.argv.slice(2);
240
+ const wantList = argv.includes('--list');
241
+ const maxIdx = argv.indexOf('--max');
242
+ const maxVal =
243
+ maxIdx >= 0 && argv[maxIdx + 1] ? Number.parseInt(argv[maxIdx + 1], 10) : undefined;
244
+
245
+ if (wantList) {
246
+ const ids = listWorkers();
247
+ process.stdout.write(JSON.stringify(ids, null, 2) + '\n');
248
+ process.exit(0);
249
+ }
250
+
251
+ // Build the prompt by dropping only `--max` and the value that follows it
252
+ // (and only when `--max` was actually present).
253
+ const prompt = argv
254
+ .filter((a, i) => !(maxIdx >= 0 && (i === maxIdx || i === maxIdx + 1)))
255
+ .join(' ');
256
+ const result = dispatch(prompt, { maxSuggestions: maxVal });
257
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
258
+ process.exit(0);
259
+ }