claude-flow 3.47.1 → 3.49.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.
Files changed (84) hide show
  1. package/.claude/.proven-config-version +1 -0
  2. package/.claude/helpers/auto-memory-hook.mjs +6 -2
  3. package/.claude/helpers/hook-handler.cjs +7 -4
  4. package/.claude/helpers/learning-service.mjs +4 -2
  5. package/.claude/helpers/memory.cjs +1 -1
  6. package/.claude/helpers/metrics-db.mjs +4 -2
  7. package/.claude/helpers/router.cjs +1 -1
  8. package/.claude/helpers/session.cjs +1 -1
  9. package/.claude/proven-config.json +42 -0
  10. package/.claude-plugin/marketplace.json +16 -1
  11. package/README.md +1 -53
  12. package/README.zh-CN.md +1 -53
  13. package/node_modules/@claude-flow/codex/package.json +1 -1
  14. package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts.map +1 -1
  15. package/node_modules/@claude-flow/mcp/dist/tool-registry.js +11 -3
  16. package/node_modules/@claude-flow/mcp/dist/tool-registry.js.map +1 -1
  17. package/node_modules/@claude-flow/mcp/package.json +6 -3
  18. package/node_modules/@claude-flow/plugin-agent-federation/package.json +3 -3
  19. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +2 -6
  20. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
  21. package/node_modules/@claude-flow/security/dist/policy/engine.js +35 -1
  22. package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
  23. package/node_modules/@claude-flow/security/dist/policy/types.d.ts +18 -0
  24. package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
  25. package/node_modules/@claude-flow/security/package.json +2 -3
  26. package/package.json +7 -7
  27. package/v3/@claude-flow/cli/README.md +1 -53
  28. package/v3/@claude-flow/cli/bin/cli.js +4 -1
  29. package/v3/@claude-flow/cli/catalog-manifest.json +4 -4
  30. package/v3/@claude-flow/cli/dist/src/commands/analyze.js +2 -2
  31. package/v3/@claude-flow/cli/dist/src/commands/config.js +5 -7
  32. package/v3/@claude-flow/cli/dist/src/commands/doctor.d.ts +19 -1
  33. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +69 -8
  34. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +22 -9
  35. package/v3/@claude-flow/cli/dist/src/commands/memory.js +119 -34
  36. package/v3/@claude-flow/cli/dist/src/commands/metaharness.js +1 -0
  37. package/v3/@claude-flow/cli/dist/src/commands/plugins.js +44 -7
  38. package/v3/@claude-flow/cli/dist/src/commands/policy.js +5 -2
  39. package/v3/@claude-flow/cli/dist/src/commands/security.js +113 -55
  40. package/v3/@claude-flow/cli/dist/src/commands/session.js +132 -25
  41. package/v3/@claude-flow/cli/dist/src/commands/swarm.js +11 -11
  42. package/v3/@claude-flow/cli/dist/src/commands/task.js +5 -4
  43. package/v3/@claude-flow/cli/dist/src/index.js +10 -1
  44. package/v3/@claude-flow/cli/dist/src/init/executor.js +11 -5
  45. package/v3/@claude-flow/cli/dist/src/init/helper-companions.d.ts +3 -0
  46. package/v3/@claude-flow/cli/dist/src/init/helper-companions.js +34 -0
  47. package/v3/@claude-flow/cli/dist/src/init/helper-integrity.d.ts +21 -0
  48. package/v3/@claude-flow/cli/dist/src/init/helper-integrity.js +62 -0
  49. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +18 -11
  50. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +56 -13
  51. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +14 -12
  52. package/v3/@claude-flow/cli/dist/src/mcp-server.js +10 -3
  53. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +29 -62
  54. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +16 -7
  55. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +226 -51
  56. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.d.ts +7 -0
  57. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +131 -40
  58. package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.d.ts +8 -1
  59. package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.js +80 -8
  60. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.d.ts +15 -0
  61. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +270 -58
  62. package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.d.ts +13 -0
  63. package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.js +14 -0
  64. package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.d.ts +3 -0
  65. package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.js +5 -0
  66. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +11 -20
  67. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +348 -134
  68. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +9 -1
  69. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +47 -34
  70. package/v3/@claude-flow/cli/dist/src/plugins/manager.d.ts +37 -10
  71. package/v3/@claude-flow/cli/dist/src/plugins/manager.js +106 -20
  72. package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.d.ts +61 -0
  73. package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.js +84 -0
  74. package/v3/@claude-flow/cli/dist/src/ruvector/graph-analyzer.js +4 -1
  75. package/v3/@claude-flow/cli/dist/src/services/config-file-manager.js +30 -8
  76. package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +1 -1
  77. package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +67 -27
  78. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +6 -0
  79. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +27 -2
  80. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +2 -2
  81. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +23 -14
  82. package/v3/@claude-flow/cli/package.json +9 -9
  83. package/v3/@claude-flow/guidance/package.json +5 -6
  84. package/v3/@claude-flow/shared/package.json +6 -3
