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.
Files changed (31) 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/memory-distill.d.ts +27 -0
  4. package/v3/@claude-flow/cli/dist/src/commands/memory-distill.js +374 -0
  5. package/v3/@claude-flow/cli/dist/src/commands/memory.js +4 -2
  6. package/v3/@claude-flow/cli/dist/src/index.d.ts +1 -1
  7. package/v3/@claude-flow/cli/dist/src/index.js +22 -1
  8. package/v3/@claude-flow/cli/dist/src/init/executor.js +20 -0
  9. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +19 -0
  10. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +175 -0
  11. package/v3/@claude-flow/cli/dist/src/init/helper-signing.d.ts +30 -0
  12. package/v3/@claude-flow/cli/dist/src/init/helper-signing.js +60 -0
  13. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.d.ts +16 -0
  14. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +63 -6
  15. package/v3/@claude-flow/cli/dist/src/init/statusline-generator.js +18 -7
  16. package/v3/@claude-flow/cli/dist/src/mcp-client.js +13 -0
  17. package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-speculate-tools.js +9 -2
  18. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.d.ts +162 -0
  19. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.js +548 -0
  20. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.d.ts +9 -1
  21. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.js +4 -1
  22. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +67 -47
  23. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +71 -0
  24. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +330 -2
  25. package/v3/@claude-flow/cli/dist/src/services/distill-tuning.d.ts +111 -0
  26. package/v3/@claude-flow/cli/dist/src/services/distill-tuning.js +510 -0
  27. package/v3/@claude-flow/cli/dist/src/services/memory-distillation.d.ts +41 -0
  28. package/v3/@claude-flow/cli/dist/src/services/memory-distillation.js +277 -0
  29. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +22 -1
  30. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +117 -6
  31. 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(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
  '',
@@ -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
- const sql = Q + 'SELECT (SELECT COUNT(*) FROM memory_entries WHERE embedding IS NOT NULL)||' + "'|'" + '||(SELECT COUNT(*) FROM vector_indexes);' + Q;
215
- const out = safeExec("sqlite3 'file:" + memDb + "?mode=ro' " + sql, 1500);
216
- if (out && out.indexOf('|') !== -1) {
217
- const parts = out.split('|');
218
- result.vectorCount = parseInt(parts[0], 10) || 0;
219
- result.hasHnsw = (parseInt(parts[1], 10) || 0) > 0;
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 = rawCandidates.find((c) => validateLabel(String(c.label)) === label)?.branchPath;
161
+ const explicit = explicitBranchPaths.get(label);
155
162
  if (explicit)
156
- return resolveMemoryPath(String(explicit));
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