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
@@ -18,6 +18,7 @@ import { generatePreCommitHook, generatePostCommitHook, generateSessionManager,
18
18
  import { getInstalledCliVersion, HELPERS_STAMP_FILE } from './helper-refresh.js';
19
19
  import { generateClaudeMd } from './claudemd-generator.js';
20
20
  import { recordMemoryPackagePath } from './memory-package-resolver.js';
21
+ import { ensureCommonJsCompanions } from './helper-companions.js';
21
22
  import { scanSettingsForRisk, formatRiskFindingsAsWarnings } from './settings-risk-scanner.js';
22
23
  /**
23
24
  * Skills to copy based on configuration
@@ -499,7 +500,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
499
500
  const sourceHelpersForUpgrade = findSourceHelpersDir();
500
501
  if (sourceHelpersForUpgrade) {
501
502
  // Keep in sync with helper-refresh.ts:CRITICAL_HELPERS.
502
- const criticalHelpers = ['auto-memory-hook.mjs', 'hook-handler.cjs', 'intelligence.cjs', 'statusline.cjs', 'router.js'];
503
+ const criticalHelpers = ['auto-memory-hook.mjs', 'hook-handler.cjs', 'intelligence.cjs', 'statusline.cjs', 'router.cjs'];
503
504
  for (const helperName of criticalHelpers) {
504
505
  const targetPath = path.join(targetDir, '.claude', 'helpers', helperName);
505
506
  const sourcePath = path.join(sourceHelpersForUpgrade, helperName);
@@ -524,7 +525,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
524
525
  'hook-handler.cjs': generateHookHandler(),
525
526
  'intelligence.cjs': generateIntelligenceStub(),
526
527
  'auto-memory-hook.mjs': generateAutoMemoryHook(),
527
- 'router.js': generateAgentRouter(), // ADR-389
528
+ 'router.cjs': generateAgentRouter(), // ADR-389 / #3555
528
529
  };
529
530
  for (const [helperName, content] of Object.entries(generatedCritical)) {
530
531
  const targetPath = path.join(targetDir, '.claude', 'helpers', helperName);
@@ -541,6 +542,11 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
541
542
  catch { }
542
543
  }
543
544
  }
545
+ // #3555: the refreshed hook-handler requires the .cjs companions; older
546
+ // installs only have session.js / memory.js, which can't load in ESM.
547
+ for (const name of await ensureCommonJsCompanions(path.join(targetDir, '.claude', 'helpers'))) {
548
+ result.created.push(`.claude/helpers/${name}`);
549
+ }
544
550
  // Stamp the installed version so the startup auto-refresh treats these as
545
551
  // current (no redundant re-copy on the next command).
546
552
  try {
@@ -1295,9 +1301,9 @@ async function writeHelpers(targetDir, options, result) {
1295
1301
  const helpers = {
1296
1302
  'pre-commit': generatePreCommitHook(),
1297
1303
  'post-commit': generatePostCommitHook(),
1298
- 'session.js': generateSessionManager(),
1299
- 'router.js': generateAgentRouter(),
1300
- 'memory.js': generateMemoryHelper(),
1304
+ 'session.cjs': generateSessionManager(),
1305
+ 'router.cjs': generateAgentRouter(),
1306
+ 'memory.cjs': generateMemoryHelper(),
1301
1307
  'hook-handler.cjs': generateHookHandler(),
1302
1308
  'intelligence.cjs': generateIntelligenceStub(),
1303
1309
  'auto-memory-hook.mjs': generateAutoMemoryHook(),
@@ -0,0 +1,3 @@
1
+ export declare const COMMONJS_COMPANION_HELPERS: readonly ["session.cjs", "memory.cjs"];
2
+ export declare function ensureCommonJsCompanions(helpersDir: string): Promise<string[]>;
3
+ //# sourceMappingURL=helper-companions.d.ts.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * CommonJS companion helpers that `hook-handler.cjs` requires but that are not
3
+ * in the signed critical set (#3555).
4
+ *
5
+ * They are CommonJS, so they ship as `.cjs`: a `.js` copy cannot load in a
6
+ * `"type":"module"` project. Installs made before the rename have only the
7
+ * `.js` copies, which the refreshed hook-handler no longer requires, so the
8
+ * refresh writes any missing `.cjs` companion from the CLI's own compiled
9
+ * generators (the trust root; no external file to verify). Stale `.js` copies
10
+ * are left in place and ignored. Existing `.cjs` files are never overwritten.
11
+ */
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ export const COMMONJS_COMPANION_HELPERS = ['session.cjs', 'memory.cjs'];
15
+ export async function ensureCommonJsCompanions(helpersDir) {
16
+ const missing = COMMONJS_COMPANION_HELPERS.filter((name) => !fs.existsSync(path.join(helpersDir, name)));
17
+ if (missing.length === 0)
18
+ return [];
19
+ const gen = await import('./helpers-generator.js');
20
+ const content = {
21
+ 'session.cjs': gen.generateSessionManager,
22
+ 'memory.cjs': gen.generateMemoryHelper,
23
+ };
24
+ const written = [];
25
+ for (const name of missing) {
26
+ try {
27
+ fs.writeFileSync(path.join(helpersDir, name), content[name](), { encoding: 'utf-8', mode: 0o755 });
28
+ written.push(name);
29
+ }
30
+ catch { /* best-effort: hook-handler degrades gracefully without it */ }
31
+ }
32
+ return written;
33
+ }
34
+ //# sourceMappingURL=helper-companions.js.map
@@ -0,0 +1,21 @@
1
+ export interface IntegrityResult {
2
+ /** Critical helper names whose on-disk hash doesn't match the signed manifest
3
+ * (or that are missing from disk despite being part of the signed set). */
4
+ tampered: string[];
5
+ /** Set only when the package's OWN signed manifest can't be verified at all —
6
+ * there's no ground truth to check the installed files against, distinct
7
+ * from a clean `tampered: []` result. */
8
+ blocked?: string;
9
+ }
10
+ /**
11
+ * Re-verify each of `criticalHelpers` present on disk in `helpersDir` against
12
+ * `sourceDir`'s signed manifest. Read-only — safe to call without holding any
13
+ * refresh lock; callers only need to acquire one if `tampered` comes back
14
+ * non-empty and they intend to repair it.
15
+ *
16
+ * `sourceDir: null` (package source unresolvable) returns `{ tampered: [] }`
17
+ * — fails open rather than flagging every install as tampered when there is
18
+ * no ground truth at all to compare against.
19
+ */
20
+ export declare function verifyInstalledCriticalHelpers(helpersDir: string, sourceDir: string | null, criticalHelpers: readonly string[], pubkeyPemOverride?: string): IntegrityResult;
21
+ //# sourceMappingURL=helper-integrity.d.ts.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Post-install integrity re-verification for signed critical helpers (#3565).
3
+ *
4
+ * `helper-refresh.ts` verifies a helper's signature/hash once, at copy time,
5
+ * then stamps the target directory with the installed CLI version. Every
6
+ * later command short-circuited on "stamp already matches" with NO further
7
+ * verification — so a critical helper modified on disk after that point (a
8
+ * sibling package's postinstall, a stray older CLI process racing a write,
9
+ * direct tampering) stayed silently modified through any number of
10
+ * subsequent commands, forever. This module re-hashes the already-INSTALLED
11
+ * helpers against the signed manifest independent of the stamp, so callers
12
+ * can re-run it on every invocation rather than trusting a single past check.
13
+ *
14
+ * Deliberately has no opinion on WHAT to do about tampering (re-copy, warn,
15
+ * refuse) — that policy lives in helper-refresh.ts, which already owns the
16
+ * fail-closed verify-then-copy path this result feeds into to heal.
17
+ */
18
+ import * as fs from 'fs';
19
+ import * as path from 'path';
20
+ import { verifyHelpersManifest, sha256Hex, HELPERS_MANIFEST_FILE, } from './helper-signing.js';
21
+ /**
22
+ * Re-verify each of `criticalHelpers` present on disk in `helpersDir` against
23
+ * `sourceDir`'s signed manifest. Read-only — safe to call without holding any
24
+ * refresh lock; callers only need to acquire one if `tampered` comes back
25
+ * non-empty and they intend to repair it.
26
+ *
27
+ * `sourceDir: null` (package source unresolvable) returns `{ tampered: [] }`
28
+ * — fails open rather than flagging every install as tampered when there is
29
+ * no ground truth at all to compare against.
30
+ */
31
+ export function verifyInstalledCriticalHelpers(helpersDir, sourceDir, criticalHelpers, pubkeyPemOverride) {
32
+ if (!sourceDir)
33
+ return { tampered: [] };
34
+ let trusted = null;
35
+ try {
36
+ trusted = verifyHelpersManifest(fs.readFileSync(path.join(sourceDir, HELPERS_MANIFEST_FILE), 'utf-8'), pubkeyPemOverride);
37
+ }
38
+ catch {
39
+ trusted = null;
40
+ }
41
+ if (!trusted)
42
+ return { tampered: [], blocked: 'signed helpers manifest missing or signature invalid' };
43
+ const tampered = [];
44
+ for (const name of criticalHelpers) {
45
+ const expected = trusted.files[name];
46
+ // Not part of THIS version's signed set (or source doesn't ship it) —
47
+ // nothing to compare the installed copy against.
48
+ if (!expected || !fs.existsSync(path.join(sourceDir, name)))
49
+ continue;
50
+ let actual = null;
51
+ try {
52
+ actual = sha256Hex(fs.readFileSync(path.join(helpersDir, name)));
53
+ }
54
+ catch {
55
+ actual = null;
56
+ }
57
+ if (actual !== expected)
58
+ tampered.push(name);
59
+ }
60
+ return { tampered };
61
+ }
62
+ //# sourceMappingURL=helper-integrity.js.map
@@ -10,6 +10,20 @@ interface RefreshOptions {
10
10
  lockRetryMsOverride?: number;
11
11
  malformedLockStaleMsOverride?: number;
12
12
  }
13
+ interface RefreshResult {
14
+ refreshed: boolean;
15
+ from?: string;
16
+ to?: string;
17
+ blocked?: string;
18
+ /** Set when `refreshed` was caused by #3565 healing, not a version bump —
19
+ * the stamp already matched but one or more critical helpers failed
20
+ * on-disk integrity verification and were re-copied from the verified
21
+ * source. */
22
+ healed?: boolean;
23
+ /** Critical helper names that failed integrity verification, present only
24
+ * alongside `healed: true`. */
25
+ tampered?: string[];
26
+ }
13
27
  /**
14
28
  * ruflo-owned helpers that carry hook logic (or the render surface for the
15
29
  * funnel disclosure row) and must track the package version. Adding to this
@@ -19,6 +33,8 @@ interface RefreshOptions {
19
33
  export declare const CRITICAL_HELPERS: string[];
20
34
  /** Installed @claude-flow/cli version — the value the helpers are stamped with. */
21
35
  export declare function getInstalledCliVersion(): string;
36
+ /** Locate the in-package `.claude/helpers` dir (the copy source). Null if not found. */
37
+ export declare function findPackageHelpersDir(): string | null;
22
38
  /**
23
39
  * On CLI startup, refresh critical helpers if their stamp is older than the
24
40
  * installed CLI version. Two passes:
@@ -70,17 +86,8 @@ export declare function getInstalledCliVersion(): string;
70
86
  * compat with pre-3.31.3 callers). If the global pass ran, its own result is
71
87
  * carried in the optional `global` field.
72
88
  */
73
- export declare function autoRefreshHelpersIfStale(cwd: string, opts?: RefreshOptions): Promise<{
74
- refreshed: boolean;
75
- from?: string;
76
- to?: string;
77
- blocked?: string;
78
- global?: {
79
- refreshed: boolean;
80
- from?: string;
81
- to?: string;
82
- blocked?: string;
83
- };
89
+ export declare function autoRefreshHelpersIfStale(cwd: string, opts?: RefreshOptions): Promise<RefreshResult & {
90
+ global?: RefreshResult;
84
91
  }>;
85
92
  export {};
86
93
  //# sourceMappingURL=helper-refresh.d.ts.map
@@ -20,6 +20,8 @@ import { fileURLToPath } from 'url';
20
20
  import { createRequire } from 'module';
21
21
  import * as semver from 'semver';
22
22
  import { verifyHelpersManifest, sha256Hex, HELPERS_MANIFEST_FILE, } from './helper-signing.js';
23
+ import { ensureCommonJsCompanions } from './helper-companions.js';
24
+ import { verifyInstalledCriticalHelpers } from './helper-integrity.js';
23
25
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
26
  /**
25
27
  * Walk up from `startDir` to the nearest ancestor whose `package.json` names
@@ -71,10 +73,11 @@ export const CRITICAL_HELPERS = [
71
73
  // statusline.cjs is here so the funnel disclosure row (ADR-301) reaches
72
74
  // existing installs on the next `ruflo` command, not only fresh `ruflo init`.
73
75
  'statusline.cjs',
74
- // router.js is loaded by hook-handler.cjs to label each prompt with an agent.
76
+ // router.cjs is loaded by hook-handler.cjs to label each prompt with an agent.
75
77
  // Without it here, installs kept the pre-#2257 substring router forever
76
- // ("latest" -> tester). ADR-389 / #3401.
77
- 'router.js',
78
+ // ("latest" -> tester). ADR-389 / #3401. It is CommonJS, so it ships as
79
+ // `.cjs`: a `.js` copy cannot load in a `"type":"module"` project (#3555).
80
+ 'router.cjs',
78
81
  ];
79
82
  function errorCode(error) {
80
83
  return typeof error === 'object' && error !== null && 'code' in error
@@ -230,7 +233,7 @@ export function getInstalledCliVersion() {
230
233
  }
231
234
  }
232
235
  /** Locate the in-package `.claude/helpers` dir (the copy source). Null if not found. */
233
- function findPackageHelpersDir() {
236
+ export function findPackageHelpersDir() {
234
237
  const candidates = [];
235
238
  try {
236
239
  const esmRequire = createRequire(import.meta.url);
@@ -313,7 +316,7 @@ async function writeCriticalHelpers(helpersDir, version, opts = {}) {
313
316
  'hook-handler.cjs': gen.generateHookHandler(),
314
317
  'intelligence.cjs': gen.generateIntelligenceStub(),
315
318
  'auto-memory-hook.mjs': gen.generateAutoMemoryHook(),
316
- 'router.js': gen.generateAgentRouter(), // ADR-389
319
+ 'router.cjs': gen.generateAgentRouter(), // ADR-389 / #3555
317
320
  // Fallback needs the same generator inputs `ruflo init` uses. We match the
318
321
  // hardcoded default (maxAgents 15) because the fallback fires when the
319
322
  // installed package is unresolvable — no way to read the user's project
@@ -339,9 +342,12 @@ async function writeCriticalHelpers(helpersDir, version, opts = {}) {
339
342
  }
340
343
  /**
341
344
  * On CLI startup: if an initialized project's critical helpers are stamped older
342
- * than the installed CLI version, silently re-copy them. Fast path is a single
343
- * stamp read + string compare (sub-ms); the copy runs at most once per version
344
- * bump. Best-effort, never throws. No-op outside a ruflo project (requires an
345
+ * than the installed CLI version, silently re-copy them. Fast path is a stamp
346
+ * read + string compare, PLUS a re-hash of each installed critical helper
347
+ * against the signed manifest (#3565) — still sub-ms for the handful of small
348
+ * files involved, and lock-free unless a mismatch is found. The copy runs at
349
+ * most once per version bump; the heal-on-tamper path can run on any call.
350
+ * Best-effort, never throws. No-op outside a ruflo project (requires an
345
351
  * existing hook-handler.cjs — never creates files in an unrelated directory).
346
352
  *
347
353
  * FORWARD-ONLY (never downgrades): refreshing on any mere INEQUALITY, rather
@@ -365,6 +371,9 @@ async function writeCriticalHelpers(helpersDir, version, opts = {}) {
365
371
  * `pubkeyPemOverride` let a test build its own tiny, throwaway-keypair-
366
372
  * signed fixture and get real, deterministic coverage of the verify → hash →
367
373
  * copy logic without depending on that.
374
+ *
375
+ * #3565: also re-verifies on every stamp-match call (see helper-integrity.ts)
376
+ * and heals a mismatch via this same verify-then-copy path.
368
377
  */
369
378
  async function refreshOneHelpersDirLocked(helpersDir, version, opts) {
370
379
  if (!fs.existsSync(path.join(helpersDir, 'hook-handler.cjs')))
@@ -377,7 +386,9 @@ async function refreshOneHelpersDirLocked(helpersDir, version, opts) {
377
386
  // on this repo (CLAUDE.md "Concurrent-session helper corruption"). The
378
387
  // existing semver.gte guard below still fires for normal installs — this
379
388
  // is the escape hatch for the small set of users editing helpers directly.
380
- // Applies to whichever dir this call is refreshing (project or global).
389
+ // Applies to whichever dir this call is refreshing (project or global),
390
+ // and also exempts it from the #3565 integrity re-check below — a
391
+ // deliberate local edit is not "tampering".
381
392
  if (fs.existsSync(path.join(helpersDir, '.LOCKED'))) {
382
393
  return { refreshed: false, blocked: '.LOCKED marker present — refresh skipped (delete to re-enable)' };
383
394
  }
@@ -386,8 +397,28 @@ async function refreshOneHelpersDirLocked(helpersDir, version, opts) {
386
397
  stamped = fs.readFileSync(path.join(helpersDir, HELPERS_STAMP_FILE), 'utf-8').trim();
387
398
  }
388
399
  catch { /* pre-feature: unstamped */ }
389
- if (stamped === version)
390
- return { refreshed: false }; // up to date — fast path
400
+ if (stamped === version) {
401
+ const source = opts.sourceDirOverride ?? findPackageHelpersDir();
402
+ const integrity = verifyInstalledCriticalHelpers(helpersDir, source, CRITICAL_HELPERS, opts.pubkeyPemOverride);
403
+ if (integrity.blocked)
404
+ return { refreshed: false, blocked: integrity.blocked };
405
+ if (integrity.tampered.length === 0)
406
+ return { refreshed: false }; // up to date AND verified intact
407
+ // Tampering detected post-install — heal via the same fail-closed
408
+ // verify-then-copy path a version-bump refresh already uses.
409
+ await opts.beforeWriteOverride?.();
410
+ const healRes = await writeCriticalHelpers(helpersDir, version, {
411
+ sourceDirOverride: opts.sourceDirOverride,
412
+ pubkeyPemOverride: opts.pubkeyPemOverride,
413
+ });
414
+ if (healRes.blocked)
415
+ return { refreshed: false, blocked: healRes.blocked };
416
+ if (healRes.wrote)
417
+ await ensureCommonJsCompanions(helpersDir); // #3555
418
+ return healRes.wrote
419
+ ? { refreshed: true, healed: true, tampered: integrity.tampered, from: stamped, to: version }
420
+ : { refreshed: false };
421
+ }
391
422
  if (stamped && semver.valid(stamped) && semver.valid(version) && semver.gte(stamped, version)) {
392
423
  // Stamped version is already >= what this binary reports — refreshing
393
424
  // would silently DOWNGRADE the helpers. Skip, untouched.
@@ -400,6 +431,8 @@ async function refreshOneHelpersDirLocked(helpersDir, version, opts) {
400
431
  });
401
432
  if (res.blocked)
402
433
  return { refreshed: false, blocked: res.blocked };
434
+ if (res.wrote)
435
+ await ensureCommonJsCompanions(helpersDir); // #3555
403
436
  return res.wrote ? { refreshed: true, from: stamped || '(unstamped)', to: version } : { refreshed: false };
404
437
  }
405
438
  async function refreshOneHelpersDir(helpersDir, version, opts) {
@@ -412,8 +445,18 @@ async function refreshOneHelpersDir(helpersDir, version, opts) {
412
445
  return { refreshed: false, blocked: '.LOCKED marker present — refresh skipped (delete to re-enable)' };
413
446
  }
414
447
  try {
415
- if (fs.readFileSync(path.join(helpersDir, HELPERS_STAMP_FILE), 'utf-8').trim() === version)
416
- return { refreshed: false };
448
+ if (fs.readFileSync(path.join(helpersDir, HELPERS_STAMP_FILE), 'utf-8').trim() === version) {
449
+ // #3565: re-hash before trusting a stamp match (helper-integrity.ts).
450
+ // Read-only — no lock needed unless something's actually wrong; the
451
+ // (rare) heal path below re-verifies under lock.
452
+ const source = opts.sourceDirOverride ?? findPackageHelpersDir();
453
+ const integrity = verifyInstalledCriticalHelpers(helpersDir, source, CRITICAL_HELPERS, opts.pubkeyPemOverride);
454
+ if (integrity.blocked)
455
+ return { refreshed: false, blocked: integrity.blocked };
456
+ if (integrity.tampered.length === 0)
457
+ return { refreshed: false };
458
+ // else: tampering found — fall through to acquire the lock and heal.
459
+ }
417
460
  }
418
461
  catch { /* unstamped: continue to the locked path */ }
419
462
  const releaseLock = await acquireRefreshLock(helpersDir, opts);
@@ -242,7 +242,7 @@ const [,, command, ...args] = process.argv;
242
242
  if (command && commands[command]) {
243
243
  commands[command](...args);
244
244
  } else {
245
- console.log('Usage: session.js <start|restore|end|status|update|metric> [args]');
245
+ console.log('Usage: session.cjs <start|restore|end|status|update|metric> [args]');
246
246
  }
247
247
 
248
248
  module.exports = commands;
@@ -336,7 +336,7 @@ if (require.main === module) {
336
336
  const result = routeTask(task);
337
337
  console.log(JSON.stringify(result, null, 2));
338
338
  } else {
339
- console.log('Usage: router.js <task description>');
339
+ console.log('Usage: router.cjs <task description>');
340
340
  console.log('\\nAvailable agents:', Object.keys(AGENT_CAPABILITIES).join(', '));
341
341
  }
342
342
  }
@@ -427,7 +427,7 @@ const value = valueParts.join(' ');
427
427
  if (command && commands[command]) {
428
428
  commands[command](key, value);
429
429
  } else {
430
- console.log('Usage: memory.js <get|set|delete|clear|keys> [key] [value]');
430
+ console.log('Usage: memory.cjs <get|set|delete|clear|keys> [key] [value]');
431
431
  }
432
432
 
433
433
  module.exports = commands;
@@ -504,9 +504,9 @@ export function generateHookHandler() {
504
504
  ' return null;',
505
505
  '}',
506
506
  '',
507
- "const router = safeRequire(path.join(helpersDir, 'router.js'));",
508
- "const session = safeRequire(path.join(helpersDir, 'session.js'));",
509
- "const memory = safeRequire(path.join(helpersDir, 'memory.js'));",
507
+ "const router = safeRequire(path.join(helpersDir, 'router.cjs'));",
508
+ "const session = safeRequire(path.join(helpersDir, 'session.cjs'));",
509
+ "const memory = safeRequire(path.join(helpersDir, 'memory.cjs'));",
510
510
  "const intelligence = safeRequire(path.join(helpersDir, 'intelligence.cjs'));",
511
511
  '',
512
512
  'const [,, command, ...args] = process.argv;',
@@ -1005,12 +1005,14 @@ export function generateAutoMemoryHook() {
1005
1005
  */
1006
1006
 
1007
1007
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
1008
- import { join, dirname } from 'path';
1008
+ import { join, dirname, resolve } from 'path';
1009
1009
  import { fileURLToPath } from 'url';
1010
1010
 
1011
1011
  const __filename = fileURLToPath(import.meta.url);
1012
1012
  const __dirname = dirname(__filename);
1013
- const PROJECT_ROOT = join(__dirname, '../..');
1013
+ const PROJECT_ROOT = process.env.CLAUDE_PROJECT_DIR
1014
+ ? resolve(process.env.CLAUDE_PROJECT_DIR)
1015
+ : join(__dirname, '../..');
1014
1016
  const DATA_DIR = join(PROJECT_ROOT, '.claude-flow', 'data');
1015
1017
  const STORE_PATH = join(DATA_DIR, 'auto-memory-store.json');
1016
1018
 
@@ -1356,7 +1358,7 @@ const [,, command, ...args] = process.argv;
1356
1358
  if (command && commands[command]) {
1357
1359
  commands[command](...args);
1358
1360
  } else {
1359
- console.log('Usage: session.js <start|restore|end|status>');
1361
+ console.log('Usage: session.cjs <start|restore|end|status>');
1360
1362
  console.log(\`Platform: \${platform}\`);
1361
1363
  console.log(\`Data dir: \${SESSION_DIR}\`);
1362
1364
  }
@@ -1374,9 +1376,9 @@ export function generateHelpers(options) {
1374
1376
  helpers['pre-commit'] = generatePreCommitHook();
1375
1377
  helpers['post-commit'] = generatePostCommitHook();
1376
1378
  // Cross-platform Node.js scripts
1377
- helpers['session.js'] = generateCrossPlatformSessionManager();
1378
- helpers['router.js'] = generateAgentRouter();
1379
- helpers['memory.js'] = generateMemoryHelper();
1379
+ helpers['session.cjs'] = generateCrossPlatformSessionManager();
1380
+ helpers['router.cjs'] = generateAgentRouter();
1381
+ helpers['memory.cjs'] = generateMemoryHelper();
1380
1382
  // Windows-specific scripts
1381
1383
  helpers['daemon-manager.ps1'] = generateWindowsDaemonManager();
1382
1384
  helpers['daemon-manager.cmd'] = generateWindowsBatchWrapper();
@@ -27,6 +27,12 @@ import { fileURLToPath } from 'url';
27
27
  import { dirname } from 'path';
28
28
  import { trackRequest } from './mcp-tools/request-tracker.js';
29
29
  import { isPolicyEnforcementEnabled, loadMcpPolicy, evaluateToolCall, } from './mcp-tools/policy-enforcer.js';
30
+ /** A failed task can be valid data; only an explicit error denotes tool failure. */
31
+ function hasToolError(result) {
32
+ return result !== null && typeof result === 'object'
33
+ && typeof result.error === 'string'
34
+ && result.error.trim().length > 0;
35
+ }
30
36
  // ESM-compatible __dirname
31
37
  const __filename = fileURLToPath(import.meta.url);
32
38
  const __dirname = dirname(__filename);
@@ -666,11 +672,12 @@ export class MCPServerManager extends EventEmitter {
666
672
  }
667
673
  try {
668
674
  const result = await callMCPTool(toolName, toolParams, { sessionId });
669
- trackRequest(toolName, true);
675
+ const isError = hasToolError(result);
676
+ trackRequest(toolName, !isError);
670
677
  return {
671
678
  jsonrpc: '2.0',
672
679
  id: message.id,
673
- result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] },
680
+ result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], isError },
674
681
  };
675
682
  }
676
683
  catch (error) {
@@ -741,7 +748,7 @@ export class MCPServerManager extends EventEmitter {
741
748
  handler: async (input, context) => {
742
749
  try {
743
750
  const result = await callMCPTool(tool.name, input || {}, { sessionId: context?.sessionId || fallbackSessionId });
744
- trackRequest(tool.name, true);
751
+ trackRequest(tool.name, !hasToolError(result));
745
752
  return result;
746
753
  }
747
754
  catch (error) {