@sentry/warden 0.0.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 (199) hide show
  1. package/.agents/skills/find-bugs/SKILL.md +75 -0
  2. package/.agents/skills/vercel-react-best-practices/AGENTS.md +2934 -0
  3. package/.agents/skills/vercel-react-best-practices/SKILL.md +136 -0
  4. package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  5. package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
  6. package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
  7. package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  8. package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
  9. package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
  10. package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  11. package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  12. package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
  13. package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  14. package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  15. package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  16. package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  17. package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  18. package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  19. package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  20. package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  21. package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
  22. package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  23. package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  24. package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  25. package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  26. package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  27. package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  28. package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  29. package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  30. package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  31. package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  32. package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  33. package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  34. package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  35. package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  36. package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  37. package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  38. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  39. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  40. package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  41. package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  42. package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  43. package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  44. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  45. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  46. package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  47. package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  48. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  49. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  50. package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  51. package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  52. package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  53. package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  54. package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  55. package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
  56. package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  57. package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  58. package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
  59. package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  60. package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  61. package/.claude/settings.json +57 -0
  62. package/.claude/settings.local.json +88 -0
  63. package/.claude/skills/agent-prompt/SKILL.md +54 -0
  64. package/.claude/skills/agent-prompt/references/agentic-patterns.md +94 -0
  65. package/.claude/skills/agent-prompt/references/anti-patterns.md +140 -0
  66. package/.claude/skills/agent-prompt/references/context-design.md +124 -0
  67. package/.claude/skills/agent-prompt/references/core-principles.md +75 -0
  68. package/.claude/skills/agent-prompt/references/model-guidance.md +118 -0
  69. package/.claude/skills/agent-prompt/references/output-formats.md +98 -0
  70. package/.claude/skills/agent-prompt/references/skill-structure.md +115 -0
  71. package/.claude/skills/agent-prompt/references/system-prompts.md +115 -0
  72. package/.claude/skills/notseer/SKILL.md +131 -0
  73. package/.claude/skills/skill-writer/SKILL.md +140 -0
  74. package/.claude/skills/testing-guidelines/SKILL.md +132 -0
  75. package/.claude/skills/warden-skill/SKILL.md +250 -0
  76. package/.claude/skills/warden-skill/references/config-schema.md +133 -0
  77. package/.dex/config.toml +2 -0
  78. package/.github/workflows/ci.yml +33 -0
  79. package/.github/workflows/release.yml +54 -0
  80. package/.github/workflows/warden.yml +40 -0
  81. package/AGENTS.md +89 -0
  82. package/CONTRIBUTING.md +60 -0
  83. package/LICENSE +105 -0
  84. package/README.md +43 -0
  85. package/SPEC.md +263 -0
  86. package/action.yml +87 -0
  87. package/assets/favicon.png +0 -0
  88. package/assets/warden-icon-bw.svg +5 -0
  89. package/assets/warden-icon-purple.png +0 -0
  90. package/assets/warden-icon-purple.svg +5 -0
  91. package/docs/.claude/settings.local.json +11 -0
  92. package/docs/astro.config.mjs +43 -0
  93. package/docs/package.json +19 -0
  94. package/docs/pnpm-lock.yaml +4000 -0
  95. package/docs/public/favicon.svg +5 -0
  96. package/docs/src/components/Code.astro +141 -0
  97. package/docs/src/components/PackageManagerTabs.astro +183 -0
  98. package/docs/src/components/Terminal.astro +212 -0
  99. package/docs/src/layouts/Base.astro +380 -0
  100. package/docs/src/pages/cli.astro +167 -0
  101. package/docs/src/pages/config.astro +394 -0
  102. package/docs/src/pages/guide.astro +449 -0
  103. package/docs/src/pages/index.astro +490 -0
  104. package/docs/src/styles/global.css +551 -0
  105. package/docs/tsconfig.json +3 -0
  106. package/docs/vercel.json +5 -0
  107. package/eslint.config.js +33 -0
  108. package/package.json +73 -0
  109. package/src/action/index.ts +1 -0
  110. package/src/action/main.ts +868 -0
  111. package/src/cli/args.test.ts +477 -0
  112. package/src/cli/args.ts +415 -0
  113. package/src/cli/commands/add.ts +447 -0
  114. package/src/cli/commands/init.test.ts +136 -0
  115. package/src/cli/commands/init.ts +132 -0
  116. package/src/cli/commands/setup-app/browser.ts +38 -0
  117. package/src/cli/commands/setup-app/credentials.ts +45 -0
  118. package/src/cli/commands/setup-app/manifest.ts +48 -0
  119. package/src/cli/commands/setup-app/server.ts +172 -0
  120. package/src/cli/commands/setup-app.ts +156 -0
  121. package/src/cli/commands/sync.ts +114 -0
  122. package/src/cli/context.ts +131 -0
  123. package/src/cli/files.test.ts +155 -0
  124. package/src/cli/files.ts +89 -0
  125. package/src/cli/fix.test.ts +310 -0
  126. package/src/cli/fix.ts +387 -0
  127. package/src/cli/git.test.ts +119 -0
  128. package/src/cli/git.ts +318 -0
  129. package/src/cli/index.ts +14 -0
  130. package/src/cli/main.ts +672 -0
  131. package/src/cli/output/box.ts +235 -0
  132. package/src/cli/output/formatters.test.ts +187 -0
  133. package/src/cli/output/formatters.ts +269 -0
  134. package/src/cli/output/icons.ts +13 -0
  135. package/src/cli/output/index.ts +44 -0
  136. package/src/cli/output/ink-runner.tsx +337 -0
  137. package/src/cli/output/jsonl.test.ts +347 -0
  138. package/src/cli/output/jsonl.ts +126 -0
  139. package/src/cli/output/reporter.ts +435 -0
  140. package/src/cli/output/tasks.ts +374 -0
  141. package/src/cli/output/tty.test.ts +117 -0
  142. package/src/cli/output/tty.ts +60 -0
  143. package/src/cli/output/verbosity.test.ts +40 -0
  144. package/src/cli/output/verbosity.ts +31 -0
  145. package/src/cli/terminal.test.ts +148 -0
  146. package/src/cli/terminal.ts +301 -0
  147. package/src/config/index.ts +3 -0
  148. package/src/config/loader.test.ts +313 -0
  149. package/src/config/loader.ts +103 -0
  150. package/src/config/schema.ts +168 -0
  151. package/src/config/writer.test.ts +119 -0
  152. package/src/config/writer.ts +84 -0
  153. package/src/diff/classify.test.ts +162 -0
  154. package/src/diff/classify.ts +92 -0
  155. package/src/diff/coalesce.test.ts +208 -0
  156. package/src/diff/coalesce.ts +133 -0
  157. package/src/diff/context.test.ts +226 -0
  158. package/src/diff/context.ts +201 -0
  159. package/src/diff/index.ts +4 -0
  160. package/src/diff/parser.test.ts +212 -0
  161. package/src/diff/parser.ts +149 -0
  162. package/src/event/context.ts +132 -0
  163. package/src/event/index.ts +2 -0
  164. package/src/event/schedule-context.ts +101 -0
  165. package/src/examples/examples.integration.test.ts +66 -0
  166. package/src/examples/index.test.ts +101 -0
  167. package/src/examples/index.ts +122 -0
  168. package/src/examples/setup.ts +25 -0
  169. package/src/index.ts +115 -0
  170. package/src/output/dedup.test.ts +419 -0
  171. package/src/output/dedup.ts +607 -0
  172. package/src/output/github-checks.test.ts +300 -0
  173. package/src/output/github-checks.ts +476 -0
  174. package/src/output/github-issues.ts +329 -0
  175. package/src/output/index.ts +5 -0
  176. package/src/output/issue-renderer.ts +197 -0
  177. package/src/output/renderer.test.ts +727 -0
  178. package/src/output/renderer.ts +217 -0
  179. package/src/output/stale.test.ts +375 -0
  180. package/src/output/stale.ts +155 -0
  181. package/src/output/types.ts +34 -0
  182. package/src/sdk/index.ts +1 -0
  183. package/src/sdk/runner.test.ts +806 -0
  184. package/src/sdk/runner.ts +1232 -0
  185. package/src/skills/index.ts +36 -0
  186. package/src/skills/loader.test.ts +300 -0
  187. package/src/skills/loader.ts +423 -0
  188. package/src/skills/remote.test.ts +704 -0
  189. package/src/skills/remote.ts +604 -0
  190. package/src/triggers/matcher.test.ts +277 -0
  191. package/src/triggers/matcher.ts +152 -0
  192. package/src/types/index.ts +194 -0
  193. package/src/utils/async.ts +18 -0
  194. package/src/utils/index.test.ts +84 -0
  195. package/src/utils/index.ts +50 -0
  196. package/tsconfig.json +25 -0
  197. package/vitest.config.ts +8 -0
  198. package/vitest.integration.config.ts +11 -0
  199. package/warden.toml +19 -0
