@claude-flow/cli 3.56.7 → 3.56.8

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,6 +1,6 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.56.7",
3
+ "version": "3.56.8",
4
4
  "files": {
5
5
  "auto-memory-hook.mjs": "631fac9d518835e2852a406888316bbbddddb49e7ed8e406d2e971f46833df9a",
6
6
  "hook-handler.cjs": "0f855c667b8d2d04cb4fe91b14b351d14c3dfb30516dcd9d7ae156acbe0a689d",
@@ -9,6 +9,6 @@
9
9
  "router.cjs": "16648b3c0d73b6681ac2d8d13def6a68286c57f5a11004de5b19a0fa088c833d"
10
10
  }
11
11
  },
12
- "signature": "15NFpUZ8+naP4v/xuCBCl7IZ//YJPXQYotOg48V/Xw/uyCY1FXYeeL4a3Rwa+Bji0+CV4FRiZg61N6Zt+EStBg==",
12
+ "signature": "ld9RNoF9lnGKjlUpofQLwCZgtE9Y5QiNNkYZLGsSQhZGp51JhBnHymH9YWpFC3F1HBslxRnea6U2TVczJKK5Bg==",
13
13
  "algorithm": "ed25519"
14
14
  }
package/README.md CHANGED
@@ -49,6 +49,10 @@ npx -y ruflo@latest mcp start
49
49
 
50
50
  MCP exposes Ruflo tools to your client. The console and mods above are Claude Code integrations; hook support depends on the client.
51
51
 
