@desplega.ai/agent-swarm 1.103.0 → 1.105.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 (129) hide show
  1. package/README.md +3 -0
  2. package/dist/{actions-7txktrdj.js → actions-45g8tvc8.js} +6 -6
  3. package/dist/{app-xm3xsdye.js → app-18x5k1cj.js} +3 -3
  4. package/dist/{assistant-qpv63wk6.js → assistant-ka281qe8.js} +9 -10
  5. package/dist/{boot-reembed-9cxqka3e.js → boot-reembed-f38vbxw8.js} +4 -3
  6. package/dist/{boot-reembed-chxhrwv8.js → boot-reembed-rp2gba79.js} +5 -4
  7. package/dist/{boot-scrub-logs-8qtfkcz8.js → boot-scrub-logs-nsnc9f1a.js} +2 -2
  8. package/dist/{cli-gzepjz54.js → cli-0e24r2b6.js} +900 -352
  9. package/dist/{cli-dz3d6zjg.js → cli-0n4xxmy0.js} +121 -2
  10. package/dist/{cli-jt9d9fjw.js → cli-0s37sw3k.js} +1 -1
  11. package/dist/{cli-yeemwevj.js → cli-2hg6bj31.js} +20 -16
  12. package/dist/{cli-wjyyv518.js → cli-3j01d33e.js} +9 -14
  13. package/dist/{cli-xmtsan9k.js → cli-4df7btj0.js} +1 -1
  14. package/dist/{cli-8w95v19d.js → cli-634jy7sa.js} +351 -24
  15. package/dist/{cli-m8f6qzck.js → cli-7esh9fda.js} +1 -1
  16. package/dist/{cli-4fnp2tc3.js → cli-87fj1jk4.js} +1 -1
  17. package/dist/{cli-ksq38x4n.js → cli-8y7y1vmp.js} +2 -2
  18. package/dist/{cli-rvae030h.js → cli-ba47wf2q.js} +3 -3
  19. package/dist/{cli-2qynerth.js → cli-c4h8m0m3.js} +1 -1
  20. package/dist/{cli-fygaw191.js → cli-dd6tegmp.js} +2 -2
  21. package/dist/{cli-85pf4z38.js → cli-ehcjr5vs.js} +5 -3
  22. package/dist/{cli-rttgde5f.js → cli-gq7gvvrn.js} +1 -1
  23. package/dist/{cli-kk51f3tf.js → cli-gzmv1371.js} +5 -5
  24. package/dist/{http-zts4haq7.js → cli-sdf2qwkt.js} +83970 -76419
  25. package/dist/{cli-krvxq8bc.js → cli-sm9yseh1.js} +23 -4
  26. package/dist/{cli-8hyv6c2v.js → cli-v2fb1fxf.js} +41 -23
  27. package/dist/{cli-fwxdt7kt.js → cli-vpwffend.js} +30 -5
  28. package/dist/{cli-fedn86nz.js → cli-wztagmpk.js} +8 -3
  29. package/dist/{cli-v9283nma.js → cli-yd29d4n4.js} +1 -1
  30. package/dist/{cli-yfwwjeft.js → cli-yfyqsqzj.js} +1 -1
  31. package/dist/{cli-ns0r7mkr.js → cli-yzr0qxx1.js} +3 -3
  32. package/dist/{cli-s14sb64w.js → cli-zmta7t3y.js} +5 -7
  33. package/dist/cli.js +12 -10
  34. package/dist/{commands-aqn63mcw.js → commands-tn8zj0an.js} +2 -2
  35. package/dist/{db-azrvwsnj.js → db-gtqymk7r.js} +2 -2
  36. package/dist/{handlers-hf61har3.js → handlers-5xdqk40d.js} +10 -12
  37. package/dist/{hook-306p3yz4.js → hook-gj6j6zwj.js} +7 -5
  38. package/dist/http-4pceba9h.js +17374 -0
  39. package/dist/{index-bfh9hgek.js → index-0rfp6wh2.js} +5 -5
  40. package/dist/{index-3c651yfk.js → index-as4evz9g.js} +14 -11
  41. package/dist/{index-h2yqjyk0.js → index-paaw4xt5.js} +9 -8
  42. package/dist/{index-29vg51x6.js → index-wf0q4k03.js} +23 -10
  43. package/dist/{keepalive-a0dg5pds.js → keepalive-akbzjt41.js} +4 -4
  44. package/dist/{lead-7mcxedkc.js → lead-g8v85nf4.js} +19 -19
  45. package/dist/{maintenance-mrzqd53t.js → maintenance-9yrzmrh8.js} +5 -4
  46. package/dist/{onboard-4dkpv9wa.js → onboard-v5bx0kpy.js} +2 -2
  47. package/dist/{otel-impl-jt7gpyp9.js → otel-impl-530442da.js} +4510 -4465
  48. package/dist/{pricing-refresh-xqnzh8dt.js → pricing-refresh-dt289v11.js} +4 -4
  49. package/dist/{seed-pricing-56tjm1fj.js → seed-pricing-183er9b3.js} +3 -3
  50. package/dist/{setup-tbwk8j10.js → setup-tjgsvznx.js} +2 -2
  51. package/dist/{worker-e03za92t.js → worker-nxcxk5pb.js} +19 -19
  52. package/openapi.json +90 -4
  53. package/package.json +5 -3
  54. package/src/be/memory/constants.ts +6 -0
  55. package/src/be/memory/providers/sqlite-store.ts +468 -71
  56. package/src/be/memory/raters/retrieval.ts +4 -2
  57. package/src/be/memory/reranker.ts +5 -2
  58. package/src/be/memory/retrieval-store.ts +2 -0
  59. package/src/be/memory/types.ts +46 -0
  60. package/src/be/migrations/099_memory_structured_key_versioning.sql +41 -0
  61. package/src/be/migrations/100_memory_retrieval_source.sql +8 -0
  62. package/src/be/migrations/101_script_connections.sql +67 -0
  63. package/src/be/script-connections.ts +744 -0
  64. package/src/be/script-credential-broker.ts +38 -0
  65. package/src/be/scripts/typecheck.ts +23 -2
  66. package/src/be/seed-scripts/catalog/boot-triage.inline.ts +16 -4
  67. package/src/be/seed-scripts/catalog/boot-triage.ts +16 -4
  68. package/src/commands/runner.ts +8 -6
  69. package/src/heartbeat/heartbeat.ts +42 -1
  70. package/src/heartbeat/index.ts +7 -1
  71. package/src/http/memory.ts +127 -14
  72. package/src/http/pages-public.ts +123 -2
  73. package/src/http/scripts.ts +15 -14
  74. package/src/http/session-data.ts +16 -0
  75. package/src/otel-impl.ts +73 -0
  76. package/src/otel.ts +12 -0
  77. package/src/scripts-runtime/api-client.ts +100 -0
  78. package/src/scripts-runtime/api-types.ts +45 -0
  79. package/src/scripts-runtime/credential-broker/README.md +40 -0
  80. package/src/scripts-runtime/credential-broker/broker.ts +51 -0
  81. package/src/scripts-runtime/credential-broker/default-bindings.ts +19 -0
  82. package/src/scripts-runtime/credential-broker/fetch-patch.ts +102 -0
  83. package/src/scripts-runtime/credential-broker/index.ts +17 -0
  84. package/src/scripts-runtime/credential-broker/store.ts +72 -0
  85. package/src/scripts-runtime/credential-broker/types.ts +54 -0
  86. package/src/scripts-runtime/ctx.ts +11 -1
  87. package/src/scripts-runtime/egress-secrets.ts +14 -76
  88. package/src/scripts-runtime/eval-harness.ts +1 -1
  89. package/src/scripts-runtime/executors/types.ts +5 -5
  90. package/src/scripts-runtime/loader.ts +6 -1
  91. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  92. package/src/scripts-runtime/types/stdlib.d.ts +11 -0
  93. package/src/scripts-runtime/types/swarm-sdk.d.ts +11 -0
  94. package/src/server.ts +6 -0
  95. package/src/tests/credential-broker.test.ts +281 -0
  96. package/src/tests/heartbeat.test.ts +239 -0
  97. package/src/tests/memory-edit.test.ts +177 -0
  98. package/src/tests/memory-hybrid.test.ts +212 -0
  99. package/src/tests/otel-impl-secret-scrubbing.test.ts +101 -3
  100. package/src/tests/otel-session-cost-metrics.test.ts +281 -0
  101. package/src/tests/pages-authed-mode.test.ts +50 -0
  102. package/src/tests/runner-repo-autostash.test.ts +39 -8
  103. package/src/tests/script-connections.test.ts +360 -0
  104. package/src/tests/scripts-http.test.ts +16 -10
  105. package/src/tests/scripts-runtime-secret-egress.test.ts +26 -5
  106. package/src/tests/swarm-config-reserved-keys.test.ts +23 -0
  107. package/src/tests/update-schedule-mcp-tool.test.ts +83 -4
  108. package/src/tools/credential-bindings/index.ts +1 -0
  109. package/src/tools/credential-bindings/tool.ts +238 -0
  110. package/src/tools/memory-edit.ts +148 -0
  111. package/src/tools/memory-search.ts +17 -10
  112. package/src/tools/schedules/delete-schedule.ts +18 -8
  113. package/src/tools/schedules/update-schedule.ts +4 -9
  114. package/src/tools/script-connections/index.ts +1 -0
  115. package/src/tools/script-connections/tool.ts +167 -0
  116. package/src/tools/swarm-config/delete-config.ts +5 -0
  117. package/src/tools/swarm-config/set-config.ts +23 -1
  118. package/src/tools/tool-config.ts +6 -1
  119. package/src/types.ts +6 -0
  120. package/src/workflows/executors/swarm-script.ts +2 -0
  121. package/templates/community/dora-metrics/PLAYBOOK.md +294 -0
  122. package/templates/community/dora-metrics/README.md +39 -0
  123. package/templates/community/dora-metrics/lead-prompt.md +56 -0
  124. package/templates/community/dora-metrics/report.mjs +393 -0
  125. package/templates/community/dora-metrics/run.sh +113 -0
  126. package/templates/schedules/weekly-dora-metrics/config.json +13 -0
  127. package/templates/schedules/weekly-dora-metrics/content.md +61 -0
  128. package/dist/cli-2307phk1.js +0 -24223
  129. package/dist/cli-2deg0cen.js +0 -120
