claude-flow 3.21.0 → 3.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/v3/@claude-flow/cli/dist/src/commands/daemon.js +23 -2
  3. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +56 -0
  4. package/v3/@claude-flow/cli/dist/src/commands/memory-distill.d.ts +27 -0
  5. package/v3/@claude-flow/cli/dist/src/commands/memory-distill.js +374 -0
  6. package/v3/@claude-flow/cli/dist/src/commands/memory.js +4 -2
  7. package/v3/@claude-flow/cli/dist/src/index.d.ts +1 -1
  8. package/v3/@claude-flow/cli/dist/src/index.js +22 -1
  9. package/v3/@claude-flow/cli/dist/src/init/executor.js +37 -0
  10. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +19 -0
  11. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +175 -0
  12. package/v3/@claude-flow/cli/dist/src/init/helper-signing.d.ts +30 -0
  13. package/v3/@claude-flow/cli/dist/src/init/helper-signing.js +60 -0
  14. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.d.ts +16 -0
  15. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +89 -8
  16. package/v3/@claude-flow/cli/dist/src/init/memory-package-resolver.d.ts +53 -0
  17. package/v3/@claude-flow/cli/dist/src/init/memory-package-resolver.js +118 -0
  18. package/v3/@claude-flow/cli/dist/src/init/statusline-generator.js +18 -7
  19. package/v3/@claude-flow/cli/dist/src/mcp-client.js +13 -0
  20. package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-speculate-tools.js +9 -2
  21. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.d.ts +162 -0
  22. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.js +548 -0
  23. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.d.ts +9 -1
  24. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.js +4 -1
  25. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +115 -54
  26. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +71 -0
  27. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +330 -2
  28. package/v3/@claude-flow/cli/dist/src/services/distill-tuning.d.ts +111 -0
  29. package/v3/@claude-flow/cli/dist/src/services/distill-tuning.js +510 -0
  30. package/v3/@claude-flow/cli/dist/src/services/memory-distillation.d.ts +41 -0
  31. package/v3/@claude-flow/cli/dist/src/services/memory-distillation.js +277 -0
  32. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +22 -1
  33. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +117 -6
  34. package/v3/@claude-flow/cli/package.json +5 -2
@@ -15,7 +15,9 @@ import { generateSettingsJson, generateSettings } from './settings-generator.js'
15
15
  import { generateMCPJson } from './mcp-generator.js';
16
16
  import { generateStatuslineScript } from './statusline-generator.js';
17
17
  import { generatePreCommitHook, generatePostCommitHook, generateSessionManager, generateAgentRouter, generateMemoryHelper, generateHookHandler, generateIntelligenceStub, generateAutoMemoryHook, generateRufloHookCjs, } from './helpers-generator.js';
18
+ import { getInstalledCliVersion, HELPERS_STAMP_FILE } from './helper-refresh.js';
18
19
  import { generateClaudeMd } from './claudemd-generator.js';
20
+ import { recordMemoryPackagePath } from './memory-package-resolver.js';
19
21
  /**
20
22
  * Skills to copy based on configuration
21
23
  */
@@ -196,6 +198,29 @@ export async function executeInit(options) {
196
198
  // Generate helpers
197
199
  if (options.components.helpers) {
198
200
  await writeHelpers(targetDir, options, result);
201
+ // #2545: The auto-memory hook needs @claude-flow/memory, which is an
202
+ // optionalDependency of the CLI and lands in the npx cache — unreachable
203
+ // by a node_modules walk-up from the user's project. Resolve it from the
204
+ // CLI's own context now (where it IS present) and record the absolute
205
+ // path in a machine-local sidecar the hook reads first. Best-effort:
206
+ // when the optional dep is absent the hook fails loud and doctor flags it.
207
+ const memRecord = recordMemoryPackagePath(targetDir, 'init');
208
+ if (memRecord) {
209
+ result.created.files.push('.claude-flow/memory-package.json');
210
+ }
211
+ // #2568-followup: if this project already has a memory DB that predates
212
+ // the `vector_indexes` table (older CLI / agentdb-written), self-heal it
213
+ // so the statusline vector count + namespace routing work. Best-effort,
214
+ // dynamically imported so a WASM-only host without better-sqlite3 just
215
+ // skips it. Fresh projects have no DB yet — this is a no-op there.
216
+ try {
217
+ const memDbPath = path.join(targetDir, '.swarm', 'memory.db');
218
+ if (fs.existsSync(memDbPath)) {
219
+ const { repairVectorIndexes } = await import('../memory/memory-initializer.js');
220
+ await repairVectorIndexes(memDbPath, { autoRecover: true });
221
+ }
222
+ }
223
+ catch { /* best-effort — never block init on memory repair */ }
199
224
  }
200
225
  // Generate statusline
201
226
  if (options.components.statusline) {
@@ -472,6 +497,18 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
472
497
  catch { }
473
498
  }
474
499
  }
