claude-flow 3.16.3 → 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.
@@ -0,0 +1,277 @@
1
+ /**
2
+ * output-verifier.ts — Confidence-gated tier escalation (post-generation).
3
+ *
4
+ * ADR-026/143 route tasks to a tier BEFORE generation. 2026 SOTA cascade
5
+ * routing adds a post-generation gate: attempt the cheap tier, run a CHEAP
6
+ * verifier over the produced output, and escalate to the next tier only when
7
+ * the verifier is not confident. This module is that verifier.
8
+ *
9
+ * Design constraints (load-bearing):
10
+ * - $0 by default — NO LLM call. Every signal is a structural / lexical
11
+ * check: emptiness, refusal patterns, truncation, delimiter balance,
12
+ * degenerate repetition, and (for code tasks) a real syntax parse via the
13
+ * TypeScript compiler (lazy-imported; degrades to delimiter checks when
14
+ * typescript is not installed) or JSON.parse for JSON output.
15
+ * - Pure with respect to router state — recording the verdict into the
16
+ * bandit's learning stream is the CALLER's job (the hooks_model-verify
17
+ * MCP tool does it via ModelRouter.recordOutcome), keeping this module
18
+ * trivially unit-testable.
19
+ * - Escalation ladder mirrors the tier table: tier 2 (haiku) → tier 3
20
+ * (sonnet), sonnet → opus, opus has no bump (escalate=false even when the
21
+ * verdict is not confident — the caller should retry or surface instead).
22
+ *
23
+ * Verdict semantics: `confident=false` means "cheap signals say this output
24
+ * is likely unusable"; it is NOT a semantic-quality judgment. False
25
+ * negatives (bad output that parses fine) are expected — this gate trades
26
+ * recall for being free.
27
+ *
28
+ * @module ruvector/output-verifier
29
+ */
30
+ const MODEL_TIER = { haiku: 2, sonnet: 3, opus: 3 };
31
+ const NEXT_MODEL = {
32
+ haiku: 'sonnet',
33
+ sonnet: 'opus',
34
+ opus: null,
35
+ };
36
+ // Refusal phrasings scanned near the start of the output. Anchored to the
37
+ // head so a legitimate answer that *discusses* refusals doesn't trip it.
38
+ const REFUSAL_HEAD_CHARS = 240;
39
+ const REFUSAL_PATTERNS = [
40
+ /\bI\s+(?:can(?:no|')t|cannot)\s+(?:help|assist|do|provide|comply|fulfill|complete|write|generate)\b/i,
41
+ /\bI(?:'m| am)\s+(?:sorry|afraid)\b[\s\S]{0,60}\b(?:can(?:no|')t|cannot|unable)\b/i,
42
+ /\bI(?:'m| am)\s+(?:unable|not able)\s+to\b/i,
43
+ /\bI\s+(?:must|have to)\s+(?:decline|refuse)\b/i,
44
+ /\bI\s+won'?t\s+be\s+able\s+to\b/i,
45
+ /\bas an AI(?:\s+(?:language\s+)?model)?[,\s]+I\b/i,
46
+ ];
47
+ // A trailing dangling operator / opener strongly suggests a mid-expression cutoff.
48
+ const TRUNCATION_TAIL = /(?:,|&&|\|\||=>|[+*/%=]|\(|\[|\{|\bconst|\blet|\bvar|\bfunction|\breturn|:)\s*$/;
49
+ /** Pull fenced code blocks out of markdown-ish output. */
50
+ export function extractCodeBlocks(output) {
51
+ const blocks = [];
52
+ const re = /```([A-Za-z0-9_-]*)\n([\s\S]*?)```/g;
53
+ let m;
54
+ while ((m = re.exec(output)) !== null) {
55
+ blocks.push({ lang: (m[1] || '').toLowerCase(), code: m[2] });
56
+ }
57
+ return blocks;
58
+ }
59
+ /** Cheap stack-based bracket balance check that skips string/comment-ish content. */
60
+ export function bracketsBalanced(code) {
61
+ const stack = [];
62
+ const pairs = { ')': '(', ']': '[', '}': '{' };
63
+ let inString = null;
64
+ let inLineComment = false;
65
+ let inBlockComment = false;
66
+ for (let i = 0; i < code.length; i++) {
67
+ const c = code[i];
68
+ const next = code[i + 1];
69
+ if (inLineComment) {
70
+ if (c === '\n')
71
+ inLineComment = false;
72
+ continue;
73
+ }
74
+ if (inBlockComment) {
75
+ if (c === '*' && next === '/') {
76
+ inBlockComment = false;
77
+ i++;
78
+ }
79
+ continue;
80
+ }
81
+ if (inString) {
82
+ if (c === '\\') {
83
+ i++;
84
+ continue;
85
+ }
86
+ if (c === inString)
87
+ inString = null;
88
+ continue;
89
+ }
90
+ if (c === '/' && next === '/') {
91
+ inLineComment = true;
92
+ i++;
93
+ continue;
94
+ }
95
+ if (c === '/' && next === '*') {
96
+ inBlockComment = true;
97
+ i++;
98
+ continue;
99
+ }
100
+ if (c === '"' || c === "'" || c === '`') {
101
+ inString = c;
102
+ continue;
103
+ }
104
+ if (c === '(' || c === '[' || c === '{')
105
+ stack.push(c);
106
+ else if (c === ')' || c === ']' || c === '}') {
107
+ if (stack.pop() !== pairs[c])
108
+ return false;
109
+ }
110
+ }
111
+ return stack.length === 0;
112
+ }
113
+ const CODE_TASK_HINT = /\b(?:code|function|class|implement|refactor|typescript|javascript|python|script|method|api endpoint|unit test)\b/i;
114
+ const CODE_OUTPUT_HINT = /(?:^|\n)\s*(?:import\s|export\s|function\s|class\s|const\s|let\s|def\s|return\s)/;
115
+ function detectTaskKind(input) {
116
+ if (input.taskKind && input.taskKind !== 'auto')
117
+ return input.taskKind;
118
+ if (/\bjson\b/i.test(input.task) && /^[\s]*[[{]/.test(input.output.trim()))
119
+ return 'json';
120
+ if (extractCodeBlocks(input.output).length > 0)
121
+ return 'code';
122
+ if (CODE_TASK_HINT.test(input.task) && CODE_OUTPUT_HINT.test(input.output))
123
+ return 'code';
124
+ return 'text';
125
+ }
126
+ /**
127
+ * Syntax-check a JS/TS snippet. Uses the TypeScript compiler when available
128
+ * (transpileModule with reportDiagnostics catches syntactic errors without
129
+ * type-checking — cheap, no program construction). Falls back to bracket
130
+ * balance when typescript is not installed.
131
+ */
132
+ async function checkCodeParses(code, lang) {
133
+ if (lang === 'json') {
134
+ try {
135
+ JSON.parse(code);
136
+ return { ok: true };
137
+ }
138
+ catch (e) {
139
+ return { ok: false, detail: `JSON.parse failed: ${e instanceof Error ? e.message : String(e)}` };
140
+ }
141
+ }
142
+ const jsLike = ['', 'js', 'jsx', 'ts', 'tsx', 'javascript', 'typescript', 'mjs', 'cjs'].includes(lang);
143
+ if (!jsLike) {
144
+ // Non-JS languages: bracket balance is the best free proxy we have.
145
+ return bracketsBalanced(code)
146
+ ? { ok: true, detail: `no parser for lang="${lang}"; delimiter check only` }
147
+ : { ok: false, detail: `unbalanced delimiters in ${lang} block` };
148
+ }
149
+ try {
150
+ const ts = (await import('typescript')).default;
151
+ const result = ts.transpileModule(code, {
152
+ reportDiagnostics: true,
153
+ compilerOptions: { jsx: ts.JsxEmit.Preserve, allowJs: true },
154
+ });
155
+ const syntaxErrors = (result.diagnostics ?? []).filter(d => d.category === ts.DiagnosticCategory.Error);
156
+ if (syntaxErrors.length > 0) {
157
+ const first = syntaxErrors[0];
158
+ const msg = ts.flattenDiagnosticMessageText(first.messageText, ' ');
159
+ return { ok: false, detail: `syntax error: ${msg}` };
160
+ }
161
+ return { ok: true };
162
+ }
163
+ catch {
164
+ // typescript not installed — graceful degradation, delimiter check only.
165
+ return bracketsBalanced(code)
166
+ ? { ok: true, detail: 'typescript unavailable; delimiter check only' }
167
+ : { ok: false, detail: 'typescript unavailable; unbalanced delimiters' };
168
+ }
169
+ }
170
+ /** Detect degenerate repetition (same non-trivial line repeated many times in a row). */
171
+ function isDegenerate(output) {
172
+ const lines = output.split('\n').map(l => l.trim()).filter(l => l.length > 8);
173
+ let run = 1;
174
+ for (let i = 1; i < lines.length; i++) {
175
+ if (lines[i] === lines[i - 1]) {
176
+ run++;
177
+ if (run >= 5)
178
+ return true;
179
+ }
180
+ else {
181
+ run = 1;
182
+ }
183
+ }
184
+ return false;
185
+ }
186
+ /**
187
+ * Compute a confidence verdict for `output` from cheap structural signals,
188
+ * and suggest an escalation target when not confident.
189
+ */
190
+ export async function verifyAndEscalate(input) {
191
+ const output = input.output ?? '';
192
+ const trimmed = output.trim();
193
+ const minLength = input.minLength ?? 20;
194
+ const taskKind = detectTaskKind(input);
195
+ const model = input.model ?? 'haiku';
196
+ const tierUsed = input.tierUsed ?? MODEL_TIER[model] ?? 2;
197
+ const signals = [];
198
+ const reasons = [];
199
+ const fail = (name, detail) => {
200
+ signals.push({ name, ok: false, detail });
201
+ reasons.push(`${name}: ${detail}`);
202
+ };
203
+ const pass = (name, detail) => signals.push({ name, ok: true, detail });
204
+ // 1. Emptiness
205
+ if (trimmed.length === 0)
206
+ fail('empty-output', 'output is empty or whitespace-only');
207
+ else
208
+ pass('empty-output');
209
+ // 2. Plausible length (skip when already flagged empty)
210
+ if (trimmed.length > 0 && trimmed.length < minLength) {
211
+ fail('too-short', `output is ${trimmed.length} chars (< ${minLength})`);
212
+ }
213
+ else if (trimmed.length > 0) {
214
+ pass('too-short');
215
+ }
216
+ // 3. Refusal patterns near the head
217
+ const head = trimmed.slice(0, REFUSAL_HEAD_CHARS);
218
+ const refusal = REFUSAL_PATTERNS.find(p => p.test(head));
219
+ if (refusal)
220
+ fail('refusal', `refusal pattern near start of output (${refusal.source.slice(0, 40)}…)`);
221
+ else
222
+ pass('refusal');
223
+ // 4. Truncation: unclosed code fence, or dangling operator/opener at the tail
224
+ const fenceCount = (output.match(/```/g) ?? []).length;
225
+ if (fenceCount % 2 !== 0) {
226
+ fail('truncation', 'unclosed code fence (odd number of ``` markers)');
227
+ }
228
+ else if (trimmed.length > 0 && TRUNCATION_TAIL.test(trimmed)) {
229
+ fail('truncation', `output ends mid-expression ("…${trimmed.slice(-24).replace(/\n/g, '\\n')}")`);
230
+ }
231
+ else {
232
+ pass('truncation');
233
+ }
234
+ // 5. Degenerate repetition
235
+ if (isDegenerate(trimmed))
236
+ fail('degenerate-repetition', 'same line repeated 5+ times consecutively');
237
+ else
238
+ pass('degenerate-repetition');
239
+ // 6. Code / JSON parse checks
240
+ if (taskKind === 'code' || taskKind === 'json') {
241
+ const blocks = extractCodeBlocks(output);
242
+ const targets = blocks.length > 0
243
+ ? blocks
244
+ : [{ lang: taskKind === 'json' ? 'json' : '', code: output }];
245
+ let allOk = true;
246
+ const details = [];
247
+ for (const block of targets) {
248
+ const check = await checkCodeParses(block.code, taskKind === 'json' ? 'json' : block.lang);
249
+ if (!check.ok) {
250
+ allOk = false;
251
+ details.push(check.detail ?? 'parse failed');
252
+ }
253
+ else if (check.detail) {
254
+ details.push(check.detail);
255
+ }
256
+ }
257
+ if (allOk)
258
+ pass('code-parses', details.join('; ') || undefined);
259
+ else
260
+ fail('code-parses', details.join('; '));
261
+ }
262
+ const passed = signals.filter(s => s.ok).length;
263
+ const score = signals.length > 0 ? passed / signals.length : 0;
264
+ const confident = reasons.length === 0;
265
+ const nextModel = NEXT_MODEL[model] ?? null;
266
+ const suggestedModel = confident ? null : nextModel;
267
+ const suggestedTier = confident
268
+ ? tierUsed
269
+ : (nextModel !== null ? Math.min(3, tierUsed + 1) : tierUsed);
270
+ const escalate = !confident && nextModel !== null;
271
+ if (!confident && nextModel === null) {
272
+ reasons.push('already-at-top-tier: no higher tier to escalate to — retry or surface to the user');
273
+ }
274
+ return { confident, score, reasons, signals, suggestedTier, suggestedModel, escalate, taskKind };
275
+ }
276
+ export default verifyAndEscalate;
277
+ //# sourceMappingURL=output-verifier.js.map
@@ -0,0 +1,113 @@
1
+ /**
2
+ * trajectory-tree.ts — Execution-state-tree retrieval PROTOTYPE (MAGE-style).
3
+ *
4
+ * Research basis: MAGE (arXiv 2606.06090) observes that semantic-similarity
5
+ * retrieval fragments decision trajectories on long-horizon tasks; retrieving
6
+ * by POSITION in a hierarchical execution-state tree (the root→current path)
7
+ * preserves coherence. This module mirrors ruflo's existing trajectory
8
+ * recording (hooks_intelligence_trajectory-start/step/end) into such a tree
9
+ * and offers position-based recall — NO embedding search anywhere.
10
+ *
11
+ * Structure:
12
+ * session root ─ trajectory ─ step
13
+ * └ trajectory (nested: a start while another is open
14
+ * opens UNDER the open one) ─ step …
15
+ *
16
+ * - `trajectory-start` opens a node under the deepest open trajectory of the
17
+ * session (or the session root).
18
+ * - `trajectory-step` appends a step child; the step becomes the "current"
19
+ * position of the session.
20
+ * - `trajectory-end` closes the node; current returns to its parent.
21
+ * - `recallPath({sessionId, depth})` returns the root→current path (the
22
+ * MAGE-style working context) plus the most recent siblings of the current
23
+ * node.
24
+ *
25
+ * PROTOTYPE LIMITATIONS (deliberate — see the feature spec):
26
+ * - Persistence is a single best-effort JSON snapshot at
27
+ * `.claude-flow/intelligence/trajectory-tree.json`, written on every
28
+ * mutation. It sits ALONGSIDE the existing 'trajectories' memory-namespace
29
+ * persistence; nothing is migrated and the semantic path is untouched.
30
+ * - No concurrency control: two MCP server processes sharing a cwd will
31
+ * last-writer-win the snapshot.
32
+ * - No pruning/compaction: long-lived sessions grow the file unboundedly
33
+ * (labels are truncated to 200 chars to bound row size, not row count).
34
+ * - sessionId defaults to CLAUDE_FLOW_SESSION_ID or 'default'; hosts that
35
+ * never set it collapse into one tree.
36
+ * - Retrieval is position-only by design; hybrid position+semantic ranking
37
+ * is future work.
38
+ *
39
+ * @module ruvector/trajectory-tree
40
+ */
41
+ export type TreeNodeKind = 'session' | 'trajectory' | 'step';
42
+ export interface TreeNode {
43
+ id: string;
44
+ kind: TreeNodeKind;
45
+ /** Task text (trajectory), action text (step), or session id — ≤200 chars. */
46
+ label: string;
47
+ parentId: string | null;
48
+ childIds: string[];
49
+ status: 'open' | 'closed';
50
+ openedAt: string;
51
+ closedAt?: string;
52
+ meta?: Record<string, unknown>;
53
+ }
54
+ export interface RecallResult {
55
+ sessionId: string;
56
+ /** Root→current node path (the MAGE-style working context). */
57
+ path: TreeNode[];
58
+ /** Most recent siblings of the current node (excluding it), oldest→newest. */
59
+ siblings: TreeNode[];
60
+ /** Id of the current node (deepest position in the session). */
61
+ currentId: string | null;
62
+ strategy: 'state-tree';
63
+ }
64
+ export declare class TrajectoryTree {
65
+ private nodes;
66
+ /** sessionId → id of the deepest "current" node (step or open trajectory). */
67
+ private currentBySession;
68
+ /** trajectoryId → sessionId, so step/end calls can omit the session. */
69
+ private trajectorySession;
70
+ private readonly persistPath;
71
+ constructor(persistPath?: string);
72
+ private sessionRootId;
73
+ private ensureSession;
74
+ /** Deepest OPEN trajectory node on the current path of a session, if any. */
75
+ private deepestOpenTrajectory;
76
+ openTrajectory(args: {
77
+ sessionId: string;
78
+ trajectoryId: string;
79
+ task: string;
80
+ agent?: string;
81
+ }): TreeNode;
82
+ appendStep(args: {
83
+ trajectoryId: string;
84
+ stepId: string;
85
+ action: string;
86
+ quality?: number;
87
+ }): TreeNode | null;
88
+ closeTrajectory(args: {
89
+ trajectoryId: string;
90
+ success?: boolean;
91
+ }): TreeNode | null;
92
+ /**
93
+ * MAGE-style positional recall: the exact root→current path for a session,
94
+ * plus the most recent siblings of the current node for local context.
95
+ *
96
+ * @param depth Max number of path nodes returned, counted from the CURRENT
97
+ * node upward (deepest levels win). Default: full path.
98
+ * @param siblingWindow Max recent siblings of the current node. Default 3.
99
+ */
100
+ recallPath(args: {
101
+ sessionId: string;
102
+ depth?: number;
103
+ siblingWindow?: number;
104
+ }): RecallResult;
105
+ get size(): number;
106
+ private load;
107
+ private save;
108
+ }
109
+ export declare function getTrajectoryTree(persistPath?: string): TrajectoryTree;
110
+ /** Test/reset hook — drops the singleton so the next get() reloads from disk. */
111
+ export declare function resetTrajectoryTree(): void;
112
+ export default TrajectoryTree;
113
+ //# sourceMappingURL=trajectory-tree.d.ts.map
@@ -0,0 +1,237 @@
1
+ /**
2
+ * trajectory-tree.ts — Execution-state-tree retrieval PROTOTYPE (MAGE-style).
3
+ *
4
+ * Research basis: MAGE (arXiv 2606.06090) observes that semantic-similarity
5
+ * retrieval fragments decision trajectories on long-horizon tasks; retrieving
6
+ * by POSITION in a hierarchical execution-state tree (the root→current path)
7
+ * preserves coherence. This module mirrors ruflo's existing trajectory
8
+ * recording (hooks_intelligence_trajectory-start/step/end) into such a tree
9
+ * and offers position-based recall — NO embedding search anywhere.
10
+ *
11
+ * Structure:
12
+ * session root ─ trajectory ─ step
13
+ * └ trajectory (nested: a start while another is open
14
+ * opens UNDER the open one) ─ step …
15
+ *
16
+ * - `trajectory-start` opens a node under the deepest open trajectory of the
17
+ * session (or the session root).
18
+ * - `trajectory-step` appends a step child; the step becomes the "current"
19
+ * position of the session.
20
+ * - `trajectory-end` closes the node; current returns to its parent.
21
+ * - `recallPath({sessionId, depth})` returns the root→current path (the
22
+ * MAGE-style working context) plus the most recent siblings of the current
23
+ * node.
24
+ *
25
+ * PROTOTYPE LIMITATIONS (deliberate — see the feature spec):
26
+ * - Persistence is a single best-effort JSON snapshot at
27
+ * `.claude-flow/intelligence/trajectory-tree.json`, written on every
28
+ * mutation. It sits ALONGSIDE the existing 'trajectories' memory-namespace
29
+ * persistence; nothing is migrated and the semantic path is untouched.
30
+ * - No concurrency control: two MCP server processes sharing a cwd will
31
+ * last-writer-win the snapshot.
32
+ * - No pruning/compaction: long-lived sessions grow the file unboundedly
33
+ * (labels are truncated to 200 chars to bound row size, not row count).
34
+ * - sessionId defaults to CLAUDE_FLOW_SESSION_ID or 'default'; hosts that
35
+ * never set it collapse into one tree.
36
+ * - Retrieval is position-only by design; hybrid position+semantic ranking
37
+ * is future work.
38
+ *
39
+ * @module ruvector/trajectory-tree
40
+ */
41
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
42
+ import { dirname, join, resolve } from 'node:path';
43
+ const DEFAULT_PERSIST_PATH = join('.claude-flow', 'intelligence', 'trajectory-tree.json');
44
+ const LABEL_MAX = 200;
45
+ function now() {
46
+ return new Date().toISOString();
47
+ }
48
+ function truncate(text) {
49
+ return text.length > LABEL_MAX ? `${text.slice(0, LABEL_MAX - 1)}…` : text;
50
+ }
51
+ export class TrajectoryTree {
52
+ nodes = new Map();
53
+ /** sessionId → id of the deepest "current" node (step or open trajectory). */
54
+ currentBySession = new Map();
55
+ /** trajectoryId → sessionId, so step/end calls can omit the session. */
56
+ trajectorySession = new Map();
57
+ persistPath;
58
+ constructor(persistPath) {
59
+ this.persistPath = resolve(persistPath ?? DEFAULT_PERSIST_PATH);
60
+ this.load();
61
+ }
62
+ sessionRootId(sessionId) {
63
+ return `session:${sessionId}`;
64
+ }
65
+ ensureSession(sessionId) {
66
+ const id = this.sessionRootId(sessionId);
67
+ let node = this.nodes.get(id);
68
+ if (!node) {
69
+ node = {
70
+ id,
71
+ kind: 'session',
72
+ label: truncate(sessionId),
73
+ parentId: null,
74
+ childIds: [],
75
+ status: 'open',
76
+ openedAt: now(),
77
+ };
78
+ this.nodes.set(id, node);
79
+ }
80
+ return node;
81
+ }
82
+ /** Deepest OPEN trajectory node on the current path of a session, if any. */
83
+ deepestOpenTrajectory(sessionId) {
84
+ let cursor = this.nodes.get(this.currentBySession.get(sessionId) ?? '');
85
+ while (cursor) {
86
+ if (cursor.kind === 'trajectory' && cursor.status === 'open')
87
+ return cursor;
88
+ cursor = cursor.parentId ? this.nodes.get(cursor.parentId) : undefined;
89
+ }
90
+ return null;
91
+ }
92
+ openTrajectory(args) {
93
+ const session = this.ensureSession(args.sessionId);
94
+ const parent = this.deepestOpenTrajectory(args.sessionId) ?? session;
95
+ const node = {
96
+ id: args.trajectoryId,
97
+ kind: 'trajectory',
98
+ label: truncate(args.task),
99
+ parentId: parent.id,
100
+ childIds: [],
101
+ status: 'open',
102
+ openedAt: now(),
103
+ meta: args.agent ? { agent: args.agent } : undefined,
104
+ };
105
+ this.nodes.set(node.id, node);
106
+ parent.childIds.push(node.id);
107
+ this.trajectorySession.set(args.trajectoryId, args.sessionId);
108
+ this.currentBySession.set(args.sessionId, node.id);
109
+ this.save();
110
+ return node;
111
+ }
112
+ appendStep(args) {
113
+ const trajectory = this.nodes.get(args.trajectoryId);
114
+ if (!trajectory || trajectory.kind !== 'trajectory')
115
+ return null;
116
+ const sessionId = this.trajectorySession.get(args.trajectoryId);
117
+ // The upstream stepId scheme (`step-${Date.now()}`) can collide within a
118
+ // millisecond — uniquify locally instead of clobbering an existing node.
119
+ let stepId = args.stepId;
120
+ let bump = 1;
121
+ while (this.nodes.has(stepId))
122
+ stepId = `${args.stepId}-${bump++}`;
123
+ const node = {
124
+ id: stepId,
125
+ kind: 'step',
126
+ label: truncate(args.action),
127
+ parentId: trajectory.id,
128
+ childIds: [],
129
+ status: 'closed',
130
+ openedAt: now(),
131
+ closedAt: now(),
132
+ meta: args.quality !== undefined ? { quality: args.quality } : undefined,
133
+ };
134
+ this.nodes.set(node.id, node);
135
+ trajectory.childIds.push(node.id);
136
+ if (sessionId)
137
+ this.currentBySession.set(sessionId, node.id);
138
+ this.save();
139
+ return node;
140
+ }
141
+ closeTrajectory(args) {
142
+ const trajectory = this.nodes.get(args.trajectoryId);
143
+ if (!trajectory || trajectory.kind !== 'trajectory')
144
+ return null;
145
+ trajectory.status = 'closed';
146
+ trajectory.closedAt = now();
147
+ if (args.success !== undefined) {
148
+ trajectory.meta = { ...trajectory.meta, success: args.success };
149
+ }
150
+ const sessionId = this.trajectorySession.get(args.trajectoryId);
151
+ if (sessionId && trajectory.parentId) {
152
+ // Current position pops back to the enclosing trajectory / session root.
153
+ this.currentBySession.set(sessionId, trajectory.parentId);
154
+ }
155
+ this.save();
156
+ return trajectory;
157
+ }
158
+ /**
159
+ * MAGE-style positional recall: the exact root→current path for a session,
160
+ * plus the most recent siblings of the current node for local context.
161
+ *
162
+ * @param depth Max number of path nodes returned, counted from the CURRENT
163
+ * node upward (deepest levels win). Default: full path.
164
+ * @param siblingWindow Max recent siblings of the current node. Default 3.
165
+ */
166
+ recallPath(args) {
167
+ const currentId = this.currentBySession.get(args.sessionId) ?? null;
168
+ const path = [];
169
+ let cursor = currentId ? this.nodes.get(currentId) : undefined;
170
+ while (cursor) {
171
+ path.unshift(cursor);
172
+ cursor = cursor.parentId ? this.nodes.get(cursor.parentId) : undefined;
173
+ }
174
+ const depth = args.depth && args.depth > 0 ? args.depth : path.length;
175
+ const trimmedPath = path.slice(Math.max(0, path.length - depth));
176
+ const siblingWindow = args.siblingWindow ?? 3;
177
+ let siblings = [];
178
+ const current = currentId ? this.nodes.get(currentId) : undefined;
179
+ const parent = current?.parentId ? this.nodes.get(current.parentId) : undefined;
180
+ if (current && parent) {
181
+ siblings = parent.childIds
182
+ .filter(id => id !== current.id)
183
+ .map(id => this.nodes.get(id))
184
+ .filter((n) => !!n)
185
+ .slice(-siblingWindow);
186
+ }
187
+ return { sessionId: args.sessionId, path: trimmedPath, siblings, currentId, strategy: 'state-tree' };
188
+ }
189
+ get size() {
190
+ return this.nodes.size;
191
+ }
192
+ // ── Persistence (best-effort JSON snapshot) ──────────────────────────────
193
+ load() {
194
+ try {
195
+ if (!existsSync(this.persistPath))
196
+ return;
197
+ const raw = JSON.parse(readFileSync(this.persistPath, 'utf-8'));
198
+ if (raw?.v !== 1 || typeof raw.nodes !== 'object')
199
+ return;
200
+ this.nodes = new Map(Object.entries(raw.nodes));
201
+ this.currentBySession = new Map(Object.entries(raw.currentBySession ?? {}));
202
+ this.trajectorySession = new Map(Object.entries(raw.trajectorySession ?? {}));
203
+ }
204
+ catch {
205
+ // Corrupt/unreadable snapshot — start fresh; the semantic trajectory
206
+ // persistence in the 'trajectories' namespace is unaffected.
207
+ }
208
+ }
209
+ save() {
210
+ try {
211
+ const snapshot = {
212
+ v: 1,
213
+ nodes: Object.fromEntries(this.nodes),
214
+ currentBySession: Object.fromEntries(this.currentBySession),
215
+ trajectorySession: Object.fromEntries(this.trajectorySession),
216
+ };
217
+ mkdirSync(dirname(this.persistPath), { recursive: true });
218
+ writeFileSync(this.persistPath, JSON.stringify(snapshot), 'utf-8');
219
+ }
220
+ catch {
221
+ // Best-effort — tree still lives in memory.
222
+ }
223
+ }
224
+ }
225
+ // ── Singleton (mirrors the lazy-singleton pattern used by model-router) ────
226
+ let treeInstance = null;
227
+ export function getTrajectoryTree(persistPath) {
228
+ if (!treeInstance)
229
+ treeInstance = new TrajectoryTree(persistPath);
230
+ return treeInstance;
231
+ }
232
+ /** Test/reset hook — drops the singleton so the next get() reloads from disk. */
233
+ export function resetTrajectoryTree() {
234
+ treeInstance = null;
235
+ }
236
+ export default TrajectoryTree;
237
+ //# sourceMappingURL=trajectory-tree.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.16.3",
3
+ "version": "3.18.0",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",
@@ -115,11 +115,11 @@
115
115
  "@claude-flow/memory": "^3.0.0-alpha.21",
116
116
  "@claude-flow/plugin-gastown-bridge": "^0.1.3",
117
117
  "@claude-flow/security": "^3.0.0-alpha.10",
118
- "@metaharness/darwin": "~0.3.1",
119
- "@metaharness/kernel": "~0.1.0",
120
- "@metaharness/redblue": "~0.1.1",
118
+ "@metaharness/darwin": "~0.8.0",
119
+ "@metaharness/kernel": "~0.1.2",
120
+ "@metaharness/redblue": "~0.1.4",
121
121
  "@metaharness/router": "~0.3.2",
122
- "metaharness": "~0.2.6",
122
+ "metaharness": "~0.2.8",
123
123
  "@ruvector/attention": "^0.1.32",
124
124
  "@ruvector/attention-darwin-arm64": "0.1.32",
125
125
  "@ruvector/diskann": "^0.1.0",