@geminixiang/mikan 1.0.0-beta.2 → 1.0.0-beta.20

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 (412) hide show
  1. package/CHANGELOG.md +354 -0
  2. package/README.md +42 -16
  3. package/dist/adapter.d.ts +1 -1
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js.map +1 -1
  6. package/dist/adapters/discord/bot.d.ts +1 -0
  7. package/dist/adapters/discord/bot.d.ts.map +1 -1
  8. package/dist/adapters/discord/bot.js +36 -67
  9. package/dist/adapters/discord/bot.js.map +1 -1
  10. package/dist/adapters/discord/context.d.ts.map +1 -1
  11. package/dist/adapters/discord/context.js +17 -42
  12. package/dist/adapters/discord/context.js.map +1 -1
  13. package/dist/adapters/github/bot.d.ts +148 -0
  14. package/dist/adapters/github/bot.d.ts.map +1 -0
  15. package/dist/adapters/github/bot.js +1031 -0
  16. package/dist/adapters/github/bot.js.map +1 -0
  17. package/dist/adapters/github/client.d.ts +118 -0
  18. package/dist/adapters/github/client.d.ts.map +1 -0
  19. package/dist/adapters/github/client.js +309 -0
  20. package/dist/adapters/github/client.js.map +1 -0
  21. package/dist/adapters/github/cloudbuild.d.ts +24 -0
  22. package/dist/adapters/github/cloudbuild.d.ts.map +1 -0
  23. package/dist/adapters/github/cloudbuild.js +71 -0
  24. package/dist/adapters/github/cloudbuild.js.map +1 -0
  25. package/dist/adapters/github/context.d.ts +9 -0
  26. package/dist/adapters/github/context.d.ts.map +1 -0
  27. package/dist/adapters/github/context.js +175 -0
  28. package/dist/adapters/github/context.js.map +1 -0
  29. package/dist/adapters/github/gcp-auth.d.ts +31 -0
  30. package/dist/adapters/github/gcp-auth.d.ts.map +1 -0
  31. package/dist/adapters/github/gcp-auth.js +196 -0
  32. package/dist/adapters/github/gcp-auth.js.map +1 -0
  33. package/dist/adapters/github/ids.d.ts +34 -0
  34. package/dist/adapters/github/ids.d.ts.map +1 -0
  35. package/dist/adapters/github/ids.js +53 -0
  36. package/dist/adapters/github/ids.js.map +1 -0
  37. package/dist/adapters/github/repo.d.ts +30 -0
  38. package/dist/adapters/github/repo.d.ts.map +1 -0
  39. package/dist/adapters/github/repo.js +136 -0
  40. package/dist/adapters/github/repo.js.map +1 -0
  41. package/dist/adapters/github/tool-pack.d.ts +14 -0
  42. package/dist/adapters/github/tool-pack.d.ts.map +1 -0
  43. package/dist/adapters/github/tool-pack.js +56 -0
  44. package/dist/adapters/github/tool-pack.js.map +1 -0
  45. package/dist/adapters/github/tools/checks.d.ts +24 -0
  46. package/dist/adapters/github/tools/checks.d.ts.map +1 -0
  47. package/dist/adapters/github/tools/checks.js +116 -0
  48. package/dist/adapters/github/tools/checks.js.map +1 -0
  49. package/dist/adapters/github/tools/issue.d.ts +22 -0
  50. package/dist/adapters/github/tools/issue.d.ts.map +1 -0
  51. package/dist/adapters/github/tools/issue.js +59 -0
  52. package/dist/adapters/github/tools/issue.js.map +1 -0
  53. package/dist/adapters/github/tools/pr.d.ts +22 -0
  54. package/dist/adapters/github/tools/pr.d.ts.map +1 -0
  55. package/dist/adapters/github/tools/pr.js +59 -0
  56. package/dist/adapters/github/tools/pr.js.map +1 -0
  57. package/dist/adapters/github/tools/read.d.ts +22 -0
  58. package/dist/adapters/github/tools/read.d.ts.map +1 -0
  59. package/dist/adapters/github/tools/read.js +152 -0
  60. package/dist/adapters/github/tools/read.js.map +1 -0
  61. package/dist/adapters/github/tools/review-reply.d.ts +19 -0
  62. package/dist/adapters/github/tools/review-reply.d.ts.map +1 -0
  63. package/dist/adapters/github/tools/review-reply.js +49 -0
  64. package/dist/adapters/github/tools/review-reply.js.map +1 -0
  65. package/dist/adapters/github/tools/sync.d.ts +17 -0
  66. package/dist/adapters/github/tools/sync.d.ts.map +1 -0
  67. package/dist/adapters/github/tools/sync.js +45 -0
  68. package/dist/adapters/github/tools/sync.js.map +1 -0
  69. package/dist/adapters/github/types.d.ts +350 -0
  70. package/dist/adapters/github/types.d.ts.map +1 -0
  71. package/dist/adapters/github/types.js +2 -0
  72. package/dist/adapters/github/types.js.map +1 -0
  73. package/dist/adapters/intake.d.ts +16 -5
  74. package/dist/adapters/intake.d.ts.map +1 -1
  75. package/dist/adapters/intake.js +79 -22
  76. package/dist/adapters/intake.js.map +1 -1
  77. package/dist/adapters/slack/bot.d.ts +8 -6
  78. package/dist/adapters/slack/bot.d.ts.map +1 -1
  79. package/dist/adapters/slack/bot.js +81 -133
  80. package/dist/adapters/slack/bot.js.map +1 -1
  81. package/dist/adapters/slack/context.d.ts.map +1 -1
  82. package/dist/adapters/slack/context.js +3 -14
  83. package/dist/adapters/slack/context.js.map +1 -1
  84. package/dist/adapters/slack/response-lifecycle.d.ts.map +1 -1
  85. package/dist/adapters/slack/response-lifecycle.js +14 -17
  86. package/dist/adapters/slack/response-lifecycle.js.map +1 -1
  87. package/dist/adapters/slack/session.d.ts.map +1 -1
  88. package/dist/adapters/slack/session.js +5 -8
  89. package/dist/adapters/slack/session.js.map +1 -1
  90. package/dist/adapters/streaming.d.ts +4 -0
  91. package/dist/adapters/streaming.d.ts.map +1 -1
  92. package/dist/adapters/streaming.js +11 -0
  93. package/dist/adapters/streaming.js.map +1 -1
  94. package/dist/adapters/telegram/bot.d.ts +1 -2
  95. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  96. package/dist/adapters/telegram/bot.js +27 -62
  97. package/dist/adapters/telegram/bot.js.map +1 -1
  98. package/dist/adapters/telegram/context.d.ts.map +1 -1
  99. package/dist/adapters/telegram/context.js +8 -27
  100. package/dist/adapters/telegram/context.js.map +1 -1
  101. package/dist/adapters/types.d.ts +28 -2
  102. package/dist/adapters/types.d.ts.map +1 -1
  103. package/dist/adapters/types.js.map +1 -1
  104. package/dist/agent.d.ts +38 -14
  105. package/dist/agent.d.ts.map +1 -1
  106. package/dist/agent.js +358 -224
  107. package/dist/agent.js.map +1 -1
  108. package/dist/cli/download.d.ts.map +1 -0
  109. package/dist/cli/download.js.map +1 -0
  110. package/dist/cli/ext-git.d.ts +22 -0
  111. package/dist/cli/ext-git.d.ts.map +1 -0
  112. package/dist/cli/ext-git.js +76 -0
  113. package/dist/cli/ext-git.js.map +1 -0
  114. package/dist/cli/ext.d.ts +2 -0
  115. package/dist/cli/ext.d.ts.map +1 -0
  116. package/dist/cli/ext.js +217 -0
  117. package/dist/cli/ext.js.map +1 -0
  118. package/dist/commands/admin.d.ts.map +1 -1
  119. package/dist/commands/admin.js +2 -1
  120. package/dist/commands/admin.js.map +1 -1
  121. package/dist/commands/auto-reply.d.ts.map +1 -1
  122. package/dist/commands/auto-reply.js +2 -6
  123. package/dist/commands/auto-reply.js.map +1 -1
  124. package/dist/commands/extensions.d.ts.map +1 -1
  125. package/dist/commands/extensions.js +16 -6
  126. package/dist/commands/extensions.js.map +1 -1
  127. package/dist/commands/login.d.ts +2 -1
  128. package/dist/commands/login.d.ts.map +1 -1
  129. package/dist/commands/login.js +30 -1
  130. package/dist/commands/login.js.map +1 -1
  131. package/dist/commands/manifest.d.ts +72 -0
  132. package/dist/commands/manifest.d.ts.map +1 -0
  133. package/dist/commands/manifest.js +121 -0
  134. package/dist/commands/manifest.js.map +1 -0
  135. package/dist/commands/model.d.ts.map +1 -1
  136. package/dist/commands/model.js +3 -1
  137. package/dist/commands/model.js.map +1 -1
  138. package/dist/commands/new.d.ts.map +1 -1
  139. package/dist/commands/new.js +2 -1
  140. package/dist/commands/new.js.map +1 -1
  141. package/dist/commands/sandbox.d.ts.map +1 -1
  142. package/dist/commands/sandbox.js +16 -12
  143. package/dist/commands/sandbox.js.map +1 -1
  144. package/dist/commands/text.d.ts +2 -0
  145. package/dist/commands/text.d.ts.map +1 -0
  146. package/dist/commands/text.js +16 -0
  147. package/dist/commands/text.js.map +1 -0
  148. package/dist/commands/types.d.ts +14 -1
  149. package/dist/commands/types.d.ts.map +1 -1
  150. package/dist/commands/types.js.map +1 -1
  151. package/dist/config.d.ts +1 -1
  152. package/dist/config.d.ts.map +1 -1
  153. package/dist/config.js +90 -54
  154. package/dist/config.js.map +1 -1
  155. package/dist/events.d.ts +10 -0
  156. package/dist/events.d.ts.map +1 -1
  157. package/dist/events.js +14 -5
  158. package/dist/events.js.map +1 -1
  159. package/dist/execution-resolver.d.ts +3 -2
  160. package/dist/execution-resolver.d.ts.map +1 -1
  161. package/dist/execution-resolver.js +28 -12
  162. package/dist/execution-resolver.js.map +1 -1
  163. package/dist/harness/extensions/loader.d.ts +39 -9
  164. package/dist/harness/extensions/loader.d.ts.map +1 -1
  165. package/dist/harness/extensions/loader.js +258 -54
  166. package/dist/harness/extensions/loader.js.map +1 -1
  167. package/dist/harness/extensions/registry.d.ts +48 -1
  168. package/dist/harness/extensions/registry.d.ts.map +1 -1
  169. package/dist/harness/extensions/registry.js +146 -0
  170. package/dist/harness/extensions/registry.js.map +1 -1
  171. package/dist/harness/extensions/types.d.ts +161 -14
  172. package/dist/harness/extensions/types.d.ts.map +1 -1
  173. package/dist/harness/extensions/types.js.map +1 -1
  174. package/dist/harness/index.d.ts +5 -5
  175. package/dist/harness/index.d.ts.map +1 -1
  176. package/dist/harness/index.js +2 -2
  177. package/dist/harness/index.js.map +1 -1
  178. package/dist/harness/models.d.ts +5 -0
  179. package/dist/harness/models.d.ts.map +1 -1
  180. package/dist/harness/models.js +10 -0
  181. package/dist/harness/models.js.map +1 -1
  182. package/dist/harness/runner.d.ts +21 -2
  183. package/dist/harness/runner.d.ts.map +1 -1
  184. package/dist/harness/runner.js +80 -22
  185. package/dist/harness/runner.js.map +1 -1
  186. package/dist/harness/session-store.d.ts +4 -1
  187. package/dist/harness/session-store.d.ts.map +1 -1
  188. package/dist/harness/session-store.js +11 -2
  189. package/dist/harness/session-store.js.map +1 -1
  190. package/dist/harness/subagent-runner.d.ts +17 -0
  191. package/dist/harness/subagent-runner.d.ts.map +1 -0
  192. package/dist/harness/subagent-runner.js +307 -0
  193. package/dist/harness/subagent-runner.js.map +1 -0
  194. package/dist/harness/types.d.ts +53 -0
  195. package/dist/harness/types.d.ts.map +1 -1
  196. package/dist/harness/types.js.map +1 -1
  197. package/dist/index.d.ts +1 -1
  198. package/dist/index.d.ts.map +1 -1
  199. package/dist/index.js.map +1 -1
  200. package/dist/main.js +232 -29
  201. package/dist/main.js.map +1 -1
  202. package/dist/provisioner.d.ts.map +1 -1
  203. package/dist/provisioner.js +2 -5
  204. package/dist/provisioner.js.map +1 -1
  205. package/dist/runtime/conversation-runtime.d.ts +1 -1
  206. package/dist/runtime/conversation-runtime.d.ts.map +1 -1
  207. package/dist/runtime/conversation-runtime.js +264 -41
  208. package/dist/runtime/conversation-runtime.js.map +1 -1
  209. package/dist/runtime/types.d.ts +32 -5
  210. package/dist/runtime/types.d.ts.map +1 -1
  211. package/dist/runtime/types.js.map +1 -1
  212. package/dist/sandbox/cloudflare.d.ts +2 -0
  213. package/dist/sandbox/cloudflare.d.ts.map +1 -1
  214. package/dist/sandbox/cloudflare.js +7 -0
  215. package/dist/sandbox/cloudflare.js.map +1 -1
  216. package/dist/sandbox/container.d.ts +3 -0
  217. package/dist/sandbox/container.d.ts.map +1 -1
  218. package/dist/sandbox/container.js +8 -2
  219. package/dist/sandbox/container.js.map +1 -1
  220. package/dist/sandbox/firecracker.d.ts +2 -0
  221. package/dist/sandbox/firecracker.d.ts.map +1 -1
  222. package/dist/sandbox/firecracker.js +7 -1
  223. package/dist/sandbox/firecracker.js.map +1 -1
  224. package/dist/sandbox/gondolin-fleet.d.ts +104 -0
  225. package/dist/sandbox/gondolin-fleet.d.ts.map +1 -0
  226. package/dist/sandbox/gondolin-fleet.js +330 -0
  227. package/dist/sandbox/gondolin-fleet.js.map +1 -0
  228. package/dist/sandbox/gondolin-gateway.d.ts +85 -0
  229. package/dist/sandbox/gondolin-gateway.d.ts.map +1 -0
  230. package/dist/sandbox/gondolin-gateway.js +400 -0
  231. package/dist/sandbox/gondolin-gateway.js.map +1 -0
  232. package/dist/sandbox/gondolin-inventory.d.ts +89 -0
  233. package/dist/sandbox/gondolin-inventory.d.ts.map +1 -0
  234. package/dist/sandbox/gondolin-inventory.js +267 -0
  235. package/dist/sandbox/gondolin-inventory.js.map +1 -0
  236. package/dist/sandbox/gondolin-join.d.ts +62 -0
  237. package/dist/sandbox/gondolin-join.d.ts.map +1 -0
  238. package/dist/sandbox/gondolin-join.js +261 -0
  239. package/dist/sandbox/gondolin-join.js.map +1 -0
  240. package/dist/sandbox/gondolin-nfs-advisory.d.ts +15 -0
  241. package/dist/sandbox/gondolin-nfs-advisory.d.ts.map +1 -0
  242. package/dist/sandbox/gondolin-nfs-advisory.js +60 -0
  243. package/dist/sandbox/gondolin-nfs-advisory.js.map +1 -0
  244. package/dist/sandbox/gondolin-placement.d.ts +32 -0
  245. package/dist/sandbox/gondolin-placement.d.ts.map +1 -0
  246. package/dist/sandbox/gondolin-placement.js +83 -0
  247. package/dist/sandbox/gondolin-placement.js.map +1 -0
  248. package/dist/sandbox/gondolin-remote.d.ts +73 -0
  249. package/dist/sandbox/gondolin-remote.d.ts.map +1 -0
  250. package/dist/sandbox/gondolin-remote.js +352 -0
  251. package/dist/sandbox/gondolin-remote.js.map +1 -0
  252. package/dist/sandbox/gondolin-worker-client.d.ts +139 -0
  253. package/dist/sandbox/gondolin-worker-client.d.ts.map +1 -0
  254. package/dist/sandbox/gondolin-worker-client.js +325 -0
  255. package/dist/sandbox/gondolin-worker-client.js.map +1 -0
  256. package/dist/sandbox/gondolin-worker-main.d.ts +2 -0
  257. package/dist/sandbox/gondolin-worker-main.d.ts.map +1 -0
  258. package/dist/sandbox/gondolin-worker-main.js +22 -0
  259. package/dist/sandbox/gondolin-worker-main.js.map +1 -0
  260. package/dist/sandbox/gondolin-worker.d.ts +55 -0
  261. package/dist/sandbox/gondolin-worker.d.ts.map +1 -0
  262. package/dist/sandbox/gondolin-worker.js +201 -0
  263. package/dist/sandbox/gondolin-worker.js.map +1 -0
  264. package/dist/sandbox/gondolin.d.ts +52 -0
  265. package/dist/sandbox/gondolin.d.ts.map +1 -0
  266. package/dist/sandbox/gondolin.js +425 -0
  267. package/dist/sandbox/gondolin.js.map +1 -0
  268. package/dist/sandbox/host.d.ts +2 -0
  269. package/dist/sandbox/host.d.ts.map +1 -1
  270. package/dist/sandbox/host.js +11 -0
  271. package/dist/sandbox/host.js.map +1 -1
  272. package/dist/sandbox/identity.d.ts +21 -0
  273. package/dist/sandbox/identity.d.ts.map +1 -0
  274. package/dist/sandbox/identity.js +36 -0
  275. package/dist/sandbox/identity.js.map +1 -0
  276. package/dist/sandbox/index.d.ts +4 -3
  277. package/dist/sandbox/index.d.ts.map +1 -1
  278. package/dist/sandbox/index.js +5 -3
  279. package/dist/sandbox/index.js.map +1 -1
  280. package/dist/sandbox/types.d.ts +26 -1
  281. package/dist/sandbox/types.d.ts.map +1 -1
  282. package/dist/sandbox/types.js.map +1 -1
  283. package/dist/sandbox/utils.d.ts +11 -0
  284. package/dist/sandbox/utils.d.ts.map +1 -1
  285. package/dist/sandbox/utils.js +35 -0
  286. package/dist/sandbox/utils.js.map +1 -1
  287. package/dist/sessions/agent-memory-file-manager.d.ts.map +1 -1
  288. package/dist/sessions/agent-memory-file-manager.js +16 -30
  289. package/dist/sessions/agent-memory-file-manager.js.map +1 -1
  290. package/dist/sessions/history-line.d.ts +20 -0
  291. package/dist/sessions/history-line.d.ts.map +1 -0
  292. package/dist/sessions/history-line.js +24 -0
  293. package/dist/sessions/history-line.js.map +1 -0
  294. package/dist/sessions/policy.d.ts.map +1 -1
  295. package/dist/sessions/policy.js +5 -3
  296. package/dist/sessions/policy.js.map +1 -1
  297. package/dist/sessions/session-key.d.ts +31 -0
  298. package/dist/sessions/session-key.d.ts.map +1 -0
  299. package/dist/sessions/session-key.js +44 -0
  300. package/dist/sessions/session-key.js.map +1 -0
  301. package/dist/sessions/store.d.ts.map +1 -1
  302. package/dist/sessions/store.js +2 -2
  303. package/dist/sessions/store.js.map +1 -1
  304. package/dist/tools/bash.d.ts +3 -1
  305. package/dist/tools/bash.d.ts.map +1 -1
  306. package/dist/tools/bash.js +24 -17
  307. package/dist/tools/bash.js.map +1 -1
  308. package/dist/tools/edit.d.ts.map +1 -1
  309. package/dist/tools/edit.js +9 -12
  310. package/dist/tools/edit.js.map +1 -1
  311. package/dist/tools/event.d.ts +1 -1
  312. package/dist/tools/event.d.ts.map +1 -1
  313. package/dist/tools/event.js +33 -4
  314. package/dist/tools/event.js.map +1 -1
  315. package/dist/tools/generate-image.d.ts +17 -0
  316. package/dist/tools/generate-image.d.ts.map +1 -0
  317. package/dist/tools/generate-image.js +63 -0
  318. package/dist/tools/generate-image.js.map +1 -0
  319. package/dist/tools/index.d.ts +16 -2
  320. package/dist/tools/index.d.ts.map +1 -1
  321. package/dist/tools/index.js +27 -3
  322. package/dist/tools/index.js.map +1 -1
  323. package/dist/tools/react.d.ts +15 -0
  324. package/dist/tools/react.d.ts.map +1 -0
  325. package/dist/tools/react.js +40 -0
  326. package/dist/tools/react.js.map +1 -0
  327. package/dist/tools/sandbox.d.ts +2 -2
  328. package/dist/tools/sandbox.d.ts.map +1 -1
  329. package/dist/tools/sandbox.js +8 -7
  330. package/dist/tools/sandbox.js.map +1 -1
  331. package/dist/tools/subagent.d.ts +69 -0
  332. package/dist/tools/subagent.d.ts.map +1 -0
  333. package/dist/tools/subagent.js +286 -0
  334. package/dist/tools/subagent.js.map +1 -0
  335. package/dist/tools/types.d.ts +34 -1
  336. package/dist/tools/types.d.ts.map +1 -1
  337. package/dist/tools/types.js.map +1 -1
  338. package/dist/tools/write.d.ts.map +1 -1
  339. package/dist/tools/write.js +1 -8
  340. package/dist/tools/write.js.map +1 -1
  341. package/dist/trigger.d.ts.map +1 -1
  342. package/dist/trigger.js +1 -2
  343. package/dist/trigger.js.map +1 -1
  344. package/dist/types.d.ts +139 -7
  345. package/dist/types.d.ts.map +1 -1
  346. package/dist/types.js.map +1 -1
  347. package/dist/vault/disabled.d.ts +9 -0
  348. package/dist/vault/disabled.d.ts.map +1 -0
  349. package/dist/vault/disabled.js +23 -0
  350. package/dist/vault/disabled.js.map +1 -0
  351. package/dist/vault/index.d.ts.map +1 -1
  352. package/dist/vault/index.js +3 -24
  353. package/dist/vault/index.js.map +1 -1
  354. package/dist/vault/policy.d.ts +20 -0
  355. package/dist/vault/policy.d.ts.map +1 -0
  356. package/dist/vault/policy.js +20 -0
  357. package/dist/vault/policy.js.map +1 -0
  358. package/dist/vault/routing.d.ts +1 -0
  359. package/dist/vault/routing.d.ts.map +1 -1
  360. package/dist/vault/routing.js +3 -10
  361. package/dist/vault/routing.js.map +1 -1
  362. package/dist/web/admin/portal.d.ts +27 -0
  363. package/dist/web/admin/portal.d.ts.map +1 -1
  364. package/dist/web/admin/portal.js +91 -130
  365. package/dist/web/admin/portal.js.map +1 -1
  366. package/dist/web/admin/types.d.ts +3 -0
  367. package/dist/web/admin/types.d.ts.map +1 -1
  368. package/dist/web/admin/types.js.map +1 -1
  369. package/dist/web/login/oauth.d.ts +2 -3
  370. package/dist/web/login/oauth.d.ts.map +1 -1
  371. package/dist/web/login/oauth.js +0 -31
  372. package/dist/web/login/oauth.js.map +1 -1
  373. package/dist/web/login/portal.js +1 -1
  374. package/dist/web/login/portal.js.map +1 -1
  375. package/dist/web/login/types.d.ts +0 -11
  376. package/dist/web/login/types.d.ts.map +1 -1
  377. package/dist/web/login/types.js.map +1 -1
  378. package/dist/web/portal-shell.d.ts +4 -0
  379. package/dist/web/portal-shell.d.ts.map +1 -0
  380. package/dist/{portal-shell.js → web/portal-shell.js} +2 -2
  381. package/dist/web/portal-shell.js.map +1 -0
  382. package/dist/web/server.d.ts.map +1 -1
  383. package/dist/web/server.js +7 -0
  384. package/dist/web/server.js.map +1 -1
  385. package/dist/web/session-view/command.d.ts.map +1 -1
  386. package/dist/web/session-view/command.js +4 -1
  387. package/dist/web/session-view/command.js.map +1 -1
  388. package/dist/web/session-view/portal.d.ts.map +1 -1
  389. package/dist/web/session-view/portal.js +4 -3
  390. package/dist/web/session-view/portal.js.map +1 -1
  391. package/dist/web/session-view/service.d.ts.map +1 -1
  392. package/dist/web/session-view/service.js +2 -1
  393. package/dist/web/session-view/service.js.map +1 -1
  394. package/dist/web/session-view/types.d.ts +2 -1
  395. package/dist/web/session-view/types.d.ts.map +1 -1
  396. package/dist/web/session-view/types.js.map +1 -1
  397. package/package.json +7 -4
  398. package/dist/download.d.ts.map +0 -1
  399. package/dist/download.js.map +0 -1
  400. package/dist/model-registry.d.ts +0 -4
  401. package/dist/model-registry.d.ts.map +0 -1
  402. package/dist/model-registry.js +0 -7
  403. package/dist/model-registry.js.map +0 -1
  404. package/dist/portal-shell.d.ts +0 -4
  405. package/dist/portal-shell.d.ts.map +0 -1
  406. package/dist/portal-shell.js.map +0 -1
  407. package/dist/runtime/agent-run-controller.d.ts +0 -31
  408. package/dist/runtime/agent-run-controller.d.ts.map +0 -1
  409. package/dist/runtime/agent-run-controller.js +0 -195
  410. package/dist/runtime/agent-run-controller.js.map +0 -1
  411. /package/dist/{download.d.ts → cli/download.d.ts} +0 -0
  412. /package/dist/{download.js → cli/download.js} +0 -0
