@desplega.ai/agent-swarm 1.102.0 → 1.103.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.
Files changed (107) hide show
  1. package/dist/{actions-j9399ktm.js → actions-7txktrdj.js} +6 -6
  2. package/dist/{app-tnq1pz27.js → app-xm3xsdye.js} +3 -3
  3. package/dist/{assistant-c7y92gkw.js → assistant-qpv63wk6.js} +9 -9
  4. package/dist/{boot-reembed-k7e6fc1p.js → boot-reembed-9cxqka3e.js} +3 -3
  5. package/dist/{boot-reembed-b97r699t.js → boot-reembed-chxhrwv8.js} +4 -4
  6. package/dist/{boot-scrub-logs-14kbc61y.js → boot-scrub-logs-8qtfkcz8.js} +2 -2
  7. package/dist/{claude-managed-adapter-g07dbz3b.js → claude-managed-adapter-z29hjgzz.js} +3 -3
  8. package/dist/{cli-tsz1q7gk.js → cli-2307phk1.js} +375 -23
  9. package/dist/{cli-g5px3d7x.js → cli-2957pqpj.js} +24 -1
  10. package/dist/{cli-9qascwj7.js → cli-2qynerth.js} +1 -1
  11. package/dist/{cli-k1ghyj8b.js → cli-4fnp2tc3.js} +1 -1
  12. package/dist/{cli-hc4rd6sv.js → cli-85pf4z38.js} +1 -1
  13. package/dist/{cli-zk4gn40w.js → cli-8hyv6c2v.js} +6 -14
  14. package/dist/{cli-k81y1cjb.js → cli-8w95v19d.js} +1 -1
  15. package/dist/{cli-7xdq3jg3.js → cli-92fqyjcv.js} +1 -1
  16. package/dist/{cli-zctkpdmd.js → cli-dz3d6zjg.js} +1 -1
  17. package/dist/{cli-d0t4854c.js → cli-fedn86nz.js} +213 -476
  18. package/dist/{cli-g861fwjp.js → cli-fwxdt7kt.js} +3 -3
  19. package/dist/{cli-2aaa9mqr.js → cli-fygaw191.js} +2 -2
  20. package/dist/{cli-qbc6mncp.js → cli-gzepjz54.js} +2 -2
  21. package/dist/{cli-r68m8arr.js → cli-jt9d9fjw.js} +1 -1
  22. package/dist/{cli-axtg1zaa.js → cli-khe4x17a.js} +1 -1
  23. package/dist/{cli-sygq3fhn.js → cli-kk51f3tf.js} +5 -5
  24. package/dist/{cli-dgab5x2h.js → cli-krvxq8bc.js} +1 -1
  25. package/dist/{cli-xde8vhhp.js → cli-ksq38x4n.js} +2 -2
  26. package/dist/{cli-stqqf5kh.js → cli-m8f6qzck.js} +1 -1
  27. package/dist/{cli-ap4md0gc.js → cli-ns0r7mkr.js} +30 -5
  28. package/dist/{cli-t2jk33ta.js → cli-rttgde5f.js} +18 -6
  29. package/dist/{cli-mhj07xbq.js → cli-rvae030h.js} +3 -3
  30. package/dist/{cli-q6jzg6wj.js → cli-s14sb64w.js} +4 -4
  31. package/dist/{cli-zmf7refz.js → cli-v9283nma.js} +1 -1
  32. package/dist/{cli-fjx4x7nn.js → cli-xmtsan9k.js} +1 -1
  33. package/dist/{cli-vgck9mqq.js → cli-yeemwevj.js} +155 -37
  34. package/dist/{cli-5bqa41fd.js → cli-yfwwjeft.js} +1 -1
  35. package/dist/{cli-s8jw5bmg.js → cli-yp2krx5r.js} +2 -2
  36. package/dist/cli.js +9 -9
  37. package/dist/{codex-adapter-wf119mnw.js → codex-adapter-w0xharsg.js} +3 -3
  38. package/dist/{codex-session-runner-1gknw7mh.js → codex-session-runner-kzbjcm2k.js} +3 -3
  39. package/dist/{commands-2wyn3swb.js → commands-aqn63mcw.js} +2 -2
  40. package/dist/{db-567ntpxv.js → db-azrvwsnj.js} +2 -2
  41. package/dist/{handlers-wvwpk2kj.js → handlers-hf61har3.js} +9 -9
  42. package/dist/{hook-fatkywc0.js → hook-306p3yz4.js} +2 -2
  43. package/dist/{http-eq6wxve6.js → http-zts4haq7.js} +81 -62
  44. package/dist/{index-400x6p3j.js → index-29vg51x6.js} +6 -6
  45. package/dist/{index-9wqh5352.js → index-3c651yfk.js} +10 -10
  46. package/dist/{index-6aj8966p.js → index-bfh9hgek.js} +5 -5
  47. package/dist/{index-d1rm4ea3.js → index-h2yqjyk0.js} +8 -8
  48. package/dist/{keepalive-6x9nx92g.js → keepalive-a0dg5pds.js} +4 -4
  49. package/dist/{lead-5fgqwe13.js → lead-7mcxedkc.js} +21 -21
  50. package/dist/{maintenance-eajx5k1k.js → maintenance-mrzqd53t.js} +4 -4
  51. package/dist/{onboard-jc1w1edg.js → onboard-4dkpv9wa.js} +2 -2
  52. package/dist/{otel-impl-8k375amf.js → otel-impl-jt7gpyp9.js} +1 -1
  53. package/dist/{pi-mono-adapter-hf8sw7hs.js → pi-mono-adapter-zf7dt1wg.js} +1 -1
  54. package/dist/{pricing-refresh-zed6916t.js → pricing-refresh-xqnzh8dt.js} +4 -4
  55. package/dist/{seed-pricing-5805qzkz.js → seed-pricing-56tjm1fj.js} +3 -3
  56. package/dist/{setup-wz05vxh0.js → setup-tbwk8j10.js} +2 -2
  57. package/dist/{worker-j359graf.js → worker-e03za92t.js} +21 -21
  58. package/openapi.json +70 -1
  59. package/package.json +1 -1
  60. package/src/be/db.ts +83 -13
  61. package/src/be/migrations/098_repo_hooks_config.sql +3 -0
  62. package/src/be/task-lifecycle-events.ts +42 -0
  63. package/src/commands/runner.ts +174 -25
  64. package/src/github/task-reactions.ts +21 -0
  65. package/src/hooks/tool-loop-detection.test.ts +40 -0
  66. package/src/hooks/tool-loop-detection.ts +37 -1
  67. package/src/http/agents.ts +7 -0
  68. package/src/http/index.ts +16 -2
  69. package/src/http/prompt-templates.ts +1 -1
  70. package/src/http/repos.ts +5 -1
  71. package/src/http/schedules.ts +1 -1
  72. package/src/http/session-data.ts +2 -0
  73. package/src/http/tasks.ts +2 -1
  74. package/src/http/workflows.ts +1 -0
  75. package/src/prompts/resolver.ts +1 -1
  76. package/src/scheduler/scheduler.ts +25 -2
  77. package/src/server-runtime-counters.ts +12 -0
  78. package/src/server-user.ts +1 -2
  79. package/src/server.ts +5 -0
  80. package/src/telemetry.ts +18 -1
  81. package/src/tests/http-api-integration.test.ts +4 -0
  82. package/src/tests/model-control.test.ts +6 -6
  83. package/src/tests/swarm-repos.test.ts +25 -2
  84. package/src/tests/task-lifecycle-events.test.ts +53 -0
  85. package/src/tests/task-lifecycle-telemetry.test.ts +37 -1
  86. package/src/tests/telemetry-init.test.ts +32 -0
  87. package/src/tools/prompt-templates/preview.ts +1 -1
  88. package/src/tools/repos/update-repo.ts +6 -1
  89. package/src/tools/schedules/create-schedule.ts +1 -1
  90. package/src/tools/schedules/update-schedule.ts +1 -1
  91. package/src/tools/send-task.ts +7 -2
  92. package/src/tools/task-action.ts +6 -2
  93. package/src/types.ts +155 -1
  94. package/src/utils/template.ts +127 -0
  95. package/src/workflows/engine.ts +7 -0
  96. package/src/workflows/executors/agent-task.ts +1 -2
  97. package/src/workflows/template.ts +12 -121
  98. package/src/workflows/triggers.ts +6 -2
  99. package/templates/community/code-health-reports/PLAYBOOK.md +355 -0
  100. package/templates/community/code-health-reports/README.md +38 -0
  101. package/templates/community/code-health-reports/lead-prompt.md +52 -0
  102. package/templates/community/code-health-reports/report.mjs +378 -0
  103. package/templates/community/code-health-reports/run.sh +143 -0
  104. package/templates/schedules/weekly-code-health-reports/config.json +13 -0
  105. package/templates/schedules/weekly-code-health-reports/content.md +60 -0
  106. package/src/model-tiers.ts +0 -140
  107. package/templates/community/.gitkeep +0 -0
