claude-flow 3.17.0 → 3.18.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-flow",
3
- "version": "3.17.0",
3
+ "version": "3.18.0",
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",
@@ -98,12 +98,18 @@
98
98
  "qs": ">=6.15.2",
99
99
  "protobufjs": ">=8.2.0",
100
100
  "uuid": ">=14.0.0",
101
- "@opentelemetry/core": "1.25.1",
102
- "@opentelemetry/resources": "1.25.1",
103
- "@opentelemetry/sdk-trace-base": "1.25.1",
104
- "@opentelemetry/sdk-node": ">=0.218.0",
101
+ "@opentelemetry/core": ">=2.8.0",
102
+ "@opentelemetry/resources": ">=2.8.0",
103
+ "@opentelemetry/sdk-trace-base": ">=2.8.0",
104
+ "@opentelemetry/sdk-node": ">=0.220.0",
105
105
  "@opentelemetry/auto-instrumentations-node": ">=0.75.0",
106
- "@opentelemetry/exporter-prometheus": ">=0.217.0",
106
+ "@opentelemetry/exporter-prometheus": ">=0.220.0",
107
+ "@opentelemetry/exporter-trace-otlp-grpc": ">=0.220.0",
108
+ "@opentelemetry/exporter-trace-otlp-http": ">=0.220.0",
109
+ "@opentelemetry/exporter-trace-otlp-proto": ">=0.220.0",
110
+ "@opentelemetry/otlp-exporter-base": ">=0.220.0",
111
+ "@opentelemetry/otlp-grpc-exporter-base": ">=0.220.0",
112
+ "@opentelemetry/otlp-transformer": ">=0.220.0",
107
113
  "axios": ">=1.13.2",
108
114
  "fast-uri": ">=3.1.0",
109
115
  "vite": ">=8.0.16",
@@ -33,6 +33,21 @@ function validateScore(value, defaultVal) {
33
33
  return defaultVal;
34
34
  return Math.max(0, Math.min(1, value));
35
35
  }
36
+ /**
37
+ * Validate an optional ISO-8601 timestamp param (temporal validity fields).
38
+ * Returns { value } when absent or valid, { error } when present but unparseable.
39
+ */
40
+ function validateIsoTimestamp(value, name) {
41
+ if (value === undefined || value === null)
42
+ return {};
43
+ if (typeof value !== 'string' || value.length === 0 || value.length > 64) {
44
+ return { error: `${name} must be a non-empty ISO-8601 timestamp string (max 64 chars)` };
45
+ }
46
+ if (Number.isNaN(Date.parse(value))) {
47
+ return { error: `${name} is not a parseable ISO-8601 timestamp: ${value.substring(0, 64)}` };
48
+ }
49
+ return { value };
50
+ }
36
51
  function sanitizeError(error) {
37
52
  if (error instanceof Error) {
38
53
  // Strip filesystem paths from error messages
@@ -616,6 +631,18 @@ export const agentdbHierarchicalStore = {
616
631
  enum: ['working', 'episodic', 'semantic'],
617
632
  default: 'working',
618
633
  },
634
+ validFrom: {
635
+ type: 'string',
636
+ description: 'Optional ISO-8601 timestamp from which this fact is valid (temporal validity — Zep/Graphiti-style). Omit for always-valid.',
637
+ },
638
+ validUntil: {
639
+ type: 'string',
640
+ description: 'Optional ISO-8601 timestamp after which this fact is no longer valid. Expired facts are hidden from recall unless includeExpired=true.',
641
+ },
642
+ supersedes: {
643
+ type: 'string',
644
+ description: 'Optional id (or key) of an existing entry this fact supersedes. The old entry is INVALIDATED (stamped validUntil=now + supersededBy=<new id>), not deleted — it stays auditable via recall includeExpired=true.',
645
+ },
619
646
  },
620
647
  required: ['key', 'value'],
621
648
  },
