@claude-flow/cli 3.64.0 → 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 (89) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/catalog-manifest.json +2 -2
  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 +21 -3
  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 +8 -2
  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.js +2 -1
  38. package/dist/src/memory/verified-guards.d.ts +70 -0
  39. package/dist/src/memory/verified-guards.js +101 -0
  40. package/dist/src/memory/verified-outcomes.d.ts +68 -3
  41. package/dist/src/memory/verified-outcomes.js +88 -19
  42. package/dist/src/memory/verified-reverify.d.ts +63 -0
  43. package/dist/src/memory/verified-reverify.js +174 -0
  44. package/dist/src/missions/schemas.d.ts +4 -4
  45. package/dist/src/mods/apply.d.ts +2 -0
  46. package/dist/src/mods/apply.js +68 -8
  47. package/dist/src/mods/command-registry/schema.d.ts +36 -36
  48. package/dist/src/mods/compact-snapshots.d.ts +21 -0
  49. package/dist/src/mods/compact-snapshots.js +45 -0
  50. package/dist/src/mods/headless-check.d.ts +58 -0
  51. package/dist/src/mods/headless-check.js +137 -0
  52. package/dist/src/mods/headless-plugins.d.ts +44 -0
  53. package/dist/src/mods/headless-plugins.js +92 -0
  54. package/dist/src/mods/install-trust.d.ts +28 -0
  55. package/dist/src/mods/install-trust.js +127 -0
  56. package/dist/src/mods/install.d.ts +19 -0
  57. package/dist/src/mods/install.js +38 -1
  58. package/dist/src/mods/plugin-paths.d.ts +60 -0
  59. package/dist/src/mods/plugin-paths.js +131 -0
  60. package/dist/src/mods/plugin-repair.d.ts +8 -0
  61. package/dist/src/mods/plugin-repair.js +7 -0
  62. package/dist/src/plugins/marketplace-trust/scan.d.ts +23 -0
  63. package/dist/src/plugins/marketplace-trust/scan.js +255 -0
  64. package/dist/src/plugins/marketplace-trust/signature.d.ts +53 -0
  65. package/dist/src/plugins/marketplace-trust/signature.js +140 -0
  66. package/dist/src/plugins/marketplace-trust/verify-status.d.ts +42 -0
  67. package/dist/src/plugins/marketplace-trust/verify-status.js +110 -0
  68. package/dist/src/services/headless-worker-executor.d.ts +2 -0
  69. package/dist/src/services/headless-worker-executor.js +13 -3
  70. package/package.json +2 -1
  71. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +8 -1
  72. package/plugins/ruflo-metaharness/CHANGELOG.md +6 -0
  73. package/plugins/ruflo-metaharness/README.md +14 -1
  74. package/plugins/ruflo-metaharness/agents/metaharness-architect.md +2 -0
  75. package/plugins/ruflo-metaharness/hooks/hooks.json +16 -2
  76. package/plugins/ruflo-metaharness/scripts/_redblue.mjs +12 -2
  77. package/plugins/ruflo-metaharness/scripts/_secret-env.cjs +50 -0
  78. package/plugins/ruflo-metaharness/scripts/_secret.mjs +35 -0
  79. package/plugins/ruflo-metaharness/scripts/smoke.sh +7 -7
  80. package/plugins/ruflo-metaharness/skills/harness-bench/SKILL.md +2 -1
  81. package/plugins/ruflo-metaharness/skills/harness-drift-from-history/SKILL.md +1 -1
  82. package/plugins/ruflo-metaharness/skills/harness-evolve/SKILL.md +2 -1
  83. package/plugins/ruflo-metaharness/skills/harness-gepa/SKILL.md +1 -1
  84. package/plugins/ruflo-metaharness/skills/harness-learn/SKILL.md +2 -1
  85. package/plugins/ruflo-metaharness/skills/harness-mint/SKILL.md +2 -1
  86. package/plugins/ruflo-metaharness/skills/harness-oia-audit/SKILL.md +1 -0
  87. package/plugins/ruflo-metaharness/skills/harness-score/SKILL.md +1 -1
  88. package/plugins/ruflo-metaharness/skills/harness-security-bench/SKILL.md +2 -1
  89. package/plugins/ruflo-metaharness/skills/harness-similarity/SKILL.md +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.64.0",
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": "Qw6jX67mOGFxojusAtPx/k6mLPS5HNxycI6ME0v1PmlweDQsfT8nd3vsu8eAhR+qjm+ib++gbEN3U6CFelq4AA==",
12
+ "signature": "cwZNQ9x3YdIZ8qur+qzjhgVAubgv3FSXoWEfhkPtNc86sTPBOg6zlIb8Z2bt8ogZ8PnVmNgKas6Ft6QswIBiBg==",
13
13
  "algorithm": "ed25519"
14
14
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 7,
4
- "generatedAt": "2026-10-29T20:56:47.419Z",
5
- "gitSha": "00c0f743",
4
+ "generatedAt": "2026-10-30T02:51:18.963Z",
5
+ "gitSha": "d000e27e",
6
6
  "catalog": {
7
7
  "agents": 177,
8
8
  "tools": 445,
@@ -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,11 +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
+ }
2325
2332
  // #4581 / ADR-502: ruflo-mods learning loop — is it recording, and do its verified outcomes reach SONA? Warn-only.
2326
2333
  async function checkLearningLoop() {
2327
2334
  const { learningLoopHealth } = await import('../memory/verified-outcomes.js');
2328
2335
  return { name: 'Learning loop (#4581)', ...learningLoopHealth(process.cwd()) };
2329
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
+ }
2330
2342
  // Opt-in @ruvector/typesafe task router (optional peer). `--component typesafe` only.