@@ -0,0 +1,1031 @@
1
+ import { existsSync } from "fs";
2
+ import { dirname, join } from "path";
3
+ import { Type } from "@sinclair/typebox";
4
+ import * as log from "../../log.js";
5
+ import { ensureDirExists, readJsonSchemaFileIfExists } from "../../utils/file-guards.js";
6
+ import { atomicWritePrivateFile } from "../../utils/fs-atomic.js";
7
+ import { resolveChatSessionKey } from "../../sessions/policy.js";
8
+ import { appendBotResponseLog, appendChannelLog, MessagingEventQueue, splitText, withRetry, } from "../shared.js";
9
+ import { processMessageIntake } from "../intake.js";
10
+ import { GithubApiError, GithubClient, githubIsRateLimited } from "./client.js";
11
+ import { CLOUD_BUILD_APP_SLUG, CloudBuildLogUnavailableError, fetchCloudBuildLog, isCloudBuildId, projectFromDetailsUrl, } from "./cloudbuild.js";
12
+ import { createGithubAdapters } from "./context.js";
13
+ import { cloneRepo, GITHUB_PUSH_BRANCH_PATTERN, pushBranch, syncRepo } from "./repo.js";
14
+ import { buildGithubConversationId, GITHUB_ISSUE_BODY_TS, githubReviewCommentTs, parseGithubConversationId, parseReviewCommentTs, } from "./ids.js";
15
+ const SyncStateSchema = Type.Object({
16
+ repos: Type.Record(Type.String(), Type.Object({
17
+ baseline: Type.String(),
18
+ cursor: Type.String(),
19
+ seenComments: Type.Array(Type.Number()),
20
+ seenIssues: Type.Array(Type.Number()),
21
+ // Optional: state files written before review-comment polling must keep
22
+ // loading — a schema failure re-baselines and silently drops dedup state.
23
+ seenReviewComments: Type.Optional(Type.Array(Type.Number())),
24
+ })),
25
+ });
26
+ /**
27
+ * Re-fetch this far behind the cursor each poll. Dedup is by id (the
28
+ * watermark), so the overlap only costs bandwidth and closes the boundary
29
+ * races of a bare `since` timestamp (DESIGN.md § Event source).
30
+ */
31
+ const POLL_OVERLAP_MS = 5 * 60 * 1000;
32
+ const MAX_SEEN_IDS = 5000;
33
+ /** Effective role → rank, for the trigger-permission gate. */
34
+ const PERMISSION_RANK = {
35
+ none: 0,
36
+ read: 1,
37
+ triage: 2,
38
+ write: 3,
39
+ maintain: 4,
40
+ admin: 5,
41
+ };
42
+ /**
43
+ * Commenters need write or better to trigger the agent. Fixed, not a config
44
+ * knob: it guards public repos (anyone can comment there), and the people who
45
+ * ask an agent to change code are exactly the people with push access.
46
+ */
47
+ const REQUIRED_TRIGGER_RANK = PERMISSION_RANK.write;
48
+ /** Permission lookups are cached this long per repo+user. */
49
+ const PERMISSION_CACHE_TTL_MS = 5 * 60 * 1000;
50
+ const githubRetry = (fn) => withRetry(fn, { isRateLimited: githubIsRateLimited });
51
+ /** GitHub comment bodies cap at 65536 chars; leave headroom for markup. */
52
+ export const GITHUB_MAX_COMMENT_LENGTH = 60000;
53
+ export const formatGithubContinuation = (partNum) => `*(continued ${partNum})*`;
54
+ /** Slack-style emoji short names → GitHub reaction content values. */
55
+ const GITHUB_REACTIONS = {
56
+ "+1": "+1",
57
+ thumbsup: "+1",
58
+ "-1": "-1",
59
+ thumbsdown: "-1",
60
+ laugh: "laugh",
61
+ smile: "laugh",
62
+ confused: "confused",
63
+ heart: "heart",
64
+ hooray: "hooray",
65
+ tada: "hooray",
66
+ rocket: "rocket",
67
+ eyes: "eyes",
68
+ };
69
+ // Repo refs are lowercased at every entry point (env config here, the
70
+ // installation API in start()) so conversation ids and sync-state keys never
71
+ // depend on how a repo's name was spelled — GitHub is case-insensitive.
72
+ function parseRepoList(repos) {
73
+ return repos.map((entry) => {
74
+ const [owner, repo, ...rest] = entry.split("/");
75
+ if (!owner || !repo || rest.length > 0) {
76
+ throw new Error(`Invalid GITHUB_REPOS entry (expected owner/repo): ${entry}`);
77
+ }
78
+ return { owner: owner.toLowerCase(), repo: repo.toLowerCase() };
79
+ });
80
+ }
81
+ /** Sets iterate in insertion order; drop the oldest ids once over the cap. */
82
+ function pruneSeen(seen) {
83
+ if (seen.size <= MAX_SEEN_IDS)
84
+ return;
85
+ let toDrop = seen.size - MAX_SEEN_IDS / 2;
86
+ for (const id of seen) {
87
+ if (toDrop-- <= 0)
88
+ break;
89
+ seen.delete(id);
90
+ }
91
+ }
92
+ /** Issue number from a comment's `issue_url` (…/repos/o/r/issues/42). */
93
+ function issueNumberFromUrl(issueUrl) {
94
+ const match = /\/issues\/(\d+)$/.exec(issueUrl);
95
+ if (!match) {
96
+ throw new Error(`Cannot parse issue number from ${issueUrl}`);
97
+ }
98
+ return Number(match[1]);
99
+ }
100
+ /** PR number from a review comment's `pull_request_url` (…/repos/o/r/pulls/42). */
101
+ function prNumberFromUrl(pullRequestUrl) {
102
+ const match = /\/pulls\/(\d+)$/.exec(pullRequestUrl);
103
+ if (!match) {
104
+ throw new Error(`Cannot parse PR number from ${pullRequestUrl}`);
105
+ }
106
+ return Number(match[1]);
107
+ }
108
+ /** Keep the tail: a review comment anchors to the hunk's final lines. */
109
+ const MAX_DIFF_HUNK_CHARS = 1500;
110
+ const MAX_THREAD_TURNS = 10;
111
+ const MAX_THREAD_TURN_CHARS = 500;
112
+ export class GithubMessagingBot {
113
+ constructor(handler, config, client) {
114
+ this.appSlug = null;
115
+ this.botEmail = null;
116
+ this.watchedRepos = [];
117
+ this.queues = new Map();
118
+ this.repoState = new Map();
119
+ this.permissionCache = new Map();
120
+ /** Cloud Build ids seen in getChecks summaries → their GCP project. */
121
+ this.buildRef = new Map();
122
+ this.pollInFlight = false;
123
+ this.handler = handler;
124
+ this.config = config;
125
+ this.client =
126
+ client ??
127
+ new GithubClient({
128
+ appId: config.appId,
129
+ privateKey: config.privateKey,
130
+ installationId: config.installationId,
131
+ });
132
+ }
133
+ // ==========================================================================
134
+ // Public API (implements MessagingBot)
135
+ // ==========================================================================
136
+ async start() {
137
+ this.appSlug = await this.client.getAppSlug();
138
+ // Canonical noreply author email (<user-id>+<login>@users.noreply.github.com)
139
+ // so the bot's commits link to its avatar/profile; degrade to a plain
140
+ // noreply spelling when the lookup fails.
141
+ try {
142
+ const botUserId = await this.client.getUserId(`${this.appSlug}[bot]`);
143
+ this.botEmail = `${botUserId}+${this.appSlug}[bot]@users.noreply.github.com`;
144
+ }
145
+ catch {
146
+ this.botEmail = `${this.appSlug}[bot]@users.noreply.github.com`;
147
+ }
148
+ this.watchedRepos =
149
+ this.config.repos.length > 0
150
+ ? parseRepoList(this.config.repos)
151
+ : (await this.client.listInstallationRepositories()).map((repository) => ({
152
+ owner: repository.owner.login.toLowerCase(),
153
+ repo: repository.name.toLowerCase(),
154
+ }));
155
+ if (this.watchedRepos.length === 0) {
156
+ log.logWarning("GitHub: installation has no repositories; nothing to poll");
157
+ }
158
+ // Restore persisted watermarks; a repo watched for the first time gets a
159
+ // baseline of "now" so history never triggers (DESIGN.md § Event source).
160
+ const persisted = this.loadSyncState();
161
+ const now = new Date().toISOString();
162
+ for (const repo of this.watchedRepos) {
163
+ const repoKey = `${repo.owner}/${repo.repo}`;
164
+ const saved = persisted?.repos[repoKey];
165
+ this.repoState.set(repoKey, saved
166
+ ? {
167
+ baseline: saved.baseline,
168
+ cursor: saved.cursor,
169
+ seenComments: new Set(saved.seenComments),
170
+ seenIssues: new Set(saved.seenIssues),
171
+ seenReviewComments: new Set(saved.seenReviewComments ?? []),
172
+ }
173
+ : {
174
+ baseline: now,
175
+ cursor: now,
176
+ seenComments: new Set(),
177
+ seenIssues: new Set(),
178
+ seenReviewComments: new Set(),
179
+ });
180
+ }
181
+ this.persistSyncState();
182
+ setInterval(() => {
183
+ void this.poll();
184
+ }, this.config.pollIntervalMs).unref();
185
+ log.logConnected("GitHub");
186
+ log.logInfo(`GitHub bot started as @${this.appSlug}[bot], polling ${this.watchedRepos.length} repo(s) every ${Math.round(this.config.pollIntervalMs / 1000)}s`);
187
+ }
188
+ async postMessage(channel, text) {
189
+ const ref = parseGithubConversationId(channel);
190
+ const parts = splitText(text, GITHUB_MAX_COMMENT_LENGTH, formatGithubContinuation);
191
+ const firstId = await this.postComment(ref, parts[0]);
192
+ for (const part of parts.slice(1)) {
193
+ await this.postComment(ref, part);
194
+ }
195
+ return String(firstId);
196
+ }
197
+ async updateMessage(channel, ts, text) {
198
+ const ref = parseGithubConversationId(channel);
199
+ await githubRetry(() => this.client.updateIssueComment(ref.owner, ref.repo, Number(ts), text));
200
+ }
201
+ async addReaction(channel, messageTs, emoji) {
202
+ const ref = parseGithubConversationId(channel);
203
+ const content = GITHUB_REACTIONS[emoji.replace(/^:|:$/g, "")];
204
+ if (!content) {
205
+ throw new Error(`GitHub does not support reaction '${emoji}' (supported: ${Object.keys(GITHUB_REACTIONS).join(", ")})`);
206
+ }
207
+ const reviewCommentId = parseReviewCommentTs(messageTs);
208
+ await githubRetry(() => {
209
+ if (messageTs === GITHUB_ISSUE_BODY_TS) {
210
+ return this.client.createIssueReaction(ref.owner, ref.repo, ref.number, content);
211
+ }
212
+ if (reviewCommentId !== null) {
213
+ return this.client.createReviewCommentReaction(ref.owner, ref.repo, reviewCommentId, content);
214
+ }
215
+ return this.client.createCommentReaction(ref.owner, ref.repo, Number(messageTs), content);
216
+ });
217
+ }
218
+ enqueueEvent(event) {
219
+ const conversationId = event.conversationId;
220
+ const queue = this.getQueue(conversationId);
221
+ if (queue.size() >= 5) {
222
+ log.logWarning(`Event queue full for ${conversationId}, discarding: ${event.text.substring(0, 50)}`);
223
+ return false;
224
+ }
225
+ log.logInfo(`Enqueueing event for ${conversationId}: ${event.text.substring(0, 50)}`);
226
+ queue.enqueue(() => {
227
+ const context = createGithubAdapters(event, this);
228
+ return this.handler.handleEvent(event, this, context);
229
+ });
230
+ return true;
231
+ }
232
+ getMessagingInfo() {
233
+ return {
234
+ name: "github",
235
+ trustModel: "open-trigger",
236
+ formattingGuide: "## GitHub Formatting (GitHub Flavored Markdown)\n" +
237
+ "Standard Markdown plus tables, task lists, fenced code blocks, and ```suggestion blocks.\n" +
238
+ "Reference issues/PRs as #123 and users as @login (mentions notify people — use sparingly).",
239
+ channels: [],
240
+ users: [],
241
+ };
242
+ }
243
+ // ==========================================================================
244
+ // Internal helpers (used by context.ts)
245
+ // ==========================================================================
246
+ async postComment(ref, text) {
247
+ const comment = await githubRetry(() => this.client.createIssueComment(ref.owner, ref.repo, ref.number, text));
248
+ return comment.id;
249
+ }
250
+ async deleteComment(ref, commentId) {
251
+ await githubRetry(() => this.client.deleteIssueComment(ref.owner, ref.repo, commentId));
252
+ }
253
+ logToFile(conversationId, entry) {
254
+ appendChannelLog(this.config.workingDir, conversationId, entry);
255
+ }
256
+ logBotResponse(conversationId, text, ts) {
257
+ appendBotResponseLog(this.config.workingDir, conversationId, text, ts);
258
+ }
259
+ // ==========================================================================
260
+ // Private - polling
261
+ // ==========================================================================
262
+ getQueue(conversationId) {
263
+ let queue = this.queues.get(conversationId);
264
+ if (!queue) {
265
+ queue = new MessagingEventQueue("GitHub");
266
+ this.queues.set(conversationId, queue);
267
+ }
268
+ return queue;
269
+ }
270
+ /** One poll tick over every watched repo. Public for tests. */
271
+ async poll() {
272
+ if (this.pollInFlight)
273
+ return;
274
+ this.pollInFlight = true;
275
+ try {
276
+ let changed = false;
277
+ for (const repo of this.watchedRepos) {
278
+ try {
279
+ changed = (await this.pollRepo(repo)) || changed;
280
+ }
281
+ catch (err) {
282
+ log.logWarning(`GitHub poll failed for ${repo.owner}/${repo.repo}`, err instanceof Error ? err.message : String(err));
283
+ }
284
+ }
285
+ if (changed) {
286
+ this.persistSyncState();
287
+ }
288
+ }
289
+ finally {
290
+ this.pollInFlight = false;
291
+ }
292
+ }
293
+ async pollRepo(repo) {
294
+ const repoKey = `${repo.owner}/${repo.repo}`;
295
+ const state = this.repoState.get(repoKey);
296
+ if (!state)
297
+ return false;
298
+ const since = new Date(Date.parse(state.cursor) - POLL_OVERLAP_MS).toISOString();
299
+ let changed = false;
300
+ // Comment activity also bumps the parent issue's updated_at, so the issues
301
+ // list mostly echoes the comments list; only issues *created* since the
302
+ // baseline are new conversations (their body is the first message).
303
+ const issues = (await this.client.listIssuesSince(repo.owner, repo.repo, since)) ?? [];
304
+ for (const issue of issues) {
305
+ if (issue.updated_at > state.cursor) {
306
+ state.cursor = issue.updated_at;
307
+ changed = true;
308
+ }
309
+ // Edits re-surface old items with a fresh updated_at; the id watermark
310
+ // plus the creation baseline decide what actually triggers.
311
+ if (issue.created_at < state.baseline || state.seenIssues.has(issue.id))
312
+ continue;
313
+ state.seenIssues.add(issue.id);
314
+ changed = true;
315
+ if (issue.user.type === "Bot")
316
+ continue;
317
+ await this.handleIncoming({
318
+ ref: { ...repo, number: issue.number },
319
+ ts: GITHUB_ISSUE_BODY_TS,
320
+ user: issue.user.login,
321
+ text: `# ${issue.title}\n\n${issue.body ?? ""}`.trim(),
322
+ createdAt: issue.created_at,
323
+ isPr: Boolean(issue.pull_request),
324
+ });
325
+ }
326
+ const comments = (await this.client.listIssueCommentsSince(repo.owner, repo.repo, since)) ?? [];
327
+ for (const comment of comments) {
328
+ if (comment.updated_at > state.cursor) {
329
+ state.cursor = comment.updated_at;
330
+ changed = true;
331
+ }
332
+ if (comment.created_at < state.baseline || state.seenComments.has(comment.id))
333
+ continue;
334
+ state.seenComments.add(comment.id);
335
+ changed = true;
336
+ if (comment.user.type === "Bot")
337
+ continue;
338
+ await this.handleIncoming({
339
+ ref: { ...repo, number: issueNumberFromUrl(comment.issue_url) },
340
+ ts: String(comment.id),
341
+ user: comment.user.login,
342
+ text: comment.body,
343
+ createdAt: comment.created_at,
344
+ });
345
+ }
346
+ // Inline PR review comments live in their own endpoint and id space; the
347
+ // same watermark discipline applies. The bot's own thread replies (the
348
+ // github_review_reply tool) re-surface here and are dropped as Bot-authored.
349
+ const reviewComments = (await this.client.listPullReviewCommentsSince(repo.owner, repo.repo, since)) ?? [];
350
+ for (const comment of reviewComments) {
351
+ if (comment.updated_at > state.cursor) {
352
+ state.cursor = comment.updated_at;
353
+ changed = true;
354
+ }
355
+ if (comment.created_at < state.baseline || state.seenReviewComments.has(comment.id)) {
356
+ continue;
357
+ }
358
+ state.seenReviewComments.add(comment.id);
359
+ changed = true;
360
+ if (comment.user.type === "Bot")
361
+ continue;
362
+ await this.handleIncoming({
363
+ ref: { ...repo, number: prNumberFromUrl(comment.pull_request_url) },
364
+ ts: githubReviewCommentTs(comment.id),
365
+ user: comment.user.login,
366
+ text: comment.body,
367
+ createdAt: comment.created_at,
368
+ isPr: true,
369
+ review: {
370
+ commentId: comment.id,
371
+ path: comment.path,
372
+ line: comment.line,
373
+ diffHunk: comment.diff_hunk,
374
+ inReplyToId: comment.in_reply_to_id,
375
+ },
376
+ });
377
+ }
378
+ pruneSeen(state.seenComments);
379
+ pruneSeen(state.seenIssues);
380
+ pruneSeen(state.seenReviewComments);
381
+ return changed;
382
+ }
383
+ // ==========================================================================
384
+ // Private - watermark persistence
385
+ // ==========================================================================
386
+ loadSyncState() {
387
+ try {
388
+ return readJsonSchemaFileIfExists(this.config.syncStatePath, SyncStateSchema, (detail) => `Malformed GitHub sync state at ${this.config.syncStatePath}: ${detail}`);
389
+ }
390
+ catch (err) {
391
+ // A bad state file re-baselines (history won't trigger) rather than
392
+ // blocking startup; the next persist overwrites it.
393
+ log.logWarning("GitHub: ignoring unreadable sync state", err instanceof Error ? err.message : String(err));
394
+ return undefined;
395
+ }
396
+ }
397
+ persistSyncState() {
398
+ const state = { repos: {} };
399
+ for (const [repoKey, watermark] of this.repoState) {
400
+ state.repos[repoKey] = {
401
+ baseline: watermark.baseline,
402
+ cursor: watermark.cursor,
403
+ seenComments: [...watermark.seenComments],
404
+ seenIssues: [...watermark.seenIssues],
405
+ seenReviewComments: [...watermark.seenReviewComments],
406
+ };
407
+ }
408
+ ensureDirExists(dirname(this.config.syncStatePath));
409
+ atomicWritePrivateFile(this.config.syncStatePath, JSON.stringify(state));
410
+ }
411
+ // ==========================================================================
412
+ // Private - triggering
413
+ // ==========================================================================
414
+ /** Fresh regex per call: the `g` flag makes `test`/`replace` stateful. */
415
+ mentionPattern() {
416
+ return this.appSlug ? new RegExp(`@${this.appSlug}(?![\\w-])`, "gi") : null;
417
+ }
418
+ isMentioned(text) {
419
+ const pattern = this.mentionPattern();
420
+ return pattern ? pattern.test(text) : false;
421
+ }
422
+ stripMention(text) {
423
+ const pattern = this.mentionPattern();
424
+ return (pattern ? text.replace(pattern, "") : text).trim();
425
+ }
426
+ isParticipating(conversationId) {
427
+ return existsSync(join(this.config.workingDir, conversationId, "log.jsonl"));
428
+ }
429
+ /**
430
+ * True when `user` holds at least write permission on the repo. Fails
431
+ * closed: a failed lookup denies the trigger (and logs) rather than
432
+ * letting an unverified commenter drive the agent.
433
+ */
434
+ async hasTriggerPermission(ref, user) {
435
+ const cacheKey = `${ref.owner}/${ref.repo}#${user}`;
436
+ const cached = this.permissionCache.get(cacheKey);
437
+ if (cached && Date.now() < cached.expiresAt) {
438
+ return cached.rank >= REQUIRED_TRIGGER_RANK;
439
+ }
440
+ let rank;
441
+ try {
442
+ const role = await githubRetry(() => this.client.getCollaboratorPermission(ref.owner, ref.repo, user));
443
+ // Custom role_name values are unrankable; the legacy permission field
444
+ // still carries their closest standard mapping, so take the stronger.
445
+ rank = Math.max(PERMISSION_RANK[role.role_name ?? ""] ?? 0, PERMISSION_RANK[role.permission] ?? 0);
446
+ }
447
+ catch (err) {
448
+ log.logWarning(`GitHub: permission lookup failed for ${user} on ${ref.owner}/${ref.repo}; denying trigger`, err instanceof Error ? err.message : String(err));
449
+ return false;
450
+ }
451
+ this.permissionCache.set(cacheKey, {
452
+ rank,
453
+ expiresAt: Date.now() + PERMISSION_CACHE_TTL_MS,
454
+ });
455
+ return rank >= REQUIRED_TRIGGER_RANK;
456
+ }
457
+ async handleIncoming(item) {
458
+ const conversationId = buildGithubConversationId(item.ref);
459
+ const mentioned = this.isMentioned(item.text);
460
+ const participating = this.isParticipating(conversationId);
461
+ // Narrow trigger (DESIGN.md): only threads the bot is mentioned in or
462
+ // already participates in. Everything else is ignored without logging so
463
+ // busy repos don't grow conversation dirs for untouched issues.
464
+ if (!mentioned && !participating)
465
+ return;
466
+ // Permission gate: on public repos anyone can comment, so a mention from
467
+ // anyone below write permission is ignored entirely (no log, no state).
468
+ if (!(await this.hasTriggerPermission(item.ref, item.user))) {
469
+ log.logInfo(`GitHub: ignoring ${conversationId} comment from ${item.user} (below write permission)`);
470
+ return;
471
+ }
472
+ const cleanedText = this.stripMention(item.text);
473
+ const sessionKey = resolveChatSessionKey({
474
+ conversationId,
475
+ conversationKind: "shared",
476
+ messageId: item.ts,
477
+ persistentTopLevel: true,
478
+ });
479
+ const messageText = item.review
480
+ ? await this.formatReviewMessage(item, cleanedText)
481
+ : cleanedText;
482
+ const eventBase = {
483
+ type: item.ts === GITHUB_ISSUE_BODY_TS ? "issue" : "message",
484
+ conversationId,
485
+ conversationKind: "shared",
486
+ ts: item.ts,
487
+ sessionKey,
488
+ user: item.user,
489
+ userName: item.user,
490
+ text: messageText,
491
+ };
492
+ await processMessageIntake({
493
+ eventBase,
494
+ workingDir: this.config.workingDir,
495
+ isAutoReplyCandidate: false,
496
+ // Match on the user-typed text, not the review-decorated messageText.
497
+ magicWord: { text: cleanedText, addressed: mentioned, scopeFallback: "never" },
498
+ busyPolicy: "queue",
499
+ logEntryBase: {
500
+ date: item.createdAt,
501
+ ts: item.ts,
502
+ user: item.user,
503
+ userName: item.user,
504
+ text: messageText,
505
+ isMessagingBot: false,
506
+ },
507
+ log: (entry) => this.logToFile(conversationId, entry),
508
+ // GitHub has no attachments; the prep hook is used to lay down the
509
+ // issue context and repo clone only for messages that will dispatch,
510
+ // and before the comment itself is logged.
511
+ processAttachments: async () => {
512
+ await this.prepareConversation(item, conversationId, participating);
513
+ return [];
514
+ },
515
+ queueKey: conversationId,
516
+ enqueue: (queueKey, work) => this.getQueue(queueKey).enqueue(work),
517
+ handler: this.handler,
518
+ bot: this,
519
+ createContext: (event) => createGithubAdapters(event, this),
520
+ });
521
+ }
522
+ /**
523
+ * First contact: log the issue title/body ahead of the comment so the
524
+ * session history starts with what the thread is about. The clone is
525
+ * (re)attempted on every trigger while ./repo is missing — a no-op once
526
+ * it exists — so a failed first clone (e.g. App permissions granted
527
+ * later) heals on the next mention instead of wedging the conversation.
528
+ */
529
+ async prepareConversation(item, conversationId, participating) {
530
+ let isPrHint = item.isPr;
531
+ if (!participating && item.ts !== GITHUB_ISSUE_BODY_TS) {
532
+ const issue = await this.logIssueContext(item.ref, conversationId, item.createdAt);
533
+ // Only comments arrive without the PR-ness hint; review comments carry
534
+ // isPr: true (they only exist on PRs) and that knowledge is authoritative.
535
+ if (issue && isPrHint === undefined)
536
+ isPrHint = Boolean(issue.pull_request);
537
+ }
538
+ if (!existsSync(this.repoDir(conversationId))) {
539
+ const isPr = isPrHint ?? (await this.fetchIsPr(item.ref));
540
+ await this.ensureRepoClone(item.ref, conversationId, isPr);
541
+ }
542
+ }
543
+ /**
544
+ * Decorates an inline review comment with its diff anchor and (for replies)
545
+ * the thread's earlier turns, so the agent knows which code the feedback is
546
+ * about without hunting through the PR. The rc-<id> in the header is what
547
+ * github_review_reply takes to answer in-thread.
548
+ */
549
+ async formatReviewMessage(item, cleanedText) {
550
+ const review = item.review;
551
+ const location = review.line !== null ? `${review.path}:${review.line}` : review.path;
552
+ const parts = [`[PR review comment ${githubReviewCommentTs(review.commentId)} on ${location}]`];
553
+ if (review.diffHunk) {
554
+ const hunk = review.diffHunk.length > MAX_DIFF_HUNK_CHARS
555
+ ? `…${review.diffHunk.slice(-MAX_DIFF_HUNK_CHARS)}`
556
+ : review.diffHunk;
557
+ parts.push(`\`\`\`diff\n${hunk}\n\`\`\``);
558
+ }
559
+ if (review.inReplyToId !== undefined) {
560
+ const turns = await this.reviewThreadContext(item.ref, review.inReplyToId, review.commentId);
561
+ if (turns.length > 0) {
562
+ parts.push(`Thread so far:\n${turns.join("\n")}`);
563
+ }
564
+ }
565
+ parts.push(cleanedText);
566
+ return parts.join("\n");
567
+ }
568
+ /**
569
+ * Earlier turns of one review thread (root + replies before the trigger),
570
+ * newest-truncated. Fail-soft: context is a nicety, the trigger is not.
571
+ */
572
+ async reviewThreadContext(ref, rootId, beforeId) {
573
+ try {
574
+ const all = await githubRetry(() => this.client.listPullReviewComments(ref.owner, ref.repo, ref.number));
575
+ return all
576
+ .filter((comment) => (comment.id === rootId || comment.in_reply_to_id === rootId) && comment.id < beforeId)
577
+ .slice(-MAX_THREAD_TURNS)
578
+ .map((comment) => {
579
+ const body = comment.body.length > MAX_THREAD_TURN_CHARS
580
+ ? `${comment.body.slice(0, MAX_THREAD_TURN_CHARS)}…`
581
+ : comment.body;
582
+ return `@${comment.user.login}: ${body}`;
583
+ });
584
+ }
585
+ catch (err) {
586
+ log.logWarning(`GitHub: could not fetch review thread context for ${ref.owner}/${ref.repo}#${ref.number}`, err instanceof Error ? err.message : String(err));
587
+ return [];
588
+ }
589
+ }
590
+ async logIssueContext(ref, conversationId, triggerCreatedAt) {
591
+ let issue;
592
+ try {
593
+ issue = await githubRetry(() => this.client.getIssue(ref.owner, ref.repo, ref.number));
594
+ }
595
+ catch (err) {
596
+ log.logWarning(`GitHub: could not fetch issue context for ${conversationId}`, err instanceof Error ? err.message : String(err));
597
+ return null;
598
+ }
599
+ // Dated just before the triggering comment, NOT issue.created_at: history
600
+ // sync date-sorts entries and drops anything older than its recency
601
+ // window, so an old issue's real creation date would silently evict the
602
+ // one message that says what the thread is about.
603
+ const triggerMs = Date.parse(triggerCreatedAt);
604
+ const contextDate = new Date((Number.isFinite(triggerMs) ? triggerMs : Date.now()) - 1000);
605
+ this.logToFile(conversationId, {
606
+ date: contextDate.toISOString(),
607
+ ts: GITHUB_ISSUE_BODY_TS,
608
+ user: issue.user.login,
609
+ userName: issue.user.login,
610
+ text: `# ${issue.title}\n\n${issue.body ?? ""}`.trim(),
611
+ attachments: [],
612
+ isMessagingBot: false,
613
+ });
614
+ return issue;
615
+ }
616
+ // ==========================================================================
617
+ // Repo clone + pull requests
618
+ // ==========================================================================
619
+ repoDir(conversationId) {
620
+ return join(this.config.workingDir, conversationId, "repo");
621
+ }
622
+ /** Whether the conversation's issue is a PR, for clone retries where the
623
+ * triggering item didn't carry that knowledge. Errs toward plain issue. */
624
+ async fetchIsPr(ref) {
625
+ try {
626
+ const issue = await githubRetry(() => this.client.getIssue(ref.owner, ref.repo, ref.number));
627
+ return Boolean(issue.pull_request);
628
+ }
629
+ catch {
630
+ return false;
631
+ }
632
+ }
633
+ /**
634
+ * The PR's head branch name when it lives in this repo — that is the name
635
+ * the local checkout should carry, so committing on it and calling
636
+ * github_pr pushes back to the PR itself instead of opening a new one.
637
+ * Fork heads (unpushable with the repo-scoped token) and failed lookups
638
+ * return undefined; callers then fall back to the synthetic pr-<n>.
639
+ */
640
+ async fetchPrHeadBranch(ref) {
641
+ try {
642
+ const pr = await githubRetry(() => this.client.getPullRequest(ref.owner, ref.repo, ref.number));
643
+ const sameRepo = pr.head?.repo?.full_name?.toLowerCase() === `${ref.owner}/${ref.repo}`.toLowerCase();
644
+ return sameRepo ? pr.head?.ref : undefined;
645
+ }
646
+ catch {
647
+ return undefined;
648
+ }
649
+ }
650
+ /**
651
+ * Clone the conversation's repo into its dir (bind-mounted into the
652
+ * sandbox) using an ephemeral contents:read token that never leaves the
653
+ * host. Non-fatal: a failed clone degrades to the pre-clone experience.
654
+ */
655
+ async ensureRepoClone(ref, conversationId, isPr) {
656
+ const dir = this.repoDir(conversationId);
657
+ if (existsSync(dir))
658
+ return;
659
+ try {
660
+ const token = await this.client.createScopedInstallationToken(ref.repo, {
661
+ contents: "read",
662
+ });
663
+ const prHeadBranch = isPr ? await this.fetchPrHeadBranch(ref) : undefined;
664
+ await cloneRepo({
665
+ url: `https://github.com/${ref.owner}/${ref.repo}.git`,
666
+ dir,
667
+ token,
668
+ botLogin: `${this.appSlug}[bot]`,
669
+ botEmail: this.botEmail ?? `${this.appSlug}[bot]@users.noreply.github.com`,
670
+ prNumber: isPr ? ref.number : undefined,
671
+ prHeadBranch,
672
+ });
673
+ log.logInfo(`[${conversationId}] Cloned ${ref.owner}/${ref.repo}${isPr ? ` and checked out PR #${ref.number} head as ${prHeadBranch ?? `pr-${ref.number}`}` : ""}`);
674
+ }
675
+ catch (err) {
676
+ log.logWarning(`GitHub: repo clone failed for ${conversationId}`, err instanceof Error ? err.message : String(err));
677
+ }
678
+ }
679
+ /**
680
+ * Backs the `github_pr` tool: push a `pi/*` branch the agent prepared in
681
+ * the conversation's clone, then open a pull request as the App. The write
682
+ * token is minted per call, scoped to this one repo, and never enters the
683
+ * sandbox.
684
+ */
685
+ async pushAndCreatePr(conversationId, request) {
686
+ const ref = parseGithubConversationId(conversationId);
687
+ const dir = this.repoDir(conversationId);
688
+ if (!existsSync(dir)) {
689
+ throw new Error("This conversation has no ./repo clone to push from.");
690
+ }
691
+ if (!GITHUB_PUSH_BRANCH_PATTERN.test(request.branch)) {
692
+ throw new Error(`Branch '${request.branch}' is not pushable: name it pi/<something> (e.g. pi/fix-${ref.number}).`);
693
+ }
694
+ const repository = await githubRetry(() => this.client.getRepository(ref.owner, ref.repo));
695
+ const base = request.base ?? repository.default_branch;
696
+ const token = await this.client.createScopedInstallationToken(ref.repo, {
697
+ contents: "write",
698
+ pull_requests: "write",
699
+ });
700
+ await pushBranch({ dir, branch: request.branch, token });
701
+ try {
702
+ const pr = await githubRetry(() => this.client.createPullRequest(ref.owner, ref.repo, {
703
+ title: request.title,
704
+ head: request.branch,
705
+ base,
706
+ body: request.body,
707
+ draft: request.draft,
708
+ }));
709
+ log.logInfo(`[${conversationId}] Opened PR #${pr.number}: ${pr.html_url}`);
710
+ return { number: pr.number, url: pr.html_url };
711
+ }
712
+ catch (err) {
713
+ // Idempotency: pushing more commits to a branch that already has an
714
+ // open PR updates that PR; the create call then 422s. Surface the
715
+ // existing PR instead of failing the fix-CI-push-again loop.
716
+ if (err instanceof GithubApiError &&
717
+ err.status === 422 &&
718
+ /already exists/i.test(err.message)) {
719
+ const existing = await this.client.findOpenPullRequestByBranch(ref.owner, ref.repo, request.branch);
720
+ if (existing) {
721
+ log.logInfo(`[${conversationId}] Pushed to existing PR #${existing.number}: ${existing.html_url}`);
722
+ return { number: existing.number, url: existing.html_url, updatedExisting: true };
723
+ }
724
+ }
725
+ throw err;
726
+ }
727
+ }
728
+ /**
729
+ * Backs the `github_checks` tool: CI check runs for a branch the agent
730
+ * pushed, or for the conversation's PR head when no branch is given.
731
+ */
732
+ async getChecks(conversationId, branch) {
733
+ const ref = parseGithubConversationId(conversationId);
734
+ let target = branch;
735
+ if (!target) {
736
+ let pr;
737
+ try {
738
+ pr = await githubRetry(() => this.client.getPullRequest(ref.owner, ref.repo, ref.number));
739
+ }
740
+ catch {
741
+ pr = null;
742
+ }
743
+ if (!pr?.head) {
744
+ throw new Error("This conversation is not a pull request — pass the branch whose checks you want.");
745
+ }
746
+ target = pr.head.sha;
747
+ }
748
+ const runs = await githubRetry(() => this.client.listCheckRuns(ref.owner, ref.repo, target));
749
+ return runs.map((run) => {
750
+ const externalId = run.external_id?.trim() || null;
751
+ let buildLogAvailable = false;
752
+ // Cloud Build runs carry their build UUID in external_id and the GCP
753
+ // project in details_url. Remember project-by-build so getBuildLog only
754
+ // serves builds the agent actually saw on this repo's checks.
755
+ if (this.config.cloudBuild &&
756
+ run.app?.slug === CLOUD_BUILD_APP_SLUG &&
757
+ externalId &&
758
+ isCloudBuildId(externalId)) {
759
+ const project = projectFromDetailsUrl(run.details_url) ?? this.config.cloudBuild.projectFallback;
760
+ if (project) {
761
+ this.rememberBuild(externalId, project);
762
+ buildLogAvailable = true;
763
+ }
764
+ }
765
+ return {
766
+ id: run.id,
767
+ name: run.name,
768
+ status: run.status,
769
+ conclusion: run.conclusion,
770
+ url: run.html_url,
771
+ appSlug: run.app?.slug ?? null,
772
+ outputSummary: run.output?.summary?.trim() ? run.output.summary.slice(0, 500) : null,
773
+ externalId,
774
+ buildLogAvailable,
775
+ };
776
+ });
777
+ }
778
+ rememberBuild(buildId, project) {
779
+ // Insertion-ordered Map as a small LRU: re-insert on touch, prune oldest.
780
+ this.buildRef.delete(buildId);
781
+ this.buildRef.set(buildId, { project });
782
+ if (this.buildRef.size > 200) {
783
+ const oldest = this.buildRef.keys().next().value;
784
+ if (oldest !== undefined)
785
+ this.buildRef.delete(oldest);
786
+ }
787
+ }
788
+ /**
789
+ * Backs the Cloud Build log mode of `github_checks`. GCP credentials stay
790
+ * host-side (ADC via GOOGLE_APPLICATION_CREDENTIALS — e.g. a Workload
791
+ * Identity Federation external_account file); the build must have appeared
792
+ * in a github_checks summary first, which is also where its project is
793
+ * learned. Unreadable logs degrade to the console url, never a dead end.
794
+ */
795
+ async getBuildLog(conversationId, buildId) {
796
+ if (!this.config.cloudBuild) {
797
+ throw new Error("Cloud Build log access is not configured on this host (no GCP credentials).");
798
+ }
799
+ const known = this.buildRef.get(buildId);
800
+ if (!known) {
801
+ throw new Error(`Unknown build id ${buildId}: run github_checks first and take a [build …] id from ` +
802
+ `the summary.`);
803
+ }
804
+ let logText;
805
+ try {
806
+ logText = await fetchCloudBuildLog({
807
+ tokenProvider: this.config.cloudBuild.tokenProvider,
808
+ project: known.project,
809
+ buildId,
810
+ });
811
+ }
812
+ catch (err) {
813
+ if (err instanceof CloudBuildLogUnavailableError) {
814
+ throw new Error(`${err.message}${err.consoleUrl ? ` Console: ${err.consoleUrl}` : ""} Use the check's ` +
815
+ `summary/url from github_checks, or reproduce the failure locally in ./repo.`, { cause: err });
816
+ }
817
+ throw err;
818
+ }
819
+ const MAX_LOG_CHARS = 20000;
820
+ return logText.length > MAX_LOG_CHARS
821
+ ? `…(truncated to the last ${MAX_LOG_CHARS} chars)\n${logText.slice(-MAX_LOG_CHARS)}`
822
+ : logText;
823
+ }
824
+ /**
825
+ * Backs the `github_sync` tool: refresh the conversation's ./repo clone
826
+ * from origin with an ephemeral read token. Fetch-only when moving the
827
+ * checkout could lose agent work; the formatted report tells the agent
828
+ * what happened and what to do next.
829
+ */
830
+ async syncRepo(conversationId, branch) {
831
+ const ref = parseGithubConversationId(conversationId);
832
+ const dir = this.repoDir(conversationId);
833
+ if (!existsSync(dir)) {
834
+ throw new Error("This conversation has no ./repo clone to sync.");
835
+ }
836
+ const token = await this.client.createScopedInstallationToken(ref.repo, {
837
+ contents: "read",
838
+ });
839
+ let prNumber;
840
+ let prHeadBranch;
841
+ let defaultBranch;
842
+ if (!branch) {
843
+ if (await this.fetchIsPr(ref)) {
844
+ prNumber = ref.number;
845
+ prHeadBranch = await this.fetchPrHeadBranch(ref);
846
+ }
847
+ else {
848
+ const repository = await githubRetry(() => this.client.getRepository(ref.owner, ref.repo));
849
+ defaultBranch = repository.default_branch;
850
+ }
851
+ }
852
+ const result = await syncRepo({ dir, token, branch, prNumber, prHeadBranch, defaultBranch });
853
+ if (result.updatedCheckout) {
854
+ return `Updated ./repo: branch ${result.target} is now at ${result.fetchedSha.slice(0, 12)}.`;
855
+ }
856
+ const reasons = [];
857
+ if (result.currentBranch !== result.target) {
858
+ reasons.push(`the checkout is on '${result.currentBranch}', not '${result.target}'`);
859
+ }
860
+ if (result.dirty) {
861
+ reasons.push("the working tree has uncommitted changes");
862
+ }
863
+ if (result.localCommits > 0) {
864
+ reasons.push(`'${result.target}' has ${result.localCommits} local commit(s) not on origin`);
865
+ }
866
+ return (`Fetched ${result.target} to FETCH_HEAD (${result.fetchedSha.slice(0, 12)}) but left the ` +
867
+ `checkout alone: ${reasons.join("; ")}. Merge or rebase FETCH_HEAD yourself ` +
868
+ `(e.g. git merge FETCH_HEAD), or commit your work to a pi/* branch first.`);
869
+ }
870
+ /**
871
+ * Backs the `github_read` tool: PR/issue metadata the ./repo clone cannot
872
+ * provide (diff stats, review state, other issues in the repo). Owner/repo
873
+ * always come from the conversation id, so reads never leave this repo.
874
+ */
875
+ async readGithub(conversationId, request) {
876
+ const ref = parseGithubConversationId(conversationId);
877
+ const number = request.number ?? ref.number;
878
+ switch (request.action) {
879
+ case "pr": {
880
+ const pr = await githubRetry(() => this.client.getPullRequest(ref.owner, ref.repo, number));
881
+ return { kind: "pr", pr };
882
+ }
883
+ case "pr_files": {
884
+ const files = await githubRetry(() => this.client.listPullRequestFiles(ref.owner, ref.repo, number));
885
+ return { kind: "pr_files", files };
886
+ }
887
+ case "pr_reviews": {
888
+ const [reviews, threads] = await Promise.all([
889
+ githubRetry(() => this.client.listPullRequestReviews(ref.owner, ref.repo, number)),
890
+ githubRetry(() => this.client.listPullReviewComments(ref.owner, ref.repo, number)),
891
+ ]);
892
+ return { kind: "pr_reviews", reviews, threads };
893
+ }
894
+ case "issue": {
895
+ const issue = await githubRetry(() => this.client.getIssue(ref.owner, ref.repo, number));
896
+ return { kind: "issue", issue };
897
+ }
898
+ case "comments": {
899
+ const comments = await githubRetry(() => this.client.listIssueComments(ref.owner, ref.repo, number));
900
+ return { kind: "comments", comments };
901
+ }
902
+ case "list": {
903
+ const issues = await githubRetry(() => this.client.listIssues(ref.owner, ref.repo, {
904
+ state: request.state,
905
+ labels: request.labels,
906
+ creator: request.creator,
907
+ }));
908
+ return { kind: "list", issues };
909
+ }
910
+ default:
911
+ throw new Error(`Unknown github_read action: ${String(request.action)}`);
912
+ }
913
+ }
914
+ /**
915
+ * Backs the `github_issue` tool: labels, assignees, and open/closed state
916
+ * on any issue of this conversation's repo (triage). Same-repo by
917
+ * construction — owner/repo come from the conversation id; the action enum
918
+ * is closed, so lock/delete/transfer cannot be expressed at all.
919
+ */
920
+ async manageIssue(conversationId, request) {
921
+ const ref = parseGithubConversationId(conversationId);
922
+ const number = request.number ?? ref.number;
923
+ try {
924
+ switch (request.action) {
925
+ case "add_labels": {
926
+ if (!request.labels?.length) {
927
+ throw new Error("add_labels requires a non-empty labels array.");
928
+ }
929
+ await githubRetry(() => this.client.addIssueLabels(ref.owner, ref.repo, number, request.labels));
930
+ return `Added label(s) ${request.labels.join(", ")} to #${number}.`;
931
+ }
932
+ case "remove_label": {
933
+ if (!request.label) {
934
+ throw new Error("remove_label requires a label name.");
935
+ }
936
+ await githubRetry(() => this.client.removeIssueLabel(ref.owner, ref.repo, number, request.label));
937
+ return `Removed label ${request.label} from #${number}.`;
938
+ }
939
+ case "add_assignees": {
940
+ if (!request.assignees?.length) {
941
+ throw new Error("add_assignees requires a non-empty assignees array.");
942
+ }
943
+ await githubRetry(() => this.client.addIssueAssignees(ref.owner, ref.repo, number, request.assignees));
944
+ return `Assigned ${request.assignees.map((login) => `@${login}`).join(", ")} to #${number}.`;
945
+ }
946
+ case "remove_assignees": {
947
+ if (!request.assignees?.length) {
948
+ throw new Error("remove_assignees requires a non-empty assignees array.");
949
+ }
950
+ await githubRetry(() => this.client.removeIssueAssignees(ref.owner, ref.repo, number, request.assignees));
951
+ return `Unassigned ${request.assignees.map((login) => `@${login}`).join(", ")} from #${number}.`;
952
+ }
953
+ case "close": {
954
+ await githubRetry(() => this.client.updateIssueState(ref.owner, ref.repo, number, "closed", request.state_reason));
955
+ return `Closed #${number}${request.state_reason ? ` as ${request.state_reason}` : ""}.`;
956
+ }
957
+ case "reopen": {
958
+ await githubRetry(() => this.client.updateIssueState(ref.owner, ref.repo, number, "open"));
959
+ return `Reopened #${number}.`;
960
+ }
961
+ default:
962
+ throw new Error(`Unknown github_issue action: ${String(request.action)}`);
963
+ }
964
+ }
965
+ catch (err) {
966
+ if (err instanceof GithubApiError && err.status === 404) {
967
+ throw new Error(`Issue #${number} not found in ${ref.owner}/${ref.repo}.`, { cause: err });
968
+ }
969
+ throw err;
970
+ }
971
+ }
972
+ /**
973
+ * Backs the `github_review_reply` tool: answer inside one inline review
974
+ * thread of this conversation's PR. The comment id comes from an
975
+ * `[PR review comment rc-<id> …]` message; GitHub validates it belongs to
976
+ * this PR (404 otherwise, translated into agent-actionable guidance).
977
+ */
978
+ async replyToReviewThread(conversationId, commentId, body) {
979
+ if (!Number.isInteger(commentId) || commentId <= 0) {
980
+ throw new Error("comment_id must be the numeric id from an [PR review comment rc-<id> …] message.");
981
+ }
982
+ const ref = parseGithubConversationId(conversationId);
983
+ const text = body.length > GITHUB_MAX_COMMENT_LENGTH
984
+ ? `${body.slice(0, GITHUB_MAX_COMMENT_LENGTH)}\n…(truncated)`
985
+ : body;
986
+ try {
987
+ const reply = await githubRetry(() => this.client.replyToReviewComment(ref.owner, ref.repo, ref.number, commentId, text));
988
+ return {
989
+ url: `https://github.com/${ref.owner}/${ref.repo}/pull/${ref.number}#discussion_r${reply.id}`,
990
+ };
991
+ }
992
+ catch (err) {
993
+ if (err instanceof GithubApiError && err.status === 404) {
994
+ throw new Error(`Comment ${commentId} is not a review comment on this PR. Take the id from an ` +
995
+ `[PR review comment rc-<id> …] message in this conversation; do not guess ids.`, { cause: err });
996
+ }
997
+ throw err;
998
+ }
999
+ }
1000
+ /**
1001
+ * Backs the log mode of `github_checks`: the tail of one CI job's log so
1002
+ * the agent can diagnose a failing check without sandbox credentials. Only
1003
+ * GitHub Actions checks have logs on GitHub — external CI apps (e.g. Cloud
1004
+ * Build) keep theirs on their own service, so a 404 here is translated
1005
+ * into guidance instead of a dead end.
1006
+ */
1007
+ async getJobLog(conversationId, jobId) {
1008
+ if (!Number.isInteger(jobId) || jobId <= 0) {
1009
+ throw new Error("job_id must be a positive Actions job id taken from a [job …] entry in the github_checks summary.");
1010
+ }
1011
+ const ref = parseGithubConversationId(conversationId);
1012
+ let logText;
1013
+ try {
1014
+ logText = await githubRetry(() => this.client.getJobLog(ref.owner, ref.repo, jobId));
1015
+ }
1016
+ catch (err) {
1017
+ if (err instanceof GithubApiError && err.status === 404) {
1018
+ throw new Error(`No GitHub Actions log for job ${jobId}. Logs are only available for checks reported ` +
1019
+ `by github-actions; external CI (e.g. Cloud Build) keeps logs on its own service — ` +
1020
+ `use that check's summary/url from github_checks, or reproduce the failure locally ` +
1021
+ `in ./repo instead. Do not retry with other ids.`, { cause: err });
1022
+ }
1023
+ throw err;
1024
+ }
1025
+ const MAX_LOG_CHARS = 20000;
1026
+ return logText.length > MAX_LOG_CHARS
1027
+ ? `…(truncated to the last ${MAX_LOG_CHARS} chars)\n${logText.slice(-MAX_LOG_CHARS)}`
1028
+ : logText;
1029
+ }
1030
+ }
1031
+ //# sourceMappingURL=bot.js.map