@@ -642,8 +669,26 @@ export const agentdbHierarchicalStore = {
642
669
  if (!['working', 'episodic', 'semantic'].includes(tier)) {
643
670
  return { success: false, error: `Invalid tier: ${tier}. Must be working, episodic, or semantic` };
644
671
  }
672
+ // Temporal validity fields (all optional, backward compatible)
673
+ const validFrom = validateIsoTimestamp(params.validFrom, 'validFrom');
674
+ if (validFrom.error)
675
+ return { success: false, error: validFrom.error };
676
+ const validUntil = validateIsoTimestamp(params.validUntil, 'validUntil');
677
+ if (validUntil.error)
678
+ return { success: false, error: validUntil.error };
679
+ const supersedes = params.supersedes !== undefined
680
+ ? validateString(params.supersedes, 'supersedes', 1000)
681
+ : undefined;
682
+ if (params.supersedes !== undefined && !supersedes) {
683
+ return { success: false, error: 'supersedes must be a non-empty string (entry id or key, max 1KB)' };
684
+ }
645
685
  const bridge = await getBridge();
646
- const result = await bridge.bridgeHierarchicalStore({ key, value, tier });
686
+ const result = await bridge.bridgeHierarchicalStore({
687
+ key, value, tier,
688
+ validFrom: validFrom.value,
689
+ validUntil: validUntil.value,
690
+ supersedes: supersedes ?? undefined,
691
+ });
647
692
  return result ?? { success: false, error: 'AgentDB bridge not available. Use memory_store/memory_search instead.' };
648
693
  }
649
694
  catch (error) {
@@ -661,6 +706,11 @@ export const agentdbHierarchicalRecall = {
661
706
  query: { type: 'string', description: 'Recall query' },
662
707
  tier: { type: 'string', description: 'Filter by tier (working, episodic, semantic)' },
663
708
  topK: { type: 'number', description: 'Number of results (default: 5)' },
709
+ includeExpired: {
710
+ type: 'boolean',
711
+ description: 'Include temporally-invalid entries (superseded, expired, or not-yet-valid). Audit escape hatch — default false.',
712
+ default: false,
713
+ },
664
714
  },
665
715
  required: ['query'],
666
716
  },
@@ -686,6 +736,7 @@ export const agentdbHierarchicalRecall = {
686
736
  query,
687
737
  tier: tier ?? undefined,
688
738
  topK: validatePositiveInt(params.topK, 5, MAX_TOP_K),
739
+ includeExpired: params.includeExpired === true,
689
740
  });
690
741
  return result ?? { results: [], error: 'AgentDB bridge not available. Use memory_search instead.' };
691
742
  }
@@ -12,7 +12,11 @@
12
12
  * (for trajectory hooks + RVF), and the bridged `memory` namespace (for
13
13
  * AgentDB index). They do not inline a replay engine; replay
14
14
  * enumerates trajectory steps and returns them for the caller to dispatch.
15
- * - Pinned to ruvector@0.2.25 to match `ruflo-ruvector` ADR-0001.
15
+ * - ruvector resolution is local-first: if a locally installed `ruvector`
16
+ * package is found (the CLI already depends on ruvector@0.2.27), its bin
17
+ * is spawned directly with `node`; otherwise we fall back to
18
+ * `npx -y ruvector@0.2.27`. The previous 0.2.25 pin forced cold npx
19
+ * downloads of a *second* ruvector version — 4 per browser session.
16
20
  * - Best-effort: missing dependencies (no `ruvector`, no `agent-browser`,
17
21
  * no AgentDB controller) degrade gracefully with a structured error
18
22
  * rather than a process crash.
@@ -12,13 +12,21 @@
12
12
  * (for trajectory hooks + RVF), and the bridged `memory` namespace (for
13
13
  * AgentDB index). They do not inline a replay engine; replay
14
14
  * enumerates trajectory steps and returns them for the caller to dispatch.
15
- * - Pinned to ruvector@0.2.25 to match `ruflo-ruvector` ADR-0001.
15
+ * - ruvector resolution is local-first: if a locally installed `ruvector`
16
+ * package is found (the CLI already depends on ruvector@0.2.27), its bin
17
+ * is spawned directly with `node`; otherwise we fall back to
18
+ * `npx -y ruvector@0.2.27`. The previous 0.2.25 pin forced cold npx
19
+ * downloads of a *second* ruvector version — 4 per browser session.
16
20
  * - Best-effort: missing dependencies (no `ruvector`, no `agent-browser`,
17
21
  * no AgentDB controller) degrade gracefully with a structured error
18
22
  * rather than a process crash.
19
23
  */
20
24
  import { validateIdentifier, validateText } from './validate-input.js';
