@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.63.9",
3
+ "version": "3.65.0",
4
4
  "files": {
5
5
  "auto-memory-hook.mjs": "631fac9d518835e2852a406888316bbbddddb49e7ed8e406d2e971f46833df9a",
6
6
  "hook-handler.cjs": "24502a3bcb78770971147940515c992e35b6736ae14ee2dc54e07d4cdef6a1d8",
@@ -9,6 +9,6 @@
9
9
  "router.cjs": "16648b3c0d73b6681ac2d8d13def6a68286c57f5a11004de5b19a0fa088c833d"
10
10
  }
11
11
  },
12
- "signature": "AuXvfIuEeQvPuxybD+KGB1BhyyvRU6a/3RmXZFXRMdcrpqxujPvNgw83w29VbI6MxMsnBczXqlniGF8iA6+MBg==",
12
+ "signature": "cwZNQ9x3YdIZ8qur+qzjhgVAubgv3FSXoWEfhkPtNc86sTPBOg6zlIb8Z2bt8ogZ8PnVmNgKas6Ft6QswIBiBg==",
13
13
  "algorithm": "ed25519"
14
14
  }
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 7,
4
- "generatedAt": "2026-10-29T14:18:53.918Z",
5
- "gitSha": "7f6fb334",
4
+ "generatedAt": "2026-10-30T02:51:18.963Z",
5
+ "gitSha": "d000e27e",
6
6
  "catalog": {
7
7
  "agents": 177,
8
- "tools": 444,
8
+ "tools": 445,
9
9
  "skills": 34
10
10
  },
