swarmdo 1.15.0 → 1.16.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.
@@ -515,7 +515,7 @@ function getCostFromStdin() {
515
515
  // misses, the displayed version is meaningful (matches what the user
516
516
  // installed), not a stale hard-coded string.
517
517
  function getPkgVersion() {
518
- let ver = '1.15.0';
518
+ let ver = '1.16.0';
519
519
  try {
520
520
  const home = os.homedir();
521
521
  const pkgPaths = [
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![swarmdo](swarmdo/assets/brand/logo-full.svg)](https://swarmdo.com)
4
4
 
5
- [![npm version (swarmdo)](https://img.shields.io/badge/npx%20swarmdo-v1.15.0-cb3837?style=for-the-badge&logo=npm&logoColor=white)](https://github.com/SwarmDo/swarmdo/releases)
5
+ [![npm version (swarmdo)](https://img.shields.io/badge/npx%20swarmdo-v1.16.0-cb3837?style=for-the-badge&logo=npm&logoColor=white)](https://github.com/SwarmDo/swarmdo/releases)
6
6
  [![MIT License](https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge)](https://github.com/SwarmDo/swarmdo/blob/main/LICENSE)
7
7
  [![Website](https://img.shields.io/badge/swarmdo.com-e2a33c?style=for-the-badge&logoColor=black)](https://swarmdo.com)
8
8
  [![Star on GitHub](https://img.shields.io/github/stars/SwarmDo/swarmdo?style=for-the-badge&logo=github&color=gold)](https://github.com/SwarmDo/swarmdo)
@@ -284,6 +284,7 @@ The recent release train added a full day-to-day operations layer around the swa
284
284
  | `swarmdo license` | **Audit dependency licenses** — walk `node_modules`, resolve each package's SPDX license, and gate on an allow/deny policy so a GPL or unknown license can't slip into a permissive tree. `--allow MIT,Apache-2.0 --ci` fails the build; distinct from `security` (CVEs). Also an MCP tool (`license_check`). Deterministic |
285
285
  | `swarmdo sbom` | **Software Bill of Materials from the lockfile** — emit a CycloneDX (default) or SPDX JSON manifest of every dependency with version, purl, license, and integrity hash, for compliance/vuln tooling. `--spec spdx`, `-o sbom.json`, `--production`. Completes the env/license/sbom supply-chain trio. Deterministic |
286
286
  | `swarmdo apply` | **A forgiving `git apply`** — apply a unified diff with fuzzy context matching, so an agent's patch lands even when line numbers have drifted or a context line is slightly off, and reports exactly which hunks couldn't. `--dry-run` to preview, `--fuzz N` to tolerate more drift. Also an MCP tool (`apply_patch`). Deterministic |
287
+ | `swarmdo hotspots` | **Change-risk hotspots from git history** — rank files by churn × recency × author-spread to surface the technical debt worth refactoring or testing, answered from data instead of a guess. `--since 90d`, `--by risk\|churn\|commits\|authors`, `--top N`, `--format json`. Pairs with `codegraph`; also an MCP tool (`hotspots`). Deterministic |
287
288
  | `swarmdo integrations` (alias `integrate`) | **Use swarmdo from Codex CLI, GitHub Copilot CLI, and pi** — one command wires AGENTS.md + each CLI's MCP config (idempotent, dry-run first, never touches your Claude Code setup) |
288
289
  | OpenRouter model pool | **Let swarms pick from any models you configure** — declare tier-mapped OpenRouter models in `swarmdo.config.json`; the router Thompson-samples among them per task and the execution layer dispatches the winner |
289
290
  | `swarmdo changelog` (alias `notes`) | **Release notes from conventional commits** — `--out NOTES.md` feeds `gh release create --notes-file` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swarmdo",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "Swarmdo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -32,7 +32,10 @@ async function run(ctx) {
32
32
  const root = ctx.cwd || process.cwd();
33
33
  const dryRun = ctx.flags['dry-run'] === true;
34
34
  const partial = ctx.flags.partial === true;
35
- const fuzz = typeof ctx.flags.fuzz === 'string' ? parseInt(ctx.flags.fuzz, 10) : 2;
35
+ // The parser may deliver --fuzz as a number or a string; accept both.
36
+ const fuzz = typeof ctx.flags.fuzz === 'number' ? ctx.flags.fuzz
37
+ : typeof ctx.flags.fuzz === 'string' ? parseInt(ctx.flags.fuzz, 10)
38
+ : 2;
36
39
  // Read the patch: a file arg, or stdin.
37
40
  let patchText;
38
41
  if (ctx.args[0]) {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `swarmdo hotspots` — rank files by change-risk mined from git history.
3
+ *
4
+ * swarmdo hotspots # top risk files in the repo
5
+ * swarmdo hotspots src --since 90d # scope to a path + window
6
+ * swarmdo hotspots --by churn --top 10 # sort/limit
7
+ * swarmdo hotspots --format json # machine-readable
8
+ *
9
+ * "Where is the technical debt?" answered from data: files that change often,
10
+ * churn heavily, are touched by many hands, and were edited recently. Pairs
11
+ * with `codegraph`. Engine (../hotspots/hotspots.ts) is pure + tested; this
12
+ * captures the git log.
13
+ */
14
+ import type { Command } from '../types.js';
15
+ export declare const hotspotsCommand: Command;
16
+ export default hotspotsCommand;
17
+ //# sourceMappingURL=hotspots.d.ts.map
@@ -0,0 +1,80 @@
1
+ /**
2
+ * `swarmdo hotspots` — rank files by change-risk mined from git history.
3
+ *
4
+ * swarmdo hotspots # top risk files in the repo
5
+ * swarmdo hotspots src --since 90d # scope to a path + window
6
+ * swarmdo hotspots --by churn --top 10 # sort/limit
7
+ * swarmdo hotspots --format json # machine-readable
8
+ *
9
+ * "Where is the technical debt?" answered from data: files that change often,
10
+ * churn heavily, are touched by many hands, and were edited recently. Pairs
11
+ * with `codegraph`. Engine (../hotspots/hotspots.ts) is pure + tested; this
12
+ * captures the git log.
13
+ */
14
+ import { execFileSync } from 'node:child_process';
15
+ import { output } from '../output.js';
16
+ import { parseGitLog, computeHotspots, formatHotspots } from '../hotspots/hotspots.js';
17
+ const SORT_KEYS = ['risk', 'churn', 'commits', 'authors'];
18
+ /** Read a numeric flag that the parser may deliver as a number OR a string. */
19
+ function numFlag(v, def) {
20
+ const n = typeof v === 'number' ? v : typeof v === 'string' ? parseInt(v, 10) : NaN;
21
+ return Number.isFinite(n) ? n : def;
22
+ }
23
+ async function run(ctx) {
24
+ const root = ctx.cwd || process.cwd();
25
+ const pathArg = ctx.args[0];
26
+ const since = typeof ctx.flags.since === 'string' ? ctx.flags.since : '1 year ago';
27
+ const top = numFlag(ctx.flags.top, 20);
28
+ const minCommits = numFlag(ctx.flags['min-commits'], 1);
29
+ const by = (typeof ctx.flags.by === 'string' ? ctx.flags.by : 'risk');
30
+ if (!SORT_KEYS.includes(by)) {
31
+ output.printError(`unknown --by '${by}' (use ${SORT_KEYS.join('|')})`);
32
+ return { success: false, exitCode: 1 };
33
+ }
34
+ // Global --format (text|json|table); text and table both render the table.
35
+ const asJson = ctx.flags.format === 'json';
36
+ // Capture history: SOH-delimited header + numstat, no merges.
37
+ const args = ['log', '--no-merges', '--numstat', `--since=${since}`, '--format=format:%x01%H%x1f%an%x1f%aI'];
38
+ if (pathArg)
39
+ args.push('--', pathArg);
40
+ let raw;
41
+ try {
42
+ raw = execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 128 * 1024 * 1024 });
43
+ }
44
+ catch {
45
+ output.printError('git log failed — is this a git repository?');
46
+ return { success: false, exitCode: 1 };
47
+ }
48
+ const now = Date.now();
49
+ const spots = computeHotspots(parseGitLog(raw), now, { by, top: top > 0 ? top : undefined, minCommits });
50
+ if (asJson) {
51
+ process.stdout.write(JSON.stringify({ generated: new Date(now).toISOString(), by, count: spots.length, hotspots: spots }, null, 2) + '\n');
52
+ }
53
+ else {
54
+ if (spots.length === 0) {
55
+ output.writeln(output.dim('no hotspots found — no matching git history in the window'));
56
+ }
57
+ else {
58
+ output.writeln(output.bold(`Change-risk hotspots (by ${by}, since ${since})`));
59
+ output.writeln(formatHotspots(spots, now));
60
+ }
61
+ }
62
+ return { success: true, exitCode: 0 };
63
+ }
64
+ export const hotspotsCommand = {
65
+ name: 'hotspots',
66
+ description: 'Rank files by change-risk mined from git history (churn × recency × author-spread) — find the technical debt worth refactoring or testing',
67
+ options: [
68
+ { name: 'since', description: 'history window, e.g. 90d or "3 months ago" (default 1 year)', type: 'string' },
69
+ { name: 'top', description: 'keep only the top N files (default 20; 0 = all)', type: 'string' },
70
+ { name: 'min-commits', description: 'drop files with fewer than N commits (default 1)', type: 'string' },
71
+ { name: 'by', description: `sort key: ${SORT_KEYS.join('|')} (default risk)`, type: 'string' },
72
+ ],
73
+ examples: [
74
+ { command: 'swarmdo hotspots src --since 90d', description: 'Risk hotspots under src/ in the last 90 days' },
75
+ { command: 'swarmdo hotspots --by churn --top 10 --format json', description: 'Top-10 by churn as JSON' },
76
+ ],
77
+ action: run,
78
+ };
79
+ export default hotspotsCommand;
80
+ //# sourceMappingURL=hotspots.js.map
@@ -41,6 +41,9 @@ const commandLoaders = {
41
41
  // Fuzzy unified-diff applier (agent-diff pain) — a forgiving `git apply` that
42
42
  // lands hunks despite context drift and reports what it couldn't.
43
43
  apply: () => import('./apply.js'),
44
+ // Change-risk hotspots from git history (code-maat demand) — rank files by
45
+ // churn × recency × author-spread; "where is the tech debt?" from data.
46
+ hotspots: () => import('./hotspots.js'),
44
47
  // Queryable exported-symbol index (codegraph demand) — where things are
45
48
  // defined without grep+read round-trips.
46
49
  codegraph: () => import('./codegraph.js'),
@@ -274,7 +277,7 @@ export async function getCommandsByCategory() {
274
277
  // three slots from 'statusline' onward (completionsCmd received the
275
278
  // statusline module, analyzeCmd received completions, …), scrambling the
276
279
  // categorized help. Every load now has a named slot.
277
- const [daemonCmd, doctorCmd, embeddingsCmd, neuralCmd, performanceCmd, securityCmd, swarmvectorCmd, hiveMindCmd, configCmd, statuslineCmd, compressCmd, efficiencyCmd, completionsCmd, migrateCmd, workflowCmd, analyzeCmd, routeCmd, progressCmd, providersCmd, pluginsCmd, deploymentCmd, claimsCmd, issuesCmd, updateCmd, processCmd, guidanceCmd, applianceCmd, cleanupCmd, autopilotCmd, demoCmd, usageCmd, repairCmd, hudCmd, compactCmd, codegraphCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd, applyCmd,] = await Promise.all([
280
+ const [daemonCmd, doctorCmd, embeddingsCmd, neuralCmd, performanceCmd, securityCmd, swarmvectorCmd, hiveMindCmd, configCmd, statuslineCmd, compressCmd, efficiencyCmd, completionsCmd, migrateCmd, workflowCmd, analyzeCmd, routeCmd, progressCmd, providersCmd, pluginsCmd, deploymentCmd, claimsCmd, issuesCmd, updateCmd, processCmd, guidanceCmd, applianceCmd, cleanupCmd, autopilotCmd, demoCmd, usageCmd, repairCmd, hudCmd, compactCmd, codegraphCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd, applyCmd, hotspotsCmd,] = await Promise.all([
278
281
  loadCommand('daemon'), loadCommand('doctor'), loadCommand('embeddings'), loadCommand('neural'),
279
282
  loadCommand('performance'), loadCommand('security'), loadCommand('swarmvector'), loadCommand('hive-mind'),
280
283
  loadCommand('config'), loadCommand('statusline'), loadCommand('compress'), loadCommand('efficiency'),
@@ -282,7 +285,7 @@ export async function getCommandsByCategory() {
282
285
  loadCommand('analyze'), loadCommand('route'), loadCommand('progress'), loadCommand('providers'),
283
286
  loadCommand('plugins'), loadCommand('deployment'), loadCommand('claims'), loadCommand('issues'),
284
287
  loadCommand('update'), loadCommand('process'), loadCommand('guidance'), loadCommand('appliance'),
285
- loadCommand('cleanup'), loadCommand('autopilot'), loadCommand('demo'), loadCommand('usage'), loadCommand('repair'), loadCommand('hud'), loadCommand('compact'), loadCommand('codegraph'), loadCommand('redact'), loadCommand('pack'), loadCommand('env'), loadCommand('license'), loadCommand('sbom'), loadCommand('apply'),
288
+ loadCommand('cleanup'), loadCommand('autopilot'), loadCommand('demo'), loadCommand('usage'), loadCommand('repair'), loadCommand('hud'), loadCommand('compact'), loadCommand('codegraph'), loadCommand('redact'), loadCommand('pack'), loadCommand('env'), loadCommand('license'), loadCommand('sbom'), loadCommand('apply'), loadCommand('hotspots'),
286
289
  ]);
287
290
  return {
288
291
  primary: [
@@ -298,7 +301,7 @@ export async function getCommandsByCategory() {
298
301
  utility: [
299
302
  configCmd, doctorCmd, daemonCmd, completionsCmd,
300
303
  migrateCmd, workflowCmd, demoCmd,
301
- statuslineCmd, compressCmd, compactCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd, applyCmd, efficiencyCmd,
304
+ statuslineCmd, compressCmd, compactCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd, applyCmd, hotspotsCmd, efficiencyCmd,
302
305
  ].filter(Boolean),
303
306
  analysis: [
304
307
  analyzeCmd, routeCmd, progressCmd, usageCmd, hudCmd, codegraphCmd,
@@ -0,0 +1,54 @@
1
+ /**
2
+ * hotspots.ts — rank files by CHANGE-RISK mined from git history.
3
+ *
4
+ * The "where is the technical debt?" query, answered from data instead of a
5
+ * guess: files that change often, are churned heavily, are touched by many
6
+ * hands, and were edited recently are the ones most worth refactoring, testing,
7
+ * or reviewing carefully. Inspired by code-maat / "Your Code as a Crime Scene".
8
+ *
9
+ * Pure + deterministic: the scoring core takes the raw `git log --numstat`
10
+ * string (+ a fixed `now` for recency) and folds it into a ranked list. No LLM,
11
+ * no network — the git subprocess lives in ../commands/hotspots.ts, so this
12
+ * module is fully fixture-testable.
13
+ *
14
+ * Expected log format (control-char delimited to avoid collisions):
15
+ * <SOH>%H<US>%an<US>%aI ← one header line per commit
16
+ * <added>\t<deleted>\t<path> ← one numstat line per file ('-' for binary)
17
+ */
18
+ export interface CommitFile {
19
+ path: string;
20
+ added: number;
21
+ deleted: number;
22
+ }
23
+ export interface Commit {
24
+ hash: string;
25
+ author: string;
26
+ /** epoch milliseconds, parsed from the ISO author date */
27
+ date: number;
28
+ files: CommitFile[];
29
+ }
30
+ export interface FileHotspot {
31
+ path: string;
32
+ commits: number;
33
+ churn: number;
34
+ authors: number;
35
+ lastTouched: number;
36
+ firstTouched: number;
37
+ risk: number;
38
+ }
39
+ export type SortKey = 'risk' | 'churn' | 'commits' | 'authors';
40
+ export interface HotspotOptions {
41
+ /** drop files with fewer than this many commits (default 1) */
42
+ minCommits?: number;
43
+ /** sort key (default 'risk') */
44
+ by?: SortKey;
45
+ /** keep only the top N after sorting (default: all) */
46
+ top?: number;
47
+ }
48
+ /** Parse a control-char-delimited `git log --numstat` dump into commits. Pure. */
49
+ export declare function parseGitLog(raw: string): Commit[];
50
+ /** Aggregate commits into per-file hotspots, scored and ranked. Pure. */
51
+ export declare function computeHotspots(commits: Commit[], now: number, opts?: HotspotOptions): FileHotspot[];
52
+ /** Human-readable table. Pure. */
53
+ export declare function formatHotspots(spots: FileHotspot[], now: number): string;
54
+ //# sourceMappingURL=hotspots.d.ts.map
@@ -0,0 +1,122 @@
1
+ /**
2
+ * hotspots.ts — rank files by CHANGE-RISK mined from git history.
3
+ *
4
+ * The "where is the technical debt?" query, answered from data instead of a
5
+ * guess: files that change often, are churned heavily, are touched by many
6
+ * hands, and were edited recently are the ones most worth refactoring, testing,
7
+ * or reviewing carefully. Inspired by code-maat / "Your Code as a Crime Scene".
8
+ *
9
+ * Pure + deterministic: the scoring core takes the raw `git log --numstat`
10
+ * string (+ a fixed `now` for recency) and folds it into a ranked list. No LLM,
11
+ * no network — the git subprocess lives in ../commands/hotspots.ts, so this
12
+ * module is fully fixture-testable.
13
+ *
14
+ * Expected log format (control-char delimited to avoid collisions):
15
+ * <SOH>%H<US>%an<US>%aI ← one header line per commit
16
+ * <added>\t<deleted>\t<path> ← one numstat line per file ('-' for binary)
17
+ */
18
+ const SOH = '\x01'; // start-of-commit marker (git --format=format:%x01...)
19
+ const US = '\x1f'; // field separator (%x1f)
20
+ /** Parse a control-char-delimited `git log --numstat` dump into commits. Pure. */
21
+ export function parseGitLog(raw) {
22
+ const commits = [];
23
+ let cur = null;
24
+ for (const line of raw.split('\n')) {
25
+ if (line === '')
26
+ continue;
27
+ if (line.startsWith(SOH)) {
28
+ const [hash, author, iso] = line.slice(1).split(US);
29
+ const t = Date.parse(iso ?? '');
30
+ cur = { hash: hash ?? '', author: author ?? '', date: Number.isNaN(t) ? 0 : t, files: [] };
31
+ commits.push(cur);
32
+ continue;
33
+ }
34
+ if (!cur)
35
+ continue;
36
+ // numstat: added<TAB>deleted<TAB>path ('-' means binary → count as 0
37
+ const tab1 = line.indexOf('\t');
38
+ const tab2 = line.indexOf('\t', tab1 + 1);
39
+ if (tab1 < 0 || tab2 < 0)
40
+ continue;
41
+ const a = line.slice(0, tab1);
42
+ const d = line.slice(tab1 + 1, tab2);
43
+ const path = line.slice(tab2 + 1);
44
+ if (!path)
45
+ continue;
46
+ cur.files.push({ path, added: a === '-' ? 0 : parseInt(a, 10) || 0, deleted: d === '-' ? 0 : parseInt(d, 10) || 0 });
47
+ }
48
+ return commits;
49
+ }
50
+ /**
51
+ * Composite risk score. Deterministic and monotonic:
52
+ * risk = commits · log2(1 + churn) · authorFactor · recencyWeight
53
+ * where authorFactor = 1 + log2(authors) (coordination cost of many hands)
54
+ * and recencyWeight = 1 / (1 + ageDays/30) (recent churn is riskier; decays
55
+ * over a ~month scale). `now` is injected so the function is pure/testable.
56
+ */
57
+ function scoreOf(h, now) {
58
+ const authorFactor = 1 + Math.log2(h.authors);
59
+ const ageDays = Math.max(0, (now - h.lastTouched) / 86_400_000);
60
+ const recencyWeight = 1 / (1 + ageDays / 30);
61
+ const raw = h.commits * Math.log2(1 + h.churn) * authorFactor * recencyWeight;
62
+ return Math.round(raw * 100) / 100;
63
+ }
64
+ /** Aggregate commits into per-file hotspots, scored and ranked. Pure. */
65
+ export function computeHotspots(commits, now, opts = {}) {
66
+ const minCommits = opts.minCommits ?? 1;
67
+ const by = opts.by ?? 'risk';
68
+ const acc = new Map();
69
+ for (const c of commits) {
70
+ for (const f of c.files) {
71
+ let a = acc.get(f.path);
72
+ if (!a) {
73
+ a = { commits: 0, churn: 0, authors: new Set(), lastTouched: 0, firstTouched: Infinity };
74
+ acc.set(f.path, a);
75
+ }
76
+ a.commits += 1;
77
+ a.churn += f.added + f.deleted;
78
+ a.authors.add(c.author);
79
+ if (c.date > a.lastTouched)
80
+ a.lastTouched = c.date;
81
+ if (c.date < a.firstTouched)
82
+ a.firstTouched = c.date;
83
+ }
84
+ }
85
+ let out = [];
86
+ for (const [path, a] of acc) {
87
+ if (a.commits < minCommits)
88
+ continue;
89
+ const base = {
90
+ path,
91
+ commits: a.commits,
92
+ churn: a.churn,
93
+ authors: a.authors.size,
94
+ lastTouched: a.lastTouched,
95
+ firstTouched: a.firstTouched === Infinity ? 0 : a.firstTouched,
96
+ };
97
+ out.push({ ...base, risk: scoreOf(base, now) });
98
+ }
99
+ // Deterministic sort: chosen key desc, then path asc for stable ties.
100
+ out.sort((x, y) => y[by] - x[by] || (x.path < y.path ? -1 : x.path > y.path ? 1 : 0));
101
+ if (opts.top && opts.top > 0)
102
+ out = out.slice(0, opts.top);
103
+ return out;
104
+ }
105
+ /** Human-readable table. Pure. */
106
+ export function formatHotspots(spots, now) {
107
+ if (spots.length === 0)
108
+ return 'no hotspots found (no matching history)';
109
+ const lines = [];
110
+ lines.push(`rank risk commits churn authors file`);
111
+ spots.forEach((s, i) => {
112
+ const ageDays = Math.round((now - s.lastTouched) / 86_400_000);
113
+ const rank = String(i + 1).padStart(4);
114
+ const risk = s.risk.toFixed(2).padStart(7);
115
+ const commits = String(s.commits).padStart(7);
116
+ const churn = String(s.churn).padStart(6);
117
+ const authors = String(s.authors).padStart(7);
118
+ lines.push(`${rank} ${risk} ${commits} ${churn} ${authors} ${s.path} ${ageDays === 0 ? '(today)' : `(${ageDays}d ago)`}`);
119
+ });
120
+ return lines.join('\n');
121
+ }
122
+ //# sourceMappingURL=hotspots.js.map
@@ -24,6 +24,7 @@ import { redactTools } from './mcp-tools/redact-tools.js';
24
24
  import { envTools } from './mcp-tools/env-tools.js';
25
25
  import { licenseTools } from './mcp-tools/license-tools.js';
26
26
  import { applyTools } from './mcp-tools/apply-tools.js';
27
+ import { hotspotsTools } from './mcp-tools/hotspots-tools.js';
27
28
  import { progressTools } from './mcp-tools/progress-tools.js';
28
29
  import { embeddingsTools } from './mcp-tools/embeddings-tools.js';
29
30
  import { claimsTools } from './mcp-tools/claims-tools.js';
@@ -112,6 +113,7 @@ const TOOL_GROUPS = {
112
113
  env: () => envTools,
113
114
  license: () => licenseTools,
114
115
  apply: () => applyTools,
116
+ hotspots: () => hotspotsTools,
115
117
  progress: () => progressTools,
116
118
  embeddings: () => embeddingsTools,
117
119
  claims: () => claimsTools,
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Hotspots MCP Tool
3
+ *
4
+ * Let an agent ask "which files carry the most change-risk?" and get ranked
5
+ * JSON — files that change often, churn heavily, are touched by many hands, and
6
+ * were edited recently. Data-driven "where is the tech debt?" so an agent can
7
+ * focus refactoring/testing/review effort. Shares the pure engine in
8
+ * ../hotspots/hotspots.ts with the CLI; captures git history via subprocess.
9
+ */
10
+ import type { MCPTool } from './types.js';
11
+ export declare const hotspotsTools: MCPTool[];
12
+ //# sourceMappingURL=hotspots-tools.d.ts.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Hotspots MCP Tool
3
+ *
4
+ * Let an agent ask "which files carry the most change-risk?" and get ranked
5
+ * JSON — files that change often, churn heavily, are touched by many hands, and
6
+ * were edited recently. Data-driven "where is the tech debt?" so an agent can
7
+ * focus refactoring/testing/review effort. Shares the pure engine in
8
+ * ../hotspots/hotspots.ts with the CLI; captures git history via subprocess.
9
+ */
10
+ import { execFileSync } from 'node:child_process';
11
+ import { parseGitLog, computeHotspots } from '../hotspots/hotspots.js';
12
+ const SORT_KEYS = ['risk', 'churn', 'commits', 'authors'];
13
+ const hotspotsTool = {
14
+ name: 'hotspots',
15
+ description: 'Rank files by change-risk mined from git history (churn × recency × author-spread). Returns ranked JSON — call this to find the technical debt worth refactoring or testing before you start, instead of guessing. Runs in a git repository.',
16
+ category: 'hotspots',
17
+ tags: ['git', 'risk', 'churn', 'refactor', 'debt'],
18
+ inputSchema: {
19
+ type: 'object',
20
+ properties: {
21
+ path: { type: 'string', description: 'repo path to run in (default cwd)' },
22
+ subpath: { type: 'string', description: 'limit history to this subdirectory/file' },
23
+ since: { type: 'string', description: 'history window, e.g. 90d or "3 months ago" (default 1 year)' },
24
+ top: { type: 'number', description: 'keep only the top N files (default 20)' },
25
+ by: { type: 'string', enum: SORT_KEYS, description: 'sort key (default risk)' },
26
+ minCommits: { type: 'number', description: 'drop files with fewer than N commits (default 1)' },
27
+ },
28
+ },
29
+ handler: async (params) => {
30
+ const cwd = typeof params.path === 'string' ? params.path : process.cwd();
31
+ const since = typeof params.since === 'string' ? params.since : '1 year ago';
32
+ const by = (typeof params.by === 'string' && SORT_KEYS.includes(params.by) ? params.by : 'risk');
33
+ const top = typeof params.top === 'number' ? params.top : 20;
34
+ const minCommits = typeof params.minCommits === 'number' ? params.minCommits : 1;
35
+ const args = ['log', '--no-merges', '--numstat', `--since=${since}`, '--format=format:%x01%H%x1f%an%x1f%aI'];
36
+ if (typeof params.subpath === 'string' && params.subpath)
37
+ args.push('--', params.subpath);
38
+ let raw;
39
+ try {
40
+ raw = execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 128 * 1024 * 1024 });
41
+ }
42
+ catch {
43
+ return { error: true, message: 'git log failed — not a git repository?' };
44
+ }
45
+ const now = Date.now();
46
+ const hotspots = computeHotspots(parseGitLog(raw), now, { by, top: top > 0 ? top : undefined, minCommits });
47
+ return { by, count: hotspots.length, hotspots };
48
+ },
49
+ };
50
+ export const hotspotsTools = [hotspotsTool];
51
+ //# sourceMappingURL=hotspots-tools.js.map
@@ -20,6 +20,7 @@ export { redactTools } from './redact-tools.js';
20
20
  export { envTools } from './env-tools.js';
21
21
  export { licenseTools } from './license-tools.js';
22
22
  export { applyTools } from './apply-tools.js';
23
+ export { hotspotsTools } from './hotspots-tools.js';
23
24
  export { progressTools } from './progress-tools.js';
24
25
  export { transferTools } from './transfer-tools.js';
25
26
  export { securityTools } from './security-tools.js';
@@ -19,6 +19,7 @@ export { redactTools } from './redact-tools.js';
19
19
  export { envTools } from './env-tools.js';
20
20
  export { licenseTools } from './license-tools.js';
21
21
  export { applyTools } from './apply-tools.js';
22
+ export { hotspotsTools } from './hotspots-tools.js';
22
23
  export { progressTools } from './progress-tools.js';
23
24
  export { transferTools } from './transfer-tools.js';
24
25
  export { securityTools } from './security-tools.js';
@@ -26,6 +26,7 @@ const LEAN_GROUPS = [
26
26
  'redact', // redact_text / redact_scan — secret guard on the data path
27
27
  'env', // env_check — env-var drift (missing/unused/undocumented)
28
28
  'apply', // apply_patch — fuzzy unified-diff applier for agent edits
29
+ 'hotspots', // hotspots — git-history change-risk ranking ("where's the debt?")
29
30
  ];
30
31
  /** Balanced = lean + orchestration/session/system the average user reaches for. */
31
32
  const BALANCED_EXTRA = [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmdo/cli",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "type": "module",
5
5
  "description": "Swarmdo 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",