52
+ <img src="docs/assets/readme/icons/terminal.svg" width="28" height="28" alt=""> **Prefer a plain terminal? Optional.** The console also runs on its own, with no Claude Code: `npx ruflo tui` (add `--plain` to print one frame for scripts). It shows Overview, Cost, Swarm, Events and Missions, plus an optional AI page. [Terminal UI guide](docs/USERGUIDE.md#terminal-ui-optional)
53
+
54
+ <p align="center"><img src="docs/assets/ruflo-tui-compact.svg" width="640" alt="Looping recording of the optional ruflo terminal UI cycling through Overview, Cost, Swarm, Events and Missions in a sample project"></p>
55
+
52
56
  [Compare install paths](#quick-start) · [Open the console](#console-walkthrough) · [User guide](docs/USERGUIDE.md)
53
57
 
54
58
  <div align="center">
@@ -116,9 +120,11 @@ User → Ruflo (CLI/MCP) → Router → Swarm → Agents → Memory → LLM prov
116
120
 
117
121
  <p align="center"><img src="docs/assets/readme/console.svg" alt="Mission control: inspect agent work" width="100%"></p>
118
122
 
119
- <p align="center"><img src="docs/assets/ruflo-console-workflows.svg" width="952" height="964" alt="An animated recording of the ruflo console inside Claude Code: the boot (the neon sign and every area checked), then the Workflows page on a sample run: phases and agents with model, tokens and time; drilling from a run into one agent's activity, log and result; search; failure triage; cost; replay and compare; the control tab with a confirm card; and the mission autopilot panel"></p>
123
+ <p align="center"><a href="https://raw.githubusercontent.com/ruvnet/ruflo/main/docs/assets/ruflo-console-theme-compact.svg"><img src="docs/assets/ruflo-console-theme-compact.svg" width="100%" alt="Ruflo Orbital theme: animated boot checks, block logo, menu icons, rotating globe, swarm routing, six color palettes and responsive Theme Studio"></a></p>
124
+
125
+ [Open animated demo](https://raw.githubusercontent.com/ruvnet/ruflo/main/docs/assets/ruflo-console-theme-compact.svg) · [Theme Studio guide](plugins/ruflo-console/README.md) · [Workflow demo](docs/assets/ruflo-console-workflows.svg)
120
126
 
121
- <img src="docs/assets/readme/console-icon-workflows.svg" width="28" height="28" alt=""> **Your agent cockpit.** Track workflows, agents, models, tokens and cost beside Claude Code. The animation shows startup checks and a **sample workflow**, not a live session.
127
+ <img src="docs/assets/readme/console-icon-workflows.svg" width="28" height="28" alt=""> **Your agent cockpit.** Track workflows, agents, models, tokens and cost beside Claude Code. The 38 second animation shows **Orbital and Theme Studio** through actual renderer frames, including boot checks, six palettes, customization, saved themes, JSON import and resizing. This compact 3.84 MB SVG retains all 456 frames. Native host and AI provider execution are not demonstrated.
122
128
 
123
129
  <img src="docs/assets/readme/console-icon-inspect.svg" width="28" height="28" alt=""> **Inspect any run.** Open agent logs and results, search, triage failures, replay and compare. Start with `npx ruflo init`, restart Claude Code, then `/ruflo`. [Full console tour](docs/assets/ruflo-console-walkthrough-wide.gif).
124
130
 
@@ -136,7 +142,7 @@ User → Ruflo (CLI/MCP) → Router → Swarm → Agents → Memory → LLM prov
136
142
  4. **Verify the result.** Check that Missions contains the goal and Overview records the action. Creating a mission does not mean the feature has been implemented.
137
143
  5. **Keep control.** **Take back control** pauses Claude's console tools. A later call should be refused until you restore control.
138
144
 
139
- **What the recording demonstrates:** a real Claude Haiku session with `write + auto`, mission creation, page navigation, a refused swarm stop, and control being taken back. The steps above use `ask` so you approve creation yourself. The separate workflow animation uses sample data; neither is evidence of completed swarm work or memory recall.
145
+ **What the recording demonstrates:** a real Claude Haiku session with `write + auto`, mission creation, page navigation, a refused swarm stop, and control being taken back. The steps above use `ask` so you approve creation yourself. The linked workflow animation uses sample data, while the theme demo replays renderer frames. Neither is evidence of completed swarm work or memory recall.
140
146
 
141
147
  [Implementation and recorded test findings](v3/docs/adr/ADR-444-claude-controls-the-console.md) · [Reproduction script](plugins/ruflo-console/scripts/e2e-control.sh)
142
148
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 7,
4
- "generatedAt": "2026-10-11T22:43:38.354Z",
5
- "gitSha": "aac16fbd",
4
+ "generatedAt": "2026-10-12T17:29:00.623Z",
5
+ "gitSha": "4f1b3e96",
6
6
  "catalog": {
7
7
  "agents": 176,
8
8
  "tools": 423,
@@ -0,0 +1,29 @@
1
+ interface CoverageFileEntry {
2
+ filePath: string;
3
+ lines: number;
4
+ branches: number;
5
+ functions: number;
6
+ statements: number;
7
+ }
8
+ interface CoverageData {
9
+ found: boolean;
10
+ source: string;
11
+ entries: CoverageFileEntry[];
12
+ summary: {
13
+ totalFiles: number;
14
+ overallLineCoverage: number;
15
+ overallBranchCoverage: number;
16
+ overallFunctionCoverage: number;
17
+ overallStatementCoverage: number;
18
+ };
19
+ }
20
+ /**
21
+ * Read coverage data from disk. Checks these locations in order:
22
+ * 1. coverage/coverage-summary.json (Jest/Istanbul)
23
+ * 2. coverage/lcov.info (lcov format)
24
+ * 3. coverage/coverage-final.json (Istanbul counters)
25
+ * 4. .nyc_output/out.json (nyc counters or legacy summary)
26
+ */
27
+ export declare function readCoverageFromDisk(): CoverageData;
28
+ export {};
29
+ //# sourceMappingURL=coverage-reader.d.ts.map
@@ -0,0 +1,247 @@
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /** Coverage reports are build output the project controls: refuse anything large enough to stall or exhaust the process. */
4
+ const MAX_REPORT_BYTES = 64 * 1024 * 1024;
5
+ /** The file's text, or undefined when it is missing, not a regular file, unreadable, or over the size cap. */
6
+ function readReport(path) {
7
+ try {
8
+ const stat = statSync(path);
9
+ if (!stat.isFile() || stat.size > MAX_REPORT_BYTES)
10
+ return undefined;
11
+ return readFileSync(path, 'utf-8');
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
17
+ /**
18
+ * Read coverage data from disk. Checks these locations in order:
19
+ * 1. coverage/coverage-summary.json (Jest/Istanbul)
20
+ * 2. coverage/lcov.info (lcov format)
21
+ * 3. coverage/coverage-final.json (Istanbul counters)
22
+ * 4. .nyc_output/out.json (nyc counters or legacy summary)
23
+ */
24
+ export function readCoverageFromDisk() {
25
+ const cwd = process.cwd();
26
+ const noData = {
27
+ found: false,
28
+ source: 'none',
29
+ entries: [],
30
+ summary: { totalFiles: 0, overallLineCoverage: 0, overallBranchCoverage: 0, overallFunctionCoverage: 0, overallStatementCoverage: 0 },
31
+ };
32
+ // 1. Try coverage-summary.json (Jest/Istanbul)
33
+ for (const relPath of ['coverage/coverage-summary.json', 'coverage-summary.json']) {
34
+ const text = readReport(join(cwd, relPath));
35
+ if (text !== undefined) {
36
+ try {
37
+ const raw = JSON.parse(text);
38
+ const report = parseCoverageSummaryJson(raw, relPath);
39
+ if (report.found)
40
+ return report;
41
+ }
42
+ catch {
43
+ // malformed, try next
44
+ }
45
+ }
46
+ }
47
+ // 2. Try lcov.info
48
+ for (const relPath of ['coverage/lcov.info', 'lcov.info']) {
49
+ const raw = readReport(join(cwd, relPath));
50
+ if (raw !== undefined) {
51
+ try {
52
+ const report = parseLcovInfo(raw, relPath);
53
+ if (report.found)
54
+ return report;
55
+ }
56
+ catch {
57
+ // malformed, try next
58
+ }
59
+ }
60
+ }
61
+ // Istanbul's raw counter map (Jest, Vitest, nyc).
62
+ for (const relPath of ['coverage/coverage-final.json', 'coverage-final.json', '.nyc_output/out.json']) {
63
+ const text = readReport(join(cwd, relPath));
64
+ if (text === undefined)
65
+ continue;
66
+ try {
67
+ const raw = JSON.parse(text);
68
+ const report = parseIstanbulCoverage(raw, relPath);
69
+ if (report.found)
70
+ return report;
71
+ const legacy = parseCoverageSummaryJson(raw, relPath);
72
+ if (legacy.found)
73
+ return legacy;
74
+ }
75
+ catch {
76
+ // Missing or malformed, try the next supported report.
77
+ }
78
+ }
79
+ return noData;
80
+ }
81
+ function parseCoverageSummaryJson(data, source) {
82
+ const entries = [];
83
+ const metricsByFile = [];
84
+ for (const [filePath, metrics] of Object.entries(data)) {
85
+ if (filePath === 'total')
86
+ continue;
87
+ const m = metrics;
88
+ if (!m || typeof m !== 'object')
89
+ continue;
90
+ if (!['lines', 'branches', 'functions', 'statements'].some(key => m[key]))
91
+ continue;
92
+ metricsByFile.push(m);
93
+ const linePct = percentage(m.lines) ?? 0;
94
+ const branchPct = (percentage(m.branches) ?? 0);
95
+ const funcPct = (percentage(m.functions) ?? 0);
96
+ const stmtPct = (percentage(m.statements) ?? 0);
97
+ entries.push({ filePath, lines: linePct, branches: branchPct, functions: funcPct, statements: stmtPct });
98
+ }
99
+ // Only aggregate complete counts. A missing denominator is unknown, not empty.
100
+ const total = data['total'];
101
+ const aggregate = (key) => {
102
+ const supplied = percentage(total?.[key]);
103
+ if (supplied !== undefined)
104
+ return supplied;
105
+ const metrics = metricsByFile.map(file => file[key]);
106
+ if (metrics.length === 0 || !metrics.every(hasCounts))
107
+ return 0;
108
+ return ratio(metrics.reduce((sum, m) => sum + m.covered, 0), metrics.reduce((sum, m) => sum + m.total, 0));
109
+ };
110
+ // Sort by lowest line coverage
111
+ entries.sort((a, b) => a.lines - b.lines);
112
+ return {
113
+ found: entries.length > 0,
114
+ source,
115
+ entries,
116
+ summary: {
117
+ totalFiles: entries.length,
118
+ overallLineCoverage: aggregate('lines'),
119
+ overallBranchCoverage: aggregate('branches'),
120
+ overallFunctionCoverage: aggregate('functions'),
121
+ overallStatementCoverage: aggregate('statements'),
122
+ },
123
+ };
124
+ }
125
+ function parseLcovInfo(raw, source) {
126
+ const entries = [];
127
+ let currentFile = '';
128
+ let linesHit = 0, linesFound = 0;
129
+ let branchesHit = 0, branchesFound = 0;
130
+ let functionsHit = 0, functionsFound = 0;
131
+ let totalLH = 0, totalLF = 0, totalBH = 0, totalBF = 0, totalFH = 0, totalFF = 0;
132
+ const flushRecord = () => {
133
+ if (currentFile) {
134
+ // A corrupt record cannot report more hits than lines, or a negative count.
135
+ linesHit = Math.min(linesHit, linesFound);
136
+ branchesHit = Math.min(branchesHit, branchesFound);
137
+ functionsHit = Math.min(functionsHit, functionsFound);
138
+ totalLH += linesHit;
139
+ totalLF += linesFound;
140
+ totalBH += branchesHit;
141
+ totalBF += branchesFound;
142
+ totalFH += functionsHit;
143
+ totalFF += functionsFound;
144
+ entries.push({
145
+ filePath: currentFile,
146
+ lines: linesFound > 0 ? (linesHit / linesFound) * 100 : 100,
147
+ branches: branchesFound > 0 ? (branchesHit / branchesFound) * 100 : 100,
148
+ functions: functionsFound > 0 ? (functionsHit / functionsFound) * 100 : 100,
149
+ statements: linesFound > 0 ? (linesHit / linesFound) * 100 : 100,
150
+ });
151
+ }
152
+ };
153
+ for (const line of raw.split('\n')) {
154
+ const trimmed = line.trim();
155
+ if (trimmed.startsWith('SF:')) {
156
+ flushRecord();
157
+ currentFile = trimmed.slice(3);
158
+ linesHit = 0;
159
+ linesFound = 0;
160
+ branchesHit = 0;
161
+ branchesFound = 0;
162
+ functionsHit = 0;
163
+ functionsFound = 0;
164
+ }
165
+ else if (trimmed.startsWith('LH:')) {
166
+ linesHit = count(trimmed.slice(3));
167
+ }
168
+ else if (trimmed.startsWith('LF:')) {
169
+ linesFound = count(trimmed.slice(3));
170
+ }
171
+ else if (trimmed.startsWith('BRH:')) {
172
+ branchesHit = count(trimmed.slice(4));
173
+ }
174
+ else if (trimmed.startsWith('BRF:')) {
175
+ branchesFound = count(trimmed.slice(4));
176
+ }
177
+ else if (trimmed.startsWith('FNH:')) {
178
+ functionsHit = count(trimmed.slice(4));
179
+ }
180
+ else if (trimmed.startsWith('FNF:')) {
181
+ functionsFound = count(trimmed.slice(4));
182
+ }
183
+ else if (trimmed === 'end_of_record') {
184
+ flushRecord();
185
+ currentFile = '';
186
+ }
187
+ }
188
+ flushRecord();
189
+ entries.sort((a, b) => a.lines - b.lines);
190
+ return {
191
+ found: entries.length > 0,
192
+ source,
193
+ entries,
194
+ summary: {
195
+ totalFiles: entries.length,
196
+ overallLineCoverage: ratio(totalLH, totalLF),
197
+ overallBranchCoverage: ratio(totalBH, totalBF),
198
+ overallFunctionCoverage: ratio(totalFH, totalFF),
199
+ overallStatementCoverage: ratio(totalLH, totalLF),
200
+ },
201
+ };
202
+ }
203
+ /** A non-negative integer count; anything else in a report line is treated as 0. */
204
+ function count(text) {
205
+ const n = parseInt(text, 10);
206
+ return Number.isFinite(n) && n > 0 ? n : 0;
207
+ }
208
+ function ratio(covered, total) {
209
+ return total > 0 ? covered / total * 100 : 100;
210
+ }
211
+ function hasCounts(metric) {
212
+ return typeof metric?.total === 'number' && Number.isFinite(metric.total) && metric.total >= 0
213
+ && typeof metric.covered === 'number' && Number.isFinite(metric.covered)
214
+ && metric.covered >= 0 && metric.covered <= metric.total;
215
+ }
216
+ function percentage(metric) {
217
+ if (typeof metric?.pct === 'number' && Number.isFinite(metric.pct)
218
+ && metric.pct >= 0 && metric.pct <= 100)
219
+ return metric.pct;
220
+ return hasCounts(metric) ? ratio(metric.covered, metric.total) : undefined;
221
+ }
222
+ /** Istanbul line coverage uses the maximum statement hit count on each start line. */
223
+ function parseIstanbulCoverage(data, source) {
224
+ const summary = {};
225
+ for (const [filePath, value] of Object.entries(data)) {
226
+ const cov = value;
227
+ if (!cov || !cov.s || !cov.f || !cov.b || !cov.statementMap)
228
+ continue;
229
+ const lines = new Map();
230
+ for (const [id, hits] of Object.entries(cov.s)) {
231
+ const line = cov.statementMap[id]?.start?.line;
232
+ if (!Number.isInteger(line) || line < 1)
233
+ throw new Error('Invalid Istanbul statement location');
234
+ lines.set(line, Math.max(lines.get(line) ?? 0, hits));
235
+ }
236
+ const metric = (hits) => {
237
+ if (hits.some(n => !Number.isFinite(n) || n < 0))
238
+ throw new Error('Invalid Istanbul counter');
239
+ const covered = hits.filter(n => n > 0).length;
240
+ return { total: hits.length, covered, pct: ratio(covered, hits.length) };
241
+ };
242
+ summary[filePath] = { lines: metric([...lines.values()]), statements: metric(Object.values(cov.s)),
243
+ functions: metric(Object.values(cov.f)), branches: metric(Object.values(cov.b).flat()) };
244
+ }
245
+ return parseCoverageSummaryJson(summary, source);
246
+ }
247
+ //# sourceMappingURL=coverage-reader.js.map
@@ -1,10 +1,14 @@
1
+ /**
2
+ * V3 CLI Hooks Command
3
+ * Self-learning hooks system for intelligent workflow automation
4
+ */
1
5
  import { validateFeedbackPatterns } from '../memory/feedback-patterns.js';
6
+ import { readCoverageFromDisk } from './coverage-reader.js';
2
7
  import { output } from '../output.js';
3
8
  import { confirm } from '../prompt.js';
4
9
  import { callMCPTool, MCPClientError } from '../mcp-client.js';
5
10
  import { storeCommand } from './transfer-store.js';
6
- import { existsSync, readFileSync, statSync } from 'node:fs';
7
- import { join } from 'node:path';
11
+ import { readFileSync, statSync } from 'node:fs';
8
12
  import { getSecurityStatus as sharedGetSecurityStatus, getSwarmStatus as sharedGetSwarmStatus, getGitUncommittedCount as sharedGetGitUncommittedCount, } from '../funnel/local-signals.js';
9
13
  /**
10
14
  * #1686 — `?? 0` only defaults null/undefined; NaN slips through and
@@ -16,181 +20,6 @@ function safeNum(value, fallback = 0) {
16
20
  const n = typeof value === 'number' ? value : Number(value);
17
21
  return Number.isFinite(n) ? n : fallback;
18
22
  }
19
- /**
20
- * Read coverage data from disk. Checks these locations in order:
21
- * 1. coverage/coverage-summary.json (Jest/Istanbul)
22
- * 2. coverage/lcov.info (lcov format)
23
- * 3. .nyc_output/out.json (nyc)
24
- */
25
- function readCoverageFromDisk() {
26
- const cwd = process.cwd();
27
- const noData = {
28
- found: false,
29
- source: 'none',
30
- entries: [],
31
- summary: { totalFiles: 0, overallLineCoverage: 0, overallBranchCoverage: 0, overallFunctionCoverage: 0, overallStatementCoverage: 0 },
32
- };
33
- // 1. Try coverage-summary.json (Jest/Istanbul)
34
- for (const relPath of ['coverage/coverage-summary.json', 'coverage-summary.json']) {
35
- const summaryPath = join(cwd, relPath);
36
- if (existsSync(summaryPath)) {
37
- try {
38
- const raw = JSON.parse(readFileSync(summaryPath, 'utf-8'));
39
- return parseCoverageSummaryJson(raw, relPath);
40
- }
41
- catch {
42
- // malformed, try next
43
- }
44
- }
45
- }
46
- // 2. Try lcov.info
47
- for (const relPath of ['coverage/lcov.info', 'lcov.info']) {
48
- const lcovPath = join(cwd, relPath);
49
- if (existsSync(lcovPath)) {
50
- try {
51
- const raw = readFileSync(lcovPath, 'utf-8');
52
- return parseLcovInfo(raw, relPath);
53
- }
54
- catch {
55
- // malformed, try next
56
- }
57
- }
58
- }
59
- // 3. Try .nyc_output/out.json
60
- const nycPath = join(cwd, '.nyc_output', 'out.json');
61
- if (existsSync(nycPath)) {
62
- try {
63
- const raw = JSON.parse(readFileSync(nycPath, 'utf-8'));
64
- return parseCoverageSummaryJson(raw, '.nyc_output/out.json');
65
- }
66
- catch {
67
- // malformed
68
- }
69
- }
70
- return noData;
71
- }
72
- function parseCoverageSummaryJson(data, source) {
73
- const entries = [];
74
- let totalLines = 0, coveredLines = 0;
75
- let totalBranches = 0, coveredBranches = 0;
76
- let totalFunctions = 0, coveredFunctions = 0;
77
- let totalStatements = 0, coveredStatements = 0;
78
- for (const [filePath, metrics] of Object.entries(data)) {
79
- if (filePath === 'total')
80
- continue;
81
- const m = metrics;
82
- if (!m || typeof m !== 'object')
83
- continue;
84
- const linePct = m.lines?.pct ?? m.lines?.covered != null ? ((m.lines?.covered ?? 0) / Math.max(m.lines?.total ?? 1, 1)) * 100 : 0;
85
- const branchPct = m.branches?.pct ?? (m.branches?.total ? ((m.branches?.covered ?? 0) / m.branches.total) * 100 : 100);
86
- const funcPct = m.functions?.pct ?? (m.functions?.total ? ((m.functions?.covered ?? 0) / m.functions.total) * 100 : 100);
87
- const stmtPct = m.statements?.pct ?? (m.statements?.total ? ((m.statements?.covered ?? 0) / m.statements.total) * 100 : 100);
88
- entries.push({ filePath, lines: linePct, branches: branchPct, functions: funcPct, statements: stmtPct });
89
- totalLines += m.lines?.total ?? 0;
90
- coveredLines += m.lines?.covered ?? 0;
91
- totalBranches += m.branches?.total ?? 0;
92
- coveredBranches += m.branches?.covered ?? 0;
93
- totalFunctions += m.functions?.total ?? 0;
94
- coveredFunctions += m.functions?.covered ?? 0;
95
- totalStatements += m.statements?.total ?? 0;
96
- coveredStatements += m.statements?.covered ?? 0;
97
- }
98
- // Also read the total key if present
99
- const total = data['total'];
100
- const overallLine = total?.lines?.pct ?? (totalLines > 0 ? (coveredLines / totalLines) * 100 : 0);
101
- const overallBranch = total?.branches?.pct ?? (totalBranches > 0 ? (coveredBranches / totalBranches) * 100 : 0);
102
- const overallFunction = total?.functions?.pct ?? (totalFunctions > 0 ? (coveredFunctions / totalFunctions) * 100 : 0);
103
- const overallStatement = total?.statements?.pct ?? (totalStatements > 0 ? (coveredStatements / totalStatements) * 100 : 0);
104
- // Sort by lowest line coverage
105
- entries.sort((a, b) => a.lines - b.lines);
106
- return {
107
- found: true,
108
- source,
109
- entries,
110
- summary: {
111
- totalFiles: entries.length,
112
- overallLineCoverage: overallLine,
113
- overallBranchCoverage: overallBranch,
114
- overallFunctionCoverage: overallFunction,
115
- overallStatementCoverage: overallStatement,
116
- },
117
- };
118
- }
119
- function parseLcovInfo(raw, source) {
120
- const entries = [];
121
- let currentFile = '';
122
- let linesHit = 0, linesFound = 0;
123
- let branchesHit = 0, branchesFound = 0;
124
- let functionsHit = 0, functionsFound = 0;
125
- const flushRecord = () => {
126
- if (currentFile) {
127
- entries.push({
128
- filePath: currentFile,
129
- lines: linesFound > 0 ? (linesHit / linesFound) * 100 : 0,
130
- branches: branchesFound > 0 ? (branchesHit / branchesFound) * 100 : 100,
131
- functions: functionsFound > 0 ? (functionsHit / functionsFound) * 100 : 100,
132
- statements: linesFound > 0 ? (linesHit / linesFound) * 100 : 0,
133
- });
134
- }
135
- };
136
- for (const line of raw.split('\n')) {
137
- const trimmed = line.trim();
138
- if (trimmed.startsWith('SF:')) {
139
- currentFile = trimmed.slice(3);
140
- linesHit = 0;
141
- linesFound = 0;
142
- branchesHit = 0;
143
- branchesFound = 0;
144
- functionsHit = 0;
145
- functionsFound = 0;
146
- }
147
- else if (trimmed.startsWith('LH:')) {
148
- linesHit = parseInt(trimmed.slice(3), 10) || 0;
149
- }
150
- else if (trimmed.startsWith('LF:')) {
151
- linesFound = parseInt(trimmed.slice(3), 10) || 0;
152
- }
153
- else if (trimmed.startsWith('BRH:')) {
154
- branchesHit = parseInt(trimmed.slice(4), 10) || 0;
155
- }
156
- else if (trimmed.startsWith('BRF:')) {
157
- branchesFound = parseInt(trimmed.slice(4), 10) || 0;
158
- }
159
- else if (trimmed.startsWith('FNH:')) {
160
- functionsHit = parseInt(trimmed.slice(4), 10) || 0;
161
- }
162
- else if (trimmed.startsWith('FNF:')) {
163
- functionsFound = parseInt(trimmed.slice(4), 10) || 0;
164
- }
165
- else if (trimmed === 'end_of_record') {
166
- flushRecord();
167
- currentFile = '';
168
- }
169
- }
170
- flushRecord();
171
- entries.sort((a, b) => a.lines - b.lines);
172
- let totalLH = 0, totalLF = 0, totalBH = 0, totalBF = 0;
173
- for (const e of entries) {
174
- // Approximate from percentages (we lost exact counts after flush, but summaries are okay)
175
- totalLH += e.lines;
176
- totalLF += 100;
177
- totalBH += e.branches;
178
- totalBF += 100;
179
- }
180
- const n = entries.length || 1;
181
- return {
182
- found: true,
183
- source,
184
- entries,
185
- summary: {
186
- totalFiles: entries.length,
187
- overallLineCoverage: totalLH / n,
188
- overallBranchCoverage: totalBH / n,
189
- overallFunctionCoverage: 0,
190
- overallStatementCoverage: totalLH / n,
191
- },
192
- };
193
- }
194
23
  /**
195
24
  * Classify a coverage gap by priority type based on coverage percentage and threshold
196
25
  */
@@ -2996,7 +2825,7 @@ const coverageRouteCommand = {
2996
2825
  'c8: npx c8 npm test',
2997
2826
  ]);
2998
2827
  output.writeln();
2999
- output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, or .nyc_output/out.json'));
2828
+ output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, coverage/coverage-final.json, or .nyc_output/out.json'));
3000
2829
  return { success: false, exitCode: 1 };
3001
2830
  }
3002
2831
  }
@@ -3187,7 +3016,7 @@ const coverageSuggestCommand = {
3187
3016
  'c8: npx c8 npm test',
3188
3017
  ]);
3189
3018
  output.writeln();
3190
- output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, or .nyc_output/out.json'));
3019
+ output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, coverage/coverage-final.json, or .nyc_output/out.json'));
3191
3020
  return { success: false, exitCode: 1 };
3192
3021
  }
3193
3022
  }