11
11
  "benchmark": null
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `ruflo cost` — the budget file and gateway export (ADR-508).
3
+ *
4
+ * ruflo cost budget set --daily 10 --monthly 100 [--hard-stop] [--fallback-model haiku]
5
+ * ruflo cost budget get
6
+ * ruflo cost budget export --gateway litellm [--format json] [--model <gateway model>] (YAML unless the global --format is json)
7
+ * ruflo cost budget export --gateway litellm --apply --url https://proxy --key-env LITELLM_MASTER_KEY
8
+ * ruflo cost report [--days 30] [--json]
9
+ *
10
+ * Attribution stays in the ruflo-mods cost ledger; enforcement across
11
+ * sessions and machines belongs to the gateway. The budget file holds no
12
+ * credential, and the gateway's master key is only ever read from the
13
+ * environment variable `--key-env` names.
14
+ */
15
+ import type { Command } from '../types.js';
16
+ export declare const costCommand: Command;
17
+ export default costCommand;
18
+ //# sourceMappingURL=cost.d.ts.map
@@ -0,0 +1,195 @@
1
+ /**
2
+ * `ruflo cost` — the budget file and gateway export (ADR-508).
3
+ *
4
+ * ruflo cost budget set --daily 10 --monthly 100 [--hard-stop] [--fallback-model haiku]
5
+ * ruflo cost budget get
6
+ * ruflo cost budget export --gateway litellm [--format json] [--model <gateway model>] (YAML unless the global --format is json)
7
+ * ruflo cost budget export --gateway litellm --apply --url https://proxy --key-env LITELLM_MASTER_KEY
8
+ * ruflo cost report [--days 30] [--json]
9
+ *
10
+ * Attribution stays in the ruflo-mods cost ledger; enforcement across
11
+ * sessions and machines belongs to the gateway. The budget file holds no
12
+ * credential, and the gateway's master key is only ever read from the
13
+ * environment variable `--key-env` names.
14
+ */
15
+ import { output } from '../output.js';
16
+ import { BUDGET_PATH, budgetFromFlags, ladderWindow, readBudget, writeBudget } from '../cost/budget-file.js';
17
+ import { applyToLiteLLM, formatBody, gatewayRefusal, keyFromEnv, keyGenerateUrl, maskKey, toLiteLLMKeyBody } from '../cost/litellm.js';
18
+ import { ATTRIBUTION_PATH, readAttribution } from '../cost/attribution.js';
19
+ /** A flag by its camelCase name (the parser's) or its kebab-case spelling. */
20
+ function flag(ctx, name) {
21
+ const camel = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
22
+ return ctx.flags[camel] ?? ctx.flags[name];
23
+ }
24
+ const describe = (b) => `${b.windows.map((w) => `$${w.usd} per ${w.period}`).join(', ')}; hard stop ${b.hardStop ? 'on' : 'off'}${b.fallbackModel ? `; fallback ${b.fallbackModel}` : ''}`;
25
+ function loadOrFail(cwd) {
26
+ const read = readBudget(cwd);
27
+ if (read.kind === 'ok')
28
+ return { budget: read.budget };
29
+ const message = read.kind === 'absent' ? `no budget file at ${BUDGET_PATH}; write one with: ruflo cost budget set --daily 10 --monthly 100` : read.reason;
30
+ output.printError(message);
31
+ return { result: { success: false, message, exitCode: 1 } };
32
+ }
33
+ const setSub = {
34
+ name: 'set',
35
+ description: 'Write .claude-flow/cost/budget.json (read by the ruflo-mods cost ladder and by export)',
36
+ options: [
37
+ { name: 'daily', description: 'USD per 24h window', type: 'string' },
38
+ { name: 'weekly', description: 'USD per 7d window', type: 'string' },
39
+ { name: 'monthly', description: 'USD per 30d window', type: 'string' },
40
+ { name: 'hard-stop', description: 'At 100% of the smallest window, the mod refuses new subagents', type: 'boolean', default: false },
41
+ { name: 'fallback-model', description: 'Model a gateway falls back to when a per-model budget runs out', type: 'string' },
42
+ ],
43
+ action: async (ctx) => {
44
+ const { budget, error } = budgetFromFlags({
45
+ daily: flag(ctx, 'daily'), weekly: flag(ctx, 'weekly'), monthly: flag(ctx, 'monthly'),
46
+ hardStop: flag(ctx, 'hard-stop'), fallbackModel: flag(ctx, 'fallback-model'),
47
+ });
48
+ if (!budget) {
49
+ output.printError(error ?? 'invalid budget');
50
+ return { success: false, message: error, exitCode: 2 };
51
+ }
52
+ const file = writeBudget(ctx.cwd, budget);
53
+ output.printSuccess(`Budget written to ${file}: ${describe(budget)}`);
54
+ output.writeln(`The ruflo-mods ladder uses the $${ladderWindow(budget).usd} ${ladderWindow(budget).period} window per session when costBudgetUsd is 0.`);
55
+ output.writeln('Enforce it across sessions at a gateway: ruflo cost budget export --gateway litellm');
56
+ return { success: true, data: budget };
57
+ },
58
+ };
59
+ const getSub = {
60
+ name: 'get',
61
+ description: 'Show the budget file',
62
+ options: [{ name: 'json', description: 'Print JSON', type: 'boolean', default: false }],
63
+ action: async (ctx) => {
64
+ const { budget, result } = loadOrFail(ctx.cwd);
65
+ if (!budget)
66
+ return result;
67
+ if (flag(ctx, 'json'))
68
+ output.printJson(budget);
69
+ else
70
+ output.writeln(`${BUDGET_PATH}: ${describe(budget)}`);
71
+ return { success: true, data: budget };
72
+ },
73
+ };
74
+ const exportSub = {
75
+ name: 'export',
76
+ description: 'Print the budget as gateway key settings (LiteLLM), or --apply it to a proxy',
77
+ options: [
78
+ { name: 'gateway', description: 'Gateway: litellm (meta-llm has no documented budget API yet)', type: 'string', required: true },
79
+ { name: 'model', description: 'Gateway model name for model_max_budget / budget_fallbacks', type: 'string' },
80
+ { name: 'apply', description: 'POST the budget fields to <url>/key/generate', type: 'boolean', default: false },
81
+ { name: 'url', description: 'Proxy base URL (https, or http to a loopback host only)', type: 'string' },
82
+ { name: 'key-env', description: 'Name of the environment variable holding the proxy master key', type: 'string' },
83
+ { name: 'print-key', description: 'Print the generated virtual key in full (masked by default)', type: 'boolean', default: false },
84
+ ],
85
+ action: async (ctx) => {
86
+ const refusal = gatewayRefusal(flag(ctx, 'gateway'));
87
+ if (refusal) {
88
+ output.printError(refusal);
89
+ return { success: false, message: refusal, exitCode: 2 };
90
+ }
91
+ const { budget, result } = loadOrFail(ctx.cwd);
92
+ if (!budget)
93
+ return result;
94
+ const model = flag(ctx, 'model');
95
+ let body;
96
+ try {
97
+ body = toLiteLLMKeyBody(budget, model === undefined ? undefined : String(model));
98
+ }
99
+ catch (error) {
100
+ const message = String(error.message);
101
+ output.printError(message);
102
+ return { success: false, message, exitCode: 2 };
103
+ }
104
+ if (!flag(ctx, 'apply')) {
105
+ // The global --format (text|json|table): json prints the /key/generate body, anything else the YAML key settings.
106
+ const format = flag(ctx, 'format') === 'json' ? 'json' : 'yaml';
107
+ output.write(formatBody(body, format));
108
+ if (model === undefined && budget.fallbackModel) {
109
+ output.writeErrorln(`note: pass --model <gateway model> to add model_max_budget and budget_fallbacks (-> ${budget.fallbackModel})`);
110
+ }
111
+ return { success: true, data: body };
112
+ }
113
+ // --apply: every check before anything is sent.
114
+ const target = keyGenerateUrl(flag(ctx, 'url'));
115
+ if (!target.url) {
116
+ output.printError(target.error);
117
+ return { success: false, message: target.error, exitCode: 2 };
118
+ }
119
+ const master = keyFromEnv(flag(ctx, 'key-env'));
120
+ if (!master.key) {
121
+ output.printError(master.error);
122
+ return { success: false, message: master.error, exitCode: 2 };
123
+ }
124
+ const applied = await applyToLiteLLM(target.url, master.key, body);
125
+ if (!applied.ok) {
126
+ const message = `LiteLLM refused /key/generate (${applied.status || 'no response'}): ${applied.error ?? ''}`;
127
+ output.printError(message);
128
+ return { success: false, message, exitCode: 1 };
129
+ }
130
+ output.printSuccess(`Budget applied at ${target.url.origin}: a virtual key with ${body.budget_limits.length} budget window(s).`);
131
+ if (applied.generatedKey) {
132
+ output.writeln(`Virtual key: ${flag(ctx, 'print-key') ? applied.generatedKey : maskKey(applied.generatedKey)}`);
133
+ if (!flag(ctx, 'print-key'))
134
+ output.writeln('(masked; it is shown once by LiteLLM; use --print-key to see it, or look it up in the LiteLLM UI)');
135
+ }
136
+ return { success: true, data: { status: applied.status } };
137
+ },
138
+ };
139
+ const budgetCommand = {
140
+ name: 'budget',
141
+ description: 'The shared budget file and its export to a gateway (ADR-508)',
142
+ subcommands: [setSub, getSub, exportSub],
143
+ action: getSub.action,
144
+ };
145
+ const reportSub = {
146
+ name: 'report',
147
+ description: 'Which subagent types and tools spent the money (from the ruflo-mods cost ledger)',
148
+ options: [
149
+ { name: 'days', description: 'Only sessions that ended in the last N days (0: all)', type: 'string', default: '0' },
150
+ { name: 'json', description: 'Print JSON', type: 'boolean', default: false },
151
+ ],
152
+ action: async (ctx) => {
153
+ const days = Number(flag(ctx, 'days') ?? 0);
154
+ const since = Number.isFinite(days) && days > 0 ? Date.now() - days * 86_400_000 : 0;
155
+ const { report, error } = readAttribution(ctx.cwd, since);
156
+ if (!report) {
157
+ output.printError(error ?? 'cannot read the attribution file');
158
+ return { success: false, message: error, exitCode: 1 };
159
+ }
160
+ if (flag(ctx, 'json')) {
161
+ output.printJson(report);
162
+ return { success: true, data: report };
163
+ }
164
+ if (report.sessions === 0) {
165
+ output.writeln(`No attributed sessions in ${ATTRIBUTION_PATH}. The ruflo-mods costLedger option (default on) writes one line per session.`);
166
+ return { success: true, data: report };
167
+ }
168
+ const pct = (v) => (report.totalUsd > 0 ? `${((v / report.totalUsd) * 100).toFixed(1)}%` : '-');
169
+ output.writeln(`${report.sessions} session(s), $${report.totalUsd.toFixed(4)} attributed${report.refusals ? `, ${report.refusals} gateway budget refusal(s)` : ''}${report.skipped ? ` (${report.skipped} unreadable line(s) skipped)` : ''}`);
170
+ output.writeln('Estimate: each turn\'s cost is split evenly across the tool calls made in it (a turn without tool calls goes to main / (model)).');
171
+ for (const [title, rows] of [['By agent', report.byAgent], ['By tool', report.byTool]]) {
172
+ output.writeln('');
173
+ output.writeln(output.bold(title));
174
+ output.printTable({
175
+ columns: [{ key: 'name', header: 'Name', width: 28 }, { key: 'usd', header: 'USD', width: 12, align: 'right' }, { key: 'share', header: 'Share', width: 8, align: 'right' }],
176
+ data: rows.slice(0, 20).map(([name, usd]) => ({ name, usd: usd.toFixed(4), share: pct(usd) })),
177
+ });
178
+ }
179
+ return { success: true, data: report };
180
+ },
181
+ };
182
+ export const costCommand = {
183
+ name: 'cost',
184
+ description: 'Cost budgets, gateway budget export and per-agent/tool attribution (ADR-508)',
185
+ subcommands: [budgetCommand, reportSub],
186
+ examples: [
187
+ { command: 'ruflo cost budget set --daily 10 --monthly 100', description: 'Write the shared budget file' },
188
+ { command: 'ruflo cost budget export --gateway litellm', description: 'Print LiteLLM key budget settings (YAML)' },
189
+ { command: 'ruflo cost budget export --gateway litellm --apply --url https://proxy.example --key-env LITELLM_MASTER_KEY', description: 'Create a budgeted LiteLLM virtual key' },
190
+ { command: 'ruflo cost report', description: 'Spend by subagent type and by tool' },
191
+ ],
192
+ action: reportSub.action,
193
+ };
194
+ export default costCommand;
195
+ //# sourceMappingURL=cost.js.map
@@ -0,0 +1,9 @@
1
+ export interface AutoApproveCheck {
2
+ name: string;
3
+ status: 'pass' | 'warn' | 'fail';
4
+ message: string;
5
+ fix?: string;
6
+ }
7
+ export declare const AUTO_APPROVE_CHECK_NAME = "Project auto-approve settings (ADR-509)";
8
+ export declare function checkProjectAutoApproveSettings(cwd?: string): Promise<AutoApproveCheck>;
9
+ //# sourceMappingURL=doctor-auto-approve.d.ts.map
@@ -0,0 +1,32 @@
1
+ /**
2
+ * ADR-509 §2.3 — `ruflo doctor` reports project-level auto-approve keys
3
+ * (enableAllProjectMcpServers, enabledMcpjsonServers, bypassPermissions,
4
+ * blanket Bash allows, non-ruflo hooks) in `.claude/settings.json` and
5
+ * `.claude/settings.local.json`, and project `.claude/helpers` that are not
6
+ * ruflo's signed copies, as a warning, in any project.
7
+ */
8
+ import { join } from 'node:path';
9
+ import { findPackageHelpersDir } from '../init/helper-refresh.js';
10
+ import { checkRiskySettings, untrustedProjectHelpers } from '../init/risky-settings-gate.js';
11
+ import { formatRiskFindingsAsWarnings } from '../init/settings-risk-scanner.js';
12
+ export const AUTO_APPROVE_CHECK_NAME = 'Project auto-approve settings (ADR-509)';
13
+ export async function checkProjectAutoApproveSettings(cwd = process.cwd()) {
14
+ const hits = [];
15
+ for (const file of ['.claude/settings.json', '.claude/settings.local.json']) {
16
+ const gate = checkRiskySettings(join(cwd, file), 'keep');
17
+ if (gate)
18
+ hits.push(...gate.lines.map((l) => `${file}: ${l}`));
19
+ }
20
+ // A project-shipped helper that is not ruflo's signed copy runs under ruflo's own hook commands.
21
+ hits.push(...formatRiskFindingsAsWarnings(untrustedProjectHelpers(cwd, findPackageHelpersDir())));
22
+ if (hits.length === 0)
23
+ return { name: AUTO_APPROVE_CHECK_NAME, status: 'pass', message: 'no auto-approve keys in project settings' };
24
+ const shown = hits.slice(0, 3).join(' | ');
25
+ return {
26
+ name: AUTO_APPROVE_CHECK_NAME,
27
+ status: 'warn',
28
+ message: `${hits.length} auto-approve key(s): ${shown}${hits.length > 3 ? ` | … ${hits.length - 3} more` : ''}`,
29
+ fix: 'Review them; `ruflo init upgrade --settings --drop-risky-settings` rewrites settings.json without them',
30
+ };
31
+ }
32
+ //# sourceMappingURL=doctor-auto-approve.js.map
@@ -11,6 +11,7 @@ import { fileURLToPath, pathToFileURL } from 'url';
11
11
  import { createHash } from 'crypto';
