@claude-flow/cli 3.45.0 → 3.46.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 (73) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/README.md +2 -0
  3. package/bin/cli.js +13 -0
  4. package/bin/mcp-server.js +16 -0
  5. package/catalog-manifest.json +2 -2
  6. package/dist/src/commands/doctor.d.ts +3 -0
  7. package/dist/src/commands/doctor.js +131 -2
  8. package/dist/src/commands/embeddings.js +50 -7
  9. package/dist/src/commands/init.js +5 -2
  10. package/dist/src/commands/mcp.js +27 -4
  11. package/dist/src/mcp-server.d.ts +50 -6
  12. package/dist/src/mcp-server.js +267 -33
  13. package/dist/src/mcp-tools/agent-execute-core.js +7 -0
  14. package/dist/src/mcp-tools/agentdb-tools.js +20 -8
  15. package/dist/src/mcp-tools/browser-tools.js +30 -23
  16. package/dist/src/mcp-tools/memory-tools.js +9 -4
  17. package/dist/src/mcp-tools/metaharness-tools.js +1 -1
  18. package/dist/src/mcp-tools/policy-enforcer.d.ts +4 -4
  19. package/dist/src/mcp-tools/policy-enforcer.js +4 -4
  20. package/dist/src/mcp-tools/terminal-tools.js +11 -1
  21. package/dist/src/mcp-tools/workflow-tools.js +1 -1
  22. package/dist/src/memory/ewc-consolidation.d.ts +16 -1
  23. package/dist/src/memory/ewc-consolidation.js +63 -13
  24. package/dist/src/memory/intelligence.d.ts +6 -2
  25. package/dist/src/memory/intelligence.js +18 -11
  26. package/dist/src/memory/memory-bridge.js +84 -0
  27. package/dist/src/ruvector/model-router.js +19 -1
  28. package/dist/src/services/distill-oracle.js +2 -1
  29. package/dist/src/services/git-workspace-identity.js +5 -8
  30. package/dist/src/services/policy-runtime.js +114 -4
  31. package/dist/src/services/worker-daemon.js +8 -3
  32. package/node_modules/@claude-flow/codex/dist/worktrees/coordinator.d.ts.map +1 -1
  33. package/node_modules/@claude-flow/codex/dist/worktrees/coordinator.js +2 -6
  34. package/node_modules/@claude-flow/codex/dist/worktrees/coordinator.js.map +1 -1
  35. package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts.map +1 -1
  36. package/node_modules/@claude-flow/mcp/dist/transport/http.js +14 -1
  37. package/node_modules/@claude-flow/mcp/dist/transport/http.js.map +1 -1
  38. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.d.ts +40 -0
  39. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.d.ts.map +1 -0
  40. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.js +100 -0
  41. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.js.map +1 -0
  42. package/node_modules/@claude-flow/plugin-agent-federation/dist/index.d.ts +1 -0
  43. package/node_modules/@claude-flow/plugin-agent-federation/dist/index.d.ts.map +1 -1
  44. package/node_modules/@claude-flow/plugin-agent-federation/dist/index.js +1 -0
  45. package/node_modules/@claude-flow/plugin-agent-federation/dist/index.js.map +1 -1
  46. package/node_modules/@claude-flow/plugin-agent-federation/package.json +1 -0
  47. package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
  48. package/node_modules/@claude-flow/security/dist/safe-git.d.ts +22 -0
  49. package/node_modules/@claude-flow/security/dist/safe-git.d.ts.map +1 -0
  50. package/node_modules/@claude-flow/security/dist/safe-git.js +63 -0
  51. package/node_modules/@claude-flow/security/dist/safe-git.js.map +1 -0
  52. package/package.json +5 -5
  53. package/plugins/ruflo-metaharness/agents/metaharness-architect.md +1 -1
  54. package/plugins/ruflo-metaharness/commands/ruflo-metaharness.md +1 -1
  55. package/plugins/ruflo-metaharness/scripts/_darwin.mjs +84 -12
  56. package/plugins/ruflo-metaharness/scripts/_harness.mjs +10 -4
  57. package/plugins/ruflo-metaharness/scripts/_invoke.mjs +120 -6
  58. package/plugins/ruflo-metaharness/scripts/_redblue.mjs +33 -6
  59. package/plugins/ruflo-metaharness/scripts/audit-list.mjs +11 -10
  60. package/plugins/ruflo-metaharness/scripts/audit-trend.mjs +8 -7
  61. package/plugins/ruflo-metaharness/scripts/drift-from-history.mjs +2 -3
  62. package/plugins/ruflo-metaharness/scripts/oia-audit.mjs +8 -7
  63. package/plugins/ruflo-metaharness/scripts/similarity.mjs +9 -8
  64. package/plugins/ruflo-metaharness/scripts/smoke.sh +31 -0
  65. package/plugins/ruflo-metaharness/scripts/test-no-tool-time-npx.mjs +319 -0
  66. package/plugins/ruflo-metaharness/scripts/test-pin-alignment.mjs +182 -0
  67. package/plugins/ruflo-metaharness/skills/harness-evolve/SKILL.md +4 -2
  68. package/plugins/ruflo-metaharness/skills/harness-gepa/SKILL.md +1 -1
  69. package/plugins/ruflo-metaharness/skills/harness-learn/SKILL.md +1 -1
  70. package/plugins/ruflo-metaharness/skills/harness-mcp-scan/SKILL.md +1 -1
  71. package/plugins/ruflo-metaharness/skills/harness-score/SKILL.md +1 -1
  72. package/plugins/ruflo-metaharness/skills/harness-security-bench/SKILL.md +3 -1
  73. package/plugins/ruflo-metaharness/skills/harness-threat-model/SKILL.md +1 -1
