@claude-flow/cli 3.56.4 → 3.56.6

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.
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.56.4",
3
+ "version": "3.56.6",
4
4
  "files": {
5
5
  "auto-memory-hook.mjs": "381412515fb490def7d821ddfe1ebf8d9bee76e49a86e182806c0274f568bf15",
6
- "hook-handler.cjs": "38fc235079ddcf74b62452a91b9e56703026a1a7682dcce3c635be5ad1c8ea19",
6
+ "hook-handler.cjs": "0f855c667b8d2d04cb4fe91b14b351d14c3dfb30516dcd9d7ae156acbe0a689d",
7
7
  "intelligence.cjs": "ac5033a77bcea9796856629d5d319bde9ab6b7edf0dcd0d4af187ce30af90836",
8
8
  "statusline.cjs": "d72417ecb38634011a19f095a30e07b9929e6f97663520172ece9be7491b2eaa",
9
9
  "router.cjs": "16648b3c0d73b6681ac2d8d13def6a68286c57f5a11004de5b19a0fa088c833d"
10
10
  }
11
11
  },
12
- "signature": "Dhlixv3hNK+hc+t5recMVcHtVZ6ilyT4uN4YLuFsoSVwbTI6dt6VfzyZZXDjgfm6ei8NR9IHIjjmVgKkUmRBDw==",
12
+ "signature": "3wmXMrroE4WEmOLGOLYSxXlcB2GITYHa8xpd3Jwu/xLCyFsf+saWI4Vq8EsvCzuwC8u3XpKX7aWrvQchQK3oAg==",
13
13
  "algorithm": "ed25519"
14
14
  }
@@ -213,6 +213,198 @@ function spawnDetachedAdvisorRefresh() {
213
213
  spawnDetachedHookRefresh('refresh-advisor');
214
214
  }
215
215
 
