lastlight 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (478) hide show
  1. package/README.md +265 -118
  2. package/agent-context/rules.md +62 -9
  3. package/agent-context/security.md +69 -0
  4. package/config/default.yaml +89 -0
  5. package/deploy/.env.production.example +20 -2
  6. package/deploy/entrypoint.sh +16 -17
  7. package/deploy/native/README.md +160 -0
  8. package/deploy/native/install.sh +110 -0
  9. package/deploy/native/lastlight.env.example +95 -0
  10. package/deploy/native/lastlight.service +50 -0
  11. package/deploy/sandbox-entrypoint.sh +59 -42
  12. package/dist/admin/auth.d.ts +1 -1
  13. package/dist/admin/auth.js +3 -1
  14. package/dist/admin/auth.js.map +1 -1
  15. package/dist/admin/chat-session-reader.d.ts +11 -14
  16. package/dist/admin/chat-session-reader.js +27 -51
  17. package/dist/admin/chat-session-reader.js.map +1 -1
  18. package/dist/admin/docker.d.ts +37 -0
  19. package/dist/admin/docker.js +86 -1
  20. package/dist/admin/docker.js.map +1 -1
  21. package/dist/admin/index.js +38 -3
  22. package/dist/admin/index.js.map +1 -1
  23. package/dist/admin/log-search.test.d.ts +1 -0
  24. package/dist/admin/log-search.test.js +78 -0
  25. package/dist/admin/log-search.test.js.map +1 -0
  26. package/dist/admin/routes.d.ts +52 -3
  27. package/dist/admin/routes.js +780 -63
  28. package/dist/admin/routes.js.map +1 -1
  29. package/dist/admin/routes.test.js +680 -14
  30. package/dist/admin/routes.test.js.map +1 -1
  31. package/dist/admin/server-logs.test.d.ts +1 -0
  32. package/dist/admin/server-logs.test.js +58 -0
  33. package/dist/admin/server-logs.test.js.map +1 -0
  34. package/dist/admin/sessions.d.ts +26 -39
  35. package/dist/admin/sessions.js +86 -310
  36. package/dist/admin/sessions.js.map +1 -1
  37. package/dist/admin/sessions.test.d.ts +1 -0
  38. package/dist/admin/sessions.test.js +78 -0
  39. package/dist/admin/sessions.test.js.map +1 -0
  40. package/dist/admin/version.d.ts +15 -0
  41. package/dist/admin/version.js +73 -0
  42. package/dist/admin/version.js.map +1 -0
  43. package/dist/admin/version.test.d.ts +1 -0
  44. package/dist/admin/version.test.js +71 -0
  45. package/dist/admin/version.test.js.map +1 -0
  46. package/dist/cli-config.d.ts +55 -0
  47. package/dist/cli-config.js +119 -0
  48. package/dist/cli-config.js.map +1 -0
  49. package/dist/cli-config.test.d.ts +1 -0
  50. package/dist/cli-config.test.js +92 -0
  51. package/dist/cli-config.test.js.map +1 -0
  52. package/dist/cli-format.d.ts +19 -0
  53. package/dist/cli-format.js +107 -0
  54. package/dist/cli-format.js.map +1 -0
  55. package/dist/cli-server.d.ts +73 -0
  56. package/dist/cli-server.js +347 -0
  57. package/dist/cli-server.js.map +1 -0
  58. package/dist/cli-server.test.d.ts +1 -0
  59. package/dist/cli-server.test.js +38 -0
  60. package/dist/cli-server.test.js.map +1 -0
  61. package/dist/cli-timeline.d.ts +35 -0
  62. package/dist/cli-timeline.js +373 -0
  63. package/dist/cli-timeline.js.map +1 -0
  64. package/dist/cli-timeline.test.d.ts +1 -0
  65. package/dist/cli-timeline.test.js +104 -0
  66. package/dist/cli-timeline.test.js.map +1 -0
  67. package/dist/cli.d.ts +0 -19
  68. package/dist/cli.js +910 -194
  69. package/dist/cli.js.map +1 -1
  70. package/dist/config-overlay.test.d.ts +1 -0
  71. package/dist/config-overlay.test.js +112 -0
  72. package/dist/config-overlay.test.js.map +1 -0
  73. package/dist/config-resolve.d.ts +28 -0
  74. package/dist/config-resolve.js +47 -0
  75. package/dist/config-resolve.js.map +1 -0
  76. package/dist/config-resolve.test.d.ts +1 -0
  77. package/dist/config-resolve.test.js +68 -0
  78. package/dist/config-resolve.test.js.map +1 -0
  79. package/dist/config.d.ts +62 -49
  80. package/dist/config.js +452 -76
  81. package/dist/config.js.map +1 -1
  82. package/dist/config.test.js +201 -32
  83. package/dist/config.test.js.map +1 -1
  84. package/dist/connectors/github-webhook.js +83 -5
  85. package/dist/connectors/github-webhook.js.map +1 -1
  86. package/dist/connectors/github-webhook.test.d.ts +1 -0
  87. package/dist/connectors/github-webhook.test.js +156 -0
  88. package/dist/connectors/github-webhook.test.js.map +1 -0
  89. package/dist/connectors/messaging/session-manager.d.ts +18 -0
  90. package/dist/connectors/messaging/session-manager.js +83 -2
  91. package/dist/connectors/messaging/session-manager.js.map +1 -1
  92. package/dist/connectors/messaging/session-manager.test.d.ts +1 -0
  93. package/dist/connectors/messaging/session-manager.test.js +144 -0
  94. package/dist/connectors/messaging/session-manager.test.js.map +1 -0
  95. package/dist/connectors/slack/connector.d.ts +9 -0
  96. package/dist/connectors/slack/connector.js +15 -0
  97. package/dist/connectors/slack/connector.js.map +1 -1
  98. package/dist/connectors/slack/mrkdwn.js +81 -0
  99. package/dist/connectors/slack/mrkdwn.js.map +1 -1
  100. package/dist/connectors/slack/mrkdwn.test.js +49 -0
  101. package/dist/connectors/slack/mrkdwn.test.js.map +1 -1
  102. package/dist/connectors/types.d.ts +1 -1
  103. package/dist/cron/fanout.d.ts +33 -0
  104. package/dist/cron/fanout.js +55 -0
  105. package/dist/cron/fanout.js.map +1 -0
  106. package/dist/cron/fanout.test.d.ts +1 -0
  107. package/dist/cron/fanout.test.js +73 -0
  108. package/dist/cron/fanout.test.js.map +1 -0
  109. package/dist/cron/jobs.d.ts +5 -0
  110. package/dist/cron/jobs.js +10 -3
  111. package/dist/cron/jobs.js.map +1 -1
  112. package/dist/cron/scheduler.d.ts +12 -0
  113. package/dist/cron/scheduler.js +27 -1
  114. package/dist/cron/scheduler.js.map +1 -1
  115. package/dist/engine/agent-executor.d.ts +158 -0
  116. package/dist/engine/agent-executor.js +1140 -0
  117. package/dist/engine/agent-executor.js.map +1 -0
  118. package/dist/engine/agent-executor.test.d.ts +1 -0
  119. package/dist/engine/agent-executor.test.js +395 -0
  120. package/dist/engine/agent-executor.test.js.map +1 -0
  121. package/dist/engine/chat-runner.d.ts +123 -0
  122. package/dist/engine/chat-runner.js +379 -0
  123. package/dist/engine/chat-runner.js.map +1 -0
  124. package/dist/engine/chat-runner.test.d.ts +1 -0
  125. package/dist/engine/chat-runner.test.js +104 -0
  126. package/dist/engine/chat-runner.test.js.map +1 -0
  127. package/dist/engine/chat-skills.d.ts +45 -0
  128. package/dist/engine/chat-skills.js +184 -0
  129. package/dist/engine/chat-skills.js.map +1 -0
  130. package/dist/engine/chat-skills.test.d.ts +1 -0
  131. package/dist/engine/chat-skills.test.js +20 -0
  132. package/dist/engine/chat-skills.test.js.map +1 -0
  133. package/dist/engine/chat.d.ts +20 -23
  134. package/dist/engine/chat.js +251 -155
  135. package/dist/engine/chat.js.map +1 -1
  136. package/dist/engine/chat.test.d.ts +1 -0
  137. package/dist/engine/chat.test.js +42 -0
  138. package/dist/engine/chat.test.js.map +1 -0
  139. package/dist/engine/classifier.d.ts +35 -2
  140. package/dist/engine/classifier.js +170 -31
  141. package/dist/engine/classifier.js.map +1 -1
  142. package/dist/engine/classifier.test.d.ts +1 -0
  143. package/dist/engine/classifier.test.js +115 -0
  144. package/dist/engine/classifier.test.js.map +1 -0
  145. package/dist/engine/dispatcher.d.ts +60 -0
  146. package/dist/engine/dispatcher.js +601 -0
  147. package/dist/engine/dispatcher.js.map +1 -0
  148. package/dist/engine/dispatcher.test.d.ts +1 -0
  149. package/dist/engine/dispatcher.test.js +511 -0
  150. package/dist/engine/dispatcher.test.js.map +1 -0
  151. package/dist/engine/event-shim.d.ts +117 -0
  152. package/dist/engine/event-shim.js +435 -0
  153. package/dist/engine/event-shim.js.map +1 -0
  154. package/dist/engine/event-shim.test.d.ts +1 -0
  155. package/dist/engine/event-shim.test.js +194 -0
  156. package/dist/engine/event-shim.test.js.map +1 -0
  157. package/dist/engine/git-auth.d.ts +13 -17
  158. package/dist/engine/git-auth.js +99 -42
  159. package/dist/engine/git-auth.js.map +1 -1
  160. package/dist/engine/git-auth.test.d.ts +1 -0
  161. package/dist/engine/git-auth.test.js +117 -0
  162. package/dist/engine/git-auth.test.js.map +1 -0
  163. package/dist/engine/github-app-client.d.ts +7 -0
  164. package/dist/engine/github-app-client.js +16 -0
  165. package/dist/engine/github-app-client.js.map +1 -0
  166. package/dist/engine/github-app-client.test.d.ts +1 -0
  167. package/dist/engine/github-app-client.test.js +44 -0
  168. package/dist/engine/github-app-client.test.js.map +1 -0
  169. package/dist/engine/github-tools.d.ts +12 -0
  170. package/dist/engine/github-tools.js +261 -0
  171. package/dist/engine/github-tools.js.map +1 -0
  172. package/dist/engine/github.d.ts +1027 -43
  173. package/dist/engine/github.js +153 -14
  174. package/dist/engine/github.js.map +1 -1
  175. package/dist/engine/llm.d.ts +47 -0
  176. package/dist/engine/llm.js +221 -0
  177. package/dist/engine/llm.js.map +1 -0
  178. package/dist/engine/llm.test.d.ts +1 -0
  179. package/dist/engine/llm.test.js +189 -0
  180. package/dist/engine/llm.test.js.map +1 -0
  181. package/dist/engine/profiles.d.ts +249 -0
  182. package/dist/engine/profiles.js +42 -0
  183. package/dist/engine/profiles.js.map +1 -0
  184. package/dist/engine/router.d.ts +12 -6
  185. package/dist/engine/router.js +339 -81
  186. package/dist/engine/router.js.map +1 -1
  187. package/dist/engine/router.test.js +428 -78
  188. package/dist/engine/router.test.js.map +1 -1
  189. package/dist/engine/screen.d.ts +41 -0
  190. package/dist/engine/screen.js +93 -0
  191. package/dist/engine/screen.js.map +1 -0
  192. package/dist/engine/screen.test.d.ts +1 -0
  193. package/dist/engine/screen.test.js +82 -0
  194. package/dist/engine/screen.test.js.map +1 -0
  195. package/dist/index.js +423 -437
  196. package/dist/index.js.map +1 -1
  197. package/dist/managed-repos.d.ts +7 -9
  198. package/dist/managed-repos.js +12 -15
  199. package/dist/managed-repos.js.map +1 -1
  200. package/dist/managed-repos.test.js +24 -19
  201. package/dist/managed-repos.test.js.map +1 -1
  202. package/dist/notify/index.d.ts +15 -0
  203. package/dist/notify/index.js +6 -0
  204. package/dist/notify/index.js.map +1 -0
  205. package/dist/notify/model.d.ts +57 -0
  206. package/dist/notify/model.js +89 -0
  207. package/dist/notify/model.js.map +1 -0
  208. package/dist/notify/model.test.d.ts +1 -0
  209. package/dist/notify/model.test.js +109 -0
  210. package/dist/notify/model.test.js.map +1 -0
  211. package/dist/notify/notifier.d.ts +17 -0
  212. package/dist/notify/notifier.js +87 -0
  213. package/dist/notify/notifier.js.map +1 -0
  214. package/dist/notify/notifier.test.d.ts +1 -0
  215. package/dist/notify/notifier.test.js +100 -0
  216. package/dist/notify/notifier.test.js.map +1 -0
  217. package/dist/notify/render.d.ts +21 -0
  218. package/dist/notify/render.js +59 -0
  219. package/dist/notify/render.js.map +1 -0
  220. package/dist/notify/render.test.d.ts +1 -0
  221. package/dist/notify/render.test.js +65 -0
  222. package/dist/notify/render.test.js.map +1 -0
  223. package/dist/notify/transports/github.d.ts +26 -0
  224. package/dist/notify/transports/github.js +26 -0
  225. package/dist/notify/transports/github.js.map +1 -0
  226. package/dist/notify/transports/slack.d.ts +26 -0
  227. package/dist/notify/transports/slack.js +28 -0
  228. package/dist/notify/transports/slack.js.map +1 -0
  229. package/dist/notify/transports.test.d.ts +1 -0
  230. package/dist/notify/transports.test.js +71 -0
  231. package/dist/notify/transports.test.js.map +1 -0
  232. package/dist/notify/types.d.ts +95 -0
  233. package/dist/notify/types.js +16 -0
  234. package/dist/notify/types.js.map +1 -0
  235. package/dist/sandbox/docker-compose.test.d.ts +1 -0
  236. package/dist/sandbox/docker-compose.test.js +128 -0
  237. package/dist/sandbox/docker-compose.test.js.map +1 -0
  238. package/dist/sandbox/docker.d.ts +97 -9
  239. package/dist/sandbox/docker.js +192 -41
  240. package/dist/sandbox/docker.js.map +1 -1
  241. package/dist/sandbox/docker.test.d.ts +1 -0
  242. package/dist/sandbox/docker.test.js +137 -0
  243. package/dist/sandbox/docker.test.js.map +1 -0
  244. package/dist/sandbox/egress-allowlist.d.ts +69 -0
  245. package/dist/sandbox/egress-allowlist.js +164 -0
  246. package/dist/sandbox/egress-allowlist.js.map +1 -0
  247. package/dist/sandbox/egress-allowlist.test.d.ts +1 -0
  248. package/dist/sandbox/egress-allowlist.test.js +65 -0
  249. package/dist/sandbox/egress-allowlist.test.js.map +1 -0
  250. package/dist/sandbox/egress-firewall-config.d.ts +127 -0
  251. package/dist/sandbox/egress-firewall-config.js +434 -0
  252. package/dist/sandbox/egress-firewall-config.js.map +1 -0
  253. package/dist/sandbox/egress-firewall-config.test.d.ts +1 -0
  254. package/dist/sandbox/egress-firewall-config.test.js +244 -0
  255. package/dist/sandbox/egress-firewall-config.test.js.map +1 -0
  256. package/dist/sandbox/images.d.ts +23 -0
  257. package/dist/sandbox/images.js +55 -0
  258. package/dist/sandbox/images.js.map +1 -0
  259. package/dist/sandbox/index.d.ts +82 -3
  260. package/dist/sandbox/index.js +223 -93
  261. package/dist/sandbox/index.js.map +1 -1
  262. package/dist/sandbox/index.test.d.ts +1 -0
  263. package/dist/sandbox/index.test.js +157 -0
  264. package/dist/sandbox/index.test.js.map +1 -0
  265. package/dist/session-log.d.ts +63 -0
  266. package/dist/session-log.js +290 -0
  267. package/dist/session-log.js.map +1 -0
  268. package/dist/session-log.test.d.ts +1 -0
  269. package/dist/session-log.test.js +85 -0
  270. package/dist/session-log.test.js.map +1 -0
  271. package/dist/setup.d.ts +31 -12
  272. package/dist/setup.js +516 -174
  273. package/dist/setup.js.map +1 -1
  274. package/dist/setup.test.js +138 -6
  275. package/dist/setup.test.js.map +1 -1
  276. package/dist/state/approval-store.d.ts +86 -0
  277. package/dist/state/approval-store.js +140 -0
  278. package/dist/state/approval-store.js.map +1 -0
  279. package/dist/state/build-assets.d.ts +50 -0
  280. package/dist/state/build-assets.js +154 -0
  281. package/dist/state/build-assets.js.map +1 -0
  282. package/dist/state/build-assets.test.d.ts +1 -0
  283. package/dist/state/build-assets.test.js +106 -0
  284. package/dist/state/build-assets.test.js.map +1 -0
  285. package/dist/state/db.d.ts +65 -333
  286. package/dist/state/db.js +112 -835
  287. package/dist/state/db.js.map +1 -1
  288. package/dist/state/db.test.js +127 -91
  289. package/dist/state/db.test.js.map +1 -1
  290. package/dist/state/execution-store.d.ts +257 -0
  291. package/dist/state/execution-store.js +527 -0
  292. package/dist/state/execution-store.js.map +1 -0
  293. package/dist/state/migrate.d.ts +15 -0
  294. package/dist/state/migrate.js +175 -0
  295. package/dist/state/migrate.js.map +1 -0
  296. package/dist/state/workflow-run-store.d.ts +195 -0
  297. package/dist/state/workflow-run-store.js +363 -0
  298. package/dist/state/workflow-run-store.js.map +1 -0
  299. package/dist/state/workflow-run-store.test.d.ts +1 -0
  300. package/dist/state/workflow-run-store.test.js +264 -0
  301. package/dist/state/workflow-run-store.test.js.map +1 -0
  302. package/dist/telemetry/index.d.ts +38 -0
  303. package/dist/telemetry/index.js +253 -0
  304. package/dist/telemetry/index.js.map +1 -0
  305. package/dist/telemetry/index.test.d.ts +1 -0
  306. package/dist/telemetry/index.test.js +73 -0
  307. package/dist/telemetry/index.test.js.map +1 -0
  308. package/dist/telemetry/pi-events.d.ts +12 -0
  309. package/dist/telemetry/pi-events.js +134 -0
  310. package/dist/telemetry/pi-events.js.map +1 -0
  311. package/dist/telemetry/pi-events.test.d.ts +1 -0
  312. package/dist/telemetry/pi-events.test.js +69 -0
  313. package/dist/telemetry/pi-events.test.js.map +1 -0
  314. package/dist/workflows/dag.d.ts +13 -1
  315. package/dist/workflows/dag.js +7 -3
  316. package/dist/workflows/dag.js.map +1 -1
  317. package/dist/workflows/dag.test.js +22 -0
  318. package/dist/workflows/dag.test.js.map +1 -1
  319. package/dist/workflows/golden-build.test.d.ts +1 -0
  320. package/dist/workflows/golden-build.test.js +45 -0
  321. package/dist/workflows/golden-build.test.js.map +1 -0
  322. package/dist/workflows/loader-overlay.test.d.ts +1 -0
  323. package/dist/workflows/loader-overlay.test.js +101 -0
  324. package/dist/workflows/loader-overlay.test.js.map +1 -0
  325. package/dist/workflows/loader.d.ts +36 -11
  326. package/dist/workflows/loader.js +311 -87
  327. package/dist/workflows/loader.js.map +1 -1
  328. package/dist/workflows/loader.test.js +114 -10
  329. package/dist/workflows/loader.test.js.map +1 -1
  330. package/dist/workflows/loop-eval.js +2 -0
  331. package/dist/workflows/loop-eval.js.map +1 -1
  332. package/dist/workflows/loop-eval.test.js +14 -0
  333. package/dist/workflows/loop-eval.test.js.map +1 -1
  334. package/dist/workflows/phase-executor.d.ts +145 -0
  335. package/dist/workflows/phase-executor.js +691 -0
  336. package/dist/workflows/phase-executor.js.map +1 -0
  337. package/dist/workflows/phase-executor.test.d.ts +1 -0
  338. package/dist/workflows/phase-executor.test.js +434 -0
  339. package/dist/workflows/phase-executor.test.js.map +1 -0
  340. package/dist/workflows/phase-ref.d.ts +44 -0
  341. package/dist/workflows/phase-ref.js +78 -0
  342. package/dist/workflows/phase-ref.js.map +1 -0
  343. package/dist/workflows/phase-ref.test.d.ts +1 -0
  344. package/dist/workflows/phase-ref.test.js +24 -0
  345. package/dist/workflows/phase-ref.test.js.map +1 -0
  346. package/dist/workflows/resume.d.ts +5 -11
  347. package/dist/workflows/resume.js +88 -7
  348. package/dist/workflows/resume.js.map +1 -1
  349. package/dist/workflows/runner.d.ts +34 -27
  350. package/dist/workflows/runner.js +252 -930
  351. package/dist/workflows/runner.js.map +1 -1
  352. package/dist/workflows/runner.test.js +346 -35
  353. package/dist/workflows/runner.test.js.map +1 -1
  354. package/dist/workflows/schema.d.ts +52 -10
  355. package/dist/workflows/schema.js +147 -7
  356. package/dist/workflows/schema.js.map +1 -1
  357. package/dist/workflows/simple.d.ts +67 -3
  358. package/dist/workflows/simple.js +248 -87
  359. package/dist/workflows/simple.js.map +1 -1
  360. package/dist/workflows/simple.test.d.ts +1 -0
  361. package/dist/workflows/simple.test.js +107 -0
  362. package/dist/workflows/simple.test.js.map +1 -0
  363. package/dist/workflows/templates.d.ts +29 -0
  364. package/dist/workflows/templates.js +42 -4
  365. package/dist/workflows/templates.js.map +1 -1
  366. package/dist/workflows/templates.test.js +103 -0
  367. package/dist/workflows/templates.test.js.map +1 -1
  368. package/dist/workflows/triggers.d.ts +21 -0
  369. package/dist/workflows/triggers.js +44 -0
  370. package/dist/workflows/triggers.js.map +1 -0
  371. package/dist/workflows/verdict.d.ts +19 -0
  372. package/dist/workflows/verdict.js +30 -0
  373. package/dist/workflows/verdict.js.map +1 -0
  374. package/dist/workflows/verdict.test.d.ts +1 -0
  375. package/dist/workflows/verdict.test.js +44 -0
  376. package/dist/workflows/verdict.test.js.map +1 -0
  377. package/dist/worktree/manager.d.ts +1 -1
  378. package/dist/worktree/manager.js +14 -14
  379. package/dist/worktree/manager.js.map +1 -1
  380. package/dist/worktree/manager.test.d.ts +1 -0
  381. package/dist/worktree/manager.test.js +87 -0
  382. package/dist/worktree/manager.test.js.map +1 -0
  383. package/docker-compose.yml +206 -5
  384. package/package.json +19 -7
  385. package/sandbox.Dockerfile +97 -18
  386. package/skills/README.md +40 -0
  387. package/skills/browser-qa/SKILL.md +193 -0
  388. package/skills/browser-qa/scripts/agent-browser.mjs +380 -0
  389. package/skills/building/SKILL.md +95 -0
  390. package/skills/chat/SKILL.md +30 -11
  391. package/skills/code-review/SKILL.md +59 -0
  392. package/skills/debug-production/SKILL.md +108 -0
  393. package/skills/demo/SKILL.md +194 -0
  394. package/skills/demo/scripts/compose-demo.sh +205 -0
  395. package/skills/issue-answer/SKILL.md +90 -0
  396. package/skills/issue-comment/SKILL.md +38 -24
  397. package/skills/issue-triage/SKILL.md +103 -37
  398. package/skills/issue-triage/references/AGENT-BRIEF.md +61 -0
  399. package/skills/pr-comment/SKILL.md +64 -0
  400. package/skills/pr-review/SKILL.md +65 -49
  401. package/skills/qa-test/SKILL.md +97 -0
  402. package/skills/repo-health/SKILL.md +48 -45
  403. package/skills/security-feedback/SKILL.md +173 -0
  404. package/skills/security-feedback/references/templates.md +77 -0
  405. package/skills/security-review/SKILL.md +213 -0
  406. package/skills/security-review/references/issue-format.md +302 -0
  407. package/skills/verify/SKILL.md +118 -0
  408. package/workflows/answer.yaml +37 -0
  409. package/workflows/build.yaml +32 -22
  410. package/workflows/cron-security.yaml +6 -0
  411. package/workflows/demo.yaml +55 -0
  412. package/workflows/explore.yaml +9 -0
  413. package/workflows/pr-comment.yaml +16 -0
  414. package/workflows/pr-fix.yaml +3 -0
  415. package/workflows/pr-review.yaml +4 -1
  416. package/workflows/prompts/answer.md +67 -0
  417. package/workflows/prompts/architect.md +24 -7
  418. package/workflows/prompts/demo.md +100 -0
  419. package/workflows/prompts/executor.md +29 -20
  420. package/workflows/prompts/explore-ask.md +51 -22
  421. package/workflows/prompts/explore-publish.md +2 -2
  422. package/workflows/prompts/explore-read.md +24 -10
  423. package/workflows/prompts/explore-synthesize.md +15 -8
  424. package/workflows/prompts/fix.md +9 -10
  425. package/workflows/prompts/guardrails.md +26 -8
  426. package/workflows/prompts/pr-fix.md +6 -7
  427. package/workflows/prompts/pr.md +11 -10
  428. package/workflows/prompts/qa-browser.md +95 -0
  429. package/workflows/prompts/qa-synth.md +44 -0
  430. package/workflows/prompts/qa-test.md +60 -0
  431. package/workflows/prompts/re-reviewer.md +8 -9
  432. package/workflows/prompts/reviewer.md +10 -9
  433. package/workflows/prompts/verify-browser.md +93 -0
  434. package/workflows/prompts/verify-synth.md +44 -0
  435. package/workflows/prompts/verify.md +63 -0
  436. package/workflows/qa-test.yaml +87 -0
  437. package/workflows/security-feedback.yaml +25 -0
  438. package/workflows/security-review.yaml +29 -0
  439. package/workflows/verify.yaml +85 -0
  440. package/deploy/mcp-config.tmpl.json +0 -14
  441. package/dist/cron/rate-limits.d.ts +0 -26
  442. package/dist/cron/rate-limits.js +0 -193
  443. package/dist/cron/rate-limits.js.map +0 -1
  444. package/dist/engine/executor.d.ts +0 -75
  445. package/dist/engine/executor.js +0 -292
  446. package/dist/engine/executor.js.map +0 -1
  447. package/mcp-github-app/package.json +0 -18
  448. package/mcp-github-app/src/auth.js +0 -66
  449. package/mcp-github-app/src/github.js +0 -371
  450. package/mcp-github-app/src/index.js +0 -534
  451. package/skills/devops/webhook-subscriptions/SKILL.md +0 -180
  452. package/skills/github/DESCRIPTION.md +0 -3
  453. package/skills/github/codebase-inspection/SKILL.md +0 -115
  454. package/skills/github/github-auth/SKILL.md +0 -54
  455. package/skills/github/github-auth/scripts/gh-env.sh +0 -66
  456. package/skills/github/github-code-review/SKILL.md +0 -480
  457. package/skills/github/github-code-review/references/review-output-template.md +0 -74
  458. package/skills/github/github-issues/SKILL.md +0 -369
  459. package/skills/github/github-issues/templates/bug-report.md +0 -35
  460. package/skills/github/github-issues/templates/feature-request.md +0 -31
  461. package/skills/github/github-pr-workflow/SKILL.md +0 -395
  462. package/skills/github/github-pr-workflow/references/ci-troubleshooting.md +0 -183
  463. package/skills/github/github-pr-workflow/references/conventional-commits.md +0 -71
  464. package/skills/github/github-pr-workflow/templates/pr-body-bugfix.md +0 -35
  465. package/skills/github/github-pr-workflow/templates/pr-body-feature.md +0 -33
  466. package/skills/github/github-repo-management/SKILL.md +0 -515
  467. package/skills/github/github-repo-management/references/github-api-cheatsheet.md +0 -161
  468. package/skills/github-orchestrator/SKILL.md +0 -103
  469. package/skills/mcp/DESCRIPTION.md +0 -3
  470. package/skills/mcp/mcporter/SKILL.md +0 -122
  471. package/skills/mcp/native-mcp/SKILL.md +0 -356
  472. package/skills/software-development/architect/SKILL.md +0 -154
  473. package/skills/software-development/assure-guardrails/SKILL.md +0 -239
  474. package/skills/software-development/plan/SKILL.md +0 -64
  475. package/skills/software-development/requesting-code-review/SKILL.md +0 -291
  476. package/skills/software-development/subagent-driven-development/SKILL.md +0 -433
  477. package/skills/software-development/systematic-debugging/SKILL.md +0 -366
  478. package/skills/software-development/test-driven-development/SKILL.md +0 -342
