@probelabs/visor 0.1.181 → 0.1.182

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 (158) hide show
  1. package/defaults/code-talk.yaml +80 -14
  2. package/defaults/engineer.yaml +33 -15
  3. package/defaults/skills/code-explorer.yaml +5 -0
  4. package/dist/agent-protocol/a2a-frontend.d.ts +10 -0
  5. package/dist/agent-protocol/a2a-frontend.d.ts.map +1 -1
  6. package/dist/agent-protocol/task-evaluator.d.ts +52 -0
  7. package/dist/agent-protocol/task-evaluator.d.ts.map +1 -0
  8. package/dist/agent-protocol/task-store.d.ts +5 -3
  9. package/dist/agent-protocol/task-store.d.ts.map +1 -1
  10. package/dist/agent-protocol/tasks-cli-handler.d.ts.map +1 -1
  11. package/dist/agent-protocol/tasks-tui.d.ts +34 -0
  12. package/dist/agent-protocol/tasks-tui.d.ts.map +1 -0
  13. package/dist/agent-protocol/trace-serializer.d.ts +90 -0
  14. package/dist/agent-protocol/trace-serializer.d.ts.map +1 -0
  15. package/dist/agent-protocol/track-execution.d.ts +2 -0
  16. package/dist/agent-protocol/track-execution.d.ts.map +1 -1
  17. package/dist/cli-main.d.ts.map +1 -1
  18. package/dist/defaults/code-talk.yaml +80 -14
  19. package/dist/defaults/engineer.yaml +33 -15
  20. package/dist/defaults/skills/code-explorer.yaml +5 -0
  21. package/dist/docs/commands.md +57 -14
  22. package/dist/docs/configuration.md +2 -0
  23. package/dist/docs/guides/graceful-restart.md +178 -0
  24. package/dist/docs/observability.md +69 -0
  25. package/dist/docs/production-deployment.md +17 -0
  26. package/dist/email/polling-runner.d.ts +4 -0
  27. package/dist/email/polling-runner.d.ts.map +1 -1
  28. package/dist/generated/config-schema.d.ts +70 -6
  29. package/dist/generated/config-schema.d.ts.map +1 -1
  30. package/dist/generated/config-schema.json +73 -6
  31. package/dist/index.js +5006 -886
  32. package/dist/output/traces/{run-2026-03-17T13-58-29-402Z.ndjson → run-2026-03-18T19-02-50-465Z.ndjson} +84 -84
  33. package/dist/{traces/run-2026-03-17T13-59-10-403Z.ndjson → output/traces/run-2026-03-18T19-03-30-428Z.ndjson} +2037 -2037
  34. package/dist/providers/mcp-custom-sse-server.d.ts +4 -0
  35. package/dist/providers/mcp-custom-sse-server.d.ts.map +1 -1
  36. package/dist/runners/graceful-restart.d.ts +46 -0
  37. package/dist/runners/graceful-restart.d.ts.map +1 -0
  38. package/dist/runners/mcp-server-runner.d.ts +12 -0
  39. package/dist/runners/mcp-server-runner.d.ts.map +1 -1
  40. package/dist/runners/runner-factory.d.ts.map +1 -1
  41. package/dist/runners/runner-host.d.ts +12 -0
  42. package/dist/runners/runner-host.d.ts.map +1 -1
  43. package/dist/runners/runner.d.ts +12 -0
  44. package/dist/runners/runner.d.ts.map +1 -1
  45. package/dist/sdk/{a2a-frontend-IWOUJOIZ.mjs → a2a-frontend-4LP3MLTS.mjs} +47 -5
  46. package/dist/sdk/a2a-frontend-4LP3MLTS.mjs.map +1 -0
  47. package/dist/sdk/a2a-frontend-5J3UNFY4.mjs +1718 -0
  48. package/dist/sdk/a2a-frontend-5J3UNFY4.mjs.map +1 -0
  49. package/dist/sdk/{a2a-frontend-BDACLGMA.mjs → a2a-frontend-MU5EO2HZ.mjs} +35 -1
  50. package/dist/sdk/a2a-frontend-MU5EO2HZ.mjs.map +1 -0
  51. package/dist/sdk/{check-provider-registry-4YKTEDKF.mjs → check-provider-registry-MHXQGUNN.mjs} +7 -7
  52. package/dist/sdk/{check-provider-registry-4YFVBGYU.mjs → check-provider-registry-RRWCXSTG.mjs} +3 -3
  53. package/dist/sdk/{check-provider-registry-67ZLGDDQ.mjs → check-provider-registry-Y33CRFVD.mjs} +7 -7
  54. package/dist/sdk/{chunk-DGIH6EX3.mjs → chunk-4AXAVXG5.mjs} +151 -281
  55. package/dist/sdk/chunk-4AXAVXG5.mjs.map +1 -0
  56. package/dist/sdk/{chunk-VMVIM4JB.mjs → chunk-4I3TJ7UJ.mjs} +37 -7
  57. package/dist/sdk/chunk-4I3TJ7UJ.mjs.map +1 -0
  58. package/dist/sdk/{chunk-VXC2XNQJ.mjs → chunk-5J3DNRF7.mjs} +3 -3
  59. package/dist/sdk/{chunk-7YZSSO4X.mjs → chunk-6DPPP7LD.mjs} +10 -10
  60. package/dist/sdk/chunk-7ERVRLDV.mjs +296 -0
  61. package/dist/sdk/chunk-7ERVRLDV.mjs.map +1 -0
  62. package/dist/sdk/{chunk-4DVP6KVC.mjs → chunk-7Z2WHX2J.mjs} +71 -30
  63. package/dist/sdk/chunk-7Z2WHX2J.mjs.map +1 -0
  64. package/dist/sdk/chunk-ANUT54HW.mjs +1502 -0
  65. package/dist/sdk/chunk-ANUT54HW.mjs.map +1 -0
  66. package/dist/sdk/{chunk-J73GEFPT.mjs → chunk-DHETLQIX.mjs} +2 -2
  67. package/dist/sdk/{chunk-QGBASDYP.mjs → chunk-JCOSKBMP.mjs} +71 -30
  68. package/dist/sdk/chunk-JCOSKBMP.mjs.map +1 -0
  69. package/dist/sdk/chunk-MK7ONH47.mjs +739 -0
  70. package/dist/sdk/chunk-MK7ONH47.mjs.map +1 -0
  71. package/dist/sdk/chunk-QXT47ZHR.mjs +390 -0
  72. package/dist/sdk/chunk-QXT47ZHR.mjs.map +1 -0
  73. package/dist/sdk/chunk-V75NEIXL.mjs +296 -0
  74. package/dist/sdk/chunk-V75NEIXL.mjs.map +1 -0
  75. package/dist/sdk/chunk-ZOF5QT6U.mjs +5943 -0
  76. package/dist/sdk/chunk-ZOF5QT6U.mjs.map +1 -0
  77. package/dist/sdk/{config-TSA5FUOM.mjs → config-2STD74CJ.mjs} +2 -2
  78. package/dist/sdk/config-JE4HKTWW.mjs +16 -0
  79. package/dist/sdk/{failure-condition-evaluator-HTPB5FYW.mjs → failure-condition-evaluator-5DZYMCGW.mjs} +4 -4
  80. package/dist/sdk/failure-condition-evaluator-R6DCDJAV.mjs +18 -0
  81. package/dist/sdk/{github-frontend-3SDFCCKI.mjs → github-frontend-3PSCKPAJ.mjs} +4 -4
  82. package/dist/sdk/github-frontend-L3F5JXPJ.mjs +1394 -0
  83. package/dist/sdk/github-frontend-L3F5JXPJ.mjs.map +1 -0
  84. package/dist/sdk/{host-QE4L7UXE.mjs → host-54CHV2LW.mjs} +3 -3
  85. package/dist/sdk/{host-VBBSLUWG.mjs → host-WAU6CT42.mjs} +3 -3
  86. package/dist/sdk/{host-CVH2CSHM.mjs → host-X5ZZCEWN.mjs} +2 -2
  87. package/dist/sdk/{routing-YVMTKFDZ.mjs → routing-CVQT4KHX.mjs} +5 -5
  88. package/dist/sdk/routing-EBAE5SSO.mjs +26 -0
  89. package/dist/sdk/{schedule-tool-Z5VG67JK.mjs → schedule-tool-POY3CDZL.mjs} +7 -7
  90. package/dist/sdk/{schedule-tool-ADUXTCY7.mjs → schedule-tool-R2OAATUS.mjs} +7 -7
  91. package/dist/sdk/{schedule-tool-ZMX3Y7LF.mjs → schedule-tool-Z6QYL2B3.mjs} +3 -3
  92. package/dist/sdk/{schedule-tool-handler-N7UNABOA.mjs → schedule-tool-handler-J4NUETJ6.mjs} +3 -3
  93. package/dist/sdk/{schedule-tool-handler-PCERK6ZZ.mjs → schedule-tool-handler-JMAKHPI7.mjs} +7 -7
  94. package/dist/sdk/{schedule-tool-handler-QOJVFRB4.mjs → schedule-tool-handler-MWFUIQKR.mjs} +7 -7
  95. package/dist/sdk/sdk.d.mts +33 -0
  96. package/dist/sdk/sdk.d.ts +33 -0
  97. package/dist/sdk/sdk.js +2058 -342
  98. package/dist/sdk/sdk.js.map +1 -1
  99. package/dist/sdk/sdk.mjs +6 -6
  100. package/dist/sdk/task-evaluator-HLNXKKVV.mjs +1278 -0
  101. package/dist/sdk/task-evaluator-HLNXKKVV.mjs.map +1 -0
  102. package/dist/sdk/{trace-helpers-KXDOJWBL.mjs → trace-helpers-HL5FBX65.mjs} +3 -3
  103. package/dist/sdk/trace-helpers-WJXYVV4S.mjs +29 -0
  104. package/dist/sdk/trace-helpers-WJXYVV4S.mjs.map +1 -0
  105. package/dist/sdk/trace-reader-ZY77OFNM.mjs +266 -0
  106. package/dist/sdk/trace-reader-ZY77OFNM.mjs.map +1 -0
  107. package/dist/sdk/track-execution-MKIQXP2C.mjs +136 -0
  108. package/dist/sdk/track-execution-MKIQXP2C.mjs.map +1 -0
  109. package/dist/sdk/track-execution-YUXQ6WQH.mjs +136 -0
  110. package/dist/sdk/track-execution-YUXQ6WQH.mjs.map +1 -0
  111. package/dist/sdk/{workflow-check-provider-NTHC5ZBF.mjs → workflow-check-provider-SE5I7EMA.mjs} +7 -7
  112. package/dist/sdk/workflow-check-provider-SE5I7EMA.mjs.map +1 -0
  113. package/dist/sdk/{workflow-check-provider-SRIMWKLQ.mjs → workflow-check-provider-VKYGI5GK.mjs} +3 -3
  114. package/dist/sdk/workflow-check-provider-VKYGI5GK.mjs.map +1 -0
  115. package/dist/sdk/{workflow-check-provider-CJXW2Z4F.mjs → workflow-check-provider-YDGZRI3Z.mjs} +7 -7
  116. package/dist/sdk/workflow-check-provider-YDGZRI3Z.mjs.map +1 -0
  117. package/dist/slack/socket-runner.d.ts +12 -0
  118. package/dist/slack/socket-runner.d.ts.map +1 -1
  119. package/dist/teams/webhook-runner.d.ts +4 -0
  120. package/dist/teams/webhook-runner.d.ts.map +1 -1
  121. package/dist/telegram/polling-runner.d.ts +2 -0
  122. package/dist/telegram/polling-runner.d.ts.map +1 -1
  123. package/dist/traces/{run-2026-03-17T13-58-29-402Z.ndjson → run-2026-03-18T19-02-50-465Z.ndjson} +84 -84
  124. package/dist/{output/traces/run-2026-03-17T13-59-10-403Z.ndjson → traces/run-2026-03-18T19-03-30-428Z.ndjson} +2037 -2037
  125. package/dist/types/config.d.ts +33 -0
  126. package/dist/types/config.d.ts.map +1 -1
  127. package/dist/whatsapp/webhook-runner.d.ts +4 -0
  128. package/dist/whatsapp/webhook-runner.d.ts.map +1 -1
  129. package/package.json +2 -2
  130. package/dist/sdk/a2a-frontend-BDACLGMA.mjs.map +0 -1
  131. package/dist/sdk/a2a-frontend-IWOUJOIZ.mjs.map +0 -1
  132. package/dist/sdk/chunk-4DVP6KVC.mjs.map +0 -1
  133. package/dist/sdk/chunk-DGIH6EX3.mjs.map +0 -1
  134. package/dist/sdk/chunk-QGBASDYP.mjs.map +0 -1
  135. package/dist/sdk/chunk-VMVIM4JB.mjs.map +0 -1
  136. /package/dist/sdk/{check-provider-registry-4YFVBGYU.mjs.map → check-provider-registry-MHXQGUNN.mjs.map} +0 -0
  137. /package/dist/sdk/{check-provider-registry-4YKTEDKF.mjs.map → check-provider-registry-RRWCXSTG.mjs.map} +0 -0
  138. /package/dist/sdk/{check-provider-registry-67ZLGDDQ.mjs.map → check-provider-registry-Y33CRFVD.mjs.map} +0 -0
  139. /package/dist/sdk/{chunk-VXC2XNQJ.mjs.map → chunk-5J3DNRF7.mjs.map} +0 -0
  140. /package/dist/sdk/{chunk-7YZSSO4X.mjs.map → chunk-6DPPP7LD.mjs.map} +0 -0
  141. /package/dist/sdk/{chunk-J73GEFPT.mjs.map → chunk-DHETLQIX.mjs.map} +0 -0
  142. /package/dist/sdk/{config-TSA5FUOM.mjs.map → config-2STD74CJ.mjs.map} +0 -0
  143. /package/dist/sdk/{failure-condition-evaluator-HTPB5FYW.mjs.map → config-JE4HKTWW.mjs.map} +0 -0
  144. /package/dist/sdk/{routing-YVMTKFDZ.mjs.map → failure-condition-evaluator-5DZYMCGW.mjs.map} +0 -0
  145. /package/dist/sdk/{schedule-tool-ADUXTCY7.mjs.map → failure-condition-evaluator-R6DCDJAV.mjs.map} +0 -0
  146. /package/dist/sdk/{github-frontend-3SDFCCKI.mjs.map → github-frontend-3PSCKPAJ.mjs.map} +0 -0
  147. /package/dist/sdk/{host-CVH2CSHM.mjs.map → host-54CHV2LW.mjs.map} +0 -0
  148. /package/dist/sdk/{host-QE4L7UXE.mjs.map → host-WAU6CT42.mjs.map} +0 -0
  149. /package/dist/sdk/{host-VBBSLUWG.mjs.map → host-X5ZZCEWN.mjs.map} +0 -0
  150. /package/dist/sdk/{schedule-tool-Z5VG67JK.mjs.map → routing-CVQT4KHX.mjs.map} +0 -0
  151. /package/dist/sdk/{schedule-tool-ZMX3Y7LF.mjs.map → routing-EBAE5SSO.mjs.map} +0 -0
  152. /package/dist/sdk/{schedule-tool-handler-N7UNABOA.mjs.map → schedule-tool-POY3CDZL.mjs.map} +0 -0
  153. /package/dist/sdk/{schedule-tool-handler-PCERK6ZZ.mjs.map → schedule-tool-R2OAATUS.mjs.map} +0 -0
  154. /package/dist/sdk/{schedule-tool-handler-QOJVFRB4.mjs.map → schedule-tool-Z6QYL2B3.mjs.map} +0 -0
  155. /package/dist/sdk/{trace-helpers-KXDOJWBL.mjs.map → schedule-tool-handler-J4NUETJ6.mjs.map} +0 -0
  156. /package/dist/sdk/{workflow-check-provider-CJXW2Z4F.mjs.map → schedule-tool-handler-JMAKHPI7.mjs.map} +0 -0
  157. /package/dist/sdk/{workflow-check-provider-NTHC5ZBF.mjs.map → schedule-tool-handler-MWFUIQKR.mjs.map} +0 -0
  158. /package/dist/sdk/{workflow-check-provider-SRIMWKLQ.mjs.map → trace-helpers-HL5FBX65.mjs.map} +0 -0