21
- const RUVECTOR_PIN = 'ruvector@0.2.25';
25
+ // Pin matches the version in this package's own dependency tree so the npx
26
+ // fallback never downloads a second ruvector. Subcommand surface verified
27
+ // against ruvector@0.2.27 --help: `rvf create/compact/status/derive/segments`
28
+ // and `hooks trajectory-begin/step/end` all exist with the flags used below.
29
+ const RUVECTOR_PIN = 'ruvector@0.2.27';
22
30
  const RVF_DIR_DEFAULT = '.ruflo/browser-sessions';
23
31
  async function shell(cmd, args, opts = {}) {
24
32
  const { execFile } = await import('node:child_process');
@@ -41,6 +49,57 @@ async function shell(cmd, args, opts = {}) {
41
49
  };
42
50
  }
43
51
  }
52
+ /**
53
+ * Resolve the locally installed ruvector CLI (memoized). Returns the absolute
54
+ * path to its bin script, or null when ruvector is not installed locally.
55
+ * Spawning `node <bin>` directly avoids the ~2-8s cold `npx -y ruvector@…`
56
+ * download that previously hit every ruvector shell-out (4 per session).
57
+ */
58
+ let ruvectorCliPromise = null;
59
+ function resolveLocalRuvectorCli() {
60
+ if (ruvectorCliPromise)
61
+ return ruvectorCliPromise;
62
+ ruvectorCliPromise = (async () => {
63
+ try {
64
+ const { createRequire } = await import('node:module');
65
+ const path = await import('node:path');
66
+ const { readFile } = await import('node:fs/promises');
67
+ const req = createRequire(import.meta.url);
68
+ const entry = req.resolve('ruvector');
69
+ // Walk up from the resolved entry to the package root (package.json
70
+ // itself may not be exported, so we can't require.resolve it directly).
71
+ let dir = path.dirname(entry);
72
+ for (let i = 0; i < 6; i++) {
73
+ try {
74
+ const pkg = JSON.parse(await readFile(path.join(dir, 'package.json'), 'utf-8'));
75
+ if (pkg.name === 'ruvector') {
76
+ const bin = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.ruvector;
77
+ return bin ? path.join(dir, bin) : null;
78
+ }
79
+ }
80
+ catch {
81
+ // no package.json at this level — keep walking
82
+ }
83
+ const parent = path.dirname(dir);
84
+ if (parent === dir)
85
+ break;
86
+ dir = parent;
87
+ }
88
+ }
89
+ catch {
90
+ // ruvector not installed locally
91
+ }
92
+ return null;
93
+ })();
94
+ return ruvectorCliPromise;
95
+ }
96
+ /** Run a ruvector CLI command: local install first, npx pin as fallback. */
97
+ async function ruvector(args, opts = {}) {
98
+ const cli = await resolveLocalRuvectorCli();
99
+ if (cli)
100
+ return shell(process.execPath, [cli, ...args], opts);
101
+ return shell('npx', ['-y', RUVECTOR_PIN, ...args], opts);
102
+ }
44
103
  async function ensureSessionsDir() {
45
104
  const { mkdir } = await import('node:fs/promises');
46
105
  const path = await import('node:path');
@@ -110,11 +169,21 @@ export const browserSessionTools = [
110
169
  //
111
170
  // 384 matches the MiniLM-L6 default used elsewhere in the
112
171
  // toolchain (ONNX embedder + AgentDB vector indexes).
113
- const rvf = await shell('npx', ['-y', RUVECTOR_PIN, 'rvf', 'create', rvfPath, '--dimension', '384'], { timeout: 60000 });
172
+ const rvf = await ruvector(['rvf', 'create', rvfPath, '--dimension', '384'], { timeout: 60000 });
114
173
  if (!rvf.success)
115
174
  return fail('rvf create failed', { detail: rvf.error, stderr: rvf.stderr, sessionId, rvfPath });
116
- // 2. trajectory-begin
117
- const tb = await shell('npx', ['-y', RUVECTOR_PIN, 'hooks', 'trajectory-begin', '--session-id', sessionId, '--task', input.task]);
175
+ // 2. trajectory-begin.
176
+ // Flag names verified against ruvector 0.2.25 AND 0.2.27 --help:
177
+ // trajectory-begin takes `-c/--context` + `-a/--agent` — the
178
+ // `--session-id`/`--task` flags previously passed here were never
179
+ // valid on either version (commander exited with "unknown option"),
180
+ // so every trajectory call failed. Same bug class as the #2015
181
+ // `rvf create --kind` fix documented above. The session id is folded
182
+ // into the context string since ruvector tracks a single current
183
+ // trajectory per project.
184
+ const tb = await ruvector(['hooks', 'trajectory-begin',
185
+ '--context', `${sessionId}: ${input.task}`,
186
+ '--agent', 'browser-session']);
118
187
  if (!tb.success)
119
188
  return fail('trajectory-begin failed', { detail: tb.error, stderr: tb.stderr, sessionId, rvfPath });
120
189
  // 3. browser_open via agent-browser
@@ -125,11 +194,11 @@ export const browserSessionTools = [
125
194
  return fail('browser open failed', { detail: npxBo.error, stderr: npxBo.stderr, sessionId, rvfPath });
126
195
  }
127
196
  }
128
- // 4. log the open as the first trajectory step
129
- await shell('npx', ['-y', RUVECTOR_PIN, 'hooks', 'trajectory-step',
130
- '--session-id', sessionId,
131
- '--action', 'browser_open',
132
- '--args', JSON.stringify({ url: input.url }),
197
+ // 4. log the open as the first trajectory step.
198
+ // trajectory-step takes `--action`/`--result` only (no --session-id /
199
+ // --args on 0.2.25 or 0.2.27) — args are folded into the action string.
200
+ await ruvector(['hooks', 'trajectory-step',
201
+ '--action', `browser_open ${JSON.stringify({ url: input.url })}`,
133
202
  '--result', 'ok']);
134
203
  return ok({
135
204
  sessionId,
@@ -167,14 +236,19 @@ export const browserSessionTools = [
167
236
  const verdict = input.verdict;
168
237
  if (!['pass', 'fail', 'partial'].includes(verdict))
169
238
  return fail(`invalid verdict: ${verdict}`);
170
- // 1. trajectory-end
171
- const te = await shell('npx', ['-y', RUVECTOR_PIN, 'hooks', 'trajectory-end',
172
- '--session-id', input.session,
173
- '--verdict', verdict]);
239
+ // 1. trajectory-end.
240
+ // trajectory-end takes `--success` + `--quality <0-1>` (no
241
+ // --session-id / --verdict on 0.2.25 or 0.2.27) — the verdict maps to
242
+ // quality: pass=1 (+ --success), partial=0.5, fail=0.
243
+ const teArgs = ['hooks', 'trajectory-end',
244
+ '--quality', verdict === 'pass' ? '1' : verdict === 'partial' ? '0.5' : '0'];
245
+ if (verdict === 'pass')
246
+ teArgs.push('--success');
247
+ const te = await ruvector(teArgs);
174
248
  if (!te.success)
175
249
  return fail('trajectory-end failed', { detail: te.error, stderr: te.stderr });
176
250
  // 2. rvf compact
177
- const compact = await shell('npx', ['-y', RUVECTOR_PIN, 'rvf', 'compact', input.rvf_path]);
251
+ const compact = await ruvector(['rvf', 'compact', input.rvf_path]);
178
252
  if (!compact.success)
179
253
  return fail('rvf compact failed', { detail: compact.error, stderr: compact.stderr });
180
254
  // 3. AgentDB index — best-effort via memory store (claude-flow bridges)
@@ -224,7 +298,7 @@ export const browserSessionTools = [
224
298
  if (!vS.valid)
225
299
  return fail(vS.error || 'invalid session');
226
300
  // 1. Verify RVF container exists
227
- const status = await shell('npx', ['-y', RUVECTOR_PIN, 'rvf', 'status', input.rvf_path]);
301
+ const status = await ruvector(['rvf', 'status', input.rvf_path]);
228
302
  if (!status.success)
229
303
  return fail('rvf status failed', { detail: status.error, stderr: status.stderr });
230
304
  // 2. Derive child container if requested
@@ -236,13 +310,13 @@ export const browserSessionTools = [
236
310
  const dir = path.dirname(input.rvf_path);
237
311
  replayId = `${input.session}-replay-${Date.now()}`;
238
312
  replayPath = path.join(dir, `${replayId}.rvf`);
239
- const dr = await shell('npx', ['-y', RUVECTOR_PIN, 'rvf', 'derive', input.rvf_path, replayPath]);
313
+ const dr = await ruvector(['rvf', 'derive', input.rvf_path, replayPath]);
240
314
  if (!dr.success)
241
315
  return fail('rvf derive failed', { detail: dr.error, stderr: dr.stderr });
242
316
  }
243
317
  // 3. Surface the trajectory steps from the segments listing — the caller is
244
318
  // expected to read trajectory.ndjson from the RVF container and dispatch.
245
- const segments = await shell('npx', ['-y', RUVECTOR_PIN, 'rvf', 'segments', input.rvf_path]);
319
+ const segments = await ruvector(['rvf', 'segments', input.rvf_path]);
246
320
  return ok({
247
321
  sourceSession: input.session,
248
322
  sourceRvfPath: input.rvf_path,
@@ -48,6 +48,7 @@ export declare const hooksWorkerDetect: MCPTool;
48
48
  export declare const hooksModelRoute: MCPTool;
49
49
  export declare const hooksModelOutcome: MCPTool;
50
50
  export declare const hooksModelStats: MCPTool;
51
+ export declare const hooksModelVerify: MCPTool;
51
52
  export declare const hooksCodemod: MCPTool;
52
53
  export declare const hooksWorkerCancel: MCPTool;
53
54
  export declare const hooksTeammateIdle: MCPTool;
@@ -2384,12 +2384,14 @@ export const hooksTrajectoryStart = {
2384
2384
  properties: {
2385
2385
  task: { type: 'string', description: 'Task description' },
2386
2386
  agent: { type: 'string', description: 'Agent type' },
2387
+ sessionId: { type: 'string', description: 'Session id for the execution-state tree (default: CLAUDE_FLOW_SESSION_ID or "default")' },
2387
2388
  },
2388
2389
  required: ['task'],
2389
2390
  },
2390
2391
  handler: async (params) => {
2391
2392
  const task = params.task;
2392
2393
  const agent = params.agent || 'coder';
2394
+ const sessionId = params.sessionId || process.env.CLAUDE_FLOW_SESSION_ID || 'default';
2393
2395
  {
2394
2396
  const v = validateText(task, 'task');
2395
2397
  if (!v.valid)
@@ -2400,6 +2402,11 @@ export const hooksTrajectoryStart = {
2400
2402
  if (!v.valid)
2401
2403
  return { success: false, error: v.error };
2402
2404
  }
2405
+ if (params.sessionId) {
2406
+ const v = validateIdentifier(params.sessionId, 'sessionId');
2407
+ if (!v.valid)
2408
+ return { success: false, error: v.error };
2409
+ }
2403
2410
  const trajectoryId = `traj-${Date.now()}-${Math.random().toString(36).substring(7)}`;
2404
2411
  const startedAt = new Date().toISOString();
2405
2412
  // Create real trajectory entry in memory
@@ -2411,6 +2418,13 @@ export const hooksTrajectoryStart = {
2411
2418
  startedAt,
2412
2419
  };
2413
2420
  activeTrajectories.set(trajectoryId, trajectory);
2421
+ // MAGE-style execution-state-tree mirror (prototype, ruvector/trajectory-tree.ts).
2422
+ // Non-fatal by design — the semantic trajectory path below is untouched.
2423
+ try {
2424
+ const { getTrajectoryTree } = await import('../ruvector/trajectory-tree.js');
2425
+ getTrajectoryTree().openTrajectory({ sessionId, trajectoryId, task, agent });
2426
+ }
2427
+ catch { /* prototype path — never blocks trajectory recording */ }
2414
2428
  // Persist pending trajectory to disk so it survives MCP restarts
2415
2429
  const storeFn = await getRealStoreFunction();
2416
2430
  if (storeFn) {
@@ -2479,6 +2493,12 @@ export const hooksTrajectoryStep = {
2479
2493
  timestamp,
2480
2494
  });
2481
2495
  }
2496
+ // MAGE-style execution-state-tree mirror (prototype, non-fatal)
2497
+ try {
2498
+ const { getTrajectoryTree } = await import('../ruvector/trajectory-tree.js');
2499
+ getTrajectoryTree().appendStep({ trajectoryId, stepId, action, quality });
2500
+ }
2501
+ catch { /* prototype path — never blocks step recording */ }
2482
2502
  // ADR-130 Phase 3: fire-and-forget causal edge write
2483
2503
  // trajectory context node → step node (relation: "trajectory-caused")
2484
2504
  if (result) {
@@ -2563,6 +2583,12 @@ export const hooksTrajectoryEnd = {
2563
2583
  // Remove from active trajectories
2564
2584
  activeTrajectories.delete(trajectoryId);
2565
2585
  }
2586
+ // MAGE-style execution-state-tree mirror (prototype, non-fatal)
2587
+ try {
2588
+ const { getTrajectoryTree } = await import('../ruvector/trajectory-tree.js');
2589
+ getTrajectoryTree().closeTrajectory({ trajectoryId, success });
2590
+ }
2591
+ catch { /* prototype path — never blocks trajectory finalization */ }
2566
2592
  // SONA Learning - process trajectory outcome for routing optimization
2567
2593
  let sonaResult = {
2568
2594
  learned: false, patternKey: '', confidence: 0
@@ -2866,6 +2892,9 @@ export const hooksPatternSearch = {
2866
2892
  topK: { type: 'number', description: 'Number of results' },
2867
2893
  minConfidence: { type: 'number', description: 'Minimum similarity threshold (0-1)' },
2868
2894
  namespace: { type: 'string', description: 'Namespace to search (default: pattern)' },
2895
+ strategy: { type: 'string', enum: ['semantic', 'state-tree'], description: 'Retrieval strategy. Default "semantic" (unchanged behavior). "state-tree" returns the MAGE-style root→current execution-state path for a session instead of embedding search (prototype).' },
2896
+ sessionId: { type: 'string', description: 'Session id for strategy="state-tree" (default: CLAUDE_FLOW_SESSION_ID or "default")' },
2897
+ depth: { type: 'number', description: 'For strategy="state-tree": max path nodes returned, counted from the current node upward' },
2869
2898
  },
2870
2899
  required: ['query'],
2871
2900
  },
@@ -2874,6 +2903,7 @@ export const hooksPatternSearch = {
2874
2903
  const topK = params.topK || 5;
2875
2904
  const minConfidence = params.minConfidence || 0.3;
2876
2905
  const namespace = params.namespace || 'pattern';
2906
+ const strategy = params.strategy || 'semantic';
2877
2907
  {
2878
2908
  const v = validateText(query, 'query');
2879
2909
  if (!v.valid)
@@ -2884,6 +2914,44 @@ export const hooksPatternSearch = {
2884
2914
  if (!v.valid)
2885
2915
  return { success: false, error: v.error };
2886
2916
  }
2917
+ // Opt-in MAGE-style positional retrieval (prototype). Default stays the
2918
+ // semantic vector path below — zero behavior change unless requested.
2919
+ if (strategy === 'state-tree') {
2920
+ const sessionId = params.sessionId || process.env.CLAUDE_FLOW_SESSION_ID || 'default';
2921
+ if (params.sessionId) {
2922
+ const v = validateIdentifier(params.sessionId, 'sessionId');
2923
+ if (!v.valid)
2924
+ return { success: false, error: v.error };
2925
+ }
2926
+ try {
2927
+ const { getTrajectoryTree } = await import('../ruvector/trajectory-tree.js');
2928
+ const recall = getTrajectoryTree().recallPath({
2929
+ sessionId,
2930
+ depth: typeof params.depth === 'number' ? params.depth : undefined,
2931
+ siblingWindow: topK,
2932
+ });
2933
+ return {
2934
+ query,
2935
+ strategy: 'state-tree',
2936
+ backend: 'state-tree',
2937
+ sessionId: recall.sessionId,
2938
+ currentId: recall.currentId,
2939
+ path: recall.path,
2940
+ siblings: recall.siblings,
2941
+ note: 'Positional root→current execution-state path (no embedding search). Prototype — see ruvector/trajectory-tree.ts limitations.',
2942
+ };
2943
+ }
2944
+ catch (error) {
2945
+ return {
2946
+ query,
2947
+ strategy: 'state-tree',
2948
+ backend: 'state-tree-unavailable',
2949
+ path: [],
2950
+ siblings: [],
2951
+ error: String(error),
2952
+ };
2953
+ }
2954
+ }
2887
2955
  // Phase 3: Try ReasoningBank search via bridge first
2888
2956
  try {
2889
2957
  const bridge = await import('../memory/memory-bridge.js');
@@ -4293,6 +4361,68 @@ export const hooksModelStats = {
4293
4361
  };
4294
4362
  },
4295
4363
  };
4364
+ // Model verify — confidence-gated tier escalation (post-generation).
4365
+ // The heuristics live in ruvector/output-verifier.ts; this wrapper adds the
4366
+ // MCP surface and feeds the verdict into the SAME learning stream that
4367
+ // hooks_model-outcome uses (ModelRouter.recordOutcome → Thompson priors), so
4368
+ // the router learns from escalations. A dedicated tool (rather than a
4369
+ // "verify mode" on hooks_model-outcome) was chosen because outcome recording
4370
+ // is a terminal write while verify is a MID-LOOP decision point that returns
4371
+ // a verdict the agent acts on — overloading outcome would conflate the two
4372
+ // and break the route → generate → verify → (escalate) → outcome sequence.
4373
+ export const hooksModelVerify = {
4374
+ name: 'hooks_model-verify',
4375
+ description: 'Verify a generated output with CHEAP structural signals ($0, no LLM call) and get an escalation verdict — the post-generation half of confidence-gated tier routing (route → generate → verify → escalate on failure). Checks: empty/truncated output, refusal patterns, degenerate repetition, and real syntax parsing for code/JSON tasks (TypeScript compiler / JSON.parse). Returns {confident, reasons[], suggestedTier, suggestedModel, escalate}. By default the verdict is recorded into the model-routing learning stream (success when confident, escalated when not) so the bandit learns which task shapes the cheap tier fails on. Use when you just generated with the tier hooks_model-route picked and must decide accept-vs-escalate BEFORE acting on the output; accepting cheap-tier output unverified is wrong because structurally unusable results (refusals, truncation, unparseable code) silently propagate downstream, and pre-generation routing alone cannot catch them. Not a semantic-quality judge — it only catches structurally unusable outputs.',
4376
+ inputSchema: {
4377
+ type: 'object',
4378
+ properties: {
4379
+ task: { type: 'string', description: 'The task the output was generated for' },
4380
+ output: { type: 'string', description: 'The generated output to verify' },
4381
+ model: { type: 'string', enum: ['haiku', 'sonnet', 'opus'], description: 'Model that produced the output (drives the escalation ladder; default haiku)' },
4382
+ tierUsed: { type: 'number', enum: [1, 2, 3], description: 'Tier that produced the output; derived from model when absent' },
4383
+ taskKind: { type: 'string', enum: ['code', 'json', 'text', 'auto'], description: 'Force the task kind; default auto-detect' },
4384
+ record: { type: 'boolean', description: 'Record the verdict into the routing learning stream (default true; requires model)' },
4385
+ },
4386
+ required: ['task', 'output'],
4387
+ },
4388
+ handler: async (params) => {
4389
+ const task = params.task;
4390
+ const output = params.output;
4391
+ const model = params.model;
4392
+ {
4393
+ const v = validateText(task, 'task');
4394
+ if (!v.valid)
4395
+ return { success: false, error: v.error };
4396
+ }
4397
+ if (typeof output !== 'string')
4398
+ return { success: false, error: 'output must be a string' };
4399
+ const { verifyAndEscalate } = await import('../ruvector/output-verifier.js');
4400
+ const verdict = await verifyAndEscalate({
4401
+ task,
4402
+ output,
4403
+ model,
4404
+ tierUsed: params.tierUsed,
4405
+ taskKind: params.taskKind,
4406
+ });
4407
+ // Feed the verdict into the existing outcome/learning stream (same path
4408
+ // as hooks_model-outcome) so escalations update the Thompson priors.
4409
+ let recorded = false;
4410
+ if (params.record !== false && model) {
4411
+ const router = await getModelRouterInstance();
4412
+ if (router) {
4413
+ router.recordOutcome(task, model, verdict.confident ? 'success' : 'escalated');
4414
+ recorded = true;
4415
+ }
4416
+ }
4417
+ return {
4418
+ ...verdict,
4419
+ model: model ?? 'haiku',
4420
+ recorded,
4421
+ recordedOutcome: recorded ? (verdict.confident ? 'success' : 'escalated') : null,
4422
+ timestamp: new Date().toISOString(),
4423
+ };
4424
+ },
4425
+ };
4296
4426
  // Supported source extensions for codemods.
4297
4427
  const CODEMOD_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.mts', '.cts']);
4298
4428
  const CODEMOD_MAX_FILES = 2000;
@@ -4686,6 +4816,7 @@ export const hooksTools = [
4686
4816
  hooksModelRoute,
4687
4817
  hooksModelOutcome,
4688
4818
  hooksModelStats,
4819
+ hooksModelVerify,
4689
4820
  // Deterministic Tier-1 codemod execution (ADR-143)
4690
4821
  hooksCodemod,
4691
4822
  ];