@@ -13,6 +13,7 @@ import { createBuiltinAIDefence } from '../security/builtin-aidefence.js';
13
13
  // for both validation and the traversal-depth maps below.
14
14
  const SCAN_DEPTHS = ['quick', 'standard', 'deep'];
15
15
  const SCAN_TYPES = ['code', 'deps', 'all'];
16
+ const SCAN_OUTPUTS = ['text', 'json', 'sarif'];
16
17
  // Real type predicates, not `as` casts. Array.includes() does not narrow a
17
18
  // string to a literal union on its own, so without these the depth-map lookups
18
19
  // below need an unchecked assertion — and an unchecked assertion would let a
@@ -23,6 +24,37 @@ const SCAN_TYPES = ['code', 'deps', 'all'];
23
24
  // is being fixed for.
24
25
  const isScanDepth = (v) => SCAN_DEPTHS.includes(v);
25
26
  const isScanType = (v) => SCAN_TYPES.includes(v);
27
+ const isScanOutput = (v) => SCAN_OUTPUTS.includes(v);
28
+ function scanSarif(findings) {
29
+ const ruleId = (type) => `RUFLO-${type.toUpperCase().replace(/[^A-Z0-9]+/g, '-')}`;
30
+ const level = (severity) => severity === 'critical' || severity === 'high' ? 'error' :
31
+ severity === 'medium' ? 'warning' : 'note';
32
+ const rules = [...new Map(findings.map(finding => [finding.type, {
33
+ id: ruleId(finding.type),
34
+ name: finding.type,
35
+ shortDescription: { text: finding.type },
36
+ }])).values()];
37
+ return {
38
+ version: '2.1.0',
39
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
40
+ runs: [{
41
+ tool: { driver: { name: 'Ruflo Security Scan', rules } },
42
+ results: findings.map(finding => {
43
+ const lineMatch = /^(.*):(\d+)$/.exec(finding.location);
44
+ const file = lineMatch?.[1] ?? (finding.location.startsWith('package.json:') ? 'package.json' : finding.location);
45
+ return {
46
+ ruleId: ruleId(finding.type),
47
+ level: level(finding.severity),
48
+ message: { text: finding.description },
49
+ locations: [{ physicalLocation: {
50
+ artifactLocation: { uri: file.replace(/\\/g, '/') },
51
+ ...(lineMatch ? { region: { startLine: Number(lineMatch[2]) } } : {}),
52
+ } }],
53
+ };
54
+ }),
55
+ }],
56
+ };
57
+ }
26
58
  // `full` was never a supported depth, but the CLI itself printed it — the
27
59
  // statusline insight, the announcement, the release-notes blurb, the CLAUDE.md
28
60
  // that `init` generates, and two shipped agent definitions all tell people to
