@claude-flow/cli 3.42.5 → 3.44.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.
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Router embedder (ADR-390).
3
+ *
4
+ * `hooks_route` compares a task with each agent pattern's keywords in a 384-d
5
+ * vector index. Historically both sides came from a character hash
6
+ * (`generateSimpleEmbedding`), which measures spelling, not meaning. This
7
+ * module lets the router use the local sentence model (all-MiniLM-L6-v2, 384-d)
8
+ * instead, with the hash as the fallback.
9
+ *
10
+ * Rules (ADR-390 §Decision):
11
+ * - The MiniLM path uses `generateLocalEmbedding` ONLY. Never the bridge-first
12
+ * `generateEmbedding` — that recursed without bound in #2312.
13
+ * - One embedder per index: if ANY text cannot be embedded by the real model
14
+ * (throw, backend !== 'onnx', or a non-384 vector), EVERY text in the call
15
+ * is embedded with the hash, and the result says so.
16
+ * - The default stays `hash` until ADR-391's benchmark says otherwise.
17
+ *
18
+ * Selection: `CLAUDE_FLOW_ROUTER_EMBEDDER=minilm|hash`. The router index is
19
+ * process-lifetime state, so the env var is read when the index is (re)built,
20
+ * not per CLI invocation.
21
+ */
22
+ // memory-initializer is imported lazily (as hooks-tools does elsewhere): it is
23
+ // heavy, and the default `hash` path must not load it at all.
24
+ /** Default embedder. ADR-391 decides whether this flips to 'minilm'. */
25
+ export const DEFAULT_ROUTER_EMBEDDER = 'hash';
26
+ /** Dimension of the router index (VectorDb + SemanticRouter are built at 384). */
27
+ export const ROUTER_EMBEDDING_DIM = 384;
28
+ export const ROUTER_EMBEDDER_ENV = 'CLAUDE_FLOW_ROUTER_EMBEDDER';
29
+ /**
30
+ * Resolve which embedder the router should use.
31
+ * Precedence: explicit override > CLAUDE_FLOW_ROUTER_EMBEDDER > DEFAULT_ROUTER_EMBEDDER.
32
+ */
33
+ export function resolveRouterEmbedder(override, env = process.env) {
34
+ if (override)
35
+ return { kind: override };
36
+ // Env-only by design (registered in scripts/audit-env-var-precedence.mjs):
37
+ // the router index is process-lifetime MCP state, not owned by one CLI call.
38
+ const raw = env.CLAUDE_FLOW_ROUTER_EMBEDDER?.trim().toLowerCase();
39
+ if (!raw)
40
+ return { kind: DEFAULT_ROUTER_EMBEDDER };
41
+ if (raw === 'minilm' || raw === 'hash')
42
+ return { kind: raw };
43
+ return {
44
+ kind: DEFAULT_ROUTER_EMBEDDER,
45
+ reason: `${ROUTER_EMBEDDER_ENV}=${JSON.stringify(raw)} is not 'minilm' or 'hash'; using '${DEFAULT_ROUTER_EMBEDDER}'`,
46
+ };
47
+ }
48
+ /**
49
+ * Deterministic character-hash embedding (the router's historical embedder).
50
+ * Moved verbatim from hooks-tools.ts; do not change the math — existing routes
51
+ * and thresholds were calibrated against it.
52
+ */
53
+ export function generateSimpleEmbedding(text, dimension = ROUTER_EMBEDDING_DIM) {
54
+ const embedding = new Float32Array(dimension);
55
+ const normalized = text.toLowerCase().replace(/[^a-z0-9\s]/g, '');
56
+ const words = normalized.split(/\s+/).filter(w => w.length > 0);
57
+ for (let i = 0; i < dimension; i++) {
58
+ let value = 0;
59
+ // Word-level features
60
+ for (let w = 0; w < words.length; w++) {
61
+ const word = words[w];
62
+ for (let c = 0; c < word.length; c++) {
63
+ const charCode = word.charCodeAt(c);
64
+ value += Math.sin((charCode * (i + 1) + w * 17 + c * 23) * 0.0137);
65
+ }
66
+ }
67
+ // Character-level features
68
+ for (let c = 0; c < text.length; c++) {
69
+ value += Math.cos((text.charCodeAt(c) * (i + 1) + c * 7) * 0.0073);
70
+ }
71
+ embedding[i] = value / Math.max(1, text.length);
72
+ }
73
+ return l2Normalize(embedding);
74
+ }
75
+ function l2Normalize(v) {
76
+ let norm = 0;
77
+ for (let i = 0; i < v.length; i++)
78
+ norm += v[i] * v[i];
79
+ norm = Math.sqrt(norm);
80
+ if (norm > 0)
81
+ for (let i = 0; i < v.length; i++)
82
+ v[i] /= norm;
83
+ return v;
84
+ }
85
+ // Per-(embedder, text) memo. Keyword vectors don't change when the pattern set
86
+ // changes, so this survives router rebuilds (e.g. after saveRoutingOutcomes).
87
+ const MINILM_CACHE = new Map();
88
+ const MINILM_CACHE_MAX = 2048;
89
+ async function embedOneMiniLM(text) {
90
+ const cached = MINILM_CACHE.get(text);
91
+ if (cached)
92
+ return cached;
93
+ const { generateLocalEmbedding } = await import('../memory/memory-initializer.js');
94
+ const out = await generateLocalEmbedding(text);
95
+ if (out.backend !== 'onnx') {
96
+ throw new RouterEmbedderDegraded(`local embedder backend is '${out.backend}' (model '${out.model}'), not onnx`);
97
+ }
98
+ if (!out.embedding || out.embedding.length !== ROUTER_EMBEDDING_DIM) {
99
+ throw new RouterEmbedderDegraded(`local embedder returned ${out.embedding?.length ?? 0}-d vectors; router index is ${ROUTER_EMBEDDING_DIM}-d`);
100
+ }
101
+ const vec = l2Normalize(Float32Array.from(out.embedding));
102
+ if (MINILM_CACHE.size >= MINILM_CACHE_MAX) {
103
+ const oldest = MINILM_CACHE.keys().next().value;
104
+ if (oldest !== undefined)
105
+ MINILM_CACHE.delete(oldest);
106
+ }
107
+ MINILM_CACHE.set(text, vec);
108
+ return vec;
109
+ }
110
+ class RouterEmbedderDegraded extends Error {
111
+ }
112
+ /**
113
+ * Embed texts for the router. All vectors in one result come from ONE embedder.
114
+ * `hash` never touches the model (no load cost for default users).
115
+ */
116
+ export async function embedForRouter(texts, kind = resolveRouterEmbedder().kind) {
117
+ if (kind === 'hash') {
118
+ return { vectors: texts.map(t => generateSimpleEmbedding(t)), embedder: 'hash' };
119
+ }
120
+ try {
121
+ const vectors = [];
122
+ for (const t of texts)
123
+ vectors.push(await embedOneMiniLM(t));
124
+ return { vectors, embedder: 'minilm' };
125
+ }
126
+ catch (err) {
127
+ const why = err instanceof RouterEmbedderDegraded
128
+ ? err.message
129
+ : `local embedder threw: ${err instanceof Error ? err.message : String(err)}`;
130
+ return {
131
+ vectors: texts.map(t => generateSimpleEmbedding(t)),
132
+ embedder: 'hash',
133
+ reason: `minilm unavailable (${why}); using hash for patterns and query`,
134
+ };
135
+ }
136
+ }
137
+ /** Test hook: clear the MiniLM vector memo. */
138
+ export function clearRouterEmbedderCache() {
139
+ MINILM_CACHE.clear();
140
+ }
141
+ //# sourceMappingURL=router-embedder.js.map
@@ -0,0 +1,119 @@
1
+ /**
2
+ * typesafe-router.ts — opt-in `@ruvector/typesafe` augmentation for `hooks_route`.
3
+ *
4
+ * Mirrors ADR-150's MetaHarness rules for optional integrations:
5
+ * 1. Removable — the package is loaded with a dynamic import only; any load,
6
+ * construction or decide error falls back to the existing router.
7
+ * 2. Opt-in — does nothing unless `CLAUDE_FLOW_ROUTER_TYPESAFE=1`. With the
8
+ * flag unset the package is never imported and the legacy result is
9
+ * returned unchanged (same object, no added fields).
10
+ * 3. Optional — declared as an optional peer of `@claude-flow/cli`.
11
+ * 4. Honest — the result carries `routedBy`, typesafe's confidence, abstain
12
+ * mass and `calibrated` flag verbatim. The default `hash` embedder is
13
+ * uncalibrated, so its confidence is reported as `confidenceCalibrated:
14
+ * false` and is never copied into `estimatedMetrics.successProbability`.
15
+ *
16
+ * Gate: typesafe's answer is used only when all hold —
17
+ * - `abstain <= maxAbstain` (default 0.30)
18
+ * - lift = top-1 probability × option count >= `minLift` (default 1.2, i.e.
19
+ * 20% above chance). Lift, not raw confidence, because confidence scales
20
+ * with the option count (~0.1 for ten agents) and differs per embedder.
21
+ * - top-1 beats the runner-up by >= `minMargin` (default 0.005); a uniform
22
+ * distribution — text that matches nothing — has margin 0.
23
+ * Otherwise the legacy route is kept and `typesafe.reason` says which gate failed.
24
+ *
25
+ * @module typesafe-router
26
+ */
27
+ /** Keyword/agent table shape shared with hooks-tools' TASK_PATTERNS. */
28
+ export interface RoutingPatternLike {
29
+ keywords: string[];
30
+ agents: string[];
31
+ }
32
+ /** One `choice` option in typesafe's `{ what, not_for, examples }` form. */
33
+ export interface TypesafeCriterion {
34
+ what: string;
35
+ not_for?: string;
36
+ examples?: string[];
37
+ }
38
+ /** The subset of a typesafe choice answer this adapter reads. */
39
+ export interface TypesafeChoiceAnswer {
40
+ choice: string;
41
+ probabilities: Record<string, number>;
42
+ confidence: number;
43
+ abstain: number;
44
+ calibrated: boolean;
45
+ head?: string;
46
+ model?: string;
47
+ }
48
+ /** The subset of `@ruvector/typesafe`'s module surface this adapter uses. */
49
+ export interface TypesafeModuleLike {
50
+ createTypesafe(opts?: Record<string, unknown>): {
51
+ readonly backend?: string;
52
+ decide(state: string, questions: Record<string, unknown>): Promise<Record<string, unknown>>;
53
+ };
54
+ choice?(criteria: Record<string, TypesafeCriterion>): unknown;
55
+ }
56
+ export interface TypesafeRouterConfig {
57
+ enabled: boolean;
58
+ minLift: number;
59
+ maxAbstain: number;
60
+ minMargin: number;
61
+ /** `'hash'` (default, uncalibrated) or an ONNX model dir + manifest. */
62
+ embedder: 'hash' | {
63
+ kind: 'onnx';
64
+ modelDir: string;
65
+ manifest: string;
66
+ };
67
+ }
68
+ export interface TypesafeRouteOutcome {
69
+ used: boolean;
70
+ reason: string;
71
+ answer?: TypesafeChoiceAnswer;
72
+ backend?: string;
73
+ embedder?: string;
74
+ /** top-1 probability × option count (1.0 = chance). */
75
+ lift?: number;
76
+ thresholds: Pick<TypesafeRouterConfig, 'minLift' | 'maxAbstain' | 'minMargin'>;
77
+ }
78
+ /** Injectable deps (tests). `loadModule` defaults to a dynamic import of the package. */
79
+ export interface TypesafeRouterDeps {
80
+ env?: NodeJS.ProcessEnv;
81
+ loadModule?: () => Promise<unknown>;
82
+ debug?: (msg: string) => void;
83
+ }
84
+ /**
85
+ * Build choice options from the router's pattern table: every primary agent of
86
+ * a pattern, plus the profiled roles (researcher/reviewer) the table only lists
87
+ * as alternates. Each pattern's keywords are appended to its primary agent's
88
+ * `what`, so the options track TASK_PATTERNS rather than a parallel list.
89
+ */
90
+ export declare function buildAgentCriteria(patterns: Record<string, RoutingPatternLike>): Record<string, TypesafeCriterion>;
91
+ /** Read config from env. Invalid numeric values fall back to the defaults. */
92
+ export declare function readTypesafeConfig(env?: NodeJS.ProcessEnv): TypesafeRouterConfig;
93
+ /**
94
+ * Stateful router: loads the module and builds one engine on first use, caches
95
+ * a load failure so a missing package costs one import attempt per process.
96
+ */
97
+ export declare class TypesafeRouter {
98
+ private engine;
99
+ private mod;
100
+ private loadError;
101
+ private readonly env;
102
+ private readonly loadModule;
103
+ private readonly debug;
104
+ constructor(deps?: TypesafeRouterDeps);
105
+ isEnabled(): boolean;
106
+ private ensureEngine;
107
+ /** Ask typesafe for an agent. Never throws; `used: false` means keep the legacy route. */
108
+ route(task: string, patterns: Record<string, RoutingPatternLike>): Promise<TypesafeRouteOutcome>;
109
+ }
110
+ type RouteResult = Record<string, unknown>;
111
+ /**
112
+ * Merge a typesafe outcome into a legacy `hooks_route` result. Disabled or
113
+ * error results (`success: false`) pass through untouched.
114
+ */
115
+ export declare function applyTypesafeRouting(params: Record<string, unknown>, legacy: RouteResult, patterns: Record<string, RoutingPatternLike>, router: TypesafeRouter): Promise<RouteResult>;
116
+ /** Process-wide router used by hooks_route. */
117
+ export declare function getTypesafeRouter(): TypesafeRouter;
118
+ export {};
119
+ //# sourceMappingURL=typesafe-router.d.ts.map
@@ -0,0 +1,245 @@
1
+ /**
2
+ * typesafe-router.ts — opt-in `@ruvector/typesafe` augmentation for `hooks_route`.
3
+ *
4
+ * Mirrors ADR-150's MetaHarness rules for optional integrations:
5
+ * 1. Removable — the package is loaded with a dynamic import only; any load,
6
+ * construction or decide error falls back to the existing router.
7
+ * 2. Opt-in — does nothing unless `CLAUDE_FLOW_ROUTER_TYPESAFE=1`. With the
8
+ * flag unset the package is never imported and the legacy result is
9
+ * returned unchanged (same object, no added fields).
10
+ * 3. Optional — declared as an optional peer of `@claude-flow/cli`.
11
+ * 4. Honest — the result carries `routedBy`, typesafe's confidence, abstain
12
+ * mass and `calibrated` flag verbatim. The default `hash` embedder is
13
+ * uncalibrated, so its confidence is reported as `confidenceCalibrated:
14
+ * false` and is never copied into `estimatedMetrics.successProbability`.
15
+ *
16
+ * Gate: typesafe's answer is used only when all hold —
17
+ * - `abstain <= maxAbstain` (default 0.30)
18
+ * - lift = top-1 probability × option count >= `minLift` (default 1.2, i.e.
19
+ * 20% above chance). Lift, not raw confidence, because confidence scales
20
+ * with the option count (~0.1 for ten agents) and differs per embedder.
21
+ * - top-1 beats the runner-up by >= `minMargin` (default 0.005); a uniform
22
+ * distribution — text that matches nothing — has margin 0.
23
+ * Otherwise the legacy route is kept and `typesafe.reason` says which gate failed.
24
+ *
25
+ * @module typesafe-router
26
+ */
27
+ const MODULE_ID = '@ruvector/typesafe';
28
+ /**
29
+ * Default loader. The variable specifier keeps tsc/bundlers off the optional peer;
30
+ * `require` (the package is CJS) covers hosts that rewrite dynamic import (vite-node).
31
+ * An absent package still surfaces as MODULE_NOT_FOUND → "not installed".
32
+ */
33
+ async function loadTypesafeModule() {
34
+ try {
35
+ return await import(/* @vite-ignore */ MODULE_ID);
36
+ }
37
+ catch (importErr) {
38
+ try {
39
+ const { createRequire } = await import('node:module');
40
+ return createRequire(import.meta.url)(MODULE_ID);
41
+ }
42
+ catch (requireErr) {
43
+ throw requireErr?.code === 'MODULE_NOT_FOUND' ? requireErr : importErr;
44
+ }
45
+ }
46
+ }
47
+ /** Agent descriptions + `not_for` hints that separate neighbouring roles. */
48
+ const AGENT_PROFILES = {
49
+ tester: { what: 'write and run unit tests, integration tests, e2e tests, test coverage and specs', not_for: 'reviewing code, researching issues or reading the latest news' },
50
+ reviewer: { what: 'review code quality, pull requests, diffs and best practices', not_for: 'writing tests, implementing features or researching background information' },
51
+ researcher: { what: 'research, investigate, explore, read and summarize issues, docs, discussions and prior art', not_for: 'writing code, writing tests or reviewing a diff' },
52
+ coder: { what: 'implement features, write code, fix bugs and build functionality', not_for: 'research, review or test-only work' },
53
+ architect: { what: 'design system architecture, module boundaries, APIs, schemas and refactoring plans', not_for: 'running tests or small bug fixes' },
54
+ 'security-architect': { what: 'security, authentication, authorization, encryption, vulnerabilities, CVEs and audits', not_for: 'general feature work or performance tuning' },
55
+ 'performance-engineer': { what: 'performance optimization, profiling, benchmarks, latency and bottlenecks', not_for: 'security review or writing documentation' },
56
+ devops: { what: 'deployment, CI/CD pipelines, docker, kubernetes and infrastructure', not_for: 'application feature code or unit tests' },
57
+ 'memory-specialist': { what: 'memory systems, caches, vector stores, embeddings and persistence', not_for: 'UI work or deployment pipelines' },
58
+ 'swarm-specialist': { what: 'multi-agent swarms, coordinators, hive-mind, mesh topology and agent orchestration', not_for: 'single-file code edits' },
59
+ };
60
+ /**
61
+ * Build choice options from the router's pattern table: every primary agent of
62
+ * a pattern, plus the profiled roles (researcher/reviewer) the table only lists
63
+ * as alternates. Each pattern's keywords are appended to its primary agent's
64
+ * `what`, so the options track TASK_PATTERNS rather than a parallel list.
65
+ */
66
+ export function buildAgentCriteria(patterns) {
67
+ const keywordsByAgent = new Map();
68
+ for (const { agents, keywords } of Object.values(patterns)) {
69
+ const primary = agents[0];
70
+ if (!primary)
71
+ continue;
72
+ const set = keywordsByAgent.get(primary) ?? new Set();
73
+ keywords.forEach(k => set.add(k));
74
+ keywordsByAgent.set(primary, set);
75
+ }
76
+ for (const agent of ['researcher', 'reviewer', 'tester', 'coder']) {
77
+ if (!keywordsByAgent.has(agent))
78
+ keywordsByAgent.set(agent, new Set());
79
+ }
80
+ const criteria = {};
81
+ for (const [agent, kws] of keywordsByAgent) {
82
+ const profile = AGENT_PROFILES[agent] ?? { what: `${agent.replace(/-/g, ' ')} tasks` };
83
+ const kw = [...kws].join(', ');
84
+ criteria[agent] = {
85
+ what: kw ? `${profile.what}; keywords: ${kw}` : profile.what,
86
+ ...(profile.not_for ? { not_for: profile.not_for } : {}),
87
+ };
88
+ }
89
+ return criteria;
90
+ }
91
+ function num(raw, dflt, max = 1) {
92
+ const n = raw === undefined || raw === '' ? NaN : Number(raw);
93
+ return Number.isFinite(n) && n >= 0 && n <= max ? n : dflt;
94
+ }
95
+ /** Read config from env. Invalid numeric values fall back to the defaults. */
96
+ export function readTypesafeConfig(env = process.env) {
97
+ const modelDir = env.CLAUDE_FLOW_ROUTER_TYPESAFE_MODEL_DIR;
98
+ const manifest = env.CLAUDE_FLOW_ROUTER_TYPESAFE_MANIFEST;
99
+ return {
100
+ enabled: env.CLAUDE_FLOW_ROUTER_TYPESAFE === '1',
101
+ minLift: num(env.CLAUDE_FLOW_ROUTER_TYPESAFE_MIN_LIFT, 1.2, 255),
102
+ maxAbstain: num(env.CLAUDE_FLOW_ROUTER_TYPESAFE_MAX_ABSTAIN, 0.3),
103
+ minMargin: num(env.CLAUDE_FLOW_ROUTER_TYPESAFE_MIN_MARGIN, 0.005),
104
+ embedder: modelDir && manifest ? { kind: 'onnx', modelDir, manifest } : 'hash',
105
+ };
106
+ }
107
+ /**
108
+ * Stateful router: loads the module and builds one engine on first use, caches
109
+ * a load failure so a missing package costs one import attempt per process.
110
+ */
111
+ export class TypesafeRouter {
112
+ engine = null;
113
+ mod = null;
114
+ loadError = null;
115
+ env;
116
+ loadModule;
117
+ debug;
118
+ constructor(deps = {}) {
119
+ this.env = deps.env ?? process.env;
120
+ this.loadModule = deps.loadModule ?? loadTypesafeModule;
121
+ this.debug = deps.debug ?? ((m) => { if (this.env.CLAUDE_FLOW_LOG_LEVEL === 'debug')
122
+ console.error(`[typesafe-router] ${m}`); });
123
+ }
124
+ isEnabled() { return readTypesafeConfig(this.env).enabled; }
125
+ async ensureEngine(cfg) {
126
+ if (this.engine || this.loadError)
127
+ return this.engine;
128
+ try {
129
+ const raw = (await this.loadModule());
130
+ const mod = (typeof raw.createTypesafe === 'function' ? raw : raw.default);
131
+ if (!mod || typeof mod.createTypesafe !== 'function')
132
+ throw new Error('module has no createTypesafe export');
133
+ this.mod = mod;
134
+ this.engine = mod.createTypesafe({ embedder: cfg.embedder });
135
+ }
136
+ catch (err) {
137
+ const e = err;
138
+ this.loadError = e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND'
139
+ ? `${MODULE_ID} not installed`
140
+ : `${MODULE_ID} failed to load: ${e?.message ?? String(err)}`;
141
+ this.debug(this.loadError);
142
+ }
143
+ return this.engine;
144
+ }
145
+ /** Ask typesafe for an agent. Never throws; `used: false` means keep the legacy route. */
146
+ async route(task, patterns) {
147
+ const cfg = readTypesafeConfig(this.env);
148
+ const thresholds = { minLift: cfg.minLift, maxAbstain: cfg.maxAbstain, minMargin: cfg.minMargin };
149
+ const embedder = cfg.embedder === 'hash' ? 'hash' : 'onnx';
150
+ if (!cfg.enabled)
151
+ return { used: false, reason: 'CLAUDE_FLOW_ROUTER_TYPESAFE is not 1', thresholds };
152
+ const engine = await this.ensureEngine(cfg);
153
+ if (!engine)
154
+ return { used: false, reason: this.loadError ?? 'typesafe unavailable', thresholds, embedder };
155
+ let answer;
156
+ try {
157
+ const criteria = buildAgentCriteria(patterns);
158
+ const question = this.mod?.choice ? this.mod.choice(criteria) : { type: 'choice', criteria };
159
+ const res = await engine.decide(task, { agent: question });
160
+ answer = res.agent;
161
+ if (!answer || typeof answer.choice !== 'string')
162
+ throw new Error('no choice in answer');
163
+ }
164
+ catch (err) {
165
+ const reason = `typesafe decide failed: ${err?.message ?? String(err)}`;
166
+ this.debug(reason);
167
+ return { used: false, reason, thresholds, embedder, backend: engine.backend };
168
+ }
169
+ const probs = Object.values(answer.probabilities ?? {}).sort((a, b) => b - a);
170
+ const margin = (probs[0] ?? 0) - (probs[1] ?? 0);
171
+ const lift = (probs[0] ?? 0) * probs.length;
172
+ const base = { answer, lift, thresholds, embedder, backend: engine.backend };
173
+ const f = (n) => n.toFixed(2);
174
+ if (answer.abstain > cfg.maxAbstain) {
175
+ return { ...base, used: false, reason: `typesafe abstain ${f(answer.abstain)} > max ${f(cfg.maxAbstain)}; kept existing router` };
176
+ }
177
+ if (lift < cfg.minLift) {
178
+ return { ...base, used: false, reason: `typesafe lift ${f(lift)} (top-1 × ${probs.length} options) < min ${f(cfg.minLift)}; kept existing router` };
179
+ }
180
+ if (margin < cfg.minMargin) {
181
+ return { ...base, used: false, reason: `typesafe top-1 margin ${margin.toFixed(3)} < min ${cfg.minMargin.toFixed(3)} (no clear winner); kept existing router` };
182
+ }
183
+ return { ...base, used: true, reason: `typesafe choice "${answer.choice}" (confidence ${f(answer.confidence)}, lift ${f(lift)}, abstain ${f(answer.abstain)}, ${answer.calibrated ? 'calibrated' : 'UNCALIBRATED'} ${embedder} embedder)` };
184
+ }
185
+ }
186
+ const round2 = (n) => Math.round(n * 100) / 100;
187
+ /**
188
+ * Merge a typesafe outcome into a legacy `hooks_route` result. Disabled or
189
+ * error results (`success: false`) pass through untouched.
190
+ */
191
+ export async function applyTypesafeRouting(params, legacy, patterns, router) {
192
+ if (!router.isEnabled() || !legacy || legacy.success === false || typeof params.task !== 'string')
193
+ return legacy;
194
+ const text = typeof params.context === 'string' && params.context ? `${params.task} ${params.context}` : params.task;
195
+ const outcome = await router.route(text, patterns);
196
+ const legacyRouting = (legacy.routing ?? {});
197
+ const legacyPrimary = (legacy.primaryAgent ?? {});
198
+ const a = outcome.answer;
199
+ const typesafe = {
200
+ used: outcome.used,
201
+ reason: outcome.reason,
202
+ ...(a ? {
203
+ choice: a.choice,
204
+ confidence: round2(a.confidence),
205
+ abstain: round2(a.abstain),
206
+ lift: outcome.lift === undefined ? undefined : round2(outcome.lift),
207
+ calibrated: a.calibrated,
208
+ head: a.head,
209
+ model: a.model,
210
+ probabilities: Object.fromEntries(Object.entries(a.probabilities).map(([k, v]) => [k, round2(v)])),
211
+ } : {}),
212
+ backend: outcome.backend,
213
+ embedder: outcome.embedder,
214
+ thresholds: outcome.thresholds,
215
+ };
216
+ if (!outcome.used || !a) {
217
+ return { ...legacy, routedBy: String(legacyRouting.method ?? 'legacy'), typesafe };
218
+ }
219
+ const alternatives = Object.entries(a.probabilities)
220
+ .filter(([k]) => k !== a.choice)
221
+ .sort((x, y) => y[1] - x[1])
222
+ .slice(0, 2)
223
+ .map(([type, p]) => ({ type, confidence: round2(p), reason: 'typesafe runner-up (probability share)' }));
224
+ return {
225
+ ...legacy,
226
+ routing: { ...legacyRouting, method: 'typesafe', backend: `@ruvector/typesafe (${outcome.backend ?? 'unknown'}, ${outcome.embedder} embedder)` },
227
+ routedBy: 'typesafe',
228
+ matchedPattern: `typesafe:${a.choice}`,
229
+ primaryAgent: {
230
+ type: a.choice,
231
+ confidence: round2(a.confidence),
232
+ confidenceCalibrated: a.calibrated,
233
+ reason: outcome.reason,
234
+ },
235
+ alternativeAgents: alternatives,
236
+ fallbackRoute: { agent: legacyPrimary.type, confidence: legacyPrimary.confidence, method: legacyRouting.method },
237
+ typesafe,
238
+ };
239
+ }
240
+ let sharedRouter = null;
241
+ /** Process-wide router used by hooks_route. */
242
+ export function getTypesafeRouter() {
243
+ return (sharedRouter ??= new TypesafeRouter());
244
+ }
245
+ //# sourceMappingURL=typesafe-router.js.map
@@ -42,11 +42,40 @@ async function acquireLock(lockPath) {
42
42
  }
