claude-flow 3.21.0 → 3.22.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 (34) hide show
  1. package/package.json +1 -1
  2. package/v3/@claude-flow/cli/dist/src/commands/daemon.js +23 -2
  3. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +56 -0
  4. package/v3/@claude-flow/cli/dist/src/commands/memory-distill.d.ts +27 -0
  5. package/v3/@claude-flow/cli/dist/src/commands/memory-distill.js +374 -0
  6. package/v3/@claude-flow/cli/dist/src/commands/memory.js +4 -2
  7. package/v3/@claude-flow/cli/dist/src/index.d.ts +1 -1
  8. package/v3/@claude-flow/cli/dist/src/index.js +22 -1
  9. package/v3/@claude-flow/cli/dist/src/init/executor.js +37 -0
  10. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +19 -0
  11. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +175 -0
  12. package/v3/@claude-flow/cli/dist/src/init/helper-signing.d.ts +30 -0
  13. package/v3/@claude-flow/cli/dist/src/init/helper-signing.js +60 -0
  14. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.d.ts +16 -0
  15. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +89 -8
  16. package/v3/@claude-flow/cli/dist/src/init/memory-package-resolver.d.ts +53 -0
  17. package/v3/@claude-flow/cli/dist/src/init/memory-package-resolver.js +118 -0
  18. package/v3/@claude-flow/cli/dist/src/init/statusline-generator.js +18 -7
  19. package/v3/@claude-flow/cli/dist/src/mcp-client.js +13 -0
  20. package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-speculate-tools.js +9 -2
  21. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.d.ts +162 -0
  22. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.js +548 -0
  23. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.d.ts +9 -1
  24. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.js +4 -1
  25. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +115 -54
  26. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +71 -0
  27. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +330 -2
  28. package/v3/@claude-flow/cli/dist/src/services/distill-tuning.d.ts +111 -0
  29. package/v3/@claude-flow/cli/dist/src/services/distill-tuning.js +510 -0
  30. package/v3/@claude-flow/cli/dist/src/services/memory-distillation.d.ts +41 -0
  31. package/v3/@claude-flow/cli/dist/src/services/memory-distillation.js +277 -0
  32. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +22 -1
  33. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +117 -6
  34. package/v3/@claude-flow/cli/package.json +5 -2
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Memory Distillation Service (ADR-174)
3
+ *
4
+ * Turns raw `memory_entries` (what ruflo has been RECORDING for thousands of
5
+ * commits) into the structured intelligence substrate it has never populated:
6
+ * `episodes` → `reasoning_patterns` (+ `pattern_embeddings`) → `causal_edges`.
7
+ * This is the DISTILL/CONSOLIDATE half of the RETRIEVE→JUDGE→DISTILL→CONSOLIDATE
8
+ * pipeline; RETRIEVE (embeddings) was already populated, the rest was empty
9
+ * because the daemon's `consolidate` worker was a stub (see ADR-174 root cause).
10
+ *
11
+ * Design invariants (load-bearing):
12
+ * - $0 by default: reuses the deterministic structural extractor
13
+ * (structured-distill.ts) and the embeddings ALREADY on each row — no LLM
14
+ * call, no new embedding work, unless a caller explicitly opts into a judge.
15
+ * - Incremental: a `distill_state` cursor (per namespace, by monotonic rowid)
16
+ * means a run never rescans processed rows.
17
+ * - Non-destructive: NEVER mutates or deletes `memory_entries`; only inserts
18
+ * into the (previously empty) target tables. Writes are per-batch
19
+ * transactions — a failure rolls back the batch and advances no cursor.
20
+ * - Safe on a recovered DB: quick_check gate before any write; skips (not
21
+ * throws) on corruption; better-sqlite3 optional (silent no-op if absent).
22
+ * - Provenance discipline (ADR-171): `feedback` outcomes are execution-tier
23
+ * ground truth; everything else is proxy. Proxy-tier patterns are written
24
+ * but never `promoted` — visible for audit, excluded from promoted recall.
25
+ */
26
+ import * as fs from 'fs';
27
+ import * as path from 'path';
28
+ import { distillTrajectoryContent, serialiseDistilled } from '../memory/structured-distill.js';
29
+ function emptyReport(dryRun, extra = {}) {
30
+ return {
31
+ processed: 0, episodes: 0, patterns: 0, patternEmbeddings: 0, causalEdges: 0,
32
+ promoted: 0, byProvenance: {}, namespaces: [], dryRun, spendUsd: 0, ...extra,
33
+ };
34
+ }
35
+ function parseEmbedding(raw) {
36
+ if (typeof raw !== 'string' || !raw)
37
+ return null;
38
+ try {
39
+ const v = JSON.parse(raw);
40
+ return Array.isArray(v) ? v : null;
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ function cosine(a, b) {
47
+ const len = Math.min(a.length, b.length);
48
+ let dot = 0, na = 0, nb = 0;
49
+ for (let i = 0; i < len; i++) {
50
+ dot += a[i] * b[i];
51
+ na += a[i] * a[i];
52
+ nb += b[i] * b[i];
53
+ }
54
+ const mag = Math.sqrt(na * nb);
55
+ return mag === 0 ? 0 : dot / mag;
56
+ }
57
+ /**
58
+ * Best-effort success/reward extraction for a `feedback` outcome record. These
59
+ * are recorded post-edit outcomes — execution-observed ground truth (ADR-171
60
+ * execution tier), not a proxy guess.
61
+ */
62
+ function judgeFeedback(content) {
63
+ try {
64
+ const o = JSON.parse(content);
65
+ if (typeof o.success === 'boolean')
66
+ return { success: o.success, reward: o.success ? 1 : 0 };
67
+ if (typeof o.outcome === 'string') {
68
+ const ok = /success|pass|ok|true/i.test(o.outcome);
69
+ return { success: ok, reward: ok ? 1 : 0 };
70
+ }
71
+ if (typeof o.error === 'string' && o.error)
72
+ return { success: false, reward: 0 };
73
+ }
74
+ catch { /* not JSON — treat as neutral */ }
75
+ return { success: true, reward: 0.5 };
76
+ }
77
+ /**
78
+ * Distill accumulated memory into the structured intelligence tables.
79
+ * Incremental, $0, transactional, provenance-tagged.
80
+ */
81
+ export async function runDistillation(options) {
82
+ const { dbPath, namespaces, batchSize = 200, maxEntries, dedupDistance = 0.2, dryRun = false, judge = 'structural', sinceRowid, verbose = false, } = options;
83
+ if (judge === 'fable') {
84
+ // ADR-172 cost gate: the LLM judge path is not enabled in this $0 service.
85
+ return emptyReport(dryRun, { skipped: 'judge:fable requires the cost-bounded advisor path (ADR-172), not enabled here' });
86
+ }
87
+ if (!dbPath || !fs.existsSync(dbPath))
88
+ return emptyReport(dryRun, { skipped: 'no-db' });
89
+ let Database;
90
+ try {
91
+ const mod = 'better-sqlite3';
92
+ Database = (await import(mod)).default;
93
+ }
94
+ catch {
95
+ return emptyReport(dryRun, { skipped: 'better-sqlite3 unavailable' });
96
+ }
97
+ const report = emptyReport(dryRun);
98
+ let db;
99
+ try {
100
+ db = new Database(dbPath, { timeout: 3000 });
101
+ const tableExists = (name) => (db.prepare("SELECT COUNT(*) AS c FROM sqlite_master WHERE type='table' AND name=?").get(name)?.c ?? 0) > 0;
102
+ if (!tableExists('memory_entries')) {
103
+ db.close();
104
+ return { ...report, skipped: 'no memory_entries' };
105
+ }
106
+ for (const t of ['episodes', 'reasoning_patterns', 'pattern_embeddings', 'causal_edges']) {
107
+ if (!tableExists(t)) {
108
+ db.close();
109
+ return { ...report, skipped: `target table ${t} missing (agentdb schema not initialised)` };
110
+ }
111
+ }
112
+ // Safety gate: never distill into a structurally-corrupt DB.
113
+ const qc = db.prepare('PRAGMA quick_check(1)').get();
114
+ if (qc && String(Object.values(qc)[0] ?? '').toLowerCase() !== 'ok') {
115
+ db.close();
116
+ return { ...report, corrupt: true, skipped: 'memory DB reports corruption — run recoverMemoryDatabase first' };
117
+ }
118
+ // M0: incremental cursor table (per namespace, by monotonic rowid).
119
+ db.exec(`CREATE TABLE IF NOT EXISTS distill_state (
120
+ namespace TEXT PRIMARY KEY,
121
+ last_rowid INTEGER NOT NULL DEFAULT 0,
122
+ last_run_at INTEGER
123
+ )`);
124
+ // Which namespaces to process.
125
+ const nsAll = db.prepare("SELECT DISTINCT COALESCE(namespace,'default') AS ns FROM memory_entries WHERE embedding IS NOT NULL").all().map(r => r.ns);
126
+ const nsList = namespaces && namespaces.length ? nsAll.filter(n => namespaces.includes(n)) : nsAll;
127
+ report.namespaces = nsList;
128
+ const getCursor = db.prepare('SELECT last_rowid FROM distill_state WHERE namespace = ?');
129
+ const setCursor = db.prepare('INSERT INTO distill_state (namespace, last_rowid, last_run_at) VALUES (?, ?, ?) ' +
130
+ 'ON CONFLICT(namespace) DO UPDATE SET last_rowid = excluded.last_rowid, last_run_at = excluded.last_run_at');
131
+ const insEpisode = db.prepare('INSERT INTO episodes (session_id, task, input, output, reward, success, tags, metadata) VALUES (?,?,?,?,?,?,?,?)');
132
+ const insPattern = db.prepare('INSERT INTO reasoning_patterns (task_type, approach, success_rate, uses, avg_reward, tags, metadata) VALUES (?,?,?,?,?,?,?)');
133
+ const insPatEmb = db.prepare('INSERT OR REPLACE INTO pattern_embeddings (pattern_id, embedding) VALUES (?, ?)');
134
+ const insEdge = db.prepare('INSERT INTO causal_edges (from_memory_id, from_memory_type, to_memory_id, to_memory_type, similarity, confidence, mechanism, metadata) VALUES (?,?,?,?,?,?,?,?)');
135
+ let remaining = typeof maxEntries === 'number' ? maxEntries : Infinity;
136
+ for (const ns of nsList) {
137
+ if (remaining <= 0)
138
+ break;
139
+ let cursor = typeof sinceRowid === 'number' ? sinceRowid : (getCursor.get(ns)?.last_rowid ?? 0);
140
+ // Process this namespace in bounded batches until drained or cap hit.
141
+ // eslint-disable-next-line no-constant-condition
142
+ while (true) {
143
+ if (remaining <= 0)
144
+ break;
145
+ const lim = Math.min(batchSize, remaining);
146
+ const rows = db.prepare("SELECT rowid AS rowid, id, COALESCE(namespace,'default') AS namespace, content, embedding " +
147
+ 'FROM memory_entries WHERE embedding IS NOT NULL AND COALESCE(namespace,' + "'default'" + ') = ? AND rowid > ? ' +
148
+ 'ORDER BY rowid LIMIT ?').all(ns, cursor, lim);
149
+ if (rows.length === 0)
150
+ break;
151
+ const src = rows.map(r => ({
152
+ rowid: r.rowid, id: r.id, namespace: r.namespace,
153
+ content: String(r.content ?? ''), embedding: parseEmbedding(r.embedding),
154
+ }));
155
+ // Greedy cosine clustering within the batch (bounded n → O(n²) is fine).
156
+ const clusters = [];
157
+ for (const row of src) {
158
+ let placed = false;
159
+ if (row.embedding) {
160
+ for (const cl of clusters) {
161
+ if (cl.rep.embedding && cosine(row.embedding, cl.rep.embedding) >= 1 - dedupDistance) {
162
+ cl.members.push(row);
163
+ if (ns === 'feedback' && judgeFeedback(row.content).success)
164
+ cl.successCount++;
165
+ placed = true;
166
+ break;
167
+ }
168
+ }
169
+ }
170
+ if (!placed) {
171
+ const success = ns === 'feedback' ? judgeFeedback(row.content).success : false;
172
+ clusters.push({
173
+ rep: row, members: [row], successCount: success ? 1 : 0,
174
+ provenance: ns === 'feedback' ? 'oracle:test-exec' : 'proxy:structural',
175
+ });
176
+ }
177
+ }
178
+ const maxRowid = Math.max(...src.map(r => r.rowid), cursor);
179
+ // Embedding-coverage invariant: a cluster is only written if it has an
180
+ // embeddable representative — reselect to any member with a valid
181
+ // (parseable) vector; skip the cluster if none (unretrievable anyway).
182
+ // Guarantees every reasoning_pattern has exactly one pattern_embedding.
183
+ const embVecOf = (cl) => cl.rep.embedding ?? cl.members.find(m => m.embedding)?.embedding ?? null;
184
+ if (!dryRun) {
185
+ const commit = db.transaction(() => {
186
+ let prevEpId = null;
187
+ for (const cl of clusters) {
188
+ const embVec = embVecOf(cl);
189
+ if (!embVec)
190
+ continue; // no embeddable member → skip (keeps 1:1)
191
+ const distilled = distillTrajectoryContent(cl.rep.content);
192
+ const taskType = distilled.labels[0] ?? ns;
193
+ const approach = distilled.summary || serialiseDistilled(distilled).slice(0, 200);
194
+ const uses = cl.members.length;
195
+ const successRate = cl.provenance === 'oracle:test-exec' ? cl.successCount / uses : 0;
196
+ const avgReward = successRate; // structural: reward == success fraction
197
+ const promoted = cl.provenance === 'oracle:test-exec'; // proxy NEVER promotes (ADR-171)
198
+ const provMeta = {
199
+ provenance: cl.provenance, provenance_tier: cl.provenance, promoted,
200
+ sourceIds: cl.members.map(m => m.id).slice(0, 25),
201
+ paths: distilled.paths.slice(0, 10), namespace: ns, distilledBy: 'structural',
202
+ };
203
+ const epInfo = insEpisode.run(`distill:${ns}`, approach, cl.rep.content.slice(0, 2000), '', avgReward, promoted && cl.successCount > 0 ? 1 : 0, JSON.stringify(distilled.labels), JSON.stringify(provMeta));
204
+ report.episodes++;
205
+ const patInfo = insPattern.run(taskType, approach, successRate, uses, avgReward, JSON.stringify(distilled.labels), JSON.stringify(provMeta));
206
+ const patternId = Number(patInfo.lastInsertRowid);
207
+ report.patterns++;
208
+ report.byProvenance[cl.provenance] = (report.byProvenance[cl.provenance] ?? 0) + 1;
209
+ if (promoted)
210
+ report.promoted++;
211
+ // pattern_embeddings: reuse the representative's existing 384-dim
212
+ // vector as a Float32 BLOB — $0, no re-embedding. Guaranteed 1:1.
213
+ insPatEmb.run(patternId, Buffer.from(Float32Array.from(embVec).buffer));
214
+ report.patternEmbeddings++;
215
+ // WEAK relational edge — NOT causal proof. Links to the actual
216
+ // previous episode (not epId-1, which wrongly assumed consecutive
217
+ // rowids). Explicitly typed co-occurrence / proxy-tier / non-
218
+ // promoted: may rank retrieval, must NOT justify autonomous action
219
+ // (ADR-174 edge contract).
220
+ const epId = Number(epInfo.lastInsertRowid);
221
+ if (prevEpId !== null) {
222
+ insEdge.run(prevEpId, 'episode', epId, 'episode', 0, 0.3, 'co-occurrence', JSON.stringify({
223
+ edge_type: 'cooccurrence',
224
+ provenance_tier: 'proxy:structural',
225
+ confidence: 0.3,
226
+ promoted: false,
227
+ namespace: ns,
228
+ note: 'weak co-occurrence; may rank retrieval, must not justify autonomous action',
229
+ }));
230
+ report.causalEdges++;
231
+ }
232
+ prevEpId = epId;
233
+ }
234
+ setCursor.run(ns, maxRowid, Date.now());
235
+ });
236
+ commit();
237
+ }
238
+ else {
239
+ // dry-run accounting only — mirror the embeddable-cluster skip.
240
+ for (const cl of clusters) {
241
+ if (!embVecOf(cl))
242
+ continue;
243
+ report.patterns++;
244
+ report.episodes++;
245
+ report.byProvenance[cl.provenance] = (report.byProvenance[cl.provenance] ?? 0) + 1;
246
+ if (cl.provenance === 'oracle:test-exec')
247
+ report.promoted++;
248
+ }
249
+ }
250
+ report.processed += rows.length;
251
+ remaining -= rows.length;
252
+ cursor = maxRowid;
253
+ if (rows.length < lim)
254
+ break; // namespace drained
255
+ }
256
+ }
257
+ db.close();
258
+ if (verbose) {
259
+ console.log(`distilled ${report.processed} entries → ${report.patterns} patterns ` +
260
+ `(${report.promoted} promoted), ${report.episodes} episodes, ${report.causalEdges} edges` +
261
+ (dryRun ? ' [dry-run]' : ''));
262
+ }
263
+ }
264
+ catch (e) {
265
+ try {
266
+ db?.close();
267
+ }
268
+ catch { /* */ }
269
+ return { ...report, skipped: `error: ${e?.message ?? e}` };
270
+ }
271
+ return report;
272
+ }
273
+ /** Where the memory DB lives, for daemon/CLI callers. */
274
+ export function defaultMemoryDbPath(cwd = process.cwd()) {
275
+ return path.join(cwd, '.swarm', 'memory.db');
276
+ }
277
+ //# sourceMappingURL=memory-distillation.js.map
@@ -6,7 +6,8 @@
6
6
  * - map: Codebase mapping (5 min interval)
7
7
  * - audit: Security analysis (10 min interval)
8
8
  * - optimize: Performance optimization (15 min interval)
9
- * - consolidate: Memory consolidation (30 min interval)
9
+ * - consolidate: Memory distillation memory_entries -> episodes/reasoning_patterns/
10
+ * causal_edges (30 min interval, ADR-174; RUFLO_DAEMON_NO_DISTILL=1 / --no-distill to skip)
10
11
  * - testgaps: Test coverage analysis (20 min interval)
11
12
  */
12
13
  import { EventEmitter } from 'events';
@@ -297,6 +298,26 @@ export declare class WorkerDaemon extends EventEmitter {
297
298
  * Local optimize worker (fallback when headless unavailable)
298
299
  */
299
300
  private runOptimizeWorkerLocal;
301
+ /**
302
+ * ADR-174 M3: memory consolidation — runs the real DISTILL/CONSOLIDATE pass
303
+ * (memory-distillation.ts) against `.swarm/memory.db`, turning raw
304
+ * `memory_entries` into `episodes` / `reasoning_patterns` /
305
+ * `pattern_embeddings` / `causal_edges`. Previously this wrote a hardcoded
306
+ * `{ patternsConsolidated: 0 }` stub and touched no database — the root
307
+ * cause of the intelligence tables staying empty.
308
+ *
309
+ * Kept as `runConsolidateWorker` / worker type `'consolidate'` for
310
+ * back-compat with existing `-w consolidate` scripts and docs.
311
+ *
312
+ * Safety:
313
+ * - Bounded via CONSOLIDATE_MAX_ENTRIES_PER_TICK so a large backlog can
314
+ * never approach DEFAULT_WORKER_TIMEOUT_MS — the incremental cursor in
315
+ * runDistillation() drains a bounded slice per tick and picks up where
316
+ * it left off on the next scheduled run.
317
+ * - runDistillation() never throws (it catches internally and returns
318
+ * `{ skipped }` / `{ corrupt: true }`), but this worker still wraps the
319
+ * call defensively — a background worker must never crash the daemon.
320
+ */
300
321
  private runConsolidateWorker;
301
322
  /**
302
323
  * Local testgaps worker (fallback when headless unavailable)
@@ -6,7 +6,8 @@
6
6
  * - map: Codebase mapping (5 min interval)
7
7
  * - audit: Security analysis (10 min interval)
8
8
  * - optimize: Performance optimization (15 min interval)
9
- * - consolidate: Memory consolidation (30 min interval)
9
+ * - consolidate: Memory distillation memory_entries -> episodes/reasoning_patterns/
10
+ * causal_edges (30 min interval, ADR-174; RUFLO_DAEMON_NO_DISTILL=1 / --no-distill to skip)
10
11
  * - testgaps: Test coverage analysis (20 min interval)
11
12
  */
12
13
  import { EventEmitter } from 'events';
@@ -14,12 +15,18 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync, unl
14
15
  import { cpus } from 'os';
15
16
  import { join } from 'path';
16
17
  import { HeadlessWorkerExecutor, isHeadlessWorker, } from './headless-worker-executor.js';
18
+ // ADR-174 M3: the consolidate worker below drives the real DISTILL/CONSOLIDATE
19
+ // pass instead of writing a hardcoded { patternsConsolidated: 0 } stub. The
20
+ // service itself is owned/frozen elsewhere (memory-distillation.ts) — it is
21
+ // incremental (rowid cursor), non-destructive, transactional, and
22
+ // quick_check-gated, so it's safe to call unconditionally on every tick.
23
+ import { runDistillation, defaultMemoryDbPath } from './memory-distillation.js';
17
24
  // Default worker configurations with improved intervals (P0 fix: map 5min -> 15min)
18
25
  const DEFAULT_WORKERS = [
19
26
  { type: 'map', intervalMs: 15 * 60 * 1000, offsetMs: 0, priority: 'normal', description: 'Codebase mapping', enabled: true },
20
27
  { type: 'audit', intervalMs: 10 * 60 * 1000, offsetMs: 2 * 60 * 1000, priority: 'critical', description: 'Security analysis', enabled: true },
21
28
  { type: 'optimize', intervalMs: 15 * 60 * 1000, offsetMs: 4 * 60 * 1000, priority: 'high', description: 'Performance optimization', enabled: true },
22
- { type: 'consolidate', intervalMs: 30 * 60 * 1000, offsetMs: 6 * 60 * 1000, priority: 'low', description: 'Memory consolidation', enabled: true },
29
+ { type: 'consolidate', intervalMs: 30 * 60 * 1000, offsetMs: 6 * 60 * 1000, priority: 'low', description: 'Memory distillation (ADR-174)', enabled: true },
23
30
  { type: 'testgaps', intervalMs: 20 * 60 * 1000, offsetMs: 8 * 60 * 1000, priority: 'normal', description: 'Test coverage analysis', enabled: true },
24
31
  { type: 'predict', intervalMs: 10 * 60 * 1000, offsetMs: 0, priority: 'low', description: 'Predictive preloading', enabled: false },
25
32
  { type: 'document', intervalMs: 60 * 60 * 1000, offsetMs: 0, priority: 'low', description: 'Auto-documentation', enabled: false },
@@ -27,6 +34,27 @@ const DEFAULT_WORKERS = [
27
34
  // Worker timeout — must exceed the longest per-worker headless timeout (15 min for audit/refactor).
28
35
  // Previously 5 min, which caused orphan processes when daemon timeout fired before executor timeout (#1117).
29
36
  const DEFAULT_WORKER_TIMEOUT_MS = 16 * 60 * 1000;
37
+ // ADR-174 M3: hard cap on rows the `consolidate` worker distills in a single
38
+ // tick. The distillation service is cursor-driven (per-namespace rowid) and
39
+ // batches in transactions of `batchSize`, so a capped `maxEntries` guarantees
40
+ // this worker returns well within DEFAULT_WORKER_TIMEOUT_MS even against a
41
+ // large backlog — the cursor just picks up where it left off next tick
42
+ // (every 30 min per DEFAULT_WORKERS), draining an arbitrarily large backlog
43
+ // over several ticks instead of blocking on one.
44
+ const CONSOLIDATE_MAX_ENTRIES_PER_TICK = 1000;
45
+ const CONSOLIDATE_BATCH_SIZE = 200;
46
+ // ADR-174 M5: platform-default distillation config, chosen by the M4 self-
47
+ // optimization grid-search (scripts/tune-distill.mjs) on the real ~7.9k-entry
48
+ // corpus. Winner: batchSize=200, dedupDistance=0.2 (held-out MRR@10 0.753 vs
49
+ // 0.749 baseline — measured on-par, not a large uplift; the payoff is the
50
+ // populated substrate + trainable model). Override per-run via `memory distill`.
51
+ const CONSOLIDATE_DEDUP_DISTANCE = 0.2;
52
+ // ADR-174 M3 opt-out: set to skip the real distillation pass entirely (e.g.
53
+ // constrained CI hosts, or a user who wants the daemon's other workers but
54
+ // not this one without touching persisted worker-enabled state). Mirrors the
55
+ // `--no-distill` flag on `daemon start` (see commands/daemon.ts), which sets
56
+ // this env var on the forked/foreground daemon process.
57
+ const NO_DISTILL_ENV = 'RUFLO_DAEMON_NO_DISTILL';
30
58
  // #2356 — Self-terminating lifecycle defaults. A background daemon with no
31
59
  // upper bound on its lifetime runs until the box reboots; in the field this
32
60
  // leaked tens of thousands of headless `claude --print` sweeps over many days
@@ -1268,18 +1296,101 @@ export class WorkerDaemon extends EventEmitter {
1268
1296
  writeFileSync(optimizeFile, JSON.stringify(perf, null, 2));
1269
1297
  return perf;
1270
1298
  }
1299
+ /**
1300
+ * ADR-174 M3: memory consolidation — runs the real DISTILL/CONSOLIDATE pass
1301
+ * (memory-distillation.ts) against `.swarm/memory.db`, turning raw
1302
+ * `memory_entries` into `episodes` / `reasoning_patterns` /
1303
+ * `pattern_embeddings` / `causal_edges`. Previously this wrote a hardcoded
1304
+ * `{ patternsConsolidated: 0 }` stub and touched no database — the root
1305
+ * cause of the intelligence tables staying empty.
1306
+ *
1307
+ * Kept as `runConsolidateWorker` / worker type `'consolidate'` for
1308
+ * back-compat with existing `-w consolidate` scripts and docs.
1309
+ *
1310
+ * Safety:
1311
+ * - Bounded via CONSOLIDATE_MAX_ENTRIES_PER_TICK so a large backlog can
1312
+ * never approach DEFAULT_WORKER_TIMEOUT_MS — the incremental cursor in
1313
+ * runDistillation() drains a bounded slice per tick and picks up where
1314
+ * it left off on the next scheduled run.
1315
+ * - runDistillation() never throws (it catches internally and returns
1316
+ * `{ skipped }` / `{ corrupt: true }`), but this worker still wraps the
1317
+ * call defensively — a background worker must never crash the daemon.
1318
+ */
1271
1319
  async runConsolidateWorker() {
1272
- // Memory consolidation - clean up old patterns
1273
1320
  const consolidateFile = join(this.projectRoot, '.claude-flow', 'metrics', 'consolidation.json');
1274
1321
  const metricsDir = join(this.projectRoot, '.claude-flow', 'metrics');
1275
1322
  if (!existsSync(metricsDir)) {
1276
1323
  mkdirSync(metricsDir, { recursive: true });
1277
1324
  }
1325
+ // Opt-out: RUFLO_DAEMON_NO_DISTILL=1 (or `daemon start --no-distill`)
1326
+ // skips the real distillation pass entirely without touching persisted
1327
+ // worker-enabled state (see also: `daemon enable -w consolidate --disable`,
1328
+ // which disables the worker's schedule altogether).
1329
+ if (process.env[NO_DISTILL_ENV] === '1') {
1330
+ const disabledResult = {
1331
+ timestamp: new Date().toISOString(),
1332
+ distillationEnabled: false,
1333
+ note: `Distillation disabled via ${NO_DISTILL_ENV}=1 / --no-distill`,
1334
+ patternsConsolidated: 0,
1335
+ memoryCleaned: 0,
1336
+ duplicatesRemoved: 0,
1337
+ };
1338
+ writeFileSync(consolidateFile, JSON.stringify(disabledResult, null, 2));
1339
+ return disabledResult;
1340
+ }
1341
+ let report;
1342
+ try {
1343
+ report = await runDistillation({
1344
+ dbPath: defaultMemoryDbPath(this.projectRoot),
1345
+ maxEntries: CONSOLIDATE_MAX_ENTRIES_PER_TICK,
1346
+ batchSize: CONSOLIDATE_BATCH_SIZE,
1347
+ dedupDistance: CONSOLIDATE_DEDUP_DISTANCE,
1348
+ });
1349
+ }
1350
+ catch (error) {
1351
+ // Defensive only — runDistillation() is internally try/catch'd and
1352
+ // should never reach here. A worker must never crash the daemon.
1353
+ const message = error instanceof Error ? error.message : String(error);
1354
+ this.log('warn', `Consolidate worker: distillation threw unexpectedly: ${message}`);
1355
+ const errorResult = {
1356
+ timestamp: new Date().toISOString(),
1357
+ distillationEnabled: true,
1358
+ error: message,
1359
+ patternsConsolidated: 0,
1360
+ memoryCleaned: 0,
1361
+ duplicatesRemoved: 0,
1362
+ };
1363
+ writeFileSync(consolidateFile, JSON.stringify(errorResult, null, 2));
1364
+ return errorResult;
1365
+ }
1366
+ if (report.corrupt) {
1367
+ this.log('warn', `Consolidate worker: memory DB reports corruption — ${report.skipped ?? 'skipped'}`);
1368
+ }
1369
+ else if (report.skipped) {
1370
+ this.log('info', `Consolidate worker: distillation skipped (${report.skipped})`);
1371
+ }
1278
1372
  const result = {
1279
1373
  timestamp: new Date().toISOString(),
1280
- patternsConsolidated: 0,
1281
- memoryCleaned: 0,
1282
- duplicatesRemoved: 0,
1374
+ distillationEnabled: true,
1375
+ // Mapping onto the pre-existing metrics shape (ADR-174 M3):
1376
+ patternsConsolidated: report.patterns,
1377
+ // rows drained from the incremental cursor this tick — distillation is
1378
+ // non-destructive (never mutates/deletes memory_entries), so "cleaned"
1379
+ // here means "processed into the intelligence tables", not removed.
1380
+ memoryCleaned: report.processed,
1381
+ // clustering collapses near-duplicate entries into a single pattern
1382
+ // rather than deleting them; this is the count that got merged away
1383
+ // instead of becoming their own distinct pattern.
1384
+ duplicatesRemoved: Math.max(0, report.processed - report.patterns),
1385
+ episodes: report.episodes,
1386
+ patternEmbeddings: report.patternEmbeddings,
1387
+ causalEdges: report.causalEdges,
1388
+ promoted: report.promoted,
1389
+ byProvenance: report.byProvenance,
1390
+ namespaces: report.namespaces,
1391
+ dryRun: report.dryRun,
1392
+ corrupt: report.corrupt ?? false,
1393
+ skipped: report.skipped,
1283
1394
  };
1284
1395
  writeFileSync(consolidateFile, JSON.stringify(result, null, 2));
1285
1396
  return result;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.21.0",
3
+ "version": "3.22.0",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",
@@ -75,8 +75,10 @@
75
75
  },
76
76
  "files": [
77
77
  "dist",
78
+ "!dist/**/*.map",
79
+ "!dist/**/*.tsbuildinfo",
78
80
  "bin",
79
- "scripts",
81
+ "scripts/postinstall.cjs",
80
82
  ".claude",
81
83
  "plugins",
82
84
  "README.md"
@@ -132,6 +134,7 @@
132
134
  "agentic-flow": "^3.0.0-alpha.1",
133
135
  "agenticow": "~0.2.4",
134
136
  "metaharness": "~0.2.8",
137
+ "page-agent": "~1.11.0",
135
138
  "ruvector": "^0.2.27"
136
139
  },
137
140
  "publishConfig": {