omegon 0.6.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 (160) hide show
  1. package/.gitattributes +3 -0
  2. package/AGENTS.md +16 -0
  3. package/LICENSE +15 -0
  4. package/README.md +289 -0
  5. package/bin/pi.mjs +30 -0
  6. package/extensions/00-secrets/index.ts +1126 -0
  7. package/extensions/01-auth/auth.ts +401 -0
  8. package/extensions/01-auth/index.ts +289 -0
  9. package/extensions/auto-compact.ts +42 -0
  10. package/extensions/bootstrap/deps.ts +291 -0
  11. package/extensions/bootstrap/index.ts +811 -0
  12. package/extensions/chronos/chronos.sh +487 -0
  13. package/extensions/chronos/index.ts +148 -0
  14. package/extensions/cleave/assessment.ts +754 -0
  15. package/extensions/cleave/bridge.ts +31 -0
  16. package/extensions/cleave/conflicts.ts +250 -0
  17. package/extensions/cleave/dispatcher.ts +808 -0
  18. package/extensions/cleave/guardrails.ts +426 -0
  19. package/extensions/cleave/index.ts +3121 -0
  20. package/extensions/cleave/lifecycle-emitter.ts +20 -0
  21. package/extensions/cleave/openspec.ts +811 -0
  22. package/extensions/cleave/planner.ts +260 -0
  23. package/extensions/cleave/review.ts +579 -0
  24. package/extensions/cleave/skills.ts +355 -0
  25. package/extensions/cleave/types.ts +261 -0
  26. package/extensions/cleave/workspace.ts +861 -0
  27. package/extensions/cleave/worktree.ts +243 -0
  28. package/extensions/core-renderers.ts +253 -0
  29. package/extensions/dashboard/context-gauge.ts +58 -0
  30. package/extensions/dashboard/file-watch.ts +14 -0
  31. package/extensions/dashboard/footer.ts +1145 -0
  32. package/extensions/dashboard/git.ts +185 -0
  33. package/extensions/dashboard/index.ts +478 -0
  34. package/extensions/dashboard/memory-audit.ts +34 -0
  35. package/extensions/dashboard/overlay-data.ts +705 -0
  36. package/extensions/dashboard/overlay.ts +365 -0
  37. package/extensions/dashboard/render-utils.ts +54 -0
  38. package/extensions/dashboard/types.ts +191 -0
  39. package/extensions/dashboard/uri-helper.ts +45 -0
  40. package/extensions/debug.ts +69 -0
  41. package/extensions/defaults.ts +282 -0
  42. package/extensions/design-tree/dashboard-state.ts +161 -0
  43. package/extensions/design-tree/design-card.ts +362 -0
  44. package/extensions/design-tree/index.ts +2130 -0
  45. package/extensions/design-tree/lifecycle-emitter.ts +41 -0
  46. package/extensions/design-tree/tree.ts +1607 -0
  47. package/extensions/design-tree/types.ts +163 -0
  48. package/extensions/distill.ts +127 -0
  49. package/extensions/effort/index.ts +395 -0
  50. package/extensions/effort/tiers.ts +146 -0
  51. package/extensions/effort/types.ts +105 -0
  52. package/extensions/lib/git-state.ts +227 -0
  53. package/extensions/lib/local-models.ts +157 -0
  54. package/extensions/lib/model-preferences.ts +51 -0
  55. package/extensions/lib/model-routing.ts +720 -0
  56. package/extensions/lib/operator-fallback.ts +205 -0
  57. package/extensions/lib/operator-profile.ts +360 -0
  58. package/extensions/lib/slash-command-bridge.ts +253 -0
  59. package/extensions/lib/typebox-helpers.ts +16 -0
  60. package/extensions/local-inference/index.ts +727 -0
  61. package/extensions/mcp-bridge/README.md +220 -0
  62. package/extensions/mcp-bridge/index.ts +951 -0
  63. package/extensions/mcp-bridge/lib.ts +365 -0
  64. package/extensions/mcp-bridge/mcp.json +3 -0
  65. package/extensions/mcp-bridge/package.json +11 -0
  66. package/extensions/model-budget.ts +752 -0
  67. package/extensions/offline-driver.ts +403 -0
  68. package/extensions/openspec/archive-gate.ts +164 -0
  69. package/extensions/openspec/branch-cleanup.ts +64 -0
  70. package/extensions/openspec/dashboard-state.ts +50 -0
  71. package/extensions/openspec/index.ts +1917 -0
  72. package/extensions/openspec/lifecycle-emitter.ts +65 -0
  73. package/extensions/openspec/lifecycle-files.ts +70 -0
  74. package/extensions/openspec/lifecycle.ts +50 -0
  75. package/extensions/openspec/reconcile.ts +187 -0
  76. package/extensions/openspec/spec.ts +1385 -0
  77. package/extensions/openspec/types.ts +98 -0
  78. package/extensions/project-memory/DESIGN-global-mind.md +198 -0
  79. package/extensions/project-memory/README.md +202 -0
  80. package/extensions/project-memory/api-types.ts +382 -0
  81. package/extensions/project-memory/compaction-policy.ts +29 -0
  82. package/extensions/project-memory/core.ts +164 -0
  83. package/extensions/project-memory/embeddings.ts +230 -0
  84. package/extensions/project-memory/extraction-v2.ts +861 -0
  85. package/extensions/project-memory/factstore.ts +2177 -0
  86. package/extensions/project-memory/index.ts +3459 -0
  87. package/extensions/project-memory/injection-metrics.ts +91 -0
  88. package/extensions/project-memory/jsonl-io.ts +12 -0
  89. package/extensions/project-memory/lifecycle.ts +331 -0
  90. package/extensions/project-memory/migration.ts +293 -0
  91. package/extensions/project-memory/package.json +9 -0
  92. package/extensions/project-memory/sci-renderers.ts +7 -0
  93. package/extensions/project-memory/template.ts +103 -0
  94. package/extensions/project-memory/triggers.ts +52 -0
  95. package/extensions/project-memory/types.ts +102 -0
  96. package/extensions/render/composition/fonts/Inter-Bold.ttf +0 -0
  97. package/extensions/render/composition/fonts/Inter-Regular.ttf +0 -0
  98. package/extensions/render/composition/fonts/Tomorrow-Bold.ttf +0 -0
  99. package/extensions/render/composition/fonts/Tomorrow-Regular.ttf +0 -0
  100. package/extensions/render/composition/package-lock.json +534 -0
  101. package/extensions/render/composition/package.json +22 -0
  102. package/extensions/render/composition/render.mjs +246 -0
  103. package/extensions/render/composition/test-comp.tsx +87 -0
  104. package/extensions/render/composition/types.ts +24 -0
  105. package/extensions/render/excalidraw/UPSTREAM.md +81 -0
  106. package/extensions/render/excalidraw/elements.ts +764 -0
  107. package/extensions/render/excalidraw/index.ts +66 -0
  108. package/extensions/render/excalidraw/types.ts +223 -0
  109. package/extensions/render/excalidraw-renderer/pyproject.toml +8 -0
  110. package/extensions/render/excalidraw-renderer/render_excalidraw.py +182 -0
  111. package/extensions/render/excalidraw-renderer/render_template.html +59 -0
  112. package/extensions/render/index.ts +830 -0
  113. package/extensions/render/native-diagrams/index.ts +57 -0
  114. package/extensions/render/native-diagrams/motifs.ts +542 -0
  115. package/extensions/render/native-diagrams/raster.ts +8 -0
  116. package/extensions/render/native-diagrams/scene.ts +75 -0
  117. package/extensions/render/native-diagrams/spec.ts +204 -0
  118. package/extensions/render/native-diagrams/svg.ts +116 -0
  119. package/extensions/sci-ui.ts +304 -0
  120. package/extensions/session-log.ts +174 -0
  121. package/extensions/shared-state.ts +146 -0
  122. package/extensions/spinner-verbs.ts +91 -0
  123. package/extensions/style.ts +281 -0
  124. package/extensions/terminal-title.ts +191 -0
  125. package/extensions/tool-profile/index.ts +291 -0
  126. package/extensions/tool-profile/profiles.ts +290 -0
  127. package/extensions/types.d.ts +9 -0
  128. package/extensions/vault/index.ts +185 -0
  129. package/extensions/version-check.ts +90 -0
  130. package/extensions/view/index.ts +859 -0
  131. package/extensions/view/uri-resolver.ts +148 -0
  132. package/extensions/web-search/index.ts +182 -0
  133. package/extensions/web-search/providers.ts +121 -0
  134. package/extensions/web-ui/index.ts +110 -0
  135. package/extensions/web-ui/server.ts +265 -0
  136. package/extensions/web-ui/state.ts +462 -0
  137. package/extensions/web-ui/static/index.html +145 -0
  138. package/extensions/web-ui/types.ts +284 -0
  139. package/package.json +76 -0
  140. package/prompts/init.md +75 -0
  141. package/prompts/new-repo.md +54 -0
  142. package/prompts/oci-login.md +56 -0
  143. package/prompts/status.md +50 -0
  144. package/settings.json +4 -0
  145. package/skills/cleave/SKILL.md +218 -0
  146. package/skills/git/SKILL.md +209 -0
  147. package/skills/git/_reference/ci-validation.md +204 -0
  148. package/skills/oci/SKILL.md +338 -0
  149. package/skills/openspec/SKILL.md +346 -0
  150. package/skills/pi-extensions/SKILL.md +191 -0
  151. package/skills/pi-tui/SKILL.md +517 -0
  152. package/skills/python/SKILL.md +189 -0
  153. package/skills/rust/SKILL.md +268 -0
  154. package/skills/security/SKILL.md +206 -0
  155. package/skills/style/SKILL.md +264 -0
  156. package/skills/typescript/SKILL.md +225 -0
  157. package/skills/vault/SKILL.md +102 -0
  158. package/themes/alpharius-legacy.json +85 -0
  159. package/themes/alpharius.conf +59 -0
  160. package/themes/alpharius.json +88 -0