package/dist/index.js CHANGED
@@ -1,25 +1,90 @@
1
- import { mkdirSync, writeFileSync } from "fs";
1
+ import { existsSync, mkdirSync, readFileSync } from "fs";
2
2
  import { resolve } from "path";
3
3
  import { randomUUID } from "crypto";
4
- import { loadConfig, generateMcpConfig, resolveModel } from "./config.js";
4
+ import { loadConfig, resolveModel, resolveVariant } from "./config.js";
5
5
  import { ConnectorRegistry, GitHubWebhookConnector, SlackConnector, SessionManager, MessageDeliveryService } from "./connectors/index.js";
6
- import { routeEvent } from "./engine/router.js";
7
- import { handleChatMessage } from "./engine/chat.js";
6
+ import { dispatch } from "./engine/dispatcher.js";
7
+ import { CHAT_SYSTEM_SUFFIX, handleChatMessage, loadAgentContext } from "./engine/chat.js";
8
+ import { configureWorkflowAssets, validateAssets, getWorkflow } from "./workflows/loader.js";
9
+ import { ChatRunner } from "./engine/chat-runner.js";
10
+ import { buildReadSkillTool, loadChatSkillCatalogue } from "./engine/chat-skills.js";
8
11
  import { configureGitAuth } from "./engine/git-auth.js";
