@claude-flow/cli 3.72.2 → 3.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.claude/helpers/.helpers-version +1 -1
  2. package/.claude/helpers/auto-memory-hook.mjs +53 -224
  3. package/.claude/helpers/helpers.manifest.json +6 -6
  4. package/.claude/helpers/hook-handler.cjs +10 -89
  5. package/.claude/helpers/intelligence.cjs +1 -23
  6. package/.claude/helpers/statusline.cjs +14 -29
  7. package/README.md +29 -21
  8. package/catalog-manifest.json +2 -2
  9. package/dist/src/commands/agent.js +2 -0
  10. package/dist/src/commands/doctor.js +5 -3
  11. package/dist/src/commands/funnel.d.ts +1 -0
  12. package/dist/src/commands/funnel.js +37 -2
  13. package/dist/src/commands/hooks.js +16 -2
  14. package/dist/src/commands/init.js +70 -16
  15. package/dist/src/commands/mcp.js +15 -0
  16. package/dist/src/commands/swarm.js +2 -0
  17. package/dist/src/funnel/activation.d.ts +30 -0
  18. package/dist/src/funnel/activation.js +69 -0
  19. package/dist/src/funnel/events.js +4 -1
  20. package/dist/src/funnel/index.d.ts +2 -1
  21. package/dist/src/funnel/index.js +2 -1
  22. package/dist/src/funnel/message-transport.d.ts +12 -0
  23. package/dist/src/funnel/message-transport.js +23 -1
  24. package/dist/src/funnel/types.d.ts +2 -2
  25. package/dist/src/init/first-win.d.ts +24 -0
  26. package/dist/src/init/first-win.js +97 -0
  27. package/dist/src/mcp-tools/agent-tools.js +2 -0
  28. package/dist/src/mcp-tools/memory-tools.js +17 -0
  29. package/dist/src/mcp-tools/metaharness-tools.js +19 -0
  30. package/dist/src/mcp-tools/swarm-tools.js +2 -0
  31. package/dist/src/memory/memory-bridge.d.ts +2 -0
  32. package/dist/src/memory/memory-bridge.js +1 -1
  33. package/dist/src/memory/memory-initializer.d.ts +2 -0
  34. package/dist/src/memory/memory-initializer.js +1 -1
  35. package/dist/src/metaharness/darwin-lib.d.ts +68 -0
  36. package/dist/src/metaharness/darwin-lib.js +150 -0
  37. package/dist/src/transfer/store/download.d.ts +1 -1
  38. package/dist/src/transfer/store/download.js +22 -21
  39. package/dist/src/transfer/store/publish.d.ts +1 -1
  40. package/dist/src/transfer/store/publish.js +13 -14
  41. package/node_modules/@claude-flow/plugin-agent-federation/package.json +5 -5
  42. package/node_modules/@claude-flow/security/dist/input-validator.d.ts +138 -106
  43. package/node_modules/@claude-flow/security/dist/input-validator.d.ts.map +1 -1
  44. package/node_modules/@claude-flow/security/dist/input-validator.js +23 -26
  45. package/node_modules/@claude-flow/security/dist/input-validator.js.map +1 -1
  46. package/node_modules/@claude-flow/security/package.json +2 -2
  47. package/package.json +10 -10
  48. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -1
  49. package/plugins/ruflo-metaharness/CHANGELOG.md +3 -0
  50. package/plugins/ruflo-metaharness/scripts/_darwin.mjs +13 -0
  51. package/plugins/ruflo-metaharness/scripts/bench.mjs +34 -2
  52. package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -4
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Activation funnel (ruflo adoption plan; ADR-309 opt-in telemetry).
3
+ *
4
+ * installed -> init_completed -> first_agent_run -> session_active (7-day retention)
5
+ *
6
+ * Every event goes through recordFunnelEvent, which records nothing without
7
+ * an explicit, current `telemetry` consent receipt. The once-ever and
8
+ * once-per-day markers live in their own state file because a successful
9
+ * flush removes sent events from the queue (lastRecordedEvent would forget).
10
+ */
11
+ import { getConsent } from './consent.js';
12
+ import { recordFunnelEvent } from './events.js';
13
+ import { isCI, isInteractive } from './environment.js';
14
+ import { resolveFunnelEnabled } from './precedence.js';
15
+ import { readStateJson, writeStateJson } from './state.js';
16
+ const MARKERS_FILE = 'funnel-activation.json';
17
+ const day = (now) => now.toISOString().slice(0, 10);
18
+ function markers() {
19
+ return readStateJson(MARKERS_FILE) ?? {};
20
+ }
21
+ /** `ruflo init` finished in a project. Recorded on every successful init. */
22
+ export function recordInitCompleted(release, now = new Date()) {
23
+ return recordFunnelEvent('init_completed', 'init', release, now);
24
+ }
25
+ /** The first agent spawn or swarm init on this machine. Recorded once, ever. */
26
+ export function recordFirstAgentRun(release, surface = 'cli', now = new Date()) {
27
+ const m = markers();
28
+ if (m.firstAgentRunAt)
29
+ return false;
30
+ if (!recordFunnelEvent('first_agent_run', surface, release, now))
31
+ return false;
32
+ writeStateJson(MARKERS_FILE, { ...m, firstAgentRunAt: now.toISOString() });
33
+ return true;
34
+ }
35
+ /** At most one "active today" marker per calendar day (UTC). */
36
+ export function recordSessionActive(release, now = new Date()) {
37
+ const m = markers();
38
+ const today = day(now);
39
+ if (m.lastActiveDay === today)
40
+ return false;
41
+ if (!recordFunnelEvent('session_active', 'cli', release, now))
42
+ return false;
43
+ writeStateJson(MARKERS_FILE, { ...m, lastActiveDay: today });
44
+ return true;
45
+ }
46
+ /**
47
+ * Whether `ruflo init` should ask the one-time telemetry question: an
48
+ * interactive terminal, not CI, funnel not disabled, no telemetry receipt
49
+ * yet (asked once, whatever the answer), and not suppressed by flag.
50
+ */
51
+ export function shouldOfferTelemetry(opts = {}) {
52
+ const env = opts.env ?? process.env;
53
+ if (opts.noTelemetryPrompt)
54
+ return false;
55
+ if (isCI(env))
56
+ return false;
57
+ if (!isInteractive())
58
+ return false;
59
+ if (!resolveFunnelEnabled(opts.cwd ?? process.cwd(), env).enabled)
60
+ return false;
61
+ return getConsent('telemetry').at === null;
62
+ }
63
+ /** Fire-and-forget wrapper for command handlers: never throws, never blocks. */
64
+ export function noteFirstAgentRun(surface = 'cli') {
65
+ void import('../mcp-protocol.js')
66
+ .then(({ rufloPackageVersion }) => { recordFirstAgentRun(rufloPackageVersion(), surface); })
67
+ .catch(() => undefined);
68
+ }
69
+ //# sourceMappingURL=activation.js.map
@@ -39,8 +39,11 @@ const EVENT_NAMES = [
39
39
  'training_share_disabled',
40
40
  'advisor_tip_enabled',
41
41
  'advisor_tip_disabled',
42
+ 'init_completed',
43
+ 'first_agent_run',
44
+ 'session_active',
42
45
  ];
