@claude-flow/cli 3.71.2 → 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.
- package/.claude/helpers/auto-memory-hook.mjs +208 -54
- package/.claude/helpers/helpers.manifest.json +3 -3
- package/.claude/settings.json +2 -1
- package/README.md +2 -0
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/daemon.js +20 -1
- package/dist/src/commands/hive-mind-worker.d.ts +10 -0
- package/dist/src/commands/hive-mind-worker.js +20 -0
- package/dist/src/commands/hive-mind.js +27 -4
- package/dist/src/init/settings-generator.js +4 -0
- package/dist/src/mcp-tools/agentdb-tools.js +3 -3
- package/dist/src/mcp-tools/claims-tools.js +16 -9
- package/dist/src/mcp-tools/hooks-tools.js +11 -1
- package/dist/src/mcp-tools/task-tools.js +11 -0
- package/dist/src/missions/schemas.d.ts +10 -10
- package/dist/src/mods/headless-plugins.d.ts +16 -0
- package/dist/src/mods/headless-plugins.js +30 -1
- package/dist/src/services/daemon-autostart.d.ts +24 -0
- package/dist/src/services/daemon-autostart.js +55 -1
- package/dist/src/services/headless-worker-executor.d.ts +15 -0
- package/dist/src/services/headless-worker-executor.js +28 -0
- package/package.json +1 -1
- package/plugins/ruflo-console/dist/tui.mjs +132 -132
|
@@ -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
|
-
//
|
|
80
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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.
|
|
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) {
|
|
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.
|
|
203
|
-
async bulkDelete(ids) { let n = 0; for (const id of ids) {
|
|
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.
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
509
|
+
failures.push(`import: ${err.message}`);
|
|
408
510
|
}
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
535
|
-
|
|
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
|
|
538
|
-
process.exit(
|
|
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.
|
|
3
|
+
"version": "3.71.3",
|
|
4
4
|
"files": {
|
|
5
|
-
"auto-memory-hook.mjs": "
|
|
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": "
|
|
13
|
+
"signature": "LJPNZ7bN0gsibn3NGkh0QPxBjPtMGYX4OibxWJwGZ5XCq3LdURP2lhGe+Yr4i5fBumpBTqyOElIuVCG0kReLCQ==",
|
|
14
14
|
"algorithm": "ed25519"
|
|
15
15
|
}
|
package/.claude/settings.json
CHANGED
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:
|
package/catalog-manifest.json
CHANGED
|
@@ -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})` : ''}`);
|
|
@@ -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();
|
|
@@ -10,7 +10,7 @@ import { select, confirm, input } from '../prompt.js';
|
|
|
10
10
|
import { callMCPTool, MCPClientError } from '../mcp-client.js';
|
|
11
11
|
import { spawn as childSpawn } from 'child_process';
|
|
12
12
|
import { mkdir, writeFile } from 'fs/promises';
|
|
13
|
-
import { resolveWorkerMcpConfig, currentRufloServer, workCommand } from './hive-mind-worker.js';
|
|
13
|
+
import { resolveWorkerMcpConfig, resolveWorkerModel, currentRufloServer, workCommand } from './hive-mind-worker.js';
|
|
14
14
|
import { join } from 'path';
|
|
15
15
|
import { resolveClaudeLaunchCommand } from '../runtime/claude-command.js';
|
|
16
16
|
import { withHeadlessPlugins } from '../mods/headless-plugins.js';
|
|
@@ -168,6 +168,7 @@ async function spawnClaudeCodeInstance(swarmId, swarmName, objective, workers, f
|
|
|
168
168
|
const workerGroups = groupWorkersByType(workers);
|
|
169
169
|
const hiveMindPrompt = generateHiveMindPrompt(swarmId, swarmName, objective, workers, workerGroups, flags);
|
|
170
170
|
spinner.succeed('Hive Mind coordination prompt ready!');
|
|
171
|
+
const { model: workerModel } = resolveWorkerModel(flags);
|
|
171
172
|
// Display coordination summary
|
|
172
173
|
output.writeln();
|
|
173
174
|
output.writeln(output.bold('🧠 Hive Mind Configuration'));
|
|
@@ -179,6 +180,7 @@ async function spawnClaudeCodeInstance(swarmId, swarmName, objective, workers, f
|
|
|
179
180
|
`Worker Count: ${output.highlight(String(workers.length))}`,
|
|
180
181
|
`Worker Types: ${output.highlight(Object.keys(workerGroups).join(', '))}`,
|
|
181
182
|
`Consensus: ${output.highlight(flags.consensus || 'byzantine')}`,
|
|
183
|
+
`Model: ${output.highlight(workerModel ?? 'Claude Code default')}`,
|
|
182
184
|
`MCP Tools: ${output.success('Full Claude-Flow integration enabled')}`
|
|
183
185
|
]);
|
|
184
186
|
// Ensure sessions directory exists
|
|
@@ -249,6 +251,10 @@ async function spawnClaudeCodeInstance(swarmId, swarmName, objective, workers, f
|
|
|
249
251
|
output.printWarning('Using --dangerously-skip-permissions for seamless hive-mind execution');
|
|
250
252
|
}
|
|
251
253
|
}
|
|
254
|
+
// #1870: without --model the argv is unchanged and Claude Code picks its default.
|
|
255
|
+
if (workerModel) {
|
|
256
|
+
claudeArgs.push('--model', workerModel);
|
|
257
|
+
}
|
|
252
258
|
// ADR-510 §2.2: a non-interactive (-p) run is a headless worker and loads
|
|
253
259
|
// no marketplace plugins on a fresh config, so stage the worker profile
|
|
254
260
|
// (default ruflo-mods) as --plugin-dir before the prompt. The interactive
|
|
@@ -344,11 +350,12 @@ async function spawnClaudeCodeInstance(swarmId, swarmName, objective, workers, f
|
|
|
344
350
|
output.writeln();
|
|
345
351
|
output.writeln(output.bold('📋 Manual Execution Instructions:'));
|
|
346
352
|
output.writeln(output.dim('─'.repeat(50)));
|
|
353
|
+
const modelArg = workerModel ? ` --model ${workerModel}` : '';
|
|
347
354
|
output.printList([
|
|
348
355
|
'Install Claude Code: npm install -g @anthropic-ai/claude-code',
|
|
349
|
-
`Run with saved prompt: claude < ${promptFile}`,
|
|
350
|
-
`Or copy manually: cat ${promptFile} | claude`,
|
|
351
|
-
`With auto-permissions: claude --dangerously-skip-permissions < ${promptFile}`
|
|
356
|
+
`Run with saved prompt: claude${modelArg} < ${promptFile}`,
|
|
357
|
+
`Or copy manually: cat ${promptFile} | claude${modelArg}`,
|
|
358
|
+
`With auto-permissions: claude${modelArg} --dangerously-skip-permissions < ${promptFile}`
|
|
352
359
|
]);
|
|
353
360
|
return { success: true, promptFile };
|
|
354
361
|
}
|
|
@@ -529,6 +536,12 @@ const spawnCommand = {
|
|
|
529
536
|
description: 'Objective for the hive mind (used with --claude)',
|
|
530
537
|
type: 'string'
|
|
531
538
|
},
|
|
539
|
+
{
|
|
540
|
+
name: 'model',
|
|
541
|
+
short: 'm',
|
|
542
|
+
description: 'Model for the Claude Code worker started by --claude, passed as claude --model (e.g. sonnet, opus, or a full model id; default: the Claude Code default)',
|
|
543
|
+
type: 'string'
|
|
544
|
+
},
|
|
532
545
|
{
|
|
533
546
|
name: 'dangerously-skip-permissions',
|
|
534
547
|
description: 'Skip permission prompts in Claude Code (use with caution)',
|
|
@@ -564,6 +577,7 @@ const spawnCommand = {
|
|
|
564
577
|
{ command: 'claude-flow hive-mind spawn -n 3 -r specialist', description: 'Spawn 3 specialists' },
|
|
565
578
|
{ command: 'claude-flow hive-mind spawn -t coder -p my-coder', description: 'Spawn coder with custom prefix' },
|
|
566
579
|
{ command: 'claude-flow hive-mind spawn --claude -o "Build a REST API"', description: 'Launch Claude Code with objective' },
|
|
580
|
+
{ command: 'claude-flow hive-mind spawn --claude -m sonnet -o "Build a REST API"', description: 'Launch Claude Code on a pinned model' },
|
|
567
581
|
{ command: 'claude-flow hive-mind spawn -n 5 --claude -o "Research AI patterns"', description: 'Spawn workers and launch Claude Code' }
|
|
568
582
|
],
|
|
569
583
|
action: async (ctx) => {
|
|
@@ -574,6 +588,15 @@ const spawnCommand = {
|
|
|
574
588
|
const prefix = ctx.flags.prefix || 'hive-worker';
|
|
575
589
|
const launchClaude = ctx.flags.claude;
|
|
576
590
|
let objective = ctx.flags.objective || ctx.args.join(' ');
|
|
591
|
+
// #1870: refuse a bad --model before any worker is spawned.
|
|
592
|
+
const workerModel = resolveWorkerModel(ctx.flags);
|
|
593
|
+
if (workerModel.error) {
|
|
594
|
+
output.printError(workerModel.error);
|
|
595
|
+
return { success: false, exitCode: 1 };
|
|
596
|
+
}
|
|
597
|
+
if (workerModel.model && !launchClaude) {
|
|
598
|
+
output.printWarning('--model only applies with --claude; ignoring it');
|
|
599
|
+
}
|
|
577
600
|
output.printInfo(`Spawning ${count} ${role} agent(s)...`);
|
|
578
601
|
try {
|
|
579
602
|
const result = await callMCPTool('hive-mind_spawn', {
|
|
@@ -210,6 +210,10 @@ function generateHooksConfig(config, scope) {
|
|
|
210
210
|
type: 'command',
|
|
211
211
|
command: hookHandlerCmd('pre-bash', scope),
|
|
212
212
|
timeout: config.timeout,
|
|
213
|
+
// Claude Code 2.1.295+: if the safety gate can't start, times out or
|
|
214
|
+
// exits with an unexpected code, block the command instead of letting
|
|
215
|
+
// it run. Older versions ignore the key.
|
|
216
|
+
onFailure: 'block',
|
|
213
217
|
},
|
|
214
218
|
],
|
|
215
219
|
},
|