@claude-flow/cli 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.
@@ -1 +1 @@
1
- 3.42.0
1
+ 3.51.1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.51.1",
3
+ "version": "3.52.0",
4
4
  "files": {
5
5
  "auto-memory-hook.mjs": "c8d218526a3c34034c2a69de23f507f0eb0f70d59c16426a8edc849e6ac22529",
6
6
  "hook-handler.cjs": "fbd827dfac52e48ea1a576a4484826b6570c0fa710ca4c28faad63b003ac1274",
@@ -9,6 +9,6 @@
9
9
  "router.cjs": "16648b3c0d73b6681ac2d8d13def6a68286c57f5a11004de5b19a0fa088c833d"
10
10
  }
11
11
  },
12
- "signature": "D+NMHUsWriB+ZO/yQdOlrw4daC5sNvZxsDDxF7Y82SruJc7xd4icePgaDDntBVOq/znDpFZUCqqiUYX7sy0uCw==",
12
+ "signature": "1Cbum5dGDkHM/a2vsLktQc8gPTFT3CVXUNsq4ReHQLIduwvX8dxpT3nyO1ExAf5lYGPY5fznJlPcRsoh321yDA==",
13
13
  "algorithm": "ed25519"
14
14
  }
File without changes
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
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 7,
4
- "generatedAt": "2026-10-02T16:06:24.442Z",
5
- "gitSha": "09a1cb02",
4
+ "generatedAt": "2026-10-05T14:24:23.258Z",
5
+ "gitSha": "0f327015",
6
6
  "catalog": {
7
7
  "agents": 176,
8
8
  "tools": 423,
@@ -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