@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.
- package/.claude/helpers/helpers.manifest.json +2 -2
- package/catalog-manifest.json +3 -3
- package/dist/src/commands/cost.d.ts +18 -0
- package/dist/src/commands/cost.js +195 -0
- package/dist/src/commands/doctor-auto-approve.d.ts +9 -0
- package/dist/src/commands/doctor-auto-approve.js +32 -0
- package/dist/src/commands/doctor.js +28 -2
- package/dist/src/commands/hive-mind-worker.d.ts +2 -0
- package/dist/src/commands/hive-mind-worker.js +9 -3
- package/dist/src/commands/hive-mind.js +15 -1
- package/dist/src/commands/hooks.js +45 -0
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/init.js +76 -2
- package/dist/src/commands/mods.js +13 -0
- package/dist/src/commands/plugins-paths.d.ts +13 -0
- package/dist/src/commands/plugins-paths.js +55 -0
- package/dist/src/commands/plugins-verify.d.ts +9 -0
- package/dist/src/commands/plugins-verify.js +92 -0
- package/dist/src/commands/plugins.js +13 -1
- package/dist/src/cost/attribution.d.ts +35 -0
- package/dist/src/cost/attribution.js +101 -0
- package/dist/src/cost/budget-file.d.ts +57 -0
- package/dist/src/cost/budget-file.js +113 -0
- package/dist/src/cost/litellm.d.ts +69 -0
- package/dist/src/cost/litellm.js +135 -0
- package/dist/src/init/executor.d.ts +7 -2
- package/dist/src/init/executor.js +64 -10
- package/dist/src/init/helper-commands.d.ts +16 -0
- package/dist/src/init/helper-commands.js +73 -0
- package/dist/src/init/risky-settings-gate.d.ts +39 -0
- package/dist/src/init/risky-settings-gate.js +130 -0
- package/dist/src/init/settings-risk-scanner.d.ts +35 -0
- package/dist/src/init/settings-risk-scanner.js +85 -0
- package/dist/src/init/types.d.ts +12 -0
- package/dist/src/mcp-tools/capability-brain.d.ts +1 -1
- package/dist/src/mcp-tools/capability-brain.js +1 -1
- package/dist/src/mcp-tools/hooks-tools.d.ts +1 -0
- package/dist/src/mcp-tools/hooks-tools.js +38 -0
- package/dist/src/memory/verified-guards.d.ts +70 -0
- package/dist/src/memory/verified-guards.js +101 -0
- package/dist/src/memory/verified-outcomes.d.ts +228 -0
- package/dist/src/memory/verified-outcomes.js +372 -0
- package/dist/src/memory/verified-reverify.d.ts +63 -0
- package/dist/src/memory/verified-reverify.js +174 -0
- package/dist/src/missions/schemas.d.ts +36 -36
- package/dist/src/mods/apply.d.ts +2 -0
- package/dist/src/mods/apply.js +68 -8
- package/dist/src/mods/command-registry/schema.d.ts +222 -222
- package/dist/src/mods/compact-snapshots.d.ts +21 -0
- package/dist/src/mods/compact-snapshots.js +45 -0
- package/dist/src/mods/headless-check.d.ts +58 -0
- package/dist/src/mods/headless-check.js +137 -0
- package/dist/src/mods/headless-plugins.d.ts +44 -0
- package/dist/src/mods/headless-plugins.js +92 -0
- package/dist/src/mods/install-trust.d.ts +28 -0
- package/dist/src/mods/install-trust.js +127 -0
- package/dist/src/mods/install.d.ts +19 -0
- package/dist/src/mods/install.js +38 -1
- package/dist/src/mods/plugin-paths.d.ts +60 -0
- package/dist/src/mods/plugin-paths.js +131 -0
- package/dist/src/mods/plugin-repair.d.ts +8 -0
- package/dist/src/mods/plugin-repair.js +7 -0
- package/dist/src/plugins/marketplace-trust/scan.d.ts +23 -0
- package/dist/src/plugins/marketplace-trust/scan.js +255 -0
- package/dist/src/plugins/marketplace-trust/signature.d.ts +53 -0
- package/dist/src/plugins/marketplace-trust/signature.js +140 -0
- package/dist/src/plugins/marketplace-trust/verify-status.d.ts +42 -0
- package/dist/src/plugins/marketplace-trust/verify-status.js +110 -0
- package/dist/src/services/headless-worker-executor.d.ts +2 -0
- package/dist/src/services/headless-worker-executor.js +13 -3
- package/package.json +2 -1
- package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +8 -1
- package/plugins/ruflo-metaharness/CHANGELOG.md +6 -0
- package/plugins/ruflo-metaharness/README.md +14 -1
- package/plugins/ruflo-metaharness/agents/metaharness-architect.md +2 -0
- package/plugins/ruflo-metaharness/hooks/hooks.json +16 -2
- package/plugins/ruflo-metaharness/scripts/_redblue.mjs +12 -2
- package/plugins/ruflo-metaharness/scripts/_secret-env.cjs +50 -0
- package/plugins/ruflo-metaharness/scripts/_secret.mjs +35 -0
- package/plugins/ruflo-metaharness/scripts/smoke.sh +7 -7
- package/plugins/ruflo-metaharness/skills/harness-bench/SKILL.md +2 -1
- package/plugins/ruflo-metaharness/skills/harness-drift-from-history/SKILL.md +1 -1
- package/plugins/ruflo-metaharness/skills/harness-evolve/SKILL.md +2 -1
- package/plugins/ruflo-metaharness/skills/harness-gepa/SKILL.md +1 -1
- package/plugins/ruflo-metaharness/skills/harness-learn/SKILL.md +2 -1
- package/plugins/ruflo-metaharness/skills/harness-mint/SKILL.md +2 -1
- package/plugins/ruflo-metaharness/skills/harness-oia-audit/SKILL.md +1 -0
- package/plugins/ruflo-metaharness/skills/harness-score/SKILL.md +1 -1
- package/plugins/ruflo-metaharness/skills/harness-security-bench/SKILL.md +2 -1
- 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
|
package/dist/src/init/types.d.ts
CHANGED
|
@@ -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
|