@claude-flow/cli 3.71.1 → 3.71.3

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 (37) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +208 -54
  2. package/.claude/helpers/helpers.manifest.json +3 -3
  3. package/.claude/settings.json +2 -1
  4. package/README.md +2 -0
  5. package/catalog-manifest.json +2 -2
  6. package/dist/src/commands/daemon.js +20 -1
  7. package/dist/src/commands/embeddings.js +6 -6
  8. package/dist/src/commands/hive-mind-worker.d.ts +10 -0
  9. package/dist/src/commands/hive-mind-worker.js +20 -0
  10. package/dist/src/commands/hive-mind.js +27 -4
  11. package/dist/src/commands/init.js +4 -0
  12. package/dist/src/commands/session.js +10 -7
  13. package/dist/src/init/executor.js +15 -5
  14. package/dist/src/init/settings-generator.js +4 -0
  15. package/dist/src/init/types.d.ts +2 -0
  16. package/dist/src/init/types.js +4 -1
  17. package/dist/src/mcp-tools/agentdb-tools.js +3 -3
  18. package/dist/src/mcp-tools/claims-tools.js +23 -10
  19. package/dist/src/mcp-tools/daa-tools.js +22 -2
  20. package/dist/src/mcp-tools/embeddings-tools.js +2 -1
  21. package/dist/src/mcp-tools/hooks-tools.js +11 -1
  22. package/dist/src/mcp-tools/session-tools.js +42 -4
  23. package/dist/src/mcp-tools/task-tools.js +11 -0
  24. package/dist/src/mcp-tools/terminal-tools.js +5 -0
  25. package/dist/src/mcp-tools/workflow-tools.js +22 -3
  26. package/dist/src/memory/memory-bridge.js +29 -5
  27. package/dist/src/memory/memory-initializer.js +32 -10
  28. package/dist/src/missions/schemas.d.ts +10 -10
  29. package/dist/src/mods/headless-plugins.d.ts +16 -0
  30. package/dist/src/mods/headless-plugins.js +30 -1
  31. package/dist/src/services/claims-store.js +5 -2
  32. package/dist/src/services/daemon-autostart.d.ts +24 -0
  33. package/dist/src/services/daemon-autostart.js +55 -1
  34. package/dist/src/services/headless-worker-executor.d.ts +15 -0
  35. package/dist/src/services/headless-worker-executor.js +28 -0
  36. package/package.json +1 -1
  37. package/plugins/ruflo-console/dist/tui.mjs +134 -134
@@ -9,6 +9,10 @@
9
9
  * node auto-memory-hook.mjs import # SessionStart: import auto memory files into backend
10
10
  * node auto-memory-hook.mjs sync # SessionEnd: sync insights back to MEMORY.md
11
11
  * node auto-memory-hook.mjs status # Show bridge status
12
+ * node auto-memory-hook.mjs read <namespace> <key> # Read-only exact lookup, prints JSON
13
+ *
14
+ * import and sync exit 1 when they fail (never 2, which Claude Code treats as
15
+ * "block"), so a failed write is visible without stopping the session.
12
16
  */
13
17
 
14
18
  import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, rmdirSync, rmSync, statSync } from 'fs';
@@ -57,6 +61,9 @@ function warnMemoryUnavailable() {
57
61
 
58
62
  const DEBUG = !!(process.env.RUFLO_DEBUG || process.env.DEBUG);
59
63
 
64
+ /** A command failure whose message is already complete; main prints it and exits 1. */
65
+ class HookFailure extends Error {}
66
+
60
67
  // ── Graceful shutdown (FIX 3) ───────────────────────────────────────────────
61
68
  // Track the backend in use so a SIGTERM/SIGINT mid-run can still flush it
62
69
  // (the JSON backend persists; a SQLite-backed one closes/flushes WAL) instead
@@ -76,8 +83,9 @@ async function gracefulExit(signal) {
76
83
  process.on('SIGTERM', () => { gracefulExit('SIGTERM'); });
77
84
  process.on('SIGINT', () => { gracefulExit('SIGINT'); });
78
85
 
79
- // Ensure data dir
80
- if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
86
+ // The data directory is created lazily by import and sync only, so the
87
+ // read-only `read` subcommand never creates directories (#4685).
88
+ function ensureDataDir() { if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true }); }
81
89
 
82
90
  // ============================================================================
83
91
  // Store write safety (#4058): intelligence.cjs read-modify-writes the same
@@ -118,6 +126,83 @@ function writeFileAtomic(filePath, text) {
118
126
  }