@@ -0,0 +1,393 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { execFileSync } from "node:child_process";
4
+
5
+ const [outDir, repoDir, repoName, runDate, branch = "main", windowDaysArg = "90", hotfixWindowArg = "24", tagPattern = "v*"] = process.argv.slice(2);
6
+ if (!outDir || !repoDir || !repoName || !runDate) {
7
+ console.error("Usage: node report.mjs <outDir> <repoDir> <repoName> <runDate> [branch] [windowDays] [hotfixWindowHours] [tagPattern]");
8
+ process.exit(1);
9
+ }
10
+
11
+ const windowDays = Number(windowDaysArg);
12
+ const hotfixWindowHours = Number(hotfixWindowArg);
13
+ const now = new Date(`${runDate}T23:59:59Z`);
14
+ const start = new Date(now.getTime() - windowDays * 24 * 60 * 60 * 1000);
15
+
16
+ const CONFIG = {
17
+ deploymentFrequency: {
18
+ elitePerDay: 1,
19
+ highPerDay: 1 / 7,
20
+ mediumPerDay: 1 / 30,
21
+ },
22
+ leadTimeHours: {
23
+ eliteMax: 24,
24
+ highMax: 24 * 7,
25
+ mediumMax: 24 * 30,
26
+ },
27
+ changeFailureRate: {
28
+ eliteMax: 0.05,
29
+ highMax: 0.2,
30
+ mediumMax: 0.1,
31
+ },
32
+ failedDeploymentRecoveryHours: {
33
+ eliteMax: 1,
34
+ highMax: 24,
35
+ mediumMax: 24,
36
+ },
37
+ };
38
+
39
+ const read = (file) => fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
40
+ const esc = (value) => String(value ?? "")
41
+ .replaceAll("&", "&amp;")
42
+ .replaceAll("<", "&lt;")
43
+ .replaceAll(">", "&gt;")
44
+ .replaceAll('"', "&quot;");
45
+ const hoursBetween = (a, b) => Math.max(0, (b.getTime() - a.getTime()) / 36e5);
46
+ const fmtHours = (hours) => {
47
+ if (!Number.isFinite(hours)) return "n/a";
48
+ if (hours < 48) return `${Math.round(hours * 10) / 10}h`;
49
+ return `${Math.round((hours / 24) * 10) / 10}d`;
50
+ };
51
+ const fmtPct = (value) => `${Math.round(value * 1000) / 10}%`;
52
+ const median = (values) => {
53
+ const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
54
+ if (!sorted.length) return null;
55
+ const mid = Math.floor(sorted.length / 2);
56
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
57
+ };
58
+ const git = (args) => execFileSync("git", ["-C", repoDir, ...args], { encoding: "utf8" }).trim();
59
+
60
+ function parseTsv(text, fields) {
61
+ return text.trim().split("\n").filter(Boolean).map((line) => {
62
+ const parts = line.split("\t");
63
+ return Object.fromEntries(fields.map((field, i) => [field, parts[i] ?? ""]));
64
+ });
65
+ }
66
+
67
+ function classifyDeploymentFrequency(perDay) {
68
+ if (perDay >= CONFIG.deploymentFrequency.elitePerDay) return "Elite";
69
+ if (perDay >= CONFIG.deploymentFrequency.highPerDay) return "High";
70
+ if (perDay >= CONFIG.deploymentFrequency.mediumPerDay) return "Medium";
71
+ return "Low";
72
+ }
73
+
74
+ function classifyLeadTime(hours) {
75
+ if (!Number.isFinite(hours)) return "Unknown";
76
+ if (hours < CONFIG.leadTimeHours.eliteMax) return "Elite";
77
+ if (hours <= CONFIG.leadTimeHours.highMax) return "High";
78
+ if (hours <= CONFIG.leadTimeHours.mediumMax) return "Medium";
79
+ return "Low";
80
+ }
81
+
82
+ function classifyChangeFailureRate(rate) {
83
+ if (!Number.isFinite(rate)) return "Unknown";
84
+ if (rate <= CONFIG.changeFailureRate.eliteMax) return "Elite";
85
+ if (rate <= CONFIG.changeFailureRate.mediumMax) return "Medium";
86
+ if (rate <= CONFIG.changeFailureRate.highMax) return "High";
87
+ return "Low";
88
+ }
89
+
90
+ function classifyRecovery(hours) {
91
+ if (!Number.isFinite(hours)) return "Unknown";
92
+ if (hours < CONFIG.failedDeploymentRecoveryHours.eliteMax) return "Elite";
93
+ if (hours <= CONFIG.failedDeploymentRecoveryHours.highMax) return "High";
94
+ if (hours <= CONFIG.failedDeploymentRecoveryHours.mediumMax) return "Medium";
95
+ return "Low";
96
+ }
97
+
98
+ function commitsBetween(fromRef, toRef) {
99
+ const range = fromRef ? `${fromRef}..${toRef}` : toRef;
100
+ const text = git(["log", range, "--format=%H%x09%ct%x09%s", "--no-merges"]);
101
+ return parseTsv(text, ["sha", "unix", "subject"]).map((row) => ({
102
+ ...row,
103
+ date: new Date(Number(row.unix) * 1000),
104
+ }));
105
+ }
106
+
107
+ const allTags = parseTsv(read(path.join(outDir, "tags.tsv")), ["name", "sha", "date", "unix"])
108
+ .map((tag) => ({ ...tag, at: new Date(tag.date), unix: Number(tag.unix) }))
109
+ .filter((tag) => tag.name && Number.isFinite(tag.unix))
110
+ .sort((a, b) => a.unix - b.unix);
111
+
112
+ const deployments = allTags.filter((tag) => tag.at >= start && tag.at <= now);
113
+ const leadSamples = [];
114
+ const deploymentRows = [];
115
+
116
+ for (const tag of deployments) {
117
+ const idx = allTags.findIndex((candidate) => candidate.name === tag.name);
118
+ const previous = idx > 0 ? allTags[idx - 1] : null;
119
+ const commits = commitsBetween(previous?.name, tag.name);
120
+ const samples = commits
121
+ .map((commit) => hoursBetween(commit.date, tag.at))
122
+ .filter((hours) => Number.isFinite(hours) && hours >= 0);
123
+ leadSamples.push(...samples);
124
+ deploymentRows.push({
125
+ tag: tag.name,
126
+ date: tag.date,
127
+ commitCount: commits.length,
128
+ medianLeadTimeHours: median(samples),
129
+ previousTag: previous?.name ?? null,
130
+ });
131
+ }
132
+
133
+ const remediationCommits = parseTsv(read(path.join(outDir, "remediation-commits.tsv")), ["sha", "unix", "author", "subject"])
134
+ .map((row) => ({ ...row, at: new Date(Number(row.unix) * 1000), source: "commit" }));
135
+
136
+ const prs = JSON.parse(read(path.join(outDir, "prs.json")) || "[]");
137
+ const hotfixPrs = prs
138
+ .filter((pr) => pr?.mergedAt && /(revert|rollback|hotfix|fix-forward)/i.test(pr.title ?? ""))
139
+ .map((pr) => ({
140
+ sha: `PR #${pr.number}`,
141
+ at: new Date(pr.mergedAt),
142
+ author: pr.author?.login ?? "",
143
+ subject: pr.title,
144
+ url: pr.url,
145
+ source: "pull-request",
146
+ }))
147
+ .filter((signal) => signal.at >= start && signal.at <= now);
148
+
149
+ const remediationSignals = [...remediationCommits, ...hotfixPrs]
150
+ .filter((signal) => signal.at >= start && signal.at <= now)
151
+ .sort((a, b) => a.at - b.at);
152
+
153
+ const failureSignals = [];
154
+ const failedDeploymentNames = new Set();
155
+ for (const signal of remediationSignals) {
156
+ const fixing = deployments.find((tag) => tag.at >= signal.at) ?? deployments[deployments.length - 1];
157
+ const failing = [...deployments].reverse().find((tag) => {
158
+ const diff = (signal.at.getTime() - tag.at.getTime()) / 36e5;
159
+ return diff >= 0 && diff <= hotfixWindowHours;
160
+ });
161
+ if (!failing || !fixing) continue;
162
+ if (fixing.at < signal.at) continue;
163
+ if (fixing.name === failing.name) continue;
164
+ failedDeploymentNames.add(failing.name);
165
+ failureSignals.push({
166
+ source: signal.source,
167
+ subject: signal.subject,
168
+ sha: signal.sha,
169
+ url: signal.url,
170
+ signalAt: signal.at.toISOString(),
171
+ failingTag: failing.name,
172
+ failingAt: failing.at.toISOString(),
173
+ fixingTag: fixing.name,
174
+ fixingAt: fixing.at.toISOString(),
175
+ recoveryHours: hoursBetween(failing.at, fixing.at),
176
+ });
177
+ }
178
+
179
+ const deploymentCount = deployments.length;
180
+ const deploymentFrequencyPerDay = deploymentCount / windowDays;
181
+ const leadTimeMedianHours = median(leadSamples);
182
+ const changeFailureRate = deploymentCount ? failedDeploymentNames.size / deploymentCount : 0;
183
+ const recoveryMedianHours = median(failureSignals.map((signal) => signal.recoveryHours));
184
+
185
+ const metrics = {
186
+ deploymentFrequency: {
187
+ label: "Deployment Frequency",
188
+ quality: "EXACT",
189
+ value: deploymentFrequencyPerDay,
190
+ display: `${Math.round(deploymentFrequencyPerDay * 100) / 100}/day`,
191
+ band: classifyDeploymentFrequency(deploymentFrequencyPerDay),
192
+ detail: `${deploymentCount} ${tagPattern} deployments over ${windowDays} days`,
193
+ source: `${tagPattern} git tags mapped to production releases`,
194
+ },
195
+ leadTimeForChanges: {
196
+ label: "Lead Time for Changes",
197
+ quality: "EXACT",
198
+ valueHours: leadTimeMedianHours,
199
+ display: fmtHours(leadTimeMedianHours),
200
+ band: classifyLeadTime(leadTimeMedianHours),
201
+ detail: `Median across ${leadSamples.length} non-merge commits included in release tags`,
202
+ source: "Commit timestamp to containing release tag timestamp",
203
+ },
204
+ changeFailureRate: {
205
+ label: "Change Failure Rate",
206
+ quality: "PROXY",
207
+ value: changeFailureRate,
208
+ display: fmtPct(changeFailureRate),
209
+ band: classifyChangeFailureRate(changeFailureRate),
210
+ detail: `${failedDeploymentNames.size} failed-release proxies / ${deploymentCount} deployments`,
211
+ source: `Revert, rollback, hotfix, and fix-forward signals within ${hotfixWindowHours}h of a release`,
212
+ },
213
+ failedDeploymentRecoveryTime: {
214
+ label: "Failed Deployment Recovery Time",
215
+ quality: "PROXY",
216
+ valueHours: recoveryMedianHours,
217
+ display: fmtHours(recoveryMedianHours),
218
+ band: classifyRecovery(recoveryMedianHours),
219
+ detail: `${failureSignals.length} remediation signal(s) paired to release tags`,
220
+ source: "Time from failed-release proxy tag to fixing tag",
221
+ },
222
+ };
223
+
224
+ const summary = {
225
+ repoName,
226
+ runDate,
227
+ generatedAt: new Date().toISOString(),
228
+ branch,
229
+ window: {
230
+ days: windowDays,
231
+ start: start.toISOString(),
232
+ end: now.toISOString(),
233
+ },
234
+ tagPattern,
235
+ hotfixWindowHours,
236
+ commit: read(path.join(outDir, "revision.txt")).trim(),
237
+ commitSummary: read(path.join(outDir, "revision-summary.txt")).trim(),
238
+ metrics,
239
+ deployments: deploymentRows,
240
+ failureSignals,
241
+ notes: [
242
+ "Deployment Frequency and Lead Time for Changes are exact only when the configured tag pattern maps 1:1 to production releases.",
243
+ "Change Failure Rate and Failed Deployment Recovery Time are proxy estimates. They can miss manual incidents and can include non-incident remediation work.",
244
+ "Use a formal incident tracker or production deployment event stream before treating CFR/MTTR as precise operational truth.",
245
+ ],
246
+ references: {
247
+ doraMetrics: "https://dora.dev/guides/dora-metrics/",
248
+ dora2024Report: "https://dora.dev/research/2024/dora-report/",
249
+ },
250
+ };
251
+
252
+ fs.writeFileSync(path.join(outDir, "summary.json"), JSON.stringify(summary, null, 2));
253
+ fs.writeFileSync(path.join(outDir, "latest-pointer.json"), JSON.stringify({
254
+ repoName,
255
+ runDate,
256
+ path: outDir,
257
+ latestJson: path.join(path.dirname(outDir), "latest.json"),
258
+ latestHtml: path.join(path.dirname(outDir), "latest.html"),
259
+ generatedAt: summary.generatedAt,
260
+ }, null, 2));
261
+
262
+ const dataJson = JSON.stringify(summary).replaceAll("</script", "<\\/script");
263
+ const bandClass = (band) => String(band ?? "Unknown").toLowerCase();
264
+ const metricCards = Object.values(metrics).map((metric) => `
265
+ <div class="metric ${esc(bandClass(metric.band))} ${esc(String(metric.quality).toLowerCase())}">
266
+ <span>${esc(metric.label)} <em>${esc(metric.quality)}</em></span>
267
+ <strong>${esc(metric.display)}</strong>
268
+ <b>${esc(metric.band)}</b>
269
+ <p>${esc(metric.detail)}</p>
270
+ </div>`).join("");
271
+
272
+ const deploymentRowsHtml = deploymentRows.slice(-30).reverse().map((row) => `
273
+ <tr>
274
+ <td>${esc(row.tag)}</td>
275
+ <td>${esc(row.date)}</td>
276
+ <td>${esc(row.commitCount)}</td>
277
+ <td>${esc(fmtHours(row.medianLeadTimeHours))}</td>
278
+ </tr>`).join("");
279
+
280
+ const failureRowsHtml = failureSignals.map((signal) => `
281
+ <tr>
282
+ <td>${esc(signal.failingTag)}</td>
283
+ <td>${esc(signal.fixingTag)}</td>
284
+ <td>${esc(fmtHours(signal.recoveryHours))}</td>
285
+ <td>${esc(signal.source)}</td>
286
+ <td>${signal.url ? `<a href="${esc(signal.url)}">${esc(signal.subject)}</a>` : esc(signal.subject)}</td>
287
+ </tr>`).join("") || `<tr><td colspan="5">No proxy remediation signals detected in this window.</td></tr>`;
288
+
289
+ const html = `<!doctype html>
290
+ <html lang="en">
291
+ <head>
292
+ <meta charset="utf-8" />
293
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
294
+ <title>DORA Metrics - ${esc(repoName)}</title>
295
+ <style>
296
+ 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; }
297
+ main { width: min(1180px, calc(100% - 32px)); margin: 0 auto; padding: 48px 0 80px; }
298
+ h1 { font-size: clamp(2rem, 5vw, 4rem); line-height: 1; margin: 0 0 12px; }
299
+ h2 { margin-top: 40px; }
300
+ p, .muted { color: #686660; }
301
+ .grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin: 28px 0; }
302
+ .metric, .card, .callout { background: #fff; border: 1px solid #e4ded3; border-radius: 8px; padding: 18px; }
303
+ .metric span { display: flex; justify-content: space-between; gap: 8px; color: #686660; font-size: 12px; font-weight: 700; text-transform: uppercase; }
304
+ .metric em { color: #181817; font-style: normal; }
305
+ .metric strong { display: block; margin-top: 8px; font-size: 30px; }
306
+ .metric b { display: inline-block; margin-top: 8px; padding: 2px 8px; border-radius: 999px; background: #f0ece5; font-size: 12px; text-transform: uppercase; }
307
+ .metric.proxy { border-color: #d4a84f; }
308
+ .charts { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
309
+ .chart { height: 320px; }
310
+ svg { width: 100%; height: 100%; display: block; }
311
+ table { width: 100%; border-collapse: collapse; font-size: 13px; background: #fff; border: 1px solid #e4ded3; border-radius: 8px; overflow: hidden; }
312
+ th, td { padding: 8px 10px; border-bottom: 1px solid #eee8dc; text-align: left; vertical-align: top; }
313
+ th { color: #686660; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }
314
+ td { max-width: 520px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
315
+ code { background: #fff; border: 1px solid #e4ded3; padding: 1px 5px; border-radius: 4px; }
316
+ a { color: #8b5a00; }
317
+ @media (max-width: 920px) { .grid, .charts { grid-template-columns: 1fr; } }
318
+ </style>
319
+ </head>
320
+ <body>
321
+ <main>
322
+ <p class="muted">Generated ${esc(summary.generatedAt)}</p>
323
+ <h1>DORA Metrics</h1>
324
+ <p><strong>${esc(repoName)}</strong> at <code>${esc(summary.commit.slice(0, 12))}</code>. Window: ${esc(summary.window.start.slice(0, 10))} to ${esc(summary.window.end.slice(0, 10))}. Release signal: <code>${esc(tagPattern)}</code> tags.</p>
325
+
326
+ <section class="grid">
327
+ ${metricCards}
328
+ </section>
329
+
330
+ <section class="callout">
331
+ <h2>Data-source reality check</h2>
332
+ <p>Deployment Frequency and Lead Time for Changes are <strong>exact</strong> for this configuration because release tags are treated as production deployments. Change Failure Rate and Failed Deployment Recovery Time are <strong>proxy estimates</strong> from remediation signals. They are useful for trend watching, not a substitute for incident tracking.</p>
333
+ </section>
334
+
335
+ <section class="charts">
336
+ <div class="card">
337
+ <h2>Deployments by week</h2>
338
+ <div id="deployments" class="chart"></div>
339
+ </div>
340
+ <div class="card">
341
+ <h2>Lead time by release</h2>
342
+ <div id="leadtime" class="chart"></div>
343
+ </div>
344
+ </section>
345
+
346
+ <h2>Recent deployments</h2>
347
+ <table>
348
+ <thead><tr><th>Tag</th><th>Date</th><th>Commits</th><th>Median lead time</th></tr></thead>
349
+ <tbody>${deploymentRowsHtml}</tbody>
350
+ </table>
351
+
352
+ <h2>Proxy failure signals</h2>
353
+ <table>
354
+ <thead><tr><th>Failing tag</th><th>Fixing tag</th><th>Recovery</th><th>Source</th><th>Signal</th></tr></thead>
355
+ <tbody>${failureRowsHtml}</tbody>
356
+ </table>
357
+
358
+ <h2>References</h2>
359
+ <ul>
360
+ <li><a href="https://dora.dev/guides/dora-metrics/">DORA metrics guide</a></li>
361
+ <li><a href="https://dora.dev/research/2024/dora-report/">2024 Accelerate State of DevOps Report</a></li>
362
+ </ul>
363
+ </main>
364
+ <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
365
+ <script id="report-data" type="application/json">${dataJson}</script>
366
+ <script>
367
+ const data = JSON.parse(document.getElementById("report-data").textContent);
368
+ const deployments = data.deployments.map((d) => ({ ...d, dateObj: new Date(d.date), lead: d.medianLeadTimeHours || 0 }));
369
+ function drawBars(id, values, getX, getY) {
370
+ const el = document.getElementById(id);
371
+ const width = el.clientWidth || 520;
372
+ const height = el.clientHeight || 320;
373
+ const margin = { top: 16, right: 16, bottom: 42, left: 46 };
374
+ const svg = d3.select(el).append("svg").attr("viewBox", [0, 0, width, height]);
375
+ const x = d3.scaleBand().domain(values.map(getX)).range([margin.left, width - margin.right]).padding(0.2);
376
+ const y = d3.scaleLinear().domain([0, d3.max(values, getY) || 1]).nice().range([height - margin.bottom, margin.top]);
377
+ svg.append("g").attr("transform", "translate(0," + (height - margin.bottom) + ")").call(d3.axisBottom(x).tickValues(x.domain().filter((_, i) => i % Math.ceil(x.domain().length / 6) === 0))).selectAll("text").attr("transform", "rotate(-35)").style("text-anchor", "end");
378
+ svg.append("g").attr("transform", "translate(" + margin.left + ",0)").call(d3.axisLeft(y).ticks(5));
379
+ svg.append("g").selectAll("rect").data(values).join("rect")
380
+ .attr("x", (d) => x(getX(d)))
381
+ .attr("y", (d) => y(getY(d)))
382
+ .attr("width", x.bandwidth())
383
+ .attr("height", (d) => y(0) - y(getY(d)))
384
+ .attr("fill", "#b58118");
385
+ }
386
+ const byWeek = Array.from(d3.rollup(deployments, (v) => v.length, (d) => d3.utcFormat("%Y-W%U")(d.dateObj)), ([week, count]) => ({ week, count }));
387
+ drawBars("deployments", byWeek, (d) => d.week, (d) => d.count);
388
+ drawBars("leadtime", deployments.slice(-20), (d) => d.tag, (d) => d.lead);
389
+ </script>
390
+ </body>
391
+ </html>`;
392
+
393
+ fs.writeFileSync(path.join(outDir, "report.html"), html);
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # DORA metrics recurring 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 22 * * 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
+ # Data-source note:
18
+ # Deployment Frequency and Lead Time for Changes are exact for repositories
19
+ # where v* tags map 1:1 to production releases. Change Failure Rate and Failed
20
+ # Deployment Recovery Time are proxy estimates from revert/hotfix signals.
21
+
22
+ BASE_DIR="${BASE_DIR:-/workspace/dora-metrics}"
23
+ REPO_NAME="${REPO_NAME:-my-repo}"
24
+ REPO_URL="${REPO_URL:-https://github.com/OWNER/REPO.git}"
25
+ BRANCH="${BRANCH:-main}"
26
+ LOCAL_SOURCE="${LOCAL_SOURCE:-}"
27
+ WINDOW_DAYS="${WINDOW_DAYS:-90}"
28
+ HOTFIX_WINDOW_HOURS="${HOTFIX_WINDOW_HOURS:-24}"
29
+ TAG_PATTERN="${TAG_PATTERN:-v*}"
30
+
31
+ REPO_DIR="$BASE_DIR/repos/$REPO_NAME"
32
+ OUT_ROOT="$BASE_DIR/out/$REPO_NAME"
33
+ RUN_DATE="${RUN_DATE:-$(date -u +%F)}"
34
+ OUT_DIR="$OUT_ROOT/$RUN_DATE"
35
+
36
+ mkdir -p "$BASE_DIR/repos" "$OUT_DIR"
37
+
38
+ need_cmd() {
39
+ command -v "$1" >/dev/null 2>&1
40
+ }
41
+
42
+ ensure_cmd() {
43
+ local cmd="$1"
44
+ local pkg="${2:-$1}"
45
+ if ! need_cmd "$cmd"; then
46
+ sudo apt-get update
47
+ sudo apt-get install -y "$pkg"
48
+ fi
49
+ }
50
+
51
+ prepare_repo() {
52
+ if [ -d "$REPO_DIR/.git" ]; then
53
+ git -C "$REPO_DIR" remote set-url --push origin DISABLED >/dev/null 2>&1 || true
54
+ git -C "$REPO_DIR" fetch origin "$BRANCH" --prune
55
+ git -C "$REPO_DIR" fetch origin "refs/tags/$TAG_PATTERN:refs/tags/$TAG_PATTERN" --prune --force || true
56
+ git -C "$REPO_DIR" checkout -q "$BRANCH" || git -C "$REPO_DIR" checkout -q -B "$BRANCH" "origin/$BRANCH"
57
+ git -C "$REPO_DIR" reset --hard "origin/$BRANCH" >/dev/null
58
+ git -C "$REPO_DIR" clean -fdx >/dev/null
59
+ return
60
+ fi
61
+
62
+ if [ -n "$LOCAL_SOURCE" ] && [ -d "$LOCAL_SOURCE/.git" ]; then
63
+ git clone --no-hardlinks "$LOCAL_SOURCE" "$REPO_DIR"
64
+ git -C "$REPO_DIR" remote set-url origin "$REPO_URL" || true
65
+ git -C "$REPO_DIR" remote set-url --push origin DISABLED || true
66
+ git -C "$REPO_DIR" fetch origin "$BRANCH" --prune
67
+ git -C "$REPO_DIR" fetch origin "refs/tags/$TAG_PATTERN:refs/tags/$TAG_PATTERN" --prune --force || true
68
+ git -C "$REPO_DIR" checkout -q "$BRANCH" || git -C "$REPO_DIR" checkout -q -B "$BRANCH" "origin/$BRANCH"
69
+ git -C "$REPO_DIR" reset --hard "origin/$BRANCH" >/dev/null
70
+ else
71
+ git clone --branch "$BRANCH" "$REPO_URL" "$REPO_DIR"
72
+ git -C "$REPO_DIR" remote set-url --push origin DISABLED || true
73
+ git -C "$REPO_DIR" fetch origin "refs/tags/$TAG_PATTERN:refs/tags/$TAG_PATTERN" --prune --force || true
74
+ fi
75
+ }
76
+
77
+ write_pr_metadata() {
78
+ local slug
79
+ slug="$(printf '%s\n' "$REPO_URL" | sed -E 's#^git@github.com:##; s#^https://github.com/##; s#\.git$##')"
80
+ if need_cmd gh && gh auth status >/dev/null 2>&1 && printf '%s\n' "$slug" | grep -Eq '^[^/]+/[^/]+$'; then
81
+ gh pr list \
82
+ --repo "$slug" \
83
+ --state merged \
84
+ --base "$BRANCH" \
85
+ --limit 1000 \
86
+ --json number,title,mergedAt,url,author,headRefName \
87
+ > "$OUT_DIR/prs.json" || printf '[]\n' > "$OUT_DIR/prs.json"
88
+ else
89
+ printf '[]\n' > "$OUT_DIR/prs.json"
90
+ fi
91
+ }
92
+
93
+ ensure_cmd git git
94
+ ensure_cmd jq jq
95
+ ensure_cmd node nodejs
96
+ prepare_repo
97
+
98
+ git -C "$REPO_DIR" rev-parse HEAD > "$OUT_DIR/revision.txt"
99
+ git -C "$REPO_DIR" log -1 --format='%h %ad %an %s' --date=short > "$OUT_DIR/revision-summary.txt"
100
+ git -C "$REPO_DIR" for-each-ref "refs/tags/$TAG_PATTERN" --sort=creatordate --format='%(refname:short)%09%(objectname)%09%(creatordate:iso-strict)%09%(creatordate:unix)' > "$OUT_DIR/tags.tsv"
101
+ git -C "$REPO_DIR" log "origin/$BRANCH" --since="$WINDOW_DAYS days ago" --format='%H%x09%ct%x09%an%x09%s' > "$OUT_DIR/recent-commits.tsv"
102
+ git -C "$REPO_DIR" log "origin/$BRANCH" --since="$WINDOW_DAYS days ago" --grep='revert' --grep='rollback' --grep='hotfix' --grep='fix-forward' --regexp-ignore-case --format='%H%x09%ct%x09%an%x09%s' > "$OUT_DIR/remediation-commits.tsv"
103
+ write_pr_metadata
104
+
105
+ node "$BASE_DIR/report.mjs" "$OUT_DIR" "$REPO_DIR" "$REPO_NAME" "$RUN_DATE" "$BRANCH" "$WINDOW_DAYS" "$HOTFIX_WINDOW_HOURS" "$TAG_PATTERN"
106
+
107
+ cp "$OUT_DIR/summary.json" "$OUT_ROOT/latest.json"
108
+ cp "$OUT_DIR/report.html" "$OUT_ROOT/latest.html"
109
+ cp "$OUT_DIR/latest-pointer.json" "$OUT_ROOT/latest-pointer.json"
110
+
111
+ echo "Wrote $OUT_DIR"
112
+ echo "Latest summary: $OUT_ROOT/latest.json"
113
+ echo "Latest page HTML: $OUT_ROOT/latest.html"
@@ -0,0 +1,13 @@
1
+ {
2
+ "kind": "schedule",
3
+ "name": "weekly-dora-metrics",
4
+ "displayName": "Weekly DORA Metrics",
5
+ "slug": "weekly-dora-metrics",
6
+ "title": "Weekly DORA Metrics",
7
+ "description": "Gallery entry point for the community DORA metrics report package. Run a weekly DORA report and update a stable page in place.",
8
+ "version": "1.0.0",
9
+ "category": "schedules",
10
+ "placeholders": ["REPO_URL", "BRANCH", "TAG_PATTERN", "REPORT_NAME", "PAGE_ID", "TIMEZONE"],
11
+ "runAllSeedersCandidate": false,
12
+ "tags": ["dora", "metrics", "engineering-productivity", "reporting", "community-template"]
13
+ }
@@ -0,0 +1,61 @@
1
+ # Weekly DORA Metrics
2
+
3
+ Run a recurring DORA metrics 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/dora-metrics/`. 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 22 * * 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, branch, release tag pattern, report name, page ID, timezone, and worker targeting details with values for your project.
21
+
22
+ Task Type: Weekly DORA Metrics
23
+
24
+ Repository: `https://github.com/OWNER/REPO.git`
25
+ Default branch: `main`
26
+ Release tag pattern: `v*`
27
+ Report name: `my-repo`
28
+ Stable page ID: `<PAGE_ID>`
29
+
30
+ ## Instructions
31
+
32
+ 1. Use the community template in `templates/community/dora-metrics/`. See the docs playbook at `https://docs.agent-swarm.dev/docs/playbooks/dora-metrics` for the full setup, metric-definition notes, and proxy caveats.
33
+ 2. Install or update the runner workspace under `/workspace/dora-metrics`:
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/dora-metrics \
41
+ REPO_NAME=my-repo \
42
+ REPO_URL=https://github.com/OWNER/REPO.git \
43
+ BRANCH=main \
44
+ TAG_PATTERN='v*' \
45
+ WINDOW_DAYS=90 \
46
+ bash /workspace/dora-metrics/run.sh
47
+ ```
48
+
49
+ 4. Publish `/workspace/dora-metrics/out/my-repo/latest.html` to the existing stable page ID. Do not create a new page for routine refreshes.
50
+ 5. Verify the rendered page loads, D3 charts are not blank, and the exact/proxy labels are visible.
51
+ 6. If the run fails because of a local runner, dependency, branch, tag-pattern, or report-generator issue, diagnose and fix it before reporting failure.
52
+
53
+ ## Cadence
54
+
55
+ The default cron is `0 22 * * 0`, weekly on Sunday at 22: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.
56
+
57
+ The cadence is separate from page identity. Keep updating the same page ID so the report URL remains stable.
58
+
59
+ ## Completion
60
+
61
+ Call `store-progress` with status `completed` and an output summary that includes the stable page URL, report workspace path, commit analyzed, generated timestamp, and the data-source caveat: Deployment Frequency and Lead Time are exact from release tags/commits; Change Failure Rate and Failed Deployment Recovery Time are proxy estimates from revert/hotfix-style remediation signals.