43
43
  throw new Error('policy-state-lock-timeout');
44
44
  }
45
- function writeJsonAtomic(file, value) {
45
+ // #3398: Windows rename over a file another process holds open fails with
46
+ // EPERM/EBUSY/EACCES. Retry (~1.3s total, inside LOCK_WAIT_MS), never leave the
47
+ // temp file; a final failure still throws — the state holds the receipt ledger
48
+ // and consumed approval uses, so dropping it would let an approval be reused.
49
+ const RENAME_RETRY_DELAYS_MS = [25, 50, 100, 150, 250, 300, 400];
50
+ const TRANSIENT_RENAME_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
51
+ async function writeJsonAtomic(file, value) {
46
52
  mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
47
53
  const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
48
- writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
49
- renameSync(temporary, file);
54
+ let renamed = false;
55
+ try {
56
+ writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
57
+ for (let attempt = 0;; attempt++) {
58
+ try {
59
+ renameSync(temporary, file);
60
+ renamed = true;
61
+ return;
62
+ }
63
+ catch (error) {
64
+ const code = error?.code ?? '';
65
+ if (!TRANSIENT_RENAME_CODES.has(code) || attempt >= RENAME_RETRY_DELAYS_MS.length)
66
+ throw error;
67
+ await sleep(RENAME_RETRY_DELAYS_MS[attempt]);
68
+ }
69
+ }
70
+ }
71
+ finally {
72
+ if (!renamed) {
73
+ try {
74
+ unlinkSync(temporary);
75
+ }
76
+ catch { /* never created or already gone */ }
77
+ }
78
+ }
50
79
  }
51
80
  function trustPaths(projectRoot) {
52
81
  const trustRoot = join(userInfo().homedir, '.config', 'ruflo', 'policy-trust');
@@ -87,7 +116,7 @@ function verifyStateAnchor(projectRoot, state) {
87
116
  throw new Error('policy-state-authentication-failed');
88
117
  }
89
118
  }
90
- function writePolicyState(projectRoot, statePath, state) {
119
+ async function writePolicyState(projectRoot, statePath, state) {
91
120
  const anchorPath = trustPaths(projectRoot).anchor;
92
121
  if (state.mode === 'enforce' || existsSync(anchorPath)) {
93
122
  const key = trustKey(projectRoot, true);
@@ -102,15 +131,15 @@ function writePolicyState(projectRoot, statePath, state) {
102
131
  // crash then leaves either a valid pair or an anchored mismatch that
103
132
  // fails closed; it can never leave enforce state silently unanchored.
104
133
  if (!existsSync(anchorPath)) {
105
- writeJsonAtomic(anchorPath, anchor);
106
- writeJsonAtomic(statePath, state);
134
+ await writeJsonAtomic(anchorPath, anchor);
135
+ await writeJsonAtomic(statePath, state);
107
136
  return;
108
137
  }
109
- writeJsonAtomic(statePath, state);
110
- writeJsonAtomic(anchorPath, anchor);
138
+ await writeJsonAtomic(statePath, state);
139
+ await writeJsonAtomic(anchorPath, anchor);
111
140
  return;
112
141
  }
113
- writeJsonAtomic(statePath, state);
142
+ await writeJsonAtomic(statePath, state);
114
143
  }
115
144
  function detectLegacyCapabilities(projectRoot) {
116
145
  const candidates = [
@@ -179,7 +208,7 @@ export async function autoMigratePolicyStateIfNeeded(projectRoot = process.cwd()
179
208
  state.mode = configured;
180
209
  state.configuredMode = configured;
181
210
  }
182
- writePolicyState(projectRoot, target.state, state);
211
+ await writePolicyState(projectRoot, target.state, state);
183
212
  }
184
213
  }
185
214
  finally {
@@ -202,7 +231,7 @@ export async function withPolicyTransaction(projectRoot, operation, options = {}
202
231
  const nextState = engine.exportState();
203
232
  if (!engine.verifyLedger().valid)
204
233
  throw new Error('policy-ledger-verification-failed');
205
- writePolicyState(projectRoot, target.state, nextState);
234
+ await writePolicyState(projectRoot, target.state, nextState);
206
235
  return result;
207
236
  }
208
237
  finally {
File without changes
@@ -159,14 +159,14 @@ export declare const SpawnAgentSchema: z.ZodObject<{
159
159
  timeout: z.ZodOptional<z.ZodNumber>;
160
160
  }, "strip", z.ZodTypeAny, {
161
161
  type: "coder" | "reviewer" | "tester" | "planner" | "researcher" | "security-architect" | "security-auditor" | "memory-specialist" | "swarm-specialist" | "integration-architect" | "performance-engineer" | "core-architect" | "test-architect" | "queen-coordinator" | "project-coordinator";
162
- id?: string | undefined;
163
162
  config?: Record<string, unknown> | undefined;
164
163
  timeout?: number | undefined;
164
+ id?: string | undefined;
165
165
  }, {
166
166
  type: "coder" | "reviewer" | "tester" | "planner" | "researcher" | "security-architect" | "security-auditor" | "memory-specialist" | "swarm-specialist" | "integration-architect" | "performance-engineer" | "core-architect" | "test-architect" | "queen-coordinator" | "project-coordinator";
167
- id?: string | undefined;
168
167
  config?: Record<string, unknown> | undefined;
169
168
  timeout?: number | undefined;
169
+ id?: string | undefined;
170
170
  }>;
171
171
  /**
172
172
  * Task input schema
@@ -181,13 +181,13 @@ export declare const TaskInputSchema: z.ZodObject<{
181
181
  taskId: string;
182
182
  content: string;
183
183
  agentType: "coder" | "reviewer" | "tester" | "planner" | "researcher" | "security-architect" | "security-auditor" | "memory-specialist" | "swarm-specialist" | "integration-architect" | "performance-engineer" | "core-architect" | "test-architect" | "queen-coordinator" | "project-coordinator";
184
- priority?: "low" | "medium" | "high" | "critical" | undefined;
184
+ priority?: "critical" | "high" | "medium" | "low" | undefined;
185
185
  metadata?: Record<string, unknown> | undefined;
186
186
  }, {
187
187
  taskId: string;
188
188
  content: string;
189
189
  agentType: "coder" | "reviewer" | "tester" | "planner" | "researcher" | "security-architect" | "security-auditor" | "memory-specialist" | "swarm-specialist" | "integration-architect" | "performance-engineer" | "core-architect" | "test-architect" | "queen-coordinator" | "project-coordinator";
190
- priority?: "low" | "medium" | "high" | "critical" | undefined;
190
+ priority?: "critical" | "high" | "medium" | "low" | undefined;
191
191
  metadata?: Record<string, unknown> | undefined;
192
192
  }>;
193
193
  /**
@@ -234,16 +234,16 @@ export declare const ExecutorConfigSchema: z.ZodObject<{
234
234
  cwd: z.ZodOptional<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
235
235
  allowSudo: z.ZodDefault<z.ZodBoolean>;
236
236
  }, "strip", z.ZodTypeAny, {
237
- timeout: number;
238
237
  allowedCommands: string[];
238
+ timeout: number;
239
239
  maxBuffer: number;
240
240
  allowSudo: boolean;
241
241
  blockedPatterns?: string[] | undefined;
242
242
  cwd?: string | undefined;
243
243
  }, {
244
244
  allowedCommands: string[];
245
- timeout?: number | undefined;
246
245
  blockedPatterns?: string[] | undefined;
246
+ timeout?: number | undefined;
247
247
  maxBuffer?: number | undefined;
248
248
  cwd?: string | undefined;
249
249
  allowSudo?: boolean | undefined;