claude-flow 3.51.1 → 3.52.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.
@@ -388,13 +388,14 @@ const handlers = {
388
388
  if (router && router.routeTask) {
389
389
  const result = router.routeTask(prompt);
390
390
  // Format output for Claude Code hook consumption — real data only
391
+ const row = (text) => `| ${text.substring(0, 60).padEnd(60)} |`;
391
392
  const output = [
392
393
  `[INFO] Routing task: ${prompt.substring(0, 80) || '(no prompt)'}`,
393
394
  '',
394
395
  '+------------------- Primary Recommendation -------------------+',
395
- `| Agent: ${result.agent.padEnd(53)}|`,
396
- `| Confidence: ${(result.confidence * 100).toFixed(1)}%${' '.repeat(44)}|`,
397
- `| Reason: ${(result.reason || '').substring(0, 53).padEnd(53)}|`,
396
+ row(`Agent: ${result.agent}`),
397
+ row(`Confidence: ${(result.confidence * 100).toFixed(1)}%`),
398
+ row(`Reason: ${result.reason || ''}`),
398
399
  '+--------------------------------------------------------------+',
399
400
  ];
400
401
  console.log(output.join('\n'));
@@ -462,8 +463,77 @@ const handlers = {
462
463
  // (silently exiting 0 and letting the dangerous command through).
463
464
  const cmd = String(hookInput.command || toolInput.command || prompt || '').toLowerCase();
464
465
  const dangerous = ['rm -rf /', 'format c:', 'del /s /q c:\\', ':(){:|:&};:'];
466
+ // Join literal quote fragments and normalize root paths without shell evaluation.
467
+ function hasRootDelete(command, depth = 0) {
468
+ let word = '', quote = '', started = false, redirect = false
469
+ let inRm = false, optionsEnded = false, recursive = false, force = false, root = false
470
+ const isRoot = (operand) => {
471
+ if (!operand.startsWith('/')) return false
472
+ const parts = []
473
+ for (const part of operand.split('/')) {
474
+ if (!part || part === '.') continue
475
+ if (part === '..') parts.pop()
476
+ else parts.push(part)
477
+ }
478
+ return parts.length === 0 || /[*?\[]/.test(parts[0])
479
+ }
480
+ const finishWord = () => {
481
+ if (!started) return false
482
+ // Literal shell strings (e.g. sh -c 'rm -rf /') also carried the old guard.
483
+ // Bound rescanning to four levels; beyond that retain its conservative check.
484
+ if (word.includes('rm') && /[\s;&|()]/.test(word)) {
485
+ if (depth < 4 ? hasRootDelete(word, depth + 1) : word.includes('rm -rf /')) return true
486
+ }
487
+ if (!inRm) inRm = word === 'rm' || word.endsWith('/rm')
488
+ else if (!optionsEnded && word === '--') optionsEnded = true
489
+ else if (!optionsEnded && word.startsWith('-')) {
490
+ recursive = recursive || word === '--recursive' || /^-[a-z]*r[a-z]*$/.test(word)
491
+ force = force || word === '--force' || /^-[a-z]*f[a-z]*$/.test(word)
492
+ } else root = root || isRoot(word)
493
+ word = ''; started = false
494
+ return inRm && recursive && force && root
495
+ }
496
+ const finishCommand = () => {
497
+ const denied = inRm && recursive && force && root
498
+ inRm = optionsEnded = recursive = force = root = false
499
+ return denied
500
+ }
501
+ for (let i = 0; i < command.length; i++) {
502
+ const char = command[i]
503
+ const redirectionAmpersand = char === '&' && (redirect || command[i + 1] === '>')
504
+ redirect = false
505
+ if (quote) {
506
+ if (char === quote) quote = ''
507
+ else if (quote === '"' && char === '\\' && i + 1 < command.length &&
508
+ (command[i + 1] === '"' || command[i + 1] === '\\' || command[i + 1] === '$' ||
509
+ command.charCodeAt(i + 1) === 96 || command[i + 1] === '\n')) {
510
+ const next = command[++i]
511
+ if (next !== '\n') word += next
512
+ } else word += char
513
+ continue
514
+ }
515
+ if (char === '\\' && i + 1 < command.length) {
516
+ const next = command[++i]
517
+ if (next !== '\n') { word += next; started = true }
518
+ } else if (char === '"' || char === "'") {
519
+ quote = char; started = true
520
+ } else if (char === '#' && !started) {
521
+ while (i < command.length && command[i] !== '\n') i++
522
+ if (finishCommand()) return true
523
+ } else if (char === ' ' || char === '\t' || char === '\r' || char === '\n' ||
524
+ ';|&()<>'.includes(char)) {
525
+ if (finishWord()) return true
526
+ // Redirections separate words, but later operands still belong to rm.
527
+ redirect = char === '<' || char === '>'
528
+ if (!redirectionAmpersand && (char === '\n' || ';|&()'.includes(char)) && finishCommand()) return true
529
+ } else {
530
+ word += char; started = true
531
+ }
532
+ }
533
+ return finishWord() || finishCommand()
534
+ }
465
535
  for (const d of dangerous) {
466
- if (cmd.includes(d)) {
536
+ if (d === 'rm -rf /' ? hasRootDelete(cmd) : cmd.includes(d)) {
467
537
  console.error(`[BLOCKED] Dangerous command detected: ${d}`);
468
538
  // Claude Code PreToolUse: exit 2 blocks execution; exit 1 is non-blocking.
469
539
  process.exit(2);
@@ -1,5 +1,5 @@
1
1
  {
2
- "adoptedAt": 1790956447417,
2
+ "adoptedAt": 1791210220767,
3
3
  "championId": "sha256:6141a8ea990c5063b77e090ae8f37f9c539d8aa8f58dcceb30f3a82f97e57319",
4
4
  "manifest": {
5
5
  "schema": "ruflo.proven-config/v1",
package/README.md CHANGED
@@ -50,9 +50,26 @@ User --> Ruflo (CLI/MCP) --> Router --> Swarm --> Agents --> Memory --> LLM Prov
50
50
 
51
51
  ---
52
52
 
53
- ![The ruflo console inside Claude Code: swarm topology, claims, approvals, missions, events and more](docs/assets/ruflo-console-tour.gif)
53
+ <p align="center"><img src="docs/assets/ruflo-console-walkthrough.gif" alt="A walkthrough of the ruflo console inside Claude Code in a near-square frame: the whole boot (the neon sign in its border, the ruvector constellation, every area checked), the main menu entering and its cards folding, then twenty-odd pages one after another (Swarm, Federation, Sandbox, Cost, Memory, Dev Tools and more), the command palette, ruHelp, settings and refresh"></p>
54
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>
55
+ <sub>The <code>/ruflo</code> console running in Claude Code, shown in a compact, near-square frame (the cockpit docked beside Claude, cropped to the cockpit) so it reads on a phone as well as a desktop: the whole boot (the neon sign in its border, the ruvector constellation lit by what is really installed, every area checked, an easter egg), the main menu entering and its cards folding, the grouped nav, a quick tour of twenty-odd pages (the Swarm topology, the Federation map, the Sandbox page for tmux, RVF and RVM, Cost, Memory, Dev Tools and more), the command palette (a tag on every entry for what it does), <b>ruHelp</b> (built-in help: ask a question, get steps with buttons; a guide for every capability), settings and refresh. A <a href="docs/assets/ruflo-console-walkthrough-wide.gif">wide-display walkthrough</a> shows the same console with room for every label. <code>npx ruflo init</code>, restart Claude Code, then <code>/ruflo</code>.</sub>
56
+
57
+ **Let Claude drive the console.** The console can also be driven *by* Claude: with control turned on (Settings → Claude control), Claude gets four tools (`console_state`, `console_open`, `console_set`, `console_run`) and sets up a mission, opens the Learning Lab or Security page, or runs a palette entry, while the cockpit shows it happening. How far it may go is your setting (`read`, `write`, `manage`, `full`: an action above the level is refused and nothing runs) and whether it waits for your Yes (`ask`) or confirms itself (`auto`). Overview shows a live log of every action and a **Take back control** button that stops all of it at once. It is off by default. See [ADR-444](v3/docs/adr/ADR-444-claude-controls-the-console.md).
58
+
59
+ <p align="center"><img src="docs/assets/ruflo-console-claude-control.gif" alt="Claude Code with the ruflo console beside it: Claude sets a mission goal, creates the mission and opens the Learning, Security and Overview pages through the console tools; the Claude control dashboard logs each action; swarm-stop is refused because it needs the full level; the person clicks Take back control and the next request is refused"></p>
60
+
61
+ <sub>A real recording (Claude Haiku, a few cents): control set to <code>write</code> with auto-confirm. Setting a goal and creating the mission is within that level; stopping a swarm needs <code>full</code>, so it is refused; after <b>Take back control</b> every call is refused.</sub>
62
+
63
+ **Install the mods from the ruflo marketplace** (Claude Code 2.1.287 or later; mods run with your account's permissions and are not sandboxed, so read the code first):
64
+
65
+ ```text
66
+ /plugin marketplace add ruvnet/ruflo
67
+ /plugin install ruflo-console@ruflo
68
+ /plugin install ruflo-mods@ruflo
69
+ /reload-plugins
70
+ ```
71
+
72
+ `ruflo-console` is the cockpit above, `ruflo-mods` routes prompts and enforces policy in-process, `ruflo-swarm` shows the swarm in a pane, and `ruflo-ruos` adds the ruOS status segment. Open `/plugin` to confirm they appear in the active mods line. Inside the console, `/ruflo market` is the Plugin Catalog: every ruflo plugin, mod and skill with what it ships, and buttons to install, enable, disable and update (each asks first).
56
73
 
57
74
  ## Quick Start
58
75
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-flow",
3
- "version": "3.51.1",
3
+ "version": "3.52.0",
4
4
  "workspaces": [
5
5
  "v3/@claude-flow/codex",
6
6
  "v3/@claude-flow/mcp",
@@ -16,6 +16,8 @@
16
16
 
17
17
  # Ruflo
18
18
 
19
+ [English](README.md) · [简体中文](README.zh-CN.md)
20
+
19
21
  **An agent meta-harness for Claude Code and Codex.**
20
22
 
21
23
  [![RuFlo Explained — build an AI team that plans, remembers, tests, and improves](docs/assets/ruflo-explained/ch14.jpg)](docs/ruflo-explained.md)
@@ -48,7 +50,26 @@ User --> Ruflo (CLI/MCP) --> Router --> Swarm --> Agents --> Memory --> LLM Prov
48
50
 
49
51
  ---
50
52
 
51
- ![Ruflo Plugins](./ruflo-plugins.gif)
53
+ <p align="center"><img src="docs/assets/ruflo-console-walkthrough.gif" alt="A walkthrough of the ruflo console inside Claude Code in a near-square frame: the whole boot (the neon sign in its border, the ruvector constellation, every area checked), the main menu entering and its cards folding, then twenty-odd pages one after another (Swarm, Federation, Sandbox, Cost, Memory, Dev Tools and more), the command palette, ruHelp, settings and refresh"></p>
54
+
55
+ <sub>The <code>/ruflo</code> console running in Claude Code, shown in a compact, near-square frame (the cockpit docked beside Claude, cropped to the cockpit) so it reads on a phone as well as a desktop: the whole boot (the neon sign in its border, the ruvector constellation lit by what is really installed, every area checked, an easter egg), the main menu entering and its cards folding, the grouped nav, a quick tour of twenty-odd pages (the Swarm topology, the Federation map, the Sandbox page for tmux, RVF and RVM, Cost, Memory, Dev Tools and more), the command palette (a tag on every entry for what it does), <b>ruHelp</b> (built-in help: ask a question, get steps with buttons; a guide for every capability), settings and refresh. A <a href="docs/assets/ruflo-console-walkthrough-wide.gif">wide-display walkthrough</a> shows the same console with room for every label. <code>npx ruflo init</code>, restart Claude Code, then <code>/ruflo</code>.</sub>
56
+
57
+ **Let Claude drive the console.** The console can also be driven *by* Claude: with control turned on (Settings → Claude control), Claude gets four tools (`console_state`, `console_open`, `console_set`, `console_run`) and sets up a mission, opens the Learning Lab or Security page, or runs a palette entry, while the cockpit shows it happening. How far it may go is your setting (`read`, `write`, `manage`, `full`: an action above the level is refused and nothing runs) and whether it waits for your Yes (`ask`) or confirms itself (`auto`). Overview shows a live log of every action and a **Take back control** button that stops all of it at once. It is off by default. See [ADR-444](v3/docs/adr/ADR-444-claude-controls-the-console.md).
58
+
59
+ <p align="center"><img src="docs/assets/ruflo-console-claude-control.gif" alt="Claude Code with the ruflo console beside it: Claude sets a mission goal, creates the mission and opens the Learning, Security and Overview pages through the console tools; the Claude control dashboard logs each action; swarm-stop is refused because it needs the full level; the person clicks Take back control and the next request is refused"></p>
60
+
61
+ <sub>A real recording (Claude Haiku, a few cents): control set to <code>write</code> with auto-confirm. Setting a goal and creating the mission is within that level; stopping a swarm needs <code>full</code>, so it is refused; after <b>Take back control</b> every call is refused.</sub>
62
+
63
+ **Install the mods from the ruflo marketplace** (Claude Code 2.1.287 or later; mods run with your account's permissions and are not sandboxed, so read the code first):
64
+
65
+ ```text
66
+ /plugin marketplace add ruvnet/ruflo
67
+ /plugin install ruflo-console@ruflo
68
+ /plugin install ruflo-mods@ruflo
69
+ /reload-plugins
70
+ ```
71
+
72
+ `ruflo-console` is the cockpit above, `ruflo-mods` routes prompts and enforces policy in-process, `ruflo-swarm` shows the swarm in a pane, and `ruflo-ruos` adds the ruOS status segment. Open `/plugin` to confirm they appear in the active mods line. Inside the console, `/ruflo market` is the Plugin Catalog: every ruflo plugin, mod and skill with what it ships, and buttons to install, enable, disable and update (each asks first).
52
73
 
53
74
  ## Quick Start
54
75
 
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generation": 6,
4
- "generatedAt": "2026-09-17T21:28:47.530Z",
5
- "gitSha": "702d1461",
3
+ "generation": 7,
4
+ "generatedAt": "2026-10-05T14:24:23.258Z",
5
+ "gitSha": "0f327015",
6
6
  "catalog": {
7
- "agents": 167,
8
- "tools": 418,
7
+ "agents": 176,
8
+ "tools": 423,
9
9
  "skills": 34
10
10
  },
11
11
  "benchmark": null
@@ -12,6 +12,8 @@ const compileCommand = {
12
12
  { name: 'root', short: 'r', type: 'string', description: 'Root guidance file path', default: './CLAUDE.md' },
13
13
  { name: 'local', short: 'l', type: 'string', description: 'Local guidance overlay file path' },
14
14
  { name: 'output', short: 'o', type: 'string', description: 'Output directory for compiled bundle' },
15
+ { name: 'mod-projection', type: 'boolean', description: 'Export bounded advisory guidance for the native mod', default: 'false' },
16
+ { name: 'revision', type: 'string', description: 'Immutable source commit SHA for the mod projection' },
15
17
  { name: 'json', type: 'boolean', description: 'Output as JSON', default: 'false' },
16
18
  ],
17
19
  examples: [
@@ -32,14 +34,29 @@ const compileCommand = {
32
34
  output.writeln(output.error(`Root guidance file not found: ${rootPath}`));
33
35
  return { success: false, message: `File not found: ${rootPath}` };
34
36
  }
35
- const rootContent = await readFile(rootPath, 'utf-8');
37
+ const modExport = ctx.flags['mod-projection'] === true;
38
+ const checked = modExport ? await (await import('../guidance/mod-sources.js')).readModGuidanceSources({
39
+ rootPath, localPath, revision: String(ctx.flags.revision || ''),
40
+ }) : undefined;
41
+ const rootContent = checked ? checked.rootContent : await readFile(rootPath, 'utf-8');
36
42
  let localContent;
37
- if (localPath && existsSync(localPath)) {
43
+ if (checked)
44
+ localContent = checked.localContent;
45
+ else if (localPath && existsSync(localPath)) {
38
46
  localContent = await readFile(localPath, 'utf-8');
39
47
  }
40
48
  const { GuidanceCompiler } = await import('@claude-flow/guidance/compiler');
41
49
  const compiler = new GuidanceCompiler();
42
50
  const bundle = compiler.compile(rootContent, localContent);
51
+ if (checked) {
52
+ const { buildModProjection, writeModProjection, MOD_GUIDANCE_DIR } = await import('../guidance/mod-projection.js');
53
+ const sourceDigests = Object.fromEntries(Object.entries(checked.sources).map(([key, binding]) => [key, binding.sha256]));
54
+ const projection = buildModProjection(bundle, checked.sourceRevision, sourceDigests);
55
+ const path = await writeModProjection(String(ctx.flags.output || MOD_GUIDANCE_DIR), projection);
56
+ const data = { path, projection, sources: checked.sources };
57
+ output.writeln(jsonOutput ? JSON.stringify(data) : output.success(`Git source matched; advisory mod projection exported: ${path}`));
58
+ return { success: true, data };
59
+ }
43
60
  if (jsonOutput) {
44
61
  output.writeln(JSON.stringify(bundle, null, 2));
45
62
  }
@@ -513,6 +530,28 @@ const abTestCommand = {
513
530
  }
514
531
  },
515
532
  };
533
+ const modCandidatesCommand = {
534
+ name: 'mod-candidates',
535
+ description: 'Review unverified native mod observations without training or promotion',
536
+ options: [
537
+ { name: 'directory', type: 'string', description: 'Observation directory', default: '.claude-flow/mods/guidance/observations' },
538
+ { name: 'bundle-id', type: 'string', description: 'Filter to one exported guidance bundle digest' },
539
+ { name: 'json', type: 'boolean', description: 'Output candidate report as JSON', default: 'false' },
540
+ ],
541
+ action: async (ctx) => {
542
+ try {
543
+ const { collectModCandidates } = await import('../guidance/mod-projection.js');
544
+ const data = await collectModCandidates(String(ctx.flags.directory || '.claude-flow/mods/guidance/observations'), ctx.flags['bundle-id']);
545
+ output.writeln(JSON.stringify(data, null, ctx.flags.json === true ? undefined : 2));
546
+ return { success: true, data };
547
+ }
548
+ catch (error) {
549
+ const message = error instanceof Error ? error.message : String(error);
550
+ output.writeln(output.error(message));
551
+ return { success: false, message };
552
+ }
553
+ },
554
+ };
516
555
  // Main guidance command
517
556
  export const guidanceCommand = {
518
557
  name: 'guidance',
@@ -520,6 +559,7 @@ export const guidanceCommand = {
520
559
  aliases: ['guide', 'policy'],
521
560
  subcommands: [
522
561
  compileCommand,
562
+ modCandidatesCommand,
523
563
  retrieveCommand,
524
564
  gatesCommand,
525
565
  statusCommand,
@@ -542,6 +582,7 @@ export const guidanceCommand = {
542
582
  output.writeln();
543
583
  output.writeln('Available subcommands:');
544
584
  output.writeln(` ${output.bold('compile')} Compile CLAUDE.md into policy bundle`);
585
+ output.writeln(` ${output.bold('mod-candidates')} Review unverified mod observations`);
545
586
  output.writeln(` ${output.bold('retrieve')} Retrieve task-relevant guidance shards`);
546
587
  output.writeln(` ${output.bold('gates')} Evaluate enforcement gates`);
547
588
  output.writeln(` ${output.bold('status')} Show control plane status`);
@@ -0,0 +1,120 @@
1
+ interface Rule {
2
+ id: string;
3
+ text: string;
4
+ source: string;
5
+ isConstitution: boolean;
6
+ intents: string[];
7
+ priority: number;
8
+ }
9
+ interface Bundle {
10
+ constitution: {
11
+ rules: Rule[];
12
+ hash: string;
13
+ };
14
+ shards: {
15
+ rule: Rule;
16
+ }[];
17
+ manifest: {
18
+ sourceHashes: Record<string, string>;
19
+ };
20
+ }
21
+ export declare const MOD_GUIDANCE_DIR = ".claude-flow/mods/guidance";
22
+ export interface ModObservation {
23
+ version: 1;
24
+ kind: 'guidance-observation';
25
+ id: string;
26
+ runId: string;
27
+ taskId: number;
28
+ bundleId: string;
29
+ sourceRevision: string;
30
+ ruleIds: string[];
31
+ checks: {
32
+ allow: number;
33
+ ask: number;
34
+ deny: number;
35
+ };
36
+ tools: {
37
+ ok: number;
38
+ error: number;
39
+ denied: number;
40
+ };
41
+ completion: 'completed' | 'aborted' | 'interrupted';
42
+ verified: false;
43
+ learningEligible: false;
44
+ }
45
+ /** Stable digest of the exported rules; a provenance identifier, not a signature. */
46
+ export declare function buildModProjection(bundle: Bundle, sourceRevision: string, sourceDigests?: Record<string, string>): {
47
+ bundleId: string;
48
+ entries: {
49
+ id: string;
50
+ text: string;
51
+ source: string;
52
+ constitution: boolean;
53
+ intents: string[];
54
+ priority: number;
55
+ }[];
56
+ sourceDigests?: {
57
+ [k: string]: string;
58
+ } | undefined;
59
+ version: 1;
60
+ sourceRevision: string;
61
+ constitutionHash: string;
62
+ sourceHashes: {
63
+ [k: string]: string;
64
+ };
65
+ };
66
+ /** Explicit CLI export only. Never called from the native prompt/tool hooks. */
67
+ export declare function writeModProjection(directory: string, projection: ReturnType<typeof buildModProjection>): Promise<string>;
68
+ /** Strict allowlist: a writable `verified: true` or receipt cannot become evidence. */
69
+ export declare function parseModObservations(text: string): ModObservation[];
70
+ /** Review priorities only. Never feeds the accepted RunEvent ledger or optimizer. */
71
+ export declare function collectModCandidates(directory: string, bundleId?: string): Promise<{
72
+ version: number;
73
+ status: string;
74
+ verified: boolean;
75
+ learningEligible: boolean;
76
+ observations: number;
77
+ excluded: number;
78
+ summary: {
79
+ files: number;
80
+ withGuidance: number;
81
+ withoutGuidance: number;
82
+ completed: number;
83
+ aborted: number;
84
+ interrupted: number;
85
+ checks: {
86
+ allow: number;
87
+ ask: number;
88
+ deny: number;
89
+ };
90
+ tools: {
91
+ ok: number;
92
+ error: number;
93
+ denied: number;
94
+ };
95
+ };
96
+ candidates: {
97
+ verified: boolean;
98
+ learningEligible: boolean;
99
+ toolErrorRate: number | null;
100
+ deniedCheckRate: number | null;
101
+ reviewReasons: string[];
102
+ bundleId: string;
103
+ sourceRevision: string;
104
+ ruleId: string;
105
+ observations: number;
106
+ affectedObservations: number;
107
+ toolErrors: number;
108
+ toolExecutions: number;
109
+ deniedTools: number;
110
+ deniedChecks: number;
111
+ permissionChecks: number;
112
+ aborted: number;
113
+ interrupted: number;
114
+ completed: number;
115
+ }[];
116
+ interpretation: string;
117
+ requiredBeforeLearning: string[];
118
+ }>;
119
+ export {};
120
+ //# sourceMappingURL=mod-projection.d.ts.map
@@ -0,0 +1,155 @@
1
+ /** Native mod adapter: compiler projection and untrusted review candidates. */
2
+ import { createHash } from 'node:crypto';
3
+ import { lstat, mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ export const MOD_GUIDANCE_DIR = '.claude-flow/mods/guidance';
6
+ const MAX_BYTES = 256 * 1024;
7
+ const HEX = /^[a-f0-9]{64}$/;
8
+ const RUN = /^mod-[a-z0-9]+-[a-z0-9]{1,12}-[a-z0-9]{1,12}$/;
9
+ const ID = /^[A-Z]{1,58}-?\d{3,4}$/;
10
+ /** Stable digest of the exported rules; a provenance identifier, not a signature. */
11
+ export function buildModProjection(bundle, sourceRevision, sourceDigests) {
12
+ if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(sourceRevision))
13
+ throw new Error('--revision must be an immutable source commit SHA');
14
+ const rules = [...bundle.constitution.rules, ...bundle.shards.map(s => s.rule)];
15
+ if (rules.length > 256)
16
+ throw new Error('Mod projection supports at most 256 rules; supply a reviewed scoped source');
17
+ const ids = new Set();
18
+ const entries = rules.map(rule => {
19
+ if (!ID.test(rule.id) || ids.has(rule.id) || !['root', 'local'].includes(rule.source) || rule.text.length > 1200 ||
20
+ !Number.isFinite(rule.priority) || rule.priority < 0 || rule.priority > 10000)
21
+ throw new Error('Unsupported mod guidance rule');
22
+ ids.add(rule.id);
23
+ return { id: rule.id, text: rule.text, source: rule.source, constitution: rule.isConstitution, intents: [...rule.intents], priority: rule.priority };
24
+ }).sort((a, b) => a.id.localeCompare(b.id));
25
+ const sourceHashes = Object.fromEntries(Object.entries(bundle.manifest.sourceHashes).sort(([a], [b]) => a.localeCompare(b)));
26
+ if (!/^[a-f0-9]{16}$/.test(bundle.constitution.hash) || Object.entries(sourceHashes).some(([key, value]) => !['root', 'local'].includes(key) || !/^[a-f0-9]{16}$/.test(value))) {
27
+ throw new Error('Unsupported compiler source hashes');
28
+ }
29
+ // The compiler omits an empty local overlay from its manifest. Bind that
30
+ // explicitly supplied empty source too, without changing compiler semantics.
31
+ const emptyDigest = createHash('sha256').update('').digest('hex');
32
+ if (sourceDigests && (Object.entries(sourceHashes).some(([key, hash]) => sourceDigests[key]?.slice(0, 16) !== hash) ||
33
+ Object.entries(sourceDigests).some(([key, digest]) => !['root', 'local'].includes(key) || !HEX.test(digest) ||
34
+ (!sourceHashes[key] && !(key === 'local' && digest === emptyDigest)))))
35
+ throw new Error('Source digests do not match compiler snapshots');
36
+ const data = { version: 1, sourceRevision, constitutionHash: bundle.constitution.hash, sourceHashes,
37
+ ...(sourceDigests ? { sourceDigests: Object.fromEntries(Object.entries(sourceDigests).sort(([a], [b]) => a.localeCompare(b))) } : {}), entries };
38
+ const bundleId = createHash('sha256').update(JSON.stringify(data)).digest('hex');
39
+ const projection = { ...data, bundleId };
40
+ if (Buffer.byteLength(JSON.stringify(projection)) > MAX_BYTES)
41
+ throw new Error('Mod projection exceeds 256 KiB');
42
+ return projection;
43
+ }
44
+ /** Explicit CLI export only. Never called from the native prompt/tool hooks. */
45
+ export async function writeModProjection(directory, projection) {
46
+ const serialized = `${JSON.stringify(projection)}\n`;
47
+ if (Buffer.byteLength(serialized) > MAX_BYTES)
48
+ throw new Error('Serialized mod projection exceeds 256 KiB');
49
+ await mkdir(directory, { recursive: true });
50
+ const target = join(directory, 'projection.json');
51
+ const temp = join(directory, `.projection-${process.pid}-${Date.now()}.tmp`);
52
+ await writeFile(temp, serialized, { flag: 'wx', mode: 0o600 });
53
+ await rename(temp, target);
54
+ return target;
55
+ }
56
+ const keys = ['version', 'kind', 'id', 'runId', 'taskId', 'bundleId', 'sourceRevision', 'ruleIds', 'checks', 'tools', 'completion', 'verified', 'learningEligible'];
57
+ const counters = (value, names) => value && !Array.isArray(value) && Object.keys(value).length === names.length && names.every(k => Number.isSafeInteger(value[k]) && value[k] >= 0 && value[k] <= 100000);
58
+ /** Strict allowlist: a writable `verified: true` or receipt cannot become evidence. */
59
+ export function parseModObservations(text) {
60
+ if (Buffer.byteLength(text) > MAX_BYTES)
61
+ throw new Error('Observation file exceeds 256 KiB');
62
+ const records = JSON.parse(text);
63
+ if (!Array.isArray(records) || records.length > 128)
64
+ throw new Error('Invalid observation queue');
65
+ const seen = new Set();
66
+ for (const r of records) {
67
+ if (!r || Object.keys(r).length !== keys.length || Object.keys(r).some(k => !keys.includes(k)) ||
68
+ r.version !== 1 || r.kind !== 'guidance-observation' || !RUN.test(r.runId ?? '') ||
69
+ !Number.isSafeInteger(r.taskId) || r.taskId < 1 || r.id !== `${r.runId}:${r.taskId}` || seen.has(r.id) ||
70
+ !HEX.test(r.bundleId ?? '') || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(r.sourceRevision ?? '') ||
71
+ !Array.isArray(r.ruleIds) || r.ruleIds.length > 5 || r.ruleIds.some((id) => typeof id !== 'string' || !ID.test(id)) || new Set(r.ruleIds).size !== r.ruleIds.length ||
72
+ !counters(r.checks, ['allow', 'ask', 'deny']) || !counters(r.tools, ['ok', 'error', 'denied']) ||
73
+ !['completed', 'aborted', 'interrupted'].includes(r.completion) || r.verified !== false || r.learningEligible !== false) {
74
+ throw new Error('Invalid or forged mod observation');
75
+ }
76
+ seen.add(r.id);
77
+ }
78
+ return records;
79
+ }
80
+ /** Review priorities only. Never feeds the accepted RunEvent ledger or optimizer. */
81
+ export async function collectModCandidates(directory, bundleId) {
82
+ if (bundleId !== undefined && !HEX.test(bundleId))
83
+ throw new Error('Invalid bundle ID filter');
84
+ let names;
85
+ try {
86
+ names = (await readdir(directory)).filter(name => /^mod-[a-z0-9-]+\.json$/.test(name)).sort();
87
+ }
88
+ catch (error) {
89
+ if (error.code === 'ENOENT')
90
+ names = [];
91
+ else
92
+ throw error;
93
+ }
94
+ if (names.length > 128)
95
+ throw new Error('At most 128 observation files may be reviewed per batch');
96
+ const seen = new Set();
97
+ const groups = new Map();
98
+ let observations = 0;
99
+ let excluded = 0;
100
+ const summary = { files: names.length, withGuidance: 0, withoutGuidance: 0, completed: 0, aborted: 0, interrupted: 0,
101
+ checks: { allow: 0, ask: 0, deny: 0 }, tools: { ok: 0, error: 0, denied: 0 } };
102
+ for (const name of names) {
103
+ const path = join(directory, name);
104
+ const stat = await lstat(path);
105
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_BYTES)
106
+ throw new Error('Unsafe observation file');
107
+ for (const r of parseModObservations(await readFile(path, 'utf8'))) {
108
+ if (name !== `${r.runId}.json`)
109
+ throw new Error('Observation namespace mismatch');
110
+ if (seen.has(r.id))
111
+ throw new Error('Replayed observation');
112
+ seen.add(r.id);
113
+ if (bundleId && r.bundleId !== bundleId) {
114
+ excluded++;
115
+ continue;
116
+ }
117
+ observations++;
118
+ summary[r.ruleIds.length ? 'withGuidance' : 'withoutGuidance']++;
119
+ summary[r.completion]++;
120
+ for (const key of ['allow', 'ask', 'deny'])
121
+ summary.checks[key] += r.checks[key];
122
+ for (const key of ['ok', 'error', 'denied'])
123
+ summary.tools[key] += r.tools[key];
124
+ for (const ruleId of r.ruleIds) {
125
+ const key = `${r.bundleId}:${r.sourceRevision}:${ruleId}`;
126
+ const group = groups.get(key) ?? { bundleId: r.bundleId, sourceRevision: r.sourceRevision, ruleId, observations: 0, affectedObservations: 0,
127
+ toolErrors: 0, toolExecutions: 0, deniedTools: 0, deniedChecks: 0, permissionChecks: 0, aborted: 0, interrupted: 0, completed: 0 };
128
+ group.observations++;
129
+ group.affectedObservations += r.tools.error > 0 || r.tools.denied > 0 || r.checks.deny > 0 || r.completion !== 'completed' ? 1 : 0;
130
+ group.toolErrors += r.tools.error;
131
+ group.toolExecutions += r.tools.ok + r.tools.error;
132
+ group.deniedTools += r.tools.denied;
133
+ group.deniedChecks += r.checks.deny;
134
+ group.permissionChecks += r.checks.allow + r.checks.ask + r.checks.deny;
135
+ group[r.completion]++;
136
+ groups.set(key, group);
137
+ }
138
+ }
139
+ }
140
+ return {
141
+ version: 1, status: 'pending-independent-verification', verified: false, learningEligible: false,
142
+ observations, excluded, summary,
143
+ candidates: [...groups.values()].sort((a, b) => b.affectedObservations - a.affectedObservations || b.toolErrors - a.toolErrors ||
144
+ b.deniedChecks - a.deniedChecks || a.bundleId.localeCompare(b.bundleId) || a.sourceRevision.localeCompare(b.sourceRevision) || a.ruleId.localeCompare(b.ruleId))
145
+ .map(group => ({ ...group, verified: false, learningEligible: false,
146
+ toolErrorRate: group.toolExecutions ? group.toolErrors / group.toolExecutions : null,
147
+ deniedCheckRate: group.permissionChecks ? group.deniedChecks / group.permissionChecks : null,
148
+ reviewReasons: [group.toolErrors ? 'observed tool errors' : '', group.deniedTools ? 'denied tool calls' : '', group.deniedChecks ? 'denied checks' : '',
149
+ group.aborted ? 'aborted turns' : '', group.interrupted ? 'interrupted turns' : ''].filter(Boolean),
150
+ })),
151
+ interpretation: 'Activity correlations only. Completed turns are not accepted successes; displayed rules are not proven causes.',
152
+ requiredBeforeLearning: ['task-bound acceptance evidence', 'held-out baseline and candidate evaluation', 'authorized promotion'],
153
+ };
154
+ }
155
+ //# sourceMappingURL=mod-projection.js.map
@@ -0,0 +1,22 @@
1
+ export declare const MAX_GUIDANCE_SOURCE_BYTES: number;
2
+ export interface ModSourceBinding {
3
+ path: string;
4
+ blobId: string;
5
+ sha256: string;
6
+ byteLength: number;
7
+ }
8
+ /** A commit label alone is insufficient. Both supplied files must match its blobs. */
9
+ export declare function readModGuidanceSources(input: {
10
+ rootPath: string;
11
+ localPath?: string;
12
+ revision: string;
13
+ }): Promise<{
14
+ sourceRevision: string;
15
+ rootContent: string;
16
+ localContent: string | undefined;
17
+ sources: {
18
+ local?: ModSourceBinding | undefined;
19
+ root: ModSourceBinding;
20
+ };
21
+ }>;
22
+ //# sourceMappingURL=mod-sources.d.ts.map
@@ -0,0 +1,123 @@
1
+ /** Explicit export only: bind the compiled snapshots to immutable Git blobs. */
2
+ import { execFile } from 'node:child_process';
3
+ import { constants } from 'node:fs';
4
+ import { lstat, open, realpath } from 'node:fs/promises';
5
+ import { devNull } from 'node:os';
6
+ import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
7
+ import { createHash } from 'node:crypto';
8
+ import { promisify } from 'node:util';
9
+ import { safeGitArgv } from '@claude-flow/security/safe-git';
10
+ const run = promisify(execFile);
11
+ const OID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
12
+ export const MAX_GUIDANCE_SOURCE_BYTES = 1024 * 1024;
13
+ /** Local reads only; no shell, Git hooks, filters, replacement refs or lazy fetch. */
14
+ async function git(repo, args) {
15
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith('GIT_')));
16
+ Object.assign(env, { GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: devNull, GIT_NO_LAZY_FETCH: '1', GIT_ALLOW_PROTOCOL: '', GIT_TERMINAL_PROMPT: '0', GIT_OPTIONAL_LOCKS: '0' });
17
+ const { stdout } = await run('git', safeGitArgv(repo, ['--no-replace-objects', '--literal-pathspecs', '-c', `core.hooksPath=${devNull}`, '-c', 'protocol.allow=never', ...args]), {
18
+ encoding: 'buffer', env, maxBuffer: 2 * MAX_GUIDANCE_SOURCE_BYTES, timeout: 10_000, windowsHide: true,
19
+ });
20
+ return stdout;
21
+ }
22
+ /** Older Git ignores GIT_NO_LAZY_FETCH. Refuse all promisor configuration
23
+ * before reading objects, including repositories whose objects happen to exist.
24
+ * Shallow clones with ordinary local objects remain supported. */
25
+ async function requireLocalObjects(repo) {
26
+ try {
27
+ await git(repo, ['config', '--get-regexp', '^(extensions\\.partialclone|remote\\..*\\.(promisor|partialclonefilter))$']);
28
+ }
29
+ catch (error) {
30
+ const result = error;
31
+ // git config returns 1 only for no matching entries. Other errors fail closed.
32
+ if (result.code === 1 && !result.signal && result.stdout?.length === 0 && result.stderr?.length === 0)
33
+ return;
34
+ throw error;
35
+ }
36
+ throw new Error('Offline mod export does not support partial clones or promisor repositories; use a checkout with local objects');
37
+ }
38
+ async function sourcePath(path) {
39
+ const absolute = resolve(path);
40
+ if (/[\u0000-\u001f\u007f]/.test(absolute))
41
+ throw new Error('Unsupported guidance source path');
42
+ const stat = await lstat(absolute);
43
+ if (!stat.isFile() || stat.isSymbolicLink() || await realpath(absolute) !== absolute)
44
+ throw new Error('Guidance source must be a regular file without symlink components');
45
+ return absolute;
46
+ }
47
+ /** Descriptor based bounded read; never reread mutable bytes after verification. */
48
+ async function sourceBytes(path) {
49
+ const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
50
+ try {
51
+ const stat = await file.stat();
52
+ if (!stat.isFile() || stat.size > MAX_GUIDANCE_SOURCE_BYTES)
53
+ throw new Error('Guidance source exceeds 1 MiB or is not a regular file');
54
+ const bytes = Buffer.alloc(MAX_GUIDANCE_SOURCE_BYTES + 1);
55
+ let size = 0;
56
+ while (size < bytes.length) {
57
+ const { bytesRead } = await file.read(bytes, size, bytes.length - size, null);
58
+ if (!bytesRead)
59
+ break;
60
+ size += bytesRead;
61
+ }
62
+ if (size > MAX_GUIDANCE_SOURCE_BYTES)
63
+ throw new Error('Guidance source exceeds 1 MiB');
64
+ return bytes.subarray(0, size);
65
+ }
66
+ finally {
67
+ await file.close();
68
+ }
69
+ }
70
+ /** A commit label alone is insufficient. Both supplied files must match its blobs. */
71
+ export async function readModGuidanceSources(input) {
72
+ if (!OID.test(input.revision))
73
+ throw new Error('--revision must be a full immutable source commit SHA');
74
+ const rootPath = await sourcePath(input.rootPath);
75
+ let repo;
76
+ try {
77
+ repo = (await git(dirname(rootPath), ['rev-parse', '--show-toplevel'])).toString('utf8').replace(/\r?\n$/, '');
78
+ repo = await realpath(repo);
79
+ }
80
+ catch {
81
+ throw new Error('Guidance source requires a local Git repository');
82
+ }
83
+ await requireLocalObjects(repo);
84
+ try {
85
+ if ((await git(repo, ['cat-file', '-t', input.revision])).toString('utf8').trim() !== 'commit')
86
+ throw new Error('not a commit');
87
+ if ((await git(repo, ['rev-parse', '--verify', `${input.revision}^{commit}`])).toString('utf8').trim() !== input.revision)
88
+ throw new Error('abbreviated commit');
89
+ }
90
+ catch {
91
+ throw new Error('Guidance source requires an existing full immutable commit');
92
+ }
93
+ const read = async (path) => {
94
+ const absolute = await sourcePath(path);
95
+ const rel = relative(repo, absolute);
96
+ if (!rel || isAbsolute(rel) || rel === '..' || rel.startsWith(`..${sep}`))
97
+ throw new Error('Guidance sources must belong to the same Git repository');
98
+ const sourceRepo = (await git(dirname(absolute), ['rev-parse', '--show-toplevel'])).toString('utf8').replace(/\r?\n$/, '');
99
+ if (await realpath(sourceRepo) !== repo)
100
+ throw new Error('Guidance sources must belong to the same Git repository');
101
+ await requireLocalObjects(repo);
102
+ const gitPath = rel.split(sep).join('/');
103
+ const tree = (await git(repo, ['ls-tree', '-z', '--full-tree', input.revision, '--', gitPath])).toString('utf8');
104
+ const match = /^(100644|100755) blob ([a-f0-9]{40}|[a-f0-9]{64})\t([^\0]+)\0$/.exec(tree);
105
+ if (!match || match[3] !== gitPath)
106
+ throw new Error('Guidance source must be a regular tracked blob in the supplied commit');
107
+ const blobId = match[2];
108
+ const blobSize = Number((await git(repo, ['cat-file', '-s', blobId])).toString('utf8').trim());
109
+ if (!Number.isSafeInteger(blobSize) || blobSize < 0 || blobSize > MAX_GUIDANCE_SOURCE_BYTES)
110
+ throw new Error('Committed guidance source exceeds 1 MiB');
111
+ const committed = await git(repo, ['cat-file', 'blob', blobId]);
112
+ const bytes = await sourceBytes(absolute);
113
+ if (!bytes.equals(committed))
114
+ throw new Error('Guidance source bytes differ from the supplied commit; commit the reviewed source before exporting');
115
+ // Preserve a BOM in the compiler snapshot; reject invalid UTF-8 rather than replacing bytes.
116
+ const content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
117
+ return { content, binding: { path: gitPath, blobId, sha256: createHash('sha256').update(bytes).digest('hex'), byteLength: bytes.length } };
118
+ };
119
+ const root = await read(rootPath);
120
+ const local = input.localPath === undefined ? undefined : await read(input.localPath);
121
+ return { sourceRevision: input.revision, rootContent: root.content, localContent: local?.content, sources: { root: root.binding, ...(local ? { local: local.binding } : {}) } };
122
+ }
123
+ //# sourceMappingURL=mod-sources.js.map
@@ -433,6 +433,75 @@ if (command && commands[command]) {
433
433
  module.exports = commands;
434
434
  `;
435
435
  }
436
+ // Literal-word root guard mirrored in the mod and both shipped classic helpers.
437
+ const ROOT_DELETE_CHECK_SOURCE = String.raw `function hasRootDelete(command, depth = 0) {
438
+ let word = '', quote = '', started = false, redirect = false
439
+ let inRm = false, optionsEnded = false, recursive = false, force = false, root = false
440
+ const isRoot = (operand) => {
441
+ if (!operand.startsWith('/')) return false
442
+ const parts = []
443
+ for (const part of operand.split('/')) {
444
+ if (!part || part === '.') continue
445
+ if (part === '..') parts.pop()
446
+ else parts.push(part)
447
+ }
448
+ return parts.length === 0 || /[*?\[]/.test(parts[0])
449
+ }
450
+ const finishWord = () => {
451
+ if (!started) return false
452
+ // Literal shell strings (e.g. sh -c 'rm -rf /') also carried the old guard.
453
+ // Bound rescanning to four levels; beyond that retain its conservative check.
454
+ if (word.includes('rm') && /[\s;&|()]/.test(word)) {
455
+ if (depth < 4 ? hasRootDelete(word, depth + 1) : word.includes('rm -rf /')) return true
456
+ }
457
+ if (!inRm) inRm = word === 'rm' || word.endsWith('/rm')
458
+ else if (!optionsEnded && word === '--') optionsEnded = true
459
+ else if (!optionsEnded && word.startsWith('-')) {
460
+ recursive = recursive || word === '--recursive' || /^-[a-z]*r[a-z]*$/.test(word)
461
+ force = force || word === '--force' || /^-[a-z]*f[a-z]*$/.test(word)
462
+ } else root = root || isRoot(word)
463
+ word = ''; started = false
464
+ return inRm && recursive && force && root
465
+ }
466
+ const finishCommand = () => {
467
+ const denied = inRm && recursive && force && root
468
+ inRm = optionsEnded = recursive = force = root = false
469
+ return denied
470
+ }
471
+ for (let i = 0; i < command.length; i++) {
472
+ const char = command[i]
473
+ const redirectionAmpersand = char === '&' && (redirect || command[i + 1] === '>')
474
+ redirect = false
475
+ if (quote) {
476
+ if (char === quote) quote = ''
477
+ else if (quote === '"' && char === '\\' && i + 1 < command.length &&
478
+ (command[i + 1] === '"' || command[i + 1] === '\\' || command[i + 1] === '$' ||
479
+ command.charCodeAt(i + 1) === 96 || command[i + 1] === '\n')) {
480
+ const next = command[++i]
481
+ if (next !== '\n') word += next
482
+ } else word += char
483
+ continue
484
+ }
485
+ if (char === '\\' && i + 1 < command.length) {
486
+ const next = command[++i]
487
+ if (next !== '\n') { word += next; started = true }
488
+ } else if (char === '"' || char === "'") {
489
+ quote = char; started = true
490
+ } else if (char === '#' && !started) {
491
+ while (i < command.length && command[i] !== '\n') i++
492
+ if (finishCommand()) return true
493
+ } else if (char === ' ' || char === '\t' || char === '\r' || char === '\n' ||
494
+ ';|&()<>'.includes(char)) {
495
+ if (finishWord()) return true
496
+ // Redirections separate words, but later operands still belong to rm.
497
+ redirect = char === '<' || char === '>'
498
+ if (!redirectionAmpersand && (char === '\n' || ';|&()'.includes(char)) && finishCommand()) return true
499
+ } else {
500
+ word += char; started = true
501
+ }
502
+ }
503
+ return finishWord() || finishCommand()
504
+ }`;
436
505
  /**
437
506
  * Generate hook-handler.cjs (cross-platform hook dispatcher)
438
507
  * This is the inline fallback when file copy from the package fails.
@@ -569,13 +638,14 @@ export function generateHookHandler() {
569
638
  ' }',
570
639
  ' if (router && router.routeTask) {',
571
640
  ' const result = router.routeTask(prompt);',
641
+ " const row = (text) => '| ' + text.substring(0, 60).padEnd(60) + ' |';",
572
642
  ' var output = [];',
573
643
  " output.push('[INFO] Routing task: ' + (prompt.substring(0, 80) || '(no prompt)'));",
574
644
  " output.push('');",
575
645
  " output.push('+------------------- Primary Recommendation -------------------+');",
576
- " output.push('| Agent: ' + result.agent.padEnd(53) + '|');",
577
- " output.push('| Confidence: ' + (result.confidence * 100).toFixed(1) + '%' + ' '.repeat(44) + '|');",
578
- " output.push('| Reason: ' + result.reason.substring(0, 53).padEnd(53) + '|');",
646
+ " output.push(row('Agent: ' + result.agent));",
647
+ " output.push(row('Confidence: ' + (result.confidence * 100).toFixed(1) + '%'));",
648
+ " output.push(row('Reason: ' + (result.reason || '')));",
579
649
  " output.push('+--------------------------------------------------------------+');",
580
650
  " console.log(output.join('\\n'));",
581
651
  ' } else {',
@@ -637,8 +707,9 @@ export function generateHookHandler() {
637
707
  " 'pre-bash': () => {",
638
708
  " var cmd = String(hookInput.command || toolInputObj.command || prompt || '').toLowerCase();",
639
709
  " var dangerous = ['rm -rf /', 'format c:', 'del /s /q c:\\\\', ':(){:|:&};:'];",
710
+ ROOT_DELETE_CHECK_SOURCE.split('\n').map((line) => ' ' + line).join('\n'),
640
711
  ' for (var i = 0; i < dangerous.length; i++) {',
641
- ' if (cmd.includes(dangerous[i])) {',
712
+ " if (dangerous[i] === 'rm -rf /' ? hasRootDelete(cmd) : cmd.includes(dangerous[i])) {",
642
713
  " console.error('[BLOCKED] Dangerous command detected: ' + dangerous[i]);",
643
714
  ' // Claude Code PreToolUse: exit 2 blocks execution; exit 1 is non-blocking.',
644
715
  ' process.exit(2);',
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.51.1",
3
+ "version": "3.52.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",