500
+ // Stamp the installed version so the startup auto-refresh treats these as
501
+ // current (no redundant re-copy on the next command).
502
+ try {
503
+ fs.writeFileSync(path.join(targetDir, '.claude', 'helpers', HELPERS_STAMP_FILE), getInstalledCliVersion(), 'utf-8');
504
+ }
505
+ catch { /* non-fatal */ }
506
+ // #2545: (re)record the resolved @claude-flow/memory path so the auto-memory
507
+ // hook can find it on the npx install path. Best-effort — see executeInit.
508
+ const memRecord = recordMemoryPackagePath(targetDir, 'upgrade');
509
+ if (memRecord) {
510
+ result.updated.push('.claude-flow/memory-package.json');
511
+ }
475
512
  // 1. ALWAYS update statusline helper (force overwrite)
476
513
  const statuslinePath = path.join(targetDir, '.claude', 'helpers', 'statusline.cjs');
477
514
  // Use default options with statusline config
@@ -0,0 +1,19 @@
1
+ export declare const HELPERS_STAMP_FILE = ".helpers-version";
2
+ /** ruflo-owned helpers that carry hook logic and must track the package version. */
3
+ export declare const CRITICAL_HELPERS: string[];
4
+ /** Installed @claude-flow/cli version — the value the helpers are stamped with. */
5
+ export declare function getInstalledCliVersion(): string;
6
+ /**
7
+ * On CLI startup: if an initialized project's critical helpers are stamped older
8
+ * than the installed CLI version, silently re-copy them. Fast path is a single
9
+ * stamp read + string compare (sub-ms); the copy runs at most once per version
10
+ * bump. Best-effort, never throws. No-op outside a ruflo project (requires an
11
+ * existing hook-handler.cjs — never creates files in an unrelated directory).
12
+ */
13
+ export declare function autoRefreshHelpersIfStale(cwd: string): Promise<{
14
+ refreshed: boolean;
15
+ from?: string;
16
+ to?: string;
17
+ blocked?: string;
18
+ }>;
19
+ //# sourceMappingURL=helper-refresh.d.ts.map
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Version-stamped critical-helper auto-refresh.
3
+ *
4
+ * The Claude Code hooks run the PROJECT-LOCAL `.claude/helpers/*.cjs` copies,
5
+ * not the installed npm package — so `npx ruflo@latest` does NOT update them,
6
+ * and users don't know to re-run `init`. This module stamps the helpers with
7
+ * the installed CLI version and, on the next CLI command, silently re-copies
8
+ * them when the stamp is stale. Hook fixes (e.g. the ADR-174 failure-capture
9
+ * change) then propagate to every user on their next `ruflo` command with zero
10
+ * action required.
11
+ *
12
+ * This file is intentionally LIGHTWEIGHT — it is imported on every CLI startup,
13
+ * so it depends only on `fs`/`path`/`module` at load time and lazily imports the
14
+ * heavy generators only on the rare fallback path (source dir unresolvable).
15
+ */
16
+ import * as fs from 'fs';
17
+ import * as path from 'path';
18
+ import { fileURLToPath } from 'url';
19
+ import { createRequire } from 'module';
20
+ import { verifyHelpersManifest, sha256Hex, HELPERS_MANIFEST_FILE, } from './helper-signing.js';
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+ export const HELPERS_STAMP_FILE = '.helpers-version';
23
+ /** ruflo-owned helpers that carry hook logic and must track the package version. */
24
+ export const CRITICAL_HELPERS = ['auto-memory-hook.mjs', 'hook-handler.cjs', 'intelligence.cjs'];
25
+ /** Installed @claude-flow/cli version — the value the helpers are stamped with. */
26
+ export function getInstalledCliVersion() {
27
+ try {
28
+ const esmRequire = createRequire(import.meta.url);
29
+ const pkg = JSON.parse(fs.readFileSync(esmRequire.resolve('@claude-flow/cli/package.json'), 'utf-8'));
30
+ return String(pkg.version || '0.0.0');
31
+ }
32
+ catch {
33
+ // dist/src/init → package root
34
+ try {
35
+ const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', '..', '..', 'package.json'), 'utf-8'));
36
+ return String(pkg.version || '0.0.0');
37
+ }
38
+ catch {
39
+ return '0.0.0';
40
+ }
41
+ }
42
+ }
43
+ /** Locate the in-package `.claude/helpers` dir (the copy source). Null if not found. */
44
+ function findPackageHelpersDir() {
45
+ const candidates = [];
46
+ try {
47
+ const esmRequire = createRequire(import.meta.url);
48
+ const pkgRoot = path.dirname(esmRequire.resolve('@claude-flow/cli/package.json'));
49
+ candidates.push(path.join(pkgRoot, '.claude', 'helpers'));
50
+ }
51
+ catch { /* not resolvable */ }
52
+ candidates.push(path.resolve(__dirname, '..', '..', '..', '.claude', 'helpers'));
53
+ for (const c of candidates) {
54
+ if (fs.existsSync(path.join(c, 'hook-handler.cjs')))
55
+ return c;
56
+ }
57
+ return null;
58
+ }
59
+ /**
60
+ * Re-copy the critical helpers into `helpersDir` and stamp `version`.
61
+ *
62
+ * SECURITY (fail-closed): when copying from the installed package, every source
63
+ * helper is verified against ruflo's Ed25519-signed manifest FIRST — nothing is
64
+ * copied unless the manifest signature is valid AND each helper's SHA-256
65
+ * matches. A tampered helper or manifest (e.g. a sibling package's postinstall
66
+ * overwriting on-disk hook code) is REFUSED, not propagated. The generator
67
+ * fallback needs no manifest — that content comes from the CLI's own compiled
68
+ * code, which is already the trust root.
69
+ */
70
+ async function writeCriticalHelpers(helpersDir, version) {
71
+ const source = findPackageHelpersDir();
72
+ if (source) {
73
+ // 1. Verify the signed manifest against the baked public key.
74
+ let trusted = null;
75
+ try {
76
+ trusted = verifyHelpersManifest(fs.readFileSync(path.join(source, HELPERS_MANIFEST_FILE), 'utf-8'));
77
+ }
78
+ catch {
79
+ trusted = null;
80
+ }
81
+ if (!trusted)
82
+ return { wrote: false, blocked: 'signed helpers manifest missing or signature invalid' };
83
+ // 2. Verify EVERY source helper's hash before copying ANYTHING (atomic gate).
84
+ const toCopy = [];
85
+ for (const name of CRITICAL_HELPERS) {
86
+ const sp = path.join(source, name);
87
+ if (!fs.existsSync(sp))
88
+ continue;
89
+ const expected = trusted.files[name];
90
+ if (!expected || sha256Hex(fs.readFileSync(sp)) !== expected) {
91
+ return { wrote: false, blocked: `integrity check failed for ${name} — refusing to install` };
92
+ }
93
+ toCopy.push(name);
94
+ }
95
+ // 3. All verified — copy, plus the signed manifest itself as an audit trail.
96
+ let wrote = false;
97
+ for (const name of toCopy) {
98
+ const tp = path.join(helpersDir, name);
99
+ fs.copyFileSync(path.join(source, name), tp);
100
+ try {
101
+ fs.chmodSync(tp, '755');
102
+ }
103
+ catch { /* non-fatal */ }
104
+ wrote = true;
105
+ }
106
+ try {
107
+ fs.copyFileSync(path.join(source, HELPERS_MANIFEST_FILE), path.join(helpersDir, HELPERS_MANIFEST_FILE));
108
+ }
109
+ catch { /* non-fatal */ }
110
+ if (wrote) {
111
+ try {
112
+ fs.writeFileSync(path.join(helpersDir, HELPERS_STAMP_FILE), version, 'utf-8');
113
+ }
114
+ catch { /* non-fatal */ }
115
+ }
116
+ return { wrote };
117
+ }
118
+ // Fallback: source unresolvable (broken npx paths) — regenerate from the CLI's
119
+ // OWN compiled generators (the trust root; no external file to verify).
120
+ const gen = await import('./helpers-generator.js');
121
+ const files = {
122
+ 'hook-handler.cjs': gen.generateHookHandler(),
123
+ 'intelligence.cjs': gen.generateIntelligenceStub(),
124
+ 'auto-memory-hook.mjs': gen.generateAutoMemoryHook(),
125
+ };
126
+ let wrote = false;
127
+ for (const [name, content] of Object.entries(files)) {
128
+ const tp = path.join(helpersDir, name);
129
+ fs.writeFileSync(tp, content, 'utf-8');
130
+ try {
131
+ fs.chmodSync(tp, '755');
132
+ }
133
+ catch { /* non-fatal */ }
134
+ wrote = true;
135
+ }
136
+ if (wrote) {
137
+ try {
138
+ fs.writeFileSync(path.join(helpersDir, HELPERS_STAMP_FILE), version, 'utf-8');
139
+ }
140
+ catch { /* non-fatal */ }
141
+ }
142
+ return { wrote };
143
+ }
144
+ /**
145
+ * On CLI startup: if an initialized project's critical helpers are stamped older
146
+ * than the installed CLI version, silently re-copy them. Fast path is a single
147
+ * stamp read + string compare (sub-ms); the copy runs at most once per version
148
+ * bump. Best-effort, never throws. No-op outside a ruflo project (requires an
149
+ * existing hook-handler.cjs — never creates files in an unrelated directory).
150
+ */
151
+ export async function autoRefreshHelpersIfStale(cwd) {
152
+ try {
153
+ const helpersDir = path.join(cwd, '.claude', 'helpers');
154
+ if (!fs.existsSync(path.join(helpersDir, 'hook-handler.cjs')))
155
+ return { refreshed: false };
156
+ const version = getInstalledCliVersion();
157
+ let stamped = '';
158
+ try {
159
+ stamped = fs.readFileSync(path.join(helpersDir, HELPERS_STAMP_FILE), 'utf-8').trim();
160
+ }
161
+ catch { /* pre-feature: unstamped */ }
162
+ if (stamped === version)
163
+ return { refreshed: false }; // up to date — fast path
164
+ const res = await writeCriticalHelpers(helpersDir, version);
165
+ // A blocked refresh is a SECURITY signal (tampered source/manifest) — surface
166
+ // it, don't advance the stamp, and leave the project's existing helpers intact.
167
+ if (res.blocked)
168
+ return { refreshed: false, blocked: res.blocked };
169
+ return res.wrote ? { refreshed: true, from: stamped || '(unstamped)', to: version } : { refreshed: false };
170
+ }
171
+ catch {
172
+ return { refreshed: false };
173
+ }
174
+ }
175
+ //# sourceMappingURL=helper-refresh.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Ruflo helper-signing PUBLIC key (safe to commit). The matching private key is
3
+ * held out-of-repo and provided to scripts/sign-helpers.mjs at publish time via
4
+ * $RUFLO_HELPERS_SIGNING_KEY. Rotating the key = replace this constant + re-sign.
5
+ */
6
+ export declare const RUFLO_HELPERS_PUBKEY = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAhnFv74/CRcGWd0hL8zjyZ+52bIJ9SfcSgOutuKgo0Vg=\n-----END PUBLIC KEY-----";
7
+ export declare const HELPERS_MANIFEST_FILE = "helpers.manifest.json";
8
+ export interface HelpersManifest {
9
+ version: string;
10
+ files: Record<string, string>;
11
+ }
12
+ export interface SignedHelpersManifest {
13
+ manifest: HelpersManifest;
14
+ signature: string;
15
+ algorithm: 'ed25519';
16
+ }
17
+ export declare function sha256Hex(content: string | Buffer): string;
18
+ /**
19
+ * Deterministic canonical bytes of a manifest — file keys sorted so the signer
20
+ * and verifier agree byte-for-byte regardless of object insertion order.
21
+ */
22
+ export declare function canonicalManifestBytes(m: HelpersManifest): Buffer;
23
+ /**
24
+ * Verify a signed helpers manifest against ruflo's public key. Returns the
25
+ * trusted file->sha256 manifest, or null on ANY failure (bad signature,
26
+ * malformed JSON, wrong algorithm). Fail-closed — the caller MUST refuse to
27
+ * install unverified helpers.
28
+ */
29
+ export declare function verifyHelpersManifest(signedJson: string, pubkeyPem?: string): HelpersManifest | null;
30
+ //# sourceMappingURL=helper-signing.d.ts.map
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Ed25519 provenance for the auto-refreshed critical helpers (ADR-174 security).
3
+ *
4
+ * The helper auto-refresh copies auto-EXECUTING hook code (`hook-handler.cjs`
5
+ * etc.) from the installed package into a project. npm verifies the tarball at
6
+ * INSTALL time, but not the files on disk afterward — a sibling package's
7
+ * postinstall (or disk tampering) could overwrite them, and the refresh would
8
+ * faithfully propagate the tampered code. This gate closes that: every helper
9
+ * is verified against a ruflo-signed manifest before install, and a mismatch is
10
+ * REFUSED (fail-closed). The public key is baked in below; the private key is
11
+ * never in the repo (see scripts/sign-helpers.mjs).
12
+ *
13
+ * Native Node crypto (RFC 8032 Ed25519), zero external deps — same primitive as
14
+ * src/appliance/rvfa-signing.ts.
15
+ */
16
+ import { createHash, verify as edVerify } from 'crypto';
17
+ /**
18
+ * Ruflo helper-signing PUBLIC key (safe to commit). The matching private key is
19
+ * held out-of-repo and provided to scripts/sign-helpers.mjs at publish time via
20
+ * $RUFLO_HELPERS_SIGNING_KEY. Rotating the key = replace this constant + re-sign.
21
+ */
22
+ export const RUFLO_HELPERS_PUBKEY = `-----BEGIN PUBLIC KEY-----
23
+ MCowBQYDK2VwAyEAhnFv74/CRcGWd0hL8zjyZ+52bIJ9SfcSgOutuKgo0Vg=
24
+ -----END PUBLIC KEY-----`;
25
+ export const HELPERS_MANIFEST_FILE = 'helpers.manifest.json';
26
+ export function sha256Hex(content) {
27
+ return createHash('sha256').update(content).digest('hex');
28
+ }
29
+ /**
30
+ * Deterministic canonical bytes of a manifest — file keys sorted so the signer
31
+ * and verifier agree byte-for-byte regardless of object insertion order.
32
+ */
33
+ export function canonicalManifestBytes(m) {
34
+ const files = {};
35
+ for (const k of Object.keys(m.files).sort())
36
+ files[k] = m.files[k];
37
+ return Buffer.from(JSON.stringify({ version: m.version, files }), 'utf-8');
38
+ }
39
+ /**
40
+ * Verify a signed helpers manifest against ruflo's public key. Returns the
41
+ * trusted file->sha256 manifest, or null on ANY failure (bad signature,
42
+ * malformed JSON, wrong algorithm). Fail-closed — the caller MUST refuse to
43
+ * install unverified helpers.
44
+ */
45
+ export function verifyHelpersManifest(signedJson, pubkeyPem = RUFLO_HELPERS_PUBKEY) {
46
+ try {
47
+ const signed = JSON.parse(signedJson);
48
+ if (!signed || signed.algorithm !== 'ed25519' || !signed.signature || !signed.manifest)
49
+ return null;
50
+ if (!signed.manifest.files || typeof signed.manifest.files !== 'object')
51
+ return null;
52
+ const bytes = canonicalManifestBytes(signed.manifest);
53
+ const ok = edVerify(null, bytes, pubkeyPem, Buffer.from(signed.signature, 'base64'));
54
+ return ok ? signed.manifest : null;
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ //# sourceMappingURL=helper-signing.js.map
@@ -4,6 +4,22 @@
4
4
  */
5
5
  import type { InitOptions } from './types.js';
6
6
  export declare const ATTRIBUTION_FOOTER = "\uD83E\uDD16 Generated with [RuFlo](https://github.com/ruvnet/ruflo)";
7
+ /**
8
+ * Detect whether a Claude Code PostToolUse payload represents a FAILED tool run.
9
+ *
10
+ * Why this matters: the learning substrate had 898 feedback records, 100%
11
+ * success, 0 failures — because the post-edit/post-task hooks recorded a
12
+ * hardcoded `success:true` and never inspected the tool outcome. With no
13
+ * negative examples, the oracle tier can't teach good-vs-bad (see the DB
14
+ * analysis + ADR-174). Claude Code passes the tool result in the PostToolUse
15
+ * hook payload (`tool_response`), which for a failed Write/Edit/Bash carries an
16
+ * error marker. This predicate is the single source of truth the generated
17
+ * hook inlines, and is unit-tested here so the detection stays honest.
18
+ *
19
+ * Conservative: returns true only on a POSITIVE error signal; ambiguous/missing
20
+ * payloads default to success (matches prior behavior, avoids false failures).
21
+ */
22
+ export declare function isToolFailure(hookInput: unknown): boolean;
7
23
  /**
8
24
  * Generate pre-commit hook script
9
25
  */
@@ -9,6 +9,58 @@ import { generateStatuslineScript, generateStatuslineHook } from './statusline-g
9
9
  // PR body templates and release notes. It is NEVER hard-wired into the
10
10
  // static command-file templates — those are user-owned content.
11
11
  export const ATTRIBUTION_FOOTER = '🤖 Generated with [RuFlo](https://github.com/ruvnet/ruflo)';
12
+ /**
13
+ * Detect whether a Claude Code PostToolUse payload represents a FAILED tool run.
14
+ *
15
+ * Why this matters: the learning substrate had 898 feedback records, 100%
16
+ * success, 0 failures — because the post-edit/post-task hooks recorded a
17
+ * hardcoded `success:true` and never inspected the tool outcome. With no
18
+ * negative examples, the oracle tier can't teach good-vs-bad (see the DB
19
+ * analysis + ADR-174). Claude Code passes the tool result in the PostToolUse
20
+ * hook payload (`tool_response`), which for a failed Write/Edit/Bash carries an
21
+ * error marker. This predicate is the single source of truth the generated
22
+ * hook inlines, and is unit-tested here so the detection stays honest.
23
+ *
24
+ * Conservative: returns true only on a POSITIVE error signal; ambiguous/missing
25
+ * payloads default to success (matches prior behavior, avoids false failures).
26
+ */
27
+ export function isToolFailure(hookInput) {
28
+ if (!hookInput || typeof hookInput !== 'object')
29
+ return false;
30
+ const h = hookInput;
31
+ const tr = (h.tool_response ?? h.toolResponse ?? h.result);
32
+ if (tr == null)
33
+ return false;
34
+ if (typeof tr === 'string') {
35
+ return /\b(error|failed|failure|exception|not found|no such|permission denied|traceback)\b/i.test(tr);
36
+ }
37
+ if (typeof tr === 'object') {
38
+ const o = tr;
39
+ if (o.is_error === true || o.isError === true || o.success === false || o.error != null)
40
+ return true;
41
+ // Bash tool: non-zero exit code is a failure.
42
+ const code = (o.exit_code ?? o.exitCode ?? o.code);
43
+ if (typeof code === 'number' && code !== 0)
44
+ return true;
45
+ // Nested content array (Claude tool result shape): {content:[...], is_error:true}
46
+ if (Array.isArray(o.content) && o.is_error === true)
47
+ return true;
48
+ }
49
+ return false;
50
+ }
51
+ // The exact predicate the generated hook inlines — kept in sync with
52
+ // isToolFailure() above (mirrored, since the generated .cjs has no imports).
53
+ const TOOL_FAILURE_EXPR = '(function(hi){' +
54
+ 'if(!hi||typeof hi!=="object")return false;' +
55
+ 'var tr=hi.tool_response!=null?hi.tool_response:(hi.toolResponse!=null?hi.toolResponse:hi.result);' +
56
+ 'if(tr==null)return false;' +
57
+ 'if(typeof tr==="string")return /\\b(error|failed|failure|exception|not found|no such|permission denied|traceback)\\b/i.test(tr);' +
58
+ 'if(typeof tr==="object"){' +
59
+ 'if(tr.is_error===true||tr.isError===true||tr.success===false||tr.error!=null)return true;' +
60
+ 'var code=tr.exit_code!=null?tr.exit_code:(tr.exitCode!=null?tr.exitCode:tr.code);' +
61
+ 'if(typeof code==="number"&&code!==0)return true;' +
62
+ 'if(Array.isArray(tr.content)&&tr.is_error===true)return true;' +
63
+ '}return false;})(hookInput)';
12
64
  /**
13
65
  * Generate pre-commit hook script
14
66
  */
@@ -459,6 +511,9 @@ export function generateHookHandler() {
459
511
  ' // hook (#1944). Pull `.command` off whichever stdin shape Claude Code sent.',
460
512
  ' var toolInputObj = hookInput.toolInput || hookInput.tool_input || {};',
461
513
  " var prompt = hookInput.prompt || hookInput.command || toolInputObj.command || process.env.PROMPT || process.env.TOOL_INPUT_command || args.join(' ') || '';",
514
+ ' // Capture FAILURES, not just successes, so the learning substrate has',
515
+ ' // negative examples (see ADR-174 / DB analysis). Mirrors isToolFailure().',
516
+ ' var toolFailed = ' + TOOL_FAILURE_EXPR + ';',
462
517
  '',
463
518
  'const handlers = {',
464
519
  " 'route': () => {",
@@ -503,10 +558,10 @@ export function generateHookHandler() {
503
558
  ' if (intelligence && intelligence.recordEdit) {',
504
559
  ' try {',
505
560
  " var file = process.env.TOOL_INPUT_file_path || args[0] || '';",
506
- ' intelligence.recordEdit(file);',
561
+ ' intelligence.recordEdit(file, !toolFailed);',
507
562
  ' } catch (e) { /* non-fatal */ }',
508
563
  ' }',
509
- " console.log('[OK] Edit recorded');",
564
+ " console.log(toolFailed ? '[LEARN] Edit FAILURE recorded' : '[OK] Edit recorded');",
510
565
  ' },',
511
566
  '',
512
567
  " 'session-restore': () => {",
@@ -562,10 +617,10 @@ export function generateHookHandler() {
562
617
  " 'post-task': () => {",
563
618
  ' if (intelligence && intelligence.feedback) {',
564
619
  ' try {',
565
- ' intelligence.feedback(true);',
620
+ ' intelligence.feedback(!toolFailed);',
566
621
  ' } catch (e) { /* non-fatal */ }',
567
622
  ' }',
568
- " console.log('[OK] Task completed');",
623
+ " console.log(toolFailed ? '[LEARN] Task FAILURE recorded' : '[OK] Task completed');",
569
624
  ' },',
570
625
  '',
571
626
  " 'compact-manual': () => {",
@@ -809,10 +864,12 @@ export function generateIntelligenceStub() {
809
864
  ' return lines2.join("\\n");',
810
865
  ' },',
811
866
  '',
812
- ' recordEdit: function(file) {',
867
+ ' recordEdit: function(file, success) {',
813
868
  ' if (!file) return;',
814
869
  ' ensureDir(DATA_DIR);',
815
- ' var line = JSON.stringify({ type: "edit", file: file, timestamp: Date.now() }) + "\\n";',
870
+ ' // success defaults to true; an explicit false (failed edit) is recorded',
871
+ ' // so consolidation/distillation gets a negative example (ADR-174).',
872
+ ' var line = JSON.stringify({ type: "edit", file: file, success: success !== false, timestamp: Date.now() }) + "\\n";',
816
873
  ' fs.appendFileSync(PENDING_PATH, line, "utf-8");',
817
874
  ' },',
818
875
  '',
@@ -863,13 +920,37 @@ const DATA_DIR = join(PROJECT_ROOT, '.claude-flow', 'data');
863
920
  const STORE_PATH = join(DATA_DIR, 'auto-memory-store.json');
864
921
 
865
922
  const DIM = '\\x1b[2m';
923
+ const YELLOW = '\\x1b[0;33m';
866
924
  const RESET = '\\x1b[0m';
867
925
  const dim = (msg) => console.log(\` \${DIM}\${msg}\${RESET}\`);
868
926
 
927
+ // #2545: fail LOUD instead of a silent dim skip when @claude-flow/memory is
928
+ // unresolvable — self-learning imports are a no-op and the user must be told.
929
+ function warnMemoryUnavailable() {
930
+ const l1 = \`[AutoMemory] @claude-flow/memory not resolvable from \${PROJECT_ROOT} — self-learning imports are DISABLED.\`;
931
+ const l2 = ' Fix: npm i -D @claude-flow/memory (or re-run: npx ruflo@latest init, then npx ruflo@latest doctor --fix)';
932
+ console.log(\`\${YELLOW}\${l1}\${RESET}\`);
933
+ console.log(\`\${YELLOW}\${l2}\${RESET}\`);
934
+ process.stderr.write(\`\${l1}\\n\${l2}\\n\`);
935
+ }
936
+
869
937
  // Ensure data dir
870
938
  if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
871
939
 
872
940
  async function loadMemoryPackage() {
941
+ // Strategy 0 (#2545): sidecar recorded by \`init\` / \`doctor --fix\`. On the npx
942
+ // path @claude-flow/memory lands in the npx cache (unreachable by walk-up), so
943
+ // init records its absolute path here — the only strategy that works there.
944
+ try {
945
+ const sidecar = join(PROJECT_ROOT, '.claude-flow', 'memory-package.json');
946
+ if (existsSync(sidecar)) {
947
+ const rec = JSON.parse(readFileSync(sidecar, 'utf-8'));
948
+ if (rec && rec.distPath && existsSync(rec.distPath)) {
949
+ return await import(\`file://\${rec.distPath}\`);
950
+ }
951
+ }
952
+ } catch { /* fall through */ }
953
+
873
954
  // Strategy 1: Use createRequire for CJS-style resolution (handles nested node_modules
874
955
  // when installed as a transitive dependency via npx ruflo / npx claude-flow)
875
956
  try {
@@ -899,7 +980,7 @@ async function doImport() {
899
980
  const memPkg = await loadMemoryPackage();
900
981
 
901
982
  if (!memPkg || !memPkg.AutoMemoryBridge) {
902
- dim('Memory package not available — auto memory import skipped (non-critical)');
983
+ warnMemoryUnavailable();
903
984
  return;
904
985
  }
905
986
 
@@ -916,7 +997,7 @@ async function doSync() {
916
997
  const memPkg = await loadMemoryPackage();
917
998
 
918
999
  if (!memPkg || !memPkg.AutoMemoryBridge) {
919
- dim('Memory package not available — sync skipped (non-critical)');
1000
+ warnMemoryUnavailable();
920
1001
  return;
921
1002
  }
922
1003
 
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Memory Package Resolver (#2545)
3
+ *
4
+ * `@claude-flow/memory` is an *optionalDependency* of `@claude-flow/cli`. On the
5
+ * documented `npx ruflo` install path it lands in the npx cache
6
+ * (`~/.npm/_npx/<hash>/node_modules`), which is NOT on the node_modules walk-up
7
+ * path from the user's project. The SessionStart auto-memory hook therefore
8
+ * could never resolve it and self-learning silently no-op'd.
9
+ *
10
+ * At init time, however, the CLI *can* resolve the package from its own module
11
+ * context (it is installed alongside the CLI in that same npx cache). We resolve
12
+ * it once and record the absolute path in a machine-local project sidecar
13
+ * (`.claude-flow/memory-package.json`). The hook reads this sidecar first, so it
14
+ * reuses the copy npx already downloaded — no second install, no vendoring.
15
+ *
16
+ * This module is deliberately dependency-free and best-effort: nothing here ever
17
+ * throws into the init/doctor flow.
18
+ */
19
+ export declare const MEMORY_PACKAGE = "@claude-flow/memory";
20
+ /** Project-relative path of the resolver sidecar written by init / doctor --fix. */
21
+ export declare const MEMORY_SIDECAR_REL: string;
22
+ export interface MemoryPackageRecord {
23
+ /** Absolute path to the package's main entry (dist/index.js). */
24
+ distPath: string;
25
+ /** Resolved package version, if readable. */
26
+ version: string | null;
27
+ /** What produced this record ("init" | "doctor" | "upgrade"). */
28
+ resolvedBy: string;
29
+ /** ISO timestamp of resolution. */
30
+ resolvedAt: string;
31
+ }
32
+ /**
33
+ * Resolve `@claude-flow/memory`'s main entry from the CLI's own module context.
34
+ * Returns the absolute dist path, or null when the optional dependency is absent
35
+ * (e.g. installed with `--omit=optional`).
36
+ */
37
+ export declare function resolveMemoryPackageFromCli(): string | null;
38
+ /**
39
+ * Resolve `@claude-flow/memory` the same way the runtime hook does, from a target
40
+ * project directory: sidecar → project package.json → node_modules walk-up.
41
+ * Used by `doctor` so its verdict matches what the hook will actually experience.
42
+ */
43
+ export declare function resolveMemoryPackageFromProject(targetDir: string): string | null;
44
+ /** Read the version of the resolved memory package (dist/index.js → ../package.json). */
45
+ export declare function readMemoryPackageVersion(distPath: string): string | null;
46
+ /**
47
+ * Best-effort: resolve `@claude-flow/memory` from the CLI and record its path in
48
+ * the project sidecar so the runtime hook can find it on the npx install path.
49
+ * Returns the written record, or null when the package is not resolvable from the
50
+ * CLI (in which case the hook fails loud and `doctor` flags it).
51
+ */
52
+ export declare function recordMemoryPackagePath(targetDir: string, resolvedBy?: string): MemoryPackageRecord | null;
53
+ //# sourceMappingURL=memory-package-resolver.d.ts.map