@@ -0,0 +1,426 @@
1
+ /**
2
+ * cleave/guardrails — Deterministic guardrail discovery, execution, and formatting.
3
+ *
4
+ * Discovers checks from package.json scripts, auto-detection of build tools,
5
+ * and skill frontmatter. Runs them and formats results for injection into
6
+ * review loops, post-merge checks, and /assess commands.
7
+ */
8
+
9
+ import { execSync } from "node:child_process";
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+
13
+ // ─── Types ───────────────────────────────────────────────────────────────────
14
+
15
+ export interface GuardrailCheck {
16
+ name: string;
17
+ cmd: string;
18
+ timeout: number;
19
+ source: "package-script" | "skill-frontmatter" | "auto-detect";
20
+ }
21
+
22
+ export interface GuardrailResult {
23
+ check: GuardrailCheck;
24
+ passed: boolean;
25
+ exitCode: number;
26
+ output: string;
27
+ durationMs: number;
28
+ }
29
+
30
+ export interface GuardrailSuite {
31
+ results: GuardrailResult[];
32
+ allPassed: boolean;
33
+ durationMs: number;
34
+ }
35
+
36
+ export interface SkillGuardrailEntry {
37
+ name: string;
38
+ cmd: string;
39
+ timeout?: number;
40
+ condition?: string;
41
+ }
42
+
43
+ // ─── Frontmatter Parsing ─────────────────────────────────────────────────────
44
+
45
+ /**
46
+ * Parse YAML frontmatter from a SKILL.md file, extracting the `guardrails:` array.
47
+ * Uses simple line-based parsing — no yaml dependency.
48
+ */
49
+ export function parseSkillFrontmatter(content: string): {
50
+ guardrails?: SkillGuardrailEntry[];
51
+ } {
52
+ const lines = content.split("\n");
53
+ if (lines[0]?.trim() !== "---") return {};
54
+
55
+ let endIdx = -1;
56
+ for (let i = 1; i < lines.length; i++) {
57
+ if (lines[i]?.trim() === "---") {
58
+ endIdx = i;
59
+ break;
60
+ }
61
+ }
62
+ if (endIdx < 0) return {};
63
+
64
+ const fmLines = lines.slice(1, endIdx);
65
+
66
+ // Find guardrails: key
67
+ let guardrailStart = -1;
68
+ for (let i = 0; i < fmLines.length; i++) {
69
+ if (fmLines[i]?.match(/^guardrails:\s*$/)) {
70
+ guardrailStart = i + 1;
71
+ break;
72
+ }
73
+ }
74
+ if (guardrailStart < 0) return {};
75
+
76
+ const entries: SkillGuardrailEntry[] = [];
77
+ let current: Partial<SkillGuardrailEntry> | null = null;
78
+
79
+ for (let i = guardrailStart; i < fmLines.length; i++) {
80
+ const line = fmLines[i] ?? "";
81
+ // Stop if we hit a top-level key (no leading whitespace, has colon)
82
+ if (line.match(/^\S/) && line.includes(":")) break;
83
+
84
+ const itemMatch = line.match(/^\s*-\s+(\w+):\s*(.+)$/);
85
+ if (itemMatch) {
86
+ // New array item starting with first property
87
+ if (current?.name && current?.cmd) {
88
+ entries.push(current as SkillGuardrailEntry);
89
+ }
90
+ current = {};
91
+ const key = itemMatch[1] as string;
92
+ const val = itemMatch[2] as string;
93
+ assignEntry(current, key, val);
94
+ continue;
95
+ }
96
+
97
+ const propMatch = line.match(/^\s+(\w+):\s*(.+)$/);
98
+ if (propMatch && current) {
99
+ const key = propMatch[1] as string;
100
+ const val = propMatch[2] as string;
101
+ assignEntry(current, key, val);
102
+ }
103
+ }
104
+
105
+ if (current?.name && current?.cmd) {
106
+ entries.push(current as SkillGuardrailEntry);
107
+ }
108
+
109
+ return entries.length > 0 ? { guardrails: entries } : {};
110
+ }
111
+
112
+ function assignEntry(
113
+ entry: Partial<SkillGuardrailEntry>,
114
+ key: string,
115
+ val: string
116
+ ): void {
117
+ switch (key) {
118
+ case "name":
119
+ entry.name = val.trim();
120
+ break;
121
+ case "cmd":
122
+ entry.cmd = val.trim();
123
+ break;
124
+ case "timeout":
125
+ entry.timeout = parseInt(val.trim(), 10) || undefined;
126
+ break;
127
+ case "condition":
128
+ entry.condition = val.trim();
129
+ break;
130
+ }
131
+ }
132
+
133
+ // ─── Condition Evaluation ────────────────────────────────────────────────────
134
+
135
+ /**
136
+ * Evaluate a condition string. Currently supports:
137
+ * - `file_exists(path)` — checks if the file exists relative to cwd
138
+ */
139
+ export function evaluateCondition(condition: string, cwd: string): boolean {
140
+ const match = condition.match(/^file_exists\((.+)\)$/);
141
+ if (match) {
142
+ const filePath = match[1] as string;
143
+ return existsSync(join(cwd, filePath));
144
+ }
145
+ // Unknown condition — default to true (include the check)
146
+ return true;
147
+ }
148
+
149
+ // ─── Project Skill Resolution ────────────────────────────────────────────────
150
+
151
+ /**
152
+ * Quick scan: which skills are relevant to this project based on files present?
153
+ * Returns skill names (not full paths — use resolveSkillPath from skills.ts for that).
154
+ */
155
+ function detectProjectSkills(cwd: string): string[] {
156
+ const skills: string[] = [];
157
+ if (existsSync(join(cwd, "tsconfig.json")) || existsSync(join(cwd, "package.json"))) {
158
+ skills.push("typescript");
159
+ }
160
+ if (existsSync(join(cwd, "pyproject.toml")) || existsSync(join(cwd, "setup.py"))) {
161
+ skills.push("python");
162
+ }
163
+ if (existsSync(join(cwd, "Cargo.toml"))) {
164
+ skills.push("rust");
165
+ }
166
+ return skills;
167
+ }
168
+
169
+ /**
170
+ * Resolve project-relevant skill SKILL.md paths for guardrail frontmatter parsing.
171
+ * Uses the same search logic as skills.ts resolveSkillPath.
172
+ */
173
+ export function resolveProjectSkillPaths(cwd: string): string[] {
174
+ const skillNames = detectProjectSkills(cwd);
175
+ const paths: string[] = [];
176
+ const home = process.env.HOME || process.env.USERPROFILE || "";
177
+
178
+ for (const name of skillNames) {
179
+ // Search well-known skill directories (mirrors skills.ts getSkillSearchPaths)
180
+ const candidates = [
181
+ join(cwd, "skills", name, "SKILL.md"),
182
+ ...(home ? [
183
+ join(home, ".pi", "agent", "skills", name, "SKILL.md"),
184
+ ] : []),
185
+ ];
186
+ for (const c of candidates) {
187
+ if (existsSync(c)) {
188
+ paths.push(c);
189
+ break;
190
+ }
191
+ }
192
+ }
193
+ return paths;
194
+ }
195
+
196
+ // ─── Discovery ───────────────────────────────────────────────────────────────
197
+
198
+ /**
199
+ * Discover guardrail checks from package.json, auto-detection, and skill frontmatter.
200
+ * Priority: package-script > auto-detect > skill-frontmatter (dedup by name).
201
+ *
202
+ * If skillPaths is not provided, automatically resolves project-relevant skills.
203
+ */
204
+ export function discoverGuardrails(
205
+ cwd: string,
206
+ skillPaths?: string[]
207
+ ): GuardrailCheck[] {
208
+ const checks: GuardrailCheck[] = [];
209
+ const seen = new Map<string, GuardrailCheck["source"]>();
210
+
211
+ function addCheck(check: GuardrailCheck): void {
212
+ const existing = seen.get(check.name);
213
+ if (existing) {
214
+ // Priority: package-script > auto-detect > skill-frontmatter
215
+ const priority: Record<GuardrailCheck["source"], number> = {
216
+ "package-script": 3,
217
+ "auto-detect": 2,
218
+ "skill-frontmatter": 1,
219
+ };
220
+ if (priority[check.source] <= priority[existing]) return;
221
+ // Replace lower-priority entry
222
+ const idx = checks.findIndex((c) => c.name === check.name);
223
+ if (idx >= 0) checks[idx] = check;
224
+ seen.set(check.name, check.source);
225
+ return;
226
+ }
227
+ checks.push(check);
228
+ seen.set(check.name, check.source);
229
+ }
230
+
231
+ // 1. package.json scripts
232
+ const pkgPath = join(cwd, "package.json");
233
+ if (existsSync(pkgPath)) {
234
+ try {
235
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as {
236
+ scripts?: Record<string, string>;
237
+ };
238
+ const scripts = pkg.scripts ?? {};
239
+ if (scripts.typecheck) {
240
+ addCheck({
241
+ name: "typecheck",
242
+ cmd: scripts.typecheck,
243
+ timeout: 30,
244
+ source: "package-script",
245
+ });
246
+ }
247
+ if (scripts.lint) {
248
+ addCheck({
249
+ name: "lint",
250
+ cmd: scripts.lint,
251
+ timeout: 30,
252
+ source: "package-script",
253
+ });
254
+ }
255
+ } catch {
256
+ // Malformed package.json — skip
257
+ }
258
+ }
259
+
260
+ // 2. Auto-detection
261
+ if (!seen.has("typecheck") && existsSync(join(cwd, "tsconfig.json"))) {
262
+ addCheck({
263
+ name: "typecheck",
264
+ cmd: "npx tsc --noEmit",
265
+ timeout: 30,
266
+ source: "auto-detect",
267
+ });
268
+ }
269
+ if (existsSync(join(cwd, "pyproject.toml"))) {
270
+ addCheck({
271
+ name: "typecheck-python",
272
+ cmd: "mypy .",
273
+ timeout: 30,
274
+ source: "auto-detect",
275
+ });
276
+ }
277
+ if (existsSync(join(cwd, "Cargo.toml"))) {
278
+ addCheck({
279
+ name: "clippy",
280
+ cmd: "cargo clippy -- -D warnings",
281
+ timeout: 60,
282
+ source: "auto-detect",
283
+ });
284
+ }
285
+
286
+ // 3. Skill frontmatter — auto-resolve if not provided
287
+ const effectiveSkillPaths = skillPaths ?? resolveProjectSkillPaths(cwd);
288
+ if (effectiveSkillPaths.length > 0) {
289
+ for (const sp of effectiveSkillPaths) {
290
+ if (!existsSync(sp)) continue;
291
+ try {
292
+ const content = readFileSync(sp, "utf-8");
293
+ const fm = parseSkillFrontmatter(content);
294
+ if (fm.guardrails) {
295
+ for (const entry of fm.guardrails) {
296
+ // Evaluate condition
297
+ if (entry.condition && !evaluateCondition(entry.condition, cwd)) {
298
+ continue;
299
+ }
300
+ addCheck({
301
+ name: entry.name,
302
+ cmd: entry.cmd,
303
+ timeout: entry.timeout ?? 30,
304
+ source: "skill-frontmatter",
305
+ });
306
+ }
307
+ }
308
+ } catch {
309
+ // Unreadable skill file — skip
310
+ }
311
+ }
312
+ }
313
+
314
+ return checks;
315
+ }
316
+
317
+ // ─── Execution ───────────────────────────────────────────────────────────────
318
+
319
+ const MAX_OUTPUT_LINES = 50;
320
+
321
+ function capOutput(output: string): string {
322
+ const lines = output.split("\n");
323
+ if (lines.length <= MAX_OUTPUT_LINES) return output;
324
+ return (
325
+ lines.slice(0, MAX_OUTPUT_LINES).join("\n") +
326
+ `\n... (${lines.length - MAX_OUTPUT_LINES} more lines truncated)`
327
+ );
328
+ }
329
+
330
+ /**
331
+ * Run all guardrail checks sequentially and collect results.
332
+ */
333
+ export function runGuardrails(
334
+ cwd: string,
335
+ checks: GuardrailCheck[]
336
+ ): GuardrailSuite {
337
+ const suiteStart = Date.now();
338
+ const results: GuardrailResult[] = [];
339
+
340
+ for (const check of checks) {
341
+ const start = Date.now();
342
+ let passed = false;
343
+ let exitCode = 1;
344
+ let output = "";
345
+
346
+ try {
347
+ const result = execSync(check.cmd, {
348
+ cwd,
349
+ timeout: check.timeout * 1000,
350
+ encoding: "utf-8",
351
+ stdio: "pipe",
352
+ });
353
+ output = capOutput(result ?? "");
354
+ passed = true;
355
+ exitCode = 0;
356
+ } catch (err: unknown) {
357
+ const e = err as {
358
+ status?: number;
359
+ stdout?: string;
360
+ stderr?: string;
361
+ killed?: boolean;
362
+ message?: string;
363
+ };
364
+ exitCode = e.status ?? 1;
365
+ const parts: string[] = [];
366
+ if (e.stdout) parts.push(e.stdout);
367
+ if (e.stderr) parts.push(e.stderr);
368
+ output = capOutput(
369
+ parts.length > 0 ? parts.join("\n") : e.message ?? "Unknown error"
370
+ );
371
+ if (e.killed) {
372
+ output = `[TIMEOUT after ${check.timeout}s]\n${output}`;
373
+ }
374
+ }
375
+
376
+ results.push({
377
+ check,
378
+ passed,
379
+ exitCode,
380
+ output,
381
+ durationMs: Date.now() - start,
382
+ });
383
+ }
384
+
385
+ return {
386
+ results,
387
+ allPassed: results.every((r) => r.passed),
388
+ durationMs: Date.now() - suiteStart,
389
+ };
390
+ }
391
+
392
+ // ─── Formatting ──────────────────────────────────────────────────────────────
393
+
394
+ /**
395
+ * Format guardrail results as markdown for injection into prompts / reports.
396
+ */
397
+ export function formatGuardrailResults(suite: GuardrailSuite): string {
398
+ if (suite.allPassed) {
399
+ const names = suite.results.map((r) => r.check.name).join(", ");
400
+ return `✅ All deterministic checks passed (${names})`;
401
+ }
402
+
403
+ const failures = suite.results.filter((r) => !r.passed);
404
+ const lines: string[] = [
405
+ "These are confirmed issues from compiler/linter output — not opinions:",
406
+ "",
407
+ ];
408
+
409
+ for (const f of failures) {
410
+ lines.push(`### ❌ ${f.check.name} (exit code ${f.exitCode})`);
411
+ lines.push("");
412
+ lines.push("```");
413
+ lines.push(f.output);
414
+ lines.push("```");
415
+ lines.push("");
416
+ }
417
+
418
+ const passed = suite.results.filter((r) => r.passed);
419
+ if (passed.length > 0) {
420
+ lines.push(
421
+ `✅ Passed: ${passed.map((r) => r.check.name).join(", ")}`
422
+ );
423
+ }
424
+
425
+ return lines.join("\n");
426
+ }