@@ -3407,7 +3236,7 @@ const coverageGapsCommand = {
3407
3236
  'c8: npx c8 npm test',
3408
3237
  ]);
3409
3238
  output.writeln();
3410
- output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, or .nyc_output/out.json'));
3239
+ output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, coverage/coverage-final.json, or .nyc_output/out.json'));
3411
3240
  return { success: false, exitCode: 1 };
3412
3241
  }
3413
3242
  }
@@ -9,7 +9,7 @@
9
9
  * a one-line hint and exit 2, unless --plain asks for one frame as plain text and exit 0 (ADR-489 decision E).
10
10
  *
11
11
  * npx ruflo tui # interactive, in a terminal
12
- * npx ruflo tui --view swarm # start on the Swarm page (keys 1-5 or o c s e m switch pages)
12
+ * npx ruflo tui --view swarm # start on the Swarm page (keys 1-6 or o c s e m a switch pages)
13
13
  * npx ruflo tui --plain | head # one plain frame, exit 0 (scripts, CI)
14
14
  * npx ruflo tui | cat # not a terminal and no --plain: a hint on stderr, exit 2
15
15
  */
@@ -55,7 +55,7 @@ export const tuiCommand = {
55
55
  ],
56
56
  examples: [
57
57
  { command: 'npx ruflo tui', description: 'Open the console in this terminal' },
58
- { command: 'npx ruflo tui --view swarm', description: 'Start on the Swarm page (keys 1-5 switch pages, tab cycles, q quits)' },
58
+ { command: 'npx ruflo tui --view swarm', description: 'Start on the Swarm page (keys 1-6 switch pages, tab cycles, q quits)' },
59
59
  { command: 'npx ruflo tui --view ai --model openai/gpt-4o-mini --max-spend-usd 0.1', description: 'The AI page on a cheap OpenRouter model, stopping at $0.10 reported (key in $OPENROUTER_API_KEY, never shown)' },
60
60
  { command: 'npx ruflo tui --plain | head -40', description: 'One plain frame, exit 0: for scripts and CI (no terminal needed)' },
61
61
  ],