12
12
  import { ownInstallAncestors } from '../plugins/own-install.js';
13
13
  import { checkNativeAddonHeaders } from './doctor-native-addon.js';
14
+ import { checkProjectAutoApproveSettings } from './doctor-auto-approve.js';
14
15
  import { execSync, exec, execFile } from 'child_process';
15
16
  import { promisify } from 'util';
16
17
  import { decodeKey, isEncryptionEnabled } from '../encryption/vault.js';
@@ -2322,6 +2323,22 @@ async function checkMods() {
2322
2323
  ? { name: 'ruflo mods (ADR-404)', status: 'pass', message: findings.find((f) => f.name === 'last mod start')?.message ?? 'enabled' }
2323
2324
  : { name: 'ruflo mods (ADR-404)', status: 'warn', message: warnings.map((f) => `${f.name}: ${f.message}`).join('; '), fix: 'ruflo mods doctor' };
2324
2325
  }
2326
+ // ADR-510: do ruflo mods run under `claude -p --plugin-dir` here? Spawns a
2327
+ // real (cheap) model call, so `--component headless-plugins` only.
2328
+ async function checkHeadlessPlugins() {
2329
+ const { checkHeadlessPlugins: run } = await import('../mods/headless-check.js');
2330
+ return run();
2331
+ }
2332
+ // #4581 / ADR-502: ruflo-mods learning loop — is it recording, and do its verified outcomes reach SONA? Warn-only.
2333
+ async function checkLearningLoop() {
2334
+ const { learningLoopHealth } = await import('../memory/verified-outcomes.js');
2335
+ return { name: 'Learning loop (#4581)', ...learningLoopHealth(process.cwd()) };
2336
+ }
2337
+ // ADR-505: ruflo-mods compaction snapshots hold conversation text; warn when they exist and git does not ignore them.
2338
+ async function checkCompactSnapshots() {
2339
+ const { compactSnapshotHealth } = await import('../mods/compact-snapshots.js');
2340
+ return { name: 'Compaction snapshots (ADR-505)', ...compactSnapshotHealth(process.cwd()) };
2341
+ }
2325
2342
  // Opt-in @ruvector/typesafe task router (optional peer). `--component typesafe` only.
