@claude-flow/cli 3.63.9 → 3.65.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 (90) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/catalog-manifest.json +3 -3
  3. package/dist/src/commands/cost.d.ts +18 -0
  4. package/dist/src/commands/cost.js +195 -0
  5. package/dist/src/commands/doctor-auto-approve.d.ts +9 -0
  6. package/dist/src/commands/doctor-auto-approve.js +32 -0
  7. package/dist/src/commands/doctor.js +28 -2
  8. package/dist/src/commands/hive-mind-worker.d.ts +2 -0
  9. package/dist/src/commands/hive-mind-worker.js +9 -3
  10. package/dist/src/commands/hive-mind.js +15 -1
  11. package/dist/src/commands/hooks.js +45 -0
  12. package/dist/src/commands/index.js +2 -0
  13. package/dist/src/commands/init.js +76 -2
  14. package/dist/src/commands/mods.js +13 -0
  15. package/dist/src/commands/plugins-paths.d.ts +13 -0
  16. package/dist/src/commands/plugins-paths.js +55 -0
  17. package/dist/src/commands/plugins-verify.d.ts +9 -0
  18. package/dist/src/commands/plugins-verify.js +92 -0
  19. package/dist/src/commands/plugins.js +13 -1
  20. package/dist/src/cost/attribution.d.ts +35 -0
  21. package/dist/src/cost/attribution.js +101 -0
  22. package/dist/src/cost/budget-file.d.ts +57 -0
  23. package/dist/src/cost/budget-file.js +113 -0
  24. package/dist/src/cost/litellm.d.ts +69 -0
  25. package/dist/src/cost/litellm.js +135 -0
  26. package/dist/src/init/executor.d.ts +7 -2
  27. package/dist/src/init/executor.js +64 -10
  28. package/dist/src/init/helper-commands.d.ts +16 -0
  29. package/dist/src/init/helper-commands.js +73 -0
  30. package/dist/src/init/risky-settings-gate.d.ts +39 -0
  31. package/dist/src/init/risky-settings-gate.js +130 -0
  32. package/dist/src/init/settings-risk-scanner.d.ts +35 -0
  33. package/dist/src/init/settings-risk-scanner.js +85 -0
  34. package/dist/src/init/types.d.ts +12 -0
  35. package/dist/src/mcp-tools/capability-brain.d.ts +1 -1
  36. package/dist/src/mcp-tools/capability-brain.js +1 -1
  37. package/dist/src/mcp-tools/hooks-tools.d.ts +1 -0
  38. package/dist/src/mcp-tools/hooks-tools.js +38 -0
  39. package/dist/src/memory/verified-guards.d.ts +70 -0
  40. package/dist/src/memory/verified-guards.js +101 -0
  41. package/dist/src/memory/verified-outcomes.d.ts +228 -0
  42. package/dist/src/memory/verified-outcomes.js +372 -0
  43. package/dist/src/memory/verified-reverify.d.ts +63 -0
  44. package/dist/src/memory/verified-reverify.js +174 -0
  45. package/dist/src/missions/schemas.d.ts +36 -36
  46. package/dist/src/mods/apply.d.ts +2 -0
  47. package/dist/src/mods/apply.js +68 -8
  48. package/dist/src/mods/command-registry/schema.d.ts +222 -222
  49. package/dist/src/mods/compact-snapshots.d.ts +21 -0
  50. package/dist/src/mods/compact-snapshots.js +45 -0
  51. package/dist/src/mods/headless-check.d.ts +58 -0
  52. package/dist/src/mods/headless-check.js +137 -0
  53. package/dist/src/mods/headless-plugins.d.ts +44 -0
  54. package/dist/src/mods/headless-plugins.js +92 -0
  55. package/dist/src/mods/install-trust.d.ts +28 -0
  56. package/dist/src/mods/install-trust.js +127 -0
  57. package/dist/src/mods/install.d.ts +19 -0
  58. package/dist/src/mods/install.js +38 -1
  59. package/dist/src/mods/plugin-paths.d.ts +60 -0
  60. package/dist/src/mods/plugin-paths.js +131 -0
  61. package/dist/src/mods/plugin-repair.d.ts +8 -0
  62. package/dist/src/mods/plugin-repair.js +7 -0
  63. package/dist/src/plugins/marketplace-trust/scan.d.ts +23 -0
  64. package/dist/src/plugins/marketplace-trust/scan.js +255 -0
  65. package/dist/src/plugins/marketplace-trust/signature.d.ts +53 -0
  66. package/dist/src/plugins/marketplace-trust/signature.js +140 -0
  67. package/dist/src/plugins/marketplace-trust/verify-status.d.ts +42 -0
  68. package/dist/src/plugins/marketplace-trust/verify-status.js +110 -0
  69. package/dist/src/services/headless-worker-executor.d.ts +2 -0
  70. package/dist/src/services/headless-worker-executor.js +13 -3
  71. package/package.json +2 -1
  72. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +8 -1
  73. package/plugins/ruflo-metaharness/CHANGELOG.md +6 -0
  74. package/plugins/ruflo-metaharness/README.md +14 -1
  75. package/plugins/ruflo-metaharness/agents/metaharness-architect.md +2 -0
  76. package/plugins/ruflo-metaharness/hooks/hooks.json +16 -2
  77. package/plugins/ruflo-metaharness/scripts/_redblue.mjs +12 -2
  78. package/plugins/ruflo-metaharness/scripts/_secret-env.cjs +50 -0
  79. package/plugins/ruflo-metaharness/scripts/_secret.mjs +35 -0
  80. package/plugins/ruflo-metaharness/scripts/smoke.sh +7 -7
  81. package/plugins/ruflo-metaharness/skills/harness-bench/SKILL.md +2 -1
  82. package/plugins/ruflo-metaharness/skills/harness-drift-from-history/SKILL.md +1 -1
  83. package/plugins/ruflo-metaharness/skills/harness-evolve/SKILL.md +2 -1
  84. package/plugins/ruflo-metaharness/skills/harness-gepa/SKILL.md +1 -1
  85. package/plugins/ruflo-metaharness/skills/harness-learn/SKILL.md +2 -1
  86. package/plugins/ruflo-metaharness/skills/harness-mint/SKILL.md +2 -1
  87. package/plugins/ruflo-metaharness/skills/harness-oia-audit/SKILL.md +1 -0
  88. package/plugins/ruflo-metaharness/skills/harness-score/SKILL.md +1 -1
  89. package/plugins/ruflo-metaharness/skills/harness-security-bench/SKILL.md +2 -1
  90. package/plugins/ruflo-metaharness/skills/harness-similarity/SKILL.md +1 -1
