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,186 @@
1
+ /**
2
+ * Turn the ruflo mods on (or off) for a project (ADR-404): merge the
3
+ * settings, sync the policy projection, then make the plugins loadable with
4
+ * `claude`. One path for `ruflo mods install`, `ruflo init` (and its wizard)
5
+ * and `ruflo init upgrade --mods`; `removeMods` is `ruflo mods uninstall`.
6
+ */
7
+ import { readFileSync } from 'node:fs';
8
+ import { homedir } from 'node:os';
9
+ import { output } from '../output.js';
10
+ import { installMod, MARKETPLACE_NAME, MARKETPLACE_SOURCE, MOD_PLUGINS, readRecord, recordClaudeInstalled, scopeOfSettingsFile, uninstallMod, } from './install.js';
11
+ import { findClaudeBinary, installArgv, marketplaceArgv, repairPluginInstall, uninstallArgv, uninstallPlugins } from './plugin-repair.js';
12
+ import { claudeConfigDir, describeSource, installedState, marketplaceState, repairCommands, resolveFindings, sameSource } from './plugin-resolve.js';
13
+ /** One line per thing a run added; empty on a re-run. */
14
+ export function describeAdded(added) {
15
+ return [
16
+ ...added.plugins.map((id) => `enabledPlugins["${id}"] = true`),
17
+ ...(added.marketplace ? ['extraKnownMarketplaces.ruflo'] : []),
18
+ ...(added.env ? ['env.CLAUDE_CODE_ENABLE_FUNCTION_HOOKS = "1"'] : []),
19
+ ];
20
+ }
21
+ /** What default-on means, said exactly (ADR-404 Amendment 1). */
22
+ export const LOAD_NOTE = [
23
+ 'ruflo-mods and ruflo-console run only where Claude Code function hooks are on: with them off, or the rollout switch off, they do nothing and the classic hooks keep every event.',
24
+ "ruflo-swarm's commands, skills and agents load without function hooks; with them on, ruflo-swarm is also a mod that can run host commands (pane actions you start).",
25
+ "Each teammate's first trusted interactive Claude Code start clones github.com/ruvnet/ruflo; a headless claude -p on a fresh config loads nothing.",
26
+ 'Optional hardening: pluginConfigs["ruflo-mods@ruflo"].options.modTrust = "refuse-risky" with modTrustAllow, in user or managed settings (project settings cannot set it).',
27
+ ];
28
+ async function syncPolicyQuietly(root) {
29
+ try {
30
+ const { loadPolicyState } = await import('../services/policy-runtime.js');
31
+ const { syncPolicyProjection } = await import('./policy-projection.js');
32
+ const result = syncPolicyProjection(root, loadPolicyState(root));
33
+ if (result.action !== 'unchanged')
34
+ output.writeln(`policy projection: ${result.action} (${result.path})`);
35
+ }
36
+ catch (error) {
37
+ output.printWarning(`policy projection not synced: ${error.message}`);
38
+ }
39
+ }
40
+ function printSteps(steps) {
41
+ for (const step of steps) {
42
+ const mark = step.state === 'ok' ? output.success('✓') : step.state === 'pending' ? output.warning('…') : output.error('✗');
43
+ output.writeln(`${mark} claude ${step.argv.join(' ')}${step.state === 'pending' ? ' (pending: not in the ruflo marketplace yet)' : ''}`);
44
+ if (step.state === 'failed' && step.output)
45
+ output.writeln(output.dim(` ${step.output.split('\n').slice(-3).join('\n ')}`));
46
+ }
47
+ }
48
+ const canon = (v) => Array.isArray(v) ? v.map(canon) : v && typeof v === 'object' ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, canon(v[k])])) : v;
49
+ function readText(path) {
50
+ try {
51
+ return readFileSync(path, 'utf8');
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ }
57
+ /** Claude Code re-serializes settings it touches; say so, never fight it. */
58
+ function noteRewrite(file, before) {
59
+ const after = readText(file);
60
+ if (before === undefined || after === undefined || before === after)
61
+ return;
62
+ try {
63
+ output.writeln(output.dim(JSON.stringify(canon(JSON.parse(before))) === JSON.stringify(canon(JSON.parse(after)))
64
+ ? `Claude Code reformatted ${file} (key order and layout only; content unchanged).`
65
+ : `Claude Code also changed ${file}; review it before committing.`));
66
+ }
67
+ catch {
68
+ // Not ours to judge further.
69
+ }
70
+ }
71
+ export async function applyMods(root, opts) {
72
+ const install = installMod(root, opts.scope, opts.dryRun === true, undefined, opts.marketplace ?? MARKETPLACE_SOURCE);
73
+ const configDir = claudeConfigDir(process.env, homedir());
74
+ const lines = describeAdded(install.added);
75
+ // Only what is enabled in the file: `claude plugin install` would flip a
76
+ // plugin someone set to false back to true.
77
+ const enabled = (install.next.enabledPlugins ?? {});
78
+ const plugins = MOD_PLUGINS.filter((p) => enabled[p.id] === true);
79
+ // The marketplace the file declares wins: a person's own declaration is kept and used.
80
+ const declared = install.next.extraKnownMarketplaces?.[MARKETPLACE_NAME] ?? MARKETPLACE_SOURCE;
81
+ const market = marketplaceState(configDir);
82
+ const known = market.known && sameSource(market.source, declared.source);
83
+ if (install.dryRun) {
84
+ output.writeln(`Would write ${install.settingsFile}:`);
85
+ output.printJson(install.next);
86
+ if (opts.pluginInstall !== false) {
87
+ output.writeln('Would run:');
88
+ const installs = declared.source.source === 'directory' ? [] : plugins.map((p) => installArgv(p.id, opts.scope));
89
+ for (const argv of [marketplaceArgv(opts.scope, known, declared), ...installs])
90
+ output.writeln(` claude ${argv.join(' ')}`);
91
+ }
92
+ return { install, resolvable: true };
93
+ }
94
+ await syncPolicyQuietly(root);
95
+ if (lines.length) {
96
+ output.printSuccess(`ruflo mods enabled in ${install.settingsFile}${install.backup ? ` (backup: ${install.backup})` : ''}`);
97
+ for (const line of lines)
98
+ output.writeln(` + ${line}`);
99
+ }
100
+ else {
101
+ output.printSuccess(`ruflo mods already enabled in ${install.settingsFile} (nothing changed)`);
102
+ }
103
+ output.writeln(` marketplace: ${describeSource(declared.source)}`);
104
+ if (opts.marketplace && !sameSource(opts.marketplace.source, declared.source)) {
105
+ output.printWarning(`extraKnownMarketplaces.ruflo was already set (${describeSource(declared.source)}); left as it is.`);
106
+ }
107
+ for (const line of LOAD_NOTE)
108
+ output.writeln(output.dim(line));
109
+ const manual = (k = known) => {
110
+ output.writeln('Run these to make the plugins loadable:');
111
+ for (const line of repairCommands(root, opts.scope, k, plugins, declared))
112
+ output.writeln(` ${line}`);
113
+ };
114
+ if (opts.pluginInstall === false)
115
+ return { install, resolvable: true, skipped: '--no-plugin-install' };
116
+ if (opts.skipInTestEnv && (process.env.VITEST || process.env.CI)) {
117
+ output.writeln(output.dim(`claude plugin step skipped (${process.env.VITEST ? 'VITEST' : 'CI'} set).`));
118
+ manual();
119
+ return { install, resolvable: false, skipped: 'test or CI environment' };
120
+ }
121
+ const claude = findClaudeBinary(process.env, homedir());
122
+ if (!claude) {
123
+ output.printWarning('No runnable claude binary on PATH: the plugins are enabled in settings, not installed.');
124
+ manual();
125
+ return { install, resolvable: false, skipped: 'no claude on PATH' };
126
+ }
127
+ if (declared.source.source === 'directory' && market.known && !sameSource(market.source, declared.source)) {
128
+ output.printWarning(`Claude Code keeps one ruflo marketplace per config dir: it now switches from ${describeSource(market.source)} to ${describeSource(declared.source)} for every project on this machine. Switch back with: claude plugin marketplace add ruvnet/ruflo`);
129
+ }
130
+ // Never claim (and so never uninstall) a plugin that was installed before, or one ruflo did not enable.
131
+ const before = new Set(plugins.filter((p) => installedState(root, configDir, p.id).installed).map((p) => p.id));
132
+ const textBefore = readText(install.settingsFile);
133
+ const repair = await repairPluginInstall({ projectRoot: root, scope: opts.scope, configDir, claude, plugins, marketplace: declared, exec: opts.exec });
134
+ printSteps(repair.steps);
135
+ if (declared.source.source === 'directory' && repair.ok)
136
+ output.writeln(output.dim(`Directory marketplace: plugins load live from ${declared.source.path} (no clone, no install).`));
137
+ const enabledByRuflo = new Set(readRecord(root)?.files[install.settingsFile]?.plugins ?? []);
138
+ const ours = repair.steps
139
+ .filter((st) => st.state === 'ok' && st.argv[1] === 'install')
140
+ .map((st) => st.argv[2])
141
+ .filter((id) => !before.has(id) && enabledByRuflo.has(id));
142
+ recordClaudeInstalled(root, install.settingsFile, ours);
143
+ noteRewrite(install.settingsFile, textBefore);
144
+ const failed = resolveFindings(root, opts.scope, configDir, undefined, plugins.map((p) => p.id), declared).filter((f) => f.status === 'fail');
145
+ if (repair.ok && failed.length === 0)
146
+ return { install, resolvable: true };
147
+ for (const f of failed)
148
+ output.writeln(`${output.error('✗')} ${f.name}: ${f.message}`);
149
+ const now = marketplaceState(configDir);
150
+ manual(now.known && sameSource(now.source, declared.source));
151
+ return { install, resolvable: false };
152
+ }
153
+ /**
154
+ * `ruflo mods uninstall`: `claude plugin uninstall` exactly the plugins ruflo
155
+ * installed, then remove exactly the settings keys it added.
156
+ */
157
+ export async function removeMods(root, opts = {}) {
158
+ const record = readRecord(root);
159
+ const plan = Object.entries(record?.files ?? {}).map(([file, added]) => ({ file, scope: scopeOfSettingsFile(file), ids: added.claudeInstalled ?? [] }));
160
+ const uninstalled = [];
161
+ const failed = [];
162
+ if (opts.dryRun) {
163
+ for (const p of plan)
164
+ for (const id of p.ids)
165
+ output.writeln(`Would run: claude ${uninstallArgv(id, p.scope).join(' ')}`);
166
+ }
167
+ else if (plan.some((p) => p.ids.length)) {
168
+ const claude = findClaudeBinary(process.env, homedir());
169
+ for (const p of plan.filter((x) => x.ids.length)) {
170
+ if (!claude) {
171
+ output.printWarning('No runnable claude binary on PATH; run these to remove what ruflo installed:');
172
+ for (const id of p.ids)
173
+ output.writeln(` cd ${JSON.stringify(root)} && claude ${uninstallArgv(id, p.scope).join(' ')}`);
174
+ failed.push(...p.ids);
175
+ continue;
176
+ }
177
+ const result = await uninstallPlugins({ projectRoot: root, scope: p.scope, ids: p.ids, claude, exec: opts.exec });
178
+ printSteps(result.steps);
179
+ for (const st of result.steps)
180
+ (st.state === 'ok' ? uninstalled : failed).push(st.argv[2]);
181
+ }
182
+ }
183
+ const result = uninstallMod(root, opts.dryRun === true);
184
+ return { ...result, uninstalled, failed };
185
+ }
186
+ //# sourceMappingURL=apply.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Which Claude Code binaries a shell would find, and whether the one it runs
3
+ * can load mods (ADR-404). Mods are on by default from Claude Code 2.1.287;
4
+ * from 2.1.277 they load with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1; older
5
+ * builds predate them. A server-side rollout switch can still hold them off
6
+ * on any version (reported separately). A stale install earlier on PATH
7
+ * (a 2.1.107 npm-global beside a current native one, say) silently decides.
8
+ */
9
+ export declare const MODS_DEFAULT_ON = "2.1.287";
10
+ export declare const MODS_FIRST = "2.1.277";
11
+ export interface ClaudeInstall {
12
+ path: string;
13
+ version: string | null;
14
+ }
15
+ export type VersionOf = (path: string) => string | null;
16
+ /** a < b for dotted numeric versions. */
17
+ export declare function versionLess(a: string, b: string): boolean;
18
+ /** `claude --version` of one binary, bounded; null when it cannot say. */
19
+ export declare const defaultVersionOf: VersionOf;
20
+ /** Every `claude` on PATH, in PATH order (the first is what runs), plus ~/.local/bin. */
21
+ export declare function findClaudeInstalls(env: NodeJS.ProcessEnv, home: string, versionOf?: VersionOf): ClaudeInstall[];
22
+ export interface InstallsFinding {
23
+ status: 'pass' | 'warn';
24
+ message: string;
25
+ fix?: string;
26
+ }
27
+ /** The doctor's reading of what `claude` resolves to and what else is installed. */
28
+ export declare function judgeInstalls(installs: readonly ClaudeInstall[], enableEnvSet: boolean): InstallsFinding;
29
+ //# sourceMappingURL=claude-installs.d.ts.map
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Which Claude Code binaries a shell would find, and whether the one it runs
3
+ * can load mods (ADR-404). Mods are on by default from Claude Code 2.1.287;
4
+ * from 2.1.277 they load with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1; older
5
+ * builds predate them. A server-side rollout switch can still hold them off
6
+ * on any version (reported separately). A stale install earlier on PATH
7
+ * (a 2.1.107 npm-global beside a current native one, say) silently decides.
8
+ */
9
+ import { execFileSync } from 'node:child_process';
10
+ import { existsSync, realpathSync, statSync } from 'node:fs';
11
+ import { delimiter, join } from 'node:path';
12
+ export const MODS_DEFAULT_ON = '2.1.287';
13
+ export const MODS_FIRST = '2.1.277';
14
+ const parse = (v) => v.split('.').map((n) => Number(n));
15
+ /** a < b for dotted numeric versions. */
16
+ export function versionLess(a, b) {
17
+ const x = parse(a);
18
+ const y = parse(b);
19
+ for (let i = 0; i < Math.max(x.length, y.length); i++) {
20
+ if ((x[i] ?? 0) !== (y[i] ?? 0))
21
+ return (x[i] ?? 0) < (y[i] ?? 0);
22
+ }
23
+ return false;
24
+ }
25
+ /** `claude --version` of one binary, bounded; null when it cannot say. */
26
+ export const defaultVersionOf = (path) => {
27
+ try {
28
+ const out = execFileSync(path, ['--version'], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
29
+ return out.match(/\d+\.\d+\.\d+/)?.[0] ?? null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ };
35
+ /** Every `claude` on PATH, in PATH order (the first is what runs), plus ~/.local/bin. */
36
+ export function findClaudeInstalls(env, home, versionOf = defaultVersionOf) {
37
+ const names = process.platform === 'win32' ? ['claude.exe', 'claude.cmd', 'claude'] : ['claude'];
38
+ const dirs = [...(env.PATH ?? '').split(delimiter).filter(Boolean), join(home, '.local', 'bin')];
39
+ const seen = new Set();
40
+ const installs = [];
41
+ for (const dir of dirs) {
42
+ for (const name of names) {
43
+ const path = join(dir, name);
44
+ let real;
45
+ try {
46
+ if (!existsSync(path) || !statSync(path).isFile())
47
+ continue;
48
+ real = realpathSync(path);
49
+ }
50
+ catch {
51
+ continue;
52
+ }
53
+ if (seen.has(real))
54
+ continue;
55
+ seen.add(real);
56
+ installs.push({ path, version: versionOf(path) });
57
+ }
58
+ }
59
+ return installs;
60
+ }
61
+ /** The doctor's reading of what `claude` resolves to and what else is installed. */
62
+ export function judgeInstalls(installs, enableEnvSet) {
63
+ if (installs.length === 0)
64
+ return { status: 'warn', message: 'no claude binary on PATH', fix: 'install Claude Code >= 2.1.287' };
65
+ const [first] = installs;
66
+ const others = installs.slice(1).filter((i) => i.version !== first.version);
67
+ const mixed = others.length
68
+ ? `; also installed: ${others.map((i) => `${i.path} (${i.version ?? 'unknown'})`).join(', ')}. The first on PATH runs; a stale one can shadow a current one.`
69
+ : '';
70
+ const runs = `${first.path} (${first.version ?? 'version unknown'})`;
71
+ if (!first.version)
72
+ return { status: 'warn', message: `claude resolves to ${runs}${mixed}` };
73
+ if (!versionLess(first.version, MODS_DEFAULT_ON)) {
74
+ return { status: others.length ? 'warn' : 'pass', message: `claude resolves to ${runs}: mods on by default${mixed}` };
75
+ }
76
+ if (!versionLess(first.version, MODS_FIRST)) {
77
+ return enableEnvSet
78
+ ? { status: others.length ? 'warn' : 'pass', message: `claude resolves to ${runs}: mods load with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 (set)${mixed}` }
79
+ : { status: 'warn', message: `claude resolves to ${runs}: below ${MODS_DEFAULT_ON}, mods need CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 (not set)${mixed}`, fix: `upgrade to >= ${MODS_DEFAULT_ON}, or export CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1` };
80
+ }
81
+ return { status: 'warn', message: `claude resolves to ${runs}: predates mods (< ${MODS_FIRST}); classic hooks handle every event${mixed}`, fix: `upgrade to >= ${MODS_DEFAULT_ON} and remove the stale install from PATH` };
82
+ }
83
+ //# sourceMappingURL=claude-installs.js.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * ADR-406 P1 — classify the inventory into `CommandDefinition`s, validate
3
+ * them, and emit a deterministic catalog.
4
+ *
5
+ * Determinism: entries sorted by id, object keys sorted recursively, no
6
+ * generation timestamp, and `source.commit` is the last commit that touched
7
+ * the source file (not the generator's HEAD), so regenerating an unchanged
8
+ * tree yields the same bytes.
9
+ */
10
+ import { type CatalogEntry, type CommandCatalog } from './schema.js';
11
+ import { type RawCommand } from './inventory.js';
12
+ export declare const REPOSITORY = "ruvnet/ruflo";
13
+ export interface CatalogOptions {
14
+ /** Repository-relative path -> last commit sha touching it. Missing = `uncommitted`. */
15
+ readonly commits?: Readonly<Record<string, string>>;
16
+ /** True when `packedFiles` was supplied to the inventory, so `shippedIn` is meaningful. */
17
+ readonly packageContentsInspected: boolean;
18
+ /** Last commit touching any command source (stable across unrelated commits). */
19
+ readonly sourceCommit?: string;
20
+ }
21
+ /** JSON with object keys sorted at every depth and no whitespace. */
22
+ export declare function canonicalJson(value: unknown): string;
23
+ export declare function catalogDigest(entries: readonly CatalogEntry[]): string;
24
+ /** Merge the occurrences of one invocation spelling into one catalog entry. */
25
+ export declare function classify(key: string, occurrences: readonly RawCommand[], options: CatalogOptions): CatalogEntry;
26
+ export interface ValidationResult {
27
+ readonly valid: boolean;
28
+ readonly errors: readonly string[];
29
+ }
30
+ /** §14 P1 gate: schema-valid definitions, no duplicate ids, no duplicate names. */
31
+ export declare function validateEntries(entries: readonly unknown[]): ValidationResult;
32
+ export declare function buildCatalog(inventory: readonly RawCommand[], options: CatalogOptions): CommandCatalog;
33
+ /** Parse and re-verify a catalog read from disk (the parity check a consumer runs). */
34
+ export declare function verifyCatalog(raw: unknown): ValidationResult;
35
+ export declare function serializeCatalog(catalog: CommandCatalog): string;
36
+ /**
37
+ * Write the catalog atomically. `expectedSha256` is the digest of the file the
38
+ * caller last read (or `null` for "must not exist"); a mismatch means someone
39
+ * else changed it and the write is refused (§6 concurrent-edit detection).
40
+ */
41
+ export declare function writeCatalog(path: string, catalog: CommandCatalog, expectedSha256?: string | null): {
42
+ written: boolean;
43
+ sha256: string;
44
+ };
45
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1,234 @@
1
+ /**
2
+ * ADR-406 P1 — classify the inventory into `CommandDefinition`s, validate
3
+ * them, and emit a deterministic catalog.
4
+ *
5
+ * Determinism: entries sorted by id, object keys sorted recursively, no
6
+ * generation timestamp, and `source.commit` is the last commit that touched
7
+ * the source file (not the generator's HEAD), so regenerating an unchanged
8
+ * tree yields the same bytes.
9
+ */
10
+ import { existsSync, readFileSync, renameSync, writeFileSync, mkdirSync, openSync, fsyncSync, closeSync } from 'node:fs';
11
+ import { dirname, basename } from 'node:path';
12
+ import { COMMAND_CATALOG_CONTRACT, LIMITS, catalogEntrySchema, commandCatalogSchema, } from './schema.js';
13
+ import { sha256Hex } from './inventory.js';
14
+ export const REPOSITORY = 'ruvnet/ruflo';
15
+ // Loader-exposed documentation is named in capitals (README, CHANGELOG,
16
+ // COMMAND_COMPLIANCE_REPORT). Lower-case names such as `performance-report`
17
+ // are real workflows and must not be matched by content words.
18
+ const DOC_NAME = /^[A-Z][A-Z0-9_]+$/;
19
+ const CHANNEL_PRIORITY = {
20
+ 'repo-project-commands': 0,
21
+ 'cli-init-template': 1,
22
+ 'marketplace-plugin': 2,
23
+ 'source-plugin': 3,
24
+ 'mod-registration': 4,
25
+ };
26
+ /** JSON with object keys sorted at every depth and no whitespace. */
27
+ export function canonicalJson(value) {
28
+ return JSON.stringify(value, (_key, v) => {
29
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
30
+ return Object.fromEntries(Object.entries(v).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
31
+ }
32
+ return v;
33
+ });
34
+ }
35
+ export function catalogDigest(entries) {
36
+ return `sha256:${sha256Hex(canonicalJson(entries))}`;
37
+ }
38
+ function frontmatter(content) {
39
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
40
+ if (!match)
41
+ return {};
42
+ const out = {};
43
+ for (const line of match[1].split(/\r?\n/)) {
44
+ const kv = line.match(/^([A-Za-z][\w-]*):\s*(.*)$/);
45
+ if (kv)
46
+ out[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, '');
47
+ }
48
+ return out;
49
+ }
50
+ function substitutionOf(content) {
51
+ if (content.includes('$ARGUMENTS'))
52
+ return '$ARGUMENTS';
53
+ if (/\$[1-9]\b/.test(content))
54
+ return 'positional';
55
+ return 'none';
56
+ }
57
+ function allowedTools(meta) {
58
+ const raw = meta['allowed-tools'];
59
+ if (!raw)
60
+ return [];
61
+ return [...new Set(raw.replace(/^\[|\]$/g, '').split(',').map((t) => t.trim().replace(/^["']|["']$/g, '')).filter(Boolean))]
62
+ .map((t) => t.slice(0, LIMITS.capability))
63
+ .slice(0, LIMITS.capabilities)
64
+ .sort();
65
+ }
66
+ function isDocumentation(source) {
67
+ return DOC_NAME.test(basename(source.sourcePath, '.md'));
68
+ }
69
+ /** Merge the occurrences of one invocation spelling into one catalog entry. */
70
+ export function classify(key, occurrences, options) {
71
+ const ordered = [...occurrences].sort((a, b) => CHANNEL_PRIORITY[a.source.channel] - CHANNEL_PRIORITY[b.source.channel]
72
+ || a.source.sourcePath.localeCompare(b.source.sourcePath));
73
+ const markdown = ordered.filter((o) => o.source.binding === 'markdown-loader');
74
+ const registered = ordered.filter((o) => o.source.binding === 'mod-register');
75
+ const middleware = ordered.filter((o) => o.source.binding === 'mod-middleware-on-markdown');
76
+ const primary = markdown[0] ?? ordered[0];
77
+ const notes = [];
78
+ const commit = (path) => options.commits?.[path] ?? 'uncommitted';
79
+ let definition;
80
+ const base = {
81
+ schemaVersion: 1,
82
+ id: `${primary.ownerPlugin}/${key}`.toLowerCase(),
83
+ revision: 1,
84
+ ownerPlugin: primary.ownerPlugin,
85
+ modelVisibility: 'same-as-legacy',
86
+ source: { repository: REPOSITORY, commit: commit(primary.source.sourcePath), path: primary.source.sourcePath },
87
+ };
88
+ const documentation = markdown.length > 0 && isDocumentation(primary.source);
89
+ if (markdown.length > 0) {
90
+ const content = primary.content ?? '';
91
+ const meta = frontmatter(content);
92
+ const hint = (meta['argument-hint'] ?? '').slice(0, 200);
93
+ definition = {
94
+ ...base,
95
+ legacyNames: [key],
96
+ modNames: [],
97
+ kind: documentation ? 'help' : 'prompt',
98
+ argumentsSchema: hint
99
+ ? { type: 'string', maxLength: 8_192, description: hint }
100
+ : { type: 'string', maxLength: 8_192 },
101
+ prompt: { path: primary.source.sourcePath, sha256: primary.source.digest.slice(7), substitution: substitutionOf(content) },
102
+ requiredCapabilities: allowedTools(meta),
103
+ requiredHostFeatures: [],
104
+ // §7: no engine path to invoke a prompt workflow with its semantics is
105
+ // verified, so the workbench may show or fill it but never run it.
106
+ disposition: documentation ? 'legacy' : 'delegate',
107
+ };
108
+ if (documentation)
109
+ notes.push('documentation file exposed by the engine command loader; not an executable workflow');
110
+ if (middleware.length > 0) {
111
+ notes.push(`mod middleware attached by ${[...new Set(middleware.map((m) => m.ownerPlugin))].join(', ')} (command.run match)`);
112
+ }
113
+ }
114
+ else {
115
+ definition = {
116
+ ...base,
117
+ legacyNames: [],
118
+ modNames: [key],
119
+ kind: 'query',
120
+ argumentsSchema: { type: 'string', maxLength: 1_024 },
121
+ requiredCapabilities: [],
122
+ requiredHostFeatures: ['command.register'],
123
+ disposition: 'view',
124
+ viewId: `${primary.ownerPlugin}/${key}`.toLowerCase(),
125
+ };
126
+ notes.push('registered by a function-hook mod; exists only while that mod is loaded');
127
+ }
128
+ if (registered.length > 0 && markdown.length > 0)
129
+ notes.push('also registered by a mod under the same spelling');
130
+ const markdownDigests = new Set(markdown.map((m) => m.source.digest));
131
+ const divergent = markdownDigests.size > 1;
132
+ if (divergent)
133
+ notes.push('copies differ between delivery channels; see sources[].digest');
134
+ const shipped = ordered.some((o) => o.source.shippedIn.length > 0
135
+ || o.source.channel === 'marketplace-plugin'
136
+ || (o.source.channel === 'mod-registration' && o.source.package.endsWith('@ruflo')));
137
+ if (!options.packageContentsInspected)
138
+ notes.push('npm package contents not inspected for this catalog');
139
+ return catalogEntrySchema.parse({
140
+ definition,
141
+ status: documentation ? 'documentation' : shipped ? 'shipped' : 'source-only',
142
+ divergent,
143
+ sources: ordered.map((o) => o.source).slice(0, 8),
144
+ notes: notes.map((n) => n.slice(0, LIMITS.note)),
145
+ });
146
+ }
147
+ /** §14 P1 gate: schema-valid definitions, no duplicate ids, no duplicate names. */
148
+ export function validateEntries(entries) {
149
+ const errors = [];
150
+ const ids = new Map();
151
+ const names = new Map();
152
+ entries.forEach((raw, index) => {
153
+ const parsed = catalogEntrySchema.safeParse(raw);
154
+ if (!parsed.success) {
155
+ errors.push(`entry ${index}: ${parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')}`);
156
+ return;
157
+ }
158
+ const def = parsed.data.definition;
159
+ if (ids.has(def.id))
160
+ errors.push(`duplicate id ${def.id} (entries ${ids.get(def.id)} and ${index})`);
161
+ else
162
+ ids.set(def.id, index);
163
+ for (const name of new Set([...def.legacyNames, ...def.modNames])) {
164
+ const owner = names.get(name.toLowerCase());
165
+ if (owner && owner !== def.id)
166
+ errors.push(`duplicate name /${name} (${owner} and ${def.id})`);
167
+ else
168
+ names.set(name.toLowerCase(), def.id);
169
+ }
170
+ });
171
+ return { valid: errors.length === 0, errors };
172
+ }
173
+ export function buildCatalog(inventory, options) {
174
+ const groups = new Map();
175
+ for (const item of inventory) {
176
+ const list = groups.get(item.key) ?? [];
177
+ list.push(item);
178
+ groups.set(item.key, list);
179
+ }
180
+ const entries = [...groups.entries()]
181
+ .map(([key, list]) => classify(key, list, options))
182
+ .sort((a, b) => (a.definition.id < b.definition.id ? -1 : a.definition.id > b.definition.id ? 1 : 0));
183
+ const validation = validateEntries(entries);
184
+ if (!validation.valid)
185
+ throw new Error(`command-registry-invalid:\n${validation.errors.join('\n')}`);
186
+ const sourceCommit = options.sourceCommit ?? 'uncommitted';
187
+ return commandCatalogSchema.parse({
188
+ contractVersion: COMMAND_CATALOG_CONTRACT,
189
+ sourceCommit,
190
+ sourceDigest: catalogDigest(entries),
191
+ entries,
192
+ });
193
+ }
194
+ /** Parse and re-verify a catalog read from disk (the parity check a consumer runs). */
195
+ export function verifyCatalog(raw) {
196
+ const parsed = commandCatalogSchema.safeParse(raw);
197
+ if (!parsed.success)
198
+ return { valid: false, errors: parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`) };
199
+ const errors = [...validateEntries(parsed.data.entries).errors];
200
+ if (catalogDigest(parsed.data.entries) !== parsed.data.sourceDigest)
201
+ errors.push('sourceDigest does not match entries');
202
+ return { valid: errors.length === 0, errors };
203
+ }
204
+ export function serializeCatalog(catalog) {
205
+ return `${JSON.stringify(JSON.parse(canonicalJson(catalog)), null, 2)}\n`;
206
+ }
207
+ /**
208
+ * Write the catalog atomically. `expectedSha256` is the digest of the file the
209
+ * caller last read (or `null` for "must not exist"); a mismatch means someone
210
+ * else changed it and the write is refused (§6 concurrent-edit detection).
211
+ */
212
+ export function writeCatalog(path, catalog, expectedSha256) {
213
+ const text = serializeCatalog(catalog);
214
+ const next = sha256Hex(text);
215
+ const current = existsSync(path) ? sha256Hex(readFileSync(path)) : null;
216
+ if (expectedSha256 !== undefined && expectedSha256 !== current) {
217
+ throw new Error(`catalog-changed-on-disk: expected ${expectedSha256 ?? 'absent'}, found ${current ?? 'absent'}`);
218
+ }
219
+ if (current === next)
220
+ return { written: false, sha256: next };
221
+ mkdirSync(dirname(path), { recursive: true });
222
+ const tmp = `${path}.${process.pid}.tmp`;
223
+ writeFileSync(tmp, text, { mode: 0o644 });
224
+ const fd = openSync(tmp, 'r');
225
+ try {
226
+ fsyncSync(fd);
227
+ }
228
+ finally {
229
+ closeSync(fd);
230
+ }
231
+ renameSync(tmp, path);
232
+ return { written: true, sha256: next };
233
+ }
234
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1,66 @@
1
+ /**
2
+ * ADR-406 P0 — engine capability report.
3
+ *
4
+ * Every probe runs a fixed argv through an injected runner (never a shell),
5
+ * with `CLAUDE_CONFIG_DIR` pointed at an isolated directory so the probe
6
+ * never reads or writes the person's real `~/.claude`. Anything that could
7
+ * not be observed is reported as `unknown` with the reason, never assumed.
8
+ */
9
+ import type { ModCommandHook } from './inventory.js';
10
+ export type Runner = (file: string, args: readonly string[], env: NodeJS.ProcessEnv) => {
11
+ status: number | null;
12
+ stdout: string;
13
+ stderr: string;
14
+ };
15
+ export interface PluginValidation {
16
+ readonly plugin: string;
17
+ readonly passed: boolean | 'unknown';
18
+ readonly commandHooks: readonly string[];
19
+ readonly hostCalls: readonly string[];
20
+ /** A `command.run{command=?}` matcher: names chosen at run time, which only the source scan can list. */
21
+ readonly dynamicCommandHook?: true;
22
+ readonly reason?: string;
23
+ }
24
+ export interface EngineReport {
25
+ readonly schemaVersion: 1;
26
+ readonly engine: {
27
+ readonly executable: string | null;
28
+ readonly version: string | null;
29
+ readonly binarySha256: string | null;
30
+ };
31
+ readonly typeGeneration: 'none-in-engine-cli' | 'unknown';
32
+ readonly vendoredTypes: readonly {
33
+ readonly path: string;
34
+ readonly sha256: string;
35
+ }[];
36
+ readonly validations: readonly PluginValidation[];
37
+ readonly affordances: {
38
+ readonly promptFill: boolean | 'unknown';
39
+ readonly processRun: boolean | 'unknown';
40
+ };
41
+ readonly collisions: readonly {
42
+ readonly name: string;
43
+ readonly builtin: boolean | 'unknown';
44
+ readonly repoCommand: boolean;
45
+ readonly verdict: 'free' | 'taken' | 'unknown';
46
+ }[];
47
+ readonly limits: readonly string[];
48
+ }
49
+ /** Parse `claude plugin validate` text: hooks and `$.` calls the static scanner found. */
50
+ export declare function parseValidateOutput(plugin: string, status: number | null, text: string): PluginValidation;
51
+ /** Plugins that are function-hook mods (have `hooks/register.ts`). */
52
+ export declare function modPlugins(repoRoot: string): string[];
53
+ export interface EngineProbeInput {
54
+ readonly repoRoot: string;
55
+ readonly executable: string | null;
56
+ readonly isolatedConfigDir: string;
57
+ readonly run: Runner;
58
+ /** Names to check for collisions, e.g. `ruflo`, `ruflo-workbench`. */
59
+ readonly proposedNames: readonly string[];
60
+ /** Every name the repository already exposes (legacy + mod names). */
61
+ readonly existingNames: ReadonlySet<string>;
62
+ }
63
+ export declare function probeEngine(input: EngineProbeInput): EngineReport;
64
+ /** Engine-observed command hooks, in the inventory's `ModCommandHook` shape. */
65
+ export declare function hooksFromValidations(validations: readonly PluginValidation[]): ModCommandHook[];
66
+ //# sourceMappingURL=engine-report.d.ts.map