@@ -153,6 +153,16 @@ export const terminalTools = [
153
153
  const startTime = Date.now();
154
154
  let output;
155
155
  let exitCode;
156
+ // dream-cycle 2026-09-21 (post-review): session.env was validated once
157
+ // at terminal_create time and trusted forever after via the on-disk
158
+ // store — a session persisted before a denylist update (or a store.json
159
+ // edited/restored out of band) could carry an env that the *current*
160
+ // validateEnv() would reject, silently bypassing the denylist for every
161
+ // execute on that pre-existing session. Re-validate at execution time
162
+ // too; drop (not fail the whole call) on now-invalid env so a stale
163
+ // session degrades to a clean environment rather than blocking.
164
+ const vSessionEnv = validateEnv(session.env, 'session.env');
165
+ const effectiveEnv = vSessionEnv.valid ? vSessionEnv.sanitized : {};
156
166
  try {
157
167
  output = execSync(command, {
158
168
  cwd,
@@ -160,7 +170,7 @@ export const terminalTools = [
160
170
  timeout,
161
171
  maxBuffer: 5 * 1024 * 1024,
162
172
  stdio: ['pipe', 'pipe', 'pipe'],
163
- env: { ...process.env, ...session.env },
173
+ env: { ...process.env, ...effectiveEnv },
164
174
  });
165
175
  exitCode = 0;
166
176
  }
@@ -256,7 +256,7 @@ export const workflowTools = [
256
256
  const stepResults = [];
257
257
  // Variable substitution: {{name}} → workflow.variables[name] OR steps[stepId].output
258
258
  const interp = (text) => {
259
- return text.replace(/\{\{\s*([a-zA-Z_][\w.]*)\s*\}\}/g, (_, key) => {
259
+ return text.replace(/\{\{\s*([a-zA-Z_][\w.-]*)\s*\}\}/g, (_, key) => {
260
260
  // Direct variable
261
261
  if (key in workflow.variables)
262
262
  return String(workflow.variables[key]);
@@ -212,7 +212,13 @@ export declare class EWCConsolidator {
212
212
  * Called by SONA after distillLearning to track which patterns
213
213
  * are important and should be protected from forgetting.
214
214
  *
215
- * Uses online averaging: F_new = alpha * F_old + (1-alpha) * F_current
215
+ * Uses online averaging: F_new = (1 - decay) * F_old + decay * F_current
216
+ * (same convention as computeFisherMatrix()/recordGradient() above: decay
217
+ * is the weight given to the new batch, so a small decay — the default,
218
+ * 0.01 — means the accumulated Fisher decays slowly and survives many
219
+ * subsequent low-signal calls, per the standard online-EWC/EWC++ update
220
+ * rule, e.g. Chaudhry et al., "Riemannian Walk for Incremental Learning",
221
+ * ECCV 2018, arXiv:1801.10112).
216
222
  *
217
223
  * @param confidenceChanges - Array of {id, embedding, oldConf, newConf}
218
224
  */
@@ -235,6 +241,15 @@ export declare class EWCConsolidator {
235
241
  * Clear all patterns and history (full reset)
236
242
  */
237
243
  clear(): void;
244
+ /**
245
+ * Validate a loaded globalFisher candidate before trusting it: must be an
246
+ * array of exactly `dimensions` finite, non-negative numbers. Fisher
247
+ * values are sums of squares (see computeFisherMatrix/recordGradient/
248
+ * updateFisherFromConfidences above), so a negative or non-finite entry
249
+ * means the persisted file is corrupted or from an incompatible layout,
250
+ * not a legitimate state to resume from.
251
+ */
252
+ private isValidFisherArray;
238
253
  /**
239
254
  * Calculate importance score for a pattern based on usage
240
255
  */
@@ -357,38 +357,68 @@ export class EWCConsolidator {
357
357
  * Called by SONA after distillLearning to track which patterns
358
358
  * are important and should be protected from forgetting.
359
359
  *
360
- * Uses online averaging: F_new = alpha * F_old + (1-alpha) * F_current
360
+ * Uses online averaging: F_new = (1 - decay) * F_old + decay * F_current
361
+ * (same convention as computeFisherMatrix()/recordGradient() above: decay
362
+ * is the weight given to the new batch, so a small decay — the default,
363
+ * 0.01 — means the accumulated Fisher decays slowly and survives many
364
+ * subsequent low-signal calls, per the standard online-EWC/EWC++ update
365
+ * rule, e.g. Chaudhry et al., "Riemannian Walk for Incremental Learning",
366
+ * ECCV 2018, arXiv:1801.10112).
361
367
  *
362
368
  * @param confidenceChanges - Array of {id, embedding, oldConf, newConf}
363
369
  */
364
370
  updateFisherFromConfidences(confidenceChanges) {
365
371
  if (confidenceChanges.length === 0)
366
372
  return;
367
- const alpha = this.config.fisherDecayRate;
373
+ const decay = this.config.fisherDecayRate;
368
374
  const currentFisher = new Array(this.config.dimensions).fill(0);
369
375
  let sampleCount = 0;
370
376
  for (const change of confidenceChanges) {
371
377
  if (!change.embedding || change.embedding.length === 0)
372
378
  continue;
379
+ if (!Number.isFinite(change.oldConf) || !Number.isFinite(change.newConf))
380
+ continue;
373
381
  const confDelta = Math.abs(change.newConf - change.oldConf);
374
382
  if (confDelta === 0)
375
383
  continue;
376
- sampleCount++;
377
384
  const len = Math.min(change.embedding.length, this.config.dimensions);
378
- // Squared gradient proxy: embedding scaled by confidence change magnitude
385
+ // Squared gradient proxy: embedding scaled by confidence change magnitude.
386
+ // Validate the whole sample before folding any of it in -- a single
387
+ // non-finite embedding dimension (NaN/Infinity) must not silently poison
388
+ // the shared accumulator for every other pattern in this batch.
389
+ const sampleFisher = new Array(len);
390
+ let sampleValid = true;
379
391
  for (let i = 0; i < len; i++) {
380
392
  const grad = change.embedding[i] * confDelta;
381
- currentFisher[i] += grad * grad;
393
+ const sq = grad * grad;
394
+ if (!Number.isFinite(sq)) {
395
+ sampleValid = false;
396
+ break;
397
+ }
398
+ sampleFisher[i] = sq;
382
399
  }
383
- }
384
- if (sampleCount > 0) {
385
- for (let i = 0; i < this.config.dimensions; i++) {
386
- currentFisher[i] /= sampleCount;
400
+ if (!sampleValid)
401
+ continue;
402
+ sampleCount++;
403
+ for (let i = 0; i < len; i++) {
404
+ currentFisher[i] += sampleFisher[i];
387
405
  }
388
406
  }
389
- // Online EMA: F_new = alpha * F_old + (1-alpha) * F_current
407
+ // No sample contributed usable signal (all filtered out, or every delta was
408
+ // zero): leave globalFisher and persisted state untouched rather than
409
+ // decaying accumulated importance toward an empty/no-op batch.
410
+ if (sampleCount === 0)
411
+ return;
412
+ for (let i = 0; i < this.config.dimensions; i++) {
413
+ currentFisher[i] /= sampleCount;
414
+ }
415
+ // Online EMA: F_new = (1 - decay) * F_old + decay * F_current
390
416
  for (let i = 0; i < this.config.dimensions; i++) {
391
- this.globalFisher[i] = alpha * this.globalFisher[i] + (1 - alpha) * currentFisher[i];
417
+ const next = (1 - decay) * this.globalFisher[i] + decay * currentFisher[i];
418
+ // Defensive: never let a non-finite result (e.g. from a corrupted prior
419
+ // globalFisher[i]) propagate into the live signal -- keep the last known
420
+ // finite value instead of poisoning it further.
421
+ this.globalFisher[i] = Number.isFinite(next) ? next : this.globalFisher[i];
392
422
  }
393
423
  this.saveToDisk();
394
424
  }
@@ -432,6 +462,19 @@ export class EWCConsolidator {
432
462
  // ============================================================================
433
463
  // Private Methods
434
464
  // ============================================================================
465
+ /**
466
+ * Validate a loaded globalFisher candidate before trusting it: must be an
467
+ * array of exactly `dimensions` finite, non-negative numbers. Fisher
468
+ * values are sums of squares (see computeFisherMatrix/recordGradient/
469
+ * updateFisherFromConfidences above), so a negative or non-finite entry
470
+ * means the persisted file is corrupted or from an incompatible layout,
471
+ * not a legitimate state to resume from.
472
+ */
473
+ isValidFisherArray(value) {
474
+ return (Array.isArray(value) &&
475
+ value.length === this.config.dimensions &&
476
+ value.every((v) => typeof v === 'number' && Number.isFinite(v) && v >= 0));
477
+ }
435
478
  /**
436
479
  * Calculate importance score for a pattern based on usage
437
480
  */
@@ -524,8 +567,15 @@ export class EWCConsolidator {
524
567
  if (state.version !== '1.0.0') {
525
568
  throw new Error(`Unsupported state version: ${state.version}`);
526
569
  }
527
- // Restore state
528
- this.globalFisher = state.globalFisher || new Array(this.config.dimensions).fill(0);
570
+ // Restore the Fisher matrix, quarantining anything malformed -- wrong
571
+ // length, non-numeric/non-finite entries (e.g. JSON's lossy NaN/Infinity
572
+ // -> null round-trip), or negative values (Fisher values are sums of
573
+ // squares and can never legitimately be negative) -- rather than
574
+ // silently absorbing corrupted or truncated state into live importance
575
+ // tracking on restart.
576
+ this.globalFisher = this.isValidFisherArray(state.globalFisher)
577
+ ? state.globalFisher
578
+ : new Array(this.config.dimensions).fill(0);
529
579
  // Restore patterns
530
580
  this.patterns.clear();
531
581
  if (state.patterns) {
@@ -248,6 +248,7 @@ export interface UnifiedLearningStats {
248
248
  signalsProcessed: number;
249
249
  lastAdaptation: number | null;
250
250
  source: string;
251
+ scope: 'project-persisted';
251
252
  };
252
253
  sona: {
253
254
  trajectoriesTotal: number;
@@ -256,6 +257,8 @@ export interface UnifiedLearningStats {
256
257
  avgAdaptationTimeMs: number;
257
258
  source: string;
258
259
  available: boolean;
260
+ scope: 'process-local';
261
+ metric: 'recent-buffered-trajectories';
259
262
  };
260
263
  memoryBridge: {
261
264
  totalEntries: number;
@@ -270,8 +273,9 @@ export interface UnifiedLearningStats {
270
273
  source: string;
271
274
  };
272
275
  consistency: {
273
- sonaTracksGlobal: boolean;
274
- sonaTracksGlobalDelta: number;
276
+ /** These counters measure different lifetimes and cannot be compared. */
277
+ sonaTracksGlobal: null;
278
+ sonaTracksGlobalDelta: null;
275
279
  notes: string[];
276
280
  };
277
281
  generatedAt: string;
@@ -701,12 +701,15 @@ export async function getUnifiedLearningStats() {
701
701
  let sonaStats = { trajectoriesTotal: 0, patternsLearned: 0, reasoningBankSize: 0, avgAdaptationTimeMs: 0 };
702
702
  if (sonaCoord) {
703
703
  try {
704
- const s = sonaCoord.stats?.() ?? {};
704
+ const s = sonaCoord.stats();
705
705
  sonaStats = {
706
- trajectoriesTotal: Number(s.trajectoriesTotal ?? s.trajectoriesProcessed ?? 0),
707
- patternsLearned: Number(s.totalPatterns ?? s.patternsLearned ?? 0),
708
- reasoningBankSize: bank?.stats?.()?.patternCount ?? 0,
709
- avgAdaptationTimeMs: sonaCoord.getAvgAdaptationTime?.() ?? 0,
706
+ // LocalSonaCoordinator.stats() reports trajectoryCount, a bounded
707
+ // recent buffer. Reading nonexistent trajectoriesTotal / processed
708
+ // fields made this metric permanently 0, even after learning.
709
+ trajectoriesTotal: s.trajectoryCount,
710
+ patternsLearned: 0,
711
+ reasoningBankSize: bank?.stats().patternCount ?? 0,
712
+ avgAdaptationTimeMs: s.avgAdaptationMs,
710
713
  };
711
714
  }
712
715
  catch { /* SONA not yet initialised */ }
@@ -729,11 +732,12 @@ export async function getUnifiedLearningStats() {
729
732
  neuralStats = nt.getNeuralStoreStats();
730
733
  }
731
734
  catch { /* neural module not loadable */ }
732
- // Consistency notes — describe (don't enforce) the cross-store relationships
733
- const sonaTracksGlobalDelta = sonaStats.trajectoriesTotal - intel.trajectoriesRecorded;
735
+ // SONA's coordinator is recreated on each process start, while globalStats
736
+ // is restored from disk. Their counts can differ by thousands after a
737
+ // restart even when both learning paths work correctly (#3198).
734
738
  const notes = [];
735
- if (sonaAvailable && Math.abs(sonaTracksGlobalDelta) > 2) {
736
- notes.push(`sona.trajectoriesTotal (${sonaStats.trajectoriesTotal}) drifts from globalStats.trajectoriesRecorded (${intel.trajectoriesRecorded}) by ${sonaTracksGlobalDelta} — expected to track within ±1`);
739
+ if (sonaAvailable && sonaStats.trajectoriesTotal !== intel.trajectoriesRecorded) {
740
+ notes.push('sona.trajectoriesTotal is a process-local, bounded recent trajectory buffer; global.trajectoriesRecorded is project-persisted. These counters have different lifetimes and are not comparable.');
737
741
  }
738
742
  if (intel.patternsLearned > 0 && neuralStats.patternCount === 0) {
739
743
  notes.push(`globalStats reports ${intel.patternsLearned} patterns learned but neural_patterns store is empty — pretrain has not written here, or trajectory-end isn't promoting patterns to the neural store yet`);
@@ -748,17 +752,20 @@ export async function getUnifiedLearningStats() {
748
752
  signalsProcessed: intel.signalsProcessed,
749
753
  lastAdaptation: intel.lastAdaptation,
750
754
  source: '.claude-flow/neural/stats.json (globalStats)',
755
+ scope: 'project-persisted',
751
756
  },
752
757
  sona: {
753
758
  ...sonaStats,
754
759
  source: 'sonaCoordinator (in-memory, resets per process)',
755
760
  available: sonaAvailable,
761
+ scope: 'process-local',
762
+ metric: 'recent-buffered-trajectories',
756
763
  },
757
764
  memoryBridge: bridgeStats,
758
765
  neuralPatterns: neuralStats,
759
766
  consistency: {
760
- sonaTracksGlobal: sonaAvailable ? Math.abs(sonaTracksGlobalDelta) <= 1 : true,
761
- sonaTracksGlobalDelta,
767
+ sonaTracksGlobal: null,
768
+ sonaTracksGlobalDelta: null,
762
769
  notes,
763
770
  },
764
771
  generatedAt: new Date().toISOString(),
@@ -565,6 +565,89 @@ async function cacheGet(registry, cacheKey) {
565
565
  return null;
566
566
  }
567
567
  }
568
+ /**
569
+ * Cross-process cache coherence.
570
+ *
571
+ * The TieredCache lives in this process, but the database file is shared:
572
+ * Claude Code, Codex and Grok each run their own `mcp start` against the
573
+ * same `.swarm/` store, and each process only invalidates entries on its
574
+ * OWN writes. A key another process updated or deleted kept coming back
575
+ * from this process's cache — old value, `found: true` — until the cached
576
+ * entry expired (TieredCache TTL, 5 minutes by default; reads don't extend
577
+ * it).
578
+ *
579
+ * SQLite's `PRAGMA data_version` changes whenever a DIFFERENT connection has
580
+ * committed to the database file since this connection last asked, and never
581
+ * for this connection's own writes (https://sqlite.org/pragma.html#pragma_data_version).
582
+ * Checking it before a cached read is an exact "has anyone else written?"
583
+ * test that costs one pragma call. When it moves, drop the whole cache:
584
+ * every cached shape was derived from the old database state. The first
585
+ * check on a handle records its baseline and clears: a no-op for a fresh
586
+ * handle, and the right thing if a handle is ever replaced under a live
587
+ * cache.
588
+ *
589
+ * Native better-sqlite3 handles only. agentdb's sql.js fallback works on a
590
+ * private in-memory image that never sees another process's commits, so
591
+ * there is nothing to detect; it is skipped rather than probed. A handle
592
+ * that cannot prepare the check is remembered and never probed again; a
593
+ * check that fails on a given read (e.g. SQLITE_BUSY) clears the cache for
594
+ * safety and is simply retried on the next read.
595
+ */
596
+ const _cacheDataVersionStmts = new WeakMap();
597
+ const _cacheDataVersions = new WeakMap();
598
+ const _cacheDataVersionUnsupported = new WeakSet();
599
+ function dropCacheIfDbChangedElsewhere(registry, ctx) {
600
+ // Never let this check break the read it guards: any throw falls back to
601
+ // the pre-fix behaviour (a possibly stale hit).
602
+ try {
603
+ const db = ctx.db;
604
+ if (!db || ctx.agentdb?.isWasm === true || !('inTransaction' in db))
605
+ return;
606
+ if (_cacheDataVersionUnsupported.has(db))
607
+ return;
608
+ let stmt = _cacheDataVersionStmts.get(db);
609
+ if (!stmt) {
610
+ try {
611
+ stmt = db.prepare('PRAGMA data_version');
612
+ _cacheDataVersionStmts.set(db, stmt);
613
+ }
614
+ catch {
615
+ _cacheDataVersionUnsupported.add(db); // structural: this handle can't answer
616
+ return;
617
+ }
618
+ }
619
+ let cache = null;
620
+ try {
621
+ cache = registry.get('tieredCache');
622
+ }
623
+ catch { /* no cache, nothing to drop */ }
624
+ try {
625
+ const version = stmt.get()?.data_version;
626
+ if (typeof version !== 'number') {
627
+ // #3360: an unusable answer is not evidence the store is unchanged.
628
+ // Same posture as the catch below — a check we cannot validate drops
629
+ // the cache rather than trusting it, and leaves the baseline alone so
630
+ // the next read re-checks.
631
+ if (cache && typeof cache.clear === 'function')
632
+ cache.clear();
633
+ return;
634
+ }
635
+ if (_cacheDataVersions.get(db) === version)
636
+ return;
637
+ if (cache && typeof cache.clear === 'function')
638
+ cache.clear();
639
+ _cacheDataVersions.set(db, version);
640
+ }
641
+ catch {
642
+ // Transient (e.g. SQLITE_BUSY): don't trust the cache for this read.
643
+ if (cache && typeof cache.clear === 'function')
644
+ cache.clear();
645
+ }
646
+ }
647
+ catch {
648
+ // Non-fatal.
649
+ }
650
+ }
568
651
  /**
569
652
  * Write to TieredCache after DB write.
570
653
  */
@@ -1323,6 +1406,7 @@ export async function bridgeGetEntry(options) {
1323
1406
  const safeNs = String(namespace).replace(/:/g, '_');
1324
1407
  const safeKey = String(key).replace(/:/g, '_');
1325
1408
  const cacheKey = `entry:${safeNs}:${safeKey}`;
1409
+ dropCacheIfDbChangedElsewhere(registry, ctx);
1326
1410
  const cached = await cacheGet(registry, cacheKey);
1327
1411
  if (cached && cached.content) {
1328
1412
  return {
@@ -296,6 +296,24 @@ function envMaxUncertainty() {
296
296
  return undefined;
297
297
  return n;
298
298
  }
299
+ // Dream Cycle 2026-09-17 — env override for priorDecay, mirroring
300
+ // envMaxUncertainty() above. The discounted-Thompson-sampling primitive
301
+ // (see priorDecay's doc comment) was built, tested, and benchmarked in
302
+ // #3049 but shipped with no way to actually turn it on — DEFAULT_CONFIG
303
+ // hardcoded 1 (disabled) with no env/config knob, unlike every sibling
304
+ // tunable in this file. Parsed once at module load; invalid/out-of-range
305
+ // values fall through to the constructor's own priorDecay<=0||>1 guard,
306
+ // which already normalizes to 1 (disabled) — so an unset or malformed env
307
+ // var is fully backward compatible with existing deployments.
308
+ function envPriorDecay() {
309
+ const raw = process.env.CLAUDE_FLOW_PRIOR_DECAY;
310
+ if (!raw)
311
+ return undefined;
312
+ const n = Number(raw);
313
+ if (!Number.isFinite(n) || n <= 0 || n > 1)
314
+ return undefined;
315
+ return n;
316
+ }
299
317
  const DEFAULT_CONFIG = {
300
318
  confidenceThreshold: 0.85,
301
319
  maxUncertainty: envMaxUncertainty() ?? 0.15,
@@ -305,7 +323,7 @@ const DEFAULT_CONFIG = {
305
323
  autoSaveInterval: 1, // Save after every decision for CLI persistence
306
324
  enableCostOptimization: true,
307
325
  preferSpeed: true,
308
- priorDecay: 1,
326
+ priorDecay: envPriorDecay() ?? 1,
309
327
  };
310
328
  // Beta shape parameters must stay well clear of 0 for sampleGamma/sampleBeta
311
329
  // to remain numerically stable across a persisted, long-running state file —
@@ -43,7 +43,8 @@ import { FableHarness, } from './fable-harness.js';
43
43
  // behavior mid-run (the #142 pin-drift failure mode). Pin it, and let
44
44
  // scripts/check-metaharness-pins.mjs watch this constant for drift. Kept in
45
45
  // lock-step with the optionalDependencies pin in package.json and the plugin
46
- // darwin cache (versioned by the plugin's own `~0.8.0` pin in _darwin.mjs).
46
+ // darwin cache (versioned by the plugin's own `~0.10.2` pin in _darwin.mjs;
47
+ // both are checked by scripts/check-metaharness-pins.mjs — #3366).
47
48
  export const MH_DARWIN_PIN = '0.10.2';
48
49
  // ── Public API ───────────────────────────────────────────────────────────
49
50
  /**
@@ -21,20 +21,17 @@
21
21
  * of the resolved directory path (prefixed `dir:`-style via isGit=false), so
22
22
  * callers never need a special case.
23
23
  */
24
- import { execFileSync } from 'child_process';
25
24
  import { createHash } from 'crypto';
26
25
  import { resolve } from 'path';
27
26
  import * as fs from 'fs';
27
+ import { safeGitTextSync } from '@claude-flow/security/safe-git';
28
28
  const GIT_TIMEOUT_MS = 3000;
29
29
  function git(cwd, ...args) {
30
30
  try {
31
- return execFileSync('git', args, {
32
- cwd,
33
- encoding: 'utf-8',
34
- timeout: GIT_TIMEOUT_MS,
35
- stdio: ['ignore', 'pipe', 'ignore'],
36
- windowsHide: true,
37
- }).trim();
31
+ return safeGitTextSync(cwd, args, {
32
+ timeoutMs: GIT_TIMEOUT_MS,
33
+ stderr: 'ignore',
34
+ });
38
35
  }
39
36
  catch {
40
37
  return null;
@@ -1,14 +1,39 @@
1
1
  import { AgenticPolicyEngine, createLegacyCompatibleState, isMcpCallerAuthEnabled, decodeTokenEnvelope, publicKeyFromHex, verifyInvocationToken, } from '@claude-flow/security';
2
2
  import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
3
3
  import { execFileSync } from 'node:child_process';
4
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
4
+ import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readlinkSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
5
5
  import { dirname, join, resolve } from 'node:path';
6
- import { userInfo } from 'node:os';
6
+ import { hostname, userInfo } from 'node:os';
7
7
  const POLICY_DIR = join('.claude-flow', 'policy');
8
8
  const POLICY_FILE = 'state.json';
9
9
  const LOCK_FILE = 'state.lock';
10
10
  const LOCK_STALE_MS = 30_000;
11
11
  const LOCK_WAIT_MS = 5_000;
12
+ // A lock is ~170 bytes plus the hostname (at most 255 bytes).
13
+ const LOCK_MAX_BYTES = 1_024;
14
+ // Linux PID namespace of this process, e.g. `pid:[4026531836]`; undefined on
15
+ // other platforms or without /proc. Recorded in the lock because a pid only
16
+ // identifies a process inside the namespace that wrote it.
17
+ const PID_NAMESPACE = (() => {
18
+ try {
19
+ return readlinkSync('/proc/self/ns/pid');
20
+ }
21
+ catch {
22
+ return undefined;
23
+ }
24
+ })();
25
+ // Linux boot id: one uuid per kernel boot, shared by every process on that
26
+ // kernel and readable inside containers. Recorded because `host` is only a
27
+ // name: two machines that share a project directory can report the same
28
+ // hostname, and the init PID namespace has the same inode on every Linux host.
29
+ const BOOT_ID = (() => {
30
+ try {
31
+ return readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim();
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ })();
12
37
  function paths(projectRoot) {
13
38
  const root = resolve(projectRoot);
14
39
  const dir = join(root, POLICY_DIR);
@@ -17,12 +42,96 @@ function paths(projectRoot) {
17
42
  function sleep(ms) {
18
43
  return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
19
44
  }
45
+ /**
46
+ * Signal-0 existence probe, as in #1799 (`isPidAlive` in swarm-tools.ts), but
47
+ * only ESRCH counts as dead (as `processIsAlive` in helper-refresh.ts): here a
48
+ * false "dead" breaks mutual exclusion, so EPERM (another user's process) and
49
+ * any unexpected error mean alive.
50
+ */
51
+ function isPidAlive(pid) {
52
+ try {
53
+ process.kill(pid, 0);
54
+ return true;
55
+ }
56
+ catch (e) {
57
+ return e.code !== 'ESRCH';
58
+ }
59
+ }
60
+ /**
61
+ * Reads a lock only if it is a small regular file: never follows a symlink,
62
+ * never blocks on a FIFO or device, never reads more than LOCK_MAX_BYTES.
63
+ * Anything else returns undefined and the caller keeps the mtime rule.
64
+ */
65
+ function readLockFile(lockPath) {
66
+ if (!lstatSync(lockPath).isFile())
67
+ return undefined;
68
+ const fd = openSync(lockPath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0) | (fsConstants.O_NONBLOCK ?? 0));
69
+ try {
70
+ const stat = fstatSync(fd);
71
+ if (!stat.isFile() || stat.size > LOCK_MAX_BYTES)
72
+ return undefined;
73
+ const buffer = Buffer.alloc(LOCK_MAX_BYTES);
74
+ return buffer.toString('utf8', 0, readSync(fd, buffer, 0, LOCK_MAX_BYTES, 0));
75
+ }
76
+ finally {
77
+ closeSync(fd);
78
+ }
79
+ }
80
+ /**
81
+ * A process killed inside a transaction (SIGKILL, OOM, or a signal whose
82
+ * default action terminates it) never runs `finally { release() }`, so every
83
+ * caller, MCP reads included, waited LOCK_WAIT_MS and failed with
84
+ * policy-state-lock-timeout until the orphan's mtime crossed LOCK_STALE_MS.
85
+ * The pid is trusted only if the lock was written by this OS, on this kernel
86
+ * boot and in this PID namespace: anywhere else (a bwrap --unshare-pid
87
+ * sandbox, a container, another machine on a shared filesystem) a live owner
88
+ * also answers ESRCH. Locks without that identity (older ruflo, a crash
89
+ * between create and write, anything unparsable) return false and keep the
90
+ * mtime rule.
91
+ */
92
+ function lockOwnerIsDead(lockPath) {
93
+ try {
94
+ const read = readLockFile(lockPath);
95
+ if (read === undefined)
96
+ return false;
97
+ const owner = JSON.parse(read);
98
+ if (!owner || owner.host !== hostname() || owner.platform !== process.platform
99
+ || owner.pidns !== PID_NAMESPACE || owner.boot !== BOOT_ID)
100
+ return false;
101
+ // Without /proc, one Linux kernel boot or PID namespace cannot be told
102
+ // from another, and `host` is only a name.
103
+ if (process.platform === 'linux'
104
+ && (PID_NAMESPACE === undefined || BOOT_ID === undefined))
105
+ return false;
106
+ const pid = owner.pid;
107
+ if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0 || pid > 0x7fffffff
108
+ || isPidAlive(pid))
109
+ return false;
110
+ // The lock can change hands while it is read and probed (its owner
111
+ // releases and exits, another process acquires). Re-read it and answer
112
+ // only if it is byte for byte the lock that was read: the next owner's
113
+ // lock always differs in `pid` or `acquiredAt`. `dev`/`ino`/`ctime` cannot
114
+ // stand in for that — ext4 reuses a freed inode number at once, and before
115
+ // Linux 6.13 two files created in one clock tick share a ctime.
116
+ return readLockFile(lockPath) === read;
117
+ }
118
+ catch {
119
+ return false;
120
+ }
121
+ }
20
122
  async function acquireLock(lockPath) {
21
123
  const started = Date.now();
22
124
  while (Date.now() - started < LOCK_WAIT_MS) {
23
125
  try {
24
126
  const fd = openSync(lockPath, 'wx', 0o600);
25
- writeFileSync(fd, JSON.stringify({ pid: process.pid, acquiredAt: Date.now() }));
127
+ writeFileSync(fd, JSON.stringify({
128
+ pid: process.pid,
129
+ acquiredAt: Date.now(),
130
+ host: hostname(),
131
+ platform: process.platform,
132
+ pidns: PID_NAMESPACE,
133
+ boot: BOOT_ID,
134
+ }));
26
135
  closeSync(fd);
27
136
  return () => {
28
137
  try {
@@ -33,7 +142,8 @@ async function acquireLock(lockPath) {
33
142
  }
34
143
  catch {
35
144
  try {
36
- if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS)
145
+ if (lockOwnerIsDead(lockPath)
146
+ || Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS)
37
147
  unlinkSync(lockPath);
38
148
  }
39
149
  catch { /* another process changed the lock */ }
@@ -14,6 +14,7 @@ import { EventEmitter } from 'events';
14
14
  import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync, unlinkSync } from 'fs';
15
15
  import { cpus } from 'os';
16
16
  import { join } from 'path';
17
+ import { createRequire } from 'node:module';
17
18
  import { writeFileAtomic } from '../fs-secure.js';
18
19
  import { HeadlessWorkerExecutor, isHeadlessWorker, } from './headless-worker-executor.js';
19
20
  // ADR-174 M3: the consolidate worker below drives the real DISTILL/CONSOLIDATE
@@ -26,6 +27,9 @@ import { backupMemoryDb } from './memory-backup.js';
26
27
  import { resolveGitWorkspaceIdentity } from './git-workspace-identity.js';
27
28
  import { getWorkspaceLeaseRegistry } from './workspace-lease.js';
28
29
  import { getRepoSupervisorRegistry } from './repo-supervisor.js';
30
+ // Daemon config is read synchronously from the constructor. In an ESM build,
31
+ // bare `require` is undefined, so use a module-relative require for YAML.
32
+ const requireYaml = createRequire(import.meta.url);
29
33
  // Default worker configurations with improved intervals (P0 fix: map 5min -> 15min)
30
34
  const DEFAULT_WORKERS = [
31
35
  { type: 'map', intervalMs: 15 * 60 * 1000, offsetMs: 0, priority: 'normal', description: 'Codebase mapping', enabled: true },
@@ -368,8 +372,7 @@ export class WorkerDaemon extends EventEmitter {
368
372
  try {
369
373
  // Lazy-load yaml so the daemon doesn't hard-require it; if the
370
374
  // dep isn't installed, fall back to the previous warn-only path.
371
- // eslint-disable-next-line @typescript-eslint/no-var-requires
372
- const yamlMod = require('yaml');
375
+ const yamlMod = requireYaml('yaml');
373
376
  const parsed = yamlMod.parse(readFileSync(yPath, 'utf-8'));
374
377
  if (parsed && typeof parsed === 'object') {
375
378
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1013,7 +1016,9 @@ export class WorkerDaemon extends EventEmitter {
1013
1016
  return `max age ${Math.round(ttlMs / 1000)}s reached`;
1014
1017
  }
1015
1018
  if (idleMs > 0) {
1016
- const lastActivity = this.lastWorkerActivityMs() ?? startedMs;
1019
+ // Worker timestamps survive restarts; they cannot make this process
1020
+ // idle before it has been alive for a full idle window (#3194).
1021
+ const lastActivity = Math.max(this.lastWorkerActivityMs() ?? startedMs, startedMs);
1017
1022
  if (now - lastActivity >= idleMs) {
1018
1023
  return `idle for ${Math.round(idleMs / 1000)}s (no worker activity)`;
1019
1024
  }
@@ -1 +1 @@
1
- {"version":3,"file":"coordinator.d.ts","sourceRoot":"","sources":["../../src/worktrees/coordinator.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,CAAC,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,kBAAkB,EAAE,CAAC;CACnC;AAcD,qBAAa,wBAAwB;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;gBAElB,QAAQ,EAAE,MAAM;IAQ5B,OAAO,CACL,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,EACjD,OAAO,GAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO,GACvD,iBAAiB;IA+CpB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB;IAkBxC,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG;QAAE,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE;IAYnE,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE;IAgBjE,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,WAAW;CAKpB"}
1
+ {"version":3,"file":"coordinator.d.ts","sourceRoot":"","sources":["../../src/worktrees/coordinator.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,CAAC,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,kBAAkB,EAAE,CAAC;CACnC;AAUD,qBAAa,wBAAwB;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;gBAElB,QAAQ,EAAE,MAAM;IAQ5B,OAAO,CACL,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,EACjD,OAAO,GAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO,GACvD,iBAAiB;IA+CpB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB;IAkBxC,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG;QAAE,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE;IAYnE,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE;IAgBjE,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,WAAW;CAKpB"}