@claude-flow/cli 3.72.2 → 3.74.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.
- package/.claude/helpers/.helpers-version +1 -1
- package/.claude/helpers/auto-memory-hook.mjs +53 -224
- package/.claude/helpers/helpers.manifest.json +6 -6
- package/.claude/helpers/hook-handler.cjs +10 -89
- package/.claude/helpers/intelligence.cjs +1 -23
- package/.claude/helpers/statusline.cjs +14 -29
- package/README.md +29 -21
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/agent.js +2 -0
- package/dist/src/commands/doctor.js +5 -3
- package/dist/src/commands/funnel.d.ts +1 -0
- package/dist/src/commands/funnel.js +37 -2
- package/dist/src/commands/hooks.js +16 -2
- package/dist/src/commands/init.js +70 -16
- package/dist/src/commands/mcp.js +15 -0
- package/dist/src/commands/swarm.js +2 -0
- package/dist/src/funnel/activation.d.ts +30 -0
- package/dist/src/funnel/activation.js +69 -0
- package/dist/src/funnel/events.js +4 -1
- package/dist/src/funnel/index.d.ts +2 -1
- package/dist/src/funnel/index.js +2 -1
- package/dist/src/funnel/message-transport.d.ts +12 -0
- package/dist/src/funnel/message-transport.js +23 -1
- package/dist/src/funnel/types.d.ts +2 -2
- package/dist/src/init/first-win.d.ts +24 -0
- package/dist/src/init/first-win.js +97 -0
- package/dist/src/mcp-tools/agent-tools.js +2 -0
- package/dist/src/mcp-tools/memory-tools.js +17 -0
- package/dist/src/mcp-tools/metaharness-tools.js +19 -0
- package/dist/src/mcp-tools/swarm-tools.js +2 -0
- package/dist/src/memory/memory-bridge.d.ts +2 -0
- package/dist/src/memory/memory-bridge.js +1 -1
- package/dist/src/memory/memory-initializer.d.ts +2 -0
- package/dist/src/memory/memory-initializer.js +1 -1
- package/dist/src/metaharness/darwin-lib.d.ts +68 -0
- package/dist/src/metaharness/darwin-lib.js +150 -0
- package/dist/src/transfer/store/download.d.ts +1 -1
- package/dist/src/transfer/store/download.js +22 -21
- package/dist/src/transfer/store/publish.d.ts +1 -1
- package/dist/src/transfer/store/publish.js +13 -14
- package/node_modules/@claude-flow/plugin-agent-federation/package.json +5 -5
- package/node_modules/@claude-flow/security/dist/input-validator.d.ts +138 -106
- package/node_modules/@claude-flow/security/dist/input-validator.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/input-validator.js +23 -26
- package/node_modules/@claude-flow/security/dist/input-validator.js.map +1 -1
- package/node_modules/@claude-flow/security/package.json +2 -2
- package/package.json +10 -10
- package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -1
- package/plugins/ruflo-metaharness/CHANGELOG.md +3 -0
- package/plugins/ruflo-metaharness/scripts/_darwin.mjs +13 -0
- package/plugins/ruflo-metaharness/scripts/bench.mjs +34 -2
- package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.63.4
|
|
@@ -9,10 +9,6 @@
|
|
|
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.
|
|
16
12
|
*/
|
|
17
13
|
|
|
18
14
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, rmdirSync, rmSync, statSync } from 'fs';
|
|
@@ -61,9 +57,6 @@ function warnMemoryUnavailable() {
|
|
|
61
57
|
|
|
62
58
|
const DEBUG = !!(process.env.RUFLO_DEBUG || process.env.DEBUG);
|
|
63
59
|
|
|
64
|
-
/** A command failure whose message is already complete; main prints it and exits 1. */
|
|
65
|
-
class HookFailure extends Error {}
|
|
66
|
-
|
|
67
60
|
// ── Graceful shutdown (FIX 3) ───────────────────────────────────────────────
|
|
68
61
|
// Track the backend in use so a SIGTERM/SIGINT mid-run can still flush it
|
|
69
62
|
// (the JSON backend persists; a SQLite-backed one closes/flushes WAL) instead
|
|
@@ -83,9 +76,8 @@ async function gracefulExit(signal) {
|
|
|
83
76
|
process.on('SIGTERM', () => { gracefulExit('SIGTERM'); });
|
|
84
77
|
process.on('SIGINT', () => { gracefulExit('SIGINT'); });
|
|
85
78
|
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
function ensureDataDir() { if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true }); }
|
|
79
|
+
// Ensure data dir
|
|
80
|
+
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
|
|
89
81
|
|
|
90
82
|
// ============================================================================
|
|
91
83
|
// Store write safety (#4058): intelligence.cjs read-modify-writes the same
|
|
@@ -126,83 +118,6 @@ function writeFileAtomic(filePath, text) {
|
|
|
126
118
|
}
|
|
127
119
|
}
|
|
128
120
|
|
|
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
|
-
|
|
206
121
|
// Claude Code shares one auto-memory directory across every worktree of a
|
|
207
122
|
// repository, but findGitRoot() in the memory package stops at a worktree's
|
|
208
123
|
// `.git` FILE and keys the directory by the worktree path (#4058). Resolve the
|
|
@@ -234,36 +149,21 @@ class JsonFileBackend {
|
|
|
234
149
|
this.entries = new Map();
|
|
235
150
|
this.loadedIds = new Set();
|
|
236
151
|
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;
|
|
240
152
|
}
|
|
241
153
|
|
|
242
154
|
async initialize() {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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');
|
|
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 */ }
|
|
253
162
|
}
|
|
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);
|
|
263
163
|
}
|
|
264
164
|
|
|
265
165
|
async shutdown() { this._persist(); }
|
|
266
|
-
async store(entry) { this.
|
|
166
|
+
async store(entry) { this.entries.set(entry.id, entry); this._persist(); }
|
|
267
167
|
async get(id) { return this.entries.get(id) ?? null; }
|
|
268
168
|
async getByKey(key, ns) {
|
|
269
169
|
for (const e of this.entries.values()) {
|
|
@@ -278,11 +178,10 @@ class JsonFileBackend {
|
|
|
278
178
|
if (updates.content !== undefined) e.content = updates.content;
|
|
279
179
|
if (updates.tags) e.tags = updates.tags;
|
|
280
180
|
e.updatedAt = Date.now();
|
|
281
|
-
this.written.set(id, e);
|
|
282
181
|
this._persist();
|
|
283
182
|
return e;
|
|
284
183
|
}
|
|
285
|
-
async delete(id) { return this.
|
|
184
|
+
async delete(id) { this.deletedIds.add(id); return this.entries.delete(id); }
|
|
286
185
|
async query(opts) {
|
|
287
186
|
let results = [...this.entries.values()];
|
|
288
187
|
if (opts?.namespace) results = results.filter(e => e.namespace === opts.namespace);
|
|
@@ -300,8 +199,8 @@ class JsonFileBackend {
|
|
|
300
199
|
return results;
|
|
301
200
|
}
|
|
302
201
|
async search() { return []; } // No vector search in JSON backend
|
|
303
|
-
async bulkInsert(entries) { for (const e of entries) this.
|
|
304
|
-
async bulkDelete(ids) { let n = 0; for (const id of ids) { if (this.
|
|
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; }
|
|
305
204
|
async count() { return this.entries.size; }
|
|
306
205
|
async listNamespaces() {
|
|
307
206
|
const ns = new Set();
|
|
@@ -310,8 +209,8 @@ class JsonFileBackend {
|
|
|
310
209
|
}
|
|
311
210
|
async clearNamespace(ns) {
|
|
312
211
|
let n = 0;
|
|
313
|
-
for (const [id, e] of
|
|
314
|
-
if (e.namespace === ns) { this.
|
|
212
|
+
for (const [id, e] of this.entries) {
|
|
213
|
+
if (e.namespace === ns) { this.deletedIds.add(id); this.entries.delete(id); n++; }
|
|
315
214
|
}
|
|
316
215
|
this._persist();
|
|
317
216
|
return n;
|
|
@@ -336,24 +235,25 @@ class JsonFileBackend {
|
|
|
336
235
|
};
|
|
337
236
|
}
|
|
338
237
|
|
|
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.
|
|
342
238
|
_persist() {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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 */ }
|
|
357
257
|
}
|
|
358
258
|
}
|
|
359
259
|
|
|
@@ -465,7 +365,6 @@ async function doImport() {
|
|
|
465
365
|
}
|
|
466
366
|
|
|
467
367
|
const config = readConfig();
|
|
468
|
-
ensureDataDir();
|
|
469
368
|
const backend = trackBackend(new JsonFileBackend(STORE_PATH));
|
|
470
369
|
await backend.initialize();
|
|
471
370
|
|
|
@@ -496,53 +395,23 @@ async function doImport() {
|
|
|
496
395
|
}
|
|
497
396
|
|
|
498
397
|
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'); });
|
|
501
398
|
|
|
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;
|
|
506
|
-
try {
|
|
507
|
-
result = await bridge.importFromAutoMemory();
|
|
508
|
-
} catch (err) {
|
|
509
|
-
failures.push(`import: ${err.message}`);
|
|
510
|
-
}
|
|
511
399
|
try {
|
|
512
|
-
await
|
|
513
|
-
|
|
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'}`);
|
|
514
406
|
} catch (err) {
|
|
515
|
-
|
|
407
|
+
dim(`Import failed (non-critical): ${err.message}`);
|
|
516
408
|
}
|
|
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'}`);
|
|
525
|
-
}
|
|
526
409
|
|
|
527
|
-
|
|
528
|
-
// entries must never be promoted into the user's own Claude Code memory,
|
|
529
|
-
// which the model loads every session as the user's notes.
|
|
530
|
-
function storeIsRepoTracked() {
|
|
531
|
-
if (!existsSync(STORE_PATH)) return false;
|
|
532
|
-
try {
|
|
533
|
-
execFileSync('git', ['ls-files', '--error-unmatch', '--', STORE_PATH], { cwd: PROJECT_ROOT, stdio: 'ignore', timeout: 2000 });
|
|
534
|
-
return true;
|
|
535
|
-
} catch {
|
|
536
|
-
return false;
|
|
537
|
-
}
|
|
410
|
+
await backend.shutdown();
|
|
538
411
|
}
|
|
539
412
|
|
|
540
413
|
async function doSync() {
|
|
541
414
|
log('Syncing insights to auto memory files...');
|
|
542
|
-
if (storeIsRepoTracked()) {
|
|
543
|
-
dim('Skipping sync: the memory store is committed to this repository, so its entries are not this machine\'s');
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
415
|
|
|
547
416
|
const memPkg = await loadMemoryPackage();
|
|
548
417
|
if (!memPkg || !memPkg.AutoMemoryBridge) {
|
|
@@ -551,7 +420,6 @@ async function doSync() {
|
|
|
551
420
|
}
|
|
552
421
|
|
|
553
422
|
const config = readConfig();
|
|
554
|
-
ensureDataDir();
|
|
555
423
|
const backend = trackBackend(new JsonFileBackend(STORE_PATH));
|
|
556
424
|
await backend.initialize();
|
|
557
425
|
|
|
@@ -586,7 +454,6 @@ async function doSync() {
|
|
|
586
454
|
|
|
587
455
|
const bridge = new memPkg.AutoMemoryBridge(backend, bridgeConfig);
|
|
588
456
|
|
|
589
|
-
let syncError = null;
|
|
590
457
|
try {
|
|
591
458
|
const syncResult = await bridge.syncToAutoMemory();
|
|
592
459
|
success(`Synced ${syncResult.synced} entries to auto memory`);
|
|
@@ -600,12 +467,11 @@ async function doSync() {
|
|
|
600
467
|
if (indexSkipped) dim(`MEMORY.md index unchanged (${indexSkipped.reason || 'nothing to curate'})`);
|
|
601
468
|
else success('Curated MEMORY.md index');
|
|
602
469
|
} catch (err) {
|
|
603
|
-
|
|
470
|
+
dim(`Sync failed (non-critical): ${err.message}`);
|
|
604
471
|
}
|
|
605
472
|
|
|
606
473
|
if (bridge.destroy) bridge.destroy();
|
|
607
474
|
await backend.shutdown();
|
|
608
|
-
if (syncError) throw new HookFailure(`Sync failed: ${syncError.message}`);
|
|
609
475
|
}
|
|
610
476
|
|
|
611
477
|
async function doStatus() {
|
|
@@ -623,44 +489,16 @@ async function doStatus() {
|
|
|
623
489
|
console.log(` MemoryGraph: ${config.memoryGraph.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
|
|
624
490
|
console.log(` AgentScopes: ${config.agentScopes.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
|
|
625
491
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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 */ }
|
|
631
497
|
}
|
|
632
498
|
|
|
633
499
|
console.log('');
|
|
634
500
|
}
|
|
635
501
|
|
|
636
|
-
// Read-only exact lookup (#4685). It never loads the memory package, creates a
|
|
637
|
-
// directory, takes the write lock or calls shutdown, so it cannot change the store.
|
|
638
|
-
const READ_SCHEMA_VERSION = 1;
|
|
639
|
-
async function doRead(namespace, key) {
|
|
640
|
-
const out = { schemaVersion: READ_SCHEMA_VERSION, store: STORE_PATH, storePresent: false, namespace: namespace ?? null, key: key ?? null, found: false, entry: null };
|
|
641
|
-
const refuse = (code, message) => {
|
|
642
|
-
out.error = { code, message };
|
|
643
|
-
console.log(JSON.stringify(out, null, 2));
|
|
644
|
-
return 1;
|
|
645
|
-
};
|
|
646
|
-
if (typeof namespace !== 'string' || !namespace || typeof key !== 'string' || !key) {
|
|
647
|
-
return refuse('USAGE', 'usage: auto-memory-hook.mjs read <namespace> <key>');
|
|
648
|
-
}
|
|
649
|
-
const backend = new JsonFileBackend(STORE_PATH);
|
|
650
|
-
try {
|
|
651
|
-
await backend.initialize();
|
|
652
|
-
} catch (err) {
|
|
653
|
-
out.storePresent = true; // initialize() only throws for a file that exists
|
|
654
|
-
return refuse(err.code || 'STORE_READ_FAILED', err.message);
|
|
655
|
-
}
|
|
656
|
-
out.storePresent = backend.storePresent;
|
|
657
|
-
const matches = await backend.query({ namespace, key });
|
|
658
|
-
if (matches.length > 1) return refuse('AMBIGUOUS_KEY', `${matches.length} entries share namespace ${JSON.stringify(namespace)} and key ${JSON.stringify(key)}`);
|
|
659
|
-
if (matches.length === 1) { out.found = true; out.entry = matches[0]; }
|
|
660
|
-
console.log(JSON.stringify(out, null, 2));
|
|
661
|
-
return 0;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
502
|
// ============================================================================
|
|
665
503
|
// Main
|
|
666
504
|
// ============================================================================
|
|
@@ -668,10 +506,8 @@ async function doRead(namespace, key) {
|
|
|
668
506
|
// Older settings.json files run `node -e "…import(file)" import`, where the
|
|
669
507
|
// subcommand lands in argv[1] and argv[2] is empty, so both hooks fell through
|
|
670
508
|
// to `status` and never imported or synced (#4058). Accept that shape too.
|
|
671
|
-
const COMMANDS = ['import', 'sync', 'status'
|
|
672
|
-
const argvBase = process.argv[2] ? 2 : 1;
|
|
509
|
+
const COMMANDS = ['import', 'sync', 'status'];
|
|
673
510
|
const command = process.argv[2] || (COMMANDS.includes(process.argv[1]) ? process.argv[1] : 'status');
|
|
674
|
-
const commandArgs = process.argv.slice(argvBase + 1);
|
|
675
511
|
|
|
676
512
|
// Dynamic import() failures can surface as unhandled rejections on a later
|
|
677
513
|
// microtask even when the awaiting call site already caught them, which would
|
|
@@ -685,25 +521,18 @@ process.on('unhandledRejection', (reason) => {
|
|
|
685
521
|
}
|
|
686
522
|
});
|
|
687
523
|
|
|
688
|
-
// A failed import or sync exits 1, never 2: Claude Code treats exit 2 from a
|
|
689
|
-
// hook as "block", while exit 1 is a non-blocking error it shows to the user,
|
|
690
|
-
// so the session carries on and the failure is no longer hidden (#4685).
|
|
691
|
-
let exitCode = 0;
|
|
692
524
|
try {
|
|
693
525
|
switch (command) {
|
|
694
526
|
case 'import': await doImport(); break;
|
|
695
527
|
case 'sync': await doSync(); break;
|
|
696
528
|
case 'status': await doStatus(); break;
|
|
697
|
-
case 'read': exitCode = await doRead(commandArgs[0], commandArgs[1]); break;
|
|
698
529
|
default:
|
|
699
|
-
console.log('Usage: auto-memory-hook.mjs <import|sync|status
|
|
530
|
+
console.log('Usage: auto-memory-hook.mjs <import|sync|status>');
|
|
700
531
|
break;
|
|
701
532
|
}
|
|
702
533
|
} catch (err) {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const line = err instanceof HookFailure ? `[AutoMemory] ${detail}` : `[AutoMemory] ${command} failed: ${detail}`;
|
|
706
|
-
try { process.stderr.write(`${line}\n`); } catch (_) {}
|
|
534
|
+
// Hooks must never crash Claude Code - fail silently
|
|
535
|
+
try { dim(`Error (non-critical): ${err.message}`); } catch (_) {}
|
|
707
536
|
}
|
|
708
|
-
// Force exit — process.exitCode alone isn't enough if async errors override it
|
|
709
|
-
process.exit(
|
|
537
|
+
// Force clean exit — process.exitCode alone isn't enough if async errors override it
|
|
538
|
+
process.exit(0);
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest": {
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.74.0",
|
|
4
4
|
"files": {
|
|
5
|
-
"auto-memory-hook.mjs": "
|
|
6
|
-
"hook-handler.cjs": "
|
|
7
|
-
"intelligence.cjs": "
|
|
8
|
-
"statusline.cjs": "
|
|
5
|
+
"auto-memory-hook.mjs": "631fac9d518835e2852a406888316bbbddddb49e7ed8e406d2e971f46833df9a",
|
|
6
|
+
"hook-handler.cjs": "24502a3bcb78770971147940515c992e35b6736ae14ee2dc54e07d4cdef6a1d8",
|
|
7
|
+
"intelligence.cjs": "1d981d6c6a35d70a66450e96458e26169e0cede11523ea246a2c129c197a5b4e",
|
|
8
|
+
"statusline.cjs": "d72417ecb38634011a19f095a30e07b9929e6f97663520172ece9be7491b2eaa",
|
|
9
9
|
"router.cjs": "16648b3c0d73b6681ac2d8d13def6a68286c57f5a11004de5b19a0fa088c833d",
|
|
10
10
|
"learning-loop.cjs": "d046cd6f8efb7dd40e1d2451516c181fc8b26477aefb1124c0df5f52a23f9190"
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
|
-
"signature": "
|
|
13
|
+
"signature": "OSR1FztpUZYdZGE3tNQPgLpD8WZWfd+UljXhcNdJoX7nPfA5OJpWHDRo1NRzSJyiNGowIXcXoiqdspnRM9owBg==",
|
|
14
14
|
"algorithm": "ed25519"
|
|
15
15
|
}
|
|
@@ -647,8 +647,6 @@ const router = safeRequire(path.join(helpersDir, 'router.cjs'));
|
|
|
647
647
|
const session = safeRequire(path.join(helpersDir, 'session.cjs'));
|
|
648
648
|
const memory = safeRequire(path.join(helpersDir, 'memory.cjs'));
|
|
649
649
|
const intelligence = safeRequire(path.join(helpersDir, 'intelligence.cjs'));
|
|
650
|
-
// ADR-512: the verified-outcome learning loop, bundled from the ruflo-mods loop source.
|
|
651
|
-
const learningLoop = safeRequire(path.join(helpersDir, 'learning-loop.cjs'));
|
|
652
650
|
|
|
653
651
|
// ── Intelligence timeout protection (fixes #1530, #1531) ───────────────────
|
|
654
652
|
const INTELLIGENCE_TIMEOUT_MS = 3000;
|
|
@@ -682,7 +680,7 @@ const [,, command, ...args] = process.argv;
|
|
|
682
680
|
|
|
683
681
|
// Read stdin with timeout — Claude Code sends hook data as JSON via stdin.
|
|
684
682
|
// Timeout prevents hanging when stdin is not properly closed (common on Windows).
|
|
685
|
-
async function readStdin(
|
|
683
|
+
async function readStdin() {
|
|
686
684
|
if (process.stdin.isTTY) return '';
|
|
687
685
|
return new Promise((resolve) => {
|
|
688
686
|
let data = '';
|
|
@@ -690,7 +688,7 @@ async function readStdin(windowMs = 500) {
|
|
|
690
688
|
process.stdin.removeAllListeners();
|
|
691
689
|
process.stdin.pause();
|
|
692
690
|
resolve(data);
|
|
693
|
-
},
|
|
691
|
+
}, 500);
|
|
694
692
|
process.stdin.setEncoding('utf8');
|
|
695
693
|
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
696
694
|
process.stdin.on('end', () => { clearTimeout(timer); resolve(data); });
|
|
@@ -705,50 +703,6 @@ async function readStdin(windowMs = 500) {
|
|
|
705
703
|
// keeps it from firing twice. Only side-effect events can be handed over:
|
|
706
704
|
// guards such as pre-bash always run, whatever the variable says.
|
|
707
705
|
const MOD_OWNABLE_EVENTS = new Set(['route', 'post-edit']);
|
|
708
|
-
|
|
709
|
-
// ADR-512: classic hooks run the verified-outcome learning loop for users without the mod. A
|
|
710
|
-
// mod that records turns itself says so with `learning-loop` (or, before ADR-512, owns
|
|
711
|
-
// `route`), so a turn is never recorded twice.
|
|
712
|
-
function modRecordsTurns(env = process.env) {
|
|
713
|
-
return String(env.RUFLO_MODS_OWNS || '').split(',').some((owned) => ['learning-loop', 'route'].includes(owned.trim()));
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
function learningLoopRoot() {
|
|
717
|
-
return process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// Records the turns the session transcript gained since the last look (all of them when the
|
|
721
|
-
// session ends). Returns the number recorded; never throws.
|
|
722
|
-
function recordLearningTurns(hookInput, final) {
|
|
723
|
-
try {
|
|
724
|
-
const root = learningLoopRoot();
|
|
725
|
-
if (!learningLoop || modRecordsTurns() || !learningLoop.enabled(root)) return 0;
|
|
726
|
-
const agentFor = (text) => {
|
|
727
|
-
try { return (router && router.routeTask && router.routeTask(text).agent) || 'coder'; } catch (e) { return 'coder'; }
|
|
728
|
-
};
|
|
729
|
-
return learningLoop.recordTurns({
|
|
730
|
-
root,
|
|
731
|
-
transcriptPath: hookInput.transcript_path || hookInput.transcriptPath,
|
|
732
|
-
session: hookInput.session_id || hookInput.sessionId,
|
|
733
|
-
final,
|
|
734
|
-
agentFor,
|
|
735
|
-
});
|
|
736
|
-
} catch (e) { return 0; }
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
// Trains SONA from the recorded turns in a detached `ruflo hooks verified-ingest` (a hook has
|
|
740
|
-
// seconds; an ingest may take longer). At most one spawn a minute per project.
|
|
741
|
-
function spawnLearningIngest() {
|
|
742
|
-
try {
|
|
743
|
-
const root = learningLoopRoot();
|
|
744
|
-
if (!learningLoop || !learningLoop.enabled(root) || !learningLoop.ingestDue(root)) return;
|
|
745
|
-
const stamp = path.join(root, '.claude-flow', 'data', 'verified-ingest.spawned');
|
|
746
|
-
try { if (Date.now() - fs.statSync(stamp).mtimeMs < 60 * 1000) return; } catch (e) { /* never spawned */ }
|
|
747
|
-
fs.mkdirSync(path.dirname(stamp), { recursive: true });
|
|
748
|
-
fs.writeFileSync(stamp, String(Date.now()));
|
|
749
|
-
spawnDetachedHookRefresh('verified-ingest');
|
|
750
|
-
} catch (e) { /* best-effort only */ }
|
|
751
|
-
}
|
|
752
706
|
function ownedByMod(cmd, env = process.env) {
|
|
753
707
|
if (!MOD_OWNABLE_EVENTS.has(cmd)) return false;
|
|
754
708
|
return String(env.RUFLO_MODS_OWNS || '').split(',').some((owned) => owned.trim() === cmd);
|
|
@@ -779,42 +733,22 @@ function claimSideEffectEvent(family, stdinData, event) {
|
|
|
779
733
|
}
|
|
780
734
|
}
|
|
781
735
|
|
|
782
|
-
// pre-bash is a gate (#4743): a path that cannot vouch for the command blocks it.
|
|
783
|
-
function blockPreBash(reason) {
|
|
784
|
-
console.error(`[BLOCKED] ${reason}`);
|
|
785
|
-
process.exit(2);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
736
|
async function main() {
|
|
789
737
|
if (ownedByMod(command)) return;
|
|
790
738
|
|
|
791
739
|
// Global safety timeout: hooks must NEVER hang (#1530, #1531)
|
|
792
740
|
const safetyTimer = setTimeout(() => {
|
|
793
741
|
process.stderr.write("[WARN] Hook handler global timeout (5s), forcing exit\n");
|
|
794
|
-
if (command === 'pre-bash') blockPreBash('pre-bash timed out before the command was validated');
|
|
795
742
|
process.exit(0);
|
|
796
743
|
}, 5000);
|
|
797
744
|
safetyTimer.unref(); // don't keep process alive just for this timer
|
|
798
745
|
|
|
799
746
|
let stdinData = '';
|
|
800
|
-
|
|
801
|
-
// wider window. The read ends at EOF, so a prompt host waits no longer.
|
|
802
|
-
try { stdinData = await readStdin(command === 'pre-bash' ? 3000 : 500); } catch (e) { /* ignore stdin errors */ }
|
|
747
|
+
try { stdinData = await readStdin(); } catch (e) { /* ignore stdin errors */ }
|
|
803
748
|
|
|
804
749
|
let hookInput = {};
|
|
805
|
-
let stdinUnparsable = false;
|
|
806
750
|
if (stdinData.trim()) {
|
|
807
|
-
try { hookInput = JSON.parse(stdinData); } catch (e) {
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// ADR-512: record what finished and train from it, before the dedup below: it keys session-end
|
|
811
|
-
// on the session id, so after a PreCompact the real SessionEnd would never record the last turn.
|
|
812
|
-
// Recording is idempotent (a per-session transcript offset, ids from transcript positions).
|
|
813
|
-
// PreCompact runs mid-session, where the open turn may still edit and verify: only SessionEnd
|
|
814
|
-
// closes it.
|
|
815
|
-
if (command === 'session-end') {
|
|
816
|
-
recordLearningTurns(hookInput, (hookInput.hook_event_name || hookInput.hookEventName) !== 'PreCompact');
|
|
817
|
-
spawnLearningIngest();
|
|
751
|
+
try { hookInput = JSON.parse(stdinData); } catch (e) { /* ignore parse errors */ }
|
|
818
752
|
}
|
|
819
753
|
|
|
820
754
|
if ((command === 'post-edit' || command === 'session-end') &&
|
|
@@ -855,16 +789,6 @@ async function main() {
|
|
|
855
789
|
|
|
856
790
|
const handlers = {
|
|
857
791
|
'route': () => {
|
|
858
|
-
// ADR-512: record the turns that finished, then cite the verified lessons that fit this prompt.
|
|
859
|
-
recordLearningTurns(hookInput, false);
|
|
860
|
-
if (learningLoop && !modRecordsTurns()) {
|
|
861
|
-
try {
|
|
862
|
-
if (learningLoop.enabled(learningLoopRoot())) {
|
|
863
|
-
const block = learningLoop.lessonsForPrompt(learningLoopRoot(), prompt);
|
|
864
|
-
if (block) console.log(block);
|
|
865
|
-
}
|
|
866
|
-
} catch (e) { /* non-fatal */ }
|
|
867
|
-
}
|
|
868
792
|
// Inject ranked intelligence context before routing
|
|
869
793
|
// ADR-472: keep the detailed recall so ONE bounded log record (ids, scores, router pick; never the prompt) is appended after routing.
|
|
870
794
|
let recall = null;
|
|
@@ -964,12 +888,11 @@ const handlers = {
|
|
|
964
888
|
|
|
965
889
|
'pre-bash': () => {
|
|
966
890
|
// Basic command safety check — prefer stdin command data from Claude Code.
|
|
967
|
-
//
|
|
968
|
-
//
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
const cmd = raw.toLowerCase();
|
|
891
|
+
// String() wrap is belt-and-suspenders for #2017: even if a future regression
|
|
892
|
+
// re-binds `prompt` or `hookInput.command` to a non-string, `.toLowerCase()`
|
|
893
|
+
// can no longer throw a TypeError that the global try/catch would swallow
|
|
894
|
+
// (silently exiting 0 and letting the dangerous command through).
|
|
895
|
+
const cmd = String(hookInput.command || toolInput.command || prompt || '').toLowerCase();
|
|
973
896
|
const dangerous = ['rm -rf /', 'format c:', 'del /s /q c:\\', ':(){:|:&};:'];
|
|
974
897
|
// Join literal quote fragments and normalize root paths without shell evaluation.
|
|
975
898
|
function hasRootDelete(command, depth = 0) {
|
|
@@ -1319,7 +1242,6 @@ const handlers = {
|
|
|
1319
1242
|
try {
|
|
1320
1243
|
await Promise.resolve(handlers[command]());
|
|
1321
1244
|
} catch (e) {
|
|
1322
|
-
if (command === 'pre-bash') blockPreBash(`pre-bash failed: ${e && e.message}`);
|
|
1323
1245
|
// Hooks should never crash Claude Code - fail silently
|
|
1324
1246
|
console.log(`[WARN] Hook ${command} encountered an error: ${e.message}`);
|
|
1325
1247
|
}
|
|
@@ -1331,7 +1253,7 @@ const handlers = {
|
|
|
1331
1253
|
}
|
|
1332
1254
|
}
|
|
1333
1255
|
|
|
1334
|
-
// Non-blocking hooks exit 0; pre-bash denials
|
|
1256
|
+
// Non-blocking hooks exit 0; pre-bash denials exit 2 to block PreToolUse.
|
|
1335
1257
|
// A denial exits immediately, before the normal completion path below.
|
|
1336
1258
|
//
|
|
1337
1259
|
// Only dispatch when run directly (node hook-handler.cjs <cmd>). When
|
|
@@ -1343,7 +1265,6 @@ const handlers = {
|
|
|
1343
1265
|
if (require.main === module) {
|
|
1344
1266
|
process.exitCode = 0;
|
|
1345
1267
|
main().catch((e) => {
|
|
1346
|
-
if (command === 'pre-bash') blockPreBash(`pre-bash failed: ${e && e.message}`);
|
|
1347
1268
|
try { console.log(`[WARN] Hook handler error: ${e.message}`); } catch (_) {}
|
|
1348
1269
|
}).finally(() => {
|
|
1349
1270
|
process.exit(0);
|