@claude-flow/cli 3.56.7 → 3.56.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.claude/helpers/helpers.manifest.json +3 -3
  2. package/.claude/helpers/hook-handler.cjs +109 -23
  3. package/README.md +9 -3
  4. package/catalog-manifest.json +2 -2
  5. package/dist/src/commands/analyze.js +40 -29
  6. package/dist/src/commands/coverage-reader.d.ts +29 -0
  7. package/dist/src/commands/coverage-reader.js +247 -0
  8. package/dist/src/commands/hooks.js +9 -180
  9. package/dist/src/commands/tui.js +2 -2
  10. package/dist/src/init/helpers-generator.js +109 -23
  11. package/dist/src/parser.js +32 -22
  12. package/dist/src/production/monitoring.d.ts +1 -0
  13. package/dist/src/production/monitoring.js +19 -6
  14. package/dist/src/production/rate-limiter.js +5 -4
  15. package/dist/src/production/retry.js +3 -0
  16. package/dist/src/services/config-file-manager.js +3 -3
  17. package/dist/src/services/policy-ledger-anchor.js +20 -2
  18. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  19. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +1 -0
  20. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
  21. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +3 -1
  22. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
  23. package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
  24. package/node_modules/@claude-flow/codex/dist/index.js +1 -1
  25. package/node_modules/@claude-flow/codex/package.json +1 -1
  26. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  27. package/node_modules/@claude-flow/security/dist/policy/envelope.js +6 -6
  28. package/node_modules/@claude-flow/security/dist/policy/envelope.js.map +1 -1
  29. package/node_modules/@claude-flow/security/package.json +1 -1
  30. package/package.json +5 -5
  31. package/plugins/ruflo-console/dist/tui.mjs +171 -162
  32. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -1
  33. package/plugins/ruflo-metaharness/CHANGELOG.md +3 -0
  34. package/plugins/ruflo-metaharness/hooks/screen.ts +10 -3
  35. package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -4
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.56.7",
3
+ "version": "3.56.9",
4
4
  "files": {
5
5
  "auto-memory-hook.mjs": "631fac9d518835e2852a406888316bbbddddb49e7ed8e406d2e971f46833df9a",
6
- "hook-handler.cjs": "0f855c667b8d2d04cb4fe91b14b351d14c3dfb30516dcd9d7ae156acbe0a689d",
6
+ "hook-handler.cjs": "bc12b3cae92d5a7c8deef699fa00d0ef07b57625c18dbeac4387f15725b8e09f",
7
7
  "intelligence.cjs": "f486be296eb2f0518f34b8f4453d9d0a988845e4aec6d18d8d47725aca6dd38b",
8
8
  "statusline.cjs": "d72417ecb38634011a19f095a30e07b9929e6f97663520172ece9be7491b2eaa",
9
9
  "router.cjs": "16648b3c0d73b6681ac2d8d13def6a68286c57f5a11004de5b19a0fa088c833d"
10
10
  }
11
11
  },
12
- "signature": "15NFpUZ8+naP4v/xuCBCl7IZ//YJPXQYotOg48V/Xw/uyCY1FXYeeL4a3Rwa+Bji0+CV4FRiZg61N6Zt+EStBg==",
12
+ "signature": "1qMXitpeXl6mUlSGGx2dT68gaou7Uf1Z6YS+eT1K/3h+B//G2GqbgEmHhdfa5mKmcxM6eml2KlsUgMMW/WknAA==",
13
13
  "algorithm": "ed25519"
14
14
  }
