claude-flow 3.10.40 → 3.10.42
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/helpers/statusline.cjs +693 -644
- package/.claude/scheduled_tasks.lock +1 -0
- package/package.json +1 -1
- package/v3/@claude-flow/cli/dist/src/commands/hive-mind.js +14 -1
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +28 -1
- package/v3/@claude-flow/cli/dist/src/commands/init.js +7 -1
- package/v3/@claude-flow/cli/dist/src/init/statusline-generator.js +53 -4
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +101 -1
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +2 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +2 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/neural-router.d.ts +49 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/neural-router.js +132 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/router-trajectory.d.ts +69 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/router-trajectory.js +87 -0
- package/v3/@claude-flow/cli/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"sessionId":"d0b433ab-a217-4dde-9ea4-b2c996ac456a","pid":35460,"acquiredAt":1781120150954}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-flow",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.42",
|
|
4
4
|
"description": "Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -306,7 +306,20 @@ async function spawnClaudeCodeInstance(swarmId, swarmName, objective, workers, f
|
|
|
306
306
|
output.printSuccess('Claude Code launched with Hive Mind coordination');
|
|
307
307
|
output.printInfo('The Queen coordinator will orchestrate all worker agents');
|
|
308
308
|
output.writeln(output.dim(`Prompt file saved at: ${promptFile}`));
|
|
309
|
-
|
|
309
|
+
// #2297: await child exit before returning. Without this, the CLI
|
|
310
|
+
// process resolves immediately, finishes, and the still-initializing
|
|
311
|
+
// `claude` child loses its controlling terminal and is killed mid-launch
|
|
312
|
+
// — visible as a stray XTVERSION reply leaking onto the next shell
|
|
313
|
+
// prompt (the terminal queried for capabilities, but the child died
|
|
314
|
+
// before reading the answer). Awaiting also makes the existing
|
|
315
|
+
// claudeProcess.on('exit', ...) log lines actually print, and lets the
|
|
316
|
+
// non-interactive (-p / --non-interactive) path complete only after
|
|
317
|
+
// Claude Code finishes.
|
|
318
|
+
const claudeExitCode = await new Promise((resolve) => {
|
|
319
|
+
claudeProcess.on('exit', (c) => resolve(c ?? 0));
|
|
320
|
+
claudeProcess.on('error', () => resolve(1));
|
|
321
|
+
});
|
|
322
|
+
return { success: claudeExitCode === 0, promptFile };
|
|
310
323
|
}
|
|
311
324
|
else if (dryRun) {
|
|
312
325
|
output.writeln();
|
|
@@ -399,9 +399,20 @@ const postEditCommand = {
|
|
|
399
399
|
metrics,
|
|
400
400
|
timestamp: Date.now(),
|
|
401
401
|
});
|
|
402
|
+
// #2352: the MCP handler returns `{success: false, error: "..."}` on
|
|
403
|
+
// validation failure (e.g. unsupported path shape) without throwing.
|
|
404
|
+
// Surface that explicitly instead of always printing the success line —
|
|
405
|
+
// Windows users were seeing `[OK]` while nothing reached the learning
|
|
406
|
+
// pipeline because absolute paths were rejected upstream.
|
|
407
|
+
const mcpFailed = result && result.success === false;
|
|
408
|
+
const mcpError = result?.error;
|
|
402
409
|
if (ctx.flags.format === 'json') {
|
|
403
410
|
output.printJson(result);
|
|
404
|
-
return { success:
|
|
411
|
+
return { success: !mcpFailed, exitCode: mcpFailed ? 1 : 0, data: result };
|
|
412
|
+
}
|
|
413
|
+
if (mcpFailed) {
|
|
414
|
+
output.printError(`Post-edit hook failed: ${mcpError || 'unknown error'}`);
|
|
415
|
+
return { success: false, exitCode: 1 };
|
|
405
416
|
}
|
|
406
417
|
output.writeln();
|
|
407
418
|
output.printSuccess(`Outcome recorded for ${filePath}`);
|
|
@@ -1560,6 +1571,19 @@ const postTaskCommand = {
|
|
|
1560
1571
|
short: 'a',
|
|
1561
1572
|
description: 'Agent that executed the task',
|
|
1562
1573
|
type: 'string'
|
|
1574
|
+
},
|
|
1575
|
+
{
|
|
1576
|
+
// ADR-147 P2: nested-subagent spawn-tree capture
|
|
1577
|
+
name: 'parent-agent-id',
|
|
1578
|
+
description: 'ID of the parent agent (from Claude Code\'s parent_agent_id OTel span tag). Omit for top-level work.',
|
|
1579
|
+
type: 'string',
|
|
1580
|
+
required: false
|
|
1581
|
+
},
|
|
1582
|
+
{
|
|
1583
|
+
name: 'depth',
|
|
1584
|
+
description: 'Chain depth from root lead session (0 = lead, 1+ = subagent). Used by ADR-147 P3 depth-aware guardrail.',
|
|
1585
|
+
type: 'number',
|
|
1586
|
+
required: false
|
|
1563
1587
|
}
|
|
1564
1588
|
],
|
|
1565
1589
|
examples: [
|
|
@@ -1579,6 +1603,9 @@ const postTaskCommand = {
|
|
|
1579
1603
|
quality: ctx.flags.quality,
|
|
1580
1604
|
agent: ctx.flags.agent,
|
|
1581
1605
|
timestamp: Date.now(),
|
|
1606
|
+
// ADR-147 P2: forward spawn-tree lineage if caller supplied it
|
|
1607
|
+
parentAgentId: ctx.flags.parentAgentId,
|
|
1608
|
+
depth: ctx.flags.depth,
|
|
1582
1609
|
});
|
|
1583
1610
|
if (ctx.flags.format === 'json') {
|
|
1584
1611
|
output.printJson(result);
|
|
@@ -773,7 +773,13 @@ const hooksCommand = {
|
|
|
773
773
|
skills: false,
|
|
774
774
|
commands: false,
|
|
775
775
|
agents: false,
|
|
776
|
-
|
|
776
|
+
// #2350: helpers MUST ship with the hooks subcommand. The hook entries
|
|
777
|
+
// in settings.json point at `.claude/helpers/hook-handler.cjs`; if
|
|
778
|
+
// that file doesn't exist, settings-generator (#1744 fix) drops the
|
|
779
|
+
// hooks block entirely — so the one subcommand whose stated purpose
|
|
780
|
+
// is "Initialize only hooks configuration" produced settings.json
|
|
781
|
+
// with no `hooks` key while reporting "N hooks enabled".
|
|
782
|
+
helpers: true,
|
|
777
783
|
statusline: false,
|
|
778
784
|
mcp: false,
|
|
779
785
|
runtime: false,
|
|
@@ -65,10 +65,51 @@ const CONFIG = {
|
|
|
65
65
|
const CWD = process.cwd();
|
|
66
66
|
|
|
67
67
|
// ─── Delegation cache ───────────────────────────────────────────
|
|
68
|
-
// Cache the CLI JSON result for
|
|
69
|
-
// (
|
|
68
|
+
// Cache the CLI JSON result for 60s so rapid prompt re-renders
|
|
69
|
+
// (Claude Code refreshes the statusline several times a second while
|
|
70
|
+
// streaming) don't re-invoke the CLI each time. #2337: bumped 10s→60s
|
|
71
|
+
// because 10s was far too short for how often Claude Code re-renders.
|
|
70
72
|
const CACHE_FILE = path.join(os.tmpdir(), 'ruflo-statusline-cache-' + require('crypto').createHash('md5').update(CWD).digest('hex').slice(0, 8) + '.json');
|
|
71
|
-
const CACHE_TTL_MS =
|
|
73
|
+
const CACHE_TTL_MS = 60000;
|
|
74
|
+
|
|
75
|
+
// #2337: resolve an already-installed @claude-flow/cli (or ruflo) bin so we
|
|
76
|
+
// can invoke it directly via \`node\`. The previous version called
|
|
77
|
+
// \`npx --yes @claude-flow/cli@latest\` on every uncached render, which forces
|
|
78
|
+
// a registry resolution + cold-start of the entire CLI per render. With
|
|
79
|
+
// multiple concurrent Claude Code sessions this storms the host (reporter
|
|
80
|
+
// saw load average 40-65 on a 12-core box).
|
|
81
|
+
//
|
|
82
|
+
// Returns the absolute path to bin/cli.js or null. Mirrors getPkgVersion()'s
|
|
83
|
+
// path probing (project, monorepo, plugin marketplace, global node_modules
|
|
84
|
+
// including custom-prefix layouts like ~/.npm-global).
|
|
85
|
+
function resolveCliBin() {
|
|
86
|
+
try {
|
|
87
|
+
const home = os.homedir();
|
|
88
|
+
const candidates = [
|
|
89
|
+
path.join(home, '.claude', 'plugins', 'marketplaces', 'ruflo', 'bin', 'cli.js'),
|
|
90
|
+
path.join(CWD, 'node_modules', '@claude-flow', 'cli', 'bin', 'cli.js'),
|
|
91
|
+
path.join(CWD, 'node_modules', 'ruflo', 'bin', 'cli.js'),
|
|
92
|
+
path.join(CWD, 'v3', '@claude-flow', 'cli', 'bin', 'cli.js'),
|
|
93
|
+
];
|
|
94
|
+
try {
|
|
95
|
+
const binDir = path.dirname(process.execPath);
|
|
96
|
+
const globalModuleDirs = [path.join(binDir, '..', 'lib', 'node_modules'), path.join(binDir, 'node_modules')];
|
|
97
|
+
for (const prefix of [process.env.npm_config_prefix, process.env.PREFIX, path.join(home, '.npm-global')]) {
|
|
98
|
+
if (prefix) globalModuleDirs.push(path.join(prefix, 'lib', 'node_modules'));
|
|
99
|
+
}
|
|
100
|
+
for (const gm of globalModuleDirs) {
|
|
101
|
+
candidates.push(
|
|
102
|
+
path.join(gm, 'ruflo', 'bin', 'cli.js'),
|
|
103
|
+
path.join(gm, '@claude-flow', 'cli', 'bin', 'cli.js'),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
} catch { /* ignore */ }
|
|
107
|
+
for (const p of candidates) {
|
|
108
|
+
if (fs.existsSync(p)) return p;
|
|
109
|
+
}
|
|
110
|
+
} catch { /* ignore */ }
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
72
113
|
|
|
73
114
|
function readCache() {
|
|
74
115
|
try {
|
|
@@ -99,8 +140,16 @@ function getStatuslineData() {
|
|
|
99
140
|
if (cached) return cached;
|
|
100
141
|
|
|
101
142
|
try {
|
|
143
|
+
// #2337: prefer an already-installed CLI bin via direct \`node\` invocation
|
|
144
|
+
// — no npx, no registry round-trip, no @latest re-resolve per render.
|
|
145
|
+
// Fall back to \`npx --prefer-offline @claude-flow/cli\` (no @latest) only
|
|
146
|
+
// when nothing is installed locally, so a cold environment still works.
|
|
147
|
+
const cliBin = resolveCliBin();
|
|
148
|
+
const cmd = cliBin
|
|
149
|
+
? '"' + process.execPath + '" "' + cliBin + '" hooks statusline --json 2>/dev/null'
|
|
150
|
+
: 'npx --prefer-offline @claude-flow/cli hooks statusline --json 2>/dev/null';
|
|
102
151
|
const raw = execSync(
|
|
103
|
-
|
|
152
|
+
cmd,
|
|
104
153
|
{ encoding: 'utf-8', timeout: 8000, stdio: ['pipe', 'pipe', 'pipe'], cwd: CWD }
|
|
105
154
|
).trim();
|
|
106
155
|
// The CLI may emit preamble lines before the JSON — find the first '{'.
|
|
@@ -1239,6 +1239,9 @@ export const hooksPostTask = {
|
|
|
1239
1239
|
quality: { type: 'number', description: 'Quality score (0-1)' },
|
|
1240
1240
|
task: { type: 'string', description: 'Task description text (used for learning keyword extraction)' },
|
|
1241
1241
|
storeDecisions: { type: 'boolean', description: 'Also store routing decision in memory DB' },
|
|
1242
|
+
// ADR-147 P2: nested-subagent spawn-tree capture
|
|
1243
|
+
parentAgentId: { type: 'string', description: 'ID of the parent agent (from Claude Code\'s parent_agent_id OTel span tag / x-claude-code-parent-agent-id header). Omit for top-level work.' },
|
|
1244
|
+
depth: { type: 'number', description: 'Chain depth from root lead session (0 = lead, 1+ = subagent). Used by ADR-147 P3 depth-aware guardrail.' },
|
|
1242
1245
|
},
|
|
1243
1246
|
required: ['taskId'],
|
|
1244
1247
|
},
|
|
@@ -1258,6 +1261,22 @@ export const hooksPostTask = {
|
|
|
1258
1261
|
if (!v.valid)
|
|
1259
1262
|
return { success: false, error: v.error };
|
|
1260
1263
|
}
|
|
1264
|
+
// ADR-147 P2: validate spawn-tree lineage if provided
|
|
1265
|
+
const parentAgentId = params.parentAgentId;
|
|
1266
|
+
if (parentAgentId !== undefined) {
|
|
1267
|
+
const v = validateIdentifier(parentAgentId, 'parentAgentId');
|
|
1268
|
+
if (!v.valid)
|
|
1269
|
+
return { success: false, error: v.error };
|
|
1270
|
+
}
|
|
1271
|
+
const depthRaw = params.depth;
|
|
1272
|
+
let depth;
|
|
1273
|
+
if (depthRaw !== undefined && depthRaw !== null) {
|
|
1274
|
+
const n = Number(depthRaw);
|
|
1275
|
+
if (!Number.isInteger(n) || n < 0 || n > 32) {
|
|
1276
|
+
return { success: false, error: 'depth must be a non-negative integer ≤ 32' };
|
|
1277
|
+
}
|
|
1278
|
+
depth = n;
|
|
1279
|
+
}
|
|
1261
1280
|
// Phase 3: Wire recordFeedback through bridge → LearningSystem + ReasoningBank
|
|
1262
1281
|
let feedbackResult = null;
|
|
1263
1282
|
try {
|
|
@@ -1269,6 +1288,9 @@ export const hooksPostTask = {
|
|
|
1269
1288
|
agent,
|
|
1270
1289
|
duration: params.duration || undefined,
|
|
1271
1290
|
patterns: params.patterns || undefined,
|
|
1291
|
+
// ADR-147 P2: forward spawn-tree lineage so it lands in feedback + memory
|
|
1292
|
+
parentAgentId,
|
|
1293
|
+
depth,
|
|
1272
1294
|
});
|
|
1273
1295
|
}
|
|
1274
1296
|
catch {
|
|
@@ -2643,7 +2665,81 @@ export const hooksTrajectoryEnd = {
|
|
|
2643
2665
|
}
|
|
2644
2666
|
catch { /* intelligence module not loadable — keep sona-only behaviour */ }
|
|
2645
2667
|
}
|
|
2668
|
+
// #2351: when an agent calls trajectory-end with no recorded steps but a
|
|
2669
|
+
// non-empty `feedback` string, the feedback was previously dropped on the
|
|
2670
|
+
// floor — `patternsExtracted` reported 0 and `pattern-search` never
|
|
2671
|
+
// surfaced it. Step-less trajectories are the common case for LLM agents
|
|
2672
|
+
// (nothing forces step logging mid-task), and feedback is often the most
|
|
2673
|
+
// distilled lesson available. Route it through the same store + embed
|
|
2674
|
+
// path that pattern-store uses so it becomes searchable. Best-effort:
|
|
2675
|
+
// failures here must not turn the trajectory-end call itself into a
|
|
2676
|
+
// failure — the trajectory record was already persisted above.
|
|
2677
|
+
let feedbackDistilled = { stored: false };
|
|
2678
|
+
const hasSteps = !!trajectory && trajectory.steps.length > 0;
|
|
2679
|
+
const trimmedFeedback = typeof feedback === 'string' ? feedback.trim() : '';
|
|
2680
|
+
if (trajectory && !hasSteps && trimmedFeedback.length > 0) {
|
|
2681
|
+
const distilledPatternId = `pattern-feedback-${trajectoryId}-${Date.now()}`;
|
|
2682
|
+
const patternMetadata = {
|
|
2683
|
+
sourceTrajectoryId: trajectoryId,
|
|
2684
|
+
task: trajectory.task,
|
|
2685
|
+
agent: trajectory.agent,
|
|
2686
|
+
outcome: success ? 'success' : 'failure',
|
|
2687
|
+
distilledFrom: 'trajectory-end-feedback',
|
|
2688
|
+
};
|
|
2689
|
+
// Modest default confidence — step-less feedback hasn't been validated
|
|
2690
|
+
// by execution evidence the way a multi-step trajectory has.
|
|
2691
|
+
const feedbackConfidence = success ? 0.6 : 0.4;
|
|
2692
|
+
try {
|
|
2693
|
+
const bridge = await import('../memory/memory-bridge.js');
|
|
2694
|
+
const rb = await bridge.bridgeStorePattern({
|
|
2695
|
+
pattern: trimmedFeedback,
|
|
2696
|
+
type: 'trajectory-feedback',
|
|
2697
|
+
confidence: feedbackConfidence,
|
|
2698
|
+
metadata: patternMetadata,
|
|
2699
|
+
});
|
|
2700
|
+
if (rb?.success) {
|
|
2701
|
+
feedbackDistilled = { stored: true, patternId: rb.patternId, controller: rb.controller };
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
catch {
|
|
2705
|
+
// Bridge unavailable — fall through to direct store
|
|
2706
|
+
}
|
|
2707
|
+
if (!feedbackDistilled.stored) {
|
|
2708
|
+
try {
|
|
2709
|
+
const storeFn = await getRealStoreFunction();
|
|
2710
|
+
if (storeFn) {
|
|
2711
|
+
const r = await storeFn({
|
|
2712
|
+
key: distilledPatternId,
|
|
2713
|
+
value: JSON.stringify({
|
|
2714
|
+
pattern: trimmedFeedback,
|
|
2715
|
+
type: 'trajectory-feedback',
|
|
2716
|
+
confidence: feedbackConfidence,
|
|
2717
|
+
metadata: patternMetadata,
|
|
2718
|
+
timestamp: endedAt,
|
|
2719
|
+
}),
|
|
2720
|
+
namespace: 'pattern',
|
|
2721
|
+
generateEmbeddingFlag: true,
|
|
2722
|
+
tags: [
|
|
2723
|
+
'trajectory-feedback',
|
|
2724
|
+
success ? 'success' : 'failure',
|
|
2725
|
+
`confidence-${Math.round(feedbackConfidence * 100)}`,
|
|
2726
|
+
],
|
|
2727
|
+
});
|
|
2728
|
+
if (r?.success) {
|
|
2729
|
+
feedbackDistilled = { stored: true, patternId: r.id || distilledPatternId, controller: 'store-fallback' };
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
catch {
|
|
2734
|
+
// Both paths failed — leave feedbackDistilled.stored = false.
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2646
2738
|
const learningTimeMs = Date.now() - startTime;
|
|
2739
|
+
// patternsExtracted now reflects either recorded steps (the original
|
|
2740
|
+
// semantics) OR a distilled feedback pattern (#2351), so step-less
|
|
2741
|
+
// trajectories with useful feedback no longer report 0.
|
|
2742
|
+
const patternsExtracted = (trajectory?.steps.length || 0) + (feedbackDistilled.stored ? 1 : 0);
|
|
2647
2743
|
return {
|
|
2648
2744
|
trajectoryId,
|
|
2649
2745
|
success,
|
|
@@ -2656,7 +2752,11 @@ export const hooksTrajectoryEnd = {
|
|
|
2656
2752
|
sonaConfidence: sonaResult.confidence || undefined,
|
|
2657
2753
|
ewcConsolidation: ewcResult.consolidated,
|
|
2658
2754
|
ewcPenalty: ewcResult.penalty || undefined,
|
|
2659
|
-
patternsExtracted
|
|
2755
|
+
patternsExtracted,
|
|
2756
|
+
feedbackDistilled: feedbackDistilled.stored ? {
|
|
2757
|
+
patternId: feedbackDistilled.patternId,
|
|
2758
|
+
controller: feedbackDistilled.controller,
|
|
2759
|
+
} : undefined,
|
|
2660
2760
|
learningTimeMs,
|
|
2661
2761
|
globalStatsTrajectoriesDelta: globalStatsDelta, // Round B: was 0, now reflects
|
|
2662
2762
|
},
|
|
@@ -1422,6 +1422,8 @@ export async function bridgeRecordFeedback(options) {
|
|
|
1422
1422
|
await learningSystem.recordFeedback({
|
|
1423
1423
|
taskId: options.taskId, success: options.success, quality: options.quality,
|
|
1424
1424
|
agent: options.agent, duration: options.duration, timestamp: Date.now(),
|
|
1425
|
+
// ADR-147 P2: forward spawn-tree lineage if present
|
|
1426
|
+
parentAgentId: options.parentAgentId, depth: options.depth,
|
|
1425
1427
|
});
|
|
1426
1428
|
controller = 'learningSystem';
|
|
1427
1429
|
updated++;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neural routing scaffold — `@ruvector/tiny-dancer` FastGRNN seam (#2334 Phase 1)
|
|
3
|
+
*
|
|
4
|
+
* Wires the optional neural path that ADR-026 originally described, behind a
|
|
5
|
+
* double gate that is OFF by default:
|
|
6
|
+
*
|
|
7
|
+
* CLAUDE_FLOW_ROUTER_NEURAL=1 — opt in to the neural path
|
|
8
|
+
* CLAUDE_FLOW_ROUTER_MODEL_PATH=<x.safetensors> — trained FastGRNN artifact
|
|
9
|
+
*
|
|
10
|
+
* Both must be set; otherwise `tryNeuralRoute` returns `null` immediately and
|
|
11
|
+
* the caller stays on the shipped heuristic + Thompson-bandit path. When the
|
|
12
|
+
* gate is open but anything fails (package not installed — it is an
|
|
13
|
+
* optionalDependency per ADR-124 — artifact missing/incompatible, runtime
|
|
14
|
+
* error), this module degrades gracefully: it returns `null`, never throws,
|
|
15
|
+
* and the caller reports `routedBy: 'bandit-fallback'` so the active path is
|
|
16
|
+
* observable rather than inferred from import success (ADR-086/074).
|
|
17
|
+
*
|
|
18
|
+
* Candidate modeling (#2334 Q3, provisional): the 3 model tiers are encoded as
|
|
19
|
+
* fixed candidates with deterministic placeholder embeddings (orthogonal-ish
|
|
20
|
+
* one-hot-block vectors). This is explicitly provisional — the trained Phase 2
|
|
21
|
+
* artifact defines what candidate embeddings mean, and this encoding is the
|
|
22
|
+
* scaffolding default until the maintainers answer #2334's candidate-modeling
|
|
23
|
+
* question. Until a real artifact exists the gate stays closed in practice, so
|
|
24
|
+
* the placeholder never influences routing.
|
|
25
|
+
*
|
|
26
|
+
* @module neural-router
|
|
27
|
+
*/
|
|
28
|
+
/** The three routable tiers — 'inherit' is never a neural candidate. */
|
|
29
|
+
export type NeuralRoutableModel = 'haiku' | 'sonnet' | 'opus';
|
|
30
|
+
export interface NeuralRouteDecision {
|
|
31
|
+
model: NeuralRoutableModel;
|
|
32
|
+
confidence: number;
|
|
33
|
+
uncertainty: number;
|
|
34
|
+
inferenceTimeUs: number;
|
|
35
|
+
}
|
|
36
|
+
/** True when the user has opted in AND pointed at a model artifact. */
|
|
37
|
+
export declare function neuralRoutingEnabled(): boolean;
|
|
38
|
+
/** Reset cached state — for tests. */
|
|
39
|
+
export declare function resetNeuralRouter(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Attempt a neural routing decision for the given task embedding.
|
|
42
|
+
*
|
|
43
|
+
* Returns `null` (never throws) when the gate is closed, the package or
|
|
44
|
+
* artifact is unavailable, or inference fails — callers fall back to the
|
|
45
|
+
* bandit and report `routedBy: 'bandit-fallback'` (when the gate was open)
|
|
46
|
+
* or `'heuristic'` (when it never was).
|
|
47
|
+
*/
|
|
48
|
+
export declare function tryNeuralRoute(embedding: number[]): Promise<NeuralRouteDecision | null>;
|
|
49
|
+
//# sourceMappingURL=neural-router.d.ts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neural routing scaffold — `@ruvector/tiny-dancer` FastGRNN seam (#2334 Phase 1)
|
|
3
|
+
*
|
|
4
|
+
* Wires the optional neural path that ADR-026 originally described, behind a
|
|
5
|
+
* double gate that is OFF by default:
|
|
6
|
+
*
|
|
7
|
+
* CLAUDE_FLOW_ROUTER_NEURAL=1 — opt in to the neural path
|
|
8
|
+
* CLAUDE_FLOW_ROUTER_MODEL_PATH=<x.safetensors> — trained FastGRNN artifact
|
|
9
|
+
*
|
|
10
|
+
* Both must be set; otherwise `tryNeuralRoute` returns `null` immediately and
|
|
11
|
+
* the caller stays on the shipped heuristic + Thompson-bandit path. When the
|
|
12
|
+
* gate is open but anything fails (package not installed — it is an
|
|
13
|
+
* optionalDependency per ADR-124 — artifact missing/incompatible, runtime
|
|
14
|
+
* error), this module degrades gracefully: it returns `null`, never throws,
|
|
15
|
+
* and the caller reports `routedBy: 'bandit-fallback'` so the active path is
|
|
16
|
+
* observable rather than inferred from import success (ADR-086/074).
|
|
17
|
+
*
|
|
18
|
+
* Candidate modeling (#2334 Q3, provisional): the 3 model tiers are encoded as
|
|
19
|
+
* fixed candidates with deterministic placeholder embeddings (orthogonal-ish
|
|
20
|
+
* one-hot-block vectors). This is explicitly provisional — the trained Phase 2
|
|
21
|
+
* artifact defines what candidate embeddings mean, and this encoding is the
|
|
22
|
+
* scaffolding default until the maintainers answer #2334's candidate-modeling
|
|
23
|
+
* question. Until a real artifact exists the gate stays closed in practice, so
|
|
24
|
+
* the placeholder never influences routing.
|
|
25
|
+
*
|
|
26
|
+
* @module neural-router
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync } from 'fs';
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Gate & lifecycle
|
|
31
|
+
// ============================================================================
|
|
32
|
+
/** True when the user has opted in AND pointed at a model artifact. */
|
|
33
|
+
export function neuralRoutingEnabled() {
|
|
34
|
+
return process.env.CLAUDE_FLOW_ROUTER_NEURAL === '1'
|
|
35
|
+
&& !!process.env.CLAUDE_FLOW_ROUTER_MODEL_PATH;
|
|
36
|
+
}
|
|
37
|
+
// Cached router instance + a sticky failure latch so a broken install/artifact
|
|
38
|
+
// costs one failed load, not one per routing call.
|
|
39
|
+
let routerInstance = null;
|
|
40
|
+
let loadFailed = false;
|
|
41
|
+
/** Reset cached state — for tests. */
|
|
42
|
+
export function resetNeuralRouter() {
|
|
43
|
+
routerInstance = null;
|
|
44
|
+
loadFailed = false;
|
|
45
|
+
}
|
|
46
|
+
async function loadRouter() {
|
|
47
|
+
if (routerInstance)
|
|
48
|
+
return routerInstance;
|
|
49
|
+
if (loadFailed)
|
|
50
|
+
return null;
|
|
51
|
+
const modelPath = process.env.CLAUDE_FLOW_ROUTER_MODEL_PATH;
|
|
52
|
+
if (!modelPath || !existsSync(modelPath)) {
|
|
53
|
+
loadFailed = true;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
// Dynamic import of an optionalDependency (ADR-124): absent on installs
|
|
58
|
+
// where the native binding failed or was skipped — degrade, don't throw.
|
|
59
|
+
const mod = await import('@ruvector/tiny-dancer');
|
|
60
|
+
const RouterCtor = mod.Router;
|
|
61
|
+
if (!RouterCtor) {
|
|
62
|
+
loadFailed = true;
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
routerInstance = new RouterCtor({ modelPath });
|
|
66
|
+
return routerInstance;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
loadFailed = true;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Candidate encoding (provisional — see header + #2334 Q3)
|
|
75
|
+
// ============================================================================
|
|
76
|
+
const TIER_ORDER = ['haiku', 'sonnet', 'opus'];
|
|
77
|
+
/**
|
|
78
|
+
* Deterministic placeholder embedding for a tier candidate: a block one-hot
|
|
79
|
+
* over the embedding dimensionality. Replaced by whatever the Phase 2 trained
|
|
80
|
+
* artifact defines as candidate space.
|
|
81
|
+
*/
|
|
82
|
+
function tierCandidateEmbedding(tierIndex, dim) {
|
|
83
|
+
const v = new Array(dim).fill(0);
|
|
84
|
+
const block = Math.max(1, Math.floor(dim / TIER_ORDER.length));
|
|
85
|
+
const start = tierIndex * block;
|
|
86
|
+
for (let i = start; i < Math.min(start + block, dim); i++)
|
|
87
|
+
v[i] = 1 / Math.sqrt(block);
|
|
88
|
+
return v;
|
|
89
|
+
}
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Routing
|
|
92
|
+
// ============================================================================
|
|
93
|
+
/**
|
|
94
|
+
* Attempt a neural routing decision for the given task embedding.
|
|
95
|
+
*
|
|
96
|
+
* Returns `null` (never throws) when the gate is closed, the package or
|
|
97
|
+
* artifact is unavailable, or inference fails — callers fall back to the
|
|
98
|
+
* bandit and report `routedBy: 'bandit-fallback'` (when the gate was open)
|
|
99
|
+
* or `'heuristic'` (when it never was).
|
|
100
|
+
*/
|
|
101
|
+
export async function tryNeuralRoute(embedding) {
|
|
102
|
+
if (!neuralRoutingEnabled())
|
|
103
|
+
return null;
|
|
104
|
+
if (!embedding || embedding.length === 0)
|
|
105
|
+
return null;
|
|
106
|
+
const router = await loadRouter();
|
|
107
|
+
if (!router)
|
|
108
|
+
return null;
|
|
109
|
+
try {
|
|
110
|
+
const response = await router.route({
|
|
111
|
+
queryEmbedding: embedding,
|
|
112
|
+
candidates: TIER_ORDER.map((tier, i) => ({
|
|
113
|
+
id: tier,
|
|
114
|
+
embedding: tierCandidateEmbedding(i, embedding.length),
|
|
115
|
+
metadata: JSON.stringify({ tier }),
|
|
116
|
+
})),
|
|
117
|
+
});
|
|
118
|
+
const best = response.decisions?.[0];
|
|
119
|
+
if (!best || !TIER_ORDER.includes(best.candidateId))
|
|
120
|
+
return null;
|
|
121
|
+
return {
|
|
122
|
+
model: best.candidateId,
|
|
123
|
+
confidence: best.confidence,
|
|
124
|
+
uncertainty: best.uncertainty,
|
|
125
|
+
inferenceTimeUs: response.inferenceTimeUs,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=neural-router.js.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router trajectory collection (#2334 Phase 1)
|
|
3
|
+
*
|
|
4
|
+
* Opt-in per-decision dataset collection for the model router. The persisted
|
|
5
|
+
* bandit state (`.swarm/model-router-state.json`) keeps only aggregates —
|
|
6
|
+
* 9 Beta(α,β) cells and a capped/truncated history — which is not trainable
|
|
7
|
+
* material for the Phase 2 FastGRNN tier-classifier. This sidecar captures
|
|
8
|
+
* the per-example rows that training needs:
|
|
9
|
+
*
|
|
10
|
+
* decision rows: { taskHash, task, embedding?, complexity, features,
|
|
11
|
+
* model, confidence, uncertainty, routedBy, ts }
|
|
12
|
+
* outcome rows: { taskHash, model, outcome, ts }
|
|
13
|
+
*
|
|
14
|
+
* joined offline on `taskHash` (sha256-16 of the task text).
|
|
15
|
+
*
|
|
16
|
+
* OFF by default. Enable with CLAUDE_FLOW_ROUTER_TRAJECTORY=1. Rows append to
|
|
17
|
+
* `.swarm/model-router-trajectories.jsonl` — local-only, same trust domain as
|
|
18
|
+
* the existing state file, but unlike it the rows contain full task text (up
|
|
19
|
+
* to 500 chars) and raw embeddings, which is why this is opt-in rather than
|
|
20
|
+
* always-on.
|
|
21
|
+
*
|
|
22
|
+
* Writes are best-effort: any fs error is swallowed (collection must never
|
|
23
|
+
* break routing), matching the state-file behavior in model-router.ts.
|
|
24
|
+
*
|
|
25
|
+
* @module router-trajectory
|
|
26
|
+
*/
|
|
27
|
+
export declare const TRAJECTORY_FILE = ".swarm/model-router-trajectories.jsonl";
|
|
28
|
+
export declare function trajectoryCollectionEnabled(): boolean;
|
|
29
|
+
/** Join key: first 16 hex chars of sha256(task). */
|
|
30
|
+
export declare function taskHash(task: string): string;
|
|
31
|
+
export interface TrajectoryDecisionRow {
|
|
32
|
+
v: number;
|
|
33
|
+
type: 'decision';
|
|
34
|
+
ts: string;
|
|
35
|
+
taskHash: string;
|
|
36
|
+
/** Task text, capped at 500 chars (cf. learningHistory's 100). */
|
|
37
|
+
task: string;
|
|
38
|
+
/** Raw embedding when one was threaded through route(); else omitted. */
|
|
39
|
+
embedding?: number[];
|
|
40
|
+
complexity: number;
|
|
41
|
+
features: {
|
|
42
|
+
lexicalComplexity: number;
|
|
43
|
+
semanticDepth: number;
|
|
44
|
+
taskScope: number;
|
|
45
|
+
uncertaintyLevel: number;
|
|
46
|
+
};
|
|
47
|
+
model: string;
|
|
48
|
+
confidence: number;
|
|
49
|
+
uncertainty: number;
|
|
50
|
+
routedBy: string;
|
|
51
|
+
}
|
|
52
|
+
export interface TrajectoryOutcomeRow {
|
|
53
|
+
v: number;
|
|
54
|
+
type: 'outcome';
|
|
55
|
+
ts: string;
|
|
56
|
+
taskHash: string;
|
|
57
|
+
model: string;
|
|
58
|
+
outcome: 'success' | 'failure' | 'escalated';
|
|
59
|
+
}
|
|
60
|
+
export declare function recordTrajectoryDecision(task: string, embedding: number[] | undefined, complexity: TrajectoryDecisionRow['features'] & {
|
|
61
|
+
score: number;
|
|
62
|
+
}, decision: {
|
|
63
|
+
model: string;
|
|
64
|
+
confidence: number;
|
|
65
|
+
uncertainty: number;
|
|
66
|
+
routedBy: string;
|
|
67
|
+
}): void;
|
|
68
|
+
export declare function recordTrajectoryOutcome(task: string, model: string, outcome: 'success' | 'failure' | 'escalated'): void;
|
|
69
|
+
//# sourceMappingURL=router-trajectory.d.ts.map
|