@@ -0,0 +1,130 @@
1
+ /**
2
+ * ADR-509 §2.3: the stop `ruflo init` / `ruflo init upgrade --settings` makes
3
+ * before carrying a project settings file's auto-approve keys forward.
4
+ *
5
+ * The check runs BEFORE anything is written, so a stop leaves the project as
6
+ * it was. `keep` proceeds (the keys are still reported); `drop` proceeds and
7
+ * the merged file is written without them.
8
+ */
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import { isRufloHelperCommand } from './helper-commands.js';
12
+ import { CRITICAL_HELPERS } from './helper-refresh.js';
13
+ import { HELPERS_MANIFEST_FILE, sha256Hex, verifyHelpersManifest } from './helper-signing.js';
14
+ import { dropAutoApproveKeys, formatRiskFindingsAsWarnings, scanProjectAutoApproveKeys, } from './settings-risk-scanner.js';
15
+ export function scanAutoApprove(settings) {
16
+ return scanProjectAutoApproveKeys(settings, isRufloHelperCommand);
17
+ }
18
+ /** Reads a settings file and judges it; null when the file is absent, unreadable or not a JSON object. */
19
+ export function checkRiskySettings(settingsPath, mode = 'stop') {
20
+ if (!fs.existsSync(settingsPath))
21
+ return null;
22
+ let parsed;
23
+ try {
24
+ parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
30
+ return null;
31
+ const findings = scanAutoApprove(parsed);
32
+ if (findings.length === 0)
33
+ return null;
34
+ return { mode, findings, lines: formatRiskFindingsAsWarnings(findings), stop: mode === 'stop' };
35
+ }
36
+ /**
37
+ * ADR-509 §2.3: the helpers ruflo's hooks launch, as the PROJECT ships them.
38
+ * `init` keeps an existing `.claude/helpers/<name>` unless --force, and the
39
+ * project-scope hook commands prefer the project's copy, so a repository that
40
+ * ships its own hook-handler.cjs runs it under an exact ruflo hook command. A
41
+ * helper is trusted when its bytes are the copy this ruflo ships, or its
42
+ * SHA-256 is in a helpers manifest signed with ruflo's key (this ruflo's, or
43
+ * the project's own, which an older signed release left). Anything else is a
44
+ * finding. A `.LOCKED` helpers dir (deliberate local edits) is skipped, and with
45
+ * no ground truth at all this fails open. `pubkeyPem` is injectable for tests.
46
+ */
47
+ export function untrustedProjectHelpers(targetDir, sourceHelpersDir, pubkeyPem) {
48
+ const helpersDir = path.join(targetDir, '.claude', 'helpers');
49
+ // .LOCKED is the sanctioned marker for deliberately edited helpers (helper-refresh honours it too).
50
+ if (fs.existsSync(path.join(helpersDir, '.LOCKED')))
51
+ return [];
52
+ const signed = (dir) => {
53
+ if (!dir)
54
+ return null;
55
+ try {
56
+ return verifyHelpersManifest(fs.readFileSync(path.join(dir, HELPERS_MANIFEST_FILE), 'utf-8'), pubkeyPem);
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ };
62
+ const manifests = [signed(sourceHelpersDir), signed(helpersDir)].filter((m) => m !== null);
63
+ // No ground truth at all (no shipped copy, no signed manifest): fail open, as
64
+ // helper-integrity does, rather than flag every install.
65
+ if (!sourceHelpersDir && manifests.length === 0)
66
+ return [];
67
+ const out = [];
68
+ for (const name of CRITICAL_HELPERS) {
69
+ const file = path.join(helpersDir, name);
70
+ let bytes;
71
+ try {
72
+ if (!fs.lstatSync(file).isFile())
73
+ throw new Error('not a regular file');
74
+ bytes = fs.readFileSync(file);
75
+ }
76
+ catch (error) {
77
+ if (error.code === 'ENOENT')
78
+ continue;
79
+ out.push({ target: { kind: 'helper', name }, location: `.claude/helpers/${name}`, snippet: '(unreadable or not a regular file)', reason: "is not ruflo's signed helper; ruflo's hooks would run it" });
80
+ continue;
81
+ }
82
+ if (sourceHelpersDir) {
83
+ try {
84
+ if (bytes.equals(fs.readFileSync(path.join(sourceHelpersDir, name))))
85
+ continue;
86
+ }
87
+ catch {
88
+ // not shipped by this ruflo: fall through to the signed manifests
89
+ }
90
+ }
91
+ const hash = sha256Hex(bytes);
92
+ if (manifests.some((m) => m.files[name] === hash))
93
+ continue;
94
+ out.push({
95
+ target: { kind: 'helper', name },
96
+ location: `.claude/helpers/${name}`,
97
+ snippet: `sha256 ${hash.slice(0, 16)}…`,
98
+ reason: "differs from ruflo's signed helper; init keeps it and ruflo's hooks would run it",
99
+ });
100
+ }
101
+ return out;
102
+ }
103
+ /**
104
+ * The init gate: the settings file's auto-approve keys plus the project's
105
+ * untrusted helpers. Null when there is nothing to report.
106
+ */
107
+ export function checkInitTrust(targetDir, mode, sourceHelpersDir) {
108
+ const settings = checkRiskySettings(path.join(targetDir, '.claude', 'settings.json'), mode);
109
+ const helpers = untrustedProjectHelpers(targetDir, sourceHelpersDir);
110
+ const findings = [...(settings?.findings ?? []), ...helpers];
111
+ if (findings.length === 0)
112
+ return null;
113
+ return { mode, findings, lines: formatRiskFindingsAsWarnings(findings), stop: mode === 'stop' };
114
+ }
115
+ /** `drop`: the settings without the auto-approve keys; otherwise the settings unchanged. */
116
+ export function applyRiskyMode(settings, mode) {
117
+ if (mode !== 'drop')
118
+ return { settings, dropped: 0 };
119
+ const findings = scanAutoApprove(settings);
120
+ return findings.length ? { settings: dropAutoApproveKeys(settings, findings), dropped: findings.length } : { settings, dropped: 0 };
121
+ }
122
+ /** Reads `--keep-risky-settings` / `--drop-risky-settings` (camel or kebab); both at once is an error. */
123
+ export function riskyModeFromFlags(flags) {
124
+ const keep = flags.keepRiskySettings === true || flags['keep-risky-settings'] === true;
125
+ const drop = flags.dropRiskySettings === true || flags['drop-risky-settings'] === true;
126
+ if (keep && drop)
127
+ return { error: '--keep-risky-settings and --drop-risky-settings cannot be used together' };
128
+ return keep ? 'keep' : drop ? 'drop' : 'stop';
129
+ }
130
+ //# sourceMappingURL=risky-settings-gate.js.map
@@ -44,4 +44,39 @@ export declare function scanAllowRuleForRisk(rule: string): string[];
44
44
  export declare function scanSettingsForRisk(settings: Record<string, unknown>): SettingsRiskFinding[];
45
45
  /** Formats findings as short, human-readable CLI warning lines. */
46
46
  export declare function formatRiskFindingsAsWarnings(findings: SettingsRiskFinding[]): string[];
47
+ /** What a finding points at, precisely enough for dropAutoApproveKeys to remove only that. */
48
+ export type AutoApproveTarget = {
49
+ kind: 'enableAllProjectMcpServers';
50
+ } | {
51
+ kind: 'enabledMcpjsonServers';
52
+ } | {
53
+ kind: 'defaultMode';
54
+ } | {
55
+ kind: 'allow';
56
+ index: number;
57
+ } | {
58
+ kind: 'hook';
59
+ event: string;
60
+ group: number;
61
+ hook: number;
62
+ }
63
+ /** A project `.claude/helpers/<name>` that is not ruflo's signed copy (init keeps it without --force). */
64
+ | {
65
+ kind: 'helper';
66
+ name: string;
67
+ };
68
+ export interface AutoApproveFinding extends SettingsRiskFinding {
69
+ target: AutoApproveTarget;
70
+ }
71
+ /**
72
+ * The auto-approve keys of a PROJECT settings file (`.claude/settings.json` or
73
+ * `.claude/settings.local.json`). `isRufloHook` decides which hook commands are
74
+ * ruflo's own helpers (injected so this module stays dependency-free).
75
+ */
76
+ export declare function scanProjectAutoApproveKeys(settings: Record<string, unknown>, isRufloHook: (command: string) => boolean): AutoApproveFinding[];
77
+ /** A deep copy of `settings` without the keys/entries the findings point at. Never mutates its input. */
78
+ export declare function dropAutoApproveKeys(settings: Record<string, unknown>, findings: readonly AutoApproveFinding[]): Record<string, unknown>;
79
+ /** What `init` does when the existing settings carry auto-approve keys. */
80
+ export type RiskySettingsMode = 'stop' | 'keep' | 'drop';
81
+ export declare const RISKY_SETTINGS_HINT = "Re-run with --drop-risky-settings to write the merged settings without these keys (and replace any flagged helper with ruflo's copy), or --keep-risky-settings to keep them.";
47
82
  //# sourceMappingURL=settings-risk-scanner.d.ts.map
@@ -177,4 +177,89 @@ export function scanSettingsForRisk(settings) {
177
177
  export function formatRiskFindingsAsWarnings(findings) {
178
178
  return findings.map((f) => `${f.location}: ${f.reason} — "${f.snippet}"`);
179
179
  }
180
+ const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
181
+ /** A bare `Bash`, `Bash(*)`, `Bash(*:*)` or a `*` wildcard pre-approves every command. */
182
+ function isBlanketAllow(rule) {
183
+ const t = rule.trim();
184
+ return t === '*' || /^Bash$/i.test(t) || /^Bash\(\s*\*\s*(:\s*\*\s*)?\)$/i.test(t) || /^\*\s*\(.*\)$/.test(t);
185
+ }
186
+ /**
187
+ * The auto-approve keys of a PROJECT settings file (`.claude/settings.json` or
188
+ * `.claude/settings.local.json`). `isRufloHook` decides which hook commands are
189
+ * ruflo's own helpers (injected so this module stays dependency-free).
190
+ */
191
+ export function scanProjectAutoApproveKeys(settings, isRufloHook) {
192
+ const out = [];
193
+ if (settings.enableAllProjectMcpServers === true) {
194
+ out.push({ target: { kind: 'enableAllProjectMcpServers' }, location: 'enableAllProjectMcpServers', snippet: 'true', reason: 'starts every .mcp.json server in the project without asking' });
195
+ }
196
+ const enabled = settings.enabledMcpjsonServers;
197
+ if (Array.isArray(enabled) && enabled.length > 0) {
198
+ out.push({ target: { kind: 'enabledMcpjsonServers' }, location: 'enabledMcpjsonServers', snippet: buildSnippet(enabled.map(String).join(', ')), reason: 'pre-approves .mcp.json servers before the trust dialog' });
199
+ }
200
+ const permissions = isRecord(settings.permissions) ? settings.permissions : undefined;
201
+ if (permissions?.defaultMode === 'bypassPermissions') {
202
+ out.push({ target: { kind: 'defaultMode' }, location: 'permissions.defaultMode', snippet: 'bypassPermissions', reason: 'turns off every permission prompt' });
203
+ }
204
+ if (Array.isArray(permissions?.allow)) {
205
+ permissions.allow.forEach((rule, index) => {
206
+ if (typeof rule === 'string' && isBlanketAllow(rule)) {
207
+ out.push({ target: { kind: 'allow', index }, location: `permissions.allow[${index}]`, snippet: buildSnippet(rule), reason: 'pre-approves every command (bare Bash or * wildcard)' });
208
+ }
209
+ });
210
+ }
211
+ const hooks = isRecord(settings.hooks) ? settings.hooks : undefined;
212
+ for (const [event, groups] of Object.entries(hooks ?? {})) {
213
+ if (!Array.isArray(groups))
214
+ continue;
215
+ groups.forEach((g, group) => {
216
+ const list = isRecord(g) && Array.isArray(g.hooks) ? g.hooks : [];
217
+ list.forEach((h, hook) => {
218
+ if (!isRecord(h) || typeof h.command !== 'string' || isRufloHook(h.command))
219
+ return;
220
+ out.push({ target: { kind: 'hook', event, group, hook }, location: `hooks.${event}[${group}].hooks[${hook}]`, snippet: buildSnippet(h.command), reason: 'runs a command that is not a ruflo helper' });
221
+ });
222
+ });
223
+ }
224
+ return out;
225
+ }
226
+ /** A deep copy of `settings` without the keys/entries the findings point at. Never mutates its input. */
227
+ export function dropAutoApproveKeys(settings, findings) {
228
+ const next = JSON.parse(JSON.stringify(settings));
229
+ const perms = isRecord(next.permissions) ? next.permissions : undefined;
230
+ const allowDrop = new Set();
231
+ const hookDrop = new Set();
232
+ for (const { target: t } of findings) {
233
+ if (t.kind === 'enableAllProjectMcpServers')
234
+ delete next.enableAllProjectMcpServers;
235
+ else if (t.kind === 'enabledMcpjsonServers')
236
+ delete next.enabledMcpjsonServers;
237
+ else if (t.kind === 'defaultMode' && perms)
238
+ delete perms.defaultMode;
239
+ else if (t.kind === 'allow')
240
+ allowDrop.add(t.index);
241
+ else if (t.kind === 'hook')
242
+ hookDrop.add(`${t.event}\u0000${t.group}\u0000${t.hook}`);
243
+ }
244
+ if (perms && Array.isArray(perms.allow))
245
+ perms.allow = perms.allow.filter((_, i) => !allowDrop.has(i));
246
+ if (isRecord(next.hooks) && hookDrop.size) {
247
+ for (const [event, groups] of Object.entries(next.hooks)) {
248
+ if (!Array.isArray(groups))
249
+ continue;
250
+ const kept = groups
251
+ .map((g, gi) => (isRecord(g) && Array.isArray(g.hooks) ? { ...g, hooks: g.hooks.filter((_, hi) => !hookDrop.has(`${event}\u0000${gi}\u0000${hi}`)) } : g))
252
+ .filter((g) => !(isRecord(g) && Array.isArray(g.hooks) && g.hooks.length === 0));
253
+ if (kept.length)
254
+ next.hooks[event] = kept;
255
+ else
256
+ delete next.hooks[event];
257
+ }
258
+ // No hooks left: drop the key, so init writes its own hooks as it does for a file without any.
259
+ if (Object.keys(next.hooks).length === 0)
260
+ delete next.hooks;
261
+ }
262
+ return next;
263
+ }
264
+ export const RISKY_SETTINGS_HINT = 'Re-run with --drop-risky-settings to write the merged settings without these keys (and replace any flagged helper with ruflo\'s copy), or --keep-risky-settings to keep them.';
180
265
  //# sourceMappingURL=settings-risk-scanner.js.map
@@ -296,6 +296,14 @@ export interface InitOptions {
296
296
  * GitHub contributor graph. Pass `--attribution` to opt in.
297
297
  */
298
298
  attribution?: boolean;
299
+ /**
300
+ * ADR-509 §2.3 — what to do when the existing project settings.json carries
301
+ * auto-approve keys (enableAllProjectMcpServers, enabledMcpjsonServers,
302
+ * bypassPermissions, blanket Bash allows, non-ruflo hooks): `stop`
303
+ * (default — nothing is written), `keep` (--keep-risky-settings) or `drop`
304
+ * (--drop-risky-settings).
305
+ */
306
+ riskySettings?: 'stop' | 'keep' | 'drop';
299
307
  }
300
308
  /**
301
309
  * Default init options - full V3 setup
@@ -334,5 +342,9 @@ export interface InitResult {
334
342
  * init — surfaced so a user can review before trusting it.
335
343
  */
336
344
  warnings?: string[];
345
+ /** ADR-509 §2.3 — auto-approve keys found in the existing project settings.json. */
346
+ riskySettings?: string[];
347
+ /** ADR-509 §2.3 — init stopped before writing anything because of riskySettings. */
348
+ stoppedOnRiskySettings?: boolean;
337
349
  }
338
350
  //# sourceMappingURL=types.d.ts.map
@@ -97,7 +97,7 @@ export interface CapabilityBrain {
97
97
  };
98
98
  }
99
99
  /** Top-level commands from the CLI command loader. Kept as a checked manifest. */
100
- export declare const RUFLO_CLI_COMMANDS: readonly ["init", "start", "status", "task", "session", "agent", "swarm", "memory", "mcp", "config", "migrate", "hooks", "workflow", "hive-mind", "process", "daemon", "version", "neural", "security", "performance", "providers", "plugins", "deployment", "claims", "policy", "embeddings", "completions", "doctor", "verify", "analyze", "route", "progress", "issues", "update", "ruvector", "benchmark", "guidance", "appliance", "appliance-advanced", "transfer-store", "cleanup", "autopilot", "gaia-bench", "metaharness", "eject", "funnel", "settings", "auth", "proxy", "advisor", "spinner", "announcements", "tui", "harness"];
100
+ export declare const RUFLO_CLI_COMMANDS: readonly ["init", "start", "status", "task", "session", "agent", "swarm", "memory", "mcp", "config", "migrate", "hooks", "workflow", "hive-mind", "process", "daemon", "version", "neural", "security", "performance", "providers", "plugins", "deployment", "claims", "policy", "embeddings", "completions", "doctor", "verify", "analyze", "route", "progress", "issues", "update", "ruvector", "benchmark", "guidance", "appliance", "appliance-advanced", "transfer-store", "cleanup", "autopilot", "gaia-bench", "metaharness", "eject", "funnel", "settings", "auth", "proxy", "advisor", "spinner", "cost", "announcements", "tui", "harness"];
101
101
  export interface ImplementationLoopStep {
102
102
  id: string;
103
103
  name: string;
@@ -15,7 +15,7 @@ export const RUFLO_CLI_COMMANDS = [
15
15
  'doctor', 'verify', 'analyze', 'route', 'progress', 'issues', 'update',
16
16
  'ruvector', 'benchmark', 'guidance', 'appliance', 'appliance-advanced',
17
17
  'transfer-store', 'cleanup', 'autopilot', 'gaia-bench', 'metaharness',
18
- 'eject', 'funnel', 'settings', 'auth', 'proxy', 'advisor', 'spinner',
18
+ 'eject', 'funnel', 'settings', 'auth', 'proxy', 'advisor', 'spinner', 'cost',
19
19
  'announcements', 'tui', 'harness',
20
20
  ];
21
21
  export const IMPLEMENTATION_LOOP = [
@@ -105,6 +105,7 @@ export declare const hooksTaskCompleted: MCPTool;
105
105
  * names its store and a `consistency` block flags relationships that drift.
106
106
  */
107
107
  export declare const hooksIntelligenceUnifiedStats: MCPTool;
108
+ export declare const hooksVerifiedIngest: MCPTool;
108
109
  export declare const hooksTools: MCPTool[];
109
110
  export default hooksTools;
110
111
  //# sourceMappingURL=hooks-tools.d.ts.map
@@ -1181,6 +1181,9 @@ export const hooksRoute = {
1181
1181
  // Q-learning and keyword suggestions retain their existing routing paths.
1182
1182
  try {
1183
1183
  const sonaStart = performance.now();
1184
+ // #4581: fold in any verified outcomes the ruflo-mods learning loop
1185
+ // recorded since the last call (one stat when nothing changed).
1186
+ await ingestVerifiedQuietly();
1184
1187
  const sona = await getSONAOptimizer();
1185
1188
  const suggestion = await sona?.getRoutingSuggestion(task);
1186
1189
  const { isSupportedSonaAgent } = await import('../memory/sona-optimizer.js');
@@ -2639,9 +2642,11 @@ export const hooksSessionEnd = {
2639
2642
  }
2640
2643
  }
2641
2644
  }
2645
+ const verifiedOutcomes = await ingestVerifiedQuietly();
2642
2646
  return {
2643
2647
  sessionId,
2644
2648
  duration,
2649
+ verifiedOutcomes,
2645
2650
  statePath: saveState ? `.claude/sessions/${sessionId}.json` : undefined,
2646
2651
  daemon: { stopped: daemonStopped },
2647
2652
  sessionPersistence: sessionPersistence || { controller: 'none', persisted: false },
@@ -5450,6 +5455,38 @@ export const hooksIntelligenceUnifiedStats = {
5450
5455
  return intel.getUnifiedLearningStats();
5451
5456
  },
5452
5457
  };
5458
+ /**
5459
+ * #4581 / ADR-502: ingest the ruflo-mods learning loop's verified outcomes.
5460
+ * Never throws; null when the module or SONA is unavailable.
5461
+ */
5462
+ async function ingestVerifiedQuietly(force = false) {
5463
+ try {
5464
+ const { ingestVerifiedOutcomes } = await import('../memory/verified-outcomes.js');
5465
+ const sona = await getSONAOptimizer();
5466
+ return await ingestVerifiedOutcomes({ force, sona: sona });
5467
+ }
5468
+ catch {
5469
+ return null;
5470
+ }
5471
+ }
5472
+ export const hooksVerifiedIngest = {
5473
+ name: 'hooks_intelligence_verified-ingest',
5474
+ description: 'Ingest the ruflo-mods learning loop\'s recorded turns (.claude-flow/data/verified-trajectories.jsonl, #4581): turns a test, build or typecheck process verified train SONA; unverified turns are counted and never train. Writes .claude-flow/data/learned-lessons.json (patterns at or above the 0.6 routing gate, plus repeated verified failures as cautions). hooks_route and hooks_session-end already ingest automatically; call this to see the counts or to force a re-read. Use when native Bash hooks (via Claude Code\'s settings.json) are wrong because the outcome must reach Ruflo-side state — SONA pattern persistence and routing — and must be gated on a verifier\'s exit status rather than recorded unconditionally.',
5475
+ inputSchema: {
5476
+ type: 'object',
5477
+ properties: {
5478
+ dryRun: { type: 'boolean', description: 'Count only: train nothing and write nothing' },
5479
+ force: { type: 'boolean', description: 'Re-read the file even if it has not changed since the last ingest' },
5480
+ },
5481
+ },
5482
+ handler: async (params) => {
5483
+ const { ingestVerifiedOutcomes } = await import('../memory/verified-outcomes.js');
5484
+ const sona = await getSONAOptimizer();
5485
+ // ADR-503 §2.3: only this explicit ingest may re-verify (run project code), and only when learningLoop.reverify is on.
5486
+ const result = await ingestVerifiedOutcomes({ dryRun: params.dryRun === true, force: params.force === true || params.dryRun === true, sona: sona, allowReverify: true });
5487
+ return { success: true, ...result };
5488
+ },
5489
+ };
5453
5490
  // Export all hooks tools
5454
5491
  export const hooksTools = [
5455
5492
  hooksIntelligenceUnifiedStats,
@@ -5479,6 +5516,7 @@ export const hooksTools = [
5479
5516
  hooksTrajectoryStart,
5480
5517
  hooksTrajectoryStep,
5481
5518
  hooksTrajectoryEnd,
5519
+ hooksVerifiedIngest,
5482
5520
  hooksPatternStore,
5483
5521
  hooksPatternSearch,
5484
5522
  hooksIntelligenceStats,
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Gaming defences for the verified-outcome learning loop (ADR-503).
3
+ *
4
+ * Every rule here can only downgrade: a tainted record does not train, a
5
+ * pattern that looks gamed is held back from the prompt. Nothing here can
6
+ * turn an `unknown` into a verified outcome.
7
+ *
8
+ * - §2.1/§2.2 taints: the mod marks a `verified-success` whose turn edited a
9
+ * test file or a verifier config, or narrowed its check. Ingest never
10
+ * trains a tainted record unless `.claude-flow/config.json`
11
+ * `learningLoop.allowTaint` lists every one of its classes.
12
+ * - §2.3 re-verify settings (the runner lives in verified-reverify.ts).
13
+ * - §2.4 the anomaly gate on lessons.
14
+ */
15
+ export type Taint = 'edited-tests' | 'edited-verifier-config' | 'narrowed-verifier';
16
+ export declare const TAINTS: readonly Taint[];
17
+ /** A record's `taint` field, validated: known classes only, each once. Anything else is dropped. */
18
+ export declare function parseTaint(raw: unknown): Taint[];
19
+ export interface LoopConfig {
20
+ /** Taint classes a project lets train (default none). */
21
+ readonly allowTaint: readonly Taint[];
22
+ /** Re-run a verifier at HEAD before a success trains (default off: it runs project code). */
23
+ readonly reverify: boolean;
24
+ readonly reverifyTimeoutMs: number;
25
+ }
26
+ export declare const DEFAULT_REVERIFY_TIMEOUT_MS = 300000;
27
+ export declare const DEFAULT_LOOP_CONFIG: LoopConfig;
28
+ /**
29
+ * `learningLoop` from `.claude-flow/config.json`. A missing, unreadable or
30
+ * malformed file means the defaults: no taint allowed, re-verify off. Only
31
+ * `reverify: true` (the boolean) turns re-verification on.
32
+ */
33
+ export declare function readLoopConfig(root: string): LoopConfig;
34
+ export declare function parseLoopConfig(raw: unknown): LoopConfig;
35
+ /** The taint classes of a record that its project did not allow; non-empty means it never trains. */
36
+ export declare const blockedTaints: (taint: readonly Taint[], config: LoopConfig) => Taint[];
37
+ /**
38
+ * What ingest knows about a verified pattern, from verified records only
39
+ * (SONA's own counts include other feeders). Sessions are capped: the gate
40
+ * only asks whether there were two.
41
+ */
42
+ export interface PatternStats {
43
+ successes: number;
44
+ failures: number;
45
+ /** Successes that carried an allowed taint. */
46
+ tainted: number;
47
+ /** Successes decided by a narrowed command. */
48
+ narrowed: number;
49
+ sessions: string[];
50
+ }
51
+ export declare const MAX_PATTERN_SESSIONS = 4;
52
+ export declare const emptyStats: () => PatternStats;
53
+ export declare function parseStats(raw: unknown): PatternStats;
54
+ /** Folds one trained record into its pattern's stats. */
55
+ export declare function addOutcome(stats: PatternStats, success: boolean, taint: readonly Taint[], session: string | null): void;
56
+ export type HoldReason = 'perfect-and-tainted' | 'only-narrowed' | 'one-session';
57
+ /** §2.4 case 1: records at or above which a perfect record with half its successes tainted is held. */
58
+ export declare const PERFECT_MIN_RECORDS = 8;
59
+ /**
60
+ * Why a would-be lesson is held back from the prompt (ADR-503 §2.4), or null.
61
+ * A held pattern stays in SONA; it only never becomes a prompt lesson.
62
+ *
63
+ * 1. 100% success over 8 or more records, half or more of them tainted (allowed).
64
+ * 2. Every success came from a narrowed command.
65
+ * 3. Every success came from one session: no independent repetition. A pattern
66
+ * with no recorded sessions (a record from before ADR-503, or one without a
67
+ * session) counts as one session.
68
+ */
69
+ export declare function holdReason(stats: PatternStats, sonaSuccesses: number): HoldReason | null;
70
+ //# sourceMappingURL=verified-guards.d.ts.map
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Gaming defences for the verified-outcome learning loop (ADR-503).
3
+ *
4
+ * Every rule here can only downgrade: a tainted record does not train, a
5
+ * pattern that looks gamed is held back from the prompt. Nothing here can
6
+ * turn an `unknown` into a verified outcome.
7
+ *
8
+ * - §2.1/§2.2 taints: the mod marks a `verified-success` whose turn edited a
9
+ * test file or a verifier config, or narrowed its check. Ingest never
10
+ * trains a tainted record unless `.claude-flow/config.json`
11
+ * `learningLoop.allowTaint` lists every one of its classes.
12
+ * - §2.3 re-verify settings (the runner lives in verified-reverify.ts).
13
+ * - §2.4 the anomaly gate on lessons.
14
+ */
15
+ import { existsSync, readFileSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ export const TAINTS = ['edited-tests', 'edited-verifier-config', 'narrowed-verifier'];
18
+ /** A record's `taint` field, validated: known classes only, each once. Anything else is dropped. */
19
+ export function parseTaint(raw) {
20
+ if (!Array.isArray(raw))
21
+ return [];
22
+ const out = [];
23
+ for (const t of raw.slice(0, 8))
24
+ if (TAINTS.includes(t) && !out.includes(t))
25
+ out.push(t);
26
+ return out;
27
+ }
28
+ export const DEFAULT_REVERIFY_TIMEOUT_MS = 300_000;
29
+ export const DEFAULT_LOOP_CONFIG = { allowTaint: [], reverify: false, reverifyTimeoutMs: DEFAULT_REVERIFY_TIMEOUT_MS };
30
+ /**
31
+ * `learningLoop` from `.claude-flow/config.json`. A missing, unreadable or
32
+ * malformed file means the defaults: no taint allowed, re-verify off. Only
33
+ * `reverify: true` (the boolean) turns re-verification on.
34
+ */
35
+ export function readLoopConfig(root) {
36
+ const path = join(root, '.claude-flow', 'config.json');
37
+ try {
38
+ if (!existsSync(path))
39
+ return DEFAULT_LOOP_CONFIG;
40
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
41
+ return parseLoopConfig(raw?.learningLoop);
42
+ }
43
+ catch {
44
+ return DEFAULT_LOOP_CONFIG;
45
+ }
46
+ }
47
+ export function parseLoopConfig(raw) {
48
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
49
+ return DEFAULT_LOOP_CONFIG;
50
+ const r = raw;
51
+ const sec = typeof r.reverifyTimeoutSec === 'number' && Number.isFinite(r.reverifyTimeoutSec) ? Math.min(Math.max(Math.floor(r.reverifyTimeoutSec), 10), 3600) : DEFAULT_REVERIFY_TIMEOUT_MS / 1000;
52
+ return { allowTaint: parseTaint(r.allowTaint), reverify: r.reverify === true, reverifyTimeoutMs: sec * 1000 };
53
+ }
54
+ /** The taint classes of a record that its project did not allow; non-empty means it never trains. */
55
+ export const blockedTaints = (taint, config) => taint.filter(t => !config.allowTaint.includes(t));
56
+ export const MAX_PATTERN_SESSIONS = 4;
57
+ const SESSION = /^[A-Za-z0-9_-]{1,40}$/;
58
+ const count = (v) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? Math.min(Math.floor(v), 1_000_000) : 0);
59
+ export const emptyStats = () => ({ successes: 0, failures: 0, tainted: 0, narrowed: 0, sessions: [] });
60
+ export function parseStats(raw) {
61
+ const r = (raw && typeof raw === 'object' ? raw : {});
62
+ const sessions = Array.isArray(r.sessions) ? [...new Set(r.sessions.filter((s) => typeof s === 'string' && SESSION.test(s)))].slice(0, MAX_PATTERN_SESSIONS) : [];
63
+ return { successes: count(r.successes), failures: count(r.failures), tainted: count(r.tainted), narrowed: count(r.narrowed), sessions };
64
+ }
65
+ /** Folds one trained record into its pattern's stats. */
66
+ export function addOutcome(stats, success, taint, session) {
67
+ if (!success) {
68
+ stats.failures++;
69
+ return;
70
+ }
71
+ stats.successes++;
72
+ if (taint.length > 0)
73
+ stats.tainted++;
74
+ if (taint.includes('narrowed-verifier'))
75
+ stats.narrowed++;
76
+ if (session && SESSION.test(session) && !stats.sessions.includes(session) && stats.sessions.length < MAX_PATTERN_SESSIONS)
77
+ stats.sessions.push(session);
78
+ }
79
+ /** §2.4 case 1: records at or above which a perfect record with half its successes tainted is held. */
80
+ export const PERFECT_MIN_RECORDS = 8;
81
+ /**
82
+ * Why a would-be lesson is held back from the prompt (ADR-503 §2.4), or null.
83
+ * A held pattern stays in SONA; it only never becomes a prompt lesson.
84
+ *
85
+ * 1. 100% success over 8 or more records, half or more of them tainted (allowed).
86
+ * 2. Every success came from a narrowed command.
87
+ * 3. Every success came from one session: no independent repetition. A pattern
88
+ * with no recorded sessions (a record from before ADR-503, or one without a
89
+ * session) counts as one session.
90
+ */
91
+ export function holdReason(stats, sonaSuccesses) {
92
+ const s = stats.successes;
93
+ if (s >= PERFECT_MIN_RECORDS && stats.failures === 0 && stats.tainted * 2 >= s)
94
+ return 'perfect-and-tainted';
95
+ if (s > 0 && stats.narrowed === s)
96
+ return 'only-narrowed';
97
+ if ((s > 0 || sonaSuccesses > 0) && stats.sessions.length < 2)
98
+ return 'one-session';
99
+ return null;
100
+ }
101
+ //# sourceMappingURL=verified-guards.js.map