lastlight 0.1.15 → 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 +377 -432
  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 +30 -1
  272. package/dist/setup.js +241 -25
  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 +17 -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,20 +1,28 @@
1
- import { existsSync, mkdirSync, readFileSync, 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";
18
26
  /**
19
27
  * Pre-flight validation — checks that config is sane before starting any
20
28
  * services. Exits with code 78 (EX_CONFIG) on configuration errors so
@@ -51,9 +59,32 @@ function validateConfig(config) {
51
59
  async function main() {
52
60
  console.log("Last Light v2.0 — Agent SDK Harness");
53
61
  console.log("====================================");
54
- // Load and validate config before starting anything
55
- 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
+ }
56
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");
57
88
  console.log(`[config] Port: ${config.port}, Model: ${config.model}`);
58
89
  const modelOverrides = Object.entries(config.models).filter(([k]) => k !== "default");
59
90
  if (modelOverrides.length > 0) {
@@ -66,11 +97,56 @@ async function main() {
66
97
  mkdirSync(resolve(config.stateDir, sub), { recursive: true });
67
98
  }
68
99
  console.log(`[state] State dir: ${config.stateDir}`);
69
- // Generate MCP config file for the Agent SDK
70
- const mcpConfig = generateMcpConfig(config);
71
- const mcpConfigPath = resolve(config.mcpConfigPath);
72
- writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
73
- console.log(`[config] MCP config written to: ${mcpConfigPath}`);
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
+ }
74
150
  // Configure git with GitHub App credentials — agents can git clone/push natively.
75
151
  // Non-fatal: the token is refreshed before each agent execution anyway, so a
76
152
  // transient failure here (DNS, rate limit) doesn't block startup.
@@ -86,9 +162,6 @@ async function main() {
86
162
  console.warn(`[git-auth] Initial token mint failed (will retry per-execution): ${err.message}`);
87
163
  }
88
164
  }
89
- // Initialize state database
90
- const db = new StateDb(config.dbPath);
91
- console.log(`[state] Database: ${config.dbPath}`);
92
165
  // GitHub API client for harness-level operations (posting comments, fetching issues)
93
166
  const github = config.githubApp ? new GitHubClient(config.githubApp) : null;
94
167
  /**
@@ -100,7 +173,7 @@ async function main() {
100
173
  * The router still uses skill names for backwards compat — for the four
101
174
  * agent skills they're 1:1 with workflow names.
102
175
  */
103
- const dispatchWorkflow = async (workflowName, context) => {
176
+ const dispatchWorkflow = async (workflowName, context, onRunStart) => {
104
177
  // Slack-initiated workflows (explore, /explore) carry a
105
178
  // `slack:{team}:{channel}:{thread}` triggerId and don't require a
106
179
  // managed `repo` — their postComment goes back to the Slack thread.
@@ -125,7 +198,7 @@ async function main() {
125
198
  }
126
199
  // Pluck the standard fields, leave the rest in `extra` for the workflow
127
200
  // template to consume.
128
- 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;
129
202
  // Preserve channelId/threadId in extra so they're stored on the
130
203
  // workflow run context — needed by boot-time resume to rebuild the
131
204
  // Slack postComment callback after a harness restart.
@@ -134,6 +207,35 @@ async function main() {
134
207
  extra.channelId = channelId;
135
208
  if (typeof threadId === "string")
136
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
+ }
137
239
  const request = {
138
240
  owner,
139
241
  repo,
@@ -146,7 +248,64 @@ async function main() {
146
248
  sender: typeof sender === "string" ? sender : "unknown",
147
249
  triggerId: slackTriggerId,
148
250
  extra,
251
+ prePopulateBranch,
149
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
+ }
150
309
  const slackPost = slackTriggerId && slackConnector && typeof channelId === "string" && typeof threadId === "string"
151
310
  ? async (msg) => {
152
311
  try {
@@ -158,7 +317,84 @@ async function main() {
158
317
  }
159
318
  }
160
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;
161
395
  const callbacks = {
396
+ reporter: reporterProxy,
397
+ publicUrl: config.publicUrl,
162
398
  postComment: slackPost
163
399
  ?? (github && issueNumber
164
400
  ? async (msg) => {
@@ -181,15 +417,29 @@ async function main() {
181
417
  }
182
418
  },
183
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,
184
430
  };
185
431
  try {
186
432
  const result = await runSimpleWorkflow(workflowName, request, {
187
- mcpConfigPath,
188
433
  model: config.model,
189
434
  maxTurns: config.maxTurns,
190
435
  stateDir: config.stateDir,
191
436
  sandboxDir: config.sandboxDir,
192
- }, 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);
193
443
  const summary = result.phases.map((p) => `${p.phase}=${p.success ? "ok" : "fail"}`).join(", ");
194
444
  if (result.paused) {
195
445
  console.log(`[dispatch] ${workflowName} paused (${summary})`);
@@ -210,8 +460,6 @@ async function main() {
210
460
  };
211
461
  // Set up connector registry
212
462
  const registry = new ConnectorRegistry();
213
- // Session manager for messaging connectors (shared across Slack, Discord, etc.)
214
- const sessionManager = new SessionManager(db.database);
215
463
  // Message delivery service for cron output
216
464
  const delivery = new MessageDeliveryService();
217
465
  // GitHub webhook connector (optional — requires both webhook secret and GitHub App)
@@ -240,34 +488,59 @@ async function main() {
240
488
  delivery.register("slack", (msg) => slackConnector.sendToDeliveryChannel(msg));
241
489
  }
242
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
+ });
243
501
  // Mount admin dashboard (needs an HTTP server — use GitHub connector or create standalone)
244
502
  if (githubConnector) {
245
503
  mountAdmin(githubConnector.honoApp, db, {
504
+ cronScheduler: cron,
246
505
  stateDir: config.stateDir,
247
- sessionsDir: resolve(process.env.CLAUDE_HOME_DIR || "./data/claude-home"),
506
+ sessionsDir: config.sessionsDir,
507
+ buildAssetsDir: config.buildAssetsDir,
508
+ buildAssets: config.buildAssets,
248
509
  adminPassword: process.env.ADMIN_PASSWORD ?? "",
249
510
  adminSecret: process.env.ADMIN_SECRET ?? "lastlight-dev-secret",
511
+ publicConfig: config.publicConfig,
250
512
  slackOAuthClientId: process.env.SLACK_OAUTH_CLIENT_ID,
251
513
  slackOAuthClientSecret: process.env.SLACK_OAUTH_CLIENT_SECRET,
252
514
  slackOAuthRedirectUri: process.env.SLACK_OAUTH_REDIRECT_URI,
253
515
  slackAllowedWorkspace: process.env.SLACK_ALLOWED_WORKSPACE,
254
- adminNotifier: slackConnector
255
- ? (msg) => slackConnector.sendToDeliveryChannel(msg)
256
- : 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,
257
520
  resumeWorkflow: async (workflowRun, sender) => {
258
521
  if (!github) {
259
522
  console.warn(`[admin] Cannot resume workflow ${workflowRun.id}: GitHub App not configured`);
260
523
  return;
261
524
  }
262
- 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("/")
263
530
  ? workflowRun.triggerId.replace(/#\d+$/, "").split("/")
264
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
+ }
265
538
  const issueNumber = workflowRun.issueNumber;
266
539
  if (!owner || !repo || !issueNumber) {
267
540
  console.warn(`[admin] Cannot resume workflow ${workflowRun.id}: missing owner/repo/issueNumber`);
268
541
  return;
269
542
  }
270
- db.resumeWorkflowRun(workflowRun.id);
543
+ db.runs.setRunning(workflowRun.id);
271
544
  console.log(`[admin] Resuming ${workflowRun.workflowName} for ${owner}/${repo}#${issueNumber} after dashboard approval by ${sender}`);
272
545
  dispatchWorkflow(workflowRun.workflowName, {
273
546
  repo: `${owner}/${repo}`,
@@ -346,429 +619,91 @@ async function main() {
346
619
  return c.json({ accepted: true, owner, repo, issueNumber }, 202);
347
620
  });
348
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
+ });
349
675
  registry.onEvent(async (envelope) => {
350
676
  console.log(`[event] ${envelope.source}:${envelope.type} from ${envelope.sender}${envelope.repo ? ` on ${envelope.repo}` : ""}`);
351
- const route = await routeEvent(envelope, { db });
352
- if (route.action === "ignore") {
353
- console.log(`[event] Ignored: ${route.reason}`);
354
- return;
355
- }
356
- if (route.action === "reply") {
357
- await envelope.reply(route.message);
358
- return;
359
- }
360
- const { skill, context } = route;
361
- // Chat messages: handle directly (no sandbox, low latency)
362
- if (skill === "chat") {
363
- const messagingSessionId = context.sessionId;
364
- const message = context.message;
365
- const sender = context.sender;
366
- // Look up the existing Agent SDK session id for this Slack thread.
367
- // First message has none → fresh session; subsequent messages resume.
368
- const messagingSession = sessionManager.getSession(messagingSessionId);
369
- const resumeAgentSessionId = messagingSession?.agentSessionId ?? undefined;
370
- // Record an executions row so chat usage shows up in dashboard stats
371
- // alongside sandbox runs. triggerId is the messaging-session id, so a
372
- // whole Slack thread groups together with `GROUP BY trigger_id`.
373
- const executionId = randomUUID();
374
- db.recordStart({
375
- id: executionId,
376
- triggerType: "chat",
377
- triggerId: messagingSessionId,
378
- skill: "chat",
379
- startedAt: new Date().toISOString(),
380
- });
381
- try {
382
- const result = await handleChatMessage(message, messagingSessionId, sender, sessionManager, {
383
- mcpConfigPath,
384
- model: resolveModel(config.models, "chat"),
385
- maxTurns: 10,
386
- }, resumeAgentSessionId);
387
- // Persist the Agent SDK session id on the first turn so the next
388
- // turn in this thread can resume into the same jsonl. We always
389
- // overwrite — if the SDK rotated the id (e.g. resume failed and it
390
- // started a fresh session) we want the latest one.
391
- if (result.agentSessionId && result.agentSessionId !== resumeAgentSessionId) {
392
- sessionManager.setAgentSessionId(messagingSessionId, result.agentSessionId);
393
- }
394
- db.recordFinish(executionId, {
395
- success: result.success,
396
- error: result.error,
397
- turns: result.turns,
398
- durationMs: result.durationMs,
399
- sessionId: result.agentSessionId,
400
- costUsd: result.costUsd,
401
- inputTokens: result.inputTokens,
402
- cacheCreationInputTokens: result.cacheCreationInputTokens,
403
- cacheReadInputTokens: result.cacheReadInputTokens,
404
- outputTokens: result.outputTokens,
405
- apiDurationMs: result.apiDurationMs,
406
- stopReason: result.stopReason,
407
- });
408
- await envelope.reply(result.text);
409
- }
410
- catch (err) {
411
- const msg = err instanceof Error ? err.message : String(err);
412
- console.error(`[event] Chat error:`, msg);
413
- db.recordFinish(executionId, {
414
- success: false,
415
- error: msg,
416
- durationMs: 0,
417
- });
418
- await envelope.reply("Sorry, I encountered an error. Please try again.");
419
- }
420
- return;
421
- }
422
- // Chat reset: deactivate the session and confirm
423
- if (skill === "chat-reset") {
424
- const sessionId = context.sessionId;
425
- if (sessionId) {
426
- sessionManager.deactivateSession(sessionId);
427
- }
428
- await envelope.reply("Session reset. Starting fresh.");
429
- return;
430
- }
431
- // Status report: return running executions
432
- if (skill === "status-report") {
433
- const running = db.runningExecutions();
434
- if (running.length === 0) {
435
- await envelope.reply("No tasks currently running.");
436
- }
437
- else {
438
- const lines = running.map((r) => `• *${r.skill}*${r.repo ? ` on ${r.repo}` : ""}${r.issueNumber ? ` #${r.issueNumber}` : ""} (started ${r.startedAt})`);
439
- await envelope.reply(`Running tasks:\n${lines.join("\n")}`);
440
- }
441
- return;
442
- }
443
- // Check if already running for this trigger
444
- const triggerId = String(envelope.issueNumber || envelope.id);
445
- if (db.isRunning(skill, triggerId)) {
446
- console.log(`[event] Skipping: ${skill} already running for ${triggerId}`);
447
- // Notify messaging users that the task is already in progress
448
- if (envelope.type === "message") {
449
- await envelope.reply(`That task is already running. Use /status to check progress.`);
450
- }
451
- return;
452
- }
453
- // PR fix: lightweight fix-and-push, no full build cycle
454
- if (skill === "pr-fix" && context.prNumber && context.repo) {
455
- const repoStr = context.repo;
456
- const [owner, repo] = repoStr.includes("/") ? repoStr.split("/") : ["", repoStr];
457
- const prNumber = context.prNumber;
458
- if (!owner || !repo) {
459
- console.error(`[event] Invalid repo format: ${repoStr}`);
460
- return;
461
- }
462
- // Fetch PR details and CI failures
463
- let prTitle = context.title || "";
464
- let prBody = context.body || "";
465
- let branch = "";
466
- let failedChecks = "";
467
- if (github) {
468
- try {
469
- const pr = await github.getPullRequest(owner, repo, prNumber);
470
- prTitle = prTitle || pr.title;
471
- prBody = prBody || pr.body || "";
472
- branch = pr.head.ref;
473
- // Fetch CI failures for the PR's head commit
474
- failedChecks = await github.getFailedChecks(owner, repo, pr.head.sha);
475
- }
476
- catch (err) {
477
- console.warn(`[event] Could not fetch PR: ${err.message}`);
478
- }
479
- }
480
- if (!branch) {
481
- console.error(`[event] Could not determine branch for PR #${prNumber}`);
482
- return;
483
- }
484
- console.log(`[event] PR fix for ${repoStr}#${prNumber} on branch ${branch}`);
485
- const ciSection = failedChecks && !failedChecks.includes("No failed checks")
486
- ? `CI FAILURES (from GitHub Actions — fix these first):\n${failedChecks}`
487
- : "";
488
- dispatchWorkflow("pr-fix", {
489
- repo: repoStr,
490
- prNumber,
491
- title: prTitle,
492
- body: prBody,
493
- commentBody: context.commentBody || "",
494
- sender: context.sender || "unknown",
495
- branch,
496
- failedChecks,
497
- ciSection,
498
- _triggerType: "webhook",
499
- }).catch((err) => {
500
- console.error(`[event] PR fix failed:`, err);
501
- });
502
- return;
503
- }
504
- // Explore reply: free-form user reply on a paused socratic explore
505
- // run. Resolve the reply gate with the message body, merge it into
506
- // scratch.socratic.qa so the next iteration sees the answer, and
507
- // re-dispatch the workflow to continue the loop.
508
- if (skill === "explore-reply") {
509
- const workflowRunId = context.workflowRunId;
510
- const replyText = context.reply || "";
511
- const sender = context.sender || "unknown";
512
- const run = db.getWorkflowRun(workflowRunId);
513
- if (!run) {
514
- console.warn(`[event] explore-reply: run ${workflowRunId} not found`);
515
- return;
516
- }
517
- const pending = db.getPendingApprovalForWorkflow(workflowRunId);
518
- if (!pending || pending.kind !== "reply") {
519
- console.warn(`[event] explore-reply: no pending reply gate on ${workflowRunId}`);
520
- return;
521
- }
522
- db.resolveReplyGate(pending.id, replyText, sender);
523
- // Append the QA entry to scratch.socratic.qa. The runner reads this
524
- // via {{scratch.socratic.qa}} on the next iteration.
525
- const prevScratch = (run.scratch || {});
526
- const prevSocratic = (prevScratch.socratic || {});
527
- const qaList = Array.isArray(prevSocratic.qa) ? [...prevSocratic.qa] : [];
528
- qaList.push({
529
- question: prevSocratic.lastOutput ?? "",
530
- answer: replyText,
531
- sender,
532
- at: new Date().toISOString(),
533
- });
534
- db.updateWorkflowRunScratch(workflowRunId, {
535
- socratic: { ...prevSocratic, qa: qaList },
536
- });
537
- // Set currentPhase to the phase BEFORE the loop owner so the
538
- // runner's nextPhaseAfter lands back on the loop phase for the
539
- // next iteration. Walk the workflow definition to find it.
540
- try {
541
- const { getWorkflow } = await import("./workflows/loader.js");
542
- const def = getWorkflow(run.workflowName);
543
- // Find the phase that owns this gate (pattern: socratic_iter_N)
544
- const gateParts = pending.gate.match(/^(.+)_iter_\d+$/);
545
- const owningPhaseName = gateParts ? gateParts[1] : null;
546
- if (owningPhaseName) {
547
- const ownIdx = def.phases.findIndex((p) => p.name === owningPhaseName);
548
- const priorPhase = ownIdx > 0 ? def.phases[ownIdx - 1].name : owningPhaseName;
549
- db.updateWorkflowPhase(workflowRunId, priorPhase, {
550
- phase: priorPhase,
551
- timestamp: new Date().toISOString(),
552
- success: true,
553
- summary: `Resumed after reply on gate: ${pending.gate}`,
554
- });
555
- }
556
- }
557
- catch (err) {
558
- console.warn(`[event] explore-reply: could not resolve owning phase:`, err);
559
- }
560
- db.resumeWorkflowRun(workflowRunId);
561
- // Re-dispatch. Use channelId/threadId from the current event context
562
- // (the router captured them from the reply envelope), not from stored
563
- // workflow context — they were never persisted there.
564
- const isSlack = run.triggerId.startsWith("slack:");
565
- const replyChannelId = context.channelId;
566
- const replyThreadId = context.threadId;
567
- // Reconstruct owner/repo from the stored workflow context.
568
- const storedCtx = (run.context || {});
569
- const storedOwner = storedCtx.owner;
570
- const resumeRepo = storedOwner && run.repo
571
- ? `${storedOwner}/${run.repo}`
572
- : run.repo || undefined;
573
- console.log(`[event] explore-reply: resuming ${workflowRunId} after reply from ${sender}`);
574
- dispatchWorkflow("explore", {
575
- repo: resumeRepo || (isSlack ? undefined : run.triggerId.split("#")[0]),
576
- issueNumber: run.issueNumber,
577
- sender,
578
- _triggerType: envelope.type === "message" ? "chat" : "webhook",
579
- triggerId: isSlack ? run.triggerId : undefined,
580
- channelId: replyChannelId,
581
- threadId: replyThreadId,
582
- }).catch((err) => console.error(`[event] explore-reply resume failed:`, err));
583
- return;
584
- }
585
- // Approval responses
586
- if (skill === "approval-response") {
587
- const decision = context.decision;
588
- const sender = context.sender || "unknown";
589
- const reason = context.reason;
590
- const triggerId = context.repo && context.issueNumber
591
- ? `${context.repo}#${context.issueNumber}`
592
- : undefined;
593
- const approval = context.workflowRunId
594
- ? db.getPendingApprovalForWorkflow(context.workflowRunId)
595
- : triggerId
596
- ? db.getPendingApprovalByTrigger(triggerId)
597
- : null;
598
- if (!approval) {
599
- await envelope.reply("No pending approval found.");
600
- return;
601
- }
602
- db.respondToApproval(approval.id, decision, sender, reason);
603
- if (decision === "approved") {
604
- // Re-trigger the build cycle — resume logic in orchestrator will pick up from DB state
605
- const workflowRun = db.getWorkflowRun(approval.workflowRunId);
606
- if (workflowRun && !github) {
607
- 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.");
608
- return;
609
- }
610
- if (workflowRun && github) {
611
- await envelope.reply(`Approved by ${sender}. Resuming \`${workflowRun.workflowName}\`...`);
612
- const [owner, repo] = workflowRun.triggerId.includes("/")
613
- ? workflowRun.triggerId.replace(/#\d+$/, "").split("/")
614
- : ["", ""];
615
- const issueNumber = workflowRun.issueNumber;
616
- if (owner && repo && issueNumber) {
617
- db.resumeWorkflowRun(workflowRun.id);
618
- dispatchWorkflow(workflowRun.workflowName, {
619
- repo: `${owner}/${repo}`,
620
- issueNumber,
621
- title: `Issue #${issueNumber}`,
622
- body: "",
623
- sender,
624
- _triggerType: "approval",
625
- }).catch((err) => console.error(`[approval] Resume failed:`, err));
626
- }
627
- }
628
- }
629
- else {
630
- const workflowRun = db.getWorkflowRun(approval.workflowRunId);
631
- if (workflowRun) {
632
- db.finishWorkflowRun(approval.workflowRunId, "failed", `Rejected by ${sender}: ${reason || "no reason given"}`);
633
- }
634
- await envelope.reply(`Rejected by ${sender}. Build cycle aborted.${reason ? ` Reason: ${reason}` : ""}`);
635
- }
636
- return;
637
- }
638
- // Build requests: route to the programmatic orchestrator instead of the SKILL.md
639
- if (skill === "github-orchestrator" && context.issueNumber && context.repo) {
640
- const repoStr = context.repo;
641
- const [owner, repo] = repoStr.includes("/") ? repoStr.split("/") : ["", repoStr];
642
- const issueNumber = context.issueNumber;
643
- if (!owner || !repo) {
644
- console.error(`[event] Invalid repo format: ${repoStr}`);
645
- return;
646
- }
647
- // Fetch full issue details if we don't have them
648
- let issueTitle = context.title || "";
649
- let issueBody = context.body || "";
650
- let issueLabels = context.labels || [];
651
- if (github && (!issueTitle || !issueBody || issueLabels.length === 0)) {
652
- try {
653
- const issue = await github.getIssue(owner, repo, issueNumber);
654
- issueTitle = issueTitle || issue.title;
655
- issueBody = issueBody || issue.body || "";
656
- if (issueLabels.length === 0) {
657
- issueLabels = (issue.labels || []).map((l) => typeof l === "string" ? l : l.name).filter(Boolean);
658
- }
659
- }
660
- catch (err) {
661
- console.warn(`[event] Could not fetch issue: ${err.message}`);
662
- }
663
- }
664
- const executionId = randomUUID();
665
- db.recordStart({
666
- id: executionId,
667
- triggerType: envelope.type === "message" ? "chat" : "webhook",
668
- triggerId: String(issueNumber),
669
- skill: "build-cycle",
670
- repo: repoStr,
671
- issueNumber,
672
- startedAt: new Date().toISOString(),
673
- });
674
- if (envelope.type === "message") {
675
- 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}`);
676
681
  }
677
- else if (github) {
678
- // GitHub-triggered builds: react with 🚀 on the triggering comment so
679
- // the user sees an instant ack before guardrails / architect / etc.
680
- // start running. Non-fatal if it fails.
681
- const commentId = envelope.raw?.comment?.id;
682
- if (commentId) {
683
- github
684
- .reactToComment(owner, repo, commentId, "rocket")
685
- .catch((err) => {
686
- const msg = err instanceof Error ? err.message : String(err);
687
- console.warn(`[event] Could not react to trigger comment: ${msg}`);
688
- });
689
- }
682
+ else if (outcome.kind === "skipped") {
683
+ console.log(`[event] Skipped: ${outcome.reason}`);
690
684
  }
691
- dispatchWorkflow("build", {
692
- repo: repoStr,
693
- issueNumber,
694
- title: issueTitle || `Issue #${issueNumber}`,
695
- body: issueBody,
696
- labels: issueLabels,
697
- commentBody: context.commentBody,
698
- sender: context.sender || "unknown",
699
- _triggerType: envelope.type === "message" ? "chat" : "webhook",
700
- }).then((result) => {
701
- db.recordFinish(executionId, {
702
- success: result.success,
703
- error: result.success ? undefined : "Build cycle failed",
704
- durationMs: 0,
705
- });
706
- if (envelope.type === "message") {
707
- envelope.reply(result.success ? `Build cycle complete.` : `Build cycle failed.`);
708
- }
709
- }).catch((err) => {
710
- console.error(`[event] Build cycle failed:`, err);
711
- db.recordFinish(executionId, { success: false, error: err.message, durationMs: 0 });
712
- });
713
- return;
714
- }
715
- // For messaging-triggered skills, acknowledge and reply when done.
716
- // The router still uses skill names — they map 1:1 to workflow YAML names
717
- // for the four agent skills (issue-triage, pr-review, repo-health, issue-comment).
718
- if (envelope.type === "message") {
719
- await envelope.reply(`Starting *${skill}*... I'll report back when it's done.`);
720
- dispatchWorkflow(skill, { ...context, _triggerType: "chat" }).then(async (result) => {
721
- if (result.paused) {
722
- // Workflow paused at a gate (approval or reply) — don't say
723
- // "completed", the workflow itself already posted instructions.
724
- }
725
- else if (result.success) {
726
- await envelope.reply(`*${skill}* completed.`);
727
- }
728
- else {
729
- await envelope.reply(`*${skill}* failed${result.error ? `: ${result.error}` : ""}.`);
730
- }
731
- }).catch(async (err) => {
732
- const msg = err instanceof Error ? err.message : String(err);
733
- console.error(`[event] workflow ${skill} threw: ${msg}`);
734
- await envelope.reply(`*${skill}* failed: ${msg}`);
735
- });
736
- return;
737
685
  }
738
- // Run workflow asynchronously (webhook triggers)
739
- dispatchWorkflow(skill, { ...context, _triggerType: "webhook" }).catch((err) => {
686
+ catch (err) {
740
687
  const msg = err instanceof Error ? err.message : String(err);
741
- console.error(`[event] Unhandled error in workflow ${skill}: ${msg}`);
742
- });
743
- });
744
- // Set up cron scheduler — each cron tick dispatches an agent workflow by name
745
- const cron = new CronScheduler(db, async (workflowName, context) => {
746
- await dispatchWorkflow(workflowName, { ...context, _triggerType: "cron" });
688
+ console.error(`[event] dispatch threw: ${msg}`);
689
+ }
747
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.
748
694
  const webhooksEnabled = !!(config.webhookSecret && config.githubApp);
749
- const jobs = getJobs({ webhooksEnabled });
695
+ const jobs = getJobs({ webhooksEnabled, db });
750
696
  for (const job of jobs) {
751
697
  cron.register(job);
752
698
  }
753
699
  if (webhooksEnabled) {
754
700
  console.log("[cron] Webhooks enabled — skipping issue/PR polling crons");
755
701
  }
756
- // API usage/capacity checker — runs every 30 minutes, no sandbox needed.
757
- // Passes a Slack notifier so the cron can alert the admin if the host
758
- // claude CLI auth degrades (it then halts itself until cleared).
759
- const { checkApiUsage } = await import("./cron/rate-limits.js");
760
- const adminNotifier = slackConnector
761
- ? (msg) => slackConnector.sendToDeliveryChannel(msg)
762
- : undefined;
763
- cron.registerDirect({
764
- name: "check-api-usage",
765
- schedule: "*/30 * * * *",
766
- handler: () => checkApiUsage(db, adminNotifier),
767
- });
768
702
  // Start everything
769
703
  await registry.startAll();
770
704
  console.log("[main] All connectors started");
771
705
  console.log("[main] Cron jobs registered");
706
+ // Chat runs in-process via pi-ai — no long-lived server to boot.
772
707
  // Boot-time recovery: any workflow_runs left in 'running' state from a
773
708
  // previous harness lifetime have already had their sandbox containers
774
709
  // killed by cleanupOrphanedSandboxes(). Mark their stale execution rows as
@@ -779,15 +714,21 @@ async function main() {
779
714
  db,
780
715
  github,
781
716
  config: {
782
- mcpConfigPath,
783
717
  model: config.model,
784
718
  maxTurns: config.maxTurns,
785
719
  stateDir: config.stateDir,
786
720
  sandboxDir: config.sandboxDir,
721
+ sessionsDir: config.sessionsDir,
722
+ sandbox: config.sandbox,
723
+ buildAssets: config.buildAssets,
724
+ buildAssetsDir: config.buildAssetsDir,
725
+ otel: config.otel,
787
726
  },
788
727
  models: config.models,
728
+ variants: config.variants,
789
729
  approvalConfig: config.approval,
790
730
  bootstrapLabel: config.bootstrapLabel,
731
+ publicUrl: config.publicUrl,
791
732
  slackPoster: slackConnector
792
733
  ? (channelId, threadId, msg) => slackConnector.sendMessage(channelId, threadId, msg).then(() => { })
793
734
  : undefined,
@@ -798,6 +739,10 @@ async function main() {
798
739
  console.log("\n[main] Shutting down...");
799
740
  cron.stopAll();
800
741
  await registry.stopAll();
742
+ if (!telemetryShutdownStarted) {
743
+ telemetryShutdownStarted = true;
744
+ await shutdownTelemetry();
745
+ }
801
746
  db.close();
802
747
  process.exit(0);
803
748
  };