216
+ // ── Settings-risk scan for the automatic SessionStart hook ────────────────
217
+ //
218
+ // Mirrors v3/@claude-flow/cli/src/init/settings-risk-scanner.ts's
219
+ // `scanSettingsForRisk()`/`formatRiskFindingsAsWarnings()`. That TS scanner
220
+ // already runs at `ruflo init`/`--upgrade` time and (via the
221
+ // `hooks_session-restore` MCP/CLI tool handler) on an explicit
222
+ // `session-restore` call — but neither covers the one path that actually
223
+ // fires automatically, unconditionally, on every real Claude Code session
224
+ // boot: THIS file, invoked as the `SessionStart` hook configured in
225
+ // .claude/settings.json itself. That is precisely the persistence mechanism
226
+ // used by the real 2026 "Mini Shai-Hulud" npm-worm campaign (a malicious
227
+ // SessionStart hook written into settings.json re-fires on every session,
228
+ // surviving removal of the original infected package) and the escalation
229
+ // path closed by CVE-2026-25725 in Claude Code's own sandbox. hook-handler.cjs
230
+ // has no build step and cannot import the TS module, so this is a deliberate
231
+ // CJS port — keep the pattern lists in sync with settings-risk-scanner.ts;
232
+ // see the logic-parity test guarding this.
233
+ //
234
+ // Advisory-only: never mutates settings.json, never blocks session-restore.
235
+ // A missing settings.json or any JSON.parse failure yields zero findings
236
+ // silently. Unlike the opt-in `hooks_session-restore` MCP-tool path (#3835,
237
+ // which narrows its catch to SyntaxError only so a real scanner bug
238
+ // surfaces), THIS path fires unconditionally on every real session boot —
239
+ // an adversarial review of this exact candidate flagged that narrowing the
240
+ // same way here would let a non-SyntaxError JSON.parse failure (e.g. a V8
241
+ // RangeError on deeply-nested JSON) abort session-restore itself, not just
242
+ // skip the scan. "Can't scan it" and "can't parse it" are the same case
243
+ // here, so any parse failure degrades to zero findings.
244
+ const SR_DANGEROUS_COMMAND_WORDS = [
245
+ 'rm', 'rmdir', 'del', 'format', 'mkfs', 'dd', 'chmod', 'chown',
246
+ 'kill', 'killall', 'pkill', 'reboot', 'shutdown', 'poweroff', 'halt',
247
+ 'sudo', 'eval',
248
+ ];
249
+ const SR_SHELL_EXEC_WORDS = ['bash', 'sh', 'zsh', 'python', 'python3', 'perl', 'ruby', 'eval'];
250
+ const SR_DOWNLOADER_WORDS = ['curl', 'wget'];
251
+ const SR_STANDALONE_RISK_PATTERNS = [
252
+ /\bInvoke-Expression\b/i,
253
+ /\biex\s*\(/i,
254
+ /\/dev\/tcp\//i,
255
+ /\bnc\s+-e\b/i,
256
+ /base64\s+(-d|--decode)\b/i,
257
+ ];
258
+ const SR_RISKY_PREAPPROVE_WORDS = [...SR_DANGEROUS_COMMAND_WORDS, ...SR_SHELL_EXEC_WORDS, ...SR_DOWNLOADER_WORDS];
259
+
260
+ // Encoded PowerShell (`powershell[.exe] ... -e|-enc|-encodedcommand <payload>`),
261
+ // matched on whitespace-separated tokens in ONE linear pass. The earlier regex
262
+ // (`powershell\\s+(?:\\S+\\s+)*?-e...`) is quadratic on repeated "powershell "
263
+ // tokens (every start rescans the tail: 256KB measured ~2.5s, 1MB ~40s) because
264
+ // the lazy token loop re-runs from each start. Same semantics: a token ending in
265
+ // powershell/powershell.exe, then LATER a token that is exactly -e/-enc/
266
+ // -encodedcommand, then at least one more token.
267
+ function srHasEncodedPowershell(command) {
268
+ let seenPs = false;
269
+ let pendingEnc = false;
270
+ for (const raw of String(command).split(/\s+/)) {
271
+ if (!raw) continue;
272
+ if (pendingEnc) return true;
273
+ const t = raw.toLowerCase();
274
+ if (seenPs && (t === '-e' || t === '-enc' || t === '-encodedcommand')) pendingEnc = true;
275
+ else if (t.endsWith('powershell') || t.endsWith('powershell.exe')) seenPs = true;
276
+ }
277
+ return false;
278
+ }
279
+
280
+ function srWordBoundaryMatches(text, word) {
281
+ return new RegExp(`\\b${word}\\b`, 'i').test(text);
282
+ }
283
+
284
+ function srScanCommandStringForRisk(command) {
285
+ const reasons = [];
286
+ if (srHasEncodedPowershell(command) || SR_STANDALONE_RISK_PATTERNS.some((pattern) => pattern.test(command))) {
287
+ reasons.push('matches a known-dangerous execution pattern (encoded/remote command execution)');
288
+ }
289
+ const hasDownloader = SR_DOWNLOADER_WORDS.some((w) => srWordBoundaryMatches(command, w));
290
+ const hasShellExec = SR_SHELL_EXEC_WORDS.some((w) => srWordBoundaryMatches(command, w));
291
+ if (hasDownloader && hasShellExec) {
292
+ reasons.push('combines a downloader (curl/wget) with a shell/interpreter invocation in the same command');
293
+ }
294
+ for (const word of SR_DANGEROUS_COMMAND_WORDS) {
295
+ if (srWordBoundaryMatches(command, word)) {
296
+ reasons.push(`invokes dangerous command "${word}"`);
297
+ }
298
+ }
299
+ return reasons;
300
+ }
301
+
302
+ function srScanAllowRuleForRisk(rule) {
303
+ const trimmed = rule.trim();
304
+ const reasons = [];
305
+ if (/^Bash\(\s*\*\s*(:\s*\*\s*)?\)$/i.test(trimmed)) {
306
+ reasons.push('grants unrestricted Bash execution (wildcard allow rule)');
307
+ }
308
+ for (const word of SR_RISKY_PREAPPROVE_WORDS) {
309
+ if (new RegExp(`^Bash\\(\\s*${word}\\s*(:| |\\))`, 'i').test(trimmed)) {
310
+ reasons.push(`pre-allows "${word}" with no per-call confirmation`);
311
+ }
312
+ }
313
+ return reasons;
314
+ }
315
+
316
+ // Strips ANSI/control characters from untrusted text before it's ever
317
+ // embedded in a warning string printed to the terminal (same rationale as
318
+ // settings-risk-scanner.ts's sanitizeForDisplay()).
319
+ function srSanitizeForDisplay(text) {
320
+ // eslint-disable-next-line no-control-regex
321
+ return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
322
+ }
323
+
324
+ function srBuildSnippet(text) {
325
+ const clean = srSanitizeForDisplay(text);
326
+ return clean.length > 120 ? `${clean.slice(0, 120)}…` : clean;
327
+ }
328
+
329
+ function srScanSettingsForRisk(settings) {
330
+ const findings = [];
331
+ const hooks = settings && settings.hooks;
332
+ if (hooks && typeof hooks === 'object') {
333
+ for (const [eventName, groups] of Object.entries(hooks)) {
334
+ if (!Array.isArray(groups)) continue;
335
+ groups.forEach((group, groupIdx) => {
336
+ const hookList = group && group.hooks;
337
+ if (!Array.isArray(hookList)) return;
338
+ hookList.forEach((h, hookIdx) => {
339
+ if (!h || typeof h.command !== 'string') return;
340
+ for (const reason of srScanCommandStringForRisk(h.command)) {
341
+ findings.push({
342
+ location: `hooks.${eventName}[${groupIdx}].hooks[${hookIdx}]`,
343
+ snippet: srBuildSnippet(h.command),
344
+ reason,
345
+ });
346
+ }
347
+ });
348
+ });
349
+ }
350
+ }
351
+ const allow = settings && settings.permissions && settings.permissions.allow;
352
+ if (Array.isArray(allow)) {
353
+ allow.forEach((rule, idx) => {
354
+ if (typeof rule !== 'string') return;
355
+ for (const reason of srScanAllowRuleForRisk(rule)) {
356
+ findings.push({
357
+ location: `permissions.allow[${idx}]`,
358
+ snippet: srBuildSnippet(rule),
359
+ reason,
360
+ });
361
+ }
362
+ });
363
+ }
364
+ return findings;
365
+ }
366
+
367
+ function srFormatRiskFindingsAsWarnings(findings) {
368
+ return findings.map((f) => `${f.location}: ${f.reason} — "${f.snippet}"`);
369
+ }
370
+
371
+ // Location + reason only, no snippet — see scanSessionStartSettingsForRisk's
372
+ // doc comment for why the automatic boot path deliberately drops it.
373
+ function srFormatRiskFindingsForAutomaticBoot(findings) {
374
+ return findings.map((f) => `${f.location}: ${f.reason}`);
375
+ }
376
+
377
+ // Reads and scans THIS project's own .claude/settings.json. Returns a
378
+ // (possibly empty) array of warning strings; never throws on a
379
+ // missing/malformed file.
380
+ //
381
+ // Deliberately omits the matched snippet (unlike srFormatRiskFindingsAsWarnings,
382
+ // which `init`/`--upgrade` uses and which DOES include it): this function's
383
+ // only caller is the 'session-restore' handler below, whose stdout is read
384
+ // back into the live Claude Code session as context — not a human reading a
385
+ // terminal. Printing up to 120 characters of attacker-controlled settings.json
386
+ // content into that context is a prompt-injection path even with control
387
+ // characters stripped (flagged by review on PR #4053). The location and
388
+ // static reason string are both scanner-generated, never attacker content.
389
+ function scanSessionStartSettingsForRisk() {
390
+ const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
391
+ const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
392
+ let raw;
393
+ try {
394
+ raw = fs.readFileSync(settingsPath, 'utf-8');
395
+ } catch (e) {
396
+ return []; // no settings.json (or unreadable) — nothing to scan
397
+ }
398
+ let settings;
399
+ try {
400
+ settings = JSON.parse(raw);
401
+ } catch (e) {
402
+ return []; // can't parse it, so can't scan it — never block session-restore for this
403
+ }
404
+ if (!settings || typeof settings !== 'object') return [];
405
+ return srFormatRiskFindingsForAutomaticBoot(srScanSettingsForRisk(settings));
406
+ }
407
+
216
408
  // Safe require with stdout suppression - the helper modules have CLI
217
409
  // sections that run unconditionally on require(), so we mute console
218
410
  // during the require to prevent noisy output.
@@ -651,6 +843,11 @@ const handlers = {
651
843
  // Fully non-blocking (detached spawn) so session-restore latency
652
844
  // is unchanged.
653
845
  firstRunAutoEnableIfEligible();
846
+ // Advisory-only settings.json risk scan — see scanSessionStartSettingsForRisk's
847
+ // own doc comment for why this runs specifically on this automatic path.
848
+ for (const warning of scanSessionStartSettingsForRisk()) {
849
+ console.log(`[WARN] ${warning}`);
850
+ }
654
851
  if (session) {
655
852
  // Try restore first, fall back to start
656
853
  const existing = session.restore && session.restore();
@@ -777,4 +974,4 @@ if (require.main === module) {
777
974
  // Which sibling helpers loaded (all CommonJS, shipped as .cjs — #3555).
778
975
  const loadedHelpers = { router: !!router, session: !!session, memory: !!memory, intelligence: !!intelligence };
779
976
 
780
- module.exports = { runWithTimeout, INTELLIGENCE_TIMEOUT_MS, loadedHelpers, ownedByMod, MOD_OWNABLE_EVENTS, resolveCliBinForHook, cliSpawnArgs, helperRoot };
977
+ module.exports = { runWithTimeout, INTELLIGENCE_TIMEOUT_MS, loadedHelpers, ownedByMod, MOD_OWNABLE_EVENTS, resolveCliBinForHook, cliSpawnArgs, helperRoot, scanSessionStartSettingsForRisk, srFormatRiskFindingsForAutomaticBoot, srScanSettingsForRisk, srScanCommandStringForRisk, srScanAllowRuleForRisk, SR_DANGEROUS_COMMAND_WORDS, SR_SHELL_EXEC_WORDS, SR_DOWNLOADER_WORDS, SR_STANDALONE_RISK_PATTERNS };
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 7,
4
- "generatedAt": "2026-10-11T01:01:00.018Z",
5
- "gitSha": "f6781f1a",
4
+ "generatedAt": "2026-10-11T16:40:50.905Z",
5
+ "gitSha": "5905d40b",
6
6
  "catalog": {
7
7
  "agents": 176,
8
8
  "tools": 423,
@@ -30,20 +30,38 @@ export function helperScopeFor(targetDir) {
30
30
  return 'project';
31
31
  return canonical(targetDir) === canonical(os.homedir()) ? 'user' : 'project';
32
32
  }
33
+ /**
34
+ * Windows hook command (#4045). It must not go through `cmd /c`: under Git Bash
35
+ * MSYS rewrites `/c` into a path, cmd starts interactively and executes the
36
+ * hook's stdin JSON, and any `>` in that text creates a file. A single
37
+ * `node -e` has no shell syntax to misread (no cmd, no `%VAR%`, no `<>|&$` and
38
+ * no inner double quotes), so cmd, PowerShell and Git Bash pass it through
39
+ * unchanged. The script and its subcommand stay in one string, because the
40
+ * ruflo-mods ownership rule matches `hook-handler.cjs <subcommand>`. Project
41
+ * scope prefers the project's helper and falls back to the user profile's
42
+ * (#1943); user scope never looks at the project. The helper runs as a child
43
+ * with inherited stdio so stdin, stdout and the exit status pass through (a
44
+ * `require` would skip the helper's `require.main === module` entry point).
45
+ */
46
+ function windowsHookCommand(script, subcommand, scope) {
47
+ // Ternaries, not `||`: the command carries no pipe or ampersand at all.
48
+ const home = "(e.USERPROFILE?e.USERPROFILE:e.HOME?e.HOME:'.')";
49
+ const pick = scope === 'user'
50
+ ? `f=p.join(${home},a[0]);`
51
+ : "f=p.join(e.CLAUDE_PROJECT_DIR?e.CLAUDE_PROJECT_DIR:'.',a[0]);" +
52
+ `if(!fs.existsSync(f))f=p.join(${home},a[0]);`;
53
+ const js = `var a='${[script, subcommand].filter(Boolean).join(' ')}'.split(' '),e=process.env,fs=require('fs'),p=require('path'),f;` + pick +
54
+ "var r=require('child_process').spawnSync(process.execPath,[f].concat(a.slice(1)),{stdio:'inherit'});" +
55
+ 'process.exit(r.status===null?1:r.status)';
56
+ return `node -e "${js}"`;
57
+ }
33
58
  /** Command that runs `node <script> <subcommand>` for a hook. */
34
59
  export function helperHookCommand(script, subcommand, scope, windows = IS_WINDOWS) {
35
- const winScript = script.replace(/\//g, '\\');
36
60
  const arg = subcommand ? ` ${subcommand}` : '';
37
- if (scope === 'user') {
38
- return windows
39
- ? `cmd /c "node "%USERPROFILE%\\${winScript}"${arg}"`
40
- : `sh -c 'exec node "\${HOME}/${script}"${arg}'`;
41
- }
42
- if (windows) {
43
- // cmd.exe equivalent of the sh probe below. `IF EXIST` checks the
44
- // project-local path; falls back to %USERPROFILE% if missing.
45
- return `cmd /c "IF EXIST \"%CLAUDE_PROJECT_DIR%\\${winScript}\" (node \"%CLAUDE_PROJECT_DIR%\\${winScript}\"${arg}) ELSE (node \"%USERPROFILE%\\${winScript}\"${arg})"`;
46
- }
61
+ if (windows)
62
+ return windowsHookCommand(script, subcommand, scope);
63
+ if (scope === 'user')
64
+ return `sh -c 'exec node "\${HOME}/${script}"${arg}'`;
47
65
  // POSIX sh: prefer project-local helpers, fall back to $HOME/.claude/.
48
66
  // eslint-disable-next-line no-template-curly-in-string
49
67
  const projVar = '${CLAUDE_PROJECT_DIR:-.}';
@@ -102,6 +120,17 @@ const IF_EXIST_PREAMBLE = /"IF EXIST "%CLAUDE_PROJECT_DIR%\\\.claude\\helpers\\[
102
120
  * commands (other scripts, redirects, chains) are left alone.
103
121
  */
104
122
  export function pinToUserHelpers(command, windows = IS_WINDOWS) {
123
+ // The #4045 Windows form: recognised only when it is exactly what ruflo generates.
124
+ const generated = /^node -e "var a='\.claude\/helpers\/([\w.-]+)((?: [\w:.=-]+)*)'\.split\(' '\)/.exec(command);
125
+ if (generated) {
126
+ if (!RUFLO_HELPERS.includes(generated[1]))
127
+ return null;
128
+ const script = `.claude/helpers/${generated[1]}`;
129
+ const subcommand = generated[2].trim();
130
+ return command === helperHookCommand(script, subcommand, 'project', true)
131
+ ? helperHookCommand(script, subcommand, 'user', true)
132
+ : null;
133
+ }
105
134
  const refs = [...command.matchAll(HELPER_REF)];
106
135
  if (refs.length === 0)
107
136
  return null;
@@ -44,7 +44,6 @@ const DOWNLOADER_WORDS = ['curl', 'wget'];
44
44
  const STANDALONE_RISK_PATTERNS = [
45
45
  /\bInvoke-Expression\b/i,
46
46
  /\biex\s*\(/i,
47
- /powershell(\.exe)?\s+.*-e(nc(odedcommand)?)?\s+\S/i,
48
47
  /\/dev\/tcp\//i,
49
48
  /\bnc\s+-e\b/i,
50
49
  /base64\s+(-d|--decode)\b/i,
@@ -54,17 +53,37 @@ const STANDALONE_RISK_PATTERNS = [
54
53
  // own the way `rm`/`chmod` are — pre-approving them removes the human
55
54
  // review step for exactly the commands download-and-execute attacks use.
56
55
  const RISKY_PREAPPROVE_WORDS = [...DANGEROUS_COMMAND_WORDS, ...SHELL_EXEC_WORDS, ...DOWNLOADER_WORDS];
56
+ // Encoded PowerShell (`powershell[.exe] ... -e|-enc|-encodedcommand <payload>`),
57
+ // matched on whitespace-separated tokens in ONE linear pass. The earlier regex
58
+ // (`powershell\\s+(?:\\S+\\s+)*?-e...`) is quadratic on repeated "powershell "
59
+ // tokens (every start rescans the tail: 256KB measured ~2.5s, 1MB ~40s) because
60
+ // the lazy token loop re-runs from each start. Same semantics: a token ending in
61
+ // powershell/powershell.exe, then LATER a token that is exactly -e/-enc/
62
+ // -encodedcommand, then at least one more token.
63
+ function hasEncodedPowershell(command) {
64
+ let seenPs = false;
65
+ let pendingEnc = false;
66
+ for (const raw of command.split(/\s+/)) {
67
+ if (!raw)
68
+ continue;
69
+ if (pendingEnc)
70
+ return true;
71
+ const t = raw.toLowerCase();
72
+ if (seenPs && (t === '-e' || t === '-enc' || t === '-encodedcommand'))
73
+ pendingEnc = true;
74
+ else if (t.endsWith('powershell') || t.endsWith('powershell.exe'))
75
+ seenPs = true;
76
+ }
77
+ return false;
78
+ }
57
79
  function wordBoundaryMatches(text, word) {
58
80
  return new RegExp(`\\b${word}\\b`, 'i').test(text);
59
81
  }
60
82
  /** Reasons a single hook `command` string looks risky (empty = clean). */
61
83
  export function scanCommandStringForRisk(command) {
62
84
  const reasons = [];
63
- for (const pattern of STANDALONE_RISK_PATTERNS) {
64
- if (pattern.test(command)) {
65
- reasons.push('matches a known-dangerous execution pattern (encoded/remote command execution)');
66
- break;
67
- }
85
+ if (hasEncodedPowershell(command) || STANDALONE_RISK_PATTERNS.some((pattern) => pattern.test(command))) {
86
+ reasons.push('matches a known-dangerous execution pattern (encoded/remote command execution)');
68
87
  }
69
88
  const hasDownloader = DOWNLOADER_WORDS.some((w) => wordBoundaryMatches(command, w));
70
89
  const hasShellExec = SHELL_EXEC_WORDS.some((w) => wordBoundaryMatches(command, w));
@@ -3305,19 +3305,23 @@ export const hooksTrajectoryEnd = {
3305
3305
  }
3306
3306
  catch { /* intelligence module not loadable — keep sona-only behaviour */ }
3307
3307
  }
3308
- // #2351: when an agent calls trajectory-end with no recorded steps but a
3309
- // non-empty `feedback` string, the feedback was previously dropped on the
3310
- // floor — `patternsExtracted` reported 0 and `pattern-search` never
3311
- // surfaced it. Step-less trajectories are the common case for LLM agents
3312
- // (nothing forces step logging mid-task), and feedback is often the most
3313
- // distilled lesson available. Route it through the same store + embed
3314
- // path that pattern-store uses so it becomes searchable. Best-effort:
3315
- // failures here must not turn the trajectory-end call itself into a
3316
- // failure — the trajectory record was already persisted above.
3308
+ // #2351: when an agent calls trajectory-end with a non-empty `feedback`
3309
+ // string, the feedback was previously dropped on the floor —
3310
+ // `patternsExtracted` reported 0 and `pattern-search` never surfaced it.
3311
+ // Route it through the same store + embed path that pattern-store uses
3312
+ // so it becomes searchable. Best-effort: failures here must not turn the
3313
+ // trajectory-end call itself into a failure — the trajectory record was
3314
+ // already persisted above.
3315
+ //
3316
+ // #3662: #2351's original fix gated this on `!hasSteps` (step-less
3317
+ // trajectories only). The common start -> step -> end path (the one real
3318
+ // agents actually use) therefore recorded SONA state while leaving the
3319
+ // feedback unindexed. Non-empty feedback is promoted regardless of
3320
+ // whether steps were recorded — a trajectory's steps and its distilled
3321
+ // feedback are not mutually exclusive evidence.
3317
3322
  let feedbackDistilled = { stored: false };
3318
- const hasSteps = !!trajectory && trajectory.steps.length > 0;
3319
3323
  const trimmedFeedback = typeof feedback === 'string' ? feedback.trim() : '';
3320
- if (trajectory && !hasSteps && trimmedFeedback.length > 0) {
3324
+ if (trajectory && trimmedFeedback.length > 0) {
3321
3325
  const distilledPatternId = `pattern-feedback-${trajectoryId}-${Date.now()}`;
3322
3326
  const patternMetadata = {
3323
3327
  sourceTrajectoryId: trajectoryId,
@@ -32,6 +32,7 @@
32
32
  import * as path from 'path';
33
33
  import * as fs from 'fs';
34
34
  import * as crypto from 'crypto';
35
+ import { execFileSync } from 'child_process';
35
36
  import { getMemoryRoot } from './memory-initializer.js';
36
37
  import { encodeEmbedding } from './embedding-quantization.js';
37
38
  import { loadBetterSqlite3 } from './shared-sqlite.js';
@@ -54,20 +55,41 @@ import { isEncryptedBlob } from '../encryption/vault.js';
54
55
  // skip the native open (it would only throw) and warn once per distinct
55
56
  // dbPath (not once per process — a process can legitimately touch more
56
57
  // than one encrypted store via a custom `dbPath` argument).
58
+ /** Child-process body: print the first `argv[2]` bytes of `argv[1]` as hex. */
59
+ const HEADER_READER = "const fs=require('fs');const fd=fs.openSync(process.argv[1],'r');const b=Buffer.alloc(Number(process.argv[2]));" +
60
+ "const n=fs.readSync(fd,b,0,b.length,0);process.stdout.write(b.subarray(0,n).toString('hex'))";
61
+ const HEADER_READ_TIMEOUT_MS = 5000;
57
62
  /** Read just the first `len` bytes of a file without loading the whole file
58
- * into memory. Mirrors the pattern in commands/doctor.ts's readHeaderBytes
59
- * (not imported from there — this is a memory-layer module and must not
60
- * depend on the command layer). */
63
+ * into memory.
64
+ *
65
+ * #4040: on POSIX this never opens the file in THIS process. close(2) on any
66
+ * descriptor for an inode drops every advisory lock the process holds on it
67
+ * (https://www.sqlite.org/howtocorrupt.html), including the ones SQLite's own
68
+ * live handle holds - a second process then believes it is the only
69
+ * connection and checkpoints/deletes the WAL under us. This module (and the
70
+ * shared AgentDB handle) can own such a handle, so the header is read by a
71
+ * short-lived child: empty environment, one bounded read, 5 s timeout. Windows
72
+ * byte-range locks are per handle, not per process, so it reads in-process. */
61
73
  function readHeaderBytes(filePath, len) {
62
- const fd = fs.openSync(filePath, 'r');
63
- try {
64
- const buf = Buffer.alloc(len);
65
- const bytesRead = fs.readSync(fd, buf, 0, len, 0);
66
- return buf.subarray(0, bytesRead);
67
- }
68
- finally {
69
- fs.closeSync(fd);
74
+ if (process.platform === 'win32') {
75
+ const fd = fs.openSync(filePath, 'r');
76
+ try {
77
+ const buf = Buffer.alloc(len);
78
+ const bytesRead = fs.readSync(fd, buf, 0, len, 0);
79
+ return buf.subarray(0, bytesRead);
80
+ }
81
+ finally {
82
+ fs.closeSync(fd);
83
+ }
70
84
  }
85
+ const hex = execFileSync(process.execPath, ['-e', HEADER_READER, filePath, String(len)], {
86
+ env: {},
87
+ timeout: HEADER_READ_TIMEOUT_MS,
88
+ maxBuffer: len * 2 + 64,
89
+ stdio: ['ignore', 'pipe', 'ignore'],
90
+ encoding: 'utf8',
91
+ });
92
+ return Buffer.from(hex, 'hex');
71
93
  }
72
94
  // RFE1 wire format is magic(4) + iv(12) + ciphertext(N) + tag(16); the
73
95
  // shortest possible blob is 32 bytes. 64 gives headroom.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.56.4",
3
+ "version": "3.56.6",
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",
@@ -98,13 +98,13 @@
98
98
  "vitest": "^4.1.0"
99
99
  },
100
100
  "dependencies": {
101
- "@claude-flow/cli-core": "3.7.0",
101
+ "@claude-flow/cli-core": "3.7.1",
102
102
  "@claude-flow/codex": "3.0.3",
103
103
  "@claude-flow/mcp": "3.1.1",
104
104
  "@claude-flow/neural": "3.0.0",
105
105
  "@claude-flow/plugin-agent-federation": "1.0.1",
106
106
  "@claude-flow/security": "3.0.4",
107
- "@claude-flow/shared": "3.0.2",
107
+ "@claude-flow/shared": "3.0.3",
108
108
  "@iarna/toml": "^2.2.5",
109
109
  "@noble/ed25519": "2.3.0",
110
110
  "@ruvector/rabitq-wasm": "0.1.0",