@@ -0,0 +1,378 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const [outDir, repoDir, repoName, runDate, scopePath = "src"] = process.argv.slice(2);
5
+ if (!outDir || !repoDir || !repoName || !runDate) {
6
+ console.error("Usage: node report.mjs <outDir> <repoDir> <repoName> <runDate> [scopePath]");
7
+ process.exit(1);
8
+ }
9
+
10
+ const read = (file) => fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
11
+ const csvPath = (name) => path.join(outDir, `${name}.csv`);
12
+ const esc = (value) => String(value ?? "")
13
+ .replaceAll("&", "&amp;")
14
+ .replaceAll("<", "&lt;")
15
+ .replaceAll(">", "&gt;")
16
+ .replaceAll('"', "&quot;");
17
+
18
+ function parseCsvRows(text) {
19
+ const rows = [];
20
+ let row = [];
21
+ let cur = "";
22
+ let quoted = false;
23
+ for (let i = 0; i < text.length; i++) {
24
+ const ch = text[i];
25
+ const next = text[i + 1];
26
+ if (quoted) {
27
+ if (ch === '"' && next === '"') {
28
+ cur += '"';
29
+ i++;
30
+ } else if (ch === '"') {
31
+ quoted = false;
32
+ } else {
33
+ cur += ch;
34
+ }
35
+ } else if (ch === '"') {
36
+ quoted = true;
37
+ } else if (ch === ",") {
38
+ row.push(cur);
39
+ cur = "";
40
+ } else if (ch === "\n") {
41
+ row.push(cur);
42
+ rows.push(row);
43
+ row = [];
44
+ cur = "";
45
+ } else if (ch !== "\r") {
46
+ cur += ch;
47
+ }
48
+ }
49
+ if (cur.length || row.length) {
50
+ row.push(cur);
51
+ rows.push(row);
52
+ }
53
+ return rows.filter((r) => r.some(Boolean));
54
+ }
55
+
56
+ function parseCsv(text) {
57
+ const rows = parseCsvRows(text);
58
+ if (!rows.length) return [];
59
+ const headers = rows.shift().map((h) => h.trim());
60
+ return rows.map((row) => Object.fromEntries(headers.map((h, i) => [h, row[i] ?? ""])));
61
+ }
62
+
63
+ const num = (value) => {
64
+ const n = Number(String(value ?? "").replace(/[%\s,]/g, ""));
65
+ return Number.isFinite(n) ? n : 0;
66
+ };
67
+
68
+ const pick = (row, names, fallback = "") => {
69
+ for (const name of names) {
70
+ if (row?.[name] !== undefined && row[name] !== "") return row[name];
71
+ }
72
+ return fallback;
73
+ };
74
+
75
+ const normalize = (file) => String(file ?? "")
76
+ .replace(`${repoDir}/`, "")
77
+ .replace(/^\.?\//, "");
78
+
79
+ function currentLocByFile() {
80
+ const files = read(path.join(outDir, "src-files.txt")).trim().split("\n").filter(Boolean);
81
+ const loc = new Map();
82
+ for (const file of files) {
83
+ const full = path.join(repoDir, file);
84
+ if (!fs.existsSync(full) || fs.statSync(full).isDirectory()) continue;
85
+ const text = fs.readFileSync(full, "utf8");
86
+ loc.set(normalize(file), text.split("\n").filter((line) => line.trim()).length);
87
+ }
88
+ return loc;
89
+ }
90
+
91
+ function complexityByFile() {
92
+ const rows = parseCsvRows(read(path.join(outDir, "lizard-functions.csv")));
93
+ const byFile = new Map();
94
+ for (const row of rows) {
95
+ const file = normalize(row[6]);
96
+ if (!file || !file.startsWith(`${scopePath}/`)) continue;
97
+ const entry = byFile.get(file) ?? { file, functions: 0, totalCcn: 0, maxCcn: 0 };
98
+ const ccn = num(row[1]);
99
+ entry.functions += 1;
100
+ entry.totalCcn += ccn;
101
+ entry.maxCcn = Math.max(entry.maxCcn, ccn);
102
+ byFile.set(file, entry);
103
+ }
104
+ return byFile;
105
+ }
106
+
107
+ const loc = currentLocByFile();
108
+ const complexity = complexityByFile();
109
+ const revisionsRows = parseCsv(read(csvPath("revisions")));
110
+ const couplingRows = parseCsv(read(csvPath("coupling")));
111
+ const ageRows = parseCsv(read(csvPath("age")));
112
+ const authorsRows = parseCsv(read(csvPath("authors")));
113
+ const ownershipRows = parseCsv(read(csvPath("main-dev")));
114
+ const summaryRows = parseCsv(read(csvPath("summary")));
115
+
116
+ const summaryStats = Object.fromEntries(summaryRows.map((row) => [pick(row, ["statistic", "name"]), num(pick(row, ["value", "n"]))]));
117
+ const authorCountByFile = new Map();
118
+ for (const row of authorsRows) {
119
+ authorCountByFile.set(normalize(pick(row, ["entity", "module", "file"])), num(pick(row, ["n-authors", "authors", "author-count", "count"])));
120
+ }
121
+
122
+ const ageByFile = new Map();
123
+ for (const row of ageRows) {
124
+ ageByFile.set(normalize(pick(row, ["entity", "module", "file"])), {
125
+ ageMonths: num(pick(row, ["age-months", "age", "months"])),
126
+ lastChange: pick(row, ["last-revision", "last-modified", "date"], ""),
127
+ });
128
+ }
129
+
130
+ const ownershipByFile = new Map();
131
+ for (const row of ownershipRows) {
132
+ const file = normalize(pick(row, ["entity", "module", "file"]));
133
+ ownershipByFile.set(file, {
134
+ mainDeveloper: pick(row, ["main-dev", "author", "developer"], "Unknown"),
135
+ ownershipPercent: (() => {
136
+ const value = num(pick(row, ["ownership", "ownership%", "contribution"]));
137
+ return value <= 1 && value > 0 ? Math.round(value * 1000) / 10 : Math.round(value * 10) / 10;
138
+ })(),
139
+ });
140
+ }
141
+
142
+ const files = revisionsRows.map((row) => {
143
+ const file = normalize(pick(row, ["entity", "module", "file"]));
144
+ const revs = num(pick(row, ["n-revs", "revisions", "revs"]));
145
+ const cx = complexity.get(file) ?? { functions: 0, totalCcn: 0, maxCcn: 0 };
146
+ const owned = ownershipByFile.get(file) ?? { mainDeveloper: "Unknown", ownershipPercent: 0 };
147
+ const age = ageByFile.get(file) ?? { ageMonths: 0, lastChange: "" };
148
+ const totalCcn = cx.totalCcn || cx.maxCcn || 0;
149
+ return {
150
+ file,
151
+ revs,
152
+ loc: loc.get(file) ?? 0,
153
+ functions: cx.functions,
154
+ totalCcn,
155
+ maxCcn: cx.maxCcn,
156
+ riskScore: Math.round(revs * Math.log2(totalCcn + 1) * 10) / 10,
157
+ mainDeveloper: owned.mainDeveloper,
158
+ ownershipPercent: owned.ownershipPercent,
159
+ authorCount: authorCountByFile.get(file) || 0,
160
+ ageMonths: age.ageMonths,
161
+ lastChange: age.lastChange,
162
+ };
163
+ }).filter((row) => row.file.startsWith(`${scopePath}/`));
164
+
165
+ files.sort((a, b) => b.riskScore - a.riskScore);
166
+ const topRisk = files.slice(0, 25);
167
+ const chartFiles = files.slice(0, 100);
168
+
169
+ const coupling = couplingRows.map((row) => ({
170
+ a: normalize(pick(row, ["entity", "entity-1", "module", "file"])),
171
+ b: normalize(pick(row, ["coupled", "entity-2", "coupled-entity"])),
172
+ degree: num(pick(row, ["degree", "coupling", "coupling%"])),
173
+ sharedRevs: num(pick(row, ["average-revs", "shared-revisions", "shared-revs", "n-revs"])),
174
+ })).filter((row) => row.a.startsWith(`${scopePath}/`) && row.b.startsWith(`${scopePath}/`))
175
+ .sort((a, b) => (b.degree - a.degree) || (b.sharedRevs - a.sharedRevs))
176
+ .slice(0, 30);
177
+
178
+ const ageBuckets = [
179
+ { label: "0-1m", min: 0, max: 1, files: 0, loc: 0 },
180
+ { label: "1-3m", min: 1, max: 3, files: 0, loc: 0 },
181
+ { label: "3-6m", min: 3, max: 6, files: 0, loc: 0 },
182
+ { label: "6-12m", min: 6, max: 12, files: 0, loc: 0 },
183
+ { label: "12m+", min: 12, max: Infinity, files: 0, loc: 0 },
184
+ ];
185
+ for (const file of files) {
186
+ const bucket = ageBuckets.find((b) => file.ageMonths >= b.min && file.ageMonths < b.max);
187
+ if (bucket) {
188
+ bucket.files += 1;
189
+ bucket.loc += file.loc;
190
+ }
191
+ }
192
+
193
+ const summary = {
194
+ repoName,
195
+ runDate,
196
+ scopePath,
197
+ generatedAt: new Date().toISOString(),
198
+ commit: read(path.join(outDir, "revision.txt")).trim(),
199
+ commitSummary: read(path.join(outDir, "revision-summary.txt")).trim(),
200
+ totals: {
201
+ commitsInScopedLog: summaryStats["number-of-commits"] || 0,
202
+ entities: summaryStats["number-of-entities"] || files.length,
203
+ trackedFiles: read(path.join(outDir, "src-files.txt")).trim().split("\n").filter(Boolean).length,
204
+ authors: summaryStats["number-of-authors"] || 0,
205
+ totalLoc: files.reduce((sum, file) => sum + file.loc, 0),
206
+ },
207
+ topRisk,
208
+ topCoupling: coupling,
209
+ ageBuckets,
210
+ charts: {
211
+ files: chartFiles,
212
+ ageBuckets,
213
+ },
214
+ references: {
215
+ d3: "https://d3js.org",
216
+ codeMaat: "https://github.com/adamtornhill/code-maat",
217
+ lizard: "https://github.com/terryyin/lizard",
218
+ },
219
+ };
220
+
221
+ fs.writeFileSync(path.join(outDir, "summary.json"), JSON.stringify(summary, null, 2));
222
+ fs.writeFileSync(path.join(outDir, "latest-pointer.json"), JSON.stringify({
223
+ repoName,
224
+ runDate,
225
+ path: outDir,
226
+ latestJson: path.join(path.dirname(outDir), "latest.json"),
227
+ latestHtml: path.join(path.dirname(outDir), "latest.html"),
228
+ generatedAt: summary.generatedAt,
229
+ }, null, 2));
230
+
231
+ const dataJson = JSON.stringify(summary).replaceAll("</script", "<\\/script");
232
+ const rows = (items, columns) => items.map((item) => `<tr>${columns.map((col) => `<td>${esc(typeof col === "function" ? col(item) : item[col])}</td>`).join("")}</tr>`).join("\n");
233
+
234
+ const html = `<!doctype html>
235
+ <html lang="en">
236
+ <head>
237
+ <meta charset="utf-8" />
238
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
239
+ <title>Code Health Report - ${esc(repoName)}</title>
240
+ <style>
241
+ body { margin: 0; background: #f7f6f3; color: #181817; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; line-height: 1.5; }
242
+ main { width: min(1180px, calc(100% - 32px)); margin: 0 auto; padding: 48px 0 80px; }
243
+ h1 { font-size: clamp(2rem, 5vw, 4rem); line-height: 1; margin: 0 0 12px; }
244
+ h2 { margin-top: 40px; }
245
+ p, .muted { color: #686660; }
246
+ .grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin: 28px 0; }
247
+ .metric, .card { background: #fff; border: 1px solid #e4ded3; border-radius: 8px; padding: 18px; }
248
+ .metric span { display: block; color: #686660; font-size: 12px; font-weight: 700; text-transform: uppercase; }
249
+ .metric strong { display: block; margin-top: 8px; font-size: 30px; }
250
+ .charts { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
251
+ .chart { height: 360px; }
252
+ svg { width: 100%; height: 100%; display: block; }
253
+ table { width: 100%; border-collapse: collapse; font-size: 13px; background: #fff; border: 1px solid #e4ded3; border-radius: 8px; overflow: hidden; }
254
+ th, td { padding: 8px 10px; border-bottom: 1px solid #eee8dc; text-align: left; vertical-align: top; }
255
+ th { color: #686660; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }
256
+ td { max-width: 380px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
257
+ code { background: #fff; border: 1px solid #e4ded3; padding: 1px 5px; border-radius: 4px; }
258
+ @media (max-width: 860px) { .grid, .charts { grid-template-columns: 1fr; } }
259
+ </style>
260
+ </head>
261
+ <body>
262
+ <main>
263
+ <p class="muted">Generated ${esc(summary.generatedAt)}</p>
264
+ <h1>Code Health Report</h1>
265
+ <p><strong>${esc(repoName)}</strong> at <code>${esc(summary.commit.slice(0, 12))}</code>. Scope: <code>${esc(scopePath)}/**</code>.</p>
266
+
267
+ <section class="grid">
268
+ <div class="metric"><span>Scoped commits</span><strong>${esc(summary.totals.commitsInScopedLog || "n/a")}</strong></div>
269
+ <div class="metric"><span>Entities</span><strong>${esc(summary.totals.entities)}</strong></div>
270
+ <div class="metric"><span>Tracked files</span><strong>${esc(summary.totals.trackedFiles)}</strong></div>
271
+ <div class="metric"><span>Current LOC</span><strong>${esc(summary.totals.totalLoc)}</strong></div>
272
+ </section>
273
+
274
+ <section class="charts">
275
+ <div class="card">
276
+ <h2>Hotspots</h2>
277
+ <p>Bubble size is current LOC. Color is churn x complexity score.</p>
278
+ <div id="bubble" class="chart"></div>
279
+ </div>
280
+ <div class="card">
281
+ <h2>Change Frequency x Complexity</h2>
282
+ <p>X is revisions. Y is total cyclomatic complexity.</p>
283
+ <div id="scatter" class="chart"></div>
284
+ </div>
285
+ </section>
286
+
287
+ <h2>Top Hotspots</h2>
288
+ <table>
289
+ <thead><tr><th>File</th><th>Revs</th><th>LOC</th><th>Total CCN</th><th>Max CCN</th><th>Risk Score</th><th>Main Dev</th><th>Authors</th><th>Own %</th></tr></thead>
290
+ <tbody>${rows(topRisk, [
291
+ "file",
292
+ "revs",
293
+ "loc",
294
+ "totalCcn",
295
+ "maxCcn",
296
+ "riskScore",
297
+ "mainDeveloper",
298
+ "authorCount",
299
+ "ownershipPercent",
300
+ ])}</tbody>
301
+ </table>
302
+
303
+ <h2>Temporal Coupling</h2>
304
+ <table>
305
+ <thead><tr><th>Entity A</th><th>Entity B</th><th>Degree %</th><th>Shared Revs</th></tr></thead>
306
+ <tbody>${rows(coupling, ["a", "b", "degree", "sharedRevs"])}</tbody>
307
+ </table>
308
+
309
+ <h2>Code Age</h2>
310
+ <div id="age" class="chart card"></div>
311
+
312
+ <h2>References</h2>
313
+ <ul>
314
+ <li><a href="https://github.com/adamtornhill/code-maat">Code Maat</a></li>
315
+ <li><a href="https://d3js.org">D3.js</a></li>
316
+ <li><a href="https://github.com/terryyin/lizard">Lizard</a></li>
317
+ </ul>
318
+ </main>
319
+ <script>window.__CODE_MAAT_DATA__ = ${dataJson};</script>
320
+ <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
321
+ <script>
322
+ const data = window.__CODE_MAAT_DATA__;
323
+ const colors = d3.scaleSequential(d3.interpolateYlOrRd).domain([0, d3.max(data.charts.files, d => d.riskScore) || 1]);
324
+
325
+ function drawBubble() {
326
+ const el = document.querySelector("#bubble");
327
+ const width = el.clientWidth;
328
+ const height = el.clientHeight;
329
+ const root = d3.hierarchy({ children: data.charts.files }).sum(d => d.loc || 1);
330
+ d3.pack().size([width, height]).padding(3)(root);
331
+ const svg = d3.select(el).append("svg");
332
+ const node = svg.selectAll("g").data(root.leaves()).join("g").attr("transform", d => \`translate(\${d.x},\${d.y})\`);
333
+ node.append("circle").attr("r", d => d.r).attr("fill", d => colors(d.data.riskScore)).attr("stroke", "#fff");
334
+ node.append("title").text(d => \`\${d.data.file}\\nrevs: \${d.data.revs}\\nccn: \${d.data.totalCcn}\\nrisk: \${d.data.riskScore}\`);
335
+ }
336
+
337
+ function drawScatter() {
338
+ const el = document.querySelector("#scatter");
339
+ const width = el.clientWidth;
340
+ const height = el.clientHeight;
341
+ const margin = { top: 20, right: 20, bottom: 38, left: 48 };
342
+ const svg = d3.select(el).append("svg");
343
+ const x = d3.scaleLinear().domain([0, d3.max(data.charts.files, d => d.revs) || 1]).nice().range([margin.left, width - margin.right]);
344
+ const y = d3.scaleLinear().domain([0, d3.max(data.charts.files, d => d.totalCcn) || 1]).nice().range([height - margin.bottom, margin.top]);
345
+ const r = d3.scaleSqrt().domain([0, d3.max(data.charts.files, d => d.loc) || 1]).range([3, 18]);
346
+ svg.append("g").attr("transform", \`translate(0,\${height - margin.bottom})\`).call(d3.axisBottom(x));
347
+ svg.append("g").attr("transform", \`translate(\${margin.left},0)\`).call(d3.axisLeft(y));
348
+ svg.selectAll("circle").data(data.charts.files).join("circle")
349
+ .attr("cx", d => x(d.revs)).attr("cy", d => y(d.totalCcn)).attr("r", d => r(d.loc))
350
+ .attr("fill", d => colors(d.riskScore)).attr("opacity", 0.82).append("title")
351
+ .text(d => \`\${d.file}\\nrevs: \${d.revs}\\nccn: \${d.totalCcn}\\nrisk: \${d.riskScore}\`);
352
+ }
353
+
354
+ function drawAge() {
355
+ const el = document.querySelector("#age");
356
+ const width = el.clientWidth;
357
+ const height = el.clientHeight;
358
+ const margin = { top: 20, right: 20, bottom: 38, left: 56 };
359
+ const svg = d3.select(el).append("svg");
360
+ const x = d3.scaleBand().domain(data.ageBuckets.map(d => d.label)).range([margin.left, width - margin.right]).padding(0.2);
361
+ const y = d3.scaleLinear().domain([0, d3.max(data.ageBuckets, d => d.loc) || 1]).nice().range([height - margin.bottom, margin.top]);
362
+ svg.append("g").attr("transform", \`translate(0,\${height - margin.bottom})\`).call(d3.axisBottom(x));
363
+ svg.append("g").attr("transform", \`translate(\${margin.left},0)\`).call(d3.axisLeft(y));
364
+ svg.selectAll("rect").data(data.ageBuckets).join("rect")
365
+ .attr("x", d => x(d.label)).attr("y", d => y(d.loc)).attr("width", x.bandwidth()).attr("height", d => y(0) - y(d.loc))
366
+ .attr("fill", "#8a5a12").append("title").text(d => \`\${d.label}: \${d.loc} LOC, \${d.files} files\`);
367
+ }
368
+
369
+ if (window.d3) {
370
+ drawBubble();
371
+ drawScatter();
372
+ drawAge();
373
+ }
374
+ </script>
375
+ </body>
376
+ </html>`;
377
+
378
+ fs.writeFileSync(path.join(outDir, "report.html"), html);
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Code Maat + D3 recurring code-health report template.
5
+ #
6
+ # Lead kickoff prompt:
7
+ # See ./lead-prompt.md in this template. It is the canonical copy-paste prompt
8
+ # for asking an agent-swarm Lead to install this runner, run it once, publish a
9
+ # stable page, and schedule weekly refreshes.
10
+ #
11
+ # Default weekly cadence for the surrounding swarm schedule:
12
+ # cron: "0 21 * * 0"
13
+ # timezone: "UTC"
14
+ # Change the cron field to adjust when the report refreshes. Keep the same page
15
+ # ID when publishing so the report URL stays stable.
16
+ #
17
+ # Code Maat license note:
18
+ # Code Maat is GPLv3. This template does not vendor or redistribute the JAR.
19
+ # It downloads the upstream standalone JAR at runtime on first run.
20
+
21
+ BASE_DIR="${BASE_DIR:-/workspace/code-maat}"
22
+ REPO_NAME="${REPO_NAME:-my-repo}"
23
+ REPO_URL="${REPO_URL:-https://github.com/OWNER/REPO.git}"
24
+ BRANCH="${BRANCH:-main}"
25
+ SCOPE_PATH="${SCOPE_PATH:-src}"
26
+ LOCAL_SOURCE="${LOCAL_SOURCE:-}"
27
+
28
+ REPO_DIR="$BASE_DIR/repos/$REPO_NAME"
29
+ OUT_ROOT="$BASE_DIR/out/$REPO_NAME"
30
+ RUN_DATE="${RUN_DATE:-$(date -u +%F)}"
31
+ OUT_DIR="$OUT_ROOT/$RUN_DATE"
32
+ JAR="$BASE_DIR/code-maat.jar"
33
+ CODE_MAAT_URL="${CODE_MAAT_URL:-https://github.com/adamtornhill/code-maat/releases/download/v1.0.4/code-maat-1.0.4-standalone.jar}"
34
+
35
+ mkdir -p "$BASE_DIR/repos" "$OUT_DIR"
36
+
37
+ need_cmd() {
38
+ command -v "$1" >/dev/null 2>&1
39
+ }
40
+
41
+ ensure_java() {
42
+ if ! need_cmd java; then
43
+ sudo apt-get update
44
+ sudo apt-get install -y default-jre-headless
45
+ fi
46
+ }
47
+
48
+ ensure_node() {
49
+ if ! need_cmd node; then
50
+ sudo apt-get update
51
+ sudo apt-get install -y nodejs
52
+ fi
53
+ }
54
+
55
+ ensure_python() {
56
+ if ! need_cmd python3; then
57
+ sudo apt-get update
58
+ sudo apt-get install -y python3 python3-pip
59
+ fi
60
+ }
61
+
62
+ ensure_lizard() {
63
+ ensure_python
64
+ if ! need_cmd lizard && [ ! -x "$HOME/.local/bin/lizard" ]; then
65
+ python3 -m pip install --break-system-packages --user lizard
66
+ fi
67
+ }
68
+
69
+ ensure_code_maat() {
70
+ if [ ! -s "$JAR" ]; then
71
+ curl -fsSL -o "$JAR" "$CODE_MAAT_URL"
72
+ fi
73
+ java -jar "$JAR" --help >/dev/null
74
+ }
75
+
76
+ prepare_repo() {
77
+ if [ -d "$REPO_DIR/.git" ]; then
78
+ git -C "$REPO_DIR" remote set-url --push origin DISABLED >/dev/null 2>&1 || true
79
+ git -C "$REPO_DIR" fetch origin "$BRANCH" --prune
80
+ git -C "$REPO_DIR" checkout -q "$BRANCH" || git -C "$REPO_DIR" checkout -q -B "$BRANCH" "origin/$BRANCH"
81
+ git -C "$REPO_DIR" reset --hard "origin/$BRANCH" >/dev/null
82
+ git -C "$REPO_DIR" clean -fdx >/dev/null
83
+ return
84
+ fi
85
+
86
+ if [ -n "$LOCAL_SOURCE" ] && [ -d "$LOCAL_SOURCE/.git" ]; then
87
+ git clone --no-hardlinks "$LOCAL_SOURCE" "$REPO_DIR"
88
+ git -C "$REPO_DIR" remote set-url origin "$REPO_URL" || true
89
+ git -C "$REPO_DIR" remote set-url --push origin DISABLED || true
90
+ git -C "$REPO_DIR" fetch origin "$BRANCH" --prune
91
+ git -C "$REPO_DIR" checkout -q "$BRANCH" || git -C "$REPO_DIR" checkout -q -B "$BRANCH" "origin/$BRANCH"
92
+ git -C "$REPO_DIR" reset --hard "origin/$BRANCH" >/dev/null
93
+ else
94
+ git clone --branch "$BRANCH" "$REPO_URL" "$REPO_DIR"
95
+ git -C "$REPO_DIR" remote set-url --push origin DISABLED || true
96
+ fi
97
+ }
98
+
99
+ run_code_maat() {
100
+ local analysis="$1"
101
+ shift || true
102
+ java -jar "$JAR" -l "$OUT_DIR/git-src.log" -c git2 -a "$analysis" "$@" -o "$OUT_DIR/$analysis.csv"
103
+ }
104
+
105
+ ensure_java
106
+ ensure_node
107
+ ensure_lizard
108
+ ensure_code_maat
109
+ prepare_repo
110
+
111
+ git -C "$REPO_DIR" log --all --numstat --date=short --pretty=format:'--%h--%ad--%aN' --no-renames -- "$SCOPE_PATH" > "$OUT_DIR/git-src.log"
112
+ git -C "$REPO_DIR" rev-parse HEAD > "$OUT_DIR/revision.txt"
113
+ git -C "$REPO_DIR" log -1 --format='%h %ad %an %s' --date=short > "$OUT_DIR/revision-summary.txt"
114
+ git -C "$REPO_DIR" ls-files "$SCOPE_PATH/**" > "$OUT_DIR/src-files.txt"
115
+
116
+ run_code_maat summary
117
+ run_code_maat revisions -r 10000 -n 1
118
+ run_code_maat coupling -r 10000 -m 2 -i 1 -s 60
119
+ run_code_maat age -r 10000 -d "$RUN_DATE"
120
+ run_code_maat authors -r 10000
121
+ run_code_maat entity-ownership -r 10000
122
+ run_code_maat entity-effort -r 10000
123
+ run_code_maat main-dev -r 10000
124
+ run_code_maat main-dev-by-revs -r 10000
125
+ run_code_maat abs-churn -r 10000
126
+ run_code_maat author-churn -r 10000
127
+ run_code_maat entity-churn -r 10000
128
+
129
+ LIZARD_BIN="$(command -v lizard || true)"
130
+ if [ -z "$LIZARD_BIN" ] && [ -x "$HOME/.local/bin/lizard" ]; then
131
+ LIZARD_BIN="$HOME/.local/bin/lizard"
132
+ fi
133
+ "$LIZARD_BIN" --csv "$REPO_DIR/$SCOPE_PATH" > "$OUT_DIR/lizard-functions.csv" || true
134
+
135
+ node "$BASE_DIR/report.mjs" "$OUT_DIR" "$REPO_DIR" "$REPO_NAME" "$RUN_DATE" "$SCOPE_PATH"
136
+
137
+ cp "$OUT_DIR/summary.json" "$OUT_ROOT/latest.json"
138
+ cp "$OUT_DIR/report.html" "$OUT_ROOT/latest.html"
139
+ cp "$OUT_DIR/latest-pointer.json" "$OUT_ROOT/latest-pointer.json"
140
+
141
+ echo "Wrote $OUT_DIR"
142
+ echo "Latest summary: $OUT_ROOT/latest.json"
143
+ echo "Latest page HTML: $OUT_ROOT/latest.html"
@@ -0,0 +1,13 @@
1
+ {
2
+ "kind": "schedule",
3
+ "name": "weekly-code-health-reports",
4
+ "displayName": "Weekly Code Health Reports",
5
+ "slug": "weekly-code-health-reports",
6
+ "title": "Weekly Code Health Reports",
7
+ "description": "Gallery entry point for the community Code Maat + D3 code-health report package. Run a weekly report and update a stable page in place.",
8
+ "version": "1.0.0",
9
+ "category": "schedules",
10
+ "placeholders": ["REPO_URL", "BRANCH", "SCOPE_PATH", "REPORT_NAME", "PAGE_ID", "TIMEZONE"],
11
+ "runAllSeedersCandidate": false,
12
+ "tags": ["code-quality", "maintenance", "reporting", "community-template"]
13
+ }
@@ -0,0 +1,60 @@
1
+ # Weekly Code Health Reports
2
+
3
+ Run a recurring Code Maat + D3 code-health report for a repository and update the same stable report page in place.
4
+
5
+ This schedule is the templates gallery entry point for the community package in `templates/community/code-health-reports/`. The package itself is intentionally not an agent-template config; it is a runnable playbook bundle (`PLAYBOOK.md`, `run.sh`, `report.mjs`, and `lead-prompt.md`) used by this schedule.
6
+
7
+ ## Schedule
8
+
9
+ ```json
10
+ {
11
+ "cron": "0 21 * * 0",
12
+ "timezone": "UTC",
13
+ "agentRole": "worker",
14
+ "enabled": true
15
+ }
16
+ ```
17
+
18
+ ## Scheduled Task
19
+
20
+ This is a reusable starting prompt. Before enabling it, replace the repository, scope, branch, report name, page ID, timezone, and worker targeting details with values for your project.
21
+
22
+ Task Type: Weekly Code Health Reports
23
+
24
+ Repository: `https://github.com/OWNER/REPO.git`
25
+ Default branch: `main`
26
+ Path scope: `src`
27
+ Report name: `my-repo`
28
+ Stable page ID: `<PAGE_ID>`
29
+
30
+ ## Instructions
31
+
32
+ 1. Use the community template in `templates/community/code-health-reports/`. See the docs playbook at `https://docs.agent-swarm.dev/docs/playbooks/code-health-reports` for the full setup and attribution notes.
33
+ 2. Install or update the runner workspace under `/workspace/code-maat`:
34
+ - `run.sh`
35
+ - `report.mjs`
36
+ - `lead-prompt.md`
37
+ 3. Run the report with project-specific parameters:
38
+
39
+ ```bash
40
+ BASE_DIR=/workspace/code-maat \
41
+ REPO_NAME=my-repo \
42
+ REPO_URL=https://github.com/OWNER/REPO.git \
43
+ BRANCH=main \
44
+ SCOPE_PATH=src \
45
+ bash /workspace/code-maat/run.sh
46
+ ```
47
+
48
+ 4. Publish `/workspace/code-maat/out/my-repo/latest.html` to the existing stable page ID. Do not create a new page for routine refreshes.
49
+ 5. Verify the rendered page loads and the D3 charts are not blank.
50
+ 6. If the run fails because of a local runner, dependency, branch, or report-generator issue, diagnose and fix it before reporting failure.
51
+
52
+ ## Cadence
53
+
54
+ The default cron is `0 21 * * 0`, weekly on Sunday at 21:00 UTC. Change the `cron` field to adjust when the report refreshes, and change `timezone` if the cron should be interpreted in a different zone.
55
+
56
+ The cadence is separate from page identity. Keep updating the same page ID so the report URL remains stable.
57
+
58
+ ## Completion
59
+
60
+ Call `store-progress` with status `completed` and an output summary that includes the stable page URL, report workspace path, commit analyzed, generated timestamp, and any dependency or chart-rendering issues found.