@claude-flow/cli 3.73.0 → 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.
Files changed (31) hide show
  1. package/.claude/helpers/.helpers-version +1 -1
  2. package/.claude/helpers/auto-memory-hook.mjs +53 -224
  3. package/.claude/helpers/helpers.manifest.json +6 -6
  4. package/.claude/helpers/hook-handler.cjs +10 -89
  5. package/.claude/helpers/intelligence.cjs +1 -23
  6. package/.claude/helpers/statusline.cjs +14 -29
  7. package/catalog-manifest.json +2 -2
  8. package/dist/src/commands/doctor.js +5 -3
  9. package/dist/src/mcp-tools/memory-tools.js +17 -0
  10. package/dist/src/mcp-tools/metaharness-tools.js +19 -0
  11. package/dist/src/memory/memory-bridge.d.ts +2 -0
  12. package/dist/src/memory/memory-bridge.js +1 -1
  13. package/dist/src/memory/memory-initializer.d.ts +2 -0
  14. package/dist/src/memory/memory-initializer.js +1 -1
  15. package/dist/src/metaharness/darwin-lib.d.ts +68 -0
  16. package/dist/src/metaharness/darwin-lib.js +150 -0
  17. package/dist/src/missions/schemas.d.ts +117 -117
  18. package/dist/src/mods/command-registry/schema.d.ts +121 -121
  19. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  20. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  21. package/node_modules/@claude-flow/security/dist/input-validator.d.ts +138 -106
  22. package/node_modules/@claude-flow/security/dist/input-validator.d.ts.map +1 -1
  23. package/node_modules/@claude-flow/security/dist/input-validator.js +23 -26
  24. package/node_modules/@claude-flow/security/dist/input-validator.js.map +1 -1
  25. package/node_modules/@claude-flow/security/package.json +2 -2
  26. package/package.json +6 -6
  27. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -1
  28. package/plugins/ruflo-metaharness/CHANGELOG.md +3 -0
  29. package/plugins/ruflo-metaharness/scripts/_darwin.mjs +13 -0
  30. package/plugins/ruflo-metaharness/scripts/bench.mjs +34 -2
  31. package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -4
@@ -1 +1 @@
1
- 3.57.1
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
- // 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 }); }
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
- // 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');
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._track(entry); this._persist(); }
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._forget(id); }
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._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; }
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 [...this.entries]) {
314
- if (e.namespace === ns) { this._forget(id); n++; }
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
- 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
- });
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 backend.shutdown();
513
- verifyStoreFile(STORE_PATH, backend.written, backend.deletedIds, sameRecord);
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
- failures.push(`persist: ${err.message}`);
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
- // A store committed to the repository came from whoever wrote the repo. Its
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
- syncError = err;
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
- try {
627
- const data = readStoreFile(STORE_PATH);
628
- if (data) console.log(` Entries: ${data.length}`);
629
- } catch (err) {
630
- console.log(` Entries: ❌ ${err.message}`);
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', 'read'];
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|read <namespace> <key>>');
530
+ console.log('Usage: auto-memory-hook.mjs <import|sync|status>');
700
531
  break;
701
532
  }
702
533
  } catch (err) {
703
- exitCode = 1;
704
- const detail = err && err.message ? err.message : String(err);
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(exitCode);
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.73.0",
3
+ "version": "3.74.0",
4
4
  "files": {
5
- "auto-memory-hook.mjs": "6078bd1c6b02dd3f80097f8d7f28904d48b3cdf7a534d2aa13966590694ca403",
6
- "hook-handler.cjs": "31db0f7492fbd8b2e48ea9adbc1ba293dfed1e006666d20e93ea76e158986947",
7
- "intelligence.cjs": "ffdcfa6d2665a16e417dedd5bc67080c8192820f76265de5013fcb49f668ec71",
8
- "statusline.cjs": "284b95629ced4340667872b977de3f2beac90fc6bb7ab1a62dd7f7ae75c5e5d1",
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": "JPs8pGUFfkmmIj+GpqM7snJ1AX9IJS8olXiwHgZjSTKCIcK0aERAd+Wjy3jfBbK1V8mqP+ZWeX8CRr4aA+3BDA==",
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(windowMs = 500) {
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
- }, windowMs);
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
- // pre-bash blocks when it gets no command (#4743), so a slow host gets a
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) { stdinUnparsable = true; }
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
- // Only a non-empty string counts as the command (#2017, #4743): input that
968
- // arrived late, did not parse, or carried no command blocks instead of passing.
969
- if (stdinUnparsable) blockPreBash('pre-bash could not parse the hook input');
970
- const raw = [hookInput.command, toolInput.command, prompt].find((c) => typeof c === 'string' && c.trim());
971
- if (!raw) blockPreBash('pre-bash received no command to validate');
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 and failures exit 2 to block PreToolUse.
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);