119
127
  }
120
128
 
129
+ // ============================================================================
130
+ // Strict store reads (#4685): only ENOENT means "no store yet". A read or
131
+ // parse failure, a non-array document, or an entry without a unique string id
132
+ // is an error, and the file is never rewritten, so its bytes stay intact.
133
+ // ============================================================================
134
+
135
+ class StoreError extends Error {
136
+ constructor(code, message, cause) {
137
+ super(message);
138
+ this.name = 'StoreError';
139
+ this.code = code;
140
+ if (cause) this.cause = cause;
141
+ }
142
+ }
143
+
144
+ function parseStoreText(text, filePath) {
145
+ let data;
146
+ try { data = JSON.parse(text); } catch (e) {
147
+ throw new StoreError('STORE_PARSE_FAILED', `${filePath} is not valid JSON (${e.message}); left unchanged`, e);
148
+ }
149
+ if (!Array.isArray(data)) {
150
+ throw new StoreError('STORE_NOT_ARRAY', `${filePath} does not hold a JSON array; left unchanged`);
151
+ }
152
+ const seen = new Set();
153
+ for (let i = 0; i < data.length; i++) {
154
+ const e = data[i];
155
+ if (!e || typeof e !== 'object' || Array.isArray(e)) {
156
+ throw new StoreError('STORE_BAD_ENTRY', `${filePath} entry ${i} is not an object; left unchanged`);
157
+ }
158
+ if (typeof e.id !== 'string' || e.id.length === 0) {
159
+ throw new StoreError('STORE_BAD_ID', `${filePath} entry ${i} has no string id; left unchanged`);
160
+ }
161
+ if (seen.has(e.id)) {
162
+ // intelligence.cjs de-duplicates the store by id when it consolidates,
163
+ // which repairs a duplicate an older writer left behind.
164
+ throw new StoreError('STORE_DUPLICATE_ID', `${filePath} holds duplicate id ${JSON.stringify(e.id)}; left unchanged`);
165
+ }
166
+ seen.add(e.id);
167
+ }
168
+ return data;
169
+ }
170
+
171
+ /** The parsed entries, or null when the file does not exist. Anything else throws a StoreError. */
172
+ function readStoreFile(filePath) {
173
+ let text;
174
+ try { text = readFileSync(filePath, 'utf-8'); } catch (e) {
175
+ if (e && e.code === 'ENOENT') return null;
176
+ const why = (e && (e.code || e.message)) || String(e);
177
+ throw new StoreError('STORE_READ_FAILED', `cannot read ${filePath} (${why}); left unchanged`, e);
178
+ }
179
+ return parseStoreText(text, filePath);
180
+ }
181
+
182
+ /**
183
+ * Re-read the store and check that every expected entry is present and equal
184
+ * under `same`, and that every deleted id is gone. Throws on any mismatch.
185
+ */
186
+ function verifyStoreFile(filePath, expected, deletedIds, same) {
187
+ const onDisk = readStoreFile(filePath);
188
+ if (onDisk === null) throw new StoreError('STORE_READBACK_MISMATCH', `${filePath} is missing after the write`);
189
+ const byId = new Map(onDisk.map((e) => [e.id, e]));
190
+ for (const [id, want] of expected) {
191
+ const got = byId.get(id);
192
+ if (!got) throw new StoreError('STORE_READBACK_MISMATCH', `${filePath} readback is missing entry ${JSON.stringify(id)}`);
193
+ if (!same(got, want)) throw new StoreError('STORE_READBACK_MISMATCH', `${filePath} readback differs for entry ${JSON.stringify(id)}`);
194
+ }
195
+ for (const id of deletedIds) {
196
+ if (!expected.has(id) && byId.has(id)) {
197
+ throw new StoreError('STORE_READBACK_MISMATCH', `${filePath} readback still holds deleted entry ${JSON.stringify(id)}`);
198
+ }
199
+ }
200
+ return onDisk;
201
+ }
202
+
203
+ const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
204
+ const sameRecord = (a, b) => a.namespace === b.namespace && a.key === b.key && a.content === b.content;
205
+
121
206
  // Claude Code shares one auto-memory directory across every worktree of a
122
207
  // repository, but findGitRoot() in the memory package stops at a worktree's
123
208
  // `.git` FILE and keys the directory by the worktree path (#4058). Resolve the
