claude-flow 3.49.0 → 3.51.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 (66) hide show
  1. package/.claude/helpers/hook-handler.cjs +14 -1
  2. package/.claude/proven-config.json +1 -1
  3. package/.claude/settings.json +11 -2
  4. package/.claude-plugin/marketplace.json +15 -0
  5. package/package.json +1 -1
  6. package/v3/@claude-flow/cli/README.md +0 -2
  7. package/v3/@claude-flow/cli/catalog-manifest.json +4 -4
  8. package/v3/@claude-flow/cli/dist/src/commands/catalog.d.ts +18 -0
  9. package/v3/@claude-flow/cli/dist/src/commands/catalog.js +89 -0
  10. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +24 -1
  11. package/v3/@claude-flow/cli/dist/src/commands/index.js +5 -0
  12. package/v3/@claude-flow/cli/dist/src/commands/init.js +60 -1
  13. package/v3/@claude-flow/cli/dist/src/commands/mission.d.ts +24 -0
  14. package/v3/@claude-flow/cli/dist/src/commands/mission.js +116 -0
  15. package/v3/@claude-flow/cli/dist/src/commands/mods.d.ts +13 -0
  16. package/v3/@claude-flow/cli/dist/src/commands/mods.js +170 -0
  17. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +12 -0
  18. package/v3/@claude-flow/cli/dist/src/mcp-client.js +3 -0
  19. package/v3/@claude-flow/cli/dist/src/mcp-tools/capability-brain.js +1 -1
  20. package/v3/@claude-flow/cli/dist/src/mcp-tools/index.d.ts +1 -0
  21. package/v3/@claude-flow/cli/dist/src/mcp-tools/index.js +2 -0
  22. package/v3/@claude-flow/cli/dist/src/mcp-tools/mission-tools.d.ts +22 -0
  23. package/v3/@claude-flow/cli/dist/src/mcp-tools/mission-tools.js +75 -0
  24. package/v3/@claude-flow/cli/dist/src/missions/fold.d.ts +19 -0
  25. package/v3/@claude-flow/cli/dist/src/missions/fold.js +238 -0
  26. package/v3/@claude-flow/cli/dist/src/missions/index.d.ts +10 -0
  27. package/v3/@claude-flow/cli/dist/src/missions/index.js +10 -0
  28. package/v3/@claude-flow/cli/dist/src/missions/observation.d.ts +70 -0
  29. package/v3/@claude-flow/cli/dist/src/missions/observation.js +77 -0
  30. package/v3/@claude-flow/cli/dist/src/missions/schemas.d.ts +735 -0
  31. package/v3/@claude-flow/cli/dist/src/missions/schemas.js +150 -0
  32. package/v3/@claude-flow/cli/dist/src/missions/service.d.ts +88 -0
  33. package/v3/@claude-flow/cli/dist/src/missions/service.js +278 -0
  34. package/v3/@claude-flow/cli/dist/src/missions/store.d.ts +68 -0
  35. package/v3/@claude-flow/cli/dist/src/missions/store.js +209 -0
  36. package/v3/@claude-flow/cli/dist/src/missions/transitions.d.ts +81 -0
  37. package/v3/@claude-flow/cli/dist/src/missions/transitions.js +178 -0
  38. package/v3/@claude-flow/cli/dist/src/mods/apply.d.ts +50 -0
  39. package/v3/@claude-flow/cli/dist/src/mods/apply.js +186 -0
  40. package/v3/@claude-flow/cli/dist/src/mods/claude-installs.d.ts +29 -0
  41. package/v3/@claude-flow/cli/dist/src/mods/claude-installs.js +83 -0
  42. package/v3/@claude-flow/cli/dist/src/mods/command-registry/catalog.d.ts +45 -0
  43. package/v3/@claude-flow/cli/dist/src/mods/command-registry/catalog.js +234 -0
  44. package/v3/@claude-flow/cli/dist/src/mods/command-registry/engine-report.d.ts +66 -0
  45. package/v3/@claude-flow/cli/dist/src/mods/command-registry/engine-report.js +119 -0
  46. package/v3/@claude-flow/cli/dist/src/mods/command-registry/generate.d.ts +51 -0
  47. package/v3/@claude-flow/cli/dist/src/mods/command-registry/generate.js +166 -0
  48. package/v3/@claude-flow/cli/dist/src/mods/command-registry/index.d.ts +9 -0
  49. package/v3/@claude-flow/cli/dist/src/mods/command-registry/index.js +9 -0
  50. package/v3/@claude-flow/cli/dist/src/mods/command-registry/inventory.d.ts +63 -0
  51. package/v3/@claude-flow/cli/dist/src/mods/command-registry/inventory.js +230 -0
  52. package/v3/@claude-flow/cli/dist/src/mods/command-registry/schema.d.ts +836 -0
  53. package/v3/@claude-flow/cli/dist/src/mods/command-registry/schema.js +137 -0
  54. package/v3/@claude-flow/cli/dist/src/mods/install.d.ts +102 -0
  55. package/v3/@claude-flow/cli/dist/src/mods/install.js +185 -0
  56. package/v3/@claude-flow/cli/dist/src/mods/plugin-repair.d.ts +81 -0
  57. package/v3/@claude-flow/cli/dist/src/mods/plugin-repair.js +120 -0
  58. package/v3/@claude-flow/cli/dist/src/mods/plugin-resolve.d.ts +63 -0
  59. package/v3/@claude-flow/cli/dist/src/mods/plugin-resolve.js +182 -0
  60. package/v3/@claude-flow/cli/dist/src/mods/policy-projection.d.ts +39 -0
  61. package/v3/@claude-flow/cli/dist/src/mods/policy-projection.js +65 -0
  62. package/v3/@claude-flow/cli/dist/src/mods/probe.d.ts +33 -0
  63. package/v3/@claude-flow/cli/dist/src/mods/probe.js +143 -0
  64. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +5 -0
  65. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +20 -1
  66. package/v3/@claude-flow/cli/package.json +1 -1
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Whether Claude Code can actually load the ruflo mod plugins (ADR-404).
3
+ *
4
+ * `enabledPlugins["ruflo-mods@ruflo"] = true` in settings is only a request.
5
+ * Observed live on Claude Code 2.1.287: an interactive, trusted session clones
6
+ * the `ruflo` marketplace a project declares and loads an enabled plugin
7
+ * straight from that clone (no install record needed); a headless `-p` run
8
+ * clones nothing. A clone from before ruflo-mods shipped has no
9
+ * `plugins/ruflo-mods`, and Claude Code skips the enabled plugin without a
10
+ * word: `/ruflo-mods` is an unknown command, and the clone is not refreshed
11
+ * on start. So the clone decides; an install record (`installed_plugins.json`,
12
+ * pointing into `plugins/cache/`) is a cached copy that still loads when the
13
+ * clone goes stale.
14
+ *
15
+ * Read-only. The config directory is `CLAUDE_CONFIG_DIR` when set, else
16
+ * `~/.claude`, as Claude Code itself resolves it.
17
+ */
18
+ import { type MarketplaceEntry, type ModPlugin, type Scope } from './install.js';
19
+ import type { Finding } from './probe.js';
20
+ /** The two filesystem reads detection needs; injectable for tests. */
21
+ export interface ReadFs {
22
+ exists(path: string): boolean;
23
+ readText(path: string): string | undefined;
24
+ }
25
+ export declare const nodeReadFs: ReadFs;
26
+ export declare function claudeConfigDir(env: NodeJS.ProcessEnv, home: string): string;
27
+ export interface MarketplaceState {
28
+ /** Listed in known_marketplaces.json (so `marketplace update ruflo` works). */
29
+ known: boolean;
30
+ /** The local clone (or, for a directory source, the directory), when it exists on disk. */
31
+ location: string | null;
32
+ /** The source Claude Code knows the marketplace by, when known. */
33
+ source?: MarketplaceEntry['source'];
34
+ }
35
+ /** "github ruvnet/ruflo" or "directory /path" (a working tree: no clone, nothing to go stale). */
36
+ export declare function describeSource(source: unknown): string;
37
+ export declare function sameSource(a: unknown, b: unknown): boolean;
38
+ export declare function marketplaceState(configDir: string, fs?: ReadFs): MarketplaceState;
39
+ /** The plugin's manifest inside a clone: its marketplace.json `source`, else plugins/<name>. */
40
+ export declare function pluginManifestIn(clone: string, id: string, fs?: ReadFs): string;
41
+ /** The clone carries this plugin. */
42
+ export declare function cloneHas(market: MarketplaceState, id: string, fs?: ReadFs): boolean;
43
+ export interface InstalledState {
44
+ installed: boolean;
45
+ scope?: string;
46
+ installPath?: string;
47
+ }
48
+ /** Installed for this project: user scope anywhere, local/project scope for this root; its cache dir present. */
49
+ export declare function installedState(projectRoot: string, configDir: string, id: string, fs?: ReadFs): InstalledState;
50
+ /**
51
+ * The exact commands that refresh the marketplace and install the plugins,
52
+ * for a person to run. `known` is true only when Claude Code already knows
53
+ * the marketplace by the wanted source (then an update suffices). A
54
+ * directory source loads live from the working tree: nothing to install.
55
+ */
56
+ export declare function repairCommands(projectRoot: string, scope: Scope | 'user', known: boolean, plugins?: readonly ModPlugin[], wanted?: MarketplaceEntry): string[];
57
+ /**
58
+ * The marketplace finding plus one finding per plugin enabled in settings.
59
+ * A required plugin missing from the clone with no cached install is the
60
+ * failure Claude Code hides; a plugin not yet released is "pending".
61
+ */
62
+ export declare function resolveFindings(projectRoot: string, scope: Scope | 'user', configDir: string, fs?: ReadFs, enabled?: readonly string[], declared?: unknown): Finding[];
63
+ //# sourceMappingURL=plugin-resolve.d.ts.map
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Whether Claude Code can actually load the ruflo mod plugins (ADR-404).
3
+ *
4
+ * `enabledPlugins["ruflo-mods@ruflo"] = true` in settings is only a request.
5
+ * Observed live on Claude Code 2.1.287: an interactive, trusted session clones
6
+ * the `ruflo` marketplace a project declares and loads an enabled plugin
7
+ * straight from that clone (no install record needed); a headless `-p` run
8
+ * clones nothing. A clone from before ruflo-mods shipped has no
9
+ * `plugins/ruflo-mods`, and Claude Code skips the enabled plugin without a
10
+ * word: `/ruflo-mods` is an unknown command, and the clone is not refreshed
11
+ * on start. So the clone decides; an install record (`installed_plugins.json`,
12
+ * pointing into `plugins/cache/`) is a cached copy that still loads when the
13
+ * clone goes stale.
14
+ *
15
+ * Read-only. The config directory is `CLAUDE_CONFIG_DIR` when set, else
16
+ * `~/.claude`, as Claude Code itself resolves it.
17
+ */
18
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
19
+ import { join, resolve } from 'node:path';
20
+ import { MARKETPLACE_NAME, MARKETPLACE_SOURCE, MOD_PLUGINS } from './install.js';
21
+ export const nodeReadFs = {
22
+ exists: (p) => existsSync(p),
23
+ readText: (p) => {
24
+ try {
25
+ return readFileSync(p, 'utf8');
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ },
31
+ };
32
+ export function claudeConfigDir(env, home) {
33
+ return env.CLAUDE_CONFIG_DIR || join(home, '.claude');
34
+ }
35
+ function readJson(fs, path) {
36
+ const text = fs.readText(path);
37
+ if (text === undefined)
38
+ return undefined;
39
+ try {
40
+ return JSON.parse(text);
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ }
46
+ function isRecord(v) {
47
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
48
+ }
49
+ const nameOf = (id) => id.split('@')[0];
50
+ /** "github ruvnet/ruflo" or "directory /path" (a working tree: no clone, nothing to go stale). */
51
+ export function describeSource(source) {
52
+ if (!isRecord(source))
53
+ return 'unknown source';
54
+ if (source.source === 'directory')
55
+ return `directory ${String(source.path)}`;
56
+ if (source.source === 'github')
57
+ return `github ${String(source.repo)}`;
58
+ return String(source.source ?? 'unknown source');
59
+ }
60
+ export function sameSource(a, b) {
61
+ if (!isRecord(a) || !isRecord(b) || a.source !== b.source)
62
+ return false;
63
+ return a.source === 'directory' ? resolve(String(a.path)) === resolve(String(b.path)) : a.repo === b.repo;
64
+ }
65
+ export function marketplaceState(configDir, fs = nodeReadFs) {
66
+ const known = readJson(fs, join(configDir, 'plugins', 'known_marketplaces.json'));
67
+ const entry = isRecord(known) ? known[MARKETPLACE_NAME] : undefined;
68
+ const declared = isRecord(entry) && typeof entry.installLocation === 'string' ? entry.installLocation : undefined;
69
+ const candidates = [declared, join(configDir, 'plugins', 'marketplaces', MARKETPLACE_NAME)].filter((p) => !!p);
70
+ const source = isRecord(entry) && isRecord(entry.source) ? entry.source : undefined;
71
+ return { known: isRecord(entry), location: candidates.find((p) => fs.exists(p)) ?? null, source };
72
+ }
73
+ /** The plugin's manifest inside a clone: its marketplace.json `source`, else plugins/<name>. */
74
+ export function pluginManifestIn(clone, id, fs = nodeReadFs) {
75
+ const catalog = readJson(fs, join(clone, '.claude-plugin', 'marketplace.json'));
76
+ const listed = isRecord(catalog) && Array.isArray(catalog.plugins)
77
+ ? catalog.plugins.find((p) => isRecord(p) && p.name === nameOf(id))
78
+ : undefined;
79
+ const source = isRecord(listed) && typeof listed.source === 'string' ? listed.source : `./plugins/${nameOf(id)}`;
80
+ return join(clone, source, '.claude-plugin', 'plugin.json');
81
+ }
82
+ /** The clone carries this plugin. */
83
+ export function cloneHas(market, id, fs = nodeReadFs) {
84
+ return market.location !== null && fs.exists(pluginManifestIn(market.location, id, fs));
85
+ }
86
+ function sameDir(a, b) {
87
+ if (resolve(a) === resolve(b))
88
+ return true;
89
+ try {
90
+ return realpathSync(a) === realpathSync(b);
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ }
96
+ /** Installed for this project: user scope anywhere, local/project scope for this root; its cache dir present. */
97
+ export function installedState(projectRoot, configDir, id, fs = nodeReadFs) {
98
+ const record = readJson(fs, join(configDir, 'plugins', 'installed_plugins.json'));
99
+ const plugins = isRecord(record) && isRecord(record.plugins) ? record.plugins : undefined;
100
+ const entries = plugins && Array.isArray(plugins[id]) ? plugins[id] : [];
101
+ for (const e of entries) {
102
+ if (!isRecord(e) || typeof e.installPath !== 'string' || !fs.exists(e.installPath))
103
+ continue;
104
+ const forHere = e.scope === 'user' || (typeof e.projectPath === 'string' && sameDir(e.projectPath, projectRoot));
105
+ if (forHere)
106
+ return { installed: true, scope: String(e.scope), installPath: e.installPath };
107
+ }
108
+ return { installed: false };
109
+ }
110
+ /**
111
+ * The exact commands that refresh the marketplace and install the plugins,
112
+ * for a person to run. `known` is true only when Claude Code already knows
113
+ * the marketplace by the wanted source (then an update suffices). A
114
+ * directory source loads live from the working tree: nothing to install.
115
+ */
116
+ export function repairCommands(projectRoot, scope, known, plugins = MOD_PLUGINS, wanted = MARKETPLACE_SOURCE) {
117
+ const dir = wanted.source.source === 'directory' ? wanted.source.path : null;
118
+ return [
119
+ `cd ${JSON.stringify(resolve(projectRoot))}`,
120
+ known
121
+ ? `claude plugin marketplace update ${MARKETPLACE_NAME}`
122
+ : `claude plugin marketplace add ${dir ? JSON.stringify(dir) : 'ruvnet/ruflo'} --scope ${scope}`,
123
+ ...(dir ? [] : plugins.filter((p) => p.required).map((p) => `claude plugin install ${p.id} --scope ${scope}`)),
124
+ ];
125
+ }
126
+ /**
127
+ * The marketplace finding plus one finding per plugin enabled in settings.
128
+ * A required plugin missing from the clone with no cached install is the
129
+ * failure Claude Code hides; a plugin not yet released is "pending".
130
+ */
131
+ export function resolveFindings(projectRoot, scope, configDir, fs = nodeReadFs, enabled = MOD_PLUGINS.map((p) => p.id), declared) {
132
+ const market = marketplaceState(configDir, fs);
133
+ const wanted = (isRecord(declared) && isRecord(declared.source) ? declared : MARKETPLACE_SOURCE);
134
+ const matches = market.known && sameSource(market.source, wanted.source);
135
+ const fix = repairCommands(projectRoot, scope, matches, MOD_PLUGINS.filter((p) => enabled.includes(p.id)), wanted).join(' && ');
136
+ const findings = [];
137
+ const kind = market.source?.source === 'directory' ? 'directory (loads live from the working tree)' : `cloned at ${market.location}`;
138
+ findings.push(market.location && market.known && !sameSource(market.source, wanted.source)
139
+ ? {
140
+ name: 'ruflo marketplace',
141
+ status: 'warn',
142
+ message: `this project declares ${describeSource(wanted.source)}, but Claude Code knows ruflo as ${describeSource(market.source)} (one ruflo marketplace per config dir)`,
143
+ fix,
144
+ }
145
+ : market.location
146
+ ? { name: 'ruflo marketplace', status: 'pass', message: `${describeSource(market.source ?? wanted.source)}: ${kind}` }
147
+ : {
148
+ name: 'ruflo marketplace',
149
+ status: 'warn',
150
+ message: `not cloned under ${join(configDir, 'plugins')}: Claude Code clones it at the next interactive start of a trusted session (headless -p runs do not)`,
151
+ fix,
152
+ });
153
+ for (const plugin of MOD_PLUGINS.filter((p) => enabled.includes(p.id))) {
154
+ const name = `plugin ${plugin.id}`;
155
+ const installed = installedState(projectRoot, configDir, plugin.id, fs);
156
+ const also = installed.installed ? `; also installed (${installed.scope} scope)` : '';
157
+ if (cloneHas(market, plugin.id, fs)) {
158
+ findings.push({ name, status: 'pass', message: `loads from the marketplace clone${also}` });
159
+ }
160
+ else if (!market.location) {
161
+ findings.push(installed.installed
162
+ ? { name, status: 'pass', message: `installed (${installed.scope} scope, ${installed.installPath})` }
163
+ : { name, status: 'warn', message: plugin.required ? 'loads once Claude Code has cloned the marketplace' : 'pending: not released in the ruflo marketplace yet' });
164
+ }
165
+ else if (!plugin.required) {
166
+ findings.push({ name, status: 'warn', message: `pending: not in the ruflo marketplace yet (${market.location})${also}` });
167
+ }
168
+ else if (installed.installed) {
169
+ findings.push({ name, status: 'warn', message: `the clone at ${market.location} is stale (no ${nameOf(plugin.id)}); the cached install still loads`, fix });
170
+ }
171
+ else {
172
+ findings.push({
173
+ name,
174
+ status: 'fail',
175
+ message: `the clone at ${market.location} is stale: it predates ${nameOf(plugin.id)}, so Claude Code skips the enabled plugin without a word${plugin.id.startsWith('ruflo-mods@') ? ' (/ruflo-mods is an unknown command)' : ''}`,
176
+ fix,
177
+ });
178
+ }
179
+ }
180
+ return findings;
181
+ }
182
+ //# sourceMappingURL=plugin-resolve.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The policy projection the ruflo mod reads (ADR-404).
3
+ *
4
+ * A hooks module may read at most 4 MiB and cannot import @claude-flow/security,
5
+ * while `.claude-flow/policy/state.json` carries the whole receipt ledger
6
+ * (tens of MB on a busy project). So every policy state write also writes a
7
+ * small file holding only what the mod's `tool.check` needs: the mode and the
8
+ * rules that name a `claude-code.` action explicitly. Legacy mode, or no such
9
+ * rule, removes the file, and the mod then adds no policy of its own.
10
+ *
11
+ * The projection never authorizes anything: the mod only tightens Claude
12
+ * Code's own verdict with it, so a deleted or stale projection returns to the
13
+ * no-mod baseline and can never loosen a call. It is not a ledger and holds
14
+ * no receipts; issue #3602 (the ledger anchor) is untouched by it.
15
+ */
16
+ import type { PolicyRule, PolicyState } from '@claude-flow/security';
17
+ export declare const PROJECTION_RELATIVE: string;
18
+ export declare const CLAUDE_CODE_ACTION_PREFIX = "claude-code.";
19
+ export interface PolicyProjection {
20
+ version: 1;
21
+ mode: PolicyState['mode'];
22
+ generatedAt: number;
23
+ rules: PolicyRule[];
24
+ }
25
+ /** Whether a rule names Claude Code tool calls explicitly (never via `*` or no actions). */
26
+ export declare function targetsClaudeCode(rule: Pick<PolicyRule, 'actions'>): boolean;
27
+ /** The projection of a state, or null when there is nothing to project. */
28
+ export declare function projectionOf(state: Pick<PolicyState, 'mode' | 'rules'>, now?: number): PolicyProjection | null;
29
+ export type ProjectionSync = {
30
+ action: 'written' | 'removed' | 'unchanged';
31
+ path: string;
32
+ };
33
+ /**
34
+ * Writes (atomically, owner-only) or removes the projection for a state.
35
+ * Throws on I/O failure; policy-runtime calls it after the state is safely
36
+ * written and keeps the state write's own result whatever happens here.
37
+ */
38
+ export declare function syncPolicyProjection(projectRoot: string, state: Pick<PolicyState, 'mode' | 'rules'>): ProjectionSync;
39
+ //# sourceMappingURL=policy-projection.d.ts.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The policy projection the ruflo mod reads (ADR-404).
3
+ *
4
+ * A hooks module may read at most 4 MiB and cannot import @claude-flow/security,
5
+ * while `.claude-flow/policy/state.json` carries the whole receipt ledger
6
+ * (tens of MB on a busy project). So every policy state write also writes a
7
+ * small file holding only what the mod's `tool.check` needs: the mode and the
8
+ * rules that name a `claude-code.` action explicitly. Legacy mode, or no such
9
+ * rule, removes the file, and the mod then adds no policy of its own.
10
+ *
11
+ * The projection never authorizes anything: the mod only tightens Claude
12
+ * Code's own verdict with it, so a deleted or stale projection returns to the
13
+ * no-mod baseline and can never loosen a call. It is not a ledger and holds
14
+ * no receipts; issue #3602 (the ledger anchor) is untouched by it.
15
+ */
16
+ import { existsSync, mkdirSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
17
+ import { dirname, join, resolve } from 'node:path';
18
+ export const PROJECTION_RELATIVE = join('.claude-flow', 'policy', 'claude-code.json');
19
+ export const CLAUDE_CODE_ACTION_PREFIX = 'claude-code.';
20
+ /** Whether a rule names Claude Code tool calls explicitly (never via `*` or no actions). */
21
+ export function targetsClaudeCode(rule) {
22
+ return Array.isArray(rule.actions) && rule.actions.some((a) => typeof a === 'string' && a.startsWith(CLAUDE_CODE_ACTION_PREFIX));
23
+ }
24
+ /** The projection of a state, or null when there is nothing to project. */
25
+ export function projectionOf(state, now = Date.now()) {
26
+ if (state.mode === 'legacy')
27
+ return null;
28
+ const rules = (Array.isArray(state.rules) ? state.rules : []).filter(targetsClaudeCode).map((rule) => ({
29
+ id: rule.id,
30
+ effect: rule.effect,
31
+ actions: [...rule.actions],
32
+ ...(rule.enabled !== undefined ? { enabled: rule.enabled } : {}),
33
+ ...(rule.priority !== undefined ? { priority: rule.priority } : {}),
34
+ ...(rule.resources ? { resources: [...rule.resources] } : {}),
35
+ ...(rule.principals ? { principals: [...rule.principals] } : {}),
36
+ ...(rule.identityTypes ? { identityTypes: [...rule.identityTypes] } : {}),
37
+ ...(rule.roles ? { roles: [...rule.roles] } : {}),
38
+ ...(rule.environments ? { environments: [...rule.environments] } : {}),
39
+ ...(rule.constraints ? { constraints: { ...rule.constraints } } : {}),
40
+ }));
41
+ if (rules.length === 0)
42
+ return null;
43
+ return { version: 1, mode: state.mode, generatedAt: now, rules };
44
+ }
45
+ /**
46
+ * Writes (atomically, owner-only) or removes the projection for a state.
47
+ * Throws on I/O failure; policy-runtime calls it after the state is safely
48
+ * written and keeps the state write's own result whatever happens here.
49
+ */
50
+ export function syncPolicyProjection(projectRoot, state) {
51
+ const path = join(resolve(projectRoot), PROJECTION_RELATIVE);
52
+ const projection = projectionOf(state);
53
+ if (!projection) {
54
+ if (!existsSync(path))
55
+ return { action: 'unchanged', path };
56
+ unlinkSync(path);
57
+ return { action: 'removed', path };
58
+ }
59
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
60
+ const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
61
+ writeFileSync(tmp, `${JSON.stringify(projection, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
62
+ renameSync(tmp, path);
63
+ return { action: 'written', path };
64
+ }
65
+ //# sourceMappingURL=policy-projection.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * What can be known, from outside a Claude Code session, about whether the
3
+ * ruflo mod will load and what it will own (ADR-404). Every finding names its
4
+ * source; nothing here claims a live load it did not see. The one direct
5
+ * evidence of a load is the heartbeat the mod writes at session start.
6
+ */
7
+ import { type ClaudeInstall } from './claude-installs.js';
8
+ import { type ReadFs } from './plugin-resolve.js';
9
+ export declare const HANDSHAKE_MARKER = "RUFLO_MODS_OWNS";
10
+ export declare const HEARTBEAT_RELATIVE: string;
11
+ export type Status = 'pass' | 'warn' | 'fail';
12
+ export interface Finding {
13
+ name: string;
14
+ status: Status;
15
+ message: string;
16
+ fix?: string;
17
+ }
18
+ /** Managed settings, where an organization sets sec-default's options. */
19
+ export declare function managedSettingsPath(os?: NodeJS.Platform): string;
20
+ export interface ProbeInputs {
21
+ projectRoot: string;
22
+ home?: string;
23
+ env?: NodeJS.ProcessEnv;
24
+ managedPath?: string;
25
+ /** Claude Code binaries on PATH; discovered (each `--version` run) when absent. */
26
+ installs?: ClaudeInstall[];
27
+ /** Claude Code's config directory; CLAUDE_CONFIG_DIR, else ~/.claude, when absent. */
28
+ configDir?: string;
29
+ /** Reads for the plugin-resolution findings; the real filesystem when absent. */
30
+ fs?: ReadFs;
31
+ }
32
+ export declare function probeMods(inputs: ProbeInputs): Finding[];
33
+ //# sourceMappingURL=probe.d.ts.map
@@ -0,0 +1,143 @@
1
+ /**
2
+ * What can be known, from outside a Claude Code session, about whether the
3
+ * ruflo mod will load and what it will own (ADR-404). Every finding names its
4
+ * source; nothing here claims a live load it did not see. The one direct
5
+ * evidence of a load is the heartbeat the mod writes at session start.
6
+ */
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { homedir, platform } from 'node:os';
9
+ import { join, resolve } from 'node:path';
10
+ import { findClaudeInstalls, judgeInstalls } from './claude-installs.js';
11
+ import { ENABLE_ENV, MOD_PLUGIN_ID, MOD_PLUGIN_IDS, readRecord, readSettingsFile, settingsFileFor } from './install.js';
12
+ import { claudeConfigDir, resolveFindings } from './plugin-resolve.js';
13
+ import { PROJECTION_RELATIVE } from './policy-projection.js';
14
+ export const HANDSHAKE_MARKER = 'RUFLO_MODS_OWNS';
15
+ export const HEARTBEAT_RELATIVE = join('.claude-flow', 'mods', 'session.json');
16
+ const SEC_DEFAULT_ID = 'cc-plugin-sec-default@builtin';
17
+ function readJson(path) {
18
+ try {
19
+ return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : undefined;
20
+ }
21
+ catch {
22
+ return undefined;
23
+ }
24
+ }
25
+ function get(obj, ...keys) {
26
+ return keys.reduce((o, k) => (o !== null && typeof o === 'object' ? o[k] : undefined), obj);
27
+ }
28
+ /** Managed settings, where an organization sets sec-default's options. */
29
+ export function managedSettingsPath(os = platform()) {
30
+ if (os === 'darwin')
31
+ return '/Library/Application Support/ClaudeCode/managed-settings.json';
32
+ if (os === 'win32')
33
+ return 'C:\\ProgramData\\ClaudeCode\\managed-settings.json';
34
+ return '/etc/claude-code/managed-settings.json';
35
+ }
36
+ export function probeMods(inputs) {
37
+ const root = resolve(inputs.projectRoot);
38
+ const home = inputs.home ?? homedir();
39
+ const env = inputs.env ?? process.env;
40
+ const configDir = inputs.configDir ?? claudeConfigDir(env, home);
41
+ const findings = [];
42
+ // 1. Enabled for this project?
43
+ const files = [settingsFileFor(root, 'local'), settingsFileFor(root, 'project'), join(configDir, 'settings.json')];
44
+ const enabledIn = files.find((f) => {
45
+ try {
46
+ return get(readSettingsFile(f), 'enabledPlugins', MOD_PLUGIN_ID) === true;
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ });
52
+ findings.push(enabledIn
53
+ ? { name: 'ruflo-mods plugin', status: 'pass', message: `enabled in settings: ${enabledIn} (whether Claude Code can load it: see below)` }
54
+ : { name: 'ruflo-mods plugin', status: 'warn', message: 'not enabled; classic hooks handle every event', fix: 'ruflo mods install' });
55
+ // 1a. Enabled is a request; Claude Code silently skips a plugin its
56
+ // marketplace clone does not carry (a clone older than the plugin).
57
+ if (enabledIn) {
58
+ const scope = enabledIn === files[0] ? 'local' : enabledIn === files[1] ? 'project' : 'user';
59
+ const enabled = MOD_PLUGIN_IDS.filter((id) => files.some((f) => {
60
+ try {
61
+ return get(readSettingsFile(f), 'enabledPlugins', id) === true;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }));
67
+ // The marketplace the first settings file declaring one names (local wins, as in Claude Code).
68
+ const declared = files.map((f) => {
69
+ try {
70
+ return get(readSettingsFile(f), 'extraKnownMarketplaces', 'ruflo');
71
+ }
72
+ catch {
73
+ return undefined;
74
+ }
75
+ }).find((d) => d !== undefined);
76
+ findings.push(...resolveFindings(root, scope, configDir, inputs.fs, enabled, declared));
77
+ }
78
+ // 2. Function hooks switched on for installed plugins (early access).
79
+ const envOn = env[ENABLE_ENV] === '1';
80
+ const settingsOn = files.some((f) => {
81
+ try {
82
+ return get(readSettingsFile(f), 'env', ENABLE_ENV) === '1';
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ });
88
+ // 2a. Which Claude Code runs, and whether a stale install shadows it.
89
+ findings.push({ name: 'claude installs', ...judgeInstalls(inputs.installs ?? findClaudeInstalls(env, home), envOn || settingsOn) });
90
+ // Observed live (ADR-404): 2.1.287 loads mods with ENABLE_ENV unset;
91
+ // 2.1.282 refuses them without it ("not turned on for installed plugins");
92
+ // on either, a server-side rollout switch served off holds them off, the
93
+ // variable notwithstanding. Which binary runs is the 'claude installs' finding.
94
+ const rollout = get(readJson(join(home, '.claude.json')), 'cachedGrowthBookFeatures', 'tengu_plugin_hooks_modules');
95
+ const enableMsg = `${ENABLE_ENV} ${envOn ? 'set in this environment' : settingsOn ? 'set in settings env' : 'not set'}`;
96
+ findings.push(rollout === true
97
+ ? { name: 'function hooks', status: 'pass', message: `Claude Code rollout switch cached on; ${enableMsg}` }
98
+ : rollout === false
99
+ ? { name: 'function hooks', status: 'warn', message: `Claude Code serves function hooks OFF for this account (cached); the mod will not load and classic hooks keep every event; ${enableMsg}` }
100
+ : { name: 'function hooks', status: 'warn', message: `rollout switch not cached (unverified whether the mod can load); ${enableMsg}`, fix: `export ${ENABLE_ENV}=1 and start Claude Code once` });
101
+ // 3. Refused by policy?
102
+ const managed = readJson(inputs.managedPath ?? managedSettingsPath());
103
+ const managedOnly = get(managed, 'pluginConfigs', SEC_DEFAULT_ID, 'options', 'allowManagedModsOnly');
104
+ findings.push(managedOnly !== undefined && managedOnly !== false
105
+ ? { name: 'managed policy', status: 'warn', message: 'allowManagedModsOnly is set: Claude Code refuses user mods; classic hooks stay in charge' }
106
+ : { name: 'managed policy', status: 'pass', message: managed === undefined ? 'no managed settings' : 'user mods allowed (allowManagedModsOnly not set)' });
107
+ // 4. Every classic helper a hook could run honours the handshake (the mod's
108
+ // own rule, plugins/ruflo-mods/hooks/session.ts); else the mod stands down.
109
+ const helpers = [...new Set([join(root, '.claude', 'helpers', 'hook-handler.cjs'), join(home, '.claude', 'helpers', 'hook-handler.cjs')])].filter(existsSync);
110
+ const stale = helpers.filter((h) => {
111
+ try {
112
+ return !readFileSync(h, 'utf8').includes(HANDSHAKE_MARKER);
113
+ }
114
+ catch {
115
+ return true;
116
+ }
117
+ });
118
+ findings.push(helpers.length === 0
119
+ ? { name: 'classic handshake', status: 'pass', message: 'no hook-handler.cjs: the mod owns route and post-edit outright' }
120
+ : stale.length === 0
121
+ ? { name: 'classic handshake', status: 'pass', message: `${helpers.join(', ')} hand route and post-edit to the mod while it runs` }
122
+ : { name: 'classic handshake', status: 'warn', message: `${stale.join(', ')} predate${stale.length === 1 ? 's' : ''} the handshake: the mod stands down and classic hooks keep route/post-edit`, fix: 'ruflo init --upgrade (refresh helpers in the project and in ~/.claude)' });
123
+ // 5. Policy projection.
124
+ const projectionPath = join(root, PROJECTION_RELATIVE);
125
+ const projection = readJson(projectionPath);
126
+ const stateMode = get(readJson(join(root, '.claude-flow', 'policy', 'state.json')), 'mode');
127
+ findings.push(projection !== undefined
128
+ ? { name: 'policy projection', status: 'pass', message: `${get(projection, 'mode')} mode, ${get(projection, 'rules')?.length ?? 0} claude-code rule(s)` }
129
+ : stateMode === 'enforce' || stateMode === 'observe'
130
+ ? { name: 'policy projection', status: 'warn', message: `policy is ${String(stateMode)} but no projection exists (no claude-code.* rules, or state written by an older CLI)`, fix: 'ruflo mods sync-policy' }
131
+ : { name: 'policy projection', status: 'pass', message: 'none (no ruflo policy for Claude Code tools)' });
132
+ // 6. Evidence of a load.
133
+ const beat = readJson(join(root, HEARTBEAT_RELATIVE));
134
+ const startedAt = get(beat, 'startedAt');
135
+ const owned = get(beat, 'owned');
136
+ findings.push(typeof startedAt === 'string'
137
+ ? { name: 'last mod start', status: 'pass', message: `${startedAt}, owning ${Array.isArray(owned) && owned.length ? owned.join(', ') : 'nothing'}` }
138
+ : { name: 'last mod start', status: enabledIn ? 'warn' : 'pass', message: 'the mod has not started in this project' + (enabledIn ? ' (restart Claude Code; check function hooks above)' : '') });
139
+ if (readRecord(root))
140
+ findings.push({ name: 'install record', status: 'pass', message: join(root, '.claude-flow', 'mods', 'install.json') });
141
+ return findings;
142
+ }
143
+ //# sourceMappingURL=probe.js.map
@@ -1,4 +1,9 @@
1
1
  import { AgenticPolicyEngine, type BudgetLimit, type CapabilityEnvelope, type PolicyApproval, type PolicyDecision, type PolicyRequest, type PolicyRule, type PolicyState } from '@claude-flow/security';
2
+ /**
3
+ * Exported for ADR-406 mission storage, which takes the same per-file lock
4
+ * (pid, pid namespace and boot id identify a dead owner) rather than a copy.
5
+ */
6
+ export declare function acquireLock(lockPath: string): Promise<() => void>;
2
7
  export declare function loadPolicyState(projectRoot?: string): PolicyState;
3
8
  export declare function autoMigratePolicyStateIfNeeded(projectRoot?: string): Promise<{
4
9
  migrated: boolean;
@@ -4,6 +4,7 @@ import { execFileSync } from 'node:child_process';
4
4
  import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readlinkSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
5
5
  import { dirname, join, resolve } from 'node:path';
6
6
  import { hostname, userInfo } from 'node:os';
7
+ import { syncPolicyProjection } from '../mods/policy-projection.js';
7
8
  const POLICY_DIR = join('.claude-flow', 'policy');
8
9
  const POLICY_FILE = 'state.json';
9
10
  const LOCK_FILE = 'state.lock';
@@ -119,7 +120,11 @@ function lockOwnerIsDead(lockPath) {
119
120
  return false;
120
121
  }
121
122
  }
122
- async function acquireLock(lockPath) {
123
+ /**
124
+ * Exported for ADR-406 mission storage, which takes the same per-file lock
125
+ * (pid, pid namespace and boot id identify a dead owner) rather than a copy.
126
+ */
127
+ export async function acquireLock(lockPath) {
123
128
  const started = Date.now();
124
129
  while (Date.now() - started < LOCK_WAIT_MS) {
125
130
  try {
@@ -227,6 +232,20 @@ function verifyStateAnchor(projectRoot, state) {
227
232
  }
228
233
  }
229
234
  async function writePolicyState(projectRoot, statePath, state) {
235
+ await writePolicyStateFiles(projectRoot, statePath, state);
236
+ // ADR-404: the ruflo mod reads Claude Code tool rules from a small
237
+ // projection, never from state.json. Written only after the state and its
238
+ // anchor are safely down; a failure here never fails the state write. A
239
+ // projection left stale by such a failure can only tighten, never loosen,
240
+ // a Claude Code verdict (the mod merges with `stricter`).
241
+ try {
242
+ syncPolicyProjection(projectRoot, state);
243
+ }
244
+ catch (error) {
245
+ process.stderr.write(`[policy] claude-code projection not written: ${error.message}\n`);
246
+ }
247
+ }
248
+ async function writePolicyStateFiles(projectRoot, statePath, state) {
230
249
  const anchorPath = trustPaths(projectRoot).anchor;
231
250
  if (state.mode === 'enforce' || existsSync(anchorPath)) {
232
251
  const key = trustKey(projectRoot, true);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.49.0",
3
+ "version": "3.51.0",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",