@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.
- package/.claude/hooks/sessionend-recall.mjs +3 -3
- package/.claude/hooks/worker-suggest.mjs +110 -0
- package/.claude/settings.json +9 -0
- package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js → EnvVarsSection-CW6fWKds.js} +3 -3
- package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js.map → EnvVarsSection-CW6fWKds.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileChat-D7zRhU9K.js +1 -0
- package/bizar-dash/dist/assets/{MobileChat-BJrqwVDd.js.map → MobileChat-D7zRhU9K.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-K-sNsn_r.js +1 -0
- package/bizar-dash/dist/assets/{MobileSettings-CEQNJNLJ.js.map → MobileSettings-K-sNsn_r.js.map} +1 -1
- package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js → Toast-WTxc3m2g.js} +1 -1
- package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js.map → Toast-WTxc3m2g.js.map} +1 -1
- package/bizar-dash/dist/assets/flow-BpU0-gK2.js +1 -0
- package/bizar-dash/dist/assets/flow-BpU0-gK2.js.map +1 -0
- package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js +1 -0
- package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js.map +1 -0
- package/bizar-dash/dist/assets/{icons-Do5N2jmX.js → icons-B0AKnYI3.js} +98 -103
- package/bizar-dash/dist/assets/icons-B0AKnYI3.js.map +1 -0
- package/bizar-dash/dist/assets/main-C-Md3DaY.css +1 -0
- package/bizar-dash/dist/assets/main-CO8fIxQK.js +18 -0
- package/bizar-dash/dist/assets/main-CO8fIxQK.js.map +1 -0
- package/bizar-dash/dist/assets/markdown-w2bDDkAH.js +1 -0
- package/bizar-dash/dist/assets/{markdown-tOLaD6nm.js.map → markdown-w2bDDkAH.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-DXYCA0hC.js +1 -0
- package/bizar-dash/dist/assets/{mobile-DYCHcUpq.js.map → mobile-DXYCA0hC.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js +2 -0
- package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js.map +1 -0
- package/bizar-dash/dist/assets/react-vendor-BnuhLJ6X.css +1 -0
- package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js +46 -0
- package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js.map +1 -0
- package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js +2 -0
- package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js.map +1 -0
- package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js +45 -0
- package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js.map +1 -0
- package/bizar-dash/dist/index.html +12 -10
- package/bizar-dash/dist/mobile.html +7 -4
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/api.mjs +11 -0
- package/bizar-dash/src/server/memory-consolidator.mjs +218 -0
- package/bizar-dash/src/server/memory-distillation.mjs +296 -0
- package/bizar-dash/src/server/routes/distill.mjs +129 -0
- package/bizar-dash/src/server/routes/goal-planner.mjs +236 -0
- package/bizar-dash/src/web/App.tsx +4 -0
- package/bizar-dash/src/web/components/Tag.tsx +18 -5
- package/bizar-dash/src/web/components/Topbar.tsx +3 -0
- package/bizar-dash/src/web/components/agents/CommunicationLog.tsx +126 -0
- package/bizar-dash/src/web/components/agents/DependencyGraph.tsx +163 -0
- package/bizar-dash/src/web/components/agents/QualityGates.tsx +123 -0
- package/bizar-dash/src/web/components/agents/RealTimeEventLog.tsx +127 -0
- package/bizar-dash/src/web/components/agents/RoutingDecisions.test.tsx +148 -0
- package/bizar-dash/src/web/components/agents/RoutingDecisions.tsx +160 -0
- package/bizar-dash/src/web/components/goals/GoalInput.tsx +86 -0
- package/bizar-dash/src/web/components/goals/PlanVisualization.tsx +111 -0
- package/bizar-dash/src/web/lib/goapPlanner.ts +614 -0
- package/bizar-dash/src/web/lib/types.ts +25 -0
- package/bizar-dash/src/web/styles/agents-redesign.css +311 -0
- package/bizar-dash/src/web/styles/main.css +251 -0
- package/bizar-dash/src/web/styles/tasks-redesign.css +231 -0
- package/bizar-dash/src/web/ui/controls/Button.tsx +77 -0
- package/bizar-dash/src/web/ui/controls/Checkbox.tsx +95 -0
- package/bizar-dash/src/web/ui/controls/IconButton.tsx +66 -0
- package/bizar-dash/src/web/ui/controls/Kbd.tsx +24 -0
- package/bizar-dash/src/web/ui/controls/NumberInput.tsx +77 -0
- package/bizar-dash/src/web/ui/controls/RadioGroup.tsx +109 -0
- package/bizar-dash/src/web/ui/controls/SearchInput.tsx +91 -0
- package/bizar-dash/src/web/ui/controls/Select.tsx +92 -0
- package/bizar-dash/src/web/ui/controls/Slider.tsx +70 -0
- package/bizar-dash/src/web/ui/controls/TextInput.tsx +67 -0
- package/bizar-dash/src/web/ui/controls/Toggle.tsx +79 -0
- package/bizar-dash/src/web/ui/controls/controls.css +454 -0
- package/bizar-dash/src/web/ui/controls/index.ts +33 -0
- package/bizar-dash/src/web/ui/data/BarChart.tsx +60 -0
- package/bizar-dash/src/web/ui/data/DataTable.tsx +216 -0
- package/bizar-dash/src/web/ui/data/EmptyState.tsx +60 -0
- package/bizar-dash/src/web/ui/data/ErrorState.tsx +56 -0
- package/bizar-dash/src/web/ui/data/KeyValueList.tsx +106 -0
- package/bizar-dash/src/web/ui/data/LoadingState.tsx +65 -0
- package/bizar-dash/src/web/ui/data/Sparkline.tsx +87 -0
- package/bizar-dash/src/web/ui/data/StatTile.tsx +87 -0
- package/bizar-dash/src/web/ui/data/data.css +535 -0
- package/bizar-dash/src/web/ui/data/index.ts +31 -0
- package/bizar-dash/src/web/ui/feedback/Badge.tsx +49 -0
- package/bizar-dash/src/web/ui/feedback/Dialog.tsx +209 -0
- package/bizar-dash/src/web/ui/feedback/ProgressBar.tsx +69 -0
- package/bizar-dash/src/web/ui/feedback/StatusDot.tsx +50 -0
- package/bizar-dash/src/web/ui/feedback/Toast.tsx +200 -0
- package/bizar-dash/src/web/ui/feedback/Tooltip.tsx +126 -0
- package/bizar-dash/src/web/ui/feedback/feedback.css +314 -0
- package/bizar-dash/src/web/ui/feedback/index.ts +43 -0
- package/bizar-dash/src/web/ui/index.ts +56 -0
- package/bizar-dash/src/web/ui/layout/AppShell.tsx +51 -0
- package/bizar-dash/src/web/ui/layout/Breadcrumbs.tsx +97 -0
- package/bizar-dash/src/web/ui/layout/Panel.tsx +73 -0
- package/bizar-dash/src/web/ui/layout/PanelHeader.tsx +39 -0
- package/bizar-dash/src/web/ui/layout/Sidebar.tsx +131 -0
- package/bizar-dash/src/web/ui/layout/Tabs.tsx +133 -0
- package/bizar-dash/src/web/ui/layout/Topbar.tsx +42 -0
- package/bizar-dash/src/web/ui/layout/index.ts +33 -0
- package/bizar-dash/src/web/ui/layout/layout.css +413 -0
- package/bizar-dash/src/web/ui/navigation/CommandPalette.tsx +199 -0
- package/bizar-dash/src/web/ui/navigation/NavGroup.tsx +95 -0
- package/bizar-dash/src/web/ui/navigation/NavLink.tsx +93 -0
- package/bizar-dash/src/web/ui/navigation/index.ts +17 -0
- package/bizar-dash/src/web/ui/navigation/navigation.css +343 -0
- package/bizar-dash/src/web/ui/primitives/Box.tsx +91 -0
- package/bizar-dash/src/web/ui/primitives/Grid.tsx +49 -0
- package/bizar-dash/src/web/ui/primitives/Inline.tsx +55 -0
- package/bizar-dash/src/web/ui/primitives/Separator.tsx +46 -0
- package/bizar-dash/src/web/ui/primitives/Stack.tsx +54 -0
- package/bizar-dash/src/web/ui/primitives/VisuallyHidden.tsx +26 -0
- package/bizar-dash/src/web/ui/primitives/index.ts +23 -0
- package/bizar-dash/src/web/ui/primitives/primitives.css +183 -0
- package/bizar-dash/src/web/ui/styles/globals.css +86 -0
- package/bizar-dash/src/web/ui/styles/reset.css +125 -0
- package/bizar-dash/src/web/ui/styles/tokens.css +195 -0
- package/bizar-dash/src/web/ui/theme/ThemeProvider.tsx +142 -0
- package/bizar-dash/src/web/ui/theme/tokens.ts +131 -0
- package/bizar-dash/src/web/ui/theme/useTheme.ts +22 -0
- package/bizar-dash/src/web/ui/utils/cx.ts +42 -0
- package/bizar-dash/src/web/views/Agents.tsx +506 -266
- package/bizar-dash/src/web/views/GoalPlanner.tsx +158 -0
- package/bizar-dash/src/web/views/Overview.tsx +996 -448
- package/bizar-dash/src/web/views/Tasks.tsx +277 -273
- package/bizar-dash/tests/communication-log.test.tsx +67 -0
- package/bizar-dash/tests/dep-graph.test.tsx +40 -0
- package/bizar-dash/tests/event-log.test.tsx +61 -0
- package/bizar-dash/tests/goal-input.test.tsx +72 -0
- package/bizar-dash/tests/goap-planner.test.ts +68 -0
- package/bizar-dash/tests/quality-gates.test.tsx +59 -0
- package/bizar-dash/tests/setup.ts +31 -1
- package/bizar-dash/tests/ui/controls/Button.test.tsx +50 -0
- package/bizar-dash/tests/ui/controls/Checkbox.test.tsx +44 -0
- package/bizar-dash/tests/ui/controls/IconButton.test.tsx +37 -0
- package/bizar-dash/tests/ui/controls/Kbd.test.tsx +39 -0
- package/bizar-dash/tests/ui/controls/NumberInput.test.tsx +56 -0
- package/bizar-dash/tests/ui/controls/RadioGroup.test.tsx +58 -0
- package/bizar-dash/tests/ui/controls/SearchInput.test.tsx +52 -0
- package/bizar-dash/tests/ui/controls/Select.test.tsx +66 -0
- package/bizar-dash/tests/ui/controls/Slider.test.tsx +41 -0
- package/bizar-dash/tests/ui/controls/TextInput.test.tsx +44 -0
- package/bizar-dash/tests/ui/controls/Toggle.test.tsx +39 -0
- package/bizar-dash/tests/ui/data/BarChart.test.tsx +55 -0
- package/bizar-dash/tests/ui/data/DataTable.test.tsx +112 -0
- package/bizar-dash/tests/ui/data/EmptyState.test.tsx +57 -0
- package/bizar-dash/tests/ui/data/ErrorState.test.tsx +47 -0
- package/bizar-dash/tests/ui/data/KeyValueList.test.tsx +59 -0
- package/bizar-dash/tests/ui/data/LoadingState.test.tsx +37 -0
- package/bizar-dash/tests/ui/data/Sparkline.test.tsx +56 -0
- package/bizar-dash/tests/ui/data/StatTile.test.tsx +69 -0
- package/bizar-dash/tests/ui/feedback/Badge.test.tsx +41 -0
- package/bizar-dash/tests/ui/feedback/Dialog.test.tsx +99 -0
- package/bizar-dash/tests/ui/feedback/ProgressBar.test.tsx +66 -0
- package/bizar-dash/tests/ui/feedback/StatusDot.test.tsx +41 -0
- package/bizar-dash/tests/ui/feedback/Toast.test.tsx +124 -0
- package/bizar-dash/tests/ui/feedback/Tooltip.test.tsx +102 -0
- package/bizar-dash/tests/ui/layout/AppShell.test.tsx +50 -0
- package/bizar-dash/tests/ui/layout/Breadcrumbs.test.tsx +85 -0
- package/bizar-dash/tests/ui/layout/Panel.test.tsx +52 -0
- package/bizar-dash/tests/ui/layout/PanelHeader.test.tsx +32 -0
- package/bizar-dash/tests/ui/layout/Sidebar.test.tsx +95 -0
- package/bizar-dash/tests/ui/layout/Tabs.test.tsx +87 -0
- package/bizar-dash/tests/ui/layout/Topbar.test.tsx +41 -0
- package/bizar-dash/tests/ui/navigation/CommandPalette.test.tsx +105 -0
- package/bizar-dash/tests/ui/navigation/NavGroup.test.tsx +55 -0
- package/bizar-dash/tests/ui/navigation/NavLink.test.tsx +61 -0
- package/bizar-dash/tests/ui/primitives/Box.test.tsx +49 -0
- package/bizar-dash/tests/ui/primitives/Grid.test.tsx +37 -0
- package/bizar-dash/tests/ui/primitives/Inline.test.tsx +33 -0
- package/bizar-dash/tests/ui/primitives/Separator.test.tsx +34 -0
- package/bizar-dash/tests/ui/primitives/Stack.test.tsx +45 -0
- package/bizar-dash/tests/ui/primitives/VisuallyHidden.test.tsx +27 -0
- package/bizar-dash/tests/views/Agents.test.tsx +359 -0
- package/bizar-dash/tests/views/Overview.test.tsx +413 -0
- package/bizar-dash/tests/views/Tasks.test.tsx +250 -0
- package/bizar-dash/vitest.config.ts +3 -1
- package/cli/__tests__/cost-gate.test.mjs +301 -0
- package/cli/__tests__/feature-list-bridge.test.mjs +371 -0
- package/cli/bin.mjs +38 -0
- package/cli/commands/claim.mjs +282 -0
- package/cli/commands/cost.mjs +279 -0
- package/cli/cost-gate.mjs +501 -0
- package/cli/feature-list-bridge.mjs +400 -0
- package/cli/memory-constants.mjs +17 -0
- package/cli/worker-dispatcher.mjs +259 -0
- package/cli/worker-dispatcher.test.mjs +204 -0
- package/config/trigger-patterns.json +186 -0
- package/package.json +2 -2
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/agent-registry.ts +304 -0
- package/packages/sdk/src/consensus/byzantine.ts +587 -0
- package/packages/sdk/src/consensus/index.ts +147 -0
- package/packages/sdk/src/consensus/queen.ts +173 -0
- package/packages/sdk/src/consensus/types.ts +199 -0
- package/packages/sdk/src/federation/audit.ts +156 -0
- package/packages/sdk/src/federation/budget.ts +269 -0
- package/packages/sdk/src/federation/envelope.ts +144 -0
- package/packages/sdk/src/federation/hmac.ts +160 -0
- package/packages/sdk/src/federation/index.ts +443 -0
- package/packages/sdk/src/federation/pii.ts +241 -0
- package/packages/sdk/src/federation/policy.ts +112 -0
- package/packages/sdk/src/federation/trust.ts +146 -0
- package/packages/sdk/src/index.ts +67 -0
- package/packages/sdk/src/mcp/server.ts +351 -1
- package/packages/sdk/src/router/codemod-intent.ts +125 -0
- package/packages/sdk/src/router/index.ts +247 -0
- package/packages/sdk/src/router/memory-distillation-shim.mjs +18 -0
- package/packages/sdk/src/router/memory-distillation.ts +249 -0
- package/packages/sdk/src/router/model-router.ts +235 -0
- package/packages/sdk/src/router/q-learning-router.ts +304 -0
- package/packages/sdk/src/swarm-topology.ts +279 -0
- package/packages/sdk/tests/agent-registry.test.ts +285 -0
- package/packages/sdk/tests/consensus.test.ts +376 -0
- package/packages/sdk/tests/federation/audit.test.ts +164 -0
- package/packages/sdk/tests/federation/budget.test.ts +189 -0
- package/packages/sdk/tests/federation/envelope.test.ts +120 -0
- package/packages/sdk/tests/federation/hmac.test.ts +194 -0
- package/packages/sdk/tests/federation/orchestrator.test.ts +176 -0
- package/packages/sdk/tests/federation/pii.test.ts +106 -0
- package/packages/sdk/tests/federation/policy.test.ts +140 -0
- package/packages/sdk/tests/federation/trust.test.ts +124 -0
- package/packages/sdk/tests/mcp-tools.test.ts +204 -0
- package/packages/sdk/tests/memory-distillation.test.mjs +186 -0
- package/packages/sdk/tests/model-router.test.mjs +108 -0
- package/packages/sdk/tests/q-learning-router.test.mjs +100 -0
- package/packages/sdk/tests/router-orchestrator.test.mjs +172 -0
- package/packages/sdk/tests/router.test.mjs +91 -0
- package/packages/sdk/tests/sdk.test.mjs +26 -2
- package/packages/sdk/tests/swarm-topology.test.ts +258 -0
- package/scripts/bh-full-e2e.mjs +11 -15
- package/bizar-dash/dist/assets/MobileChat-BJrqwVDd.js +0 -1
- package/bizar-dash/dist/assets/MobileSettings-CEQNJNLJ.js +0 -1
- package/bizar-dash/dist/assets/icons-Do5N2jmX.js.map +0 -1
- package/bizar-dash/dist/assets/main-IvfQAOfy.js +0 -18
- package/bizar-dash/dist/assets/main-IvfQAOfy.js.map +0 -1
- package/bizar-dash/dist/assets/main-ietCEg_R.css +0 -1
- package/bizar-dash/dist/assets/markdown-tOLaD6nm.js +0 -1
- package/bizar-dash/dist/assets/mobile-DYCHcUpq.js +0 -1
- package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js +0 -2
- package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js.map +0 -1
- package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js +0 -40
- package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js.map +0 -1
- package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js +0 -2
- package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js.map +0 -1
- package/bizar-dash/dist/assets/vendor-C843201K.js +0 -29
- package/bizar-dash/dist/assets/vendor-C843201K.js.map +0 -1
- 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
|
+
}
|
package/cli/memory-constants.mjs
CHANGED
|
@@ -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
|
+
}
|