2326
2343
  async function checkTypesafeRouter() {
2327
2344
  const name = '@ruvector/typesafe router';
@@ -2541,7 +2558,7 @@ export const doctorCommand = {
2541
2558
  {
2542
2559
  name: 'component',
2543
2560
  short: 'c',
2544
- description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, mcp-overhead, claude, browser, disk, typescript, agentic-flow, encryption, federation, funnel, proxy, auth, typesafe, mods, metaharness)',
2561
+ description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, mcp-overhead, claude, browser, disk, typescript, agentic-flow, encryption, federation, funnel, proxy, auth, typesafe, mods, headless-plugins, metaharness)',
2545
2562
  type: 'string'
2546
2563
  },
2547
2564
  {
@@ -2614,6 +2631,7 @@ export const doctorCommand = {
2614
2631
  'browser': checkAgentBrowserVersion,
2615
2632
  'config': checkConfigFile,
2616
2633
  'stale-settings': checkStaleSettingsNpx, // #2448
2634
+ 'auto-approve': () => checkProjectAutoApproveSettings(), // ADR-509
2617
2635
  'helpers': helperCheck, // #3565
2618
2636
  'daemon': checkDaemonStatus,
2619
2637
  get memory() {
@@ -2653,6 +2671,9 @@ export const doctorCommand = {
2653
2671
  'auth': checkAuth, // ADR-306
2654
2672
  'typesafe': checkTypesafeRouter, // opt-in @ruvector/typesafe task router
2655
2673
  'mods': checkMods, // ADR-404 — ruflo as a Claude Code mod
2674
+ 'learning-loop': checkLearningLoop, // #4581 — verified-outcome learning loop
2675
+ 'compact-snapshots': checkCompactSnapshots, // ADR-505 — snapshots must be gitignored
2676
+ 'headless-plugins': checkHeadlessPlugins, // ADR-510 — mods under claude -p (spawns claude; not in bare doctor)
2656
2677
  };
2657
2678
  if (component !== undefined && !Object.hasOwn(componentMap, component)) {
2658
2679
  const message = `Unknown doctor component: ${JSON.stringify(component)}. Valid components: ${Object.keys(componentMap).join(', ')}`;
@@ -2740,6 +2761,7 @@ export const doctorCommand = {
2740
2761
  checkGitRepo,
2741
2762
  checkConfigFile,
2742
2763
  checkStaleSettingsNpx, // #2448/#2677 — runaway `npx @latest` in settings
2764
+ () => checkProjectAutoApproveSettings(), // ADR-509 — project auto-approve keys
2743
2765
  helperCheck, // #3565 — installed signed helpers vs manifest
2744
2766
  checkDaemonStatus,
2745
2767
  checkMemoryDatabase,
@@ -2765,6 +2787,8 @@ export const doctorCommand = {
2765
2787
  checkProxySponsoredConsent, // ADR-313 — Meta LLM Proxy sponsored-downtime health
2766
2788
  checkAuth, // ADR-306 — Cognitum identity (warn-only; never fails bare `ruflo doctor`)
2767
2789
  checkMods, // ADR-404 — Claude Code mod path (warn-only)
2790
+ checkLearningLoop, // #4581 — verified outcomes reach SONA (warn-only)
2791
+ checkCompactSnapshots, // ADR-505 — compaction snapshots gitignored (warn-only)
2768
2792
  ];
2769
2793
  let checksToRun = allChecks;
2770
2794
  if (component !== undefined) {
@@ -2778,7 +2802,7 @@ export const doctorCommand = {
2778
2802
  [checkNpmVersion, 'npm'], [checkClaudeCode, 'claude'],
2779
2803
  [checkAgentBrowserVersion, 'browser'], [checkGit, 'git'],
2780
2804
  [checkGitRepo, 'git-repo'], [checkConfigFile, 'config'],
2781
- [checkStaleSettingsNpx, 'stale-settings'], [helperCheck, 'helpers'],
2805
+ [checkStaleSettingsNpx, 'stale-settings'], [() => checkProjectAutoApproveSettings(), 'auto-approve'], [helperCheck, 'helpers'],
2782
2806
  [checkDaemonStatus, 'daemon'], [checkMemoryDatabase, 'memory-presence'],
2783
2807
  [checkMemoryStructuralIntegrity, 'memory-structural'],
2784
2808
  [checkMemoryIntegrity, 'memory-integrity'],
@@ -2800,6 +2824,8 @@ export const doctorCommand = {
2800
2824
  [checkProxyBinary, 'proxy-binary'], [checkProxyProcess, 'proxy-process'],
2801
2825
  [checkProxyBindAddress, 'proxy-bind'], [checkAuth, 'auth'],
2802
2826
  [checkTypesafeRouter, 'typesafe'], [checkMods, 'mods'],
2827
+ [checkLearningLoop, 'learning-loop'], [checkHeadlessPlugins, 'headless-plugins'],
2828
+ [checkCompactSnapshots, 'compact-snapshots'],
2803
2829
  ]);
2804
2830
  nativeAgentDbChecks.forEach((check, index) => {
2805
2831
  // The path distinguishes simultaneous authority files; normalize separators.
@@ -51,6 +51,8 @@ export interface RunOutcome {
51
51
  sessionId?: string;
52
52
  costUsd?: number;
53
53
  durationMs: number;
54
+ /** ADR-510 — plugins in the worker profile that could not be found; the run went ahead without them. */
55
+ missingPlugins?: string[];
54
56
  }
55
57
  export type TaskRunner = (prompt: string, opts: {
56
58
  mcpConfig: string;
@@ -14,6 +14,7 @@ import { spawn as childSpawn } from 'child_process';
14
14
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
15
15
  import { join } from 'path';
16
16
  import { resolveClaudeLaunchCommand } from '../runtime/claude-command.js';
17
+ import { withHeadlessPlugins } from '../mods/headless-plugins.js';
17
18
  /** The server key the hive-mind prompt addresses (`mcp__ruflo__*`). */
18
19
  const RUFLO_SERVER = 'ruflo';
19
20
  function readServers(path, cwd) {
@@ -97,7 +98,10 @@ export const runWithClaude = (prompt, opts) => new Promise((resolve) => {
97
98
  }
98
99
  // The prompt goes on stdin, not argv: a task description has no length
99
100
  // bound and `--mcp-config` is variadic (#1780).
100
- const child = childSpawn(launch.command, [...launch.argsPrefix, '-p', '--output-format', 'json', `--mcp-config=${opts.mcpConfig}`], { cwd: opts.cwd, stdio: ['pipe', 'pipe', 'pipe'] });
101
+ // ADR-510: pass the worker profile's plugins (default ruflo-mods) as --plugin-dir.
102
+ const staged = withHeadlessPlugins([...launch.argsPrefix, '-p', '--output-format', 'json', `--mcp-config=${opts.mcpConfig}`], { ...process.env }, { projectRoot: opts.cwd });
103
+ const missing = staged.missing.length > 0 ? { missingPlugins: staged.missing } : {};
104
+ const child = childSpawn(launch.command, staged.argv, { cwd: opts.cwd, env: staged.env, stdio: ['pipe', 'pipe', 'pipe'] });
101
105
  let stdout = '';
102
106
  let stderr = '';
103
107
  child.stdout.on('data', (d) => (stdout += d));
@@ -105,7 +109,7 @@ export const runWithClaude = (prompt, opts) => new Promise((resolve) => {
105
109
  const timer = setTimeout(() => child.kill('SIGTERM'), opts.timeoutMs);
106
110
  child.on('error', (e) => {
107
111
  clearTimeout(timer);
108
- resolve({ ok: false, output: e.message, exitCode: null, durationMs: Date.now() - started });
112
+ resolve({ ok: false, output: e.message, exitCode: null, durationMs: Date.now() - started, ...missing });
109
113
  });
110
114
  child.on('close', (code, signal) => {
111
115
  clearTimeout(timer);
@@ -119,11 +123,12 @@ export const runWithClaude = (prompt, opts) => new Promise((resolve) => {
119
123
  sessionId: r.session_id,
120
124
  costUsd: r.total_cost_usd,
121
125
  durationMs,
126
+ ...missing,
122
127
  });
123
128
  }
124
129
  catch {
125
130
  const why = signal === 'SIGTERM' ? `timed out after ${opts.timeoutMs}ms` : (stderr || stdout).trim().slice(-2000);
126
- resolve({ ok: false, output: why, exitCode: code, durationMs });
131
+ resolve({ ok: false, output: why, exitCode: code, durationMs, ...missing });
127
132
  }
128
133
  });
129
134
  child.stdin.end(prompt);
@@ -173,6 +178,7 @@ export async function workOnce(opts) {
173
178
  durationMs: outcome.durationMs,
174
179
  ...(outcome.sessionId ? { sessionId: outcome.sessionId } : {}),
175
180
  ...(outcome.costUsd !== undefined ? { costUsd: outcome.costUsd } : {}),
181
+ ...(outcome.missingPlugins?.length ? { missingPlugins: outcome.missingPlugins } : {}),
176
182
  };
177
183
  if (outcome.ok) {
178
184
  await callMCPTool('task_complete', { taskId, result });
@@ -13,6 +13,7 @@ import { mkdir, writeFile } from 'fs/promises';
13
13
  import { resolveWorkerMcpConfig, currentRufloServer, workCommand } from './hive-mind-worker.js';
14
14
  import { join } from 'path';
15
15
  import { resolveClaudeLaunchCommand } from '../runtime/claude-command.js';
16
+ import { withHeadlessPlugins } from '../mods/headless-plugins.js';
16
17
  import { getHiveTokenForCli } from '../mcp-tools/hive-mind-tools.js';
17
18
  // Hive topologies
18
19
  const TOPOLOGIES = [
@@ -248,6 +249,18 @@ async function spawnClaudeCodeInstance(swarmId, swarmName, objective, workers, f
248
249
  output.printWarning('Using --dangerously-skip-permissions for seamless hive-mind execution');
249
250
  }
250
251
  }
252
+ // ADR-510 §2.2: a non-interactive (-p) run is a headless worker and loads
253
+ // no marketplace plugins on a fresh config, so stage the worker profile
254
+ // (default ruflo-mods) as --plugin-dir before the prompt. The interactive
255
+ // path keeps loading plugins the normal, trusted way.
256
+ let spawnEnv;
257
+ let missingPlugins = [];
258
+ if (isNonInteractive) {
259
+ const staged = withHeadlessPlugins(claudeArgs, { ...process.env }, { projectRoot: process.cwd() });
260
+ claudeArgs.splice(0, claudeArgs.length, ...staged.argv);
261
+ spawnEnv = staged.env;
262
+ missingPlugins = staged.missing;
263
+ }
251
264
  // Add the prompt as the LAST argument
252
265
  claudeArgs.push(hiveMindPrompt);
253
266
  output.writeln();
@@ -260,6 +273,7 @@ async function spawnClaudeCodeInstance(swarmId, swarmName, objective, workers, f
260
273
  ], {
261
274
  stdio: 'inherit',
262
275
  shell: false,
276
+ ...(spawnEnv ? { env: spawnEnv } : {}),
263
277
  });
264
278
  // Set up SIGINT handler for session management
265
279
  let isExiting = false;
@@ -312,7 +326,7 @@ async function spawnClaudeCodeInstance(swarmId, swarmName, objective, workers, f
312
326
  claudeProcess.on('exit', (c) => resolve(c ?? 0));
313
327
  claudeProcess.on('error', () => resolve(1));
314
328
  });
315
- return { success: claudeExitCode === 0, promptFile };
329
+ return { success: claudeExitCode === 0, promptFile, ...(missingPlugins.length > 0 ? { missingPlugins } : {}) };
316
330
  }
317
331
  else if (dryRun) {
318
332
  output.writeln();
@@ -4649,6 +4649,49 @@ const refreshAdvisorCommand = {
4649
4649
  }
4650
4650
  }
4651
4651
  };
4652
+ // #4581 / ADR-502: fold the ruflo-mods learning loop's verified outcomes into SONA.
4653
+ const verifiedIngestCommand = {
4654
+ name: 'verified-ingest',
4655
+ description: 'Train SONA from the learning loop\'s verified turns (tests/build exit status); unverified turns never train',
4656
+ options: [
4657
+ { name: 'dry-run', description: 'Count only: train nothing and write nothing', type: 'boolean', default: false },
4658
+ { name: 'force', description: 'Re-read the file even if unchanged since the last ingest', type: 'boolean', default: false },
4659
+ ],
4660
+ examples: [
4661
+ { command: 'ruflo hooks verified-ingest', description: 'Ingest new verified outcomes and refresh learned-lessons.json' },
4662
+ { command: 'ruflo hooks verified-ingest --dry-run', description: 'Show what would be trained' },
4663
+ ],
4664
+ action: async (ctx) => {
4665
+ try {
4666
+ const result = await callMCPTool('hooks_intelligence_verified-ingest', { dryRun: ctx.flags['dry-run'] === true || ctx.flags.dryRun === true, force: ctx.flags.force === true });
4667
+ if (ctx.flags.format === 'json') {
4668
+ output.printJson(result);
4669
+ return { success: true, data: result };
4670
+ }
4671
+ if (result.skipped === 'no-file') {
4672
+ output.printInfo('No recorded turns yet (.claude-flow/data/verified-trajectories.jsonl). Turn on the ruflo-mods learningLoop option.');
4673
+ return { success: true, data: result };
4674
+ }
4675
+ if (!result.sonaAvailable)
4676
+ output.printWarning('SONA unavailable: verified outcomes wait for the next ingest');
4677
+ const i = result.ingested;
4678
+ output.printSuccess(`Ingested ${i.verifiedSuccess} verified success, ${i.verifiedFailure} verified failure; ${i.unknown} unverified (not trained), ${i.tainted ?? 0} tainted (not trained), ${i.rejected} malformed${result.skipped === 'unchanged' ? ' (file unchanged)' : ''}`);
4679
+ output.writeln(`Lessons at or above the 0.6 gate: ${result.lessons}; cautions: ${result.cautions}; verified patterns below the gate: ${result.belowGate}`);
4680
+ // ADR-503 §2.4: patterns the anomaly gate kept out of the prompt (they stay in SONA).
4681
+ for (const h of result.held ?? [])
4682
+ output.writeln(`held: ${h.id} (${h.reason})`);
4683
+ const rv = result.reverify;
4684
+ if (rv?.enabled)
4685
+ output.writeln(`Re-verify: ${rv.ran} check(s) run, ${rv.passed} passed, ${rv.downgraded} downgraded to unverified${rv.deferred ? `, ${rv.deferred} waiting` : ''}`);
4686
+ output.writeln(output.dim(`All time: ${result.totals.verifiedSuccess} success / ${result.totals.verifiedFailure} failure trained, ${result.totals.unknown} unverified, ${result.totals.tainted ?? 0} tainted`));
4687
+ return { success: true, data: result };
4688
+ }
4689
+ catch (error) {
4690
+ output.printError(`verified-ingest failed: ${error instanceof Error ? error.message : String(error)}`);
4691
+ return { success: false, exitCode: 1 };
4692
+ }
4693
+ },
4694
+ };
4652
4695
  // Main hooks command
4653
4696
  export const hooksCommand = {
4654
4697
  name: 'hooks',
@@ -4696,6 +4739,8 @@ export const hooksCommand = {
4696
4739
  refreshFunnelCommand,
4697
4740
  // Advisor co-pilot tip background refresh — ADR-316
4698
4741
  refreshAdvisorCommand,
4742
+ // Verified-outcome learning loop — #4581
4743
+ verifiedIngestCommand,
4699
4744
  ],
4700
4745
  options: [],
4701
4746
  examples: [
@@ -92,6 +92,8 @@ const commandLoaders = {
92
92
  advisor: () => import('./advisor.js'),
93
93
  // Ruflo verbs in Claude Code's spinnerVerbs rotation (ADR-318)
94
94
  spinner: () => import('./spinner.js'),
95
+ // Budget file, gateway budget export and per-agent/tool cost attribution (ADR-508)
96
+ cost: () => import('./cost.js'),
95
97
  // ruflo as a Claude Code mod: function hooks, early access (ADR-404)
96
98
  mods: () => import('./mods.js'),
97
99
  // ADR-489: the console in a terminal of its own (read-only Overview and Cost); loads a bundle from this CLI's own install.