claude-flow 3.21.1 → 3.23.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 -0
- package/.claude/helpers/auto-memory-hook.mjs +59 -274
- package/.claude/helpers/helpers.manifest.json +12 -0
- package/.claude/helpers/hook-handler.cjs +132 -112
- package/.claude/helpers/intelligence.cjs +992 -196
- package/package.json +1 -1
- package/v3/@claude-flow/cli/dist/src/commands/daemon.js +23 -2
- package/v3/@claude-flow/cli/dist/src/commands/memory-backup.d.ts +11 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory-backup.js +46 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory-distill.d.ts +27 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory-distill.js +374 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +5 -2
- package/v3/@claude-flow/cli/dist/src/index.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/index.js +22 -1
- package/v3/@claude-flow/cli/dist/src/init/executor.js +20 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +19 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +175 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-signing.d.ts +30 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-signing.js +60 -0
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.d.ts +16 -0
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +63 -6
- package/v3/@claude-flow/cli/dist/src/init/statusline-generator.js +18 -7
- package/v3/@claude-flow/cli/dist/src/mcp-client.js +13 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-speculate-tools.js +9 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.d.ts +162 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.js +548 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.d.ts +9 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.js +4 -1
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +67 -47
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +71 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +330 -2
- package/v3/@claude-flow/cli/dist/src/services/distill-tuning.d.ts +111 -0
- package/v3/@claude-flow/cli/dist/src/services/distill-tuning.js +510 -0
- package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +24 -0
- package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +94 -0
- package/v3/@claude-flow/cli/dist/src/services/memory-distillation.d.ts +41 -0
- package/v3/@claude-flow/cli/dist/src/services/memory-distillation.js +277 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +31 -2
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +159 -6
- package/v3/@claude-flow/cli/package.json +5 -2
|
@@ -15,6 +15,7 @@ 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';
|
|
19
20
|
import { recordMemoryPackagePath } from './memory-package-resolver.js';
|
|
20
21
|
/**
|
|
@@ -207,6 +208,19 @@ export async function executeInit(options) {
|
|
|
207
208
|
if (memRecord) {
|
|
208
209
|
result.created.files.push('.claude-flow/memory-package.json');
|
|
209
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 */ }
|
|
210
224
|
}
|
|
211
225
|
// Generate statusline
|
|
212
226
|
if (options.components.statusline) {
|
|
@@ -483,6 +497,12 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
|
|
|
483
497
|
catch { }
|
|
484
498
|
}
|
|
485
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 */ }
|
|
486
506
|
// #2545: (re)record the resolved @claude-flow/memory path so the auto-memory
|
|
487
507
|
// hook can find it on the npx install path. Best-effort — see executeInit.
|
|
488
508
|
const memRecord = recordMemoryPackagePath(targetDir, 'upgrade');
|
|
@@ -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(
|
|
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
|
-
'
|
|
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
|
'',
|
|
@@ -211,13 +211,24 @@ function getLocalAgentDB() {
|
|
|
211
211
|
const memDb = path.join(CWD, '.swarm', 'memory.db');
|
|
212
212
|
if (fs.existsSync(memDb)) {
|
|
213
213
|
const Q = String.fromCharCode(34);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
214
|
+
// Two INDEPENDENT statements -- do NOT combine into one. Coupling the
|
|
215
|
+
// vector count with the vector_indexes row count in a single statement
|
|
216
|
+
// meant that on a DB missing the vector_indexes table (older/agentdb-
|
|
217
|
+
// written DBs), the whole statement failed at PREPARE time (SQLite
|
|
218
|
+
// compiles the full SQL before running), so the valid memory_entries
|
|
219
|
+
// count was discarded too and the statusline showed Vectors 0 despite
|
|
220
|
+
// thousands of real vectors. Split so a missing table can only zero the
|
|
221
|
+
// HNSW flag, never the count. The init self-heal provisions the table so
|
|
222
|
+
// the flag recovers on the next ruflo init / MCP start.
|
|
223
|
+
const countSql = Q + 'SELECT COUNT(*) FROM memory_entries WHERE embedding IS NOT NULL;' + Q;
|
|
224
|
+
const vc = safeExec("sqlite3 'file:" + memDb + "?mode=ro' " + countSql, 1500);
|
|
225
|
+
if (vc) result.vectorCount = parseInt(vc, 10) || 0;
|
|
226
|
+
// HNSW flag: separate statement. If vector_indexes is absent, sqlite3
|
|
227
|
+
// exits non-zero and safeExec returns empty -- hasHnsw stays false (exact
|
|
228
|
+
// original semantics: at least one index-config row present).
|
|
229
|
+
const hnswSql = Q + 'SELECT COUNT(*) FROM vector_indexes;' + Q;
|
|
230
|
+
const hn = safeExec("sqlite3 'file:" + memDb + "?mode=ro' " + hnswSql, 1500);
|
|
231
|
+
if (hn) result.hasHnsw = (parseInt(hn, 10) || 0) > 0;
|
|
221
232
|
}
|
|
222
233
|
} catch { /* ignore */ }
|
|
223
234
|
return result;
|
|
@@ -33,6 +33,10 @@ import { daaTools } from './mcp-tools/daa-tools.js';
|
|
|
33
33
|
import { coordinationTools } from './mcp-tools/coordination-tools.js';
|
|
34
34
|
import { browserTools } from './mcp-tools/browser-tools.js';
|
|
35
35
|
import { browserSessionTools } from './mcp-tools/browser-session-tools.js';
|
|
36
|
+
// ADR-175 — page-agent natural-language intent layer (browser_act). Always
|
|
37
|
+
// registered; degrades to {degraded:true} at call time when page-agent or an
|
|
38
|
+
// OpenAI-compatible LLM provider isn't available.
|
|
39
|
+
import { browserIntentTools } from './mcp-tools/browser-intent-tools.js';
|
|
36
40
|
import { execFileSync } from 'node:child_process';
|
|
37
41
|
// Phase 6: AgentDB v3 controller tools
|
|
38
42
|
import { agentdbTools } from './mcp-tools/agentdb-tools.js';
|
|
@@ -89,6 +93,14 @@ function getBrowserTools() {
|
|
|
89
93
|
function getBrowserSessionTools() {
|
|
90
94
|
return browserSessionTools;
|
|
91
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* ADR-175 `browser_act` — always registered like browserSessionTools; the
|
|
98
|
+
* handler itself resolves page-agent + LLM-provider availability at call
|
|
99
|
+
* time and returns `{degraded: true}` rather than throwing.
|
|
100
|
+
*/
|
|
101
|
+
function getBrowserIntentTools() {
|
|
102
|
+
return browserIntentTools;
|
|
103
|
+
}
|
|
92
104
|
/**
|
|
93
105
|
* MCP Tool Registry
|
|
94
106
|
* Maps tool names to their handler functions
|
|
@@ -127,6 +139,7 @@ registerTools([
|
|
|
127
139
|
...coordinationTools,
|
|
128
140
|
...getBrowserTools(),
|
|
129
141
|
...getBrowserSessionTools(),
|
|
142
|
+
...getBrowserIntentTools(),
|
|
130
143
|
// Phase 6: AgentDB v3 controller tools
|
|
131
144
|
...agentdbTools,
|
|
132
145
|
// RuVector WASM tools
|
|
@@ -115,11 +115,18 @@ export const agenticowSpeculateTools = [
|
|
|
115
115
|
}
|
|
116
116
|
// Build the generic {label, fn} candidates. Each fn ingests into its own
|
|
117
117
|
// branch handle, then (for 'nearest') probes it so we can score.
|
|
118
|
+
// Map validated label → explicit branchPath so the branchPath() resolver
|
|
119
|
+
// below is O(1) instead of re-scanning + re-validating rawCandidates per
|
|
120
|
+
// candidate (explore() calls branchPath once per candidate → was O(n²)).
|
|
121
|
+
const explicitBranchPaths = new Map();
|
|
118
122
|
const candidates = rawCandidates.map((c) => {
|
|
119
123
|
const label = validateLabel(String(c.label));
|
|
120
124
|
if (!Array.isArray(c.ingest) || c.ingest.length === 0) {
|
|
121
125
|
throw new Error(`candidate ${label} must ingest at least one vector`);
|
|
122
126
|
}
|
|
127
|
+
if (typeof c.branchPath === 'string' && c.branchPath) {
|
|
128
|
+
explicitBranchPaths.set(label, c.branchPath);
|
|
129
|
+
}
|
|
123
130
|
const records = c.ingest.map((r) => ({
|
|
124
131
|
...(Number.isInteger(r.id) ? { id: r.id } : {}),
|
|
125
132
|
vector: r.vector,
|
|
@@ -151,9 +158,9 @@ export const agenticowSpeculateTools = [
|
|
|
151
158
|
return 1 / (1 + Math.max(0, r.bestDistance));
|
|
152
159
|
};
|
|
153
160
|
const branchPath = (label) => {
|
|
154
|
-
const explicit =
|
|
161
|
+
const explicit = explicitBranchPaths.get(label);
|
|
155
162
|
if (explicit)
|
|
156
|
-
return resolveMemoryPath(
|
|
163
|
+
return resolveMemoryPath(explicit);
|
|
157
164
|
return `${basePath}.spec-${safeSegment(label)}.rvf`;
|
|
158
165
|
};
|
|
159
166
|
// ADR-171 promotion gate. For pure memory A/B the `score` IS the chosen
|