@@ -682,7 +682,8 @@ const handlers = {
682
682
  const dangerous = ['rm -rf /', 'format c:', 'del /s /q c:\\', ':(){:|:&};:'];
683
683
  // Join literal quote fragments and normalize root paths without shell evaluation.
684
684
  function hasRootDelete(command, depth = 0) {
685
- let word = '', quote = '', started = false, redirect = false
685
+ // mask has one letter per character of word: 'u' unquoted, 'q' quoted or escaped (only unquoted braces expand).
686
+ let word = '', mask = '', quote = '', started = false, redirect = false
686
687
  let ansiNul = false // a NUL inside $'...' ends that string, as in bash
687
688
  let inRm = false, optionsEnded = false, recursive = false, force = false, root = false
688
689
  const isRoot = (operand) => {
@@ -712,23 +713,92 @@ const handlers = {
712
713
  if (c === 'c' && at + 1 < s.length) return [String.fromCharCode(s.charCodeAt(at + 1) & 31), at + 1]
713
714
  return ['\\' + c, at]
714
715
  }
715
- const finishWord = () => {
716
- if (!started) return false
717
- // Literal shell strings (e.g. sh -c 'rm -rf /') also carried the old guard.
718
- // Bound rescanning to four levels; beyond that retain its conservative check.
719
- if (/[\s;&|()\x60]/.test(word)) {
720
- if (depth < 4 ? hasRootDelete(word, depth + 1) : word.includes('rm -rf /')) return true
716
+ // Past the rescan bound (or the brace budget), a string is judged by its tokens: an rm and a possible root operand
717
+ // anywhere in it deny. Quote characters are dropped first, so r"m" is still rm; a path token with braces may expand to /.
718
+ const looseRootDelete = (text) => {
719
+ const tokens = text.replace(/["'\\]/g, '').split(/[\s;&|()\x60$]+/)
720
+ return tokens.some(t => t === 'rm' || t.endsWith('/rm')) && tokens.some(t => isRoot(t) || (t.includes('/') && /[{}]/.test(t)))
721
+ }
722
+ // Bash brace expansion of one word ({a,b} lists, nested or glued to text), and nothing else: no {1..3} sequences, and
723
+ // nothing inside parameter braces after a dollar sign. Only unquoted braces and commas count. Iterative and bounded:
724
+ // past BRACE_WORDS words or BRACE_CHARS characters of work in this call it returns undefined.
725
+ const BRACE_WORDS = 4096
726
+ let braceChars = 1_000_000
727
+ // The leftmost expandable group: an unquoted {...} with an unquoted comma at its own level, outside any dollar-brace parameter. One pass.
728
+ const firstGroup = (text, marks) => {
729
+ const stack = []
730
+ let dollars = 0
731
+ let best
732
+ for (let k = 0; k < text.length; k++) {
733
+ if (marks[k] !== 'u') continue
734
+ const c = text[k]
735
+ if (c === '{') {
736
+ const dollar = k > 0 && text[k - 1] === '$' && marks[k - 1] === 'u'
737
+ if (dollar) dollars++
738
+ stack.push({ open: k, commas: [], dollar: dollar || dollars > 0 })
739
+ } else if (c === ',' && stack.length > 0) stack[stack.length - 1].commas.push(k)
740
+ else if (c === '}' && stack.length > 0) {
741
+ const top = stack.pop()
742
+ if (top.dollar && text[top.open - 1] === '$' && marks[top.open - 1] === 'u') dollars--
743
+ if (!top.dollar && top.commas.length > 0 && (best === undefined || top.open < best[0])) best = [top.open, top.commas, k]
744
+ }
745
+ }
746
+ return best
747
+ }
748
+ const braceExpand = (text, marks) => {
749
+ const out = []
750
+ const todo = [[text, marks]]
751
+ while (todo.length > 0) {
752
+ const [t, m] = todo.pop()
753
+ if ((braceChars -= t.length) < 0) return undefined
754
+ const group = firstGroup(t, m)
755
+ if (group === undefined) {
756
+ out.push(t)
757
+ if (out.length > BRACE_WORDS) return undefined
758
+ continue
759
+ }
760
+ const [open, commas, close] = group
761
+ const cuts = [open, ...commas, close]
762
+ // Pushed in reverse, so the words come out in bash's left-to-right order.
763
+ for (let c = cuts.length - 2; c >= 0; c--) {
764
+ const from = cuts[c] + 1, to = cuts[c + 1]
765
+ todo.push([t.slice(0, open) + t.slice(from, to) + t.slice(close + 1), m.slice(0, open) + m.slice(from, to) + m.slice(close + 1)])
766
+ }
767
+ if (todo.length > BRACE_WORDS) return undefined
721
768
  }
722
- if (!inRm) inRm = word === 'rm' || word.endsWith('/rm')
723
- else if (!optionsEnded && word === '--') optionsEnded = true
724
- else if (!optionsEnded && word.startsWith('-')) {
769
+ return out
770
+ }
771
+ const takeWord = (w) => {
772
+ if (!inRm) inRm = w === 'rm' || w.endsWith('/rm')
773
+ else if (!optionsEnded && w === '--') optionsEnded = true
774
+ else if (!optionsEnded && w.startsWith('-')) {
725
775
  // GNU getopt accepts any unambiguous long-option prefix: --r, --recur, --forc.
726
- recursive = recursive || (word.length > 2 && '--recursive'.startsWith(word)) || /^-[a-z]*r[a-z]*$/.test(word)
727
- force = force || (word.length > 2 && '--force'.startsWith(word)) || /^-[a-z]*f[a-z]*$/.test(word)
728
- } else root = root || isRoot(word)
729
- word = ''; started = false
776
+ recursive = recursive || (w.length > 2 && '--recursive'.startsWith(w)) || (/^-[a-z]*$/.test(w) && w.includes('r'))
777
+ force = force || (w.length > 2 && '--force'.startsWith(w)) || (/^-[a-z]*$/.test(w) && w.includes('f'))
778
+ } else root = root || isRoot(w)
730
779
  return inRm && recursive && force && root
731
780
  }
781
+ const finishWord = () => {
782
+ if (!started) return false
783
+ const text = word, marks = mask
784
+ word = ''; mask = ''; started = false
785
+ // Literal shell strings (e.g. sh -c 'rm -rf /', or sh -c '{rm,-rf,/}') also carried the old guard.
786
+ // Bound rescanning to four levels; beyond that judge the string by its tokens.
787
+ if (/[\s;&|()\x60{]/.test(text)) {
788
+ if (depth < 4 ? hasRootDelete(text, depth + 1) : looseRootDelete(text)) return true
789
+ }
790
+ // An unquoted {a,b} becomes separate words, as bash expands it: rm -rf {/*,} is rm -rf /*.
791
+ const words = text.includes('{') ? braceExpand(text, marks) : [text]
792
+ if (words === undefined) {
793
+ // Too many alternatives to check: one of them may be the root (an rm in progress fails closed), or the
794
+ // list itself may spell the rm: judge its tokens.
795
+ if (inRm) root = true
796
+ if (looseRootDelete(text.replace(/[{},]/g, ' '))) return true
797
+ return takeWord(text)
798
+ }
799
+ for (const w of words) if (w !== '' && takeWord(w)) return true
800
+ return false
801
+ }
732
802
  // A command substitution's body, from just after its opening backtick. Bash removes a
733
803
  // backslash only before $, a backtick or a backslash (and " inside double quotes) and
734
804
  // drops backslash-newline; any other backslash stays for the nested scan.
@@ -742,7 +812,7 @@ const handlers = {
742
812
  }
743
813
  body += command[j]
744
814
  }
745
- return [j, depth < 4 ? hasRootDelete(body, depth + 1) : body.includes('rm -rf /')]
815
+ return [j, depth < 4 ? hasRootDelete(body, depth + 1) : looseRootDelete(body)]
746
816
  }
747
817
  const finishCommand = () => {
748
818
  const denied = inRm && recursive && force && root
@@ -759,8 +829,8 @@ const handlers = {
759
829
  else if (char === '\\' && i + 1 < command.length) {
760
830
  const [text, last] = ansiEscape(command, i + 1)
761
831
  i = last
762
- if (!ansiNul) { const nul = text.indexOf('\0'); if (nul < 0) word += text; else { word += text.slice(0, nul); ansiNul = true } }
763
- } else if (!ansiNul) word += char
832
+ if (!ansiNul) { const nul = text.indexOf('\0'); const kept = nul < 0 ? text : text.slice(0, nul); word += kept; mask += 'q'.repeat(kept.length); if (nul >= 0) ansiNul = true }
833
+ } else if (!ansiNul) { word += char; mask += 'q' }
764
834
  continue
765
835
  }
766
836
  if (char === quote) quote = ''
@@ -770,20 +840,20 @@ const handlers = {
770
840
  // The substitution is gone from the word, as bash's empty output is (a quoted empty substitution glued to -rf leaves -rf).
771
841
  // A # right after it is still inside the word: a placeholder keeps a rescan (sh -c "...")
772
842
  // from reading it as the start of a comment.
773
- if (command[end + 1] === '#') word += '\u0001'
843
+ if (command[end + 1] === '#') { word += '\u0001'; mask += 'q' }
774
844
  i = end
775
845
  }
776
846
  else if (quote === '"' && char === '\\' && i + 1 < command.length &&
777
847
  (command[i + 1] === '"' || command[i + 1] === '\\' || command[i + 1] === '$' ||
778
848
  command.charCodeAt(i + 1) === 96 || command[i + 1] === '\n')) {
779
849
  const next = command[++i]
780
- if (next !== '\n') word += next
781
- } else word += char
850
+ if (next !== '\n') { word += next; mask += 'q' }
851
+ } else { word += char; mask += 'q' }
782
852
  continue
783
853
  }
784
854
  if (char === '\\' && i + 1 < command.length) {
785
855
  const next = command[++i]
786
- if (next !== '\n') { word += next; started = true }
856
+ if (next !== '\n') { word += next; mask += 'q'; started = true }
787
857
  } else if (char === '\x60' && command.indexOf('\x60', i + 1) > i) {
788
858
  // Command substitution: scan the body as its own command; the substitution
789
859
  // stays inside the enclosing word, so the rm being parsed keeps its state,
@@ -806,13 +876,29 @@ const handlers = {
806
876
  redirect = char === '<' || char === '>'
807
877
  if (!redirectionAmpersand && (char === '\n' || ';|&()'.includes(char)) && finishCommand()) return true
808
878
  } else {
809
- word += char; started = true
879
+ word += char; mask += 'u'; started = true
810
880
  }
811
881
  }
812
882
  return finishWord() || finishCommand()
813
883
  }
884
+
885
+ // A fork bomb's definition as bash reads it, whatever the spacing or the name: f(){ f|f& }, function f { f|f& },
886
+ // function f(){ f|f& }. Defining one is the danger: the call may come anywhere later.
887
+ function hasForkBomb(command) {
888
+ const s = command.replace(/\s+/g, '')
889
+ for (let brace = s.indexOf('{'); brace >= 0; brace = s.indexOf('{', brace + 1)) {
890
+ const end = s.startsWith('()', brace - 2) ? brace - 2 : brace
891
+ let start = end
892
+ while (start > 0 && end - start < 72 && /[a-z0-9_:.-]/.test(s[start - 1])) start--
893
+ const run = s.slice(start, end)
894
+ for (const name of run.startsWith('function') && run.length > 8 ? [run, run.slice(8)] : [run]) {
895
+ if (name && s.startsWith(name + '|' + name + '&}', brace + 1)) return true
896
+ }
897
+ }
898
+ return false
899
+ }
814
900
  for (const d of dangerous) {
815
- if (d === 'rm -rf /' ? hasRootDelete(cmd) : cmd.includes(d)) {
901
+ if (d === 'rm -rf /' ? hasRootDelete(cmd) : d === ':(){:|:&};:' ? hasForkBomb(cmd) : cmd.includes(d)) {
816
902
  console.error(`[BLOCKED] Dangerous command detected: ${d}`);
817
903
  // Claude Code PreToolUse: exit 2 blocks execution; exit 1 is non-blocking.
818
904
  process.exit(2);
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-12T21:03:06.737Z",
5
+ "gitSha": "55e2c901",
6
6
  "catalog": {
7
7
  "agents": 176,
8
8
  "tools": 423,
@@ -97,11 +97,19 @@ const diffCommand = {
97
97
  output.printInfo(`Analyzing diff: ${output.highlight(ref)}`);
98
98
  try {
99
99
  // Call MCP tool for diff analysis
100
+ // Shape returned by the analyze_diff MCP tool (ruvector/diff-classifier.ts).
101
+ // Typed from the classifier itself so the renderer cannot drift from it again.
100
102
  const result = await callMCPTool('analyze_diff', {
101
103
  ref,
102
- includeFileRisks: verbose,
104
+ // Always request file risks: the handler computes them anyway, and the
105
+ // high-risk file list below is derived from them.
106
+ includeFileRisks: true,
103
107
  includeReviewers: showReviewers || showAll,
104
108
  });
109
+ if (result.error) {
110
+ output.printError(`Diff analysis failed: ${result.message ?? 'unknown error'}`);
111
+ return { success: false, exitCode: 1 };
112
+ }
105
113
  // JSON output
106
114
  if (formatType === 'json') {
107
115
  output.printJson(result);
@@ -110,13 +118,18 @@ const diffCommand = {
110
118
  output.writeln();
111
119
  // Summary box
112
120
  const files = result.files || [];
113
- const risk = result.risk || { overall: 'unknown', score: 0, breakdown: { fileCount: 0, totalChanges: 0, highRiskFiles: [], securityConcerns: [], breakingChanges: [], testCoverage: 'unknown' } };
114
- const classification = result.classification || { category: 'unknown', confidence: 0, reasoning: '' };
121
+ const fileRisks = result.fileRisks || [];
122
+ const risk = result.risk || { overall: 'low', score: 0, breakdown: { low: 0, medium: 0, high: 0, critical: 0 } };
123
+ const classification = result.classification;
124
+ const category = classification?.primary ?? 'unknown';
125
+ const secondary = classification?.secondary?.length ? classification.secondary.join(', ') : '';
126
+ const totalChanges = files.reduce((sum, f) => sum + f.additions + f.deletions, 0);
127
+ const highRiskFiles = fileRisks.filter((r) => r.risk === 'high' || r.risk === 'critical').map((r) => r.file);
115
128
  output.printBox([
116
129
  `Ref: ${result.ref || 'HEAD'}`,
117
130
  `Files: ${files.length}`,
118
131
  `Risk: ${getRiskDisplay(risk.overall)} (${risk.score}/100)`,
119
- `Type: ${classification.category}${classification.subcategory ? ` (${classification.subcategory})` : ''}`,
132
+ `Type: ${category}${secondary ? ` (${secondary})` : ''}`,
120
133
  ``,
121
134
  result.summary || 'No summary available',
122
135
  ].join('\n'), 'Diff Analysis');
@@ -128,33 +141,28 @@ const diffCommand = {
128
141
  output.printTable({
129
142
  columns: [
130
143
  { key: 'metric', header: 'Metric', width: 25 },
131
- { key: 'value', header: 'Value', width: 30 },
144
+ { key: 'value', header: 'Value', width: 42 },
132
145
  ],
133
146
  data: [
134
147
  { metric: 'Overall Risk', value: getRiskDisplay(risk.overall) },
135
148
  { metric: 'Risk Score', value: `${risk.score}/100` },
136
- { metric: 'Files Changed', value: risk.breakdown.fileCount },
137
- { metric: 'Total Lines Changed', value: risk.breakdown.totalChanges },
138
- { metric: 'Test Coverage', value: risk.breakdown.testCoverage },
149
+ { metric: 'Files Changed', value: files.length },
150
+ { metric: 'Total Lines Changed', value: totalChanges },
151
+ { metric: 'Files by Risk', value: `low ${risk.breakdown.low} / medium ${risk.breakdown.medium} / high ${risk.breakdown.high} / critical ${risk.breakdown.critical}` },
139
152
  ],
140
153
  });
141
- // Security concerns
142
- if (risk.breakdown.securityConcerns.length > 0) {
143
- output.writeln();
144
- output.writeln(output.bold(output.warning('Security Concerns')));
145
- output.printList(risk.breakdown.securityConcerns.map(c => output.warning(c)));
146
- }
147
- // Breaking changes
148
- if (risk.breakdown.breakingChanges.length > 0) {
154
+ // Risk factors reported by the classifier
155
+ const riskFactors = classification?.riskFactors ?? [];
156
+ if (riskFactors.length > 0) {
149
157
  output.writeln();
150
- output.writeln(output.bold(output.error('Potential Breaking Changes')));
151
- output.printList(risk.breakdown.breakingChanges.map(c => output.error(c)));
158
+ output.writeln(output.bold(output.warning('Risk Factors')));
159
+ output.printList(riskFactors.map(c => output.warning(c)));
152
160
  }
153
161
  // High risk files
154
- if (risk.breakdown.highRiskFiles.length > 0) {
162
+ if (highRiskFiles.length > 0) {
155
163
  output.writeln();
156
164
  output.writeln(output.bold('High Risk Files'));
157
- output.printList(risk.breakdown.highRiskFiles.map(f => output.warning(f)));
165
+ output.printList(highRiskFiles.map(f => output.warning(f)));
158
166
  }
159
167
  }
160
168
  // Classification
@@ -168,13 +176,16 @@ const diffCommand = {
168
176
  { key: 'value', header: 'Value', width: 40 },
169
177
  ],
170
178
  data: [
171
- { field: 'Category', value: classification.category },
172
- { field: 'Subcategory', value: classification.subcategory || '-' },
173
- { field: 'Confidence', value: `${(classification.confidence * 100).toFixed(0)}%` },
179
+ { field: 'Category', value: category },
180
+ { field: 'Secondary', value: secondary || '-' },
181
+ { field: 'Confidence', value: `${((classification?.confidence ?? 0) * 100).toFixed(0)}%` },
182
+ { field: 'Impact', value: classification?.impactLevel ?? '-' },
174
183
  ],
175
184
  });
176
- output.writeln();
177
- output.writeln(output.dim(`Reasoning: ${classification.reasoning}`));
185
+ if (classification?.testingStrategy?.length) {
186
+ output.writeln();
187
+ output.writeln(output.dim(`Testing strategy: ${classification.testingStrategy.join(', ')}`));
188
+ }
178
189
  }
179
190
  // Reviewers
180
191
  if (showReviewers || showAll) {
@@ -190,13 +201,13 @@ const diffCommand = {
190
201
  }
191
202
  }
192
203
  // Verbose file-level details
193
- if (verbose && result.fileRisks) {
204
+ if (verbose && fileRisks.length > 0) {
194
205
  output.writeln();
195
206
  output.writeln(output.bold('File-Level Analysis'));
196
207
  output.writeln(output.dim('-'.repeat(50)));
197
208
  output.printTable({
198
209
  columns: [
199
- { key: 'path', header: 'File', width: 40 },
210
+ { key: 'file', header: 'File', width: 40 },
200
211
  { key: 'risk', header: 'Risk', width: 12, format: (v) => getRiskDisplay(String(v)) },
201
212
  { key: 'score', header: 'Score', width: 8, align: 'right' },
202
213
  { key: 'reasons', header: 'Reasons', width: 30, format: (v) => {
@@ -204,7 +215,7 @@ const diffCommand = {
204
215
  return reasons.slice(0, 2).join('; ');
205
216
  } },
206
217
  ],
207
- data: result.fileRisks,
218
+ data: fileRisks.map((r) => ({ ...r })),
208
219
  });
209
220
  }
210
221
  // Files changed table
@@ -219,7 +230,7 @@ const diffCommand = {
219
230
  { key: 'additions', header: '+', width: 8, align: 'right', format: (v) => output.success(`+${v}`) },
220
231
  { key: 'deletions', header: '-', width: 8, align: 'right', format: (v) => output.error(`-${v}`) },
221
232
  ],
222
- data: files.slice(0, 20),
233
+ data: files.slice(0, 20).map((f) => ({ ...f })),
223
234
  });
224
235
  if (files.length > 20) {
225
236
  output.writeln(output.dim(` ... and ${files.length - 20} more files`));
@@ -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