claude-flow 3.50.0 → 3.51.1

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 (68) hide show
  1. package/.claude/helpers/hook-handler.cjs +4 -3
  2. package/.claude/proven-config.json +1 -1
  3. package/.claude/settings.json +11 -2
  4. package/.claude-plugin/marketplace.json +5 -0
  5. package/README.md +3 -1
  6. package/package.json +1 -1
  7. package/v3/@claude-flow/cli/README.md +0 -2
  8. package/v3/@claude-flow/cli/catalog-manifest.json +4 -4
  9. package/v3/@claude-flow/cli/dist/src/commands/catalog.d.ts +18 -0
  10. package/v3/@claude-flow/cli/dist/src/commands/catalog.js +89 -0
  11. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +8 -3
  12. package/v3/@claude-flow/cli/dist/src/commands/federation.js +2 -2
  13. package/v3/@claude-flow/cli/dist/src/commands/index.js +3 -0
  14. package/v3/@claude-flow/cli/dist/src/commands/init.js +50 -11
  15. package/v3/@claude-flow/cli/dist/src/commands/mission.d.ts +24 -0
  16. package/v3/@claude-flow/cli/dist/src/commands/mission.js +116 -0
  17. package/v3/@claude-flow/cli/dist/src/commands/mods.d.ts +1 -1
  18. package/v3/@claude-flow/cli/dist/src/commands/mods.js +67 -23
  19. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +4 -2
  20. package/v3/@claude-flow/cli/dist/src/mcp-client.js +3 -0
  21. package/v3/@claude-flow/cli/dist/src/mcp-tools/capability-brain.js +1 -1
  22. package/v3/@claude-flow/cli/dist/src/mcp-tools/index.d.ts +1 -0
  23. package/v3/@claude-flow/cli/dist/src/mcp-tools/index.js +2 -0
  24. package/v3/@claude-flow/cli/dist/src/mcp-tools/mission-tools.d.ts +22 -0
  25. package/v3/@claude-flow/cli/dist/src/mcp-tools/mission-tools.js +75 -0
  26. package/v3/@claude-flow/cli/dist/src/mcp-tools/seraphina-tools.js +15 -12
  27. package/v3/@claude-flow/cli/dist/src/mcp-tools/service-url.d.ts +2 -0
  28. package/v3/@claude-flow/cli/dist/src/mcp-tools/service-url.js +34 -0
  29. package/v3/@claude-flow/cli/dist/src/mcp-tools/x-federation-tools.js +5 -4
  30. package/v3/@claude-flow/cli/dist/src/missions/fold.d.ts +19 -0
  31. package/v3/@claude-flow/cli/dist/src/missions/fold.js +238 -0
  32. package/v3/@claude-flow/cli/dist/src/missions/index.d.ts +10 -0
  33. package/v3/@claude-flow/cli/dist/src/missions/index.js +10 -0
  34. package/v3/@claude-flow/cli/dist/src/missions/observation.d.ts +70 -0
  35. package/v3/@claude-flow/cli/dist/src/missions/observation.js +77 -0
  36. package/v3/@claude-flow/cli/dist/src/missions/schemas.d.ts +735 -0
  37. package/v3/@claude-flow/cli/dist/src/missions/schemas.js +150 -0
  38. package/v3/@claude-flow/cli/dist/src/missions/service.d.ts +88 -0
  39. package/v3/@claude-flow/cli/dist/src/missions/service.js +278 -0
  40. package/v3/@claude-flow/cli/dist/src/missions/store.d.ts +68 -0
  41. package/v3/@claude-flow/cli/dist/src/missions/store.js +209 -0
  42. package/v3/@claude-flow/cli/dist/src/missions/transitions.d.ts +81 -0
  43. package/v3/@claude-flow/cli/dist/src/missions/transitions.js +178 -0
  44. package/v3/@claude-flow/cli/dist/src/mods/apply.d.ts +50 -0
  45. package/v3/@claude-flow/cli/dist/src/mods/apply.js +186 -0
  46. package/v3/@claude-flow/cli/dist/src/mods/command-registry/catalog.d.ts +45 -0
  47. package/v3/@claude-flow/cli/dist/src/mods/command-registry/catalog.js +234 -0
  48. package/v3/@claude-flow/cli/dist/src/mods/command-registry/engine-report.d.ts +66 -0
  49. package/v3/@claude-flow/cli/dist/src/mods/command-registry/engine-report.js +119 -0
  50. package/v3/@claude-flow/cli/dist/src/mods/command-registry/generate.d.ts +51 -0
  51. package/v3/@claude-flow/cli/dist/src/mods/command-registry/generate.js +166 -0
  52. package/v3/@claude-flow/cli/dist/src/mods/command-registry/index.d.ts +9 -0
  53. package/v3/@claude-flow/cli/dist/src/mods/command-registry/index.js +9 -0
  54. package/v3/@claude-flow/cli/dist/src/mods/command-registry/inventory.d.ts +63 -0
  55. package/v3/@claude-flow/cli/dist/src/mods/command-registry/inventory.js +230 -0
  56. package/v3/@claude-flow/cli/dist/src/mods/command-registry/schema.d.ts +836 -0
  57. package/v3/@claude-flow/cli/dist/src/mods/command-registry/schema.js +137 -0
  58. package/v3/@claude-flow/cli/dist/src/mods/install.d.ts +60 -22
  59. package/v3/@claude-flow/cli/dist/src/mods/install.js +91 -41
  60. package/v3/@claude-flow/cli/dist/src/mods/plugin-repair.d.ts +81 -0
  61. package/v3/@claude-flow/cli/dist/src/mods/plugin-repair.js +120 -0
  62. package/v3/@claude-flow/cli/dist/src/mods/plugin-resolve.d.ts +63 -0
  63. package/v3/@claude-flow/cli/dist/src/mods/plugin-resolve.js +182 -0
  64. package/v3/@claude-flow/cli/dist/src/mods/probe.d.ts +5 -0
  65. package/v3/@claude-flow/cli/dist/src/mods/probe.js +28 -3
  66. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +5 -0
  67. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +5 -1
  68. package/v3/@claude-flow/cli/package.json +1 -1