@@ -0,0 +1,476 @@
1
+ import type { Octokit } from '@octokit/rest';
2
+ import { SEVERITY_ORDER, filterFindingsBySeverity } from '../types/index.js';
3
+ import type { Severity, SeverityThreshold, Finding, SkillReport, UsageStats } from '../types/index.js';
4
+ import { formatDuration, formatCost, formatTokens } from '../cli/output/formatters.js';
5
+ import { escapeHtml } from '../utils/index.js';
6
+
7
+ /**
8
+ * GitHub Check annotation for inline code comments.
9
+ */
10
+ export interface CheckAnnotation {
11
+ path: string;
12
+ start_line: number;
13
+ end_line: number;
14
+ annotation_level: 'failure' | 'warning' | 'notice';
15
+ message: string;
16
+ title?: string;
17
+ }
18
+
19
+ /**
20
+ * Possible conclusions for a GitHub Check run.
21
+ */
22
+ export type CheckConclusion = 'success' | 'failure' | 'neutral' | 'cancelled';
23
+
24
+ /**
25
+ * Options for creating/updating checks.
26
+ */
27
+ export interface CheckOptions {
28
+ owner: string;
29
+ repo: string;
30
+ headSha: string;
31
+ }
32
+
33
+ /**
34
+ * Options for updating a skill check.
35
+ */
36
+ export interface UpdateSkillCheckOptions extends CheckOptions {
37
+ failOn?: SeverityThreshold;
38
+ /** Only include findings at or above this severity level in annotations */
39
+ commentOn?: SeverityThreshold;
40
+ }
41
+
42
+ /**
43
+ * Summary data for the core warden check.
44
+ */
45
+ export interface CoreCheckSummaryData {
46
+ totalSkills: number;
47
+ totalFindings: number;
48
+ findingsBySeverity: Record<Severity, number>;
49
+ totalDurationMs?: number;
50
+ totalUsage?: UsageStats;
51
+ /** All findings from all skills */
52
+ findings: Finding[];
53
+ skillResults: {
54
+ name: string;
55
+ findingCount: number;
56
+ conclusion: CheckConclusion;
57
+ durationMs?: number;
58
+ usage?: UsageStats;
59
+ }[];
60
+ }
61
+
62
+ /**
63
+ * Result from creating a check run.
64
+ */
65
+ export interface CreateCheckResult {
66
+ checkRunId: number;
67
+ url: string;
68
+ }
69
+
70
+ /**
71
+ * Maximum number of annotations per API call (GitHub limit).
72
+ */
73
+ const MAX_ANNOTATIONS_PER_REQUEST = 50;
74
+
75
+ /**
76
+ * Map severity levels to GitHub annotation levels.
77
+ * critical/high -> failure, medium -> warning, low/info -> notice
78
+ */
79
+ export function severityToAnnotationLevel(
80
+ severity: Severity
81
+ ): CheckAnnotation['annotation_level'] {
82
+ switch (severity) {
83
+ case 'critical':
84
+ case 'high':
85
+ return 'failure';
86
+ case 'medium':
87
+ return 'warning';
88
+ case 'low':
89
+ case 'info':
90
+ return 'notice';
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Convert findings to GitHub Check annotations.
96
+ * Only findings with locations can be converted to annotations.
97
+ * Returns at most MAX_ANNOTATIONS_PER_REQUEST annotations.
98
+ * If commentOn is specified, only include findings at or above that severity.
99
+ */
100
+ export function findingsToAnnotations(findings: Finding[], commentOn?: SeverityThreshold): CheckAnnotation[] {
101
+ // Filter by commentOn threshold if specified
102
+ const filtered = filterFindingsBySeverity(findings, commentOn);
103
+
104
+ // Filter to findings with location using type predicate
105
+ const withLocation = filtered.filter(
106
+ (f): f is Finding & { location: NonNullable<Finding['location']> } => Boolean(f.location)
107
+ );
108
+
109
+ // Sort by severity (most severe first)
110
+ const sorted = [...withLocation].sort(
111
+ (a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]
112
+ );
113
+
114
+ // Limit to max annotations
115
+ const limited = sorted.slice(0, MAX_ANNOTATIONS_PER_REQUEST);
116
+
117
+ return limited.map((finding) => ({
118
+ path: finding.location.path,
119
+ start_line: finding.location.startLine,
120
+ end_line: finding.location.endLine ?? finding.location.startLine,
121
+ annotation_level: severityToAnnotationLevel(finding.severity),
122
+ message: escapeHtml(finding.description),
123
+ title: escapeHtml(finding.title),
124
+ }));
125
+ }
126
+
127
+ /**
128
+ * Determine the check conclusion based on findings and failOn threshold.
129
+ * - No findings: success
130
+ * - Findings, none >= failOn: neutral
131
+ * - Findings >= failOn threshold: failure
132
+ */
133
+ export function determineConclusion(
134
+ findings: Finding[],
135
+ failOn?: SeverityThreshold
136
+ ): CheckConclusion {
137
+ if (findings.length === 0) {
138
+ return 'success';
139
+ }
140
+
141
+ if (!failOn || failOn === 'off') {
142
+ // No failure threshold or disabled, findings exist but don't cause failure
143
+ return 'neutral';
144
+ }
145
+
146
+ const failOnOrder = SEVERITY_ORDER[failOn];
147
+ const hasFailingSeverity = findings.some(
148
+ (f) => SEVERITY_ORDER[f.severity] <= failOnOrder
149
+ );
150
+
151
+ return hasFailingSeverity ? 'failure' : 'neutral';
152
+ }
153
+
154
+ /**
155
+ * Create a check run for a skill.
156
+ * The check is created with status: in_progress.
157
+ */
158
+ export async function createSkillCheck(
159
+ octokit: Octokit,
160
+ skillName: string,
161
+ options: CheckOptions
162
+ ): Promise<CreateCheckResult> {
163
+ const { data } = await octokit.checks.create({
164
+ owner: options.owner,
165
+ repo: options.repo,
166
+ name: `warden: ${skillName}`,
167
+ head_sha: options.headSha,
168
+ status: 'in_progress',
169
+ started_at: new Date().toISOString(),
170
+ });
171
+
172
+ return {
173
+ checkRunId: data.id,
174
+ url: data.html_url ?? '',
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Update a skill check with results.
180
+ * Completes the check with conclusion, summary, and annotations.
181
+ */
182
+ export async function updateSkillCheck(
183
+ octokit: Octokit,
184
+ checkRunId: number,
185
+ report: SkillReport,
186
+ options: UpdateSkillCheckOptions
187
+ ): Promise<void> {
188
+ // Conclusion is based on all findings (failOn behavior)
189
+ const conclusion = determineConclusion(report.findings, options.failOn);
190
+ // Annotations are filtered by commentOn threshold
191
+ const annotations = findingsToAnnotations(report.findings, options.commentOn);
192
+
193
+ const summary = buildSkillSummary(report);
194
+
195
+ const title = report.findings.length === 0
196
+ ? 'No issues'
197
+ : `${report.findings.length} issue${report.findings.length === 1 ? '' : 's'}`;
198
+
199
+ await octokit.checks.update({
200
+ owner: options.owner,
201
+ repo: options.repo,
202
+ check_run_id: checkRunId,
203
+ status: 'completed',
204
+ conclusion,
205
+ completed_at: new Date().toISOString(),
206
+ output: {
207
+ title,
208
+ summary,
209
+ annotations,
210
+ },
211
+ });
212
+ }
213
+
214
+ /**
215
+ * Mark a skill check as failed due to execution error.
216
+ */
217
+ export async function failSkillCheck(
218
+ octokit: Octokit,
219
+ checkRunId: number,
220
+ error: unknown,
221
+ options: CheckOptions
222
+ ): Promise<void> {
223
+ const errorMessage = error instanceof Error ? error.message : String(error);
224
+
225
+ await octokit.checks.update({
226
+ owner: options.owner,
227
+ repo: options.repo,
228
+ check_run_id: checkRunId,
229
+ status: 'completed',
230
+ conclusion: 'failure',
231
+ completed_at: new Date().toISOString(),
232
+ output: {
233
+ title: 'Skill execution failed',
234
+ summary: `Error: ${errorMessage}`,
235
+ },
236
+ });
237
+ }
238
+
239
+ /**
240
+ * Create the core warden check run.
241
+ * The check is created with status: in_progress.
242
+ */
243
+ export async function createCoreCheck(
244
+ octokit: Octokit,
245
+ options: CheckOptions
246
+ ): Promise<CreateCheckResult> {
247
+ const { data } = await octokit.checks.create({
248
+ owner: options.owner,
249
+ repo: options.repo,
250
+ name: 'warden',
251
+ head_sha: options.headSha,
252
+ status: 'in_progress',
253
+ started_at: new Date().toISOString(),
254
+ });
255
+
256
+ return {
257
+ checkRunId: data.id,
258
+ url: data.html_url ?? '',
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Update the core warden check with overall summary.
264
+ */
265
+ export async function updateCoreCheck(
266
+ octokit: Octokit,
267
+ checkRunId: number,
268
+ summaryData: CoreCheckSummaryData,
269
+ conclusion: CheckConclusion,
270
+ options: Omit<CheckOptions, 'headSha'>
271
+ ): Promise<void> {
272
+ const summary = buildCoreSummary(summaryData);
273
+
274
+ const title = summaryData.totalFindings === 0
275
+ ? 'No issues'
276
+ : `${summaryData.totalFindings} issue${summaryData.totalFindings === 1 ? '' : 's'}`;
277
+
278
+ await octokit.checks.update({
279
+ owner: options.owner,
280
+ repo: options.repo,
281
+ check_run_id: checkRunId,
282
+ status: 'completed',
283
+ conclusion,
284
+ completed_at: new Date().toISOString(),
285
+ output: {
286
+ title,
287
+ summary,
288
+ },
289
+ });
290
+ }
291
+
292
+ /**
293
+ * Build the summary markdown for a skill check.
294
+ */
295
+ function buildSkillSummary(report: SkillReport): string {
296
+ const lines: string[] = [escapeHtml(report.summary), ''];
297
+
298
+ if (report.findings.length === 0) {
299
+ lines.push('No issues found.');
300
+ } else {
301
+ // Sort findings by severity
302
+ const sortedFindings = [...report.findings].sort(
303
+ (a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]
304
+ );
305
+
306
+ // Group findings by severity
307
+ const findingsBySeverity = new Map<Severity, Finding[]>();
308
+ for (const finding of sortedFindings) {
309
+ const existing = findingsBySeverity.get(finding.severity) ?? [];
310
+ existing.push(finding);
311
+ findingsBySeverity.set(finding.severity, existing);
312
+ }
313
+
314
+ const severityOrder: Severity[] = ['critical', 'high', 'medium', 'low', 'info'];
315
+ for (const severity of severityOrder) {
316
+ const findings = findingsBySeverity.get(severity);
317
+ if (!findings?.length) continue;
318
+
319
+ const label = severity.charAt(0).toUpperCase() + severity.slice(1);
320
+ lines.push(`### ${label}`, '');
321
+
322
+ for (const finding of findings) {
323
+ const location = finding.location ? ` - ${formatLocation(finding.location)}` : '';
324
+ lines.push('<details>');
325
+ lines.push(`<summary><strong>${escapeHtml(finding.title)}</strong>${location}</summary>`, '');
326
+ lines.push(escapeHtml(finding.description), '');
327
+ lines.push('</details>', '');
328
+ }
329
+ }
330
+ }
331
+
332
+ // Add stats footer if available
333
+ if (report.durationMs !== undefined || report.usage) {
334
+ const statsParts: string[] = [];
335
+ if (report.durationMs !== undefined) {
336
+ statsParts.push(`**Duration:** ${formatDuration(report.durationMs)}`);
337
+ }
338
+ if (report.usage) {
339
+ const totalInput = report.usage.inputTokens + (report.usage.cacheReadInputTokens ?? 0);
340
+ statsParts.push(`**Tokens:** ${formatTokens(totalInput)} in / ${formatTokens(report.usage.outputTokens)} out`);
341
+ statsParts.push(`**Cost:** ${formatCost(report.usage.costUSD)}`);
342
+ }
343
+ lines.push('---', statsParts.join(' · '));
344
+ }
345
+
346
+ return lines.join('\n');
347
+ }
348
+
349
+ /**
350
+ * Format a file location as a markdown code span.
351
+ */
352
+ function formatLocation(location: { path: string; startLine: number; endLine?: number }): string {
353
+ const { path, startLine, endLine } = location;
354
+ const lineRange = endLine && endLine !== startLine ? `${startLine}-${endLine}` : `${startLine}`;
355
+ return `\`${path}:${lineRange}\``;
356
+ }
357
+
358
+ /** Maximum findings to show in the summary */
359
+ const MAX_SUMMARY_FINDINGS = 10;
360
+
361
+ /**
362
+ * Build the summary markdown for the core warden check.
363
+ */
364
+ function buildCoreSummary(data: CoreCheckSummaryData): string {
365
+ const lines: string[] = [];
366
+
367
+ // Sort findings by severity and take top N
368
+ const sortedFindings = [...data.findings].sort(
369
+ (a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]
370
+ );
371
+ const topFindings = sortedFindings.slice(0, MAX_SUMMARY_FINDINGS);
372
+
373
+ // Show findings grouped by severity, each in a collapsible details
374
+ if (topFindings.length > 0) {
375
+ const findingsBySeverity = new Map<Severity, Finding[]>();
376
+ for (const finding of topFindings) {
377
+ const existing = findingsBySeverity.get(finding.severity) ?? [];
378
+ existing.push(finding);
379
+ findingsBySeverity.set(finding.severity, existing);
380
+ }
381
+
382
+ const severityOrder: Severity[] = ['critical', 'high', 'medium', 'low', 'info'];
383
+ for (const severity of severityOrder) {
384
+ const findings = findingsBySeverity.get(severity);
385
+ if (!findings?.length) continue;
386
+
387
+ const label = severity.charAt(0).toUpperCase() + severity.slice(1);
388
+ lines.push(`### ${label}`, '');
389
+
390
+ for (const finding of findings) {
391
+ const location = finding.location ? ` - ${formatLocation(finding.location)}` : '';
392
+ lines.push('<details>');
393
+ lines.push(`<summary><strong>${escapeHtml(finding.title)}</strong>${location}</summary>`, '');
394
+ lines.push(escapeHtml(finding.description), '');
395
+ lines.push('</details>', '');
396
+ }
397
+ }
398
+
399
+ // Note if there are more findings not shown
400
+ if (data.totalFindings > topFindings.length) {
401
+ const remaining = data.totalFindings - topFindings.length;
402
+ lines.push(`*...and ${remaining} more*`, '');
403
+ }
404
+ } else {
405
+ lines.push('No issues found.', '');
406
+ }
407
+
408
+ // Skills table in collapsible section
409
+ const hasSkillStats = data.skillResults.some((s) => s.durationMs !== undefined || s.usage);
410
+ const skillPlural = data.totalSkills === 1 ? '' : 's';
411
+
412
+ lines.push('<details>');
413
+ lines.push(`<summary>${data.totalSkills} skill${skillPlural} analyzed</summary>`, '');
414
+
415
+ if (hasSkillStats) {
416
+ lines.push(
417
+ '| Skill | Findings | Duration | Cost |',
418
+ '|-------|----------|----------|------|'
419
+ );
420
+ for (const skill of data.skillResults) {
421
+ const duration = skill.durationMs !== undefined ? formatDuration(skill.durationMs) : '-';
422
+ const cost = skill.usage ? formatCost(skill.usage.costUSD) : '-';
423
+ lines.push(`| ${skill.name} | ${skill.findingCount} | ${duration} | ${cost} |`);
424
+ }
425
+ } else {
426
+ lines.push(
427
+ '| Skill | Findings |',
428
+ '|-------|----------|'
429
+ );
430
+ for (const skill of data.skillResults) {
431
+ lines.push(`| ${skill.name} | ${skill.findingCount} |`);
432
+ }
433
+ }
434
+
435
+ lines.push('', '</details>', '');
436
+
437
+ // Stats footer with labeled inline format
438
+ const hasStats = data.totalDurationMs !== undefined || data.totalUsage;
439
+ if (hasStats) {
440
+ const statsParts: string[] = [];
441
+ if (data.totalDurationMs !== undefined) {
442
+ statsParts.push(`**Duration:** ${formatDuration(data.totalDurationMs)}`);
443
+ }
444
+ if (data.totalUsage) {
445
+ const totalInput = data.totalUsage.inputTokens + (data.totalUsage.cacheReadInputTokens ?? 0);
446
+ statsParts.push(`**Tokens:** ${formatTokens(totalInput)} in / ${formatTokens(data.totalUsage.outputTokens)} out`);
447
+ statsParts.push(`**Cost:** ${formatCost(data.totalUsage.costUSD)}`);
448
+ }
449
+ lines.push('---', statsParts.join(' · '));
450
+ }
451
+
452
+ return lines.join('\n');
453
+ }
454
+
455
+ /**
456
+ * Aggregate severity counts from multiple reports.
457
+ */
458
+ export function aggregateSeverityCounts(
459
+ reports: SkillReport[]
460
+ ): Record<Severity, number> {
461
+ const counts: Record<Severity, number> = {
462
+ critical: 0,
463
+ high: 0,
464
+ medium: 0,
465
+ low: 0,
466
+ info: 0,
467
+ };
468
+
469
+ for (const report of reports) {
470
+ for (const finding of report.findings) {
471
+ counts[finding.severity]++;
472
+ }
473
+ }
474
+
475
+ return counts;
476
+ }