2331
2343
  async function checkTypesafeRouter() {
2332
2344
  const name = '@ruvector/typesafe router';
@@ -2546,7 +2558,7 @@ export const doctorCommand = {
2546
2558
  {
2547
2559
  name: 'component',
2548
2560
  short: 'c',
2549
- 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)',
2550
2562
  type: 'string'
2551
2563
  },
2552
2564
  {
@@ -2619,6 +2631,7 @@ export const doctorCommand = {
2619
2631
  'browser': checkAgentBrowserVersion,
2620
2632
  'config': checkConfigFile,
2621
2633
  'stale-settings': checkStaleSettingsNpx, // #2448
2634
+ 'auto-approve': () => checkProjectAutoApproveSettings(), // ADR-509
2622
2635
  'helpers': helperCheck, // #3565
2623
2636
  'daemon': checkDaemonStatus,
2624
2637
  get memory() {
@@ -2659,6 +2672,8 @@ export const doctorCommand = {
2659
2672
  'typesafe': checkTypesafeRouter, // opt-in @ruvector/typesafe task router
2660
2673
  'mods': checkMods, // ADR-404 — ruflo as a Claude Code mod
2661
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)
2662
2677
  };
2663
2678
  if (component !== undefined && !Object.hasOwn(componentMap, component)) {
2664
2679
  const message = `Unknown doctor component: ${JSON.stringify(component)}. Valid components: ${Object.keys(componentMap).join(', ')}`;
@@ -2746,6 +2761,7 @@ export const doctorCommand = {
2746
2761
  checkGitRepo,
2747
2762
  checkConfigFile,
2748
2763
  checkStaleSettingsNpx, // #2448/#2677 — runaway `npx @latest` in settings
2764
+ () => checkProjectAutoApproveSettings(), // ADR-509 — project auto-approve keys
2749
2765
  helperCheck, // #3565 — installed signed helpers vs manifest
2750
2766
  checkDaemonStatus,
2751
2767
  checkMemoryDatabase,
@@ -2772,6 +2788,7 @@ export const doctorCommand = {
2772
2788
  checkAuth, // ADR-306 — Cognitum identity (warn-only; never fails bare `ruflo doctor`)
2773
2789
  checkMods, // ADR-404 — Claude Code mod path (warn-only)
2774
2790
  checkLearningLoop, // #4581 — verified outcomes reach SONA (warn-only)
2791
+ checkCompactSnapshots, // ADR-505 — compaction snapshots gitignored (warn-only)
2775
2792
  ];
2776
2793
  let checksToRun = allChecks;
2777
2794
  if (component !== undefined) {
@@ -2785,7 +2802,7 @@ export const doctorCommand = {
2785
2802
  [checkNpmVersion, 'npm'], [checkClaudeCode, 'claude'],
2786
2803
  [checkAgentBrowserVersion, 'browser'], [checkGit, 'git'],
2787
2804
  [checkGitRepo, 'git-repo'], [checkConfigFile, 'config'],
2788
- [checkStaleSettingsNpx, 'stale-settings'], [helperCheck, 'helpers'],
2805
+ [checkStaleSettingsNpx, 'stale-settings'], [() => checkProjectAutoApproveSettings(), 'auto-approve'], [helperCheck, 'helpers'],
2789
2806
  [checkDaemonStatus, 'daemon'], [checkMemoryDatabase, 'memory-presence'],
2790
2807
  [checkMemoryStructuralIntegrity, 'memory-structural'],
2791
2808
  [checkMemoryIntegrity, 'memory-integrity'],
@@ -2807,7 +2824,8 @@ export const doctorCommand = {
2807
2824
  [checkProxyBinary, 'proxy-binary'], [checkProxyProcess, 'proxy-process'],
2808
2825
  [checkProxyBindAddress, 'proxy-bind'], [checkAuth, 'auth'],
2809
2826
  [checkTypesafeRouter, 'typesafe'], [checkMods, 'mods'],
2810
- [checkLearningLoop, 'learning-loop'],
2827
+ [checkLearningLoop, 'learning-loop'], [checkHeadlessPlugins, 'headless-plugins'],
2828
+ [checkCompactSnapshots, 'compact-snapshots'],
2811
2829
  ]);
2812
2830
  nativeAgentDbChecks.forEach((check, index) => {
2813
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();
@@ -4675,9 +4675,15 @@ const verifiedIngestCommand = {
4675
4675
  if (!result.sonaAvailable)
4676
4676
  output.printWarning('SONA unavailable: verified outcomes wait for the next ingest');
4677
4677
  const i = result.ingested;
4678
- output.printSuccess(`Ingested ${i.verifiedSuccess} verified success, ${i.verifiedFailure} verified failure; ${i.unknown} unverified (not trained), ${i.rejected} malformed${result.skipped === 'unchanged' ? ' (file unchanged)' : ''}`);
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
4679
  output.writeln(`Lessons at or above the 0.6 gate: ${result.lessons}; cautions: ${result.cautions}; verified patterns below the gate: ${result.belowGate}`);
4680
- output.writeln(output.dim(`All time: ${result.totals.verifiedSuccess} success / ${result.totals.verifiedFailure} failure trained, ${result.totals.unknown} unverified`));
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`));
4681
4687
  return { success: true, data: result };
4682
4688
  }
4683
4689
  catch (error) {
@@ -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.