@@ -14,10 +14,28 @@
14
14
  import { createHash } from 'node:crypto';
15
15
  import { existsSync, mkdirSync, readFileSync, realpathSync } from 'node:fs';
16
16
  import { dirname, isAbsolute, join, resolve } from 'node:path';
17
- import { homedir } from 'node:os';
17
+ import { homedir, userInfo } from 'node:os';
18
18
  const LOG_FILE = 'ledger-anchors.json';
19
19
  const MIRROR_FILE = 'ledger-anchor-head.json';
20
20
  export const anchorLogPath = (projectRoot) => join(resolve(projectRoot), '.claude-flow', 'policy', LOG_FILE);
21
+ /**
22
+ * The home directory trust material lives under: $HOME when it is an absolute path, else the account's home. An empty or relative
23
+ * $HOME (an unset variable in a wrapper script, `HOME=. cmd`) would make join() yield a path relative to the working directory and
24
+ * drop the HMAC key and anchor mirror into the project; that is never wanted, so it falls back to the account home or refuses.
25
+ */
26
+ function trustHome() {
27
+ const home = homedir();
28
+ if (home && isAbsolute(home))
29
+ return home;
30
+ let account = '';
31
+ try {
32
+ account = userInfo().homedir;
33
+ }
34
+ catch { /* no account entry for this uid */ }
35
+ if (account && isAbsolute(account))
36
+ return account;
37
+ throw new Error('policy-trust-home-unresolved: $HOME is empty or relative and this account has no home directory; set HOME to an absolute path');
38
+ }
21
39
  /**
22
40
  * Directory holding per-project trust material (anchor mirror, HMAC key).
23
41
  * Resolved from the environment at call time (#3919): $XDG_CONFIG_HOME when it
@@ -26,7 +44,7 @@ export const anchorLogPath = (projectRoot) => join(resolve(projectRoot), '.claud
26
44
  * user who already has trust state under the legacy ~/.config keeps using it.
27
45
  */
28
46
  export function policyTrustRoot() {
29
- const legacy = join(homedir(), '.config', 'ruflo', 'policy-trust');
47
+ const legacy = join(trustHome(), '.config', 'ruflo', 'policy-trust');
30
48
  const xdg = process.env.XDG_CONFIG_HOME;
31
49
  if (!xdg || !isAbsolute(xdg))
32
50
  return legacy;
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.56.7",
3
+ "version": "3.56.8",
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",
@@ -104,7 +104,7 @@
104
104
  "@claude-flow/neural": "3.0.0",
105
105
  "@claude-flow/plugin-agent-federation": "1.0.1",
106
106
  "@claude-flow/security": "3.0.4",
107
- "@claude-flow/shared": "3.0.3",
107
+ "@claude-flow/shared": "3.0.4",
108
108
  "@iarna/toml": "^2.2.5",
109
109
  "@noble/ed25519": "2.3.0",
110
110
  "@ruvector/rabitq-wasm": "0.1.0",