@@ -465,7 +465,8 @@ const handlers = {
465
465
  for (const d of dangerous) {
466
466
  if (cmd.includes(d)) {
467
467
  console.error(`[BLOCKED] Dangerous command detected: ${d}`);
468
- process.exit(1);
468
+ // Claude Code PreToolUse: exit 2 blocks execution; exit 1 is non-blocking.
469
+ process.exit(2);
469
470
  }
470
471
  }
471
472
  console.log('[OK] Command validated');
@@ -598,8 +599,8 @@ const handlers = {
598
599
  }
599
600
  }
600
601
 
601
- // Hooks must ALWAYS exit 0 — Claude Code treats non-zero as "hook error"
602
- // and skips all subsequent hooks for the event.
602
+ // Non-blocking hooks exit 0; pre-bash denials exit 2 to block PreToolUse.
603
+ // A denial exits immediately, before the normal completion path below.
603
604
  //
604
605
  // Only dispatch when run directly (node hook-handler.cjs <cmd>). When
605
606
  // require()'d by a test, expose the internals instead of reading stdin and
@@ -1,5 +1,5 @@
1
1
  {
2
- "adoptedAt": 1790898945808,
2
+ "adoptedAt": 1790956447417,
3
3
  "championId": "sha256:6141a8ea990c5063b77e090ae8f37f9c539d8aa8f58dcceb30f3a82f97e57319",
4
4
  "manifest": {
5
5
  "schema": "ruflo.proven-config/v1",
@@ -5,7 +5,8 @@
5
5
  "env": {
6
6
  "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1",
7
7
  "CLAUDE_FLOW_V3_ENABLED": "true",
8
- "CLAUDE_FLOW_HOOKS_ENABLED": "true"
8
+ "CLAUDE_FLOW_HOOKS_ENABLED": "true",
9
+ "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1"
9
10
  },
10
11
  "permissions": {
11
12
  "allow": [
@@ -280,5 +281,13 @@
280
281
  },
281
282
  "enabledMcpjsonServers": [
282
283
  "claude-flow"
283
- ]
284
+ ],
285
+ "enabledPlugins": {
286
+ "ruflo-mods@ruflo": true,
287
+ "ruflo-swarm@ruflo": true,
288
+ "ruflo-console@ruflo": true
289
+ },
290
+ "extraKnownMarketplaces": {
291
+ "ruflo": { "source": { "source": "github", "repo": "ruvnet/ruflo" } }
292
+ }
284
293
  }
@@ -225,6 +225,11 @@
225
225
  "name": "ruflo-ruos",
226
226
  "source": "./plugins/ruflo-ruos",
227
227
  "description": "Run ruflo swarm agents on your own ruOS cloud desktops via the tenant-authenticated fleet MCP or per-tenant SSH; optional, removable"
228
+ },
229
+ {
230
+ "name": "ruflo-console",
231
+ "source": "./plugins/ruflo-console",
232
+ "description": "Early access: ruflo's cockpit as a Claude Code mod (function hooks) and the one /ruflo command for every ruflo mod \u2014 overview with health alerts, swarm topology, claims flow, federation map, plugin health, learning pipeline, MetaHarness radar, memory, cost gauge, agent timeline, approvals and events; a command palette for confirm-gated actions through the ruflo CLI; auto-opens where it docks; no network by default"
228
233
  }
