claude-flow 3.21.1 → 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.
- 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-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 +4 -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-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 +22 -1
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +117 -6
- package/v3/@claude-flow/cli/package.json +5 -2
|
@@ -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
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Intent MCP Tools — ADR-175 page-agent integration.
|
|
3
|
+
*
|
|
4
|
+
* Adds a natural-language INTENT layer on top of the low-level selector-based
|
|
5
|
+
* `browser_*` tools (browser-tools.ts, driven by the `agent-browser` CLI).
|
|
6
|
+
* `browser_act({ task })` lets a caller say "Click the login button" instead
|
|
7
|
+
* of chaining browser_snapshot + browser_click.
|
|
8
|
+
*
|
|
9
|
+
* Backing library: `page-agent` (npm, MIT, https://github.com/alibaba/page-agent)
|
|
10
|
+
* — in-page injected JS that turns the DOM into text and lets an LLM execute
|
|
11
|
+
* natural-language intents via `agent.execute('Click login')`.
|
|
12
|
+
*
|
|
13
|
+
* ============================================================================
|
|
14
|
+
* VERIFIED API (recorded 2026-07-04 against page-agent@1.11.0 — `npm view
|
|
15
|
+
* page-agent`, the published README, and the actual npm tarball contents).
|
|
16
|
+
* Deviations from the original integration brief are called out inline.
|
|
17
|
+
* ============================================================================
|
|
18
|
+
*
|
|
19
|
+
* - Package layout changed since the brief was written: `page-agent` is now a
|
|
20
|
+
* thin composition of `@page-agent/core` + `@page-agent/llms` +
|
|
21
|
+
* `@page-agent/page-controller` + `@page-agent/ui`. The npm package is
|
|
22
|
+
* ESM-only (`"type":"module"`) and ships two builds:
|
|
23
|
+
* - `dist/esm/page-agent.js` — the Node/bundler ESM entry.
|
|
24
|
+
* - `dist/iife/page-agent.demo.js` — a self-contained browser IIFE that
|
|
25
|
+
* sets `window.PageAgent = <class>` (this is what we inject).
|
|
26
|
+
*
|
|
27
|
+
* - DEVIATION #1 (load-bearing): `await import('page-agent')` is NOT a safe
|
|
28
|
+
* availability probe in Node. Empirically, importing the ESM entry throws
|
|
29
|
+
* `ReferenceError: window is not defined` — even when the package IS
|
|
30
|
+
* correctly installed — because `@page-agent/core`'s module graph touches
|
|
31
|
+
* DOM globals at import time (it's designed to run in a browser, not Node).
|
|
32
|
+
* This differs from the agenticow-loader.ts pattern used elsewhere in this
|
|
33
|
+
* repo (dynamic `import()` + catch `ERR_MODULE_NOT_FOUND`), which only
|
|
34
|
+
* works for pure-Node optional deps. Instead we detect availability via
|
|
35
|
+
* `require.resolve('page-agent')` — path resolution only, never executes
|
|
36
|
+
* module code — the same technique `browser-session-tools.ts` already uses
|
|
37
|
+
* to resolve the local `ruvector` CLI bin without invoking it.
|
|
38
|
+
*
|
|
39
|
+
* - DEVIATION #2: the IIFE bundle unconditionally (also) auto-constructs a
|
|
40
|
+
* DEMO `PageAgent` instance via `setTimeout`, pointed at Alibaba's public
|
|
41
|
+
* sandbox endpoint (`model: 'qwen3.5-plus'`, a `page-ag-testing-*.run`
|
|
42
|
+
* baseURL, `apiKey: 'NA'`) UNLESS the bundle is loaded via a real
|
|
43
|
+
* `<script src="...?autoInit=false">` tag — the guard reads
|
|
44
|
+
* `document.currentScript.src`, which is `null` when the bundle runs via
|
|
45
|
+
* `eval`/CDP (our only injection path through the `agent-browser`
|
|
46
|
+
* primitive set). Left alone, every `browser_act` call would leak page
|
|
47
|
+
* content to Alibaba's test endpoint using a placeholder key. We strip the
|
|
48
|
+
* demo auto-init tail (`stripDemoAutoInit`) before injecting rather than
|
|
49
|
+
* relying on the query-param guard.
|
|
50
|
+
*
|
|
51
|
+
* - Constructor: `new PageAgent({ model, baseURL, apiKey, language })` —
|
|
52
|
+
* `apiKey` is OPTIONAL on the type (`LLMConfig.apiKey?: string`), confirmed
|
|
53
|
+
* in `@page-agent/llms`' `LLMConfig` interface.
|
|
54
|
+
*
|
|
55
|
+
* - `execute(task: string): Promise<ExecutionResult>` where
|
|
56
|
+
* `ExecutionResult = { success: boolean; data: string; history: HistoricalEvent[] }`
|
|
57
|
+
* — DEVIATION #3: NOT `{ success, result, steps }` as sketched in the
|
|
58
|
+
* original brief. We map `data` → `result` and `history.length` → `steps`
|
|
59
|
+
* in the tool's own return shape, and also pass `history` through for
|
|
60
|
+
* callers that want the full step trace.
|
|
61
|
+
*
|
|
62
|
+
* - LLM transport: confirmed via `@page-agent/llms`' `OpenAIClient` — a
|
|
63
|
+
* single non-streaming `POST ${baseURL}/chat/completions` with
|
|
64
|
+
* `Authorization: Bearer ${apiKey}` and OpenAI tool-calling JSON. This is
|
|
65
|
+
* why a bare `ANTHROPIC_API_KEY` cannot be handed to page-agent directly:
|
|
66
|
+
* Anthropic's native API is a different shape (`/v1/messages`, different
|
|
67
|
+
* tool-call format). We require an OpenAI-*compatible* endpoint — reusing
|
|
68
|
+
* this CLI's own Tier-3 fallback ladder from `agent-execute-core.ts`
|
|
69
|
+
* (OpenRouter, then Ollama Cloud), or an explicit override.
|
|
70
|
+
*
|
|
71
|
+
* ARCHITECTURAL CONSTRAINT (mirrors metaharness-tools.ts / agenticow-tools.ts)
|
|
72
|
+
* `page-agent` lives in `optionalDependencies`. Every code path degrades to
|
|
73
|
+
* `{ degraded: true }` — never throws — when the package or an LLM provider
|
|
74
|
+
* isn't available.
|
|
75
|
+
*
|
|
76
|
+
* KEY-SAFETY CONSTRAINT (load-bearing): page-agent's LLM calls happen FROM
|
|
77
|
+
* the browser page context (it's client-side injected JS), so its `apiKey`
|
|
78
|
+
* config field is necessarily visible to whatever we inject into the page.
|
|
79
|
+
* To avoid ever placing a real provider key into a page-context string, we
|
|
80
|
+
* start a short-lived local HTTP proxy (`startLocalLLMProxy`) bound to
|
|
81
|
+
* 127.0.0.1 that holds the REAL key server-side and injects the real
|
|
82
|
+
* `Authorization` header itself; the page only ever sees the proxy's
|
|
83
|
+
* loopback URL plus a constant placeholder string (`PLACEHOLDER_API_KEY`).
|
|
84
|
+
*
|
|
85
|
+
* @module @claude-flow/cli/mcp-tools/browser-intent
|
|
86
|
+
*/
|
|
87
|
+
import type { MCPTool } from './types.js';
|
|
88
|
+
export interface PageAgentBundle {
|
|
89
|
+
/** Package root directory (contains package.json). */
|
|
90
|
+
root: string;
|
|
91
|
+
/** Absolute path to the browser-injectable IIFE bundle. */
|
|
92
|
+
iifePath: string;
|
|
93
|
+
version: string;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Locate the installed `page-agent` package's browser bundle without ever
|
|
97
|
+
* executing its module code. `resolvePkg` is injectable for tests (DI over
|
|
98
|
+
* module mocking, since `vi.mock('page-agent', ...)` cannot simulate absence
|
|
99
|
+
* for a package we deliberately never `import()`).
|
|
100
|
+
*/
|
|
101
|
+
export declare function locatePageAgentBundle(resolvePkg?: (id: string) => string): PageAgentBundle | null;
|
|
102
|
+
export interface ResolvedLLMConfig {
|
|
103
|
+
baseURL: string;
|
|
104
|
+
apiKey: string;
|
|
105
|
+
model: string;
|
|
106
|
+
source: 'explicit' | 'openrouter' | 'ollama';
|
|
107
|
+
}
|
|
108
|
+
export declare function resolvePageAgentLLMConfig(env?: NodeJS.ProcessEnv): ResolvedLLMConfig | null;
|
|
109
|
+
export declare const PLACEHOLDER_API_KEY = "ruflo-proxied-key";
|
|
110
|
+
export interface LLMProxyHandle {
|
|
111
|
+
url: string;
|
|
112
|
+
close: () => void;
|
|
113
|
+
}
|
|
114
|
+
export declare function startLocalLLMProxy(real: {
|
|
115
|
+
baseURL: string;
|
|
116
|
+
apiKey: string;
|
|
117
|
+
}): Promise<LLMProxyHandle>;
|
|
118
|
+
/** Thrown when a bundle still contains a demo/sandbox endpoint after stripping. */
|
|
119
|
+
export declare class DemoLeakError extends Error {
|
|
120
|
+
readonly signature: string;
|
|
121
|
+
constructor(signature: string);
|
|
122
|
+
}
|
|
123
|
+
/** Returns the first demo-leak signature found in `source`, or null if clean. */
|
|
124
|
+
export declare function findDemoLeak(source: string): string | null;
|
|
125
|
+
/**
|
|
126
|
+
* Strip the demo bundle's auto-init tail so injecting it via eval doesn't spin
|
|
127
|
+
* up a second `window.pageAgent` pointed at Alibaba's sandbox endpoint. This is
|
|
128
|
+
* best-effort at the text level; the real guarantee is the fail-closed
|
|
129
|
+
* `findDemoLeak` firewall enforced in `buildPageAgentInjection`, NOT this strip.
|
|
130
|
+
*/
|
|
131
|
+
export declare function stripDemoAutoInit(iifeSource: string): string;
|
|
132
|
+
export declare const BROWSER_ACT_RESULT_GLOBAL = "__ruflo_browser_act_result__";
|
|
133
|
+
export interface PageAgentPageConfig {
|
|
134
|
+
baseURL: string;
|
|
135
|
+
apiKey: string;
|
|
136
|
+
model: string;
|
|
137
|
+
language?: string;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Build the full script to hand to `browser_eval` / `agent-browser eval`:
|
|
141
|
+
* the (demo-stripped) IIFE bundle, followed by our own controlled
|
|
142
|
+
* construction + `execute()` call whose settled result lands on a window
|
|
143
|
+
* global we can poll for.
|
|
144
|
+
*
|
|
145
|
+
* KEY-SAFETY: `pageConfig.apiKey` MUST be `PLACEHOLDER_API_KEY` (or another
|
|
146
|
+
* non-secret placeholder) — callers are responsible for routing the real key
|
|
147
|
+
* through `startLocalLLMProxy` first. This function only assembles strings;
|
|
148
|
+
* it does not itself guarantee key safety, so callers MUST NOT pass a real
|
|
149
|
+
* key here (see `browserIntentTools` handler for the enforced call site).
|
|
150
|
+
*/
|
|
151
|
+
export declare function buildPageAgentInjection(iifeSource: string, pageConfig: PageAgentPageConfig, task: string): string;
|
|
152
|
+
export declare function recordBrowserTrajectory(entry: {
|
|
153
|
+
task: string;
|
|
154
|
+
url?: string;
|
|
155
|
+
success: boolean;
|
|
156
|
+
dataPreview: string;
|
|
157
|
+
steps: number;
|
|
158
|
+
source: string;
|
|
159
|
+
}): Promise<boolean>;
|
|
160
|
+
export declare const browserIntentTools: MCPTool[];
|
|
161
|
+
export default browserIntentTools;
|
|
162
|
+
//# sourceMappingURL=browser-intent-tools.d.ts.map
|