43
- const SURFACES = ['statusline', 'init', 'credit_exhaustion'];
46
+ const SURFACES = ['statusline', 'init', 'credit_exhaustion', 'cli'];
44
47
  /** Rotate the pseudonymous ID every 90 days (ADR-305). */
45
48
  const FUNNEL_ID_TTL_MS = 90 * 24 * 60 * 60 * 1000;
46
49
  /**
@@ -9,7 +9,8 @@ export { resolveFunnelEnabled } from './precedence.js';
9
9
  export { CONSENT_DOMAINS, getConsent, hasConsent, readConsents, recordConsent, revokeConsent, } from './consent.js';
10
10
  export { DISCLOSURE_GRACE_MS, DISCLOSURE_ROTATION_SLOT_MS, getDisclosure, promoEligible, recordDisclosureAccepted, recordDisclosureDeclined, recordDisclosureReenabled, recordDisclosureShown, selectDisclosureMessage, } from './disclosure.js';
11
11
  export { MAX_MESSAGE_COLUMNS, MESSAGES, containsForbiddenSequences, displayWidth, eligibleMessages, eligibleMessagesFromPools, isAllowedUrl, isValidMessage, } from './messages.js';
12
- export { DEFAULT_MESSAGES_ENDPOINT, getRemoteMessages, refreshRemoteMessages, } from './message-transport.js';
12
+ export { DEFAULT_MESSAGES_ENDPOINT, getRemoteMessages, funnelUserAgent, launchSurface, refreshRemoteMessages, } from './message-transport.js';
13
+ export { flushEvents } from './event-transport.js';
13
14
  export { PROMO_REPEAT_CAP_MS, PROMO_SLOT_MODULO, ROTATION_SLOT_MS, selectMessage, } from './rotation.js';
14
15
  export { CREDIT_RECOVERY_HINT, classifyCreditError, renderCreditRecovery, shouldShowCreditRecovery, type CreditPromptSession, type ProviderErrorLike, } from './credit-errors.js';
15
16
  export { deleteFunnelData, getFunnelId, lastRecordedEvent, recordFunnelEvent } from './events.js';
@@ -9,7 +9,8 @@ export { resolveFunnelEnabled } from './precedence.js';
9
9
  export { CONSENT_DOMAINS, getConsent, hasConsent, readConsents, recordConsent, revokeConsent, } from './consent.js';
10
10
  export { DISCLOSURE_GRACE_MS, DISCLOSURE_ROTATION_SLOT_MS, getDisclosure, promoEligible, recordDisclosureAccepted, recordDisclosureDeclined, recordDisclosureReenabled, recordDisclosureShown, selectDisclosureMessage, } from './disclosure.js';
11
11
  export { MAX_MESSAGE_COLUMNS, MESSAGES, containsForbiddenSequences, displayWidth, eligibleMessages, eligibleMessagesFromPools, isAllowedUrl, isValidMessage, } from './messages.js';
12
- export { DEFAULT_MESSAGES_ENDPOINT, getRemoteMessages, refreshRemoteMessages, } from './message-transport.js';
12
+ export { DEFAULT_MESSAGES_ENDPOINT, getRemoteMessages, funnelUserAgent, launchSurface, refreshRemoteMessages, } from './message-transport.js';
13
+ export { flushEvents } from './event-transport.js';
13
14
  export { PROMO_REPEAT_CAP_MS, PROMO_SLOT_MODULO, ROTATION_SLOT_MS, selectMessage, } from './rotation.js';
14
15
  export { CREDIT_RECOVERY_HINT, classifyCreditError, renderCreditRecovery, shouldShowCreditRecovery, } from './credit-errors.js';
15
16
  export { deleteFunnelData, getFunnelId, lastRecordedEvent, recordFunnelEvent } from './events.js';
@@ -31,6 +31,18 @@
31
31
  * the trust boundary. All content is treated as untrusted regardless.
32
32
  */