229
234
  ]
230
235
  }
package/README.md CHANGED
@@ -50,7 +50,9 @@ User --> Ruflo (CLI/MCP) --> Router --> Swarm --> Agents --> Memory --> LLM Prov
50
50
 
51
51
  ---
52
52
 
53
- ![Ruflo Plugins](./ruflo-plugins.gif)
53
+ ![The ruflo console inside Claude Code: swarm topology, claims, approvals, missions, events and more](docs/assets/ruflo-console-tour.gif)
54
+
55
+ <sub>The <code>/ruflo</code> console running in Claude Code — <code>npx ruflo init</code>, restart Claude Code, then <code>/ruflo</code>.</sub>
54
56
 
55
57
  ## Quick Start
56
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-flow",
3
- "version": "3.50.0",
3
+ "version": "3.51.1",
4
4
  "workspaces": [
5
5
  "v3/@claude-flow/codex",
6
6
  "v3/@claude-flow/mcp",
@@ -16,8 +16,6 @@
16
16
 
17
17
  # Ruflo
18
18
 
19
- [English](README.md) · [简体中文](README.zh-CN.md)
20
-
21
19
  **An agent meta-harness for Claude Code and Codex.**
22
20
 
23
21
  [![RuFlo Explained — build an AI team that plans, remembers, tests, and improves](docs/assets/ruflo-explained/ch14.jpg)](docs/ruflo-explained.md)
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generation": 7,
4
- "generatedAt": "2026-10-02T00:19:27.102Z",
5
- "gitSha": "27982983",
3
+ "generation": 6,
4
+ "generatedAt": "2026-09-17T21:28:47.530Z",
5
+ "gitSha": "702d1461",
6
6
  "catalog": {
7
- "agents": 176,
7
+ "agents": 167,
8
8
  "tools": 418,
9
9
  "skills": 34
10
10
  },
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `ruflo catalog` — ADR-406 P0/P1 command inventory and canonical catalog.
3
+ *
4
+ * Interim home: ADR-406 §14 places these under `ruflo mods catalog`, but
5
+ * `commands/mods.ts` belongs to the install-fix lane (PR #3612) until it
6
+ * merges. This file moves there as a follow-up; the operations stay the same.
7
+ *
8
+ * catalog generate [--check] inventory + engine probe + deterministic catalog
9
+ * catalog verify [--file] schema, duplicate ids/names, digest parity
10
+ * catalog engine engine capability report only
11
+ *
12
+ * Reads command files; writes only the catalog and engine report under
13
+ * src/mods/command-registry/. Never touches a command file.
14
+ */
15
+ import type { Command } from '../types.js';
16
+ export declare const catalogCommand: Command;
17
+ export default catalogCommand;
18
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `ruflo catalog` — ADR-406 P0/P1 command inventory and canonical catalog.
3
+ *
4
+ * Interim home: ADR-406 §14 places these under `ruflo mods catalog`, but
5
+ * `commands/mods.ts` belongs to the install-fix lane (PR #3612) until it
6
+ * merges. This file moves there as a follow-up; the operations stay the same.
7
+ *
8
+ * catalog generate [--check] inventory + engine probe + deterministic catalog
9
+ * catalog verify [--file] schema, duplicate ids/names, digest parity
10
+ * catalog engine engine capability report only
11
+ *
12
+ * Reads command files; writes only the catalog and engine report under
13
+ * src/mods/command-registry/. Never touches a command file.
14
+ */
15
+ import { existsSync, readFileSync } from 'node:fs';
16
+ import { homedir } from 'node:os';
17
+ import { join, resolve } from 'node:path';
18
+ import { output } from '../output.js';
19
+ import { CATALOG_RELATIVE_PATH, ENGINE_REPORT_RELATIVE_PATH, generate, serializeCatalog, summarize, verifyCatalog, writeCatalog, } from '../mods/command-registry/index.js';
20
+ import { canonicalJson, sha256Hex } from '../mods/command-registry/index.js';
21
+ import { writeFileSync } from 'node:fs';
22
+ function engineFrom(flags) {
23
+ const explicit = typeof flags.engine === 'string' ? flags.engine : undefined;
24
+ const candidate = explicit ?? join(homedir(), '.local', 'bin', 'claude');
25
+ return existsSync(candidate) ? candidate : null;
26
+ }
27
+ function finish(data, ok, json, text) {
28
+ if (json)
29
+ output.writeln(JSON.stringify(data, null, 2));
30
+ else
31
+ output.writeln(text);
32
+ return { success: ok, exitCode: ok ? 0 : 1, data };
33
+ }
34
+ export const catalogCommand = {
35
+ name: 'catalog',
36
+ description: 'ADR-406 command inventory and canonical command catalog (generate, verify, engine report)',
37
+ options: [
38
+ { name: 'repo-root', type: 'string', description: 'Repository root (default: cwd)' },
39
+ { name: 'engine', type: 'string', description: 'Claude Code executable to probe (default ~/.local/bin/claude)' },
40
+ { name: 'check', type: 'boolean', description: 'generate: compare with the committed catalog instead of writing' },
41
+ { name: 'file', type: 'string', description: 'verify: catalog file to verify' },
42
+ { name: 'json', type: 'boolean', description: 'Machine-readable output' },
43
+ ],
44
+ examples: [
45
+ { command: 'ruflo catalog generate', description: 'Regenerate the catalog and engine report' },
46
+ { command: 'ruflo catalog generate --check', description: 'Fail if the committed catalog is stale' },
47
+ { command: 'ruflo catalog verify --file catalog.generated.json', description: 'Verify a copied catalog (parity)' },
48
+ ],
49
+ async action(ctx) {
50
+ const flags = ctx.flags;
51
+ const op = ctx.args[0] ?? 'generate';
52
+ const json = flags.json === true;
53
+ const root = resolve(String(flags.repoRoot ?? ctx.cwd ?? process.cwd()));
54
+ try {
55
+ if (op === 'verify') {
56
+ const file = resolve(String(flags.file ?? join(root, CATALOG_RELATIVE_PATH)));
57
+ const result = verifyCatalog(JSON.parse(readFileSync(file, 'utf8')));
58
+ return finish({ file, ...result }, result.valid, json, result.valid ? `catalog valid: ${file}` : `catalog INVALID:\n${result.errors.join('\n')}`);
59
+ }
60
+ if (op !== 'generate' && op !== 'engine') {
61
+ return finish({ error: `unknown operation ${op}` }, false, json, `unknown operation: ${op} (generate | verify | engine)`);
62
+ }
63
+ const result = generate({ repoRoot: root, engineExecutable: engineFrom(flags) });
64
+ if (op === 'engine')
65
+ return finish(result.engine, true, json, JSON.stringify(result.engine, null, 2));
66
+ const catalogPath = join(root, CATALOG_RELATIVE_PATH);
67
+ const summary = summarize(result);
68
+ if (flags.check === true) {
69
+ const committed = existsSync(catalogPath) ? readFileSync(catalogPath, 'utf8') : '';
70
+ const fresh = serializeCatalog(result.catalog);
71
+ const same = committed === fresh;
72
+ return finish({ catalogPath, upToDate: same, summary }, same, json, same ? 'catalog up to date' : `catalog is stale: run \`ruflo catalog generate\` (${catalogPath})`);
73
+ }
74
+ const previous = existsSync(catalogPath) ? sha256Hex(readFileSync(catalogPath)) : null;
75
+ const write = writeCatalog(catalogPath, result.catalog, previous);
76
+ // The engine report is host evidence (engine build, binary digest), kept
77
+ // beside the catalog but outside its digest, which must stay host-free.
78
+ writeFileSync(join(root, ENGINE_REPORT_RELATIVE_PATH), `${JSON.stringify(JSON.parse(canonicalJson(result.engine)), null, 2)}\n`);
79
+ return finish({ catalogPath, written: write.written, sha256: write.sha256, summary }, true, json, `${write.written ? 'wrote' : 'unchanged'} ${catalogPath}\n${JSON.stringify(summary, null, 2)}`);
80
+ }
81
+ catch (error) {
82
+ const message = error instanceof Error ? error.message : String(error);
83
+ output.printError(message);
84
+ return { success: false, exitCode: 1, message };
85
+ }
86
+ },
87
+ };
88
+ export default catalogCommand;
89
+ //# sourceMappingURL=catalog.js.map
@@ -2308,15 +2308,20 @@ async function checkMetaharness() {
2308
2308
  }
2309
2309
  // ADR-404 — ruflo as a Claude Code mod (function hooks, early access). One
2310
2310
  // line in a bare `doctor`: whether the mod is enabled, can load (function
2311
- // hooks on, not refused by allowManagedModsOnly) and has started; never a
2312
- // failure, since the classic hooks are the default and the fallback.
2313
- // `ruflo mods doctor` prints every finding.
2311
+ // hooks on, not refused by allowManagedModsOnly) and has started. Only an
2312
+ // opted-in project whose plugin Claude Code cannot resolve (stale ruflo
2313
+ // marketplace clone, nothing installed) fails: the classic hooks still run,
2314
+ // but the opt-in silently does nothing. `ruflo mods doctor` prints every finding.
2314
2315
  async function checkMods() {
2315
2316
  const { probeMods } = await import('../mods/probe.js');
2316
2317
  const findings = probeMods({ projectRoot: process.cwd() });
2317
2318
  const enabled = findings.find((f) => f.name === 'ruflo-mods plugin')?.status === 'pass';
2318
2319
  if (!enabled)
2319
2320
  return { name: 'ruflo mods (ADR-404)', status: 'pass', message: 'not enabled; classic hooks handle every event' };
2321
+ const failures = findings.filter((f) => f.status === 'fail');
2322
+ if (failures.length > 0) {
2323
+ return { name: 'ruflo mods (ADR-404)', status: 'fail', message: failures.map((f) => `${f.name}: ${f.message}`).join('; '), fix: failures[0].fix ?? 'ruflo mods doctor' };
2324
+ }
2320
2325
  const warnings = findings.filter((f) => f.status !== 'pass');
2321
2326
  return warnings.length === 0
2322
2327
  ? { name: 'ruflo mods (ADR-404)', status: 'pass', message: findings.find((f) => f.name === 'last mod start')?.message ?? 'enabled' }
@@ -10,7 +10,7 @@ function printJsonOrTable(ctx, data, title) {
10
10
  }
11
11
  async function run(ctx, tool, args, title) {
12
12
  try {
13
- // CLI flag --gateway takes precedence over the RUFLO_X_GATEWAY_URL env var (ADR-125).
13
+ // Credential-bearing and gateway-read tools require --gateway to match trusted server configuration.
14
14
  const data = await callMCPTool(tool, { gatewayUrl: ctx.flags.gateway, ...args });
15
15
  printJsonOrTable(ctx, data, title);
16
16
  if (data && typeof data === 'object' && (('ok' in data && data.ok === false) || ('degraded' in data && data.degraded === true)))
@@ -27,7 +27,7 @@ export const federationCommand = {
27
27
  description: 'Open swarm federation via x.ruv.io — sync messages, roster, claims, registry, invites (Nostr, signed, membership-gated)',
28
28
  options: [
29
29
  { name: 'format', short: 'f', description: 'Output format (json|text)', type: 'string', default: 'text' },
30
- { name: 'gateway', description: 'Gateway base URL (takes precedence over RUFLO_X_GATEWAY_URL; default https://x.ruv.io)', type: 'string' },
30
+ { name: 'gateway', description: 'Gateway URL; reads/admin writes require it to match RUFLO_X_GATEWAY_URL (default https://x.ruv.io); join selects its registration origin', type: 'string' },
31
31
  ],
32
32
  subcommands: [
33
33
  { name: 'join', description: 'Join the open swarm with YOUR OWN key without an invite on open relays (generates ~/.ruflo/nostr.key if absent, registers via NIP-98, verifies via NIP-42)',
@@ -90,6 +90,9 @@ const commandLoaders = {
90
90
  spinner: () => import('./spinner.js'),
91
91
  // ruflo as a Claude Code mod: function hooks, early access (ADR-404)
92
92
  mods: () => import('./mods.js'),
93
+ // ADR-406 command catalog and mission control (interim homes until mods.ts follow-up)
94
+ catalog: () => import('./catalog.js'),
95
+ mission: () => import('./mission.js'),
93
96
  // Ruflo entries in Claude Code's companyAnnouncements startup rotation (ADR-319)
94
97
  announcements: () => import('./announcements.js'),
95
98
  // AGNTCY/Outshift runtime transport selection (ADR-324 §2) — optional,
@@ -775,17 +775,18 @@ const initClaudeAction = async (ctx) => {
775
775
  output.writeln(output.warning(' Embedding initialization skipped (run manually)'));
776
776
  }
777
777
  }
778
- // ADR-404 — opt into the Claude Code mod path. Additive: the classic
779
- // hooks just written stay the default and the fallback.
780
- if (ctx.flags.mods === true) {
778
+ // ADR-404 (amended: mods default-on in init) — enable the ruflo mod
779
+ // plugins in the committed .claude/settings.json, then install them with
780
+ // claude. Additive: the classic hooks just written stay the fallback.
781
+ // --no-mods opts out; --no-plugin-install writes settings only.
782
+ if (ctx.flags.mods !== false) {
781
783
  output.writeln();
782
784
  try {
783
- const { installMod } = await import('../mods/install.js');
784
- const installed = installMod(ctx.cwd, 'local');
785
- output.writeln(output.success(` ✓ ruflo-mods enabled in ${installed.settingsFile} (early access; run "ruflo mods doctor")`));
785
+ const { applyMods } = await import('../mods/apply.js');
786
+ await applyMods(ctx.cwd, { scope: 'project', pluginInstall: ctx.flags.pluginInstall !== false && ctx.flags['plugin-install'] !== false, skipInTestEnv: true });
786
787
  }
787
788
  catch (err) {
788
- output.writeln(output.warning(` ruflo-mods not enabled: ${err instanceof Error ? err.message : String(err)}`));
789
+ output.writeln(output.warning(` ruflo mods not enabled: ${err instanceof Error ? err.message : String(err)}`));
789
790
  }
790
791
  }
791
792
  if (!startDaemon && !startAll) {
@@ -1062,6 +1063,17 @@ const wizardCommand = {
1062
1063
  output.printInfo(` ... and ${result.warnings.length - 5} more`);
1063
1064
  }
1064
1065
  }
1066
+ // ADR-404 amendment — the wizard enables the mods as `ruflo init` does.
1067
+ if (ctx.flags.mods !== false) {
1068
+ output.writeln();
1069
+ try {
1070
+ const { applyMods } = await import('../mods/apply.js');
1071
+ await applyMods(ctx.cwd, { scope: 'project', pluginInstall: ctx.flags.pluginInstall !== false && ctx.flags['plugin-install'] !== false, skipInTestEnv: true });
1072
+ }
1073
+ catch (err) {
1074
+ output.writeln(output.warning(` ruflo mods not enabled: ${err instanceof Error ? err.message : String(err)}`));
1075
+ }
1076
+ }
1065
1077
  // Initialize embeddings if enabled
1066
1078
  let embeddingsInitialized = false;
1067
1079
  if (enableEmbeddings) {
@@ -1322,14 +1334,27 @@ const upgradeCommand = {
1322
1334
  {
1323
1335
  name: 'settings',
1324
1336
  short: 's',
1325
- description: 'Merge new settings (Agent Teams, hooks) into existing settings.json',
1337
+ description: 'Merge new settings (Agent Teams, hooks, ruflo mods) into existing settings.json',
1326
1338
  type: 'boolean',
1327
1339
  default: false,
1328
1340
  },
1341
+ {
1342
+ // ADR-404 amendment — the same merge + plugin install as `ruflo init`.
1343
+ name: 'mods',
1344
+ description: 'Merge the ruflo mod plugins into .claude/settings.json and install them (also implied by --add-missing and --settings; --no-mods to skip)',
1345
+ type: 'boolean',
1346
+ },
1347
+ {
1348
+ name: 'plugin-install',
1349
+ description: 'With mods: run claude plugin install (--no-plugin-install to merge settings only)',
1350
+ type: 'boolean',
1351
+ default: true,
1352
+ },
1329
1353
  ],
1330
1354
  action: async (ctx) => {
1331
1355
  const addMissing = (ctx.flags['add-missing'] || ctx.flags.addMissing);
1332
1356
  const upgradeSettings = (ctx.flags.settings);
1357
+ const upgradeMods = ctx.flags.mods === true || (ctx.flags.mods !== false && (addMissing || upgradeSettings));
1333
1358
  output.writeln();
1334
1359
  output.writeln(output.bold('Upgrading RuFlo'));
1335
1360
  if (addMissing && upgradeSettings) {
@@ -1363,6 +1388,14 @@ const upgradeCommand = {
1363
1388
  }
1364
1389
  spinner.succeed('Upgrade complete!');
1365
1390
  output.writeln();
1391
+ // ADR-404 amendment — merge the mod plugins without clobbering: only
1392
+ // keys that are absent are added, and each is reported.
1393
+ if (upgradeMods) {
1394
+ const { applyMods, describeAdded } = await import('../mods/apply.js');
1395
+ const mods = await applyMods(ctx.cwd, { scope: 'project', pluginInstall: ctx.flags.pluginInstall !== false && ctx.flags['plugin-install'] !== false, skipInTestEnv: true });
1396
+ result.settingsUpdated = [...(result.settingsUpdated ?? []), ...describeAdded(mods.install.added)];
1397
+ output.writeln();
1398
+ }
1366
1399
  // Show what was updated
1367
1400
  if (result.updated.length > 0) {
1368
1401
  output.printBox(result.updated.map(f => `✓ ${f}`).join('\n'), 'Updated (latest version)');
@@ -1514,11 +1547,17 @@ export const initCommand = {
1514
1547
  default: false,
1515
1548
  },
1516
1549
  {
1517
- // ADR-404 — Claude Code function hooks are early access; opt-in only.
1550
+ // ADR-404 amendment — on by default; nothing loads where function hooks are off.
1518
1551
  name: 'mods',
1519
- description: 'Also enable the ruflo Claude Code mod (function hooks, early access); classic hooks stay as fallback',
1552
+ description: 'Enable the ruflo mod plugins in .claude/settings.json (default on; --no-mods to skip); classic hooks stay as fallback',
1520
1553
  type: 'boolean',
1521
- default: false,
1554
+ default: true,
1555
+ },
1556
+ {
1557
+ name: 'plugin-install',
1558
+ description: 'With mods: refresh the ruflo marketplace and claude plugin install the mods (--no-plugin-install to write settings only)',
1559
+ type: 'boolean',
1560
+ default: true,
1522
1561
  },
1523
1562
  {
1524
1563
  name: 'with-embeddings',
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `ruflo mission` — ADR-406 §19.7 mission operations from the CLI.
3
+ *
4
+ * mission create --objective <text> [--request-id <id>]
5
+ * mission plan --mission <id> --expected-revision <n> --plan-file <json> [--request-id <id>]
6
+ * mission get [--mission <id>]
7
+ * mission events --mission <id> [--after <seq>] [--limit <n>]
8
+ * mission action --mission <id> --expected-revision <n> --action <requestAuthorization|pause|cancel|admit|resume> [--reason <text>]
9
+ *
10
+ * Flags map one-to-one onto the raw input the MCP tools receive; the shared
11
+ * `MissionService` validates both. Output is JSON. A missing request id gets a
12
+ * fresh one: a new id is a new intentional request (ADR-406 §9); pass the same
13
+ * id to retry the same request safely.
14
+ */
15
+ import type { Command } from '../types.js';
16
+ import type { MissionPort } from '../mcp-tools/mission-tools.js';
17
+ export type MissionPortFactoryCli = (projectRoot: string) => MissionPort;
18
+ /** Test seam (London school): substitute the service the command talks to. */
19
+ export declare function setMissionPortFactory(next: MissionPortFactoryCli | null): void;
20
+ /** Raw operation input from flags; undefined fields are dropped so defaults apply. */
21
+ export declare function inputFromFlags(op: string, flags: Record<string, unknown>, cwd: string): Record<string, unknown>;
22
+ export declare const missionCommand: Command;
23
+ export default missionCommand;
24
+ //# sourceMappingURL=mission.d.ts.map
@@ -0,0 +1,116 @@
1
+ /**
2
+ * `ruflo mission` — ADR-406 §19.7 mission operations from the CLI.
3
+ *
4
+ * mission create --objective <text> [--request-id <id>]
5
+ * mission plan --mission <id> --expected-revision <n> --plan-file <json> [--request-id <id>]
6
+ * mission get [--mission <id>]
7
+ * mission events --mission <id> [--after <seq>] [--limit <n>]
8
+ * mission action --mission <id> --expected-revision <n> --action <requestAuthorization|pause|cancel|admit|resume> [--reason <text>]
9
+ *
10
+ * Flags map one-to-one onto the raw input the MCP tools receive; the shared
11
+ * `MissionService` validates both. Output is JSON. A missing request id gets a
12
+ * fresh one: a new id is a new intentional request (ADR-406 §9); pass the same
13
+ * id to retry the same request safely.
14
+ */
15
+ import { randomUUID } from 'node:crypto';
16
+ import { lstatSync, readFileSync } from 'node:fs';
17
+ import { resolve } from 'node:path';
18
+ import { output } from '../output.js';
19
+ import { MissionService } from '../missions/service.js';
20
+ const PLAN_FILE_MAX_BYTES = 256 * 1024;
21
+ let factory = (projectRoot) => new MissionService({ projectRoot, channel: 'cli' });
22
+ /** Test seam (London school): substitute the service the command talks to. */
23
+ export function setMissionPortFactory(next) {
24
+ factory = next ?? ((projectRoot) => new MissionService({ projectRoot, channel: 'cli' }));
25
+ }
26
+ function int(value) {
27
+ if (value === undefined || value === null || value === '')
28
+ return undefined;
29
+ const n = Number(value);
30
+ return Number.isFinite(n) ? n : Number.NaN;
31
+ }
32
+ function readPlanFile(path) {
33
+ const stat = lstatSync(path);
34
+ if (!stat.isFile())
35
+ throw new Error('plan-file must be a regular file');
36
+ if (stat.size > PLAN_FILE_MAX_BYTES)
37
+ throw new Error(`plan-file exceeds ${PLAN_FILE_MAX_BYTES} bytes`);
38
+ return JSON.parse(readFileSync(path, 'utf8'));
39
+ }
40
+ /** Raw operation input from flags; undefined fields are dropped so defaults apply. */
41
+ export function inputFromFlags(op, flags, cwd) {
42
+ const raw = {};
43
+ const set = (key, value) => { if (value !== undefined)
44
+ raw[key] = value; };
45
+ const requestId = typeof flags.requestId === 'string' ? flags.requestId : randomUUID();
46
+ if (op === 'create') {
47
+ set('requestId', requestId);
48
+ set('objective', flags.objective);
49
+ }
50
+ else if (op === 'plan') {
51
+ set('requestId', requestId);
52
+ set('missionId', flags.mission);
53
+ set('expectedRevision', int(flags.expectedRevision));
54
+ if (typeof flags.planFile === 'string')
55
+ set('plan', readPlanFile(resolve(cwd, flags.planFile)));
56
+ }
57
+ else if (op === 'get') {
58
+ set('missionId', flags.mission);
59
+ }
60
+ else if (op === 'events') {
61
+ set('missionId', flags.mission);
62
+ set('afterSequence', int(flags.after));
63
+ set('limit', int(flags.limit));
64
+ }
65
+ else if (op === 'action') {
66
+ set('requestId', requestId);
67
+ set('missionId', flags.mission);
68
+ set('expectedRevision', int(flags.expectedRevision));
69
+ set('action', flags.action);
70
+ set('reason', flags.reason);
71
+ }
72
+ return raw;
73
+ }
74
+ export const missionCommand = {
75
+ name: 'mission',
76
+ description: 'ADR-406 mission control: create, plan, get, events, action (shared runtime with the mission_* MCP tools)',
77
+ options: [
78
+ { name: 'objective', type: 'string', description: 'create: mission objective' },
79
+ { name: 'mission', type: 'string', description: 'Mission id (msn_…)' },
80
+ { name: 'request-id', type: 'string', description: 'Idempotency key; reuse only to retry the same request' },
81
+ { name: 'expected-revision', type: 'number', description: 'Revision the request was prepared against' },
82
+ { name: 'plan-file', type: 'string', description: 'plan: JSON file with { tasks, acceptance, budget, scope }' },
83
+ { name: 'after', type: 'number', description: 'events: cursor (last sequence seen)' },
84
+ { name: 'limit', type: 'number', description: 'events: page size (max 500)' },
85
+ { name: 'action', type: 'string', description: 'action: requestAuthorization | pause | cancel | admit | resume' },
86
+ { name: 'reason', type: 'string', description: 'action: reason' },
87
+ { name: 'project-root', type: 'string', description: 'Project root (default: cwd)' },
88
+ ],
89
+ examples: [
90
+ { command: 'ruflo mission create --objective "Ship the report" --request-id r1', description: 'Create a draft mission' },
91
+ { command: 'ruflo mission plan --mission msn_… --expected-revision 1 --plan-file plan.json', description: 'Submit a plan' },
92
+ { command: 'ruflo mission events --mission msn_… --after 0', description: 'Read events from a cursor' },
93
+ ],
94
+ async action(ctx) {
95
+ const flags = ctx.flags;
96
+ const op = ctx.args[0] ?? 'get';
97
+ const root = resolve(String(flags.projectRoot ?? ctx.cwd ?? process.cwd()));
98
+ const ops = { create: 'create', plan: 'plan', get: 'get', events: 'events', action: 'requestAction' };
99
+ const method = ops[op];
100
+ if (!method) {
101
+ output.printError(`unknown mission operation: ${op} (create | plan | get | events | action)`);
102
+ return { success: false, exitCode: 1 };
103
+ }
104
+ let result;
105
+ try {
106
+ result = await factory(root)[method](inputFromFlags(op, flags, root));
107
+ }
108
+ catch (error) {
109
+ result = { ok: false, code: 'invalid-input', message: error instanceof Error ? error.message : String(error) };
110
+ }
111
+ output.writeln(JSON.stringify(result, null, 2));
112
+ return { success: result.ok, exitCode: result.ok ? 0 : result.code === 'revision-conflict' ? 3 : 1, data: result };
113
+ },
114
+ };
115
+ export default missionCommand;
116
+ //# sourceMappingURL=mission.js.map
@@ -2,7 +2,7 @@
2
2
  * `ruflo mods` — opt into running ruflo as a Claude Code mod (ADR-404,
3
3
  * Claude Code function hooks, early access).
4
4
  *
5
- * install/uninstall edit one settings file and record what they added;
5
+ * install/uninstall edit settings files and record what they added;
6
6
  * status/doctor report what can be known from outside a session; sync-policy
7
7
  * rewrites the policy projection the mod's tool check reads. Classic hooks
8
8
  * are never removed: they stay the default and the fallback.