@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,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-505: `ruflo doctor` check for ruflo-mods compaction snapshots.
|
|
3
|
+
*
|
|
4
|
+
* The `compactSnapshot` option (default on) writes conversation text to
|
|
5
|
+
* `.claude-flow/sessions/compactions/`. `ruflo init` gitignores it through
|
|
6
|
+
* `.claude-flow/.gitignore` (`sessions/`); a project whose ignore rules lost
|
|
7
|
+
* that would commit transcripts. Warn-only: when snapshots exist and git does
|
|
8
|
+
* not ignore them. No snapshots, or not a git work tree, is a pass.
|
|
9
|
+
*/
|
|
10
|
+
export declare const COMPACTIONS_DIR = ".claude-flow/sessions/compactions";
|
|
11
|
+
export type SnapshotHealth = {
|
|
12
|
+
status: 'pass' | 'warn';
|
|
13
|
+
message: string;
|
|
14
|
+
fix?: string;
|
|
15
|
+
};
|
|
16
|
+
/** `ignored`: git ignores the path; `not-ignored`: it would be committed; `no-git`: not a work tree or git missing. */
|
|
17
|
+
export type IgnoreState = 'ignored' | 'not-ignored' | 'no-git';
|
|
18
|
+
/** `git check-ignore -q`: exit 0 ignored, 1 not ignored, anything else (128: not a repo; ENOENT: no git) no answer. */
|
|
19
|
+
export declare function gitIgnoreState(root: string, relPath: string): IgnoreState;
|
|
20
|
+
export declare function compactSnapshotHealth(root: string, ignoreState?: (root: string, relPath: string) => IgnoreState): SnapshotHealth;
|
|
21
|
+
//# sourceMappingURL=compact-snapshots.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-505: `ruflo doctor` check for ruflo-mods compaction snapshots.
|
|
3
|
+
*
|
|
4
|
+
* The `compactSnapshot` option (default on) writes conversation text to
|
|
5
|
+
* `.claude-flow/sessions/compactions/`. `ruflo init` gitignores it through
|
|
6
|
+
* `.claude-flow/.gitignore` (`sessions/`); a project whose ignore rules lost
|
|
7
|
+
* that would commit transcripts. Warn-only: when snapshots exist and git does
|
|
8
|
+
* not ignore them. No snapshots, or not a git work tree, is a pass.
|
|
9
|
+
*/
|
|
10
|
+
import { execFileSync } from 'node:child_process';
|
|
11
|
+
import { readdirSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
export const COMPACTIONS_DIR = '.claude-flow/sessions/compactions';
|
|
14
|
+
/** `git check-ignore -q`: exit 0 ignored, 1 not ignored, anything else (128: not a repo; ENOENT: no git) no answer. */
|
|
15
|
+
export function gitIgnoreState(root, relPath) {
|
|
16
|
+
try {
|
|
17
|
+
execFileSync('git', ['check-ignore', '-q', '--', relPath], { cwd: root, stdio: 'ignore', timeout: 5000 });
|
|
18
|
+
return 'ignored';
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
return error.status === 1 ? 'not-ignored' : 'no-git';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function compactSnapshotHealth(root, ignoreState = gitIgnoreState) {
|
|
25
|
+
let files;
|
|
26
|
+
try {
|
|
27
|
+
files = readdirSync(join(root, COMPACTIONS_DIR)).filter((name) => name.endsWith('.jsonl'));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
files = [];
|
|
31
|
+
}
|
|
32
|
+
if (files.length === 0)
|
|
33
|
+
return { status: 'pass', message: 'no compaction snapshots in this project' };
|
|
34
|
+
const sample = `${COMPACTIONS_DIR}/${files.sort()[0]}`;
|
|
35
|
+
const state = ignoreState(root, sample);
|
|
36
|
+
if (state === 'not-ignored') {
|
|
37
|
+
return {
|
|
38
|
+
status: 'warn',
|
|
39
|
+
message: `${files.length} compaction snapshot(s) in ${COMPACTIONS_DIR}/ hold conversation text and git does not ignore them`,
|
|
40
|
+
fix: 'add .claude-flow/ (or .claude-flow/sessions/) to .gitignore, or set the ruflo-mods compactSnapshot option to false',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return { status: 'pass', message: `${files.length} compaction snapshot(s), ${state === 'ignored' ? 'ignored by git' : 'not in a git work tree'}` };
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=compact-snapshots.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-510 §2.4 — `ruflo doctor --component headless-plugins`: do ruflo mods
|
|
3
|
+
* actually run under `claude -p` on this machine?
|
|
4
|
+
*
|
|
5
|
+
* Runs `claude -p --plugin-dir <ruflo-mods> --output-format stream-json
|
|
6
|
+
* --verbose "reply ok"` in a throwaway project that has a `.claude-flow/`
|
|
7
|
+
* directory, then reports:
|
|
8
|
+
* - whether the init event lists ruflo-mods from that folder;
|
|
9
|
+
* - whether `plugin_errors` is empty (Claude Code omits the key when none);
|
|
10
|
+
* - whether the mod's session heartbeat (`.claude-flow/mods/session.json`)
|
|
11
|
+
* was written — the proof that its function hooks ran.
|
|
12
|
+
*
|
|
13
|
+
* The runner is injectable so tests never spawn `claude` or need a key.
|
|
14
|
+
* Without any model credential the check is skipped (warn), never spawned.
|
|
15
|
+
*/
|
|
16
|
+
import { type PluginPaths } from './plugin-paths.js';
|
|
17
|
+
export declare const HEADLESS_TIMEOUT_MS = 60000;
|
|
18
|
+
export declare const HEARTBEAT: string;
|
|
19
|
+
export interface HeadlessRun {
|
|
20
|
+
exitCode: number | null;
|
|
21
|
+
stdout: string;
|
|
22
|
+
stderr: string;
|
|
23
|
+
timedOut?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export type HeadlessRunner = (argv: string[], env: NodeJS.ProcessEnv, cwd: string, timeoutMs: number) => Promise<HeadlessRun>;
|
|
26
|
+
export interface HeadlessCheck {
|
|
27
|
+
name: string;
|
|
28
|
+
status: 'pass' | 'warn' | 'fail';
|
|
29
|
+
message: string;
|
|
30
|
+
fix?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface HeadlessCheckDeps {
|
|
33
|
+
runner?: HeadlessRunner;
|
|
34
|
+
resolve?: () => PluginPaths;
|
|
35
|
+
env?: NodeJS.ProcessEnv;
|
|
36
|
+
home?: string;
|
|
37
|
+
/** Throwaway project dir factory (must be removable afterwards). */
|
|
38
|
+
makeProject?: () => string;
|
|
39
|
+
exists?: (path: string) => boolean;
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
model?: string;
|
|
42
|
+
}
|
|
43
|
+
export declare const spawnHeadless: HeadlessRunner;
|
|
44
|
+
/** A model credential Claude Code could use; without one the check would only fail on auth. */
|
|
45
|
+
export declare function hasModelCredential(env: NodeJS.ProcessEnv, configDir: string, exists?: (p: string) => boolean): boolean;
|
|
46
|
+
interface InitEvent {
|
|
47
|
+
plugins?: Array<{
|
|
48
|
+
name?: string;
|
|
49
|
+
path?: string;
|
|
50
|
+
source?: string;
|
|
51
|
+
}>;
|
|
52
|
+
plugin_errors?: unknown[];
|
|
53
|
+
}
|
|
54
|
+
/** The `system/init` event of a stream-json run, if any. */
|
|
55
|
+
export declare function findInitEvent(stdout: string): InitEvent | null;
|
|
56
|
+
export declare function checkHeadlessPlugins(deps?: HeadlessCheckDeps): Promise<HeadlessCheck>;
|
|
57
|
+
export {};
|
|
58
|
+
//# sourceMappingURL=headless-check.d.ts.map
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-510 §2.4 — `ruflo doctor --component headless-plugins`: do ruflo mods
|
|
3
|
+
* actually run under `claude -p` on this machine?
|
|
4
|
+
*
|
|
5
|
+
* Runs `claude -p --plugin-dir <ruflo-mods> --output-format stream-json
|
|
6
|
+
* --verbose "reply ok"` in a throwaway project that has a `.claude-flow/`
|
|
7
|
+
* directory, then reports:
|
|
8
|
+
* - whether the init event lists ruflo-mods from that folder;
|
|
9
|
+
* - whether `plugin_errors` is empty (Claude Code omits the key when none);
|
|
10
|
+
* - whether the mod's session heartbeat (`.claude-flow/mods/session.json`)
|
|
11
|
+
* was written — the proof that its function hooks ran.
|
|
12
|
+
*
|
|
13
|
+
* The runner is injectable so tests never spawn `claude` or need a key.
|
|
14
|
+
* Without any model credential the check is skipped (warn), never spawned.
|
|
15
|
+
*/
|
|
16
|
+
import { spawn } from 'node:child_process';
|
|
17
|
+
import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync } from 'node:fs';
|
|
18
|
+
import { homedir, tmpdir } from 'node:os';
|
|
19
|
+
import { join, resolve } from 'node:path';
|
|
20
|
+
import { ENABLE_ENV } from './install.js';
|
|
21
|
+
import { claudeConfigDir } from './plugin-resolve.js';
|
|
22
|
+
import { resolvePluginPaths } from './plugin-paths.js';
|
|
23
|
+
export const HEADLESS_TIMEOUT_MS = 60_000;
|
|
24
|
+
export const HEARTBEAT = join('.claude-flow', 'mods', 'session.json');
|
|
25
|
+
const PLUGIN = 'ruflo-mods';
|
|
26
|
+
const NAME = 'Headless plugins (ADR-510)';
|
|
27
|
+
export const spawnHeadless = (argv, env, cwd, timeoutMs) => new Promise((done) => {
|
|
28
|
+
let stdout = '';
|
|
29
|
+
let stderr = '';
|
|
30
|
+
let timedOut = false;
|
|
31
|
+
let child;
|
|
32
|
+
try {
|
|
33
|
+
child = spawn('claude', argv, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
done({ exitCode: null, stdout: '', stderr: e instanceof Error ? e.message : String(e) });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const timer = setTimeout(() => {
|
|
40
|
+
timedOut = true;
|
|
41
|
+
try {
|
|
42
|
+
child.kill('SIGTERM');
|
|
43
|
+
}
|
|
44
|
+
catch { /* already gone */ }
|
|
45
|
+
}, timeoutMs);
|
|
46
|
+
child.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
47
|
+
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
48
|
+
child.on('error', (e) => { clearTimeout(timer); done({ exitCode: null, stdout, stderr: e.message, timedOut }); });
|
|
49
|
+
child.on('close', (code) => { clearTimeout(timer); done({ exitCode: code, stdout, stderr, timedOut }); });
|
|
50
|
+
});
|
|
51
|
+
/** A model credential Claude Code could use; without one the check would only fail on auth. */
|
|
52
|
+
export function hasModelCredential(env, configDir, exists = existsSync) {
|
|
53
|
+
const keys = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX'];
|
|
54
|
+
if (keys.some((k) => !!env[k]))
|
|
55
|
+
return true;
|
|
56
|
+
// macOS keeps the login in the keychain, which we cannot see from here.
|
|
57
|
+
if (process.platform === 'darwin')
|
|
58
|
+
return true;
|
|
59
|
+
return exists(join(configDir, '.credentials.json'));
|
|
60
|
+
}
|
|
61
|
+
/** The `system/init` event of a stream-json run, if any. */
|
|
62
|
+
export function findInitEvent(stdout) {
|
|
63
|
+
for (const line of stdout.split('\n')) {
|
|
64
|
+
const t = line.trim();
|
|
65
|
+
if (!t.startsWith('{'))
|
|
66
|
+
continue;
|
|
67
|
+
try {
|
|
68
|
+
const ev = JSON.parse(t);
|
|
69
|
+
if (ev.type === 'system' && ev.subtype === 'init')
|
|
70
|
+
return ev;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// not a JSON line
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const canonical = (p) => {
|
|
79
|
+
try {
|
|
80
|
+
return realpathSync(p);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return resolve(p);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
export async function checkHeadlessPlugins(deps = {}) {
|
|
87
|
+
const env = deps.env ?? process.env;
|
|
88
|
+
const exists = deps.exists ?? existsSync;
|
|
89
|
+
const paths = (deps.resolve ?? (() => resolvePluginPaths([PLUGIN], { env, home: deps.home })))();
|
|
90
|
+
const mods = paths.resolved.find((p) => p.name === PLUGIN);
|
|
91
|
+
if (!mods) {
|
|
92
|
+
const why = paths.missing.find((m) => m.name === PLUGIN)?.reason ?? 'not found';
|
|
93
|
+
return { name: NAME, status: 'warn', message: `${PLUGIN} not found (${why}); headless workers run without it`, fix: 'ruflo mods install' };
|
|
94
|
+
}
|
|
95
|
+
const configDir = claudeConfigDir(env, deps.home ?? homedir());
|
|
96
|
+
if (!deps.runner && !hasModelCredential(env, configDir, exists)) {
|
|
97
|
+
return { name: NAME, status: 'warn', message: `skipped: no model credential for claude -p (${PLUGIN} resolves to ${mods.path})`, fix: 'set ANTHROPIC_API_KEY or run `claude login`, then re-run' };
|
|
98
|
+
}
|
|
99
|
+
const project = (deps.makeProject ?? (() => mkdtempSync(join(tmpdir(), 'ruflo-headless-'))))();
|
|
100
|
+
try {
|
|
101
|
+
mkdirSync(join(project, '.claude-flow'), { recursive: true });
|
|
102
|
+
const argv = ['-p', 'reply ok', '--plugin-dir', mods.path, '--output-format', 'stream-json', '--verbose', '--model', deps.model ?? 'haiku'];
|
|
103
|
+
const runEnv = { ...env, CLAUDE_ENTRYPOINT: 'worker' };
|
|
104
|
+
if (runEnv[ENABLE_ENV] === undefined)
|
|
105
|
+
runEnv[ENABLE_ENV] = '1';
|
|
106
|
+
delete runEnv.CLAUDE_SESSION_ID;
|
|
107
|
+
delete runEnv.CLAUDE_PARENT_SESSION_ID;
|
|
108
|
+
const run = await (deps.runner ?? spawnHeadless)(argv, runEnv, project, deps.timeoutMs ?? HEADLESS_TIMEOUT_MS);
|
|
109
|
+
if (run.timedOut) {
|
|
110
|
+
return { name: NAME, status: 'fail', message: `claude -p did not finish in ${(deps.timeoutMs ?? HEADLESS_TIMEOUT_MS) / 1000}s` };
|
|
111
|
+
}
|
|
112
|
+
const init = findInitEvent(run.stdout);
|
|
113
|
+
if (!init) {
|
|
114
|
+
const tail = (run.stderr || run.stdout).trim().split('\n').slice(-1)[0] ?? '';
|
|
115
|
+
return { name: NAME, status: 'fail', message: `no init event from claude -p (exit ${run.exitCode})${tail ? `: ${tail.slice(0, 200)}` : ''}`, fix: 'ruflo doctor -c claude' };
|
|
116
|
+
}
|
|
117
|
+
const want = canonical(mods.path);
|
|
118
|
+
const listed = (init.plugins ?? []).some((p) => p.name === PLUGIN && typeof p.path === 'string' && canonical(p.path) === want);
|
|
119
|
+
const errors = Array.isArray(init.plugin_errors) ? init.plugin_errors : [];
|
|
120
|
+
const heartbeat = exists(join(project, HEARTBEAT));
|
|
121
|
+
const facts = `init lists ${PLUGIN}: ${listed ? 'yes' : 'no'}; plugin_errors: ${errors.length === 0 ? 'none' : JSON.stringify(errors).slice(0, 200)}; heartbeat: ${heartbeat ? 'written' : 'absent'}`;
|
|
122
|
+
if (!listed || errors.length > 0) {
|
|
123
|
+
return { name: NAME, status: 'fail', message: `${facts} (${mods.path})`, fix: `claude -p --plugin-dir ${JSON.stringify(mods.path)} --output-format stream-json --verbose "reply ok"` };
|
|
124
|
+
}
|
|
125
|
+
if (!heartbeat) {
|
|
126
|
+
return { name: NAME, status: 'warn', message: `${facts}: the plugin loads but its function hooks did not run (Claude Code too old, or ${ENABLE_ENV}=0)` };
|
|
127
|
+
}
|
|
128
|
+
return { name: NAME, status: 'pass', message: `${facts} (${mods.source}: ${mods.path})` };
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
try {
|
|
132
|
+
rmSync(project, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
catch { /* best effort */ }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=headless-check.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-510 §2.2 — give a headless `claude -p` worker its ruflo plugins.
|
|
3
|
+
*
|
|
4
|
+
* Every place ruflo builds a `claude -p` command line passes its argv and env
|
|
5
|
+
* through `withHeadlessPlugins`. It appends `--plugin-dir <path>` per plugin
|
|
6
|
+
* in the worker profile (default: ruflo-mods) and sets
|
|
7
|
+
* `CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1` unless the caller set it (a caller's
|
|
8
|
+
* `0` stays `0`). A caller's own `--plugin-dir` for the same plugin is never
|
|
9
|
+
* duplicated.
|
|
10
|
+
*
|
|
11
|
+
* A plugin that cannot be resolved does not stop the worker: it is logged once
|
|
12
|
+
* per process ("plugin <name> not found; worker runs without it") and returned
|
|
13
|
+
* in `missing` for the worker's status.
|
|
14
|
+
*
|
|
15
|
+
* Profile override: `RUFLO_HEADLESS_PLUGINS` is a comma list of plugin names;
|
|
16
|
+
* `none` (or an empty value) turns plugin loading off.
|
|
17
|
+
*/
|
|
18
|
+
import { type ResolveOptions, type PluginPaths } from './plugin-paths.js';
|
|
19
|
+
export declare const DEFAULT_WORKER_PLUGINS: readonly string[];
|
|
20
|
+
export declare const PROFILE_ENV = "RUFLO_HEADLESS_PLUGINS";
|
|
21
|
+
export interface HeadlessPluginOptions extends ResolveOptions {
|
|
22
|
+
/** Plugin names to load; defaults to the env profile, else DEFAULT_WORKER_PLUGINS. */
|
|
23
|
+
profile?: readonly string[];
|
|
24
|
+
/** Where the once-per-process warning goes (default: stderr). */
|
|
25
|
+
warn?: (line: string) => void;
|
|
26
|
+
/** Injectable resolver, for tests. */
|
|
27
|
+
resolver?: (names: readonly string[], opts: ResolveOptions) => PluginPaths;
|
|
28
|
+
}
|
|
29
|
+
export interface HeadlessPluginResult<E> {
|
|
30
|
+
argv: string[];
|
|
31
|
+
env: E;
|
|
32
|
+
/** Plugins loaded via --plugin-dir by this call (not the caller's own). */
|
|
33
|
+
added: string[];
|
|
34
|
+
/** Plugins in the profile that could not be resolved. */
|
|
35
|
+
missing: string[];
|
|
36
|
+
}
|
|
37
|
+
/** For tests: forget which missing plugins were already logged. */
|
|
38
|
+
export declare function resetHeadlessPluginWarnings(): void;
|
|
39
|
+
/** The worker profile: explicit, else RUFLO_HEADLESS_PLUGINS, else ruflo-mods. */
|
|
40
|
+
export declare function workerProfile(env?: NodeJS.ProcessEnv, explicit?: readonly string[]): string[];
|
|
41
|
+
/** Every `--plugin-dir` value already in argv (both `--plugin-dir x` and `--plugin-dir=x`). */
|
|
42
|
+
export declare function existingPluginDirs(argv: readonly string[]): string[];
|
|
43
|
+
export declare function withHeadlessPlugins<E extends Record<string, string | undefined>>(argv: readonly string[], env: E, opts?: HeadlessPluginOptions): HeadlessPluginResult<E>;
|
|
44
|
+
//# sourceMappingURL=headless-plugins.d.ts.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-510 §2.2 — give a headless `claude -p` worker its ruflo plugins.
|
|
3
|
+
*
|
|
4
|
+
* Every place ruflo builds a `claude -p` command line passes its argv and env
|
|
5
|
+
* through `withHeadlessPlugins`. It appends `--plugin-dir <path>` per plugin
|
|
6
|
+
* in the worker profile (default: ruflo-mods) and sets
|
|
7
|
+
* `CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1` unless the caller set it (a caller's
|
|
8
|
+
* `0` stays `0`). A caller's own `--plugin-dir` for the same plugin is never
|
|
9
|
+
* duplicated.
|
|
10
|
+
*
|
|
11
|
+
* A plugin that cannot be resolved does not stop the worker: it is logged once
|
|
12
|
+
* per process ("plugin <name> not found; worker runs without it") and returned
|
|
13
|
+
* in `missing` for the worker's status.
|
|
14
|
+
*
|
|
15
|
+
* Profile override: `RUFLO_HEADLESS_PLUGINS` is a comma list of plugin names;
|
|
16
|
+
* `none` (or an empty value) turns plugin loading off.
|
|
17
|
+
*/
|
|
18
|
+
import { realpathSync } from 'node:fs';
|
|
19
|
+
import { basename, resolve } from 'node:path';
|
|
20
|
+
import { ENABLE_ENV } from './install.js';
|
|
21
|
+
import { resolvePluginPaths } from './plugin-paths.js';
|
|
22
|
+
export const DEFAULT_WORKER_PLUGINS = ['ruflo-mods'];
|
|
23
|
+
export const PROFILE_ENV = 'RUFLO_HEADLESS_PLUGINS';
|
|
24
|
+
const warned = new Set();
|
|
25
|
+
/** For tests: forget which missing plugins were already logged. */
|
|
26
|
+
export function resetHeadlessPluginWarnings() {
|
|
27
|
+
warned.clear();
|
|
28
|
+
}
|
|
29
|
+
/** The worker profile: explicit, else RUFLO_HEADLESS_PLUGINS, else ruflo-mods. */
|
|
30
|
+
export function workerProfile(env = process.env, explicit) {
|
|
31
|
+
if (explicit)
|
|
32
|
+
return [...explicit];
|
|
33
|
+
const raw = env[PROFILE_ENV];
|
|
34
|
+
if (raw === undefined)
|
|
35
|
+
return [...DEFAULT_WORKER_PLUGINS];
|
|
36
|
+
const names = raw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
37
|
+
return names.length === 1 && names[0].toLowerCase() === 'none' ? [] : names;
|
|
38
|
+
}
|
|
39
|
+
function canonical(p) {
|
|
40
|
+
try {
|
|
41
|
+
return realpathSync(p);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return resolve(p);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Every `--plugin-dir` value already in argv (both `--plugin-dir x` and `--plugin-dir=x`). */
|
|
48
|
+
export function existingPluginDirs(argv) {
|
|
49
|
+
const dirs = [];
|
|
50
|
+
for (let i = 0; i < argv.length; i++) {
|
|
51
|
+
const a = argv[i];
|
|
52
|
+
if (a === '--plugin-dir' && i + 1 < argv.length)
|
|
53
|
+
dirs.push(argv[++i]);
|
|
54
|
+
else if (a.startsWith('--plugin-dir='))
|
|
55
|
+
dirs.push(a.slice('--plugin-dir='.length));
|
|
56
|
+
}
|
|
57
|
+
return dirs;
|
|
58
|
+
}
|
|
59
|
+
export function withHeadlessPlugins(argv, env, opts = {}) {
|
|
60
|
+
const profile = workerProfile(opts.env ?? env, opts.profile);
|
|
61
|
+
const outArgv = [...argv];
|
|
62
|
+
const outEnv = { ...env };
|
|
63
|
+
if (profile.length === 0)
|
|
64
|
+
return { argv: outArgv, env: outEnv, added: [], missing: [] };
|
|
65
|
+
const resolver = opts.resolver ?? resolvePluginPaths;
|
|
66
|
+
const { resolved, missing } = resolver(profile, { ...opts, env: opts.env ?? env });
|
|
67
|
+
const own = existingPluginDirs(argv);
|
|
68
|
+
const ownPaths = new Set(own.map(canonical));
|
|
69
|
+
const ownNames = new Set(own.map((d) => basename(resolve(d))));
|
|
70
|
+
const added = [];
|
|
71
|
+
for (const plugin of resolved) {
|
|
72
|
+
// The caller already passes this plugin (same folder, or its own copy of it).
|
|
73
|
+
if (ownPaths.has(canonical(plugin.path)) || ownNames.has(plugin.name))
|
|
74
|
+
continue;
|
|
75
|
+
outArgv.push('--plugin-dir', plugin.path);
|
|
76
|
+
added.push(plugin.name);
|
|
77
|
+
}
|
|
78
|
+
// A caller-supplied copy of a "missing" plugin is not missing.
|
|
79
|
+
const stillMissing = missing.filter((m) => !ownNames.has(m.name)).map((m) => m.name);
|
|
80
|
+
if ((added.length > 0 || own.length > 0) && outEnv[ENABLE_ENV] === undefined) {
|
|
81
|
+
outEnv[ENABLE_ENV] = '1';
|
|
82
|
+
}
|
|
83
|
+
const warn = opts.warn ?? ((line) => process.stderr.write(`${line}\n`));
|
|
84
|
+
for (const name of stillMissing) {
|
|
85
|
+
if (warned.has(name))
|
|
86
|
+
continue;
|
|
87
|
+
warned.add(name);
|
|
88
|
+
warn(`plugin ${name} not found; worker runs without it`);
|
|
89
|
+
}
|
|
90
|
+
return { argv: outArgv, env: outEnv, added, missing: stillMissing };
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=headless-plugins.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type PluginVerification } from '../plugins/marketplace-trust/signature.js';
|
|
2
|
+
import { type ModPlugin } from './install.js';
|
|
3
|
+
import { type ReadFs } from './plugin-resolve.js';
|
|
4
|
+
/**
|
|
5
|
+
* Scan each plugin the clone carries. Returns the plugin ids whose install must
|
|
6
|
+
* not run, each with the reason; prints every finding; records --accept-risk.
|
|
7
|
+
*/
|
|
8
|
+
export declare function scanBeforeInstall(root: string, clone: string | null, plugins: readonly ModPlugin[], acceptRisk: ReadonlySet<string>, fs?: ReadFs,
|
|
9
|
+
/** Lines already printed / overrides already recorded by an earlier scan in the same run (scanned before enabling, then again after the clone refresh). */
|
|
10
|
+
seen?: Set<string>): {
|
|
11
|
+
blocked: Map<string, string>;
|
|
12
|
+
};
|
|
13
|
+
/** Verify the clone's ruflo plugins and write the status file; prints one line per non-verified plugin. */
|
|
14
|
+
export declare function verifyAndRecord(configDir: string, clone: string | null, names: readonly string[], home?: string): PluginVerification[];
|
|
15
|
+
export interface SignatureFinding {
|
|
16
|
+
name: string;
|
|
17
|
+
status: 'pass' | 'warn';
|
|
18
|
+
message: string;
|
|
19
|
+
fix?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* `ruflo mods doctor` (§2.1): one finding per ruflo mod plugin from its clone
|
|
23
|
+
* and install-cache signatures; writes the status file the mod's trust gate
|
|
24
|
+
* reads. `modified` / `bad-signature` warn: they are enforced only under
|
|
25
|
+
* modTrust: refuse-risky, since a local checkout is always modified.
|
|
26
|
+
*/
|
|
27
|
+
export declare function signatureFindings(configDir: string, clone: string | null, names: readonly string[], home?: string): SignatureFinding[];
|
|
28
|
+
//# sourceMappingURL=install-trust.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-509 on the `ruflo mods install` / `ruflo init` (mods) path: scan each
|
|
3
|
+
* plugin in the refreshed marketplace clone before `claude plugin install`
|
|
4
|
+
* (§2.2), and after the install verify the clone against its signatures and
|
|
5
|
+
* write the status file the ruflo-mods trust gate reads (§2.1): per plugin the
|
|
6
|
+
* worst of its clone and install-cache copies, and each copy by its directory.
|
|
7
|
+
*/
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { dirname } from 'node:path';
|
|
10
|
+
import { output } from '../output.js';
|
|
11
|
+
import { formatScanFinding, gateScan, scanPluginDir } from '../plugins/marketplace-trust/scan.js';
|
|
12
|
+
import { describeVerification } from '../plugins/marketplace-trust/signature.js';
|
|
13
|
+
import { verifyClone, verifyInstallCache, writeVerifyStatus } from '../plugins/marketplace-trust/verify-status.js';
|
|
14
|
+
import { recordAcceptedRisk } from './install.js';
|
|
15
|
+
import { pluginManifestIn, nodeReadFs } from './plugin-resolve.js';
|
|
16
|
+
const nameOf = (id) => id.split('@')[0];
|
|
17
|
+
/**
|
|
18
|
+
* Scan each plugin the clone carries. Returns the plugin ids whose install must
|
|
19
|
+
* not run, each with the reason; prints every finding; records --accept-risk.
|
|
20
|
+
*/
|
|
21
|
+
export function scanBeforeInstall(root, clone, plugins, acceptRisk, fs = nodeReadFs,
|
|
22
|
+
/** Lines already printed / overrides already recorded by an earlier scan in the same run (scanned before enabling, then again after the clone refresh). */
|
|
23
|
+
seen = new Set()) {
|
|
24
|
+
const blocked = new Map();
|
|
25
|
+
if (!clone)
|
|
26
|
+
return { blocked };
|
|
27
|
+
const byName = new Map();
|
|
28
|
+
const findings = plugins.flatMap((p) => {
|
|
29
|
+
const manifest = pluginManifestIn(clone, p.id, fs);
|
|
30
|
+
if (!fs.exists(manifest))
|
|
31
|
+
return [];
|
|
32
|
+
byName.set(nameOf(p.id), p.id);
|
|
33
|
+
return scanPluginDir(nameOf(p.id), dirname(dirname(manifest)));
|
|
34
|
+
});
|
|
35
|
+
// --accept-risk takes the plugin name or its id (name@ruflo).
|
|
36
|
+
const accept = new Set([...acceptRisk].map(nameOf));
|
|
37
|
+
const gate = gateScan(findings, accept);
|
|
38
|
+
for (const f of findings) {
|
|
39
|
+
const line = formatScanFinding(f);
|
|
40
|
+
if (seen.has(line))
|
|
41
|
+
continue;
|
|
42
|
+
seen.add(line);
|
|
43
|
+
if (f.severity === 'high')
|
|
44
|
+
output.writeln(`${output.error('✗')} scan ${line}`);
|
|
45
|
+
else
|
|
46
|
+
output.writeln(output.dim(` scan ${line}`));
|
|
47
|
+
}
|
|
48
|
+
for (const name of gate.blocked) {
|
|
49
|
+
const id = byName.get(name) ?? name;
|
|
50
|
+
const first = gate.findings.find((f) => f.plugin === name && f.severity === 'high');
|
|
51
|
+
blocked.set(id, `not installed: high scan finding in ${first.file} (${first.rule}); review it, then re-run with --accept-risk ${name}`);
|
|
52
|
+
}
|
|
53
|
+
const at = new Date().toISOString();
|
|
54
|
+
const accepted = gate.accepted.filter((plugin) => !seen.has(`accepted:${plugin}`));
|
|
55
|
+
for (const plugin of accepted)
|
|
56
|
+
seen.add(`accepted:${plugin}`);
|
|
57
|
+
recordAcceptedRisk(root, accepted.map((plugin) => ({
|
|
58
|
+
plugin,
|
|
59
|
+
at,
|
|
60
|
+
findings: gate.findings.filter((f) => f.plugin === plugin && f.severity === 'high').map(formatScanFinding),
|
|
61
|
+
})));
|
|
62
|
+
for (const plugin of accepted)
|
|
63
|
+
output.printWarning(`--accept-risk ${plugin}: installing past its high scan finding(s); recorded in .claude-flow/mods/install.json`);
|
|
64
|
+
return { blocked };
|
|
65
|
+
}
|
|
66
|
+
/** Verify the clone's ruflo plugins and write the status file; prints one line per non-verified plugin. */
|
|
67
|
+
export function verifyAndRecord(configDir, clone, names, home = homedir()) {
|
|
68
|
+
if (!clone)
|
|
69
|
+
return [];
|
|
70
|
+
let results;
|
|
71
|
+
let cache;
|
|
72
|
+
try {
|
|
73
|
+
results = verifyClone(clone, names);
|
|
74
|
+
// The install cache is what Claude Code loads an installed plugin from: it is recorded too (worst of both).
|
|
75
|
+
cache = verifyInstallCache(configDir, 'ruflo', names);
|
|
76
|
+
writeVerifyStatus(configDir, clone, results, home, cache);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
output.printWarning(`plugin signature check skipped: ${error.message}`);
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
for (const r of cache)
|
|
83
|
+
if (r.result === 'modified' || r.result === 'bad-signature')
|
|
84
|
+
output.writeln(`${output.error('✗')} signature (install cache) ${describeVerification(r)}`);
|
|
85
|
+
for (const r of results) {
|
|
86
|
+
if (r.result === 'verified')
|
|
87
|
+
output.writeln(`${output.success('✓')} signature ${r.plugin}: verified`);
|
|
88
|
+
else if (r.result !== 'unsigned')
|
|
89
|
+
output.writeln(`${output.error('✗')} signature ${describeVerification(r)}`);
|
|
90
|
+
}
|
|
91
|
+
const unsigned = results.filter((r) => r.result === 'unsigned').map((r) => r.plugin);
|
|
92
|
+
if (unsigned.length)
|
|
93
|
+
output.writeln(output.dim(` signature: unsigned (no .claude-plugin/signature.json): ${unsigned.join(', ')}`));
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* `ruflo mods doctor` (§2.1): one finding per ruflo mod plugin from its clone
|
|
98
|
+
* and install-cache signatures; writes the status file the mod's trust gate
|
|
99
|
+
* reads. `modified` / `bad-signature` warn: they are enforced only under
|
|
100
|
+
* modTrust: refuse-risky, since a local checkout is always modified.
|
|
101
|
+
*/
|
|
102
|
+
export function signatureFindings(configDir, clone, names, home = homedir()) {
|
|
103
|
+
if (!clone)
|
|
104
|
+
return [];
|
|
105
|
+
let results;
|
|
106
|
+
let cache;
|
|
107
|
+
try {
|
|
108
|
+
results = verifyClone(clone, names);
|
|
109
|
+
cache = verifyInstallCache(configDir, 'ruflo', names);
|
|
110
|
+
writeVerifyStatus(configDir, clone, results, home, cache);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return [{ name: 'plugin signatures', status: 'warn', message: `could not check: ${error.message}` }];
|
|
114
|
+
}
|
|
115
|
+
const rows = [...results.map((r) => ({ r, where: 'clone' })), ...cache.map((r) => ({ r, where: 'install cache' }))];
|
|
116
|
+
return rows.map(({ r, where }) => ({
|
|
117
|
+
name: `signature ${r.plugin} (${where})`,
|
|
118
|
+
// `unsigned` is reported, not warned: a clone from before the first signed release has no signature.json,
|
|
119
|
+
// and a --strict doctor gate must not fail on that alone. modified / bad-signature warn.
|
|
120
|
+
status: r.result === 'verified' || r.result === 'unsigned' ? 'pass' : 'warn',
|
|
121
|
+
message: r.result === 'unsigned' ? `${r.plugin}: unsigned (no .claude-plugin/signature.json in this release)` : describeVerification(r),
|
|
122
|
+
...(r.result === 'modified' || r.result === 'bad-signature'
|
|
123
|
+
? { fix: 'claude plugin marketplace update ruflo (a local checkout is always modified; enforced only with modTrust: refuse-risky)' }
|
|
124
|
+
: {}),
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=install-trust.js.map
|
|
@@ -57,11 +57,20 @@ export interface Added {
|
|
|
57
57
|
*/
|
|
58
58
|
claudeInstalled?: string[];
|
|
59
59
|
}
|
|
60
|
+
/** ADR-509 §2.2: an install that went ahead past a high scan finding because of --accept-risk <plugin>. */
|
|
61
|
+
export interface AcceptedRisk {
|
|
62
|
+
plugin: string;
|
|
63
|
+
at: string;
|
|
64
|
+
/** The high findings accepted, as printed (`[high] plugin: file (rule) detail`). */
|
|
65
|
+
findings: string[];
|
|
66
|
+
}
|
|
60
67
|
export interface InstallRecord {
|
|
61
68
|
version: 2;
|
|
62
69
|
installedAt: string;
|
|
63
70
|
/** What install added, per settings file it wrote. */
|
|
64
71
|
files: Record<string, Added>;
|
|
72
|
+
/** ADR-509 §2.2: every --accept-risk override, newest last. */
|
|
73
|
+
acceptedRisk?: AcceptedRisk[];
|
|
65
74
|
}
|
|
66
75
|
type Settings = Record<string, unknown> & {
|
|
67
76
|
enabledPlugins?: Record<string, unknown>;
|
|
@@ -89,8 +98,18 @@ export interface InstallResult {
|
|
|
89
98
|
export declare function installMod(projectRoot: string, scope: Scope, dryRun?: boolean, plugins?: readonly string[], marketplace?: MarketplaceEntry): InstallResult;
|
|
90
99
|
/** The install record, v1 (3.50.0: one file, ruflo-mods only) read as v2. */
|
|
91
100
|
export declare function readRecord(projectRoot: string): InstallRecord | null;
|
|
101
|
+
/** ADR-509 §2.2: record an --accept-risk override in .claude-flow/mods/install.json. */
|
|
102
|
+
export declare function recordAcceptedRisk(projectRoot: string, entries: readonly AcceptedRisk[]): void;
|
|
92
103
|
/** Note plugins ruflo installed with `claude plugin install` for one settings file. */
|
|
93
104
|
export declare function recordClaudeInstalled(projectRoot: string, settingsFile: string, ids: readonly string[]): void;
|
|
105
|
+
/**
|
|
106
|
+
* ADR-509 §2.2: a plugin whose scan blocked it must not stay enabled — Claude
|
|
107
|
+
* Code loads an enabled plugin straight from the marketplace clone, with or
|
|
108
|
+
* without an install. Removes `enabledPlugins[id]` (only when it is `true`: a
|
|
109
|
+
* `false` someone set is already off) and drops the id from what the record
|
|
110
|
+
* says ruflo added. Returns the ids it turned off.
|
|
111
|
+
*/
|
|
112
|
+
export declare function disableBlockedPlugins(projectRoot: string, settingsFile: string, ids: readonly string[]): string[];
|
|
94
113
|
/** The scope a recorded settings file was written at. */
|
|
95
114
|
export declare function scopeOfSettingsFile(file: string): Scope;
|
|
96
115
|
export declare function uninstallMod(projectRoot: string, dryRun?: boolean): {
|
package/dist/src/mods/install.js
CHANGED
|
@@ -156,7 +156,10 @@ export function readRecord(projectRoot) {
|
|
|
156
156
|
const strings = (v) => (Array.isArray(v) ? v.filter((p) => typeof p === 'string') : []);
|
|
157
157
|
files[file] = { plugins: strings(a.plugins), marketplace: a.marketplace === true, env: a.env === true, claudeInstalled: strings(a.claudeInstalled) };
|
|
158
158
|
}
|
|
159
|
-
|
|
159
|
+
const accepted = Array.isArray(parsed.acceptedRisk)
|
|
160
|
+
? parsed.acceptedRisk.filter(isRecord).map((r) => ({ plugin: String(r.plugin ?? ''), at: String(r.at ?? ''), findings: Array.isArray(r.findings) ? r.findings.map(String) : [] }))
|
|
161
|
+
: undefined;
|
|
162
|
+
return { version: 2, installedAt: String(parsed.installedAt ?? ''), files, ...(accepted?.length ? { acceptedRisk: accepted } : {}) };
|
|
160
163
|
}
|
|
161
164
|
if (parsed.version === 1 && typeof parsed.settingsFile === 'string' && isRecord(parsed.added)) {
|
|
162
165
|
const a = parsed.added;
|
|
@@ -168,6 +171,14 @@ export function readRecord(projectRoot) {
|
|
|
168
171
|
return null;
|
|
169
172
|
}
|
|
170
173
|
}
|
|
174
|
+
/** ADR-509 §2.2: record an --accept-risk override in .claude-flow/mods/install.json. */
|
|
175
|
+
export function recordAcceptedRisk(projectRoot, entries) {
|
|
176
|
+
if (entries.length === 0)
|
|
177
|
+
return;
|
|
178
|
+
const record = readRecord(projectRoot) ?? { version: 2, installedAt: new Date().toISOString(), files: {} };
|
|
179
|
+
record.acceptedRisk = [...(record.acceptedRisk ?? []), ...entries];
|
|
180
|
+
writeJson(join(resolve(projectRoot), INSTALL_RECORD), record);
|
|
181
|
+
}
|
|
171
182
|
/** Note plugins ruflo installed with `claude plugin install` for one settings file. */
|
|
172
183
|
export function recordClaudeInstalled(projectRoot, settingsFile, ids) {
|
|
173
184
|
const record = readRecord(projectRoot);
|
|
@@ -177,6 +188,32 @@ export function recordClaudeInstalled(projectRoot, settingsFile, ids) {
|
|
|
177
188
|
entry.claudeInstalled = [...new Set([...(entry.claudeInstalled ?? []), ...ids])];
|
|
178
189
|
writeJson(join(resolve(projectRoot), INSTALL_RECORD), record);
|
|
179
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* ADR-509 §2.2: a plugin whose scan blocked it must not stay enabled — Claude
|
|
193
|
+
* Code loads an enabled plugin straight from the marketplace clone, with or
|
|
194
|
+
* without an install. Removes `enabledPlugins[id]` (only when it is `true`: a
|
|
195
|
+
* `false` someone set is already off) and drops the id from what the record
|
|
196
|
+
* says ruflo added. Returns the ids it turned off.
|
|
197
|
+
*/
|
|
198
|
+
export function disableBlockedPlugins(projectRoot, settingsFile, ids) {
|
|
199
|
+
if (ids.length === 0 || !existsSync(settingsFile))
|
|
200
|
+
return [];
|
|
201
|
+
const settings = readSettingsFile(settingsFile);
|
|
202
|
+
const enabled = isRecord(settings.enabledPlugins) ? { ...settings.enabledPlugins } : {};
|
|
203
|
+
const off = ids.filter((id) => enabled[id] === true);
|
|
204
|
+
if (off.length) {
|
|
205
|
+
for (const id of off)
|
|
206
|
+
delete enabled[id];
|
|
207
|
+
writeJson(settingsFile, { ...settings, enabledPlugins: enabled });
|
|
208
|
+
}
|
|
209
|
+
const record = readRecord(projectRoot);
|
|
210
|
+
const entry = record?.files[settingsFile];
|
|
211
|
+
if (record && entry && entry.plugins.some((p) => ids.includes(p))) {
|
|
212
|
+
entry.plugins = entry.plugins.filter((p) => !ids.includes(p));
|
|
213
|
+
writeJson(join(resolve(projectRoot), INSTALL_RECORD), record);
|
|
214
|
+
}
|
|
215
|
+
return off;
|
|
216
|
+
}
|
|
180
217
|
/** The scope a recorded settings file was written at. */
|
|
181
218
|
export function scopeOfSettingsFile(file) {
|
|
182
219
|
return file.endsWith('settings.local.json') ? 'local' : 'project';
|