@@ -62,6 +94,7 @@ const scanCommand = {
62
94
  const target = ctx.flags.target || '.';
63
95
  const requestedDepth = ctx.flags.depth || 'standard';
64
96
  const scanType = ctx.flags.type || 'all';
97
+ const requestedOutput = ctx.flags.output || 'text';
65
98
  const fix = ctx.flags.fix;
66
99
  // A security scanner must never silently degrade on an unrecognised enum
67
100
  // value. An unknown --depth used to fall through to the shallowest
@@ -91,6 +124,11 @@ const scanCommand = {
91
124
  output.printError(`Invalid --type '${scanType}'. Expected one of: ${SCAN_TYPES.join(', ')}.`);
92
125
  return { success: false, exitCode: 1 };
93
126
  }
127
+ if (!isScanOutput(requestedOutput)) {
128
+ output.printError(`Invalid --output '${requestedOutput}'. Expected one of: ${SCAN_OUTPUTS.join(', ')}.`);
129
+ return { success: false, exitCode: 1 };
130
+ }
131
+ const outputFormat = requestedOutput;
94
132
  // --target names WHAT gets scanned, and was never validated. A path that
95
133
  // does not exist (or is a file, not a directory) made every phase read
96
134
  // nothing, and the swallowed dir-read catches turned that into zero
@@ -106,11 +144,14 @@ const scanCommand = {
106
144
  output.printError(`Target is not a directory: ${resolvedTarget}`);
107
145
  return { success: false, exitCode: 1 };
108
146
  }
109
- output.writeln();
110
- output.writeln(output.bold('Security Scan'));
111
- output.writeln(output.dim('─'.repeat(50)));
147
+ if (outputFormat === 'text') {
148
+ output.writeln();
149
+ output.writeln(output.bold('Security Scan'));
150
+ output.writeln(output.dim('─'.repeat(50)));
151
+ }
112
152
  const spinner = output.createSpinner({ text: `Scanning ${target}...`, spinner: 'dots' });
113
- spinner.start();
153
+ if (outputFormat === 'text')
154
+ spinner.start();
114
155
  const findings = [];
115
156
  let criticalCount = 0, highCount = 0, mediumCount = 0, lowCount = 0;
116
157
  try {
@@ -151,9 +192,9 @@ const scanCommand = {
151
192
  else
152
193
  lowCount++;
153
194
  findings.push({
154
- severity: sev === 'critical' ? output.error('CRITICAL') :
155
- sev === 'high' ? output.warning('HIGH') :
156
- sev === 'moderate' || sev === 'medium' ? output.warning('MEDIUM') : output.info('LOW'),
195
+ severity: sev === 'critical' ? 'critical' :
196
+ sev === 'high' ? 'high' :
197
+ sev === 'moderate' || sev === 'medium' ? 'medium' : 'low',
157
198
  type: 'Dependency CVE',
158
199
  location: `package.json:${pkg}`,
159
200
  description: title.substring(0, 35),
@@ -206,7 +247,7 @@ const scanCommand = {
206
247
  if (pattern.test(lines[i])) {
207
248
  highCount++;
208
249
  findings.push({
209
- severity: output.warning('HIGH'),
250
+ severity: 'high',
210
251
  type: 'Hardcoded Secret',
211
252
  location: `${path.relative(target, fullPath)}:${i + 1}`,
212
253
  description: type,
@@ -261,7 +302,7 @@ const scanCommand = {
261
302
  else
262
303
  mediumCount++;
263
304
  findings.push({
264
- severity: severity === 'high' ? output.warning('HIGH') : output.warning('MEDIUM'),
305
+ severity: severity === 'high' ? 'high' : 'medium',
265
306
  type,
266
307
  location: `${path.relative(target, fullPath)}:${i + 1}`,
267
308
  description: desc,
@@ -280,35 +321,57 @@ const scanCommand = {
280
321
  const scanDepth = CODE_SCAN_DEPTH[depth];
281
322
  scanCodeDir(path.resolve(target), scanDepth);
282
323
  }
283
- spinner.succeed('Scan complete');
324
+ if (outputFormat === 'text')
325
+ spinner.succeed('Scan complete');
284
326
  // Display results
285
- output.writeln();
286
- if (findings.length > 0) {
287
- output.printTable({
288
- columns: [
289
- { key: 'severity', header: 'Severity', width: 12 },
290
- { key: 'type', header: 'Type', width: 18 },
291
- { key: 'location', header: 'Location', width: 25 },
292
- { key: 'description', header: 'Description', width: 35 },
293
- ],
294
- data: findings.slice(0, 20), // Show first 20
295
- });
296
- if (findings.length > 20) {
297
- output.writeln(output.dim(`... and ${findings.length - 20} more issues`));
327
+ if (outputFormat === 'text') {
328
+ output.writeln();
329
+ if (findings.length > 0) {
330
+ output.printTable({
331
+ columns: [
332
+ { key: 'severity', header: 'Severity', width: 12 },
333
+ { key: 'type', header: 'Type', width: 18 },
334
+ { key: 'location', header: 'Location', width: 25 },
335
+ { key: 'description', header: 'Description', width: 35 },
336
+ ],
337
+ data: findings.slice(0, 20).map(finding => ({
338
+ ...finding,
339
+ severity: finding.severity === 'critical' ? output.error('CRITICAL') :
340
+ finding.severity === 'high' ? output.warning('HIGH') :
341
+ finding.severity === 'medium' ? output.warning('MEDIUM') : output.info('LOW'),
342
+ })), // Show first 20
343
+ });
344
+ if (findings.length > 20) {
345
+ output.writeln(output.dim(`... and ${findings.length - 20} more issues`));
346
+ }
298
347
  }
348
+ else {
349
+ output.writeln(output.success('No security issues found!'));
350
+ }
351
+ output.writeln();
352
+ output.printBox([
353
+ `Target: ${target}`,
354
+ `Depth: ${depth}`,
355
+ `Type: ${scanType}`,
356
+ ``,
357
+ `Critical: ${criticalCount} High: ${highCount} Medium: ${mediumCount} Low: ${lowCount}`,
358
+ `Total Issues: ${findings.length}`,
359
+ ].join('\n'), 'Scan Summary');
299
360
  }
300
- else {
301
- output.writeln(output.success('No security issues found!'));
302
- }
303
- output.writeln();
304
- output.printBox([
305
- `Target: ${target}`,
306
- `Depth: ${depth}`,
307
- `Type: ${scanType}`,
308
- ``,
309
- `Critical: ${criticalCount} High: ${highCount} Medium: ${mediumCount} Low: ${lowCount}`,
310
- `Total Issues: ${findings.length}`,
311
- ].join('\n'), 'Scan Summary');
361
+ const record = {
362
+ timestamp: new Date().toISOString(),
363
+ target,
364
+ depth,
365
+ type: scanType,
366
+ summary: {
367
+ critical: criticalCount,
368
+ high: highCount,
369
+ medium: mediumCount,
370
+ low: lowCount,
371
+ total: findings.length,
372
+ },
373
+ findings,
374
+ };
312
375
  // Persist the scan result so downstream consumers (the statusline's
313
376
  // getSecurityStatus in funnel/local-signals.ts, which reads
314
377
  // `.claude/security-scans/*.json`) reflect real scan state. Best-effort:
@@ -316,20 +379,6 @@ const scanCommand = {
316
379
  try {
317
380
  const scanDirOut = path.join(path.resolve(target), '.claude', 'security-scans');
318
381
  fs.mkdirSync(scanDirOut, { recursive: true });
319
- const record = {
320
- timestamp: new Date().toISOString(),
321
- target,
322
- depth,
323
- type: scanType,
324
- summary: {
325
- critical: criticalCount,
326
- high: highCount,
327
- medium: mediumCount,
328
- low: lowCount,
329
- total: findings.length,
330
- },
331
- findings,
332
- };
333
382
  // Deterministic name keyed on scan config so repeated runs overwrite
334
383
  // rather than accumulate stale reports.
335
384
  const outFile = path.join(scanDirOut, `scan-${scanType}-${depth}.json`);
@@ -340,24 +389,33 @@ const scanCommand = {
340
389
  }
341
390
  // Auto-fix if requested
342
391
  if (fix && criticalCount + highCount > 0) {
343
- output.writeln();
392
+ if (outputFormat === 'text')
393
+ output.writeln();
344
394
  const fixSpinner = output.createSpinner({ text: 'Attempting to fix vulnerabilities...', spinner: 'dots' });
345
- fixSpinner.start();
395
+ if (outputFormat === 'text')
396
+ fixSpinner.start();
346
397
  try {
347
398
  try {
348
399
  execSync('npm audit fix', { cwd: path.resolve(target), encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
349
400
  }
350
401
  catch { /* npm audit fix may exit non-zero */ }
351
- fixSpinner.succeed('Applied available fixes (run scan again to verify)');
402
+ if (outputFormat === 'text')
403
+ fixSpinner.succeed('Applied available fixes (run scan again to verify)');
352
404
  }
353
405
  catch {
354
- fixSpinner.fail('Some fixes could not be applied automatically');
406
+ if (outputFormat === 'text')
407
+ fixSpinner.fail('Some fixes could not be applied automatically');
355
408
  }
356
409
  }
410
+ if (outputFormat === 'json')
411
+ output.printJson(record);
412
+ else if (outputFormat === 'sarif')
413
+ output.printJson(scanSarif(findings));
357
414
  return { success: findings.length === 0 || (criticalCount === 0 && highCount === 0) };
358
415
  }
359
416
  catch (error) {
360
- spinner.fail('Scan failed');
417
+ if (outputFormat === 'text')
418
+ spinner.fail('Scan failed');
361
419
  output.printError(`Error: ${error}`);
362
420
  return { success: false };
363
421
  }
@@ -1039,7 +1097,7 @@ const defendCommand = {
1039
1097
  medium: output.info,
1040
1098
  low: output.dim,
1041
1099
  }[threat.severity] || output.dim;
1042
- output.writeln(` ${severityColor(`[${threat.severity.toUpperCase()}]`)} ${threat.type}`);
1100
+ output.writeln(` ${severityColor.call(output, `[${threat.severity.toUpperCase()}]`)} ${threat.type}`);
1043
1101
  output.writeln(` ${output.dim(threat.description)}`);
1044
1102
  output.writeln(` Confidence: ${(threat.confidence * 100).toFixed(1)}%`);
1045
1103
  output.writeln();
@@ -28,6 +28,33 @@ function formatDate(dateStr) {
28
28
  // Otherwise show date
29
29
  return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
30
30
  }
31
+ /**
32
+ * #3575: the session MCP tools report failure as `{ error }` (or
33
+ * `success: false`) rather than throwing, so every call site must check before
34
+ * printing a success line or reading result fields.
35
+ */
36
+ function toolError(result) {
37
+ if (!result || typeof result !== 'object')
38
+ return 'the tool returned no result';
39
+ const r = result;
40
+ if (typeof r.error === 'string' && r.error.length > 0)
41
+ return r.error;
42
+ if (r.success === false)
43
+ return 'the operation failed';
44
+ return null;
45
+ }
46
+ /** #3573: say what happened to memory instead of printing an invented 0. */
47
+ function memoryCaptureLabel(capture, fallback) {
48
+ if (!capture)
49
+ return fallback ?? 'unknown';
50
+ if (!capture.requested)
51
+ return 'not included';
52
+ if (capture.status === 'error')
53
+ return `not captured (${capture.error || 'error'})`;
54
+ if (capture.status === 'no-store')
55
+ return '0 (no memory store found)';
56
+ return capture.entries;
57
+ }
31
58
  // Format session status
32
59
  function formatStatus(status) {
33
60
  switch (status) {
@@ -186,6 +213,12 @@ const saveCommand = {
186
213
  includeAgents: ctx.flags['include-agents'] !== false,
187
214
  includeTasks: ctx.flags['include-tasks'] !== false
188
215
  });
216
+ const failure = toolError(result);
217
+ if (failure) {
218
+ spinner.fail('Failed to save session');
219
+ output.printError(failure);
220
+ return { success: false, exitCode: 1 };
221
+ }
189
222
  spinner.succeed('Session saved');
190
223
  output.writeln();
191
224
  const stats = result.stats || {};
@@ -201,10 +234,13 @@ const saveCommand = {
201
234
  { property: 'Saved At', value: new Date(result.savedAt).toLocaleString() },
202
235
  { property: 'Agents', value: stats.agentCount ?? stats.agents ?? 0 },
203
236
  { property: 'Tasks', value: stats.taskCount ?? stats.tasks ?? 0 },
204
- { property: 'Memory Entries', value: stats.memoryEntries ?? 0 },
237
+ { property: 'Memory Entries', value: memoryCaptureLabel(result.memoryCapture, stats.memoryEntries) },
205
238
  { property: 'Total Size', value: formatSize(stats.totalSize ?? 0) }
206
239
  ]
207
240
  });
241
+ if (result.memoryCapture?.status === 'error') {
242
+ output.printWarning(`Memory was requested but could not be captured: ${result.memoryCapture.error}`);
243
+ }
208
244
  output.writeln();
209
245
  output.printSuccess(`Session saved: ${result.sessionId}`);
210
246
  output.printInfo(`Restore with: claude-flow session restore ${result.sessionId}`);
@@ -313,8 +349,25 @@ const restoreCommand = {
313
349
  restoreAgents,
314
350
  restoreTasks
315
351
  });
316
- spinner.succeed('Session restored');
352
+ const failure = toolError(result) ?? (result.restored === false ? 'Session not found' : null);
353
+ if (failure) {
354
+ spinner.fail('Failed to restore session');
355
+ output.printError(failure);
356
+ return { success: false, exitCode: 1 };
357
+ }
358
+ const memoryRestore = result.memoryRestore;
359
+ const memoryPartial = !!memoryRestore && memoryRestore.failed > 0;
360
+ if (memoryPartial) {
361
+ spinner.fail('Session restored with memory errors');
362
+ }
363
+ else {
364
+ spinner.succeed('Session restored');
365
+ }
317
366
  output.writeln();
367
+ // #3573: distinguish "not requested" from "the session holds no memory".
368
+ const memoryStatus = result.restoredComponents.memory
369
+ ? (memoryPartial ? output.error('Partial') : output.success('Restored'))
370
+ : restoreMemory ? output.dim('Not in session') : output.dim('Skipped');
318
371
  output.printTable({
319
372
  columns: [
320
373
  { key: 'component', header: 'Component', width: 20 },
@@ -324,27 +377,37 @@ const restoreCommand = {
324
377
  data: [
325
378
  {
326
379
  component: 'Memory',
327
- status: result.restored.memory ? output.success('Restored') : output.dim('Skipped'),
328
- count: result.stats.memoryEntriesRestored
380
+ status: memoryStatus,
381
+ count: result.restoredComponents.memory
382
+ ? (result.stats.memoryEntriesRestored ?? result.stats.memoryEntries)
383
+ : 0
329
384
  },
330
385
  {
331
386
  component: 'Agents',
332
- status: result.restored.agents ? output.success('Restored') : output.dim('Skipped'),
333
- count: result.stats.agentsRestored
387
+ status: result.restoredComponents.agents ? output.success('Restored') : output.dim('Skipped'),
388
+ count: result.restoredComponents.agents ? result.stats.agents : 0
334
389
  },
335
390
  {
336
391
  component: 'Tasks',
337
- status: result.restored.tasks ? output.success('Restored') : output.dim('Skipped'),
338
- count: result.stats.tasksRestored
392
+ status: result.restoredComponents.tasks ? output.success('Restored') : output.dim('Skipped'),
393
+ count: result.restoredComponents.tasks ? result.stats.tasks : 0
339
394
  }
340
395
  ]
341
396
  });
342
397
  output.writeln();
343
- output.printSuccess(`Session ${sessionId} restored successfully`);
398
+ if (memoryPartial) {
399
+ output.printWarning(`${memoryRestore.failed} memory entries could not be restored` +
400
+ (memoryRestore.errors.length ? `: ${memoryRestore.errors.join('; ')}` : ''));
401
+ }
402
+ else {
403
+ output.printSuccess(`Session ${sessionId} restored successfully`);
404
+ }
344
405
  if (ctx.flags.format === 'json') {
345
406
  output.printJson(result);
346
407
  }
347
- return { success: true, data: result };
408
+ return memoryPartial
409
+ ? { success: false, exitCode: 1, data: result }
410
+ : { success: true, data: result };
348
411
  }
349
412
  catch (error) {
350
413
  spinner.fail('Failed to restore session');
@@ -391,6 +454,11 @@ const deleteCommand = {
391
454
  }
392
455
  try {
393
456
  const result = await callMCPTool('session_delete', { sessionId });
457
+ const failure = toolError(result) ?? (result.deleted === false ? 'Session not found' : null);
458
+ if (failure) {
459
+ output.printError(`Failed to delete session: ${failure}`);
460
+ return { success: false, exitCode: 1 };
461
+ }
394
462
  output.writeln();
395
463
  output.printSuccess(`Session ${sessionId} deleted`);
396
464
  if (ctx.flags.format === 'json') {
@@ -412,7 +480,7 @@ const deleteCommand = {
412
480
  // Export subcommand
413
481
  const exportCommand = {
414
482
  name: 'export',
415
- description: 'Export session to file',
483
+ description: 'Export a saved session to a file. Usage: session export <session-id> [-o file] (or --latest)',
416
484
  options: [
417
485
  {
418
486
  name: 'output',
@@ -420,6 +488,12 @@ const exportCommand = {
420
488
  description: 'Output file path',
421
489
  type: 'string'
422
490
  },
491
+ {
492
+ name: 'latest',
493
+ description: 'Export the most recently saved session when no session id is given',
494
+ type: 'boolean',
495
+ default: false
496
+ },
423
497
  {
424
498
  name: 'format',
425
499
  short: 'f',
@@ -446,14 +520,23 @@ const exportCommand = {
446
520
  let outputPath = ctx.flags.output;
447
521
  const exportFormat = ctx.flags.format;
448
522
  const compress = ctx.flags.compress;
449
- // Get current session if no ID provided
523
+ // #3575: like `session delete`, refuse to guess which session to act on.
524
+ // Exporting the most recent one is available, but only when asked for.
450
525
  if (!sessionId) {
526
+ if (!ctx.flags.latest) {
527
+ output.printError('Session ID is required. Pass a session id, or --latest to export the most recently saved session.');
528
+ return { success: false, exitCode: 1 };
529
+ }
451
530
  try {
452
531
  const current = await callMCPTool('session_current', {});
532
+ if (toolError(current) || !current.sessionId) {
533
+ output.printError('No saved sessions to export.');
534
+ return { success: false, exitCode: 1 };
535
+ }
453
536
  sessionId = current.sessionId;
454
537
  }
455
538
  catch {
456
- output.printError('No active session. Provide a session ID to export.');
539
+ output.printError('No saved sessions to export.');
457
540
  return { success: false, exitCode: 1 };
458
541
  }
459
542
  }
@@ -469,6 +552,12 @@ const exportCommand = {
469
552
  sessionId,
470
553
  includeMemory: ctx.flags['include-memory'] !== false
471
554
  });
555
+ const failure = toolError(result);
556
+ if (failure || result.data === undefined) {
557
+ spinner.fail('Failed to export session');
558
+ output.printError(failure || 'The export returned no session data');
559
+ return { success: false, exitCode: 1 };
560
+ }
472
561
  // Format output
473
562
  let content;
474
563
  if (exportFormat === 'yaml') {
@@ -484,7 +573,8 @@ const exportCommand = {
484
573
  fs.writeFileSync(absolutePath, content, 'utf-8');
485
574
  spinner.succeed('Session exported');
486
575
  output.writeln();
487
- const exportStats = result.stats || {};
576
+ // session_export returns the whole record under `data`; its stats live there.
577
+ const exportStats = result.stats || result.data?.stats || {};
488
578
  output.printTable({
489
579
  columns: [
490
580
  { key: 'property', header: 'Property', width: 18 },
@@ -522,7 +612,7 @@ const exportCommand = {
522
612
  // Import subcommand
523
613
  const importCommand = {
524
614
  name: 'import',
525
- description: 'Import session from file',
615
+ description: 'Import a session from a file written by session export. Usage: session import <file> [--name n] [--activate]',
526
616
  options: [
527
617
  {
528
618
  name: 'name',
@@ -555,21 +645,32 @@ const importCommand = {
555
645
  const spinner = output.createSpinner({ text: 'Importing session...' });
556
646
  spinner.start();
557
647
  try {
558
- const content = fs.readFileSync(absolutePath, 'utf-8');
559
- let data;
560
- // Parse based on extension
648
+ // The YAML written by `session export --format yaml` is display-only;
649
+ // there is no YAML parser here, so say so instead of failing obscurely.
561
650
  if (absolutePath.endsWith('.yaml') || absolutePath.endsWith('.yml')) {
562
- // Simple YAML parsing (basic implementation)
563
- data = JSON.parse(content); // Would need proper YAML parser
564
- }
565
- else {
566
- data = JSON.parse(content);
651
+ try {
652
+ JSON.parse(fs.readFileSync(absolutePath, 'utf-8'));
653
+ }
654
+ catch {
655
+ spinner.fail('Failed to import session');
656
+ output.printError('YAML session import is not supported. Export with --format json and import that file.');
657
+ return { success: false, exitCode: 1 };
658
+ }
567
659
  }
660
+ // #3575: session_import reads the file itself from `inputPath`. Passing a
661
+ // parsed `data` object used to hit its required-argument check, and the
662
+ // CLI then printed "Session imported" before crashing on the error result.
568
663
  const result = await callMCPTool('session_import', {
569
- data,
664
+ inputPath: absolutePath,
570
665
  name: sessionName,
571
666
  activate
572
667
  });
668
+ const failure = toolError(result);
669
+ if (failure) {
670
+ spinner.fail('Failed to import session');
671
+ output.printError(failure);
672
+ return { success: false, exitCode: 1 };
673
+ }
573
674
  spinner.succeed('Session imported');
574
675
  output.writeln();
575
676
  output.printTable({
@@ -619,6 +720,11 @@ const currentCommand = {
619
720
  action: async (ctx) => {
620
721
  try {
621
722
  const result = await callMCPTool('session_current', { includeStats: true });
723
+ if (toolError(result)) {
724
+ output.printWarning('No active session');
725
+ output.printInfo('Save one with "claude-flow session save"');
726
+ return { success: true, data: { active: false } };
727
+ }
622
728
  if (ctx.flags.format === 'json') {
623
729
  output.printJson(result);
624
730
  return { success: true, data: result };
@@ -727,7 +833,8 @@ export const sessionCommand = {
727
833
  { command: 'claude-flow session save -n "checkpoint-1"', description: 'Save current session' },
728
834
  { command: 'claude-flow session restore session-123', description: 'Restore a session' },
729
835
  { command: 'claude-flow session delete session-123', description: 'Delete a session' },
730
- { command: 'claude-flow session export -o backup.json', description: 'Export session to file' },
836
+ { command: 'claude-flow session export session-123 -o backup.json', description: 'Export a session to file' },
837
+ { command: 'claude-flow session export --latest -o backup.json', description: 'Export the most recently saved session' },
731
838
  { command: 'claude-flow session import backup.json', description: 'Import session from file' },
732
839
  { command: 'claude-flow session current', description: 'Show current session' }
733
840
  ],
@@ -196,17 +196,14 @@ function getSwarmStatus(swarmId) {
196
196
  // Ignore
197
197
  }
198
198
  }
199
- // Calculate dynamic progress based on actual state
200
- // If no swarm state, show 0%. Otherwise calculate from completed tasks
199
+ // Progress is completed / total tasks. With no tasks there is nothing to be
200
+ // a fraction of, so it is `null` with a reason — never an invented number
201
+ // (#3572: a hard-coded 5% was shown for a swarm with no tasks, forever).
201
202
  const totalTasks = completedTasks + inProgressTasks + pendingTasks;
202
- let progress = 0;
203
- if (totalTasks > 0) {
204
- progress = Math.round((completedTasks / totalTasks) * 100);
205
- }
206
- else if (swarmState) {
207
- // Swarm initialized but no tasks yet
208
- progress = 5;
209
- }
203
+ const progress = totalTasks > 0
204
+ ? Math.round((completedTasks / totalTasks) * 100)
205
+ : null;
206
+ const progressReason = totalTasks > 0 ? null : 'no tasks';
210
207
  // Determine status
211
208
  let status = 'idle';
212
209
  if (inProgressTasks > 0 || activeAgents > 0) {
@@ -243,6 +240,7 @@ function getSwarmStatus(swarmId) {
243
240
  completed: 0
244
241
  },
245
242
  progress,
243
+ progressReason,
246
244
  tasks: {
247
245
  total: totalTasks,
248
246
  completed: completedTasks,
@@ -738,7 +736,9 @@ const statusCommand = {
738
736
  output.writeln(output.bold(`Swarm Status: ${status.id ?? output.dim('unknown id')}`));
739
737
  output.writeln();
740
738
  // Progress bar
741
- output.writeln(`Overall Progress: ${output.progressBar(status.progress, 100, 40)}`);
739
+ output.writeln(status.progress === null
740
+ ? `Overall Progress: ${output.dim(`n/a (${status.progressReason})`)}`
741
+ : `Overall Progress: ${output.progressBar(status.progress, 100, 40)}`);
742
742
  output.writeln();
743
743
  // Agent status
744
744
  output.writeln(output.bold('Agents'));
@@ -154,7 +154,7 @@ const createCommand = {
154
154
  type: taskType,
155
155
  description,
156
156
  priority: priority || 'normal',
157
- assignedTo: ctx.flags.assign ? [ctx.flags.assign] : undefined,
157
+ assignTo: ctx.flags.assign ? String(ctx.flags.assign).split(',').map(id => id.trim()).filter(Boolean) : undefined,
158
158
  parentId: ctx.flags.parent,
159
159
  dependencies,
160
160
  tags,
@@ -245,14 +245,15 @@ const listCommand = {
245
245
  }
246
246
  ],
247
247
  action: async (ctx) => {
248
- const status = ctx.flags.all ? 'all' : ctx.flags.status || 'pending,running';
248
+ const requestedStatus = ctx.flags.all ? 'all' : ctx.flags.status || 'pending,in_progress';
249
+ const status = requestedStatus === 'all' ? undefined : requestedStatus.replace(/\brunning\b/g, 'in_progress');
249
250
  const limit = ctx.flags.limit;
250
251
  try {
251
252
  const result = await callMCPTool('task_list', {
252
253
  status,
253
254
  type: ctx.flags.type,
254
255
  priority: ctx.flags.priority,
255
- agentId: ctx.flags.agent,
256
+ assignedTo: ctx.flags.agent,
256
257
  limit,
257
258
  offset: 0
258
259
  });
@@ -277,7 +278,7 @@ const listCommand = {
277
278
  { key: 'progress', header: 'Progress', width: 10 }
278
279
  ],
279
280
  data: result.tasks.map(t => ({
280
- id: t.id,
281
+ id: t.taskId,
281
282
  type: t.type,
282
283
  description: t.description.length > 27
283
284
  ? t.description.slice(0, 27) + '...'
@@ -155,10 +155,19 @@ export class CLI {
155
155
  // loudly (not silent) — the existing project helpers were left intact.
156
156
  this.output.printWarning(`Skipped helper auto-refresh — ${r.blocked}. Reinstall @claude-flow/cli from a trusted source.`);
157
157
  }
158
+ else if (r.healed) {
159
+ // #3565: a critical helper's stamp matched but its on-disk content
160
+ // didn't hash-match the signed manifest — restored, but always
161
+ // surfaced (not verbose-gated like a routine version-bump refresh).
162
+ this.output.printWarning(`Detected tampered critical helper(s) in .claude/helpers (${(r.tampered || []).join(', ')}) — restored verified content from the installed package. If this wasn't expected, find out what modified them.`);
163
+ }
158
164
  else if (r.refreshed && this.output.isVerbose()) {
159
165
  this.output.printDebug(`Refreshed .claude/helpers (${r.from} → ${r.to})`);
160
166
  }
161
- if (r.global?.refreshed && this.output.isVerbose()) {
167
+ if (r.global?.healed) {
168
+ this.output.printWarning(`Detected tampered critical helper(s) in ~/.claude/helpers (${(r.global.tampered || []).join(', ')}) — restored verified content.`);
169
+ }
170
+ else if (r.global?.refreshed && this.output.isVerbose()) {
162
171
  this.output.printDebug(`Refreshed ~/.claude/helpers (${r.global.from} → ${r.global.to})`);
163
172
  }
164
173
  else if (r.global?.blocked && r.global.blocked !== r.blocked) {