9
12
  import { StateDb } from "./state/db.js";
10
13
  import { CronScheduler } from "./cron/scheduler.js";
11
14
  import { getJobs } from "./cron/jobs.js";
15
+ import { dispatchCronWorkflow } from "./cron/fanout.js";
12
16
  import { mountAdmin } from "./admin/index.js";
13
17
  import { cleanupOrphanedSandboxes } from "./sandbox/index.js";
18
+ import { writeEgressFirewallConfigs, writeOtelCollectorConfig } from "./sandbox/egress-firewall-config.js";
19
+ import { initTelemetry, shutdownTelemetry } from "./telemetry/index.js";
14
20
  import { authMiddleware } from "./admin/auth.js";
15
21
  import { GitHubClient } from "./engine/github.js";
22
+ import { screenForInjection, flagPrefix } from "./engine/screen.js";
16
23
  import { runSimpleWorkflow } from "./workflows/simple.js";
17
24
  import { resumeOrphanedWorkflows } from "./workflows/resume.js";
25
+ import { ProgressNotifier, GitHubTransport, SlackTransport, } from "./notify/index.js";
26
+ /**
27
+ * Pre-flight validation — checks that config is sane before starting any
28
+ * services. Exits with code 78 (EX_CONFIG) on configuration errors so
29
+ * Docker's restart policy doesn't loop forever on a misconfigured container.
30
+ */
31
+ function validateConfig(config) {
32
+ const fatal = (msg) => {
33
+ console.error(`\n[startup] FATAL: ${msg}`);
34
+ console.error("[startup] Fix your .env and restart.\n");
35
+ process.exit(78); // EX_CONFIG — sysexits.h convention
36
+ };
37
+ if (config.githubApp) {
38
+ const { appId, privateKeyPath, installationId } = config.githubApp;
39
+ if (!appId || !installationId) {
40
+ fatal("GITHUB_APP_ID and GITHUB_APP_INSTALLATION_ID are required when the GitHub App is configured.");
41
+ }
42
+ if (!existsSync(resolve(privateKeyPath))) {
43
+ fatal(`GITHUB_APP_PRIVATE_KEY_PATH points to "${privateKeyPath}" which does not exist.`);
44
+ }
45
+ try {
46
+ const content = readFileSync(resolve(privateKeyPath), "utf8");
47
+ if (!content.startsWith("-----BEGIN")) {
48
+ fatal(`GITHUB_APP_PRIVATE_KEY_PATH ("${privateKeyPath}") does not look like a PEM file.`);
49
+ }
50
+ }
51
+ catch (err) {
52
+ fatal(`Cannot read GITHUB_APP_PRIVATE_KEY_PATH ("${privateKeyPath}"): ${err.message}`);
53
+ }
54
+ }
55
+ if (!config.webhookSecret && config.githubApp) {
56
+ console.warn("[startup] WEBHOOK_SECRET is not set — webhook signature verification is disabled.");
57
+ }
58
+ }
18
59
  async function main() {
19
60
  console.log("Last Light v2.0 — Agent SDK Harness");
20
61
  console.log("====================================");
21
- // Load config
22
- const config = loadConfig();
62
+ // Load and validate config + overlay assets before starting anything. These
63
+ // throw on a broken/empty overlay, a cron targeting a missing workflow, or a
64
+ // phase whose prompt/skill can't resolve — all unfixable by a restart, so we
65
+ // exit 78 (EX_CONFIG) to stop Docker's restart policy from looping.
66
+ let config;
67
+ try {
68
+ config = loadConfig();
69
+ configureWorkflowAssets({
70
+ builtInRoot: config.builtInRoot,
71
+ overlayRoot: config.overlayDir,
72
+ disabled: config.disabled,
73
+ });
74
+ validateAssets(config.routes);
75
+ }
76
+ catch (err) {
77
+ console.error(`\n[startup] FATAL: ${err.message}`);
78
+ console.error("[startup] Fix your config/overlay and restart.\n");
79
+ process.exit(78); // EX_CONFIG — sysexits.h convention
80
+ }
81
+ validateConfig(config);
82
+ const packageJson = JSON.parse(readFileSync(resolve("package.json"), "utf8"));
83
+ await initTelemetry(config.otel, { packageVersion: packageJson.version });
84
+ let telemetryShutdownStarted = false;
85
+ console.log(config.otel.enabled
86
+ ? `[otel] enabled service=${config.otel.serviceName} forwardToSandbox=${config.otel.forwardToSandbox} includeContent=${config.otel.includeContent}`
87
+ : "[otel] disabled");
23
88
  console.log(`[config] Port: ${config.port}, Model: ${config.model}`);
24
89
  const modelOverrides = Object.entries(config.models).filter(([k]) => k !== "default");
25
90
  if (modelOverrides.length > 0) {
@@ -32,14 +97,59 @@ async function main() {
32
97
  mkdirSync(resolve(config.stateDir, sub), { recursive: true });
33
98
  }
34
99
  console.log(`[state] State dir: ${config.stateDir}`);
35
- // Generate MCP config file for the Agent SDK
36
- const mcpConfig = generateMcpConfig(config);
37
- const mcpConfigPath = resolve(config.mcpConfigPath);
38
- writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
39
- console.log(`[config] MCP config written to: ${mcpConfigPath}`);
40
- // Configure git with GitHub App credentials agents can git clone/push natively
41
- // Non-fatal: if this fails (e.g., DNS not ready yet), the app still starts.
42
- // Git auth is refreshed before each agent execution anyway.
100
+ console.log(`[state] Sessions dir: ${config.sessionsDir}`);
101
+ console.log(`[config] Sandbox backend: ${config.sandbox}`);
102
+ // Regenerate egress firewall configs (nginx ssl_preread + coredns) from
103
+ // the allowlist source of truth. Only meaningful for the docker backend;
104
+ // cheap enough to do unconditionally so a backend switch doesn't leave
105
+ // stale configs on disk. The docker backend forwards sandbox telemetry
106
+ // through the in-network OTEL collector (reached by IP), so the strict
107
+ // SNI allowlist no longer needs collector hosts that hop happens on
108
+ // the collector's trusted outbound leg, not through the firewall.
109
+ const proxyDir = writeEgressFirewallConfigs(config.stateDir);
110
+ console.log(`[state] Egress firewall configs: ${proxyDir}`);
111
+ // Generate the in-network OTEL collector config (docker backend). Derived
112
+ // from the harness's OTEL_* backend env so the collector re-exports to the
113
+ // same backend the harness uses — with auth headers that stay host-side.
114
+ // Forwarding is gated on telemetry being active: when disabled (or sandbox
115
+ // forwarding off) the collector renders an inert debug-only config so the
116
+ // static collector IP can't be used as a sandbox exfil path.
117
+ const collectorConfigPath = writeOtelCollectorConfig(config.stateDir, {
118
+ active: config.otel.enabled && config.otel.forwardToSandbox,
119
+ });
120
+ console.log(`[state] OTEL collector config: ${collectorConfigPath} (forwarding ${config.otel.enabled && config.otel.forwardToSandbox ? "active" : "disabled"})`);
121
+ // Initialize state database first — ChatRunner needs SessionManager
122
+ // (DB-backed) at construction time.
123
+ const db = new StateDb(config.dbPath);
124
+ console.log(`[state] Database: ${config.dbPath}`);
125
+ // Session manager for messaging connectors (shared across Slack, Discord, etc.)
126
+ const sessionManager = new SessionManager(db.database);
127
+ // In-process chat runner backs the messaging chat skill. One pi-ai
128
+ // conversation per Slack/Discord thread; locked down to read-only
129
+ // GitHub tools plus a `read_skill` tool that exposes the curated
130
+ // chat skill catalogue. Skills are listed in the system prompt via
131
+ // an XML <available_skills> block; the agent pulls full SKILL.md on
132
+ // demand — same progressive-disclosure model the sandbox phases use.
133
+ const chatSkills = loadChatSkillCatalogue();
134
+ const readSkill = buildReadSkillTool(chatSkills.skills);
135
+ const chatRunner = new ChatRunner({
136
+ model: resolveModel(config.models, "chat"),
137
+ thinking: resolveVariant(config.variants, "chat"),
138
+ systemPrompt: loadAgentContext() + CHAT_SYSTEM_SUFFIX + chatSkills.catalogueXml,
139
+ github: config.githubApp,
140
+ extraTools: chatSkills.skills.length > 0
141
+ ? { tools: [readSkill.tool], execute: readSkill.execute }
142
+ : undefined,
143
+ }, sessionManager);
144
+ if (chatSkills.skills.length > 0) {
145
+ console.log(`[chat] Loaded ${chatSkills.skills.length} skill(s): ${chatSkills.skills.map((s) => s.name).join(", ")}`);
146
+ }
147
+ else {
148
+ console.warn("[chat] No skills loaded — frontmatter missing or no matching SKILL.md found");
149
+ }
150
+ // Configure git with GitHub App credentials — agents can git clone/push natively.
151
+ // Non-fatal: the token is refreshed before each agent execution anyway, so a
152
+ // transient failure here (DNS, rate limit) doesn't block startup.
43
153
  if (config.githubApp) {
44
154
  try {
45
155
  await configureGitAuth({
@@ -49,12 +159,9 @@ async function main() {
49
159
  });
50
160
  }
51
161
  catch (err) {
52
- console.warn(`[git-auth] Initial git auth failed (will retry per-execution): ${err.message}`);
162
+ console.warn(`[git-auth] Initial token mint failed (will retry per-execution): ${err.message}`);
53
163
  }
54
164
  }
55
- // Initialize state database
56
- const db = new StateDb(config.dbPath);
57
- console.log(`[state] Database: ${config.dbPath}`);
58
165
  // GitHub API client for harness-level operations (posting comments, fetching issues)
59
166
  const github = config.githubApp ? new GitHubClient(config.githubApp) : null;
60
167
  /**
@@ -66,7 +173,7 @@ async function main() {
66
173
  * The router still uses skill names for backwards compat — for the four
67
174
  * agent skills they're 1:1 with workflow names.
68
175
  */
69
- const dispatchWorkflow = async (workflowName, context) => {
176
+ const dispatchWorkflow = async (workflowName, context, onRunStart) => {
70
177
  // Slack-initiated workflows (explore, /explore) carry a
71
178
  // `slack:{team}:{channel}:{thread}` triggerId and don't require a
72
179
  // managed `repo` — their postComment goes back to the Slack thread.
@@ -91,7 +198,7 @@ async function main() {
91
198
  }
92
199
  // Pluck the standard fields, leave the rest in `extra` for the workflow
93
200
  // template to consume.
94
- const { _triggerType, repo: _r, issueNumber, prNumber, title, body, labels, sender, commentBody, triggerId: _triggerId, channelId, threadId, ...rest } = context;
201
+ const { _triggerType, repo: _r, issueNumber, prNumber, title, body, labels, sender, commentBody, triggerId: _triggerId, channelId, threadId, prePopulateBranch: ctxPrePopulateBranch, branch: ctxBranch, ...rest } = context;
95
202
  // Preserve channelId/threadId in extra so they're stored on the
96
203
  // workflow run context — needed by boot-time resume to rebuild the
97
204
  // Slack postComment callback after a harness restart.
@@ -100,6 +207,35 @@ async function main() {
100
207
  extra.channelId = channelId;
101
208
  if (typeof threadId === "string")
102
209
  extra.threadId = threadId;
210
+ // For PR-scoped read workflows, resolve the PR head ref and ask the
211
+ // sandbox to pre-clone the repo at that branch. The agent then enters
212
+ // a workspace that's already a checkout of the PR's actual code —
213
+ // saves a redundant clone_repo MCP call inside the session.
214
+ //
215
+ // pr-fix already plumbs `branch` through context (line ~709 below)
216
+ // because the architect/executor need the branch name to push to;
217
+ // we honor that here as the pre-populate branch too.
218
+ let prePopulateBranch = typeof ctxPrePopulateBranch === "string" ? ctxPrePopulateBranch : undefined;
219
+ if (!prePopulateBranch && typeof ctxBranch === "string" && ctxBranch && workflowName === "pr-fix") {
220
+ prePopulateBranch = ctxBranch;
221
+ }
222
+ if (!prePopulateBranch &&
223
+ workflowName === "pr-review" &&
224
+ typeof prNumber === "number" &&
225
+ github &&
226
+ owner &&
227
+ repo) {
228
+ try {
229
+ const pr = await github.getPullRequest(owner, repo, prNumber);
230
+ prePopulateBranch = pr.head.ref;
231
+ console.log(`[dispatch] pr-review: pre-populating workspace at ${owner}/${repo}@${prePopulateBranch}`);
232
+ }
233
+ catch (err) {
234
+ const msg = err instanceof Error ? err.message : String(err);
235
+ console.warn(`[dispatch] pr-review: could not resolve PR head ref (${msg}); ` +
236
+ `agent will need to clone via MCP`);
237
+ }
238
+ }
103
239
  const request = {
104
240
  owner,
105
241
  repo,
@@ -112,7 +248,64 @@ async function main() {
112
248
  sender: typeof sender === "string" ? sender : "unknown",
113
249
  triggerId: slackTriggerId,
114
250
  extra,
251
+ prePopulateBranch,
115
252
  };
253
+ // For workflows where the architect/agent needs to see the full issue
254
+ // history (e.g. a build greenlit by "@last-light lets build this!" needs
255
+ // the spec the explore phase wrote in earlier comments), fetch the real
256
+ // issue body and the comment thread, combine them into a single context
257
+ // blob, and screen the combined text in ONE SDK call.
258
+ //
259
+ // Why single-shot: screening per-comment fans out N concurrent SDK calls,
260
+ // and on a busy issue (16+ comments) that exhausts memory and trips the
261
+ // EventEmitter listener cap. The combined-context approach keeps screen
262
+ // cost at exactly one haiku call regardless of thread length.
263
+ //
264
+ // For comment-triggered builds the envelope's `body` field is the
265
+ // triggering comment, not the issue body — we explicitly fetch the
266
+ // real issue body here so the architect sees both the spec (issue body
267
+ // + thread) and the trigger (commentBody) cleanly separated.
268
+ const ENRICH_WORKFLOWS = new Set(["build", "pr-fix", "explore"]);
269
+ if (github &&
270
+ ENRICH_WORKFLOWS.has(workflowName) &&
271
+ request.issueNumber &&
272
+ owner && repo) {
273
+ try {
274
+ const [trueIssueBody, comments] = await Promise.all([
275
+ github.getIssueBody(owner, repo, request.issueNumber),
276
+ github.listIssueComments(owner, repo, request.issueNumber),
277
+ ]);
278
+ const formattedComments = comments
279
+ .filter((c) => c.body.trim())
280
+ .map((c) => `--- @${c.user} (${c.createdAt}) ---\n${c.body}`)
281
+ .join("\n\n");
282
+ const combinedContext = [
283
+ trueIssueBody ? `# Issue body\n\n${trueIssueBody}` : "",
284
+ formattedComments ? `# Issue thread (oldest → newest)\n\n${formattedComments}` : "",
285
+ ]
286
+ .filter(Boolean)
287
+ .join("\n\n");
288
+ if (combinedContext) {
289
+ // Single screening call over the entire combined context.
290
+ const screen = await screenForInjection(combinedContext);
291
+ const annotated = screen.flagged
292
+ ? `${flagPrefix(screen.reason)}${combinedContext}`
293
+ : combinedContext;
294
+ (request.extra ||= {}).combinedContext = annotated;
295
+ // Clear individual issueBody so simple.ts uses combinedContext
296
+ // exclusively (avoids double-rendering the body).
297
+ request.issueBody = "";
298
+ if (screen.flagged) {
299
+ console.warn(`[dispatch] Screener flagged combined issue context for ${owner}/${repo}#${request.issueNumber}: ${screen.reason || "no reason"}`);
300
+ }
301
+ }
302
+ }
303
+ catch (err) {
304
+ const m = err instanceof Error ? err.message : String(err);
305
+ console.warn(`[dispatch] Failed to fetch/screen issue context: ${m}`);
306
+ // Non-fatal — workflow proceeds with whatever context the envelope had.
307
+ }
308
+ }
116
309
  const slackPost = slackTriggerId && slackConnector && typeof channelId === "string" && typeof threadId === "string"
117
310
  ? async (msg) => {
118
311
  try {
@@ -124,7 +317,84 @@ async function main() {
124
317
  }
125
318
  }
126
319
  : undefined;
320
+ // In-place "task list" progress checklist — opt-in per workflow via
321
+ // `status_checklist: true` in the YAML. Build a transport for whichever
322
+ // surface triggered the run (GitHub comment and/or Slack thread) and hand
323
+ // the runner a ProgressNotifier instead of letting it post a comment per
324
+ // phase. The notifier is created inside onRunStart because it needs the
325
+ // workflow-run id (only known once simple.ts creates the row) to persist
326
+ // its in-place update handles to scratch.notifier. better-sqlite3 is
327
+ // synchronous and simple.ts invokes onRunStart synchronously before the
328
+ // first reporter call, so the notifier is ready in time; the proxy guards
329
+ // the brief window before assignment.
330
+ // Which in-place surface(s) can the checklist edit? Knowable synchronously
331
+ // (transport existence needs only github/issue or slack/channel/thread —
332
+ // the run id is needed solely for persistence + resume handles).
333
+ const ghChecklist = !!(github && typeof issueNumber === "number");
334
+ const slackChecklist = !!(slackConnector && typeof channelId === "string" && typeof threadId === "string");
335
+ let statusChecklist = false;
336
+ try {
337
+ // Only activate the checklist when the workflow opts in AND there's a
338
+ // surface to render it on — otherwise leave `reporter` undefined so the
339
+ // runner keeps its legacy per-phase comment behavior instead of going
340
+ // silent.
341
+ statusChecklist =
342
+ getWorkflow(workflowName).status_checklist === true && (ghChecklist || slackChecklist);
343
+ }
344
+ catch {
345
+ /* unknown workflow — surfaced downstream by runSimpleWorkflow */
346
+ }
347
+ let notifier;
348
+ const reporterProxy = statusChecklist
349
+ ? {
350
+ start: (m) => notifier?.start(m) ?? Promise.resolve(),
351
+ step: (k, s, d) => notifier?.step(k, s, d) ?? Promise.resolve(),
352
+ insertStep: (st, b) => notifier?.insertStep(st, b) ?? Promise.resolve(),
353
+ note: (m) => notifier?.note(m) ?? Promise.resolve(),
354
+ footer: (m) => notifier?.footer(m) ?? Promise.resolve(),
355
+ noteTerminal: (m) => notifier?.noteTerminal(m) ?? Promise.resolve(),
356
+ }
357
+ : undefined;
358
+ const notifierOnRunStart = statusChecklist
359
+ ? (runId) => {
360
+ try {
361
+ const saved = ((db.runs.getRun(runId)?.scratch?.notifier) ?? {});
362
+ const persist = (patch) => {
363
+ const cur = ((db.runs.getRun(runId)?.scratch?.notifier) ?? {});
364
+ db.runs.mergeScratch(runId, { notifier: { ...cur, ...patch } });
365
+ };
366
+ const transports = [];
367
+ if (ghChecklist && github && typeof issueNumber === "number") {
368
+ transports.push(new GitHubTransport({
369
+ github,
370
+ owner,
371
+ repo,
372
+ issueNumber,
373
+ commentId: saved.githubCommentId,
374
+ save: (id) => persist({ githubCommentId: id }),
375
+ }));
376
+ }
377
+ if (slackChecklist && slackConnector && typeof channelId === "string" && typeof threadId === "string") {
378
+ transports.push(new SlackTransport({
379
+ slack: slackConnector,
380
+ channel: channelId,
381
+ thread: threadId,
382
+ ts: saved.slackTs,
383
+ save: (ts) => persist({ slackTs: ts, slackChannel: channelId, slackThread: threadId }),
384
+ }));
385
+ }
386
+ if (transports.length > 0)
387
+ notifier = new ProgressNotifier(transports);
388
+ }
389
+ catch (err) {
390
+ const m = err instanceof Error ? err.message : String(err);
391
+ console.warn(`[dispatch] notifier setup failed: ${m}`);
392
+ }
393
+ }
394
+ : undefined;
127
395
  const callbacks = {
396
+ reporter: reporterProxy,
397
+ publicUrl: config.publicUrl,
128
398
  postComment: slackPost
129
399
  ?? (github && issueNumber
130
400
  ? async (msg) => {
@@ -147,15 +417,29 @@ async function main() {
147
417
  }
148
418
  },
149
419
  onPhaseEnd: async (phase, result) => console.log(`[dispatch] ◀ ${workflowName}/${phase}: ${result.success ? "OK" : "FAILED"}`),
420
+ onRunStart: notifierOnRunStart
421
+ ? async (runId) => {
422
+ // Synchronous notifier setup must finish before simple.ts calls
423
+ // reporter.start() (the next statement after it invokes this), so
424
+ // run it first, then chain any caller-provided onRunStart.
425
+ notifierOnRunStart(runId);
426
+ if (onRunStart)
427
+ await onRunStart(runId);
428
+ }
429
+ : onRunStart,
150
430
  };
151
431
  try {
152
432
  const result = await runSimpleWorkflow(workflowName, request, {
153
- mcpConfigPath,
154
433
  model: config.model,
155
434
  maxTurns: config.maxTurns,
156
435
  stateDir: config.stateDir,
157
436
  sandboxDir: config.sandboxDir,
158
- }, callbacks, db, config.models, config.approval, config.bootstrapLabel);
437
+ sessionsDir: config.sessionsDir,
438
+ sandbox: config.sandbox,
439
+ buildAssets: config.buildAssets,
440
+ buildAssetsDir: config.buildAssetsDir,
441
+ otel: config.otel,
442
+ }, callbacks, db, config.models, config.approval, config.bootstrapLabel, config.variants);
159
443
  const summary = result.phases.map((p) => `${p.phase}=${p.success ? "ok" : "fail"}`).join(", ");
160
444
  if (result.paused) {
161
445
  console.log(`[dispatch] ${workflowName} paused (${summary})`);
@@ -176,8 +460,6 @@ async function main() {
176
460
  };
177
461
  // Set up connector registry
178
462
  const registry = new ConnectorRegistry();
179
- // Session manager for messaging connectors (shared across Slack, Discord, etc.)
180
- const sessionManager = new SessionManager(db.database);
181
463
  // Message delivery service for cron output
182
464
  const delivery = new MessageDeliveryService();
183
465
  // GitHub webhook connector (optional — requires both webhook secret and GitHub App)
@@ -206,34 +488,59 @@ async function main() {
206
488
  delivery.register("slack", (msg) => slackConnector.sendToDeliveryChannel(msg));
207
489
  }
208
490
  }
491
+ // Construct the cron scheduler before mounting admin so the dashboard can
492
+ // list/toggle/edit registered cron jobs. Jobs are registered further down
493
+ // (after we know whether webhooks are enabled). The runner closes over
494
+ // `dispatchWorkflow`, which is defined earlier in this file.
495
+ const cron = new CronScheduler(db, async (workflowName, context) => {
496
+ const { dispatched, failures } = await dispatchCronWorkflow(workflowName, context, dispatchWorkflow);
497
+ if (failures > 0) {
498
+ console.warn(`[cron] ${workflowName}: ${failures}/${dispatched} dispatches failed`);
499
+ }
500
+ });
209
501
  // Mount admin dashboard (needs an HTTP server — use GitHub connector or create standalone)
210
502
  if (githubConnector) {
211
503
  mountAdmin(githubConnector.honoApp, db, {
504
+ cronScheduler: cron,
212
505
  stateDir: config.stateDir,
213
- sessionsDir: resolve(process.env.CLAUDE_HOME_DIR || "./data/claude-home"),
506
+ sessionsDir: config.sessionsDir,
507
+ buildAssetsDir: config.buildAssetsDir,
508
+ buildAssets: config.buildAssets,
214
509
  adminPassword: process.env.ADMIN_PASSWORD ?? "",
215
510
  adminSecret: process.env.ADMIN_SECRET ?? "lastlight-dev-secret",
511
+ publicConfig: config.publicConfig,
216
512
  slackOAuthClientId: process.env.SLACK_OAUTH_CLIENT_ID,
217
513
  slackOAuthClientSecret: process.env.SLACK_OAUTH_CLIENT_SECRET,
218
514
  slackOAuthRedirectUri: process.env.SLACK_OAUTH_REDIRECT_URI,
219
515
  slackAllowedWorkspace: process.env.SLACK_ALLOWED_WORKSPACE,
220
- adminNotifier: slackConnector
221
- ? (msg) => slackConnector.sendToDeliveryChannel(msg)
222
- : undefined,
516
+ githubOAuthClientId: process.env.GITHUB_OAUTH_CLIENT_ID,
517
+ githubOAuthClientSecret: process.env.GITHUB_OAUTH_CLIENT_SECRET,
518
+ githubOAuthRedirectUri: process.env.GITHUB_OAUTH_REDIRECT_URI,
519
+ githubAllowedOrg: process.env.GITHUB_ALLOWED_ORG,
223
520
  resumeWorkflow: async (workflowRun, sender) => {
224
521
  if (!github) {
225
522
  console.warn(`[admin] Cannot resume workflow ${workflowRun.id}: GitHub App not configured`);
226
523
  return;
227
524
  }
228
- const [owner, repo] = workflowRun.triggerId.includes("/")
525
+ // Derive owner/repo for the resume dispatch. Prefer the triggerId
526
+ // (owner/repo#N) but fall back to the stored repo + context.owner so a
527
+ // run keyed on a non-GitHub triggerId (e.g. a Slack-thread override)
528
+ // still resumes from the dashboard/focused-approval flow.
529
+ let [owner, repo] = workflowRun.triggerId.includes("/")
229
530
  ? workflowRun.triggerId.replace(/#\d+$/, "").split("/")
230
531
  : ["", ""];
532
+ if (!owner || !repo) {
533
+ const ctxOwner = workflowRun.context?.owner || "";
534
+ const storedRepo = workflowRun.repo || "";
535
+ owner = ctxOwner || (storedRepo.includes("/") ? storedRepo.split("/")[0] : "");
536
+ repo = storedRepo.includes("/") ? storedRepo.split("/")[1] : storedRepo;
537
+ }
231
538
  const issueNumber = workflowRun.issueNumber;
232
539
  if (!owner || !repo || !issueNumber) {
233
540
  console.warn(`[admin] Cannot resume workflow ${workflowRun.id}: missing owner/repo/issueNumber`);
234
541
  return;
235
542
  }
236
- db.resumeWorkflowRun(workflowRun.id);
543
+ db.runs.setRunning(workflowRun.id);
237
544
  console.log(`[admin] Resuming ${workflowRun.workflowName} for ${owner}/${repo}#${issueNumber} after dashboard approval by ${sender}`);
238
545
  dispatchWorkflow(workflowRun.workflowName, {
239
546
  repo: `${owner}/${repo}`,
@@ -312,429 +619,91 @@ async function main() {
312
619
  return c.json({ accepted: true, owner, repo, issueNumber }, 202);
313
620
  });
314
621
  // Handle events from any connector
622
+ // Handle events from any connector. The dispatcher turns each EventEnvelope
623
+ // into a workflow dispatch (or in-process handler run) through one testable
624
+ // seam; every per-event branch lives in src/engine/dispatcher.ts. main()
625
+ // only constructs the deps and relays the typed outcome.
626
+ const dispatchDeps = {
627
+ db,
628
+ github,
629
+ dispatchWorkflow,
630
+ sessionManager,
631
+ // One in-process chat turn. handleChatMessage manages session resume via
632
+ // sessionManager internally; the dispatcher uses resumeAgentSessionId only
633
+ // to decide whether to persist a newly-minted agent session id.
634
+ runChat: (message, messagingSessionId, sender) => handleChatMessage(message, messagingSessionId, sender, sessionManager, { chatRunner, sessionsHomeDir: config.sessionsDir }, { model: resolveModel(config.models, "chat"), maxTurns: 10 }),
635
+ reviewPostsCheck: config.reviewPostsCheck,
636
+ publicUrl: config.publicUrl,
637
+ };
638
+ // One chat turn over HTTP — `lastlight chat` without a messaging platform.
639
+ // Routes through the SAME dispatcher seam Slack uses (forcing the chat
640
+ // handler), so the executions row, agent-session resume, and telemetry are
641
+ // recorded identically and the turn shows up in the dashboard Chat tab. The
642
+ // synthetic envelope's reply() just captures the assistant text to return.
643
+ githubConnector?.honoApp.post("/api/chat", async (c) => {
644
+ const body = await c.req.json().catch(() => ({}));
645
+ const message = typeof body.message === "string" ? body.message : "";
646
+ if (!message.trim())
647
+ return c.json({ error: "Missing 'message'" }, 400);
648
+ const user = typeof body.user === "string" && body.user ? body.user : "cli";
649
+ const threadId = typeof body.thread === "string" && body.thread ? body.thread : null;
650
+ const session = sessionManager.getOrCreateSession({
651
+ platform: "cli", channelId: user, threadId, userId: user,
652
+ });
653
+ let reply = "";
654
+ const envelope = {
655
+ id: session.id,
656
+ source: "cli",
657
+ type: "message",
658
+ sender: user,
659
+ senderIsBot: false,
660
+ body: message,
661
+ raw: { cli: true },
662
+ reply: async (msg) => { reply = msg; },
663
+ timestamp: new Date(),
664
+ };
665
+ const outcome = await dispatch(envelope, {
666
+ ...dispatchDeps,
667
+ route: async () => ({
668
+ action: "handler",
669
+ handler: "chat",
670
+ context: { sessionId: session.id, message, sender: user },
671
+ }),
672
+ });
673
+ return c.json({ text: reply, thread: threadId ?? session.id, sessionId: session.id, outcome: outcome.kind });
674
+ });
315
675
  registry.onEvent(async (envelope) => {
316
676
  console.log(`[event] ${envelope.source}:${envelope.type} from ${envelope.sender}${envelope.repo ? ` on ${envelope.repo}` : ""}`);
317
- const route = await routeEvent(envelope, { db });
318
- if (route.action === "ignore") {
319
- console.log(`[event] Ignored: ${route.reason}`);
320
- return;
321
- }
322
- if (route.action === "reply") {
323
- await envelope.reply(route.message);
324
- return;
325
- }
326
- const { skill, context } = route;
327
- // Chat messages: handle directly (no sandbox, low latency)
328
- if (skill === "chat") {
329
- const messagingSessionId = context.sessionId;
330
- const message = context.message;
331
- const sender = context.sender;
332
- // Look up the existing Agent SDK session id for this Slack thread.
333
- // First message has none → fresh session; subsequent messages resume.
334
- const messagingSession = sessionManager.getSession(messagingSessionId);
335
- const resumeAgentSessionId = messagingSession?.agentSessionId ?? undefined;
336
- // Record an executions row so chat usage shows up in dashboard stats
337
- // alongside sandbox runs. triggerId is the messaging-session id, so a
338
- // whole Slack thread groups together with `GROUP BY trigger_id`.
339
- const executionId = randomUUID();
340
- db.recordStart({
341
- id: executionId,
342
- triggerType: "chat",
343
- triggerId: messagingSessionId,
344
- skill: "chat",
345
- startedAt: new Date().toISOString(),
346
- });
347
- try {
348
- const result = await handleChatMessage(message, messagingSessionId, sender, sessionManager, {
349
- mcpConfigPath,
350
- model: resolveModel(config.models, "chat"),
351
- maxTurns: 10,
352
- }, resumeAgentSessionId);
353
- // Persist the Agent SDK session id on the first turn so the next
354
- // turn in this thread can resume into the same jsonl. We always
355
- // overwrite — if the SDK rotated the id (e.g. resume failed and it
356
- // started a fresh session) we want the latest one.
357
- if (result.agentSessionId && result.agentSessionId !== resumeAgentSessionId) {
358
- sessionManager.setAgentSessionId(messagingSessionId, result.agentSessionId);
359
- }
360
- db.recordFinish(executionId, {
361
- success: result.success,
362
- error: result.error,
363
- turns: result.turns,
364
- durationMs: result.durationMs,
365
- sessionId: result.agentSessionId,
366
- costUsd: result.costUsd,
367
- inputTokens: result.inputTokens,
368
- cacheCreationInputTokens: result.cacheCreationInputTokens,
369
- cacheReadInputTokens: result.cacheReadInputTokens,
370
- outputTokens: result.outputTokens,
371
- apiDurationMs: result.apiDurationMs,
372
- stopReason: result.stopReason,
373
- });
374
- await envelope.reply(result.text);
375
- }
376
- catch (err) {
377
- const msg = err instanceof Error ? err.message : String(err);
378
- console.error(`[event] Chat error:`, msg);
379
- db.recordFinish(executionId, {
380
- success: false,
381
- error: msg,
382
- durationMs: 0,
383
- });
384
- await envelope.reply("Sorry, I encountered an error. Please try again.");
385
- }
386
- return;
387
- }
388
- // Chat reset: deactivate the session and confirm
389
- if (skill === "chat-reset") {
390
- const sessionId = context.sessionId;
391
- if (sessionId) {
392
- sessionManager.deactivateSession(sessionId);
393
- }
394
- await envelope.reply("Session reset. Starting fresh.");
395
- return;
396
- }
397
- // Status report: return running executions
398
- if (skill === "status-report") {
399
- const running = db.runningExecutions();
400
- if (running.length === 0) {
401
- await envelope.reply("No tasks currently running.");
402
- }
403
- else {
404
- const lines = running.map((r) => `• *${r.skill}*${r.repo ? ` on ${r.repo}` : ""}${r.issueNumber ? ` #${r.issueNumber}` : ""} (started ${r.startedAt})`);
405
- await envelope.reply(`Running tasks:\n${lines.join("\n")}`);
406
- }
407
- return;
408
- }
409
- // Check if already running for this trigger
410
- const triggerId = String(envelope.issueNumber || envelope.id);
411
- if (db.isRunning(skill, triggerId)) {
412
- console.log(`[event] Skipping: ${skill} already running for ${triggerId}`);
413
- // Notify messaging users that the task is already in progress
414
- if (envelope.type === "message") {
415
- await envelope.reply(`That task is already running. Use /status to check progress.`);
416
- }
417
- return;
418
- }
419
- // PR fix: lightweight fix-and-push, no full build cycle
420
- if (skill === "pr-fix" && context.prNumber && context.repo) {
421
- const repoStr = context.repo;
422
- const [owner, repo] = repoStr.includes("/") ? repoStr.split("/") : ["", repoStr];
423
- const prNumber = context.prNumber;
424
- if (!owner || !repo) {
425
- console.error(`[event] Invalid repo format: ${repoStr}`);
426
- return;
427
- }
428
- // Fetch PR details and CI failures
429
- let prTitle = context.title || "";
430
- let prBody = context.body || "";
431
- let branch = "";
432
- let failedChecks = "";
433
- if (github) {
434
- try {
435
- const pr = await github.getPullRequest(owner, repo, prNumber);
436
- prTitle = prTitle || pr.title;
437
- prBody = prBody || pr.body || "";
438
- branch = pr.head.ref;
439
- // Fetch CI failures for the PR's head commit
440
- failedChecks = await github.getFailedChecks(owner, repo, pr.head.sha);
441
- }
442
- catch (err) {
443
- console.warn(`[event] Could not fetch PR: ${err.message}`);
444
- }
445
- }
446
- if (!branch) {
447
- console.error(`[event] Could not determine branch for PR #${prNumber}`);
448
- return;
449
- }
450
- console.log(`[event] PR fix for ${repoStr}#${prNumber} on branch ${branch}`);
451
- const ciSection = failedChecks && !failedChecks.includes("No failed checks")
452
- ? `CI FAILURES (from GitHub Actions — fix these first):\n${failedChecks}`
453
- : "";
454
- dispatchWorkflow("pr-fix", {
455
- repo: repoStr,
456
- prNumber,
457
- title: prTitle,
458
- body: prBody,
459
- commentBody: context.commentBody || "",
460
- sender: context.sender || "unknown",
461
- branch,
462
- failedChecks,
463
- ciSection,
464
- _triggerType: "webhook",
465
- }).catch((err) => {
466
- console.error(`[event] PR fix failed:`, err);
467
- });
468
- return;
469
- }
470
- // Explore reply: free-form user reply on a paused socratic explore
471
- // run. Resolve the reply gate with the message body, merge it into
472
- // scratch.socratic.qa so the next iteration sees the answer, and
473
- // re-dispatch the workflow to continue the loop.
474
- if (skill === "explore-reply") {
475
- const workflowRunId = context.workflowRunId;
476
- const replyText = context.reply || "";
477
- const sender = context.sender || "unknown";
478
- const run = db.getWorkflowRun(workflowRunId);
479
- if (!run) {
480
- console.warn(`[event] explore-reply: run ${workflowRunId} not found`);
481
- return;
482
- }
483
- const pending = db.getPendingApprovalForWorkflow(workflowRunId);
484
- if (!pending || pending.kind !== "reply") {
485
- console.warn(`[event] explore-reply: no pending reply gate on ${workflowRunId}`);
486
- return;
487
- }
488
- db.resolveReplyGate(pending.id, replyText, sender);
489
- // Append the QA entry to scratch.socratic.qa. The runner reads this
490
- // via {{scratch.socratic.qa}} on the next iteration.
491
- const prevScratch = (run.scratch || {});
492
- const prevSocratic = (prevScratch.socratic || {});
493
- const qaList = Array.isArray(prevSocratic.qa) ? [...prevSocratic.qa] : [];
494
- qaList.push({
495
- question: prevSocratic.lastOutput ?? "",
496
- answer: replyText,
497
- sender,
498
- at: new Date().toISOString(),
499
- });
500
- db.updateWorkflowRunScratch(workflowRunId, {
501
- socratic: { ...prevSocratic, qa: qaList },
502
- });
503
- // Set currentPhase to the phase BEFORE the loop owner so the
504
- // runner's nextPhaseAfter lands back on the loop phase for the
505
- // next iteration. Walk the workflow definition to find it.
506
- try {
507
- const { getWorkflow } = await import("./workflows/loader.js");
508
- const def = getWorkflow(run.workflowName);
509
- // Find the phase that owns this gate (pattern: socratic_iter_N)
510
- const gateParts = pending.gate.match(/^(.+)_iter_\d+$/);
511
- const owningPhaseName = gateParts ? gateParts[1] : null;
512
- if (owningPhaseName) {
513
- const ownIdx = def.phases.findIndex((p) => p.name === owningPhaseName);
514
- const priorPhase = ownIdx > 0 ? def.phases[ownIdx - 1].name : owningPhaseName;
515
- db.updateWorkflowPhase(workflowRunId, priorPhase, {
516
- phase: priorPhase,
517
- timestamp: new Date().toISOString(),
518
- success: true,
519
- summary: `Resumed after reply on gate: ${pending.gate}`,
520
- });
521
- }
522
- }
523
- catch (err) {
524
- console.warn(`[event] explore-reply: could not resolve owning phase:`, err);
525
- }
526
- db.resumeWorkflowRun(workflowRunId);
527
- // Re-dispatch. Use channelId/threadId from the current event context
528
- // (the router captured them from the reply envelope), not from stored
529
- // workflow context — they were never persisted there.
530
- const isSlack = run.triggerId.startsWith("slack:");
531
- const replyChannelId = context.channelId;
532
- const replyThreadId = context.threadId;
533
- // Reconstruct owner/repo from the stored workflow context.
534
- const storedCtx = (run.context || {});
535
- const storedOwner = storedCtx.owner;
536
- const resumeRepo = storedOwner && run.repo
537
- ? `${storedOwner}/${run.repo}`
538
- : run.repo || undefined;
539
- console.log(`[event] explore-reply: resuming ${workflowRunId} after reply from ${sender}`);
540
- dispatchWorkflow("explore", {
541
- repo: resumeRepo || (isSlack ? undefined : run.triggerId.split("#")[0]),
542
- issueNumber: run.issueNumber,
543
- sender,
544
- _triggerType: envelope.type === "message" ? "chat" : "webhook",
545
- triggerId: isSlack ? run.triggerId : undefined,
546
- channelId: replyChannelId,
547
- threadId: replyThreadId,
548
- }).catch((err) => console.error(`[event] explore-reply resume failed:`, err));
549
- return;
550
- }
551
- // Approval responses
552
- if (skill === "approval-response") {
553
- const decision = context.decision;
554
- const sender = context.sender || "unknown";
555
- const reason = context.reason;
556
- const triggerId = context.repo && context.issueNumber
557
- ? `${context.repo}#${context.issueNumber}`
558
- : undefined;
559
- const approval = context.workflowRunId
560
- ? db.getPendingApprovalForWorkflow(context.workflowRunId)
561
- : triggerId
562
- ? db.getPendingApprovalByTrigger(triggerId)
563
- : null;
564
- if (!approval) {
565
- await envelope.reply("No pending approval found.");
566
- return;
567
- }
568
- db.respondToApproval(approval.id, decision, sender, reason);
569
- if (decision === "approved") {
570
- // Re-trigger the build cycle — resume logic in orchestrator will pick up from DB state
571
- const workflowRun = db.getWorkflowRun(approval.workflowRunId);
572
- if (workflowRun && !github) {
573
- await envelope.reply("Approval recorded, but cannot resume: GitHub App is not configured. Configure GITHUB_APP_ID and related env vars to enable build resumption.");
574
- return;
575
- }
576
- if (workflowRun && github) {
577
- await envelope.reply(`Approved by ${sender}. Resuming \`${workflowRun.workflowName}\`...`);
578
- const [owner, repo] = workflowRun.triggerId.includes("/")
579
- ? workflowRun.triggerId.replace(/#\d+$/, "").split("/")
580
- : ["", ""];
581
- const issueNumber = workflowRun.issueNumber;
582
- if (owner && repo && issueNumber) {
583
- db.resumeWorkflowRun(workflowRun.id);
584
- dispatchWorkflow(workflowRun.workflowName, {
585
- repo: `${owner}/${repo}`,
586
- issueNumber,
587
- title: `Issue #${issueNumber}`,
588
- body: "",
589
- sender,
590
- _triggerType: "approval",
591
- }).catch((err) => console.error(`[approval] Resume failed:`, err));
592
- }
593
- }
594
- }
595
- else {
596
- const workflowRun = db.getWorkflowRun(approval.workflowRunId);
597
- if (workflowRun) {
598
- db.finishWorkflowRun(approval.workflowRunId, "failed", `Rejected by ${sender}: ${reason || "no reason given"}`);
599
- }
600
- await envelope.reply(`Rejected by ${sender}. Build cycle aborted.${reason ? ` Reason: ${reason}` : ""}`);
601
- }
602
- return;
603
- }
604
- // Build requests: route to the programmatic orchestrator instead of the SKILL.md
605
- if (skill === "github-orchestrator" && context.issueNumber && context.repo) {
606
- const repoStr = context.repo;
607
- const [owner, repo] = repoStr.includes("/") ? repoStr.split("/") : ["", repoStr];
608
- const issueNumber = context.issueNumber;
609
- if (!owner || !repo) {
610
- console.error(`[event] Invalid repo format: ${repoStr}`);
611
- return;
612
- }
613
- // Fetch full issue details if we don't have them
614
- let issueTitle = context.title || "";
615
- let issueBody = context.body || "";
616
- let issueLabels = context.labels || [];
617
- if (github && (!issueTitle || !issueBody || issueLabels.length === 0)) {
618
- try {
619
- const issue = await github.getIssue(owner, repo, issueNumber);
620
- issueTitle = issueTitle || issue.title;
621
- issueBody = issueBody || issue.body || "";
622
- if (issueLabels.length === 0) {
623
- issueLabels = (issue.labels || []).map((l) => typeof l === "string" ? l : l.name).filter(Boolean);
624
- }
625
- }
626
- catch (err) {
627
- console.warn(`[event] Could not fetch issue: ${err.message}`);
628
- }
629
- }
630
- const executionId = randomUUID();
631
- db.recordStart({
632
- id: executionId,
633
- triggerType: envelope.type === "message" ? "chat" : "webhook",
634
- triggerId: String(issueNumber),
635
- skill: "build-cycle",
636
- repo: repoStr,
637
- issueNumber,
638
- startedAt: new Date().toISOString(),
639
- });
640
- if (envelope.type === "message") {
641
- await envelope.reply(`Starting build cycle for ${repoStr}#${issueNumber}...`);
677
+ try {
678
+ const outcome = await dispatch(envelope, dispatchDeps);
679
+ if (outcome.kind === "ignored") {
680
+ console.log(`[event] Ignored: ${outcome.reason}`);
642
681
  }
643
- else if (github) {
644
- // GitHub-triggered builds: react with 🚀 on the triggering comment so
645
- // the user sees an instant ack before guardrails / architect / etc.
646
- // start running. Non-fatal if it fails.
647
- const commentId = envelope.raw?.comment?.id;
648
- if (commentId) {
649
- github
650
- .reactToComment(owner, repo, commentId, "rocket")
651
- .catch((err) => {
652
- const msg = err instanceof Error ? err.message : String(err);
653
- console.warn(`[event] Could not react to trigger comment: ${msg}`);
654
- });
655
- }
682
+ else if (outcome.kind === "skipped") {
683
+ console.log(`[event] Skipped: ${outcome.reason}`);
656
684
  }
657
- dispatchWorkflow("build", {
658
- repo: repoStr,
659
- issueNumber,
660
- title: issueTitle || `Issue #${issueNumber}`,
661
- body: issueBody,
662
- labels: issueLabels,
663
- commentBody: context.commentBody,
664
- sender: context.sender || "unknown",
665
- _triggerType: envelope.type === "message" ? "chat" : "webhook",
666
- }).then((result) => {
667
- db.recordFinish(executionId, {
668
- success: result.success,
669
- error: result.success ? undefined : "Build cycle failed",
670
- durationMs: 0,
671
- });
672
- if (envelope.type === "message") {
673
- envelope.reply(result.success ? `Build cycle complete.` : `Build cycle failed.`);
674
- }
675
- }).catch((err) => {
676
- console.error(`[event] Build cycle failed:`, err);
677
- db.recordFinish(executionId, { success: false, error: err.message, durationMs: 0 });
678
- });
679
- return;
680
685
  }
681
- // For messaging-triggered skills, acknowledge and reply when done.
682
- // The router still uses skill names — they map 1:1 to workflow YAML names
683
- // for the four agent skills (issue-triage, pr-review, repo-health, issue-comment).
684
- if (envelope.type === "message") {
685
- await envelope.reply(`Starting *${skill}*... I'll report back when it's done.`);
686
- dispatchWorkflow(skill, { ...context, _triggerType: "chat" }).then(async (result) => {
687
- if (result.paused) {
688
- // Workflow paused at a gate (approval or reply) — don't say
689
- // "completed", the workflow itself already posted instructions.
690
- }
691
- else if (result.success) {
692
- await envelope.reply(`*${skill}* completed.`);
693
- }
694
- else {
695
- await envelope.reply(`*${skill}* failed${result.error ? `: ${result.error}` : ""}.`);
696
- }
697
- }).catch(async (err) => {
698
- const msg = err instanceof Error ? err.message : String(err);
699
- console.error(`[event] workflow ${skill} threw: ${msg}`);
700
- await envelope.reply(`*${skill}* failed: ${msg}`);
701
- });
702
- return;
703
- }
704
- // Run workflow asynchronously (webhook triggers)
705
- dispatchWorkflow(skill, { ...context, _triggerType: "webhook" }).catch((err) => {
686
+ catch (err) {
706
687
  const msg = err instanceof Error ? err.message : String(err);
707
- console.error(`[event] Unhandled error in workflow ${skill}: ${msg}`);
708
- });
709
- });
710
- // Set up cron scheduler — each cron tick dispatches an agent workflow by name
711
- const cron = new CronScheduler(db, async (workflowName, context) => {
712
- await dispatchWorkflow(workflowName, { ...context, _triggerType: "cron" });
688
+ console.error(`[event] dispatch threw: ${msg}`);
689
+ }
713
690
  });
691
+ // Cron jobs — fan out from each tick into one workflow run per managed repo
692
+ // (see dispatchCronWorkflow). The scheduler itself was constructed earlier
693
+ // so the admin dashboard could be wired with it.
714
694
  const webhooksEnabled = !!(config.webhookSecret && config.githubApp);
715
- const jobs = getJobs({ webhooksEnabled });
695
+ const jobs = getJobs({ webhooksEnabled, db });
716
696
  for (const job of jobs) {
717
697
  cron.register(job);
718
698
  }
719
699
  if (webhooksEnabled) {
720
700
  console.log("[cron] Webhooks enabled — skipping issue/PR polling crons");
721
701
  }
722
- // API usage/capacity checker — runs every 30 minutes, no sandbox needed.
723
- // Passes a Slack notifier so the cron can alert the admin if the host
724
- // claude CLI auth degrades (it then halts itself until cleared).
725
- const { checkApiUsage } = await import("./cron/rate-limits.js");
726
- const adminNotifier = slackConnector
727
- ? (msg) => slackConnector.sendToDeliveryChannel(msg)
728
- : undefined;
729
- cron.registerDirect({
730
- name: "check-api-usage",
731
- schedule: "*/30 * * * *",
732
- handler: () => checkApiUsage(db, adminNotifier),
733
- });
734
702
  // Start everything
735
703
  await registry.startAll();
736
704
  console.log("[main] All connectors started");
737
705
  console.log("[main] Cron jobs registered");
706
+ // Chat runs in-process via pi-ai — no long-lived server to boot.
738
707
  // Boot-time recovery: any workflow_runs left in 'running' state from a
739
708
  // previous harness lifetime have already had their sandbox containers
740
709
  // killed by cleanupOrphanedSandboxes(). Mark their stale execution rows as
@@ -745,15 +714,21 @@ async function main() {
745
714
  db,
746
715
  github,
747
716
  config: {
748
- mcpConfigPath,
749
717
  model: config.model,
750
718
  maxTurns: config.maxTurns,
751
719
  stateDir: config.stateDir,
752
720
  sandboxDir: config.sandboxDir,
721
+ sessionsDir: config.sessionsDir,
722
+ sandbox: config.sandbox,
723
+ buildAssets: config.buildAssets,
724
+ buildAssetsDir: config.buildAssetsDir,
725
+ otel: config.otel,
753
726
  },
754
727
  models: config.models,
728
+ variants: config.variants,
755
729
  approvalConfig: config.approval,
756
730
  bootstrapLabel: config.bootstrapLabel,
731
+ publicUrl: config.publicUrl,
757
732
  slackPoster: slackConnector
758
733
  ? (channelId, threadId, msg) => slackConnector.sendMessage(channelId, threadId, msg).then(() => { })
759
734
  : undefined,
@@ -764,6 +739,10 @@ async function main() {
764
739
  console.log("\n[main] Shutting down...");
765
740
  cron.stopAll();
766
741
  await registry.stopAll();
742
+ if (!telemetryShutdownStarted) {
743
+ telemetryShutdownStarted = true;
744
+ await shutdownTelemetry();
745
+ }
767
746
  db.close();
768
747
  process.exit(0);
769
748
  };
@@ -772,6 +751,13 @@ async function main() {
772
751
  }
773
752
  main().catch((err) => {
774
753
  console.error("[main] Fatal error:", err);
775
- process.exit(1);
754
+ // Exit 78 (EX_CONFIG) to signal Docker restart policy that looping won't help.
755
+ // Common causes: bad PEM, wrong App ID, missing env vars.
756
+ const msg = err?.message || "";
757
+ const isConfig = msg.includes("could not be decoded") ||
758
+ msg.includes("not found") ||
759
+ msg.includes("ENOENT") ||
760
+ msg.includes("required");
761
+ process.exit(isConfig ? 78 : 1);
776
762
  });
777
763
  //# sourceMappingURL=index.js.map