33
33
  import type { FunnelMessage } from './types.js';
34
+ /**
35
+ * User-Agent for the messages fetch — ADR-309 operational telemetry (see
36
+ * docs/privacy/funnel-legitimate-interest-assessment.md). Only the CLI
37
+ * release and the coarse OS/CPU family, so the server can count releases
38
+ * and platforms. No identifier, path, hostname, or account: the
39
+ * `ruflo-funnel/messages` prefix stays first so existing log filters match.
40
+ * A trailing `surface/plugin` marks a fetch made by the ruflo-core plugin's
41
+ * MCP launch, so plugin-only users (who never run `init`) can be counted.
42
+ */
43
+ export declare function funnelUserAgent(version?: string, platform?: string, arch?: string, surface?: string | undefined): string;
44
+ /** The surface set by a launcher (plugins/ruflo-core/scripts/mcp-launch.cjs), if any. */
45
+ export declare function launchSurface(env?: NodeJS.ProcessEnv): string | undefined;
34
46
  /** Endpoint the client hits — overridable for staging / self-hosted. */
35
47
  export declare const DEFAULT_MESSAGES_ENDPOINT: string;
36
48
  /**
@@ -34,7 +34,29 @@ import * as https from 'https';
34
34
  import { URL as NodeUrl } from 'url';
35
35
  import { isValidMessage } from './messages.js';
36
36
  import { readStateJson, writeStateJson } from './state.js';
37
+ import { rufloPackageVersion } from '../mcp-protocol.js';
37
38
  const CACHE_FILE = 'funnel-messages-cache.json';
39
+ /**
40
+ * User-Agent for the messages fetch — ADR-309 operational telemetry (see
41
+ * docs/privacy/funnel-legitimate-interest-assessment.md). Only the CLI
42
+ * release and the coarse OS/CPU family, so the server can count releases
43
+ * and platforms. No identifier, path, hostname, or account: the
44
+ * `ruflo-funnel/messages` prefix stays first so existing log filters match.
45
+ * A trailing `surface/plugin` marks a fetch made by the ruflo-core plugin's
46
+ * MCP launch, so plugin-only users (who never run `init`) can be counted.
47
+ */
48
+ export function funnelUserAgent(version = rufloPackageVersion(), platform = process.platform, arch = process.arch, surface = launchSurface()) {
49
+ const clean = (v) => v.replace(/[^A-Za-z0-9._-]/g, '').slice(0, 32) || 'unknown';
50
+ const tail = surface ? ` surface/${surface}` : '';
51
+ return `ruflo-funnel/messages ruflo/${clean(version)} (${clean(platform)}; ${clean(arch)})${tail}`;
52
+ }
53
+ /** Launch surfaces the UA may name. Closed set: an env value outside it is dropped. */
54
+ const LAUNCH_SURFACES = new Set(['plugin']);
55
+ /** The surface set by a launcher (plugins/ruflo-core/scripts/mcp-launch.cjs), if any. */
56
+ export function launchSurface(env = process.env) {
57
+ const v = env.RUFLO_LAUNCH_SURFACE;
58
+ return v && LAUNCH_SURFACES.has(v) ? v : undefined;
59
+ }
38
60
  const CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6h — matches server Cache-Control