@@ -0,0 +1,1394 @@
1
+ import {
2
+ extractTextFromJson,
3
+ init_json_text_extractor
4
+ } from "./chunk-H5BOW5CR.mjs";
5
+ import {
6
+ failure_condition_evaluator_exports,
7
+ init_failure_condition_evaluator
8
+ } from "./chunk-DHETLQIX.mjs";
9
+ import "./chunk-7ERVRLDV.mjs";
10
+ import {
11
+ generateShortHumanId,
12
+ init_human_id
13
+ } from "./chunk-QXT47ZHR.mjs";
14
+ import "./chunk-34QX63WK.mjs";
15
+ import "./chunk-25IC7KXZ.mjs";
16
+ import "./chunk-LW3INISN.mjs";
17
+ import "./chunk-UFHOIB3R.mjs";
18
+ import {
19
+ init_logger,
20
+ logger
21
+ } from "./chunk-FT3I25QV.mjs";
22
+ import "./chunk-UCMJJ3IM.mjs";
23
+ import {
24
+ __esm,
25
+ __export,
26
+ __toCommonJS
27
+ } from "./chunk-J7LXIPZS.mjs";
28
+
29
+ // src/footer.ts
30
+ function generateFooter(options = {}) {
31
+ const { includeMetadata, includeSeparator = true } = options;
32
+ const parts = [];
33
+ if (includeSeparator) {
34
+ parts.push("---");
35
+ parts.push("");
36
+ }
37
+ parts.push(
38
+ "*Powered by [Visor](https://probelabs.com/visor) from [Probelabs](https://probelabs.com)*"
39
+ );
40
+ if (includeMetadata) {
41
+ const { lastUpdated, triggeredBy, commitSha } = includeMetadata;
42
+ const commitInfo = commitSha ? ` | Commit: ${commitSha.substring(0, 7)}` : "";
43
+ parts.push("");
44
+ parts.push(`*Last updated: ${lastUpdated} | Triggered by: ${triggeredBy}${commitInfo}*`);
45
+ }
46
+ parts.push("");
47
+ parts.push("\u{1F4A1} **TIP:** You can chat with Visor using `/visor ask <your question>`");
48
+ return parts.join("\n");
49
+ }
50
+ var init_footer = __esm({
51
+ "src/footer.ts"() {
52
+ "use strict";
53
+ }
54
+ });
55
+
56
+ // src/github-check-service.ts
57
+ var github_check_service_exports = {};
58
+ __export(github_check_service_exports, {
59
+ GitHubCheckService: () => GitHubCheckService
60
+ });
61
+ var GitHubCheckService;
62
+ var init_github_check_service = __esm({
63
+ "src/github-check-service.ts"() {
64
+ "use strict";
65
+ init_footer();
66
+ GitHubCheckService = class {
67
+ octokit;
68
+ maxAnnotations = 50;
69
+ // GitHub API limit
70
+ constructor(octokit) {
71
+ this.octokit = octokit;
72
+ }
73
+ /**
74
+ * Create a new check run in queued status
75
+ * M4: Includes engine_mode metadata in summary
76
+ */
77
+ async createCheckRun(options, summary) {
78
+ try {
79
+ const enhancedSummary = summary && options.engine_mode ? {
80
+ ...summary,
81
+ summary: `${summary.summary}
82
+
83
+ _Engine: ${options.engine_mode}_`
84
+ } : summary;
85
+ const response = await this.octokit.rest.checks.create({
86
+ owner: options.owner,
87
+ repo: options.repo,
88
+ name: options.name,
89
+ head_sha: options.head_sha,
90
+ status: "queued",
91
+ details_url: options.details_url,
92
+ external_id: options.external_id,
93
+ output: enhancedSummary ? {
94
+ title: enhancedSummary.title,
95
+ summary: enhancedSummary.summary,
96
+ text: enhancedSummary.text
97
+ } : void 0
98
+ });
99
+ return {
100
+ id: response.data.id,
101
+ url: response.data.html_url || ""
102
+ };
103
+ } catch (error) {
104
+ throw new Error(
105
+ `Failed to create check run: ${error instanceof Error ? error.message : String(error)}`
106
+ );
107
+ }
108
+ }
109
+ /**
110
+ * Update check run to in_progress status
111
+ */
112
+ async updateCheckRunInProgress(owner, repo, check_run_id, summary) {
113
+ try {
114
+ await this.octokit.rest.checks.update({
115
+ owner,
116
+ repo,
117
+ check_run_id,
118
+ status: "in_progress",
119
+ output: summary ? {
120
+ title: summary.title,
121
+ summary: summary.summary,
122
+ text: summary.text
123
+ } : void 0
124
+ });
125
+ } catch (error) {
126
+ throw new Error(
127
+ `Failed to update check run to in_progress: ${error instanceof Error ? error.message : String(error)}`
128
+ );
129
+ }
130
+ }
131
+ /**
132
+ * Complete a check run with results based on failure conditions
133
+ */
134
+ async completeCheckRun(owner, repo, check_run_id, checkName, failureResults, reviewIssues = [], executionError, filesChangedInCommit, prNumber, currentCommitSha) {
135
+ try {
136
+ if (prNumber && currentCommitSha) {
137
+ await this.clearOldAnnotations(
138
+ owner,
139
+ repo,
140
+ prNumber,
141
+ checkName,
142
+ currentCommitSha,
143
+ check_run_id
144
+ );
145
+ }
146
+ const { conclusion, summary } = this.determineCheckRunConclusion(
147
+ checkName,
148
+ failureResults,
149
+ reviewIssues,
150
+ executionError
151
+ );
152
+ let filteredIssues = reviewIssues.filter(
153
+ (issue) => !(issue.file === "system" && issue.line === 0)
154
+ );
155
+ if (filesChangedInCommit && filesChangedInCommit.length > 0) {
156
+ filteredIssues = filteredIssues.filter(
157
+ (issue) => filesChangedInCommit.some((changedFile) => issue.file === changedFile)
158
+ );
159
+ }
160
+ const annotations = this.convertIssuesToAnnotations(filteredIssues);
161
+ await this.octokit.rest.checks.update({
162
+ owner,
163
+ repo,
164
+ check_run_id,
165
+ status: "completed",
166
+ conclusion,
167
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
168
+ output: {
169
+ title: summary.title,
170
+ summary: summary.summary,
171
+ text: summary.text,
172
+ annotations: annotations.slice(0, this.maxAnnotations)
173
+ // GitHub limit
174
+ }
175
+ });
176
+ } catch (error) {
177
+ throw new Error(
178
+ `Failed to complete check run: ${error instanceof Error ? error.message : String(error)}`
179
+ );
180
+ }
181
+ }
182
+ /**
183
+ * Determine check run conclusion based on failure conditions and issues
184
+ */
185
+ determineCheckRunConclusion(checkName, failureResults, reviewIssues, executionError) {
186
+ if (executionError) {
187
+ return {
188
+ conclusion: "failure",
189
+ summary: {
190
+ title: "\u274C Check Execution Failed",
191
+ summary: `The ${checkName} check failed to execute properly.`,
192
+ text: `**Error:** ${executionError}
193
+
194
+ Please check your configuration and try again.`
195
+ }
196
+ };
197
+ }
198
+ const failedConditions = failureResults.filter((result) => result.failed);
199
+ const criticalIssues = reviewIssues.filter((issue) => issue.severity === "critical").length;
200
+ const errorIssues = reviewIssues.filter((issue) => issue.severity === "error").length;
201
+ const warningIssues = reviewIssues.filter((issue) => issue.severity === "warning").length;
202
+ const totalIssues = reviewIssues.length;
203
+ let conclusion;
204
+ let title;
205
+ let summaryText;
206
+ let details;
207
+ if (failedConditions.length > 0) {
208
+ conclusion = "failure";
209
+ title = "\u{1F6A8} Check Failed";
210
+ summaryText = `${checkName} check failed because fail_if condition was met.`;
211
+ details = this.formatCheckDetails(failureResults, reviewIssues, {
212
+ failedConditions: failedConditions.length,
213
+ warningConditions: 0,
214
+ criticalIssues,
215
+ errorIssues,
216
+ warningIssues,
217
+ totalIssues
218
+ });
219
+ } else {
220
+ conclusion = "success";
221
+ if (criticalIssues > 0 || errorIssues > 0) {
222
+ title = "\u2705 Check Passed (Issues Found)";
223
+ summaryText = `${checkName} check passed. Found ${criticalIssues} critical and ${errorIssues} error issues, but fail_if condition was not met.`;
224
+ } else if (warningIssues > 0) {
225
+ title = "\u2705 Check Passed (Warnings Found)";
226
+ summaryText = `${checkName} check passed. Found ${warningIssues} warning${warningIssues === 1 ? "" : "s"}, but fail_if condition was not met.`;
227
+ } else {
228
+ title = "\u2705 Check Passed";
229
+ summaryText = `${checkName} check completed successfully with no issues found.`;
230
+ }
231
+ details = this.formatCheckDetails(failureResults, reviewIssues, {
232
+ failedConditions: 0,
233
+ warningConditions: 0,
234
+ criticalIssues,
235
+ errorIssues,
236
+ warningIssues,
237
+ totalIssues
238
+ });
239
+ }
240
+ return {
241
+ conclusion,
242
+ summary: {
243
+ title,
244
+ summary: summaryText,
245
+ text: details
246
+ }
247
+ };
248
+ }
249
+ /**
250
+ * Format detailed check results for the check run summary
251
+ */
252
+ formatCheckDetails(failureResults, reviewIssues, counts) {
253
+ const sections = [];
254
+ sections.push("## \u{1F4CA} Summary");
255
+ sections.push(`- **Total Issues:** ${counts.totalIssues}`);
256
+ if (counts.criticalIssues > 0) {
257
+ sections.push(`- **Critical Issues:** ${counts.criticalIssues}`);
258
+ }
259
+ if (counts.errorIssues > 0) {
260
+ sections.push(`- **Error Issues:** ${counts.errorIssues}`);
261
+ }
262
+ if (counts.warningIssues > 0) {
263
+ sections.push(`- **Warning Issues:** ${counts.warningIssues}`);
264
+ }
265
+ sections.push("");
266
+ if (failureResults.length > 0) {
267
+ sections.push("## \u{1F50D} Failure Condition Results");
268
+ const failedConditions = failureResults.filter((result) => result.failed);
269
+ const passedConditions = failureResults.filter((result) => !result.failed);
270
+ if (failedConditions.length > 0) {
271
+ sections.push("### Failed Conditions");
272
+ failedConditions.forEach((condition) => {
273
+ sections.push(
274
+ `- **${condition.conditionName}**: ${condition.message || condition.expression}`
275
+ );
276
+ if (condition.severity) {
277
+ const icon = this.getSeverityEmoji(condition.severity);
278
+ sections.push(` - Severity: ${icon} ${condition.severity}`);
279
+ }
280
+ });
281
+ sections.push("");
282
+ }
283
+ if (passedConditions.length > 0) {
284
+ sections.push("### Passed Conditions");
285
+ passedConditions.forEach((condition) => {
286
+ sections.push(
287
+ `- **${condition.conditionName}**: ${condition.message || "Condition passed"}`
288
+ );
289
+ });
290
+ sections.push("");
291
+ }
292
+ }
293
+ if (reviewIssues.length > 0) {
294
+ const issuesByCategory = this.groupIssuesByCategory(reviewIssues);
295
+ sections.push("## Issues by Category");
296
+ Object.entries(issuesByCategory).forEach(([category, issues]) => {
297
+ if (issues.length > 0) {
298
+ sections.push(
299
+ `### ${category.charAt(0).toUpperCase() + category.slice(1)} (${issues.length})`
300
+ );
301
+ const displayIssues = issues.slice(0, 5);
302
+ displayIssues.forEach((issue) => {
303
+ const severityIcon = this.getSeverityEmoji(issue.severity);
304
+ sections.push(`- ${severityIcon} **${issue.file}:${issue.line}** - ${issue.message}`);
305
+ });
306
+ if (issues.length > 5) {
307
+ sections.push(`- *...and ${issues.length - 5} more ${category} issues*`);
308
+ }
309
+ sections.push("");
310
+ }
311
+ });
312
+ }
313
+ sections.push("");
314
+ sections.push(generateFooter());
315
+ return sections.join("\n");
316
+ }
317
+ /**
318
+ * Convert review issues to GitHub check run annotations
319
+ */
320
+ convertIssuesToAnnotations(reviewIssues) {
321
+ return reviewIssues.slice(0, this.maxAnnotations).map((issue) => ({
322
+ path: issue.file,
323
+ start_line: issue.line,
324
+ end_line: issue.endLine || issue.line,
325
+ annotation_level: this.mapSeverityToAnnotationLevel(issue.severity),
326
+ message: issue.message,
327
+ title: `${issue.category} Issue`,
328
+ raw_details: issue.suggestion || void 0
329
+ }));
330
+ }
331
+ /**
332
+ * Map Visor issue severity to GitHub annotation level
333
+ */
334
+ mapSeverityToAnnotationLevel(severity) {
335
+ switch (severity) {
336
+ case "critical":
337
+ case "error":
338
+ return "failure";
339
+ case "warning":
340
+ return "warning";
341
+ case "info":
342
+ default:
343
+ return "notice";
344
+ }
345
+ }
346
+ /**
347
+ * Group issues by category
348
+ */
349
+ groupIssuesByCategory(issues) {
350
+ const grouped = {};
351
+ issues.forEach((issue) => {
352
+ const category = issue.category || "general";
353
+ if (!grouped[category]) {
354
+ grouped[category] = [];
355
+ }
356
+ grouped[category].push(issue);
357
+ });
358
+ return grouped;
359
+ }
360
+ /**
361
+ * Get emoji for issue severity (allowed; step/category emojis are removed)
362
+ */
363
+ getSeverityEmoji(severity) {
364
+ const iconMap = {
365
+ critical: "\u{1F6A8}",
366
+ error: "\u274C",
367
+ warning: "\u26A0\uFE0F",
368
+ info: "\u2139\uFE0F"
369
+ };
370
+ return iconMap[String(severity || "").toLowerCase()] || "";
371
+ }
372
+ /**
373
+ * Create multiple check runs for different checks with failure condition support
374
+ */
375
+ async createMultipleCheckRuns(options, checkResults) {
376
+ const results = [];
377
+ for (const checkResult of checkResults) {
378
+ try {
379
+ const checkRun = await this.createCheckRun({
380
+ ...options,
381
+ name: `Visor: ${checkResult.checkName}`,
382
+ external_id: `visor-${checkResult.checkName}-${options.head_sha.substring(0, 7)}`
383
+ });
384
+ await this.updateCheckRunInProgress(options.owner, options.repo, checkRun.id, {
385
+ title: `Running ${checkResult.checkName} check...`,
386
+ summary: `Analyzing code with ${checkResult.checkName} check using AI.`
387
+ });
388
+ await this.completeCheckRun(
389
+ options.owner,
390
+ options.repo,
391
+ checkRun.id,
392
+ checkResult.checkName,
393
+ checkResult.failureResults,
394
+ checkResult.reviewIssues,
395
+ checkResult.executionError
396
+ );
397
+ results.push({
398
+ checkName: checkResult.checkName,
399
+ id: checkRun.id,
400
+ url: checkRun.url
401
+ });
402
+ } catch (error) {
403
+ console.error(`Failed to create check run for ${checkResult.checkName}:`, error);
404
+ }
405
+ }
406
+ return results;
407
+ }
408
+ /**
409
+ * Get check runs for a specific commit
410
+ */
411
+ async getCheckRuns(owner, repo, ref) {
412
+ try {
413
+ const response = await this.octokit.rest.checks.listForRef({
414
+ owner,
415
+ repo,
416
+ ref,
417
+ filter: "all"
418
+ });
419
+ return response.data.check_runs.filter((check) => check.name.startsWith("Visor:")).map((check) => ({
420
+ id: check.id,
421
+ name: check.name,
422
+ status: check.status,
423
+ conclusion: check.conclusion
424
+ }));
425
+ } catch (error) {
426
+ throw new Error(
427
+ `Failed to get check runs: ${error instanceof Error ? error.message : String(error)}`
428
+ );
429
+ }
430
+ }
431
+ /**
432
+ * Get check runs for a specific commit SHA
433
+ * Returns all check runs with the given name on this commit
434
+ */
435
+ async getCheckRunsForCommit(owner, repo, commitSha, checkName) {
436
+ try {
437
+ const checksResponse = await this.octokit.rest.checks.listForRef({
438
+ owner,
439
+ repo,
440
+ ref: commitSha,
441
+ check_name: `Visor: ${checkName}`
442
+ });
443
+ return checksResponse.data.check_runs.map((check) => ({
444
+ id: check.id,
445
+ head_sha: commitSha
446
+ }));
447
+ } catch (error) {
448
+ throw new Error(
449
+ `Failed to get check runs for commit ${commitSha}: ${error instanceof Error ? error.message : String(error)}`
450
+ );
451
+ }
452
+ }
453
+ /**
454
+ * Clear annotations from old check runs on the current commit
455
+ * This prevents annotation accumulation when a check runs multiple times on the same commit
456
+ * (e.g., force push, re-running checks)
457
+ */
458
+ async clearOldAnnotations(owner, repo, prNumber, checkName, currentCommitSha, currentCheckRunId) {
459
+ try {
460
+ const allCheckRuns = await this.getCheckRunsForCommit(
461
+ owner,
462
+ repo,
463
+ currentCommitSha,
464
+ checkName
465
+ );
466
+ const oldRuns = allCheckRuns.filter((run) => run.id !== currentCheckRunId);
467
+ if (oldRuns.length === 0) {
468
+ console.debug(`No old check runs to clear for ${checkName} on commit ${currentCommitSha}`);
469
+ return;
470
+ }
471
+ console.debug(
472
+ `Clearing ${oldRuns.length} old check run(s) for ${checkName} on commit ${currentCommitSha.substring(0, 7)} (keeping current run ${currentCheckRunId})`
473
+ );
474
+ for (const run of oldRuns) {
475
+ try {
476
+ await this.octokit.rest.checks.update({
477
+ owner,
478
+ repo,
479
+ check_run_id: run.id,
480
+ output: {
481
+ title: "Outdated",
482
+ summary: "This check has been superseded by a newer run.",
483
+ annotations: []
484
+ // Clear annotations
485
+ }
486
+ });
487
+ console.debug(`\u2713 Cleared annotations from check run ${run.id}`);
488
+ } catch (error) {
489
+ console.debug(`Could not clear annotations for check run ${run.id}:`, error);
490
+ }
491
+ }
492
+ } catch (error) {
493
+ console.warn("Failed to clear old annotations:", error);
494
+ }
495
+ }
496
+ };
497
+ }
498
+ });
499
+
500
+ // src/github-comments.ts
501
+ var github_comments_exports = {};
502
+ __export(github_comments_exports, {
503
+ CommentManager: () => CommentManager
504
+ });
505
+ var CommentManager;
506
+ var init_github_comments = __esm({
507
+ "src/github-comments.ts"() {
508
+ "use strict";
509
+ init_human_id();
510
+ init_logger();
511
+ init_footer();
512
+ CommentManager = class {
513
+ octokit;
514
+ retryConfig;
515
+ // Serial write queue: chains all updateOrCreateComment calls so only one
516
+ // GitHub comment write is in-flight at a time within a job.
517
+ _writeQueue = Promise.resolve();
518
+ constructor(octokit, retryConfig) {
519
+ this.octokit = octokit;
520
+ this.retryConfig = {
521
+ maxRetries: 3,
522
+ baseDelay: 1e3,
523
+ maxDelay: 1e4,
524
+ backoffFactor: 2,
525
+ ...retryConfig
526
+ };
527
+ }
528
+ /**
529
+ * Find existing Visor comment by comment ID marker
530
+ */
531
+ async findVisorComment(owner, repo, prNumber, commentId) {
532
+ try {
533
+ const comments = await this.octokit.rest.issues.listComments({
534
+ owner,
535
+ repo,
536
+ issue_number: prNumber,
537
+ per_page: 100
538
+ // GitHub default max
539
+ });
540
+ for (const comment of comments.data) {
541
+ if (comment.body && this.isVisorComment(comment.body, commentId)) {
542
+ return comment;
543
+ }
544
+ }
545
+ return null;
546
+ } catch (error) {
547
+ if (this.isRateLimitError(
548
+ error
549
+ )) {
550
+ await this.handleRateLimit(error);
551
+ return this.findVisorComment(owner, repo, prNumber, commentId);
552
+ }
553
+ throw error;
554
+ }
555
+ }
556
+ /**
557
+ * Update existing comment or create new one with collision detection
558
+ */
559
+ async updateOrCreateComment(owner, repo, prNumber, content, options = {}) {
560
+ return new Promise((resolve, reject) => {
561
+ this._writeQueue = this._writeQueue.then(() => this._doUpdateOrCreate(owner, repo, prNumber, content, options)).then(resolve, reject);
562
+ });
563
+ }
564
+ async _doUpdateOrCreate(owner, repo, prNumber, content, options = {}) {
565
+ const {
566
+ commentId = this.generateCommentId(),
567
+ triggeredBy = "unknown",
568
+ allowConcurrentUpdates = false,
569
+ commitSha,
570
+ cachedGithubCommentId
571
+ } = options;
572
+ return this.withRetry(async () => {
573
+ let existingComment = await this.findVisorComment(owner, repo, prNumber, commentId);
574
+ if (!existingComment && cachedGithubCommentId) {
575
+ try {
576
+ const cachedComment = await this.octokit.rest.issues.getComment({
577
+ owner,
578
+ repo,
579
+ comment_id: cachedGithubCommentId
580
+ });
581
+ if (cachedComment.data && this.isVisorComment(cachedComment.data.body || "", commentId)) {
582
+ existingComment = cachedComment.data;
583
+ logger.debug(
584
+ `[github-comments] Found comment via cached ID ${cachedGithubCommentId} (not visible in listComments yet)`
585
+ );
586
+ }
587
+ } catch (_e) {
588
+ logger.debug(
589
+ `[github-comments] Cached comment ${cachedGithubCommentId} not found, will create new`
590
+ );
591
+ }
592
+ }
593
+ const formattedContent = this.formatCommentWithMetadata(content, {
594
+ commentId,
595
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
596
+ triggeredBy,
597
+ commitSha
598
+ });
599
+ if (existingComment) {
600
+ if (!allowConcurrentUpdates) {
601
+ const currentComment = await this.octokit.rest.issues.getComment({
602
+ owner,
603
+ repo,
604
+ comment_id: existingComment.id
605
+ });
606
+ if (currentComment.data.updated_at !== existingComment.updated_at) {
607
+ throw new Error(
608
+ `Comment collision detected for comment ${commentId}. Another process may have updated it.`
609
+ );
610
+ }
611
+ }
612
+ const updatedComment = await this.octokit.rest.issues.updateComment({
613
+ owner,
614
+ repo,
615
+ comment_id: existingComment.id,
616
+ body: formattedContent
617
+ });
618
+ logger.info(
619
+ `\u2705 Successfully updated comment (ID: ${commentId}, GitHub ID: ${existingComment.id}) on PR #${prNumber} in ${owner}/${repo}`
620
+ );
621
+ return updatedComment.data;
622
+ } else {
623
+ const newComment = await this.octokit.rest.issues.createComment({
624
+ owner,
625
+ repo,
626
+ issue_number: prNumber,
627
+ body: formattedContent
628
+ });
629
+ logger.info(
630
+ `\u2705 Successfully created comment (ID: ${commentId}, GitHub ID: ${newComment.data.id}) on PR #${prNumber} in ${owner}/${repo}`
631
+ );
632
+ return newComment.data;
633
+ }
634
+ });
635
+ }
636
+ /**
637
+ * Format comment content with metadata markers
638
+ */
639
+ formatCommentWithMetadata(content, metadata) {
640
+ const { commentId, lastUpdated, triggeredBy, commitSha } = metadata;
641
+ const footer = generateFooter({
642
+ includeMetadata: {
643
+ lastUpdated,
644
+ triggeredBy,
645
+ commitSha
646
+ }
647
+ });
648
+ return `<!-- visor-comment-id:${commentId} -->
649
+ ${content}
650
+
651
+ ${footer}
652
+ <!-- /visor-comment-id:${commentId} -->`;
653
+ }
654
+ /**
655
+ * Create collapsible sections for comment content
656
+ */
657
+ createCollapsibleSection(title, content, isExpanded = false) {
658
+ const openAttribute = isExpanded ? " open" : "";
659
+ return `<details${openAttribute}>
660
+ <summary>${title}</summary>
661
+
662
+ ${content}
663
+
664
+ </details>`;
665
+ }
666
+ /**
667
+ * Group review results by check type with collapsible sections
668
+ */
669
+ formatGroupedResults(results, groupBy = "check") {
670
+ const grouped = this.groupResults(results, groupBy);
671
+ const sections = [];
672
+ for (const [groupKey, items] of Object.entries(grouped)) {
673
+ const totalScore = items.reduce((sum, item) => sum + (item.score || 0), 0) / items.length;
674
+ const totalIssues = items.reduce((sum, item) => sum + (item.issuesFound || 0), 0);
675
+ const title = this.formatGroupTitle(groupKey, totalScore, totalIssues);
676
+ const sectionContent = items.map((item) => item.content).join("\n\n");
677
+ sections.push(this.createCollapsibleSection(title, sectionContent, totalIssues > 0));
678
+ }
679
+ return sections.join("\n\n");
680
+ }
681
+ /**
682
+ * Generate unique comment ID
683
+ */
684
+ generateCommentId() {
685
+ return generateShortHumanId();
686
+ }
687
+ /**
688
+ * Check if comment is a Visor comment
689
+ */
690
+ isVisorComment(body, commentId) {
691
+ if (commentId) {
692
+ if (body.includes(`visor-comment-id:${commentId} `) || body.includes(`visor-comment-id:${commentId} -->`)) {
693
+ return true;
694
+ }
695
+ if (commentId.startsWith("pr-review-") && body.includes("visor-review-")) {
696
+ return true;
697
+ }
698
+ return false;
699
+ }
700
+ return body.includes("visor-comment-id:") && body.includes("<!-- /visor-comment-id:") || body.includes("visor-review-");
701
+ }
702
+ /**
703
+ * Extract comment ID from comment body
704
+ */
705
+ extractCommentId(body) {
706
+ const match = body.match(/visor-comment-id:([a-f0-9-]+)/);
707
+ return match ? match[1] : null;
708
+ }
709
+ /**
710
+ * Handle rate limiting with exponential backoff
711
+ */
712
+ async handleRateLimit(error) {
713
+ const resetTime = error.response?.headers?.["x-ratelimit-reset"];
714
+ if (resetTime) {
715
+ const resetDate = new Date(parseInt(resetTime) * 1e3);
716
+ const waitTime = Math.max(resetDate.getTime() - Date.now(), this.retryConfig.baseDelay);
717
+ console.log(`Rate limit exceeded. Waiting ${Math.round(waitTime / 1e3)}s until reset...`);
718
+ await this.sleep(Math.min(waitTime, this.retryConfig.maxDelay));
719
+ } else {
720
+ await this.sleep(this.retryConfig.baseDelay);
721
+ }
722
+ }
723
+ /**
724
+ * Check if error is a rate limit error
725
+ */
726
+ isRateLimitError(error) {
727
+ return error.status === 403 && (error.response?.data?.message?.includes("rate limit") ?? false);
728
+ }
729
+ /**
730
+ * Check if error should not be retried (auth errors, not found, etc.)
731
+ */
732
+ isNonRetryableError(error) {
733
+ const nonRetryableStatuses = [401, 404, 422];
734
+ const status = error.status || error.response?.status;
735
+ if (status === 403) {
736
+ return !this.isRateLimitError(error);
737
+ }
738
+ return status !== void 0 && nonRetryableStatuses.includes(status);
739
+ }
740
+ /**
741
+ * Retry wrapper with exponential backoff
742
+ */
743
+ async withRetry(operation) {
744
+ let lastError = new Error("Unknown error");
745
+ for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
746
+ try {
747
+ return await operation();
748
+ } catch (error) {
749
+ lastError = error instanceof Error ? error : new Error(String(error));
750
+ if (attempt === this.retryConfig.maxRetries) {
751
+ break;
752
+ }
753
+ if (this.isRateLimitError(
754
+ error
755
+ )) {
756
+ await this.handleRateLimit(error);
757
+ } else if (this.isNonRetryableError(error)) {
758
+ throw error;
759
+ } else {
760
+ const computed = this.retryConfig.baseDelay * Math.pow(this.retryConfig.backoffFactor, attempt);
761
+ const delay = computed > this.retryConfig.maxDelay ? Math.max(0, this.retryConfig.maxDelay - 1) : computed;
762
+ await this.sleep(delay);
763
+ }
764
+ }
765
+ }
766
+ throw lastError;
767
+ }
768
+ /**
769
+ * Sleep utility
770
+ */
771
+ sleep(ms) {
772
+ return new Promise((resolve) => {
773
+ const t = setTimeout(resolve, ms);
774
+ if (typeof t.unref === "function") {
775
+ try {
776
+ t.unref();
777
+ } catch {
778
+ }
779
+ }
780
+ });
781
+ }
782
+ /**
783
+ * Group results by specified criteria
784
+ */
785
+ groupResults(results, groupBy) {
786
+ const grouped = {};
787
+ for (const result of results) {
788
+ const key = groupBy === "check" ? result.checkType : this.getSeverityGroup(result.score);
789
+ if (!grouped[key]) {
790
+ grouped[key] = [];
791
+ }
792
+ grouped[key].push(result);
793
+ }
794
+ return grouped;
795
+ }
796
+ /**
797
+ * Get severity group based on score
798
+ */
799
+ getSeverityGroup(score) {
800
+ if (!score) return "Unknown";
801
+ if (score >= 90) return "Excellent";
802
+ if (score >= 75) return "Good";
803
+ if (score >= 50) return "Needs Improvement";
804
+ return "Critical Issues";
805
+ }
806
+ // Emoji helper removed: plain titles are used in group headers
807
+ /**
808
+ * Format group title with score and issue count
809
+ */
810
+ formatGroupTitle(groupKey, score, issuesFound) {
811
+ const formattedScore = Math.round(score);
812
+ return `${groupKey} Review (Score: ${formattedScore}/100)${issuesFound > 0 ? ` - ${issuesFound} issues found` : ""}`;
813
+ }
814
+ };
815
+ }
816
+ });
817
+
818
+ // src/frontends/github-frontend.ts
819
+ var GitHubFrontend;
820
+ var init_github_frontend = __esm({
821
+ "src/frontends/github-frontend.ts"() {
822
+ init_logger();
823
+ init_json_text_extractor();
824
+ GitHubFrontend = class {
825
+ name = "github";
826
+ subs = [];
827
+ checkRunIds = /* @__PURE__ */ new Map();
828
+ revision = 0;
829
+ cachedCommentId;
830
+ // legacy single-thread id (kept for compatibility)
831
+ // Group → (checkId → SectionState)
832
+ stepStatusByGroup = /* @__PURE__ */ new Map();
833
+ // Debounce/coalescing state
834
+ debounceMs = 400;
835
+ maxWaitMs = 2e3;
836
+ _timer = null;
837
+ _lastFlush = 0;
838
+ _pendingIds = /* @__PURE__ */ new Set();
839
+ // Mutex for serializing comment updates per group
840
+ updateLocks = /* @__PURE__ */ new Map();
841
+ minUpdateDelayMs = 1e3;
842
+ // Minimum delay between updates (public for testing)
843
+ // Cache of created GitHub comment IDs per group to handle API eventual consistency
844
+ createdCommentGithubIds = /* @__PURE__ */ new Map();
845
+ _stopped = false;
846
+ start(ctx) {
847
+ const log = ctx.logger;
848
+ const bus = ctx.eventBus;
849
+ const octokit = ctx.octokit;
850
+ const repo = ctx.run.repo;
851
+ const pr = ctx.run.pr;
852
+ const headSha = ctx.run.headSha;
853
+ const canPostComments = !!(octokit && repo && pr);
854
+ const canPostChecks = !!(octokit && repo && pr && headSha);
855
+ const svc = canPostChecks ? new (init_github_check_service(), __toCommonJS(github_check_service_exports)).GitHubCheckService(octokit) : null;
856
+ const CommentManager2 = (init_github_comments(), __toCommonJS(github_comments_exports)).CommentManager;
857
+ const comments = canPostComments ? new CommentManager2(octokit) : null;
858
+ const threadKey = repo && pr && headSha ? `${repo.owner}/${repo.name}#${pr}@${(headSha || "").substring(0, 7)}` : ctx.run.runId;
859
+ this.cachedCommentId = `visor-thread-${threadKey}`;
860
+ this.subs.push(
861
+ bus.on("CheckScheduled", async (env) => {
862
+ const ev = env && env.payload || env;
863
+ try {
864
+ if (!canPostChecks || !svc) return;
865
+ if (this.checkRunIds.has(ev.checkId)) return;
866
+ const group = this.getGroupForCheck(ctx, ev.checkId);
867
+ this.upsertSectionState(group, ev.checkId, {
868
+ status: "queued",
869
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
870
+ });
871
+ const res = await svc.createCheckRun(
872
+ {
873
+ owner: repo.owner,
874
+ repo: repo.name,
875
+ head_sha: headSha,
876
+ name: `Visor: ${ev.checkId}`,
877
+ external_id: `visor:${ctx.run.runId}:${ev.checkId}`,
878
+ engine_mode: "state-machine"
879
+ },
880
+ { title: `${ev.checkId}`, summary: "Queued" }
881
+ );
882
+ this.checkRunIds.set(ev.checkId, res.id);
883
+ } catch (e) {
884
+ log.warn(
885
+ `[github-frontend] createCheckRun failed for ${ev.checkId}: ${e instanceof Error ? e.message : e}`
886
+ );
887
+ }
888
+ })
889
+ );
890
+ this.subs.push(
891
+ bus.on("CheckCompleted", async (env) => {
892
+ const ev = env && env.payload || env;
893
+ try {
894
+ if (canPostChecks && svc && this.checkRunIds.has(ev.checkId)) {
895
+ const id = this.checkRunIds.get(ev.checkId);
896
+ const issues = Array.isArray(ev.result?.issues) ? ev.result.issues : [];
897
+ const failureResults = await this.evaluateFailureResults(ctx, ev.checkId, ev.result);
898
+ await svc.completeCheckRun(
899
+ repo.owner,
900
+ repo.name,
901
+ id,
902
+ ev.checkId,
903
+ failureResults,
904
+ issues,
905
+ void 0,
906
+ void 0,
907
+ pr,
908
+ headSha
909
+ );
910
+ }
911
+ if (canPostComments && comments) {
912
+ const count = Array.isArray(ev.result?.issues) ? ev.result.issues.length : 0;
913
+ const failureResults = await this.evaluateFailureResults(ctx, ev.checkId, ev.result);
914
+ const failed = Array.isArray(failureResults) ? failureResults.some((r) => r && r.failed) : false;
915
+ const group = this.getGroupForCheck(ctx, ev.checkId);
916
+ const rawContent = ev?.result?.content;
917
+ const extractedContent = extractTextFromJson(rawContent) ?? extractTextFromJson(ev?.result?.output);
918
+ this.upsertSectionState(group, ev.checkId, {
919
+ status: "completed",
920
+ conclusion: failed ? "failure" : "success",
921
+ issues: count,
922
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
923
+ content: extractedContent
924
+ });
925
+ await this.updateGroupedComment(ctx, comments, group, ev.checkId);
926
+ }
927
+ } catch (e) {
928
+ log.warn(
929
+ `[github-frontend] handle CheckCompleted failed: ${e instanceof Error ? e.message : e}`
930
+ );
931
+ }
932
+ })
933
+ );
934
+ this.subs.push(
935
+ bus.on("CheckErrored", async (env) => {
936
+ const ev = env && env.payload || env;
937
+ try {
938
+ if (canPostChecks && svc && this.checkRunIds.has(ev.checkId)) {
939
+ const id = this.checkRunIds.get(ev.checkId);
940
+ await svc.completeCheckRun(
941
+ repo.owner,
942
+ repo.name,
943
+ id,
944
+ ev.checkId,
945
+ [],
946
+ [],
947
+ ev.error?.message || "Execution error",
948
+ void 0,
949
+ pr,
950
+ headSha
951
+ );
952
+ }
953
+ if (canPostComments && comments) {
954
+ const group = this.getGroupForCheck(ctx, ev.checkId);
955
+ this.upsertSectionState(group, ev.checkId, {
956
+ status: "errored",
957
+ conclusion: "failure",
958
+ issues: 0,
959
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
960
+ error: ev.error?.message || "Execution error"
961
+ });
962
+ await this.updateGroupedComment(ctx, comments, group, ev.checkId);
963
+ }
964
+ } catch (e) {
965
+ log.warn(
966
+ `[github-frontend] handle CheckErrored failed: ${e instanceof Error ? e.message : e}`
967
+ );
968
+ }
969
+ })
970
+ );
971
+ this.subs.push(
972
+ bus.on("StateTransition", async (env) => {
973
+ const ev = env && env.payload || env;
974
+ try {
975
+ if (ev.to === "Completed" || ev.to === "Error") {
976
+ if (canPostComments && comments) {
977
+ for (const group of this.stepStatusByGroup.keys()) {
978
+ await this.updateGroupedComment(ctx, comments, group);
979
+ }
980
+ }
981
+ }
982
+ } catch (e) {
983
+ log.warn(
984
+ `[github-frontend] handle StateTransition failed: ${e instanceof Error ? e.message : e}`
985
+ );
986
+ }
987
+ })
988
+ );
989
+ }
990
+ async stop() {
991
+ this._stopped = true;
992
+ for (const s of this.subs) s.unsubscribe();
993
+ this.subs = [];
994
+ if (this._timer) {
995
+ clearTimeout(this._timer);
996
+ this._timer = null;
997
+ }
998
+ this._pendingIds.clear();
999
+ const pending = Array.from(this.updateLocks.values());
1000
+ if (pending.length > 0) {
1001
+ await Promise.allSettled(pending);
1002
+ }
1003
+ }
1004
+ async buildFullBody(ctx, group) {
1005
+ const header = this.renderThreadHeader(ctx, group);
1006
+ const sections = this.renderSections(ctx, group);
1007
+ return `${header}
1008
+
1009
+ ${sections}
1010
+
1011
+ <!-- visor:thread-end key="${this.threadKeyFor(ctx)}" -->`;
1012
+ }
1013
+ threadKeyFor(ctx) {
1014
+ const r = ctx.run;
1015
+ return r.repo && r.pr && r.headSha ? `${r.repo.owner}/${r.repo.name}#${r.pr}@${(r.headSha || "").substring(0, 7)}` : r.runId;
1016
+ }
1017
+ renderThreadHeader(ctx, group) {
1018
+ const header = {
1019
+ key: this.threadKeyFor(ctx),
1020
+ runId: ctx.run.runId,
1021
+ workflowId: ctx.run.workflowId,
1022
+ revision: this.revision,
1023
+ group,
1024
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
1025
+ };
1026
+ return `<!-- visor:thread=${JSON.stringify(header)} -->`;
1027
+ }
1028
+ renderSections(ctx, group) {
1029
+ const lines = [];
1030
+ const groupMap = this.stepStatusByGroup.get(group) || /* @__PURE__ */ new Map();
1031
+ for (const [checkId, st] of groupMap.entries()) {
1032
+ const start = `<!-- visor:section=${JSON.stringify({ id: checkId, revision: this.revision })} -->`;
1033
+ const end = `<!-- visor:section-end id="${checkId}" -->`;
1034
+ const body = st.content && st.content.toString().trim().length > 0 ? st.content.toString().trim() : "";
1035
+ lines.push(`${start}
1036
+ ${body}
1037
+ ${end}`);
1038
+ }
1039
+ return lines.join("\\n\\n");
1040
+ }
1041
+ /**
1042
+ * Acquires a mutex lock for the given group and executes the update.
1043
+ * This ensures only one comment update happens at a time per group,
1044
+ * preventing race conditions where updates overwrite each other.
1045
+ *
1046
+ * Uses a proper queue-based mutex: each new caller chains onto the previous
1047
+ * lock, ensuring strict serialization even when multiple callers wait
1048
+ * simultaneously.
1049
+ */
1050
+ async updateGroupedComment(ctx, comments, group, changedIds) {
1051
+ const existingLock = this.updateLocks.get(group);
1052
+ let resolveLock;
1053
+ const ourLock = new Promise((resolve) => {
1054
+ resolveLock = resolve;
1055
+ });
1056
+ this.updateLocks.set(group, ourLock);
1057
+ try {
1058
+ if (existingLock) {
1059
+ logger.info(
1060
+ `[github-frontend] Comment update for group "${group}" queued, waiting for previous update to finish...`
1061
+ );
1062
+ const queuedAt = Date.now();
1063
+ const reminder = setInterval(() => {
1064
+ const waited = Math.round((Date.now() - queuedAt) / 1e3);
1065
+ logger.info(
1066
+ `[github-frontend] Comment update for group "${group}" still queued (${waited}s).`
1067
+ );
1068
+ }, 1e4);
1069
+ try {
1070
+ await existingLock;
1071
+ } catch (error) {
1072
+ logger.warn(
1073
+ `[github-frontend] Previous update for group ${group} failed: ${error instanceof Error ? error.message : error}`
1074
+ );
1075
+ } finally {
1076
+ clearInterval(reminder);
1077
+ const waitedMs = Date.now() - queuedAt;
1078
+ if (waitedMs > 100) {
1079
+ logger.info(
1080
+ `[github-frontend] Comment update for group "${group}" dequeued after ${Math.round(waitedMs / 1e3)}s.`
1081
+ );
1082
+ }
1083
+ }
1084
+ }
1085
+ await this.performGroupedCommentUpdate(ctx, comments, group, changedIds);
1086
+ } finally {
1087
+ if (this.updateLocks.get(group) === ourLock) {
1088
+ this.updateLocks.delete(group);
1089
+ }
1090
+ resolveLock();
1091
+ }
1092
+ }
1093
+ /**
1094
+ * Performs the actual comment update with delay enforcement.
1095
+ */
1096
+ async performGroupedCommentUpdate(ctx, comments, group, changedIds) {
1097
+ try {
1098
+ if (this._stopped) return;
1099
+ if (!ctx.run.repo || !ctx.run.pr) return;
1100
+ const config = ctx.config;
1101
+ const prCommentEnabled = config?.output?.pr_comment?.enabled !== false;
1102
+ if (!prCommentEnabled) {
1103
+ logger.debug(
1104
+ `[github-frontend] PR comments disabled in config, skipping comment for group: ${group}`
1105
+ );
1106
+ return;
1107
+ }
1108
+ const timeSinceLastFlush = Date.now() - this._lastFlush;
1109
+ if (this._lastFlush > 0 && timeSinceLastFlush < this.minUpdateDelayMs) {
1110
+ const delay = this.minUpdateDelayMs - timeSinceLastFlush;
1111
+ logger.debug(
1112
+ `[github-frontend] Waiting ${delay}ms before next update to prevent rate limiting`
1113
+ );
1114
+ await this.sleep(delay);
1115
+ }
1116
+ this.revision++;
1117
+ const commentId = this.commentIdForGroup(ctx, group);
1118
+ const mergedBody = await this.mergeIntoExistingBody(ctx, comments, group, changedIds);
1119
+ const cachedGithubId = this.createdCommentGithubIds.get(commentId);
1120
+ const result = await comments.updateOrCreateComment(
1121
+ ctx.run.repo.owner,
1122
+ ctx.run.repo.name,
1123
+ ctx.run.pr,
1124
+ mergedBody,
1125
+ {
1126
+ commentId,
1127
+ triggeredBy: this.deriveTriggeredBy(ctx),
1128
+ commitSha: ctx.run.headSha,
1129
+ // Pass the cached GitHub comment ID if available
1130
+ cachedGithubCommentId: cachedGithubId
1131
+ }
1132
+ );
1133
+ if (result && result.id) {
1134
+ this.createdCommentGithubIds.set(commentId, result.id);
1135
+ }
1136
+ this._lastFlush = Date.now();
1137
+ } catch (e) {
1138
+ logger.debug(
1139
+ `[github-frontend] updateGroupedComment failed: ${e instanceof Error ? e.message : e}`
1140
+ );
1141
+ }
1142
+ }
1143
+ deriveTriggeredBy(ctx) {
1144
+ const ev = ctx.run.event || "";
1145
+ const actor = ctx.run.actor;
1146
+ const commentEvents = /* @__PURE__ */ new Set([
1147
+ "issue_comment",
1148
+ "issue_comment_created",
1149
+ "pr_comment",
1150
+ "comment",
1151
+ "pull_request_review_comment"
1152
+ ]);
1153
+ if (commentEvents.has(ev) && actor) return actor;
1154
+ if (ev) return ev;
1155
+ return actor || "unknown";
1156
+ }
1157
+ async mergeIntoExistingBody(ctx, comments, group, changedIds) {
1158
+ const repo = ctx.run.repo;
1159
+ const pr = ctx.run.pr;
1160
+ const existing = await comments.findVisorComment(
1161
+ repo.owner,
1162
+ repo.name,
1163
+ pr,
1164
+ this.commentIdForGroup(ctx, group)
1165
+ );
1166
+ if (!existing || !existing.body) return this.buildFullBody(ctx, group);
1167
+ const body = String(existing.body);
1168
+ const doc = this.parseSections(body);
1169
+ doc.header = {
1170
+ ...doc.header || {},
1171
+ key: this.threadKeyFor(ctx),
1172
+ revision: this.revision,
1173
+ group
1174
+ };
1175
+ if (changedIds) {
1176
+ const ids = Array.isArray(changedIds) ? changedIds : [changedIds];
1177
+ const fresh = this.renderSections(ctx, group);
1178
+ for (const id of ids) {
1179
+ const block = this.extractSectionById(fresh, id);
1180
+ if (block) doc.sections.set(id, block);
1181
+ }
1182
+ } else {
1183
+ const fresh = this.renderSections(ctx, group);
1184
+ const map = this.stepStatusByGroup.get(group) || /* @__PURE__ */ new Map();
1185
+ for (const [checkId] of map.entries()) {
1186
+ if (!doc.sections.has(checkId)) {
1187
+ const block = this.extractSectionById(fresh, checkId);
1188
+ if (block) doc.sections.set(checkId, block);
1189
+ }
1190
+ }
1191
+ }
1192
+ return this.serializeSections(doc);
1193
+ }
1194
+ parseSections(body) {
1195
+ const sections = /* @__PURE__ */ new Map();
1196
+ const headerRe = /<!--\s*visor:thread=(\{[\s\S]*?\})\s*-->/m;
1197
+ const startRe = /<!--\s*visor:section=(\{[\s\S]*?\})\s*-->/g;
1198
+ const endRe = /<!--\s*visor:section-end\s+id=\"([^\"]+)\"\s*-->/g;
1199
+ const safePick = (obj, allowed) => {
1200
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) return void 0;
1201
+ const out = /* @__PURE__ */ Object.create(null);
1202
+ for (const [k, t] of Object.entries(allowed)) {
1203
+ if (Object.prototype.hasOwnProperty.call(obj, k)) {
1204
+ const v = obj[k];
1205
+ if (t === "string" && typeof v === "string") out[k] = v;
1206
+ else if (t === "number" && typeof v === "number" && Number.isFinite(v)) out[k] = v;
1207
+ }
1208
+ }
1209
+ return out;
1210
+ };
1211
+ const safeParse = (text) => {
1212
+ try {
1213
+ return JSON.parse(text);
1214
+ } catch {
1215
+ return void 0;
1216
+ }
1217
+ };
1218
+ let header;
1219
+ try {
1220
+ const h = headerRe.exec(body);
1221
+ if (h) {
1222
+ const parsed = safeParse(h[1]);
1223
+ const picked = safePick(parsed, {
1224
+ key: "string",
1225
+ runId: "string",
1226
+ workflowId: "string",
1227
+ revision: "number",
1228
+ group: "string",
1229
+ generatedAt: "string"
1230
+ });
1231
+ header = picked;
1232
+ }
1233
+ } catch {
1234
+ }
1235
+ let cursor = 0;
1236
+ while (true) {
1237
+ const s = startRe.exec(body);
1238
+ if (!s) break;
1239
+ const metaRaw = safeParse(s[1]);
1240
+ const meta = safePick(metaRaw, { id: "string", revision: "number" }) || { id: "" };
1241
+ const startIdx = startRe.lastIndex;
1242
+ endRe.lastIndex = startIdx;
1243
+ const e = endRe.exec(body);
1244
+ if (!e) break;
1245
+ const id = typeof meta.id === "string" && meta.id ? String(meta.id) : String(e[1]);
1246
+ const content = body.substring(startIdx, e.index).trim();
1247
+ const block = `<!-- visor:section=${JSON.stringify(meta)} -->
1248
+ ${content}
1249
+ <!-- visor:section-end id="${id}" -->`;
1250
+ sections.set(id, block);
1251
+ cursor = endRe.lastIndex;
1252
+ startRe.lastIndex = cursor;
1253
+ }
1254
+ return { header, sections };
1255
+ }
1256
+ serializeSections(doc) {
1257
+ const header = `<!-- visor:thread=${JSON.stringify({ ...doc.header || {}, generatedAt: (/* @__PURE__ */ new Date()).toISOString() })} -->`;
1258
+ const blocks = Array.from(doc.sections.values()).join("\n\n");
1259
+ const key = doc.header && doc.header.key || "";
1260
+ return `${header}
1261
+
1262
+ ${blocks}
1263
+
1264
+ <!-- visor:thread-end key="${key}" -->`;
1265
+ }
1266
+ extractSectionById(rendered, id) {
1267
+ const rx = new RegExp(
1268
+ `<!--\\s*visor:section=(\\{[\\s\\S]*?\\})\\s*-->[\\s\\S]*?<!--\\s*visor:section-end\\s+id=\\"${this.escapeRegExp(id)}\\"\\s*-->`,
1269
+ "m"
1270
+ );
1271
+ const m = rx.exec(rendered);
1272
+ return m ? m[0] : void 0;
1273
+ }
1274
+ escapeRegExp(s) {
1275
+ return s.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
1276
+ }
1277
+ getGroupForCheck(ctx, checkId) {
1278
+ try {
1279
+ const cfg = ctx.config || {};
1280
+ const g = cfg?.checks?.[checkId]?.group || cfg?.steps?.[checkId]?.group;
1281
+ if (typeof g === "string" && g.trim().length > 0) return g;
1282
+ } catch {
1283
+ }
1284
+ return "review";
1285
+ }
1286
+ upsertSectionState(group, checkId, patch) {
1287
+ let groupMap = this.stepStatusByGroup.get(group);
1288
+ if (!groupMap) {
1289
+ groupMap = /* @__PURE__ */ new Map();
1290
+ this.stepStatusByGroup.set(group, groupMap);
1291
+ }
1292
+ const prev = groupMap.get(checkId) || { status: "queued", lastUpdated: (/* @__PURE__ */ new Date()).toISOString() };
1293
+ groupMap.set(checkId, { ...prev, ...patch });
1294
+ }
1295
+ commentIdForGroup(ctx, group) {
1296
+ if (group === "dynamic") {
1297
+ return `visor-thread-dynamic-${ctx.run.runId}`;
1298
+ }
1299
+ const r = ctx.run;
1300
+ const base = r.repo && r.pr ? `${r.repo.owner}/${r.repo.name}#${r.pr}` : r.runId;
1301
+ return `visor-thread-${group}-${base}`;
1302
+ }
1303
+ /**
1304
+ * Compute failure condition results for a completed check so Check Runs map to the
1305
+ * correct GitHub conclusion. This mirrors the engine's evaluation for fail_if.
1306
+ */
1307
+ async evaluateFailureResults(ctx, checkId, result) {
1308
+ try {
1309
+ const config = ctx.config || {};
1310
+ const checks = config && config.checks || {};
1311
+ const checkCfg = checks[checkId] || {};
1312
+ const checkSchema = typeof checkCfg.schema === "string" ? checkCfg.schema : "code-review";
1313
+ const checkGroup = checkCfg.group || "default";
1314
+ const { FailureConditionEvaluator } = (init_failure_condition_evaluator(), __toCommonJS(failure_condition_evaluator_exports));
1315
+ const evaluator = new FailureConditionEvaluator();
1316
+ const reviewSummary = { issues: Array.isArray(result?.issues) ? result.issues : [] };
1317
+ const failures = [];
1318
+ if (config.fail_if) {
1319
+ const failed = await evaluator.evaluateSimpleCondition(
1320
+ checkId,
1321
+ checkSchema,
1322
+ checkGroup,
1323
+ reviewSummary,
1324
+ config.fail_if
1325
+ );
1326
+ failures.push({
1327
+ conditionName: "global_fail_if",
1328
+ failed,
1329
+ expression: config.fail_if,
1330
+ severity: "error",
1331
+ haltExecution: false
1332
+ });
1333
+ }
1334
+ if (checkCfg.fail_if) {
1335
+ const failed = await evaluator.evaluateSimpleCondition(
1336
+ checkId,
1337
+ checkSchema,
1338
+ checkGroup,
1339
+ reviewSummary,
1340
+ checkCfg.fail_if
1341
+ );
1342
+ failures.push({
1343
+ conditionName: `${checkId}_fail_if`,
1344
+ failed,
1345
+ expression: checkCfg.fail_if,
1346
+ severity: "error",
1347
+ haltExecution: false
1348
+ });
1349
+ }
1350
+ return failures;
1351
+ } catch {
1352
+ return [];
1353
+ }
1354
+ }
1355
+ // Debounce helpers
1356
+ scheduleUpdate(ctx, comments, group, id) {
1357
+ if (id) this._pendingIds.add(id);
1358
+ const now = Date.now();
1359
+ const since = now - this._lastFlush;
1360
+ const remaining = this.maxWaitMs - since;
1361
+ if (this._timer) clearTimeout(this._timer);
1362
+ const wait = Math.max(0, Math.min(this.debounceMs, remaining));
1363
+ this._timer = setTimeout(async () => {
1364
+ const ids = Array.from(this._pendingIds);
1365
+ this._pendingIds.clear();
1366
+ this._timer = null;
1367
+ await this.updateGroupedComment(ctx, comments, group, ids.length > 0 ? ids : void 0);
1368
+ this._lastFlush = Date.now();
1369
+ }, wait);
1370
+ }
1371
+ async flushNow(ctx, comments, group) {
1372
+ if (this._timer) {
1373
+ clearTimeout(this._timer);
1374
+ this._timer = null;
1375
+ }
1376
+ const ids = Array.from(this._pendingIds);
1377
+ this._pendingIds.clear();
1378
+ await this.updateGroupedComment(ctx, comments, group, ids.length > 0 ? ids : void 0);
1379
+ this._lastFlush = Date.now();
1380
+ }
1381
+ /**
1382
+ * Sleep utility for enforcing delays
1383
+ */
1384
+ sleep(ms) {
1385
+ return new Promise((resolve) => setTimeout(resolve, ms));
1386
+ }
1387
+ };
1388
+ }
1389
+ });
1390
+ init_github_frontend();
1391
+ export {
1392
+ GitHubFrontend
1393
+ };
1394
+ //# sourceMappingURL=github-frontend-L3F5JXPJ.mjs.map