@@ -149,21 +234,36 @@ class JsonFileBackend {
149
234
  this.entries = new Map();
150
235
  this.loadedIds = new Set();
151
236
  this.deletedIds = new Set();
237
+ // Entries this instance wrote (or deleted), so a caller can confirm them on disk.
238
+ this.written = new Map();
239
+ this.storePresent = false;
152
240
  }
153
241
 
154
242
  async initialize() {
155
- if (existsSync(this.filePath)) {
156
- try {
157
- const data = JSON.parse(readFileSync(this.filePath, 'utf-8'));
158
- if (Array.isArray(data)) {
159
- for (const entry of data) { this.entries.set(entry.id, entry); this.loadedIds.add(entry.id); }
160
- }
161
- } catch { /* start fresh */ }
243
+ // Throws on anything but a missing file, so a damaged store is never
244
+ // mistaken for an empty one and later overwritten (#4685).
245
+ const data = readStoreFile(this.filePath);
246
+ this.storePresent = data !== null;
247
+ for (const entry of data || []) { this.entries.set(entry.id, entry); this.loadedIds.add(entry.id); }
248
+ }
249
+
250
+ _track(entry) {
251
+ if (!entry || typeof entry.id !== 'string' || entry.id.length === 0) {
252
+ throw new StoreError('STORE_BAD_ID', 'refusing to store an entry without a string id');
162
253
  }
254
+ this.entries.set(entry.id, entry);
255
+ this.written.set(entry.id, entry);
256
+ this.deletedIds.delete(entry.id);
257
+ }
258
+
259
+ _forget(id) {
260
+ this.deletedIds.add(id);
261
+ this.written.delete(id);
262
+ return this.entries.delete(id);
163
263
  }
164
264
 
165
265
  async shutdown() { this._persist(); }
166
- async store(entry) { this.entries.set(entry.id, entry); this._persist(); }
266
+ async store(entry) { this._track(entry); this._persist(); }
167
267
  async get(id) { return this.entries.get(id) ?? null; }
168
268
  async getByKey(key, ns) {
169
269
  for (const e of this.entries.values()) {
@@ -178,10 +278,11 @@ class JsonFileBackend {
178
278
  if (updates.content !== undefined) e.content = updates.content;
179
279
  if (updates.tags) e.tags = updates.tags;
180
280
  e.updatedAt = Date.now();
281
+ this.written.set(id, e);
181
282
  this._persist();
182
283
  return e;
183
284
  }
184
- async delete(id) { this.deletedIds.add(id); return this.entries.delete(id); }
285
+ async delete(id) { return this._forget(id); }
185
286
  async query(opts) {
186
287
  let results = [...this.entries.values()];
187
288
  if (opts?.namespace) results = results.filter(e => e.namespace === opts.namespace);
@@ -199,8 +300,8 @@ class JsonFileBackend {
199
300
  return results;
200
301
  }
201
302
  async search() { return []; } // No vector search in JSON backend
202
- async bulkInsert(entries) { for (const e of entries) this.entries.set(e.id, e); this._persist(); }
203
- async bulkDelete(ids) { let n = 0; for (const id of ids) { this.deletedIds.add(id); if (this.entries.delete(id)) n++; } this._persist(); return n; }
303
+ async bulkInsert(entries) { for (const e of entries) this._track(e); this._persist(); }
304
+ async bulkDelete(ids) { let n = 0; for (const id of ids) { if (this._forget(id)) n++; } this._persist(); return n; }
204
305
  async count() { return this.entries.size; }
205
306
  async listNamespaces() {
206
307
  const ns = new Set();
@@ -209,8 +310,8 @@ class JsonFileBackend {
209
310
  }
210
311
  async clearNamespace(ns) {
211
312
  let n = 0;
212
- for (const [id, e] of this.entries) {
213
- if (e.namespace === ns) { this.deletedIds.add(id); this.entries.delete(id); n++; }
313
+ for (const [id, e] of [...this.entries]) {
314
+ if (e.namespace === ns) { this._forget(id); n++; }
214
315
  }
215
316
  this._persist();
216
317
  return n;
@@ -235,25 +336,24 @@ class JsonFileBackend {
235
336
  };
236
337
  }
237
338
 
339
+ // Errors propagate (#4685): a failed read, write or rename must reject the
340
+ // store/bulkInsert/shutdown call instead of resolving with an in-memory-only
341
+ // update. The readback runs inside the lock so a parallel writer cannot race it.
238
342
  _persist() {
239
- try {
240
- withStoreLock(this.filePath, () => {
241
- // Keep entries another writer (intelligence.cjs, a parallel hook) added
242
- // after we loaded; ours win on the same id, and our own deletes stay deleted.
243
- const merged = new Map(this.entries);
244
- try {
245
- const onDisk = JSON.parse(readFileSync(this.filePath, 'utf-8'));
246
- if (Array.isArray(onDisk)) {
247
- for (const e of onDisk) {
248
- if (e && !merged.has(e.id) && !this.loadedIds.has(e.id) && !this.deletedIds.has(e.id)) merged.set(e.id, e);
249
- }
250
- }
251
- } catch { /* missing or unreadable: write ours */ }
252
- writeFileAtomic(this.filePath, JSON.stringify([...merged.values()], null, 2));
253
- for (const id of merged.keys()) this.loadedIds.add(id);
254
- this.entries = merged;
255
- });
256
- } catch { /* best effort */ }
343
+ withStoreLock(this.filePath, () => {
344
+ // Keep entries another writer (intelligence.cjs, a parallel hook) added
345
+ // after we loaded; ours win on the same id, and our own deletes stay deleted.
346
+ // A missing file is the only case that writes without merging.
347
+ const merged = new Map(this.entries);
348
+ for (const e of readStoreFile(this.filePath) || []) {
349
+ if (!merged.has(e.id) && !this.loadedIds.has(e.id) && !this.deletedIds.has(e.id)) merged.set(e.id, e);
350
+ }
351
+ writeFileAtomic(this.filePath, JSON.stringify([...merged.values()], null, 2));
352
+ verifyStoreFile(this.filePath, merged, this.deletedIds, sameJson);
353
+ for (const id of merged.keys()) this.loadedIds.add(id);
354
+ this.entries = merged;
355
+ this.storePresent = true;
356
+ });
257
357
  }
258
358
  }
259
359
 
@@ -365,6 +465,7 @@ async function doImport() {
365
465
  }
366
466
 
367
467
  const config = readConfig();
468
+ ensureDataDir();
368
469
  const backend = trackBackend(new JsonFileBackend(STORE_PATH));
369
470
  await backend.initialize();
370
471
 
@@ -395,19 +496,32 @@ async function doImport() {
395
496
  }
396
497
 
397
498
  const bridge = new memPkg.AutoMemoryBridge(backend, bridgeConfig);
499
+ const fileErrors = [];
500
+ if (typeof bridge.on === 'function') bridge.on('import:file-error', (info) => { fileErrors.push(info?.file || 'unknown'); });
398
501
 
502
+ // Success is reported only after the import, the final persist and a fresh
503
+ // readback of every entry this run wrote have all succeeded (#4685).
504
+ const failures = [];
505
+ let result = null;
399
506
  try {
400
- const result = await bridge.importFromAutoMemory();
401
- success(`Imported ${result.imported} entries (${result.skipped} skipped)`);
402
- dim(`├─ Backend entries: ${await backend.count()}`);
403
- dim(`├─ Learning: ${config.learningBridge.enabled ? 'active' : 'disabled'}`);
404
- dim(`├─ Graph: ${config.memoryGraph.enabled ? 'active' : 'disabled'}`);
405
- dim(`└─ Agent scopes: ${config.agentScopes.enabled ? 'active' : 'disabled'}`);
507
+ result = await bridge.importFromAutoMemory();
406
508
  } catch (err) {
407
- dim(`Import failed (non-critical): ${err.message}`);
509
+ failures.push(`import: ${err.message}`);
408
510
  }
409
-
410
- await backend.shutdown();
511
+ try {
512
+ await backend.shutdown();
513
+ verifyStoreFile(STORE_PATH, backend.written, backend.deletedIds, sameRecord);
514
+ } catch (err) {
515
+ failures.push(`persist: ${err.message}`);
516
+ }
517
+ if (fileErrors.length) failures.push(`unreadable source file(s): ${fileErrors.join(', ')}`);
518
+ if (failures.length) throw new HookFailure(`Import failed: ${failures.join('; ')}`);
519
+
520
+ success(`Imported ${result.imported} entries (${result.skipped} skipped), verified in ${STORE_PATH}`);
521
+ dim(`├─ Backend entries: ${await backend.count()}`);
522
+ dim(`├─ Learning: ${config.learningBridge.enabled ? 'active' : 'disabled'}`);
523
+ dim(`├─ Graph: ${config.memoryGraph.enabled ? 'active' : 'disabled'}`);
524
+ dim(`└─ Agent scopes: ${config.agentScopes.enabled ? 'active' : 'disabled'}`);
411
525
  }
412
526
 
413
527
  async function doSync() {
@@ -420,6 +534,7 @@ async function doSync() {
420
534
  }
421
535
 
422
536
  const config = readConfig();
537
+ ensureDataDir();
423
538
  const backend = trackBackend(new JsonFileBackend(STORE_PATH));
424
539
  await backend.initialize();
425
540
 
@@ -454,6 +569,7 @@ async function doSync() {
454
569
 
455
570
  const bridge = new memPkg.AutoMemoryBridge(backend, bridgeConfig);
456
571
 
572
+ let syncError = null;
457
573
  try {
458
574
  const syncResult = await bridge.syncToAutoMemory();
459
575
  success(`Synced ${syncResult.synced} entries to auto memory`);
@@ -467,11 +583,12 @@ async function doSync() {
467
583
  if (indexSkipped) dim(`MEMORY.md index unchanged (${indexSkipped.reason || 'nothing to curate'})`);
468
584
  else success('Curated MEMORY.md index');
469
585
  } catch (err) {
470
- dim(`Sync failed (non-critical): ${err.message}`);
586
+ syncError = err;
471
587
  }
472
588
 
473
589
  if (bridge.destroy) bridge.destroy();
474
590
  await backend.shutdown();
591
+ if (syncError) throw new HookFailure(`Sync failed: ${syncError.message}`);
475
592
  }
476
593
 
477
594
  async function doStatus() {
@@ -489,16 +606,44 @@ async function doStatus() {
489
606
  console.log(` MemoryGraph: ${config.memoryGraph.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
490
607
  console.log(` AgentScopes: ${config.agentScopes.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
491
608
 
492
- if (existsSync(STORE_PATH)) {
493
- try {
494
- const data = JSON.parse(readFileSync(STORE_PATH, 'utf-8'));
495
- console.log(` Entries: ${Array.isArray(data) ? data.length : 0}`);
496
- } catch { /* ignore */ }
609
+ try {
610
+ const data = readStoreFile(STORE_PATH);
611
+ if (data) console.log(` Entries: ${data.length}`);
612
+ } catch (err) {
613
+ console.log(` Entries: ❌ ${err.message}`);
497
614
  }
498
615
 
499
616
  console.log('');
500
617
  }
501
618
 
619
+ // Read-only exact lookup (#4685). It never loads the memory package, creates a
620
+ // directory, takes the write lock or calls shutdown, so it cannot change the store.
621
+ const READ_SCHEMA_VERSION = 1;
622
+ async function doRead(namespace, key) {
623
+ const out = { schemaVersion: READ_SCHEMA_VERSION, store: STORE_PATH, storePresent: false, namespace: namespace ?? null, key: key ?? null, found: false, entry: null };
624
+ const refuse = (code, message) => {
625
+ out.error = { code, message };
626
+ console.log(JSON.stringify(out, null, 2));
627
+ return 1;
628
+ };
629
+ if (typeof namespace !== 'string' || !namespace || typeof key !== 'string' || !key) {
630
+ return refuse('USAGE', 'usage: auto-memory-hook.mjs read <namespace> <key>');
631
+ }
632
+ const backend = new JsonFileBackend(STORE_PATH);
633
+ try {
634
+ await backend.initialize();
635
+ } catch (err) {
636
+ out.storePresent = true; // initialize() only throws for a file that exists
637
+ return refuse(err.code || 'STORE_READ_FAILED', err.message);
638
+ }
639
+ out.storePresent = backend.storePresent;
640
+ const matches = await backend.query({ namespace, key });
641
+ if (matches.length > 1) return refuse('AMBIGUOUS_KEY', `${matches.length} entries share namespace ${JSON.stringify(namespace)} and key ${JSON.stringify(key)}`);
642
+ if (matches.length === 1) { out.found = true; out.entry = matches[0]; }
643
+ console.log(JSON.stringify(out, null, 2));
644
+ return 0;
645
+ }
646
+
502
647
  // ============================================================================
503
648
  // Main
504
649
  // ============================================================================
@@ -506,8 +651,10 @@ async function doStatus() {
506
651
  // Older settings.json files run `node -e "…import(file)" import`, where the
507
652
  // subcommand lands in argv[1] and argv[2] is empty, so both hooks fell through
508
653
  // to `status` and never imported or synced (#4058). Accept that shape too.
509
- const COMMANDS = ['import', 'sync', 'status'];
654
+ const COMMANDS = ['import', 'sync', 'status', 'read'];
655
+ const argvBase = process.argv[2] ? 2 : 1;
510
656
  const command = process.argv[2] || (COMMANDS.includes(process.argv[1]) ? process.argv[1] : 'status');
657
+ const commandArgs = process.argv.slice(argvBase + 1);
511
658
 
512
659
  // Dynamic import() failures can surface as unhandled rejections on a later
513
660
  // microtask even when the awaiting call site already caught them, which would
@@ -521,18 +668,25 @@ process.on('unhandledRejection', (reason) => {
521
668
  }
522
669
  });
523
670
 
671
+ // A failed import or sync exits 1, never 2: Claude Code treats exit 2 from a
672
+ // hook as "block", while exit 1 is a non-blocking error it shows to the user,
673
+ // so the session carries on and the failure is no longer hidden (#4685).
674
+ let exitCode = 0;
524
675
  try {
525
676
  switch (command) {
526
677
  case 'import': await doImport(); break;
527
678
  case 'sync': await doSync(); break;
528
679
  case 'status': await doStatus(); break;
680
+ case 'read': exitCode = await doRead(commandArgs[0], commandArgs[1]); break;
529
681
  default:
530
- console.log('Usage: auto-memory-hook.mjs <import|sync|status>');
682
+ console.log('Usage: auto-memory-hook.mjs <import|sync|status|read <namespace> <key>>');
531
683
  break;
532
684
  }
533
685
  } catch (err) {
534
- // Hooks must never crash Claude Code - fail silently
535
- try { dim(`Error (non-critical): ${err.message}`); } catch (_) {}
686
+ exitCode = 1;
687
+ const detail = err && err.message ? err.message : String(err);
688
+ const line = err instanceof HookFailure ? `[AutoMemory] ${detail}` : `[AutoMemory] ${command} failed: ${detail}`;
689
+ try { process.stderr.write(`${line}\n`); } catch (_) {}
536
690
  }
537
- // Force clean exit — process.exitCode alone isn't enough if async errors override it
538
- process.exit(0);
691
+ // Force exit — process.exitCode alone isn't enough if async errors override it
692
+ process.exit(exitCode);
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.71.1",
3
+ "version": "3.71.3",
4
4
  "files": {
5
- "auto-memory-hook.mjs": "631fac9d518835e2852a406888316bbbddddb49e7ed8e406d2e971f46833df9a",
5
+ "auto-memory-hook.mjs": "1953b51e9e88330e177e5a294ec3998b5a9cd0b481ba93398a2b0f01b4088e5b",
6
6
  "hook-handler.cjs": "0ce70981aa33f76d008b6f49626effaf4269918c4094f1a28c1ef6fceb097d6f",
7
7
  "intelligence.cjs": "1d981d6c6a35d70a66450e96458e26169e0cede11523ea246a2c129c197a5b4e",
8
8
  "statusline.cjs": "d72417ecb38634011a19f095a30e07b9929e6f97663520172ece9be7491b2eaa",
@@ -10,6 +10,6 @@
10
10
  "learning-loop.cjs": "d046cd6f8efb7dd40e1d2451516c181fc8b26477aefb1124c0df5f52a23f9190"
11
11
  }
12
12
  },
13
- "signature": "Ocl6MP3QExazQtRc7kRkNgyaKBUY3Y8YBX1mNYxfHzV/K9DHoZMefO89yD6WRsKf8XJUIXtfRUc4J4g45wMlCQ==",
13
+ "signature": "LJPNZ7bN0gsibn3NGkh0QPxBjPtMGYX4OibxWJwGZ5XCq3LdURP2lhGe+Yr4i5fBumpBTqyOElIuVCG0kReLCQ==",
14
14
  "algorithm": "ed25519"
15
15
  }
@@ -12,7 +12,8 @@
12
12
  {
13
13
  "type": "command",
14
14
  "command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" pre-bash",
15
- "timeout": 5000
15
+ "timeout": 5000,
16
+ "onFailure": "block"
16
17
  }
17
18
  ]
18
19
  }
package/README.md CHANGED
@@ -36,6 +36,8 @@
36
36
  /ruflo
37
37
  ```
38
38
 
39
+ From a terminal on Claude Code 2.1.292 or later, one command adds the marketplace and installs a plugin: `claude plugin install ruflo-core@ruflo --marketplace ruvnet/ruflo`.
40
+
39
41
  **Core** provides foundation tools. **Console** opens the agent cockpit. **Mods** add routing and policy enforcement inside Claude Code. Mods run with your account's permissions; review their code before installing.
40
42
 
41
43
  <img src="docs/assets/readme/icons/terminal.svg" width="28" height="28" alt=""> **Using Codex or another MCP enabled tool?** Use NPX for Ruflo project setup and connect your client to the Ruflo MCP server. Run the setup wizard in your project terminal:
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 7,
4
- "generatedAt": "2026-11-04T09:54:05.169Z",
5
- "gitSha": "91e383bd",
4
+ "generatedAt": "2026-11-05T00:20:52.796Z",
5
+ "gitSha": "9045ab84",
6
6
  "catalog": {
7
7
  "agents": 177,
8
8
  "tools": 450,
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { output } from '../output.js';
6
6
  import { getDaemon, startDaemon, stopDaemon, parseEnabledWorkers } from '../services/worker-daemon.js';
7
- import { resolveDaemonProjectRoot } from '../services/daemon-autostart.js';
7
+ import { AUTOSTART_ENV, clearDaemonStopMarker, isDaemonStopMarked, resolveDaemonProjectRoot, writeDaemonStopMarker, } from '../services/daemon-autostart.js';
8
8
  import { fork } from 'child_process';
9
9
  import { fileURLToPath } from 'url';
10
10
  import { dirname, join, resolve } from 'path';
@@ -72,6 +72,19 @@ const startCommand = {
72
72
  const projectRoot = resolveWorkspaceFlag(ctx.flags.workspace)
73
73
  ?? resolveDaemonProjectRoot(process.cwd());
74
74
  const isDaemonProcess = process.env.CLAUDE_FLOW_DAEMON === '1';
75
+ // #4654: an explicit start re-enables auto-start after `daemon stop`. The
76
+ // start that auto-start spawned must not clear it, and must re-check it:
77
+ // a `daemon stop` may have landed after the hook checked, and starting
78
+ // anyway would bring back the daemon the user just stopped.
79
+ if (!isDaemonProcess) {
80
+ if (process.env[AUTOSTART_ENV] === '1') {
81
+ if (isDaemonStopMarked(projectRoot))
82
+ return { success: true };
83
+ }
84
+ else {
85
+ clearDaemonStopMarker(projectRoot);
86
+ }
87
+ }
75
88
  // Parse resource threshold overrides from CLI flags
76
89
  const config = {};
77
90
  // #2661: thread --headless into DaemonConfig so it actually gates the
@@ -649,6 +662,9 @@ const stopCommand = {
649
662
  // Git worktrees. Only processes positively identified as ruflo daemons
650
663
  // (via their self-identifying argv) are touched; each receives SIGTERM
651
664
  // so its own shutdown path reaps in-flight Claude process groups.
665
+ // #4654: record the opt-out first, so a hook firing mid-stop cannot
666
+ // auto-start a fresh daemon. Cleared only by an explicit `daemon start`.
667
+ writeDaemonStopMarker(projectRoot);
652
668
  if (ctx.flags.all) {
653
669
  return stopAllDaemons(quiet);
654
670
  }
@@ -716,6 +732,9 @@ async function stopAllDaemons(quiet) {
716
732
  else {
717
733
  process.kill(d.pid, 'SIGTERM');
718
734
  }
735
+ // #4654: keep each stopped workspace stopped until an explicit start.
736
+ if (d.workspace)
737
+ writeDaemonStopMarker(d.workspace);
719
738
  stopped++;
720
739
  if (!quiet) {
721
740
  output.printInfo(`Stopping daemon PID ${d.pid}${d.workspace ? ` (${d.workspace})` : ''}`);
@@ -121,7 +121,7 @@ const searchCommand = {
121
121
  const threshold = rawThreshold === undefined || rawThreshold === null
122
122
  ? 0.5
123
123
  : parseFloat(String(rawThreshold));
124
- const dbPath = ctx.flags['db-path'] || '.swarm/memory.db';
124
+ const dbPath = ctx.flags.dbPath || '.swarm/memory.db';
125
125
  if (!query) {
126
126
  output.printError('Query is required');
127
127
  return { success: false, exitCode: 1 };
@@ -394,7 +394,7 @@ const collectionsCommand = {
394
394
  ],
395
395
  action: async (ctx) => {
396
396
  const action = ctx.flags.action || 'list';
397
- const dbPath = ctx.flags['db-path'] || '.swarm/memory.db';
397
+ const dbPath = ctx.flags.dbPath || '.swarm/memory.db';
398
398
  output.writeln();
399
399
  output.writeln(output.bold('Embedding Collections (Namespaces)'));
400
400
  output.writeln(output.dim('─'.repeat(60)));
@@ -500,7 +500,7 @@ const indexCommand = {
500
500
  action: async (ctx) => {
501
501
  const action = ctx.flags.action || 'status';
502
502
  const collection = ctx.flags.collection;
503
- const efConstruction = parseInt(ctx.flags['ef-construction'] || '200', 10);
503
+ const efConstruction = parseInt(ctx.flags.efConstruction || '200', 10);
504
504
  const m = parseInt(ctx.flags.m || '16', 10);
505
505
  output.writeln();
506
506
  output.writeln(output.bold(`HNSW Index: ${action}`));
@@ -850,7 +850,7 @@ const chunkCommand = {
850
850
  action: async (ctx) => {
851
851
  const embeddings = await getEmbeddings();
852
852
  const text = ctx.flags.text || '';
853
- const maxSize = parseInt(ctx.flags['max-size'] || '512', 10);
853
+ const maxSize = parseInt(ctx.flags.maxSize || '512', 10);
854
854
  const overlap = parseInt(ctx.flags.overlap || '50', 10);
855
855
  const strategy = ctx.flags.strategy || 'sentence';
856
856
  output.writeln();
@@ -1275,7 +1275,7 @@ const cacheCommand = {
1275
1275
  ],
1276
1276
  action: async (ctx) => {
1277
1277
  const action = ctx.flags.action || 'stats';
1278
- const dbPath = ctx.flags['db-path'] || '.cache/embeddings.db';
1278
+ const dbPath = ctx.flags.dbPath || '.cache/embeddings.db';
1279
1279
  output.writeln();
1280
1280
  output.writeln(output.bold('Embedding Cache'));
1281
1281
  output.writeln(output.dim('─'.repeat(50)));
@@ -1479,7 +1479,7 @@ const benchmarkCommand = {
1479
1479
  ],
1480
1480
  action: async (ctx) => {
1481
1481
  const iterations = parseInt(ctx.flags.iterations || '10', 10);
1482
- const batchSize = parseInt(ctx.flags['batch-size'] || '5', 10);
1482
+ const batchSize = parseInt(ctx.flags.batchSize || '5', 10);
1483
1483
  const full = ctx.flags.full === true;
1484
1484
  output.writeln();
1485
1485
  output.writeln(output.bold('Embedding Performance Benchmark'));
@@ -44,6 +44,16 @@ export declare function currentRufloServer(): {
44
44
  command: string;
45
45
  args: string[];
46
46
  };
47
+ /**
48
+ * The model for a spawned Claude, from `--model` (#1870). With no `--model`
49
+ * nothing is passed and Claude Code picks its own default. The value becomes
50
+ * its own argv token, so one starting with "-" would be read by Claude as a
51
+ * flag and is refused.
52
+ */
53
+ export declare function resolveWorkerModel(flags: Record<string, unknown>): {
54
+ model?: string;
55
+ error?: string;
56
+ };
47
57
  export interface RunOutcome {
48
58
  ok: boolean;
49
59
  output: string;
@@ -88,6 +88,26 @@ export function currentRufloServer() {
88
88
  ? { command: process.execPath, args: [entry, 'mcp', 'start'] }
89
89
  : { command: 'npx', args: ['-y', '@claude-flow/cli@latest', 'mcp', 'start'] };
90
90
  }
91
+ const WORKER_MODEL = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,199}$/;
92
+ /**
93
+ * The model for a spawned Claude, from `--model` (#1870). With no `--model`
94
+ * nothing is passed and Claude Code picks its own default. The value becomes
95
+ * its own argv token, so one starting with "-" would be read by Claude as a
96
+ * flag and is refused.
97
+ */
98
+ export function resolveWorkerModel(flags) {
99
+ const raw = flags.model;
100
+ if (raw === undefined || raw === null)
101
+ return {};
102
+ if (typeof raw !== 'string' || raw.trim() === '') {
103
+ return { error: '--model needs a value, e.g. --model sonnet' };
104
+ }
105
+ const model = raw.trim();
106
+ if (!WORKER_MODEL.test(model)) {
107
+ return { error: `--model "${model}" is not a valid model name or id` };
108
+ }
109
+ return { model };
110
+ }
91
111
  /** Run one prompt through `claude -p` and parse its JSON result. */
92
112
  export const runWithClaude = (prompt, opts) => new Promise((resolve) => {
93
113
  const started = Date.now();