39
61
  const CACHE_MAX_BYTES = 128 * 1024;
40
62
  const CACHE_MAX_MESSAGES = 200;
@@ -79,7 +101,7 @@ function httpsGet(url) {
79
101
  path: target.pathname + target.search,
80
102
  method: 'GET',
81
103
  headers: {
82
- 'User-Agent': 'ruflo-funnel/messages',
104
+ 'User-Agent': funnelUserAgent(),
83
105
  'Accept': 'application/json',
84
106
  },
85
107
  }, (res) => {
@@ -77,8 +77,8 @@ export interface NormalizedCreditError {
77
77
  /** Original provider error, preserved verbatim for --verbose paths. */
78
78
  cause?: unknown;
79
79
  }
80
- export type FunnelEventName = 'disclosure_shown' | 'funnel_disabled' | 'signup_opened' | 'account_created' | 'proxy_activated' | 'promo_impression' | 'promo_open' | 'sponsor_mode_enabled' | 'sponsor_mode_disabled' | 'sponsor_capacity_exhausted' | 'power_saver_enabled' | 'power_saver_disabled' | 'toggle_cooldown_blocked' | 'training_share_enabled' | 'training_share_disabled' | 'advisor_tip_enabled' | 'advisor_tip_disabled';
81
- export type FunnelSurface = 'statusline' | 'init' | 'credit_exhaustion';
80
+ export type FunnelEventName = 'disclosure_shown' | 'funnel_disabled' | 'signup_opened' | 'account_created' | 'proxy_activated' | 'promo_impression' | 'promo_open' | 'sponsor_mode_enabled' | 'sponsor_mode_disabled' | 'sponsor_capacity_exhausted' | 'power_saver_enabled' | 'power_saver_disabled' | 'toggle_cooldown_blocked' | 'training_share_enabled' | 'training_share_disabled' | 'advisor_tip_enabled' | 'advisor_tip_disabled' | 'init_completed' | 'first_agent_run' | 'session_active';
81
+ export type FunnelSurface = 'statusline' | 'init' | 'credit_exhaustion' | 'cli';
82
82
  export interface FunnelEvent {
83
83
  schemaVersion: 1;
84
84
  event: FunnelEventName;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The concrete result `ruflo init` ends with (adoption plan item 1): a quick,
3
+ * local snapshot of the user's own project, plus one prompt to paste into
4
+ * Claude Code. Replaces a list of commands with something about *their* repo.
5
+ *
6
+ * Local only: `git` with argument arrays (no shell), a hard timeout per call,
7
+ * no network. Any failure returns null and init prints nothing extra.
8
+ */
9
+ export interface ProjectSnapshot {
10
+ files: number;
11
+ languages: string[];
12
+ hotFile?: {
13
+ path: string;
14
+ changes: number;
15
+ };
16
+ testCommand?: string;
17
+ }
18
+ /** Snapshot of the project at `cwd`, or null when it is not a git repo with code. */
19
+ export declare function snapshotProject(cwd: string, timeoutMs?: number): ProjectSnapshot | null;
20
+ /** The one prompt to paste into Claude Code, specific to this project. */
21
+ export declare function firstPrompt(s: ProjectSnapshot | null): string;
22
+ /** Lines for init's closing summary. Empty when there is nothing useful to say. */
23
+ export declare function snapshotLines(s: ProjectSnapshot | null): string[];
24
+ //# sourceMappingURL=first-win.d.ts.map
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The concrete result `ruflo init` ends with (adoption plan item 1): a quick,
3
+ * local snapshot of the user's own project, plus one prompt to paste into
4
+ * Claude Code. Replaces a list of commands with something about *their* repo.
5
+ *
6
+ * Local only: `git` with argument arrays (no shell), a hard timeout per call,
7
+ * no network. Any failure returns null and init prints nothing extra.
8
+ */
9
+ import { spawnSync } from 'node:child_process';
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { extname, join } from 'node:path';
12
+ const LANGUAGE = {
13
+ '.ts': 'TypeScript', '.tsx': 'TypeScript', '.js': 'JavaScript', '.jsx': 'JavaScript', '.mjs': 'JavaScript', '.cjs': 'JavaScript',
14
+ '.py': 'Python', '.rs': 'Rust', '.go': 'Go', '.java': 'Java', '.kt': 'Kotlin', '.rb': 'Ruby', '.php': 'PHP',
15
+ '.cs': 'C#', '.cpp': 'C++', '.cc': 'C++', '.c': 'C', '.h': 'C', '.swift': 'Swift', '.scala': 'Scala',
16
+ '.vue': 'Vue', '.svelte': 'Svelte', '.dart': 'Dart', '.ex': 'Elixir', '.exs': 'Elixir', '.lua': 'Lua',
17
+ };
18
+ // Files a "riskiest module" suggestion should never point at.
19
+ const NOT_CODE = /(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|Cargo\.lock|go\.sum|CHANGELOG[^/]*|README[^/]*|\.github\/|\.claude\/|\.claude-flow\/|dist\/|build\/|node_modules\/)/i;
20
+ function git(cwd, args, timeoutMs) {
21
+ const r = spawnSync('git', args, { cwd, encoding: 'utf8', timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] });
22
+ return r.status === 0 && typeof r.stdout === 'string' ? r.stdout : null;
23
+ }
24
+ function testCommandFor(cwd) {
25
+ try {
26
+ const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
27
+ const t = pkg.scripts?.test;
28
+ if (t && !/no test specified/i.test(t))
29
+ return 'npm test';
30
+ }
31
+ catch { /* not a node project */ }
32
+ if (existsSync(join(cwd, 'Cargo.toml')))
33
+ return 'cargo test';
34
+ if (existsSync(join(cwd, 'go.mod')))
35
+ return 'go test ./...';
36
+ if (existsSync(join(cwd, 'pyproject.toml')) || existsSync(join(cwd, 'pytest.ini')))
37
+ return 'pytest';
38
+ return undefined;
39
+ }
40
+ /** Snapshot of the project at `cwd`, or null when it is not a git repo with code. */
41
+ export function snapshotProject(cwd, timeoutMs = 2000) {
42
+ try {
43
+ const listed = git(cwd, ['ls-files', '-z'], timeoutMs);
44
+ if (!listed)
45
+ return null;
46
+ const files = listed.split('\0').filter(Boolean);
47
+ if (files.length === 0)
48
+ return null;
49
+ const counts = new Map();
50
+ for (const f of files) {
51
+ const lang = LANGUAGE[extname(f).toLowerCase()];
52
+ if (lang)
53
+ counts.set(lang, (counts.get(lang) ?? 0) + 1);
54
+ }
55
+ const languages = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([l]) => l);
56
+ // Most-changed code file in the last 90 days (bounded to 500 commits).
57
+ let hotFile;
58
+ const log = git(cwd, ['log', '--since=90.days', '--max-count=500', '--name-only', '--pretty=format:'], timeoutMs);
59
+ if (log) {
60
+ const tracked = new Set(files);
61
+ const churn = new Map();
62
+ for (const line of log.split('\n')) {
63
+ const p = line.trim();
64
+ if (!p || NOT_CODE.test(p) || !tracked.has(p) || !LANGUAGE[extname(p).toLowerCase()])
65
+ continue;
66
+ churn.set(p, (churn.get(p) ?? 0) + 1);
67
+ }
68
+ const top = [...churn.entries()].sort((a, b) => b[1] - a[1])[0];
69
+ if (top && top[1] >= 2)
70
+ hotFile = { path: top[0], changes: top[1] };
71
+ }
72
+ return { files: files.length, languages, hotFile, testCommand: testCommandFor(cwd) };
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ /** The one prompt to paste into Claude Code, specific to this project. */
79
+ export function firstPrompt(s) {
80
+ if (s?.hotFile) {
81
+ const verify = s.testCommand ? ` and run \`${s.testCommand}\` to prove it` : '';
82
+ return `Use a ruflo swarm to review ${s.hotFile.path} (the most-changed file here): find the riskiest logic, add tests for it${verify}, and summarise what you changed.`;
83
+ }
84
+ return 'Use a ruflo swarm to review this repo: find the riskiest module, write tests for it, and summarise what you changed.';
85
+ }
86
+ /** Lines for init's closing summary. Empty when there is nothing useful to say. */
87
+ export function snapshotLines(s) {
88
+ if (!s)
89
+ return [];
90
+ const lines = [`${s.files.toLocaleString('en-US')} tracked file${s.files === 1 ? '' : 's'}${s.languages.length ? ` · ${s.languages.join(', ')}` : ''}`];
91
+ if (s.hotFile)
92
+ lines.push(`Most-changed file (90 days): ${s.hotFile.path} (${s.hotFile.changes} commits)`);
93
+ if (s.testCommand)
94
+ lines.push(`Tests: ${s.testCommand}`);
95
+ return lines;
96
+ }
97
+ //# sourceMappingURL=first-win.js.map
@@ -12,6 +12,7 @@ import { validateIdentifier, validateText, validateAgentSpawn } from './validate
12
12
  import { executeAgentTask } from './agent-execute-core.js';
13
13
  import { AGENT_HEALTH_WINDOW, summarizeAgentExecutions } from './agent-health-metrics.js';
14
14
  import { pheromoneAgentEligibility } from './swarm-tools.js';
15
+ import { noteFirstAgentRun } from '../funnel/activation.js';
15
16
  // Storage paths
16
17
  const STORAGE_DIR = '.claude-flow';
17
18
  const AGENT_DIR = 'agents';
@@ -425,6 +426,7 @@ export const agentTools = [
425
426
  catch { /* COW branch is best-effort; agent already registered */ }
426
427
  }
427
428
  // Include deterministic codemod routing info if applicable
429
+ noteFirstAgentRun('cli'); // activation funnel (opt-in; no-op without telemetry consent)
428
430
  const response = {
429
431
  success: true,
430
432
  agentId,
@@ -434,6 +434,10 @@ export const memoryTools = [
434
434
  items: { type: 'string' },
435
435
  description: 'Optional tags for filtering',
436
436
  },
437
+ metadata: {
438
+ type: 'object',
439
+ description: 'Optional JSON object stored with the entry (e.g. { confidence: 0.9 }, which AutoMemoryBridge reads for sync eligibility). Replaces any existing metadata on upsert.',
440
+ },
437
441
  ttl: { type: 'number', description: 'Time-to-live in seconds (optional)' },
438
442
  upsert: { type: 'boolean', description: 'Update existing key instead of failing (default: true, matching CLI `memory store`; set false for strict-insert). #2775 parity.' },
439
443
  provenance_type: {
@@ -465,6 +469,18 @@ export const memoryTools = [
465
469
  // ADR-323: leave undefined when omitted so storeEntry's own default
466
470
  // ('unknown') applies uniformly across the CLI, MCP tool, and other callers.
467
471
  const provenanceType = input.provenance_type;
472
+ // #1426: metadata must be a plain JSON object; reject arrays, null and scalars.
473
+ const rawMetadata = input.metadata;
474
+ if (rawMetadata !== undefined && (typeof rawMetadata !== 'object' || rawMetadata === null || Array.isArray(rawMetadata))) {
475
+ return {
476
+ success: false,
477
+ key,
478
+ stored: false,
479
+ hasEmbedding: false,
480
+ error: 'metadata must be a JSON object',
481
+ };
482
+ }
483
+ const metadata = rawMetadata;
468
484
  if (!value) {
469
485
  return {
470
486
  success: false,
@@ -488,6 +504,7 @@ export const memoryTools = [
488
504
  namespace,
489
505
  generateEmbeddingFlag: true,
490
506
  tags,
507
+ metadata,
491
508
  ttl,
492
509
  upsert,
493
510
  provenanceType,
@@ -54,6 +54,7 @@ import { fileURLToPath } from 'node:url';
54
54
  import { homedir } from 'node:os';
55
55
  import { ownPluginDirs } from '../plugins/own-install.js';
56
56
  import { runFlywheelWorker } from '../services/harness-flywheel-runtime.js';
57
+ import { benchVerify, gepaOp } from '../metaharness/darwin-lib.js';
57
58
  import { evaluatePolicyRequest } from '../services/policy-runtime.js';
58
59
  import { listFlywheelReceipts, promoteFlywheelCandidate, readFlywheelTransactionState, resetSequentialEvidence, verifyFlywheelLedger, } from '../services/flywheel-transaction.js';
59
60
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -523,6 +524,13 @@ export const metaharnessTools = [
523
524
  required: ['op'],
524
525
  },
525
526
  handler: async (input) => {
527
+ // ADR-526: verify runs in-process (darwin is a regular dependency);
528
+ // create still goes through the darwin CLI, which owns the scaffold task.
529
+ if (input.op === 'verify') {
530
+ const inProc = await benchVerify(String(input.suite ?? ''));
531
+ if (inProc)
532
+ return { success: inProc.exitCode === 0, data: inProc.json, degraded: false, exitCode: inProc.exitCode };
533
+ }
526
534
  const args = ['--op', String(input.op)];
527
535
  if (input.repo)
528
536
  args.push('--repo', String(input.repo));
@@ -665,6 +673,17 @@ export const metaharnessTools = [
665
673
  required: ['op'],
666
674
  },
667
675
  handler: async (input) => {
676
+ // ADR-526: GEPA genome ops run in-process; the plugin script is the fallback.
677
+ const inProc = await gepaOp({
678
+ op: String(input.op),
679
+ path: input.path ? String(input.path) : undefined,
680
+ transcript: input.transcript ? String(input.transcript) : undefined,
681
+ ext: input.ext ? String(input.ext) : undefined,
682
+ glob: input.glob ? String(input.glob) : undefined,
683
+ alertOnInvalid: input.alertOnInvalid === true,
684
+ });
685
+ if (inProc)
686
+ return { success: inProc.exitCode === 0, data: inProc.json, degraded: false, exitCode: inProc.exitCode };
668
687
  const args = ['--op', String(input.op)];
669
688
  if (input.path)
670
689
  args.push('--path', String(input.path));
@@ -9,6 +9,7 @@ import { join } from 'node:path';
9
9
  import { nullPrototypeRecords, readPreservedJsonStore } from './record-store.js';
10
10
  import { getProjectCwd } from './types.js';
11
11
  import { validateIdentifier } from './validate-input.js';
12
+ import { noteFirstAgentRun } from '../funnel/activation.js';
12
13
  import { createApscState, isApscAgentEligible, normalizeApscConfig, recordApscSignal, } from '../services/pheromone-adaptive.js';
13
14
  // Swarm state persistence
14
15
  const SWARM_DIR = '.claude-flow/swarm';
@@ -283,6 +284,7 @@ export const swarmTools = [
283
284
  const store = loadSwarmStore();
284
285
  store.swarms[swarmId] = swarmState;
285
286
  saveSwarmStore(store);
287
+ noteFirstAgentRun('cli'); // activation funnel (opt-in; no-op without telemetry consent)
286
288
  return {
287
289
  success: true,
288
290
  swarmId,
@@ -74,6 +74,8 @@ export declare function bridgeStoreEntry(options: {
74
74
  namespace?: string;
75
75
  generateEmbeddingFlag?: boolean;
76
76
  tags?: string[];
77
+ /** #1426: JSON object persisted to the metadata column. */
78
+ metadata?: Record<string, unknown>;
77
79
  ttl?: number;
78
80
  dbPath?: string;
79
81
  upsert?: boolean;
@@ -1182,7 +1182,7 @@ export async function bridgeStoreEntry(options) {
1182
1182
  }
1183
1183
  catch { /* vector_indexes may not exist on legacy DBs — fall through */ }
1184
1184
  const stmt = ctx.db.prepare(insertSql);
1185
- const insert = () => stmt.run(id, key, namespace, value, embeddingJson, dimensions || null, model, tags.length > 0 ? JSON.stringify(tags) : null, '{}', provenanceType, now, now, ttl ? now + (ttl * 1000) : null);
1185
+ const insert = () => stmt.run(id, key, namespace, value, embeddingJson, dimensions || null, model, tags.length > 0 ? JSON.stringify(tags) : null, JSON.stringify(options.metadata ?? {}), provenanceType, now, now, ttl ? now + (ttl * 1000) : null);
1186
1186
  let runResult;
1187
1187
  try {
1188
1188
  runResult = options.appendOnly
@@ -444,6 +444,8 @@ export declare function storeEntry(options: {
444
444
  namespace?: string;
445
445
  generateEmbeddingFlag?: boolean;
446
446
  tags?: string[];
447
+ /** #1426: JSON object persisted to the metadata column. */
448
+ metadata?: Record<string, unknown>;
447
449
  ttl?: number;
448
450
  dbPath?: string;
449
451
  upsert?: boolean;
@@ -2733,7 +2733,7 @@ export async function storeEntry(options) {
2733
2733
  embeddingDimensions,
2734
2734
  embeddingModel,
2735
2735
  tags.length > 0 ? JSON.stringify(tags) : null,
2736
- '{}',
2736
+ JSON.stringify(options.metadata ?? {}),
2737
2737
  persistedProvenance,
2738
2738
  now,
2739
2739
  now,
@@ -0,0 +1,68 @@
1
+ /**
2
+ * In-process access to @metaharness/darwin (ADR-526).
3
+ *
4
+ * darwin is a regular dependency of the CLI, so the long-lived MCP server can
5
+ * import it once and answer `metaharness_bench --op verify` and every
6
+ * `metaharness_gepa` op without spawning `node bench.mjs` → `node darwin/cli.js`
7
+ * per call. Each function returns the same {exitCode, json} pair the plugin
8
+ * script would have produced, so callers keep the existing
9
+ * {success, data, degraded, exitCode} contract.
10
+ *
11
+ * Graceful degradation (ADR-150 rule 3) still holds: if the import fails, every
12
+ * function returns null and the caller falls back to the subprocess path.
13
+ */
14
+ import { readFileSync } from 'node:fs';
15
+ export interface DarwinResult {
16
+ exitCode: number;
17
+ /** Parsed payload, or null where the script would have printed only to stderr. */
18
+ json: unknown;
19
+ }
20
+ type DarwinRoot = {
21
+ bench: {
22
+ loadSuite(path: string): Promise<{
23
+ id: string;
24
+ version: string;
25
+ tasks: unknown[];
26
+ }>;
27
+ verifySuite(suite: unknown): {
28
+ ok: boolean;
29
+ actual: string;
30
+ };
31
+ };
32
+ };
33
+ type DarwinGepa = {
34
+ loadGenome(read: typeof readFileSync, path: string): unknown;
35
+ loadCand6Genome(): unknown;
36
+ validateGenome(genome: unknown): unknown[];
37
+ buildSystemFromGenome(genome: unknown, ext?: string, glob?: string): string;
38
+ analyzeTranscript(entries: unknown[]): unknown;
39
+ CAND6_GENOME_PATH: string;
40
+ };
41
+ export declare function loadDarwin(): Promise<DarwinRoot | null>;
42
+ export declare function loadDarwinGepa(): Promise<DarwinGepa | null>;
43
+ /** Test hook: forget the cached modules. */
44
+ export declare function resetDarwinCache(): void;
45
+ /**
46
+ * `bench verify <suite>` in-process. Mirrors plugins/ruflo-metaharness/scripts/bench.mjs:
47
+ * exit 2 + no payload for a missing path, exit 1 when the suite fails to load
48
+ * (darwin's loadSuite throws on tamper) or its hash does not match, exit 0 otherwise.
49
+ * Returns null when darwin cannot be imported.
50
+ */
51
+ export declare function benchVerify(suitePath: string): Promise<DarwinResult | null>;
52
+ export interface GepaInput {
53
+ op: string;
54
+ path?: string;
55
+ transcript?: string;
56
+ ext?: string;
57
+ glob?: string;
58
+ alertOnInvalid?: boolean;
59
+ }
60
+ /**
61
+ * GEPA genome ops in-process. Mirrors plugins/ruflo-metaharness/scripts/gepa.mjs,
62
+ * including its exit codes: 2 for input errors (no payload), 1 for an invalid
63
+ * genome with alertOnInvalid, 0 otherwise. Returns null when darwin/gepa cannot
64
+ * be imported.
65
+ */
66
+ export declare function gepaOp(input: GepaInput): Promise<DarwinResult | null>;
67
+ export {};
68
+ //# sourceMappingURL=darwin-lib.d.ts.map