lastlight 0.16.0 → 0.16.1

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 (385) hide show
  1. package/README.md +71 -692
  2. package/dist/cli-config.js.map +1 -0
  3. package/dist/cli-format.js.map +1 -0
  4. package/dist/{cli/cli-server.d.ts → cli-server.d.ts} +26 -1
  5. package/dist/{cli/cli-server.js → cli-server.js} +40 -5
  6. package/dist/cli-server.js.map +1 -0
  7. package/dist/cli-timeline.js.map +1 -0
  8. package/dist/{cli/cli.js → cli.js} +4 -3
  9. package/dist/cli.js.map +1 -0
  10. package/dist/{cli/fork-cli.d.ts → fork-cli.d.ts} +3 -3
  11. package/dist/{cli/fork-cli.js → fork-cli.js} +28 -27
  12. package/dist/fork-cli.js.map +1 -0
  13. package/dist/{cli/oauth-cli.js → oauth-cli.js} +1 -1
  14. package/dist/oauth-cli.js.map +1 -0
  15. package/dist/{cli/setup.js → setup.js} +10 -4
  16. package/dist/setup.js.map +1 -0
  17. package/dist/{cli/skills-install.d.ts → skills-install.d.ts} +6 -0
  18. package/dist/{cli/skills-install.js → skills-install.js} +6 -2
  19. package/dist/skills-install.js.map +1 -0
  20. package/package.json +20 -65
  21. package/plugins/lastlight/.claude-plugin/plugin.json +1 -1
  22. package/agent-context/rules.md +0 -80
  23. package/agent-context/security.md +0 -69
  24. package/agent-context/soul.md +0 -21
  25. package/config/default.yaml +0 -119
  26. package/deploy/.env.production.example +0 -100
  27. package/deploy/entrypoint.sh +0 -82
  28. package/deploy/native/README.md +0 -160
  29. package/deploy/native/install.sh +0 -110
  30. package/deploy/native/lastlight.env.example +0 -95
  31. package/deploy/native/lastlight.service +0 -50
  32. package/deploy/sandbox-entrypoint.sh +0 -93
  33. package/dist/admin/auth.d.ts +0 -51
  34. package/dist/admin/auth.js +0 -121
  35. package/dist/admin/auth.js.map +0 -1
  36. package/dist/admin/chat-session-reader.d.ts +0 -39
  37. package/dist/admin/chat-session-reader.js +0 -84
  38. package/dist/admin/chat-session-reader.js.map +0 -1
  39. package/dist/admin/docker.d.ts +0 -61
  40. package/dist/admin/docker.js +0 -187
  41. package/dist/admin/docker.js.map +0 -1
  42. package/dist/admin/index.d.ts +0 -9
  43. package/dist/admin/index.js +0 -59
  44. package/dist/admin/index.js.map +0 -1
  45. package/dist/admin/routes.d.ts +0 -81
  46. package/dist/admin/routes.js +0 -1381
  47. package/dist/admin/routes.js.map +0 -1
  48. package/dist/admin/sessions.d.ts +0 -89
  49. package/dist/admin/sessions.js +0 -204
  50. package/dist/admin/sessions.js.map +0 -1
  51. package/dist/admin/tail.d.ts +0 -12
  52. package/dist/admin/tail.js +0 -145
  53. package/dist/admin/tail.js.map +0 -1
  54. package/dist/admin/version.d.ts +0 -22
  55. package/dist/admin/version.js +0 -92
  56. package/dist/admin/version.js.map +0 -1
  57. package/dist/cli/cli-config.js.map +0 -1
  58. package/dist/cli/cli-format.js.map +0 -1
  59. package/dist/cli/cli-server.js.map +0 -1
  60. package/dist/cli/cli-timeline.js.map +0 -1
  61. package/dist/cli/cli.js.map +0 -1
  62. package/dist/cli/fork-cli.js.map +0 -1
  63. package/dist/cli/oauth-cli.js.map +0 -1
  64. package/dist/cli/setup.js.map +0 -1
  65. package/dist/cli/skills-install.js.map +0 -1
  66. package/dist/config/config-resolve.d.ts +0 -28
  67. package/dist/config/config-resolve.js +0 -47
  68. package/dist/config/config-resolve.js.map +0 -1
  69. package/dist/config/config.d.ts +0 -157
  70. package/dist/config/config.js +0 -581
  71. package/dist/config/config.js.map +0 -1
  72. package/dist/config/core-pin.d.ts +0 -16
  73. package/dist/config/core-pin.js +0 -85
  74. package/dist/config/core-pin.js.map +0 -1
  75. package/dist/config/overlay-assets.d.ts +0 -20
  76. package/dist/config/overlay-assets.js +0 -114
  77. package/dist/config/overlay-assets.js.map +0 -1
  78. package/dist/config/overlay-bootstrap.d.ts +0 -44
  79. package/dist/config/overlay-bootstrap.js +0 -304
  80. package/dist/config/overlay-bootstrap.js.map +0 -1
  81. package/dist/connectors/github-webhook.d.ts +0 -45
  82. package/dist/connectors/github-webhook.js +0 -321
  83. package/dist/connectors/github-webhook.js.map +0 -1
  84. package/dist/connectors/index.d.ts +0 -18
  85. package/dist/connectors/index.js +0 -41
  86. package/dist/connectors/index.js.map +0 -1
  87. package/dist/connectors/messaging/base.d.ts +0 -42
  88. package/dist/connectors/messaging/base.js +0 -139
  89. package/dist/connectors/messaging/base.js.map +0 -1
  90. package/dist/connectors/messaging/delivery.d.ts +0 -14
  91. package/dist/connectors/messaging/delivery.js +0 -38
  92. package/dist/connectors/messaging/delivery.js.map +0 -1
  93. package/dist/connectors/messaging/index.d.ts +0 -4
  94. package/dist/connectors/messaging/index.js +0 -4
  95. package/dist/connectors/messaging/index.js.map +0 -1
  96. package/dist/connectors/messaging/session-manager.d.ts +0 -53
  97. package/dist/connectors/messaging/session-manager.js +0 -255
  98. package/dist/connectors/messaging/session-manager.js.map +0 -1
  99. package/dist/connectors/messaging/types.d.ts +0 -58
  100. package/dist/connectors/messaging/types.js +0 -7
  101. package/dist/connectors/messaging/types.js.map +0 -1
  102. package/dist/connectors/slack/connector.d.ts +0 -142
  103. package/dist/connectors/slack/connector.js +0 -494
  104. package/dist/connectors/slack/connector.js.map +0 -1
  105. package/dist/connectors/slack/index.d.ts +0 -2
  106. package/dist/connectors/slack/index.js +0 -3
  107. package/dist/connectors/slack/index.js.map +0 -1
  108. package/dist/connectors/slack/mrkdwn.d.ts +0 -19
  109. package/dist/connectors/slack/mrkdwn.js +0 -240
  110. package/dist/connectors/slack/mrkdwn.js.map +0 -1
  111. package/dist/connectors/types.d.ts +0 -61
  112. package/dist/connectors/types.js +0 -2
  113. package/dist/connectors/types.js.map +0 -1
  114. package/dist/cron/fanout.d.ts +0 -33
  115. package/dist/cron/fanout.js +0 -55
  116. package/dist/cron/fanout.js.map +0 -1
  117. package/dist/cron/jobs.d.ts +0 -18
  118. package/dist/cron/jobs.js +0 -37
  119. package/dist/cron/jobs.js.map +0 -1
  120. package/dist/cron/scheduler.d.ts +0 -45
  121. package/dist/cron/scheduler.js +0 -105
  122. package/dist/cron/scheduler.js.map +0 -1
  123. package/dist/engine/agent-executor.d.ts +0 -34
  124. package/dist/engine/agent-executor.js +0 -246
  125. package/dist/engine/agent-executor.js.map +0 -1
  126. package/dist/engine/chat/chat-runner.d.ts +0 -132
  127. package/dist/engine/chat/chat-runner.js +0 -424
  128. package/dist/engine/chat/chat-runner.js.map +0 -1
  129. package/dist/engine/chat/chat-skills.d.ts +0 -45
  130. package/dist/engine/chat/chat-skills.js +0 -184
  131. package/dist/engine/chat/chat-skills.js.map +0 -1
  132. package/dist/engine/chat/chat.d.ts +0 -54
  133. package/dist/engine/chat/chat.js +0 -316
  134. package/dist/engine/chat/chat.js.map +0 -1
  135. package/dist/engine/chat/message-batcher.d.ts +0 -32
  136. package/dist/engine/chat/message-batcher.js +0 -109
  137. package/dist/engine/chat/message-batcher.js.map +0 -1
  138. package/dist/engine/dispatcher.d.ts +0 -73
  139. package/dist/engine/dispatcher.js +0 -663
  140. package/dist/engine/dispatcher.js.map +0 -1
  141. package/dist/engine/event-shim.d.ts +0 -117
  142. package/dist/engine/event-shim.js +0 -435
  143. package/dist/engine/event-shim.js.map +0 -1
  144. package/dist/engine/executors/orchestrator.d.ts +0 -81
  145. package/dist/engine/executors/orchestrator.js +0 -365
  146. package/dist/engine/executors/orchestrator.js.map +0 -1
  147. package/dist/engine/executors/shared.d.ts +0 -189
  148. package/dist/engine/executors/shared.js +0 -616
  149. package/dist/engine/executors/shared.js.map +0 -1
  150. package/dist/engine/github/git-auth.d.ts +0 -62
  151. package/dist/engine/github/git-auth.js +0 -161
  152. package/dist/engine/github/git-auth.js.map +0 -1
  153. package/dist/engine/github/github-app-client.d.ts +0 -23
  154. package/dist/engine/github/github-app-client.js +0 -33
  155. package/dist/engine/github/github-app-client.js.map +0 -1
  156. package/dist/engine/github/github-tools.d.ts +0 -18
  157. package/dist/engine/github/github-tools.js +0 -263
  158. package/dist/engine/github/github-tools.js.map +0 -1
  159. package/dist/engine/github/github.d.ts +0 -1193
  160. package/dist/engine/github/github.js +0 -357
  161. package/dist/engine/github/github.js.map +0 -1
  162. package/dist/engine/github/profiles.d.ts +0 -279
  163. package/dist/engine/github/profiles.js +0 -42
  164. package/dist/engine/github/profiles.js.map +0 -1
  165. package/dist/engine/github/review-poster.d.ts +0 -98
  166. package/dist/engine/github/review-poster.js +0 -177
  167. package/dist/engine/github/review-poster.js.map +0 -1
  168. package/dist/engine/llm.d.ts +0 -79
  169. package/dist/engine/llm.js +0 -269
  170. package/dist/engine/llm.js.map +0 -1
  171. package/dist/engine/oauth.d.ts +0 -43
  172. package/dist/engine/oauth.js +0 -103
  173. package/dist/engine/oauth.js.map +0 -1
  174. package/dist/engine/router.d.ts +0 -29
  175. package/dist/engine/router.js +0 -680
  176. package/dist/engine/router.js.map +0 -1
  177. package/dist/engine/screen/classifier.d.ts +0 -93
  178. package/dist/engine/screen/classifier.js +0 -215
  179. package/dist/engine/screen/classifier.js.map +0 -1
  180. package/dist/engine/screen/screen.d.ts +0 -41
  181. package/dist/engine/screen/screen.js +0 -93
  182. package/dist/engine/screen/screen.js.map +0 -1
  183. package/dist/evals-api.d.ts +0 -24
  184. package/dist/evals-api.js +0 -22
  185. package/dist/evals-api.js.map +0 -1
  186. package/dist/index.d.ts +0 -1
  187. package/dist/index.js +0 -940
  188. package/dist/index.js.map +0 -1
  189. package/dist/managed-repos.d.ts +0 -30
  190. package/dist/managed-repos.js +0 -72
  191. package/dist/managed-repos.js.map +0 -1
  192. package/dist/notify/blocks.d.ts +0 -23
  193. package/dist/notify/blocks.js +0 -118
  194. package/dist/notify/blocks.js.map +0 -1
  195. package/dist/notify/index.d.ts +0 -17
  196. package/dist/notify/index.js +0 -7
  197. package/dist/notify/index.js.map +0 -1
  198. package/dist/notify/model.d.ts +0 -57
  199. package/dist/notify/model.js +0 -89
  200. package/dist/notify/model.js.map +0 -1
  201. package/dist/notify/notifier.d.ts +0 -18
  202. package/dist/notify/notifier.js +0 -100
  203. package/dist/notify/notifier.js.map +0 -1
  204. package/dist/notify/render.d.ts +0 -21
  205. package/dist/notify/render.js +0 -59
  206. package/dist/notify/render.js.map +0 -1
  207. package/dist/notify/transports/github.d.ts +0 -26
  208. package/dist/notify/transports/github.js +0 -27
  209. package/dist/notify/transports/github.js.map +0 -1
  210. package/dist/notify/transports/slack.d.ts +0 -27
  211. package/dist/notify/transports/slack.js +0 -37
  212. package/dist/notify/transports/slack.js.map +0 -1
  213. package/dist/notify/types.d.ts +0 -119
  214. package/dist/notify/types.js +0 -16
  215. package/dist/notify/types.js.map +0 -1
  216. package/dist/providers.d.ts +0 -125
  217. package/dist/providers.js +0 -279
  218. package/dist/providers.js.map +0 -1
  219. package/dist/sandbox/docker.d.ts +0 -180
  220. package/dist/sandbox/docker.js +0 -517
  221. package/dist/sandbox/docker.js.map +0 -1
  222. package/dist/sandbox/egress-allowlist.d.ts +0 -74
  223. package/dist/sandbox/egress-allowlist.js +0 -164
  224. package/dist/sandbox/egress-allowlist.js.map +0 -1
  225. package/dist/sandbox/egress-firewall-config.d.ts +0 -127
  226. package/dist/sandbox/egress-firewall-config.js +0 -434
  227. package/dist/sandbox/egress-firewall-config.js.map +0 -1
  228. package/dist/sandbox/images.d.ts +0 -23
  229. package/dist/sandbox/images.js +0 -55
  230. package/dist/sandbox/images.js.map +0 -1
  231. package/dist/sandbox/index.d.ts +0 -112
  232. package/dist/sandbox/index.js +0 -332
  233. package/dist/sandbox/index.js.map +0 -1
  234. package/dist/sandbox/sandbox.d.ts +0 -214
  235. package/dist/sandbox/sandbox.js +0 -453
  236. package/dist/sandbox/sandbox.js.map +0 -1
  237. package/dist/sandbox/smol.d.ts +0 -162
  238. package/dist/sandbox/smol.js +0 -485
  239. package/dist/sandbox/smol.js.map +0 -1
  240. package/dist/session-log.d.ts +0 -63
  241. package/dist/session-log.js +0 -290
  242. package/dist/session-log.js.map +0 -1
  243. package/dist/state/approval-store.d.ts +0 -88
  244. package/dist/state/approval-store.js +0 -149
  245. package/dist/state/approval-store.js.map +0 -1
  246. package/dist/state/build-assets.d.ts +0 -50
  247. package/dist/state/build-assets.js +0 -154
  248. package/dist/state/build-assets.js.map +0 -1
  249. package/dist/state/db.d.ts +0 -81
  250. package/dist/state/db.js +0 -155
  251. package/dist/state/db.js.map +0 -1
  252. package/dist/state/execution-store.d.ts +0 -257
  253. package/dist/state/execution-store.js +0 -527
  254. package/dist/state/execution-store.js.map +0 -1
  255. package/dist/state/migrate.d.ts +0 -15
  256. package/dist/state/migrate.js +0 -175
  257. package/dist/state/migrate.js.map +0 -1
  258. package/dist/state/workflow-run-store.d.ts +0 -216
  259. package/dist/state/workflow-run-store.js +0 -403
  260. package/dist/state/workflow-run-store.js.map +0 -1
  261. package/dist/telemetry/index.d.ts +0 -38
  262. package/dist/telemetry/index.js +0 -253
  263. package/dist/telemetry/index.js.map +0 -1
  264. package/dist/telemetry/pi-events.d.ts +0 -12
  265. package/dist/telemetry/pi-events.js +0 -134
  266. package/dist/telemetry/pi-events.js.map +0 -1
  267. package/dist/workflows/dag.d.ts +0 -37
  268. package/dist/workflows/dag.js +0 -118
  269. package/dist/workflows/dag.js.map +0 -1
  270. package/dist/workflows/loader.d.ts +0 -53
  271. package/dist/workflows/loader.js +0 -421
  272. package/dist/workflows/loader.js.map +0 -1
  273. package/dist/workflows/loop-eval.d.ts +0 -28
  274. package/dist/workflows/loop-eval.js +0 -74
  275. package/dist/workflows/loop-eval.js.map +0 -1
  276. package/dist/workflows/phase-executor.d.ts +0 -222
  277. package/dist/workflows/phase-executor.js +0 -1153
  278. package/dist/workflows/phase-executor.js.map +0 -1
  279. package/dist/workflows/phase-ref.d.ts +0 -50
  280. package/dist/workflows/phase-ref.js +0 -91
  281. package/dist/workflows/phase-ref.js.map +0 -1
  282. package/dist/workflows/resume.d.ts +0 -58
  283. package/dist/workflows/resume.js +0 -313
  284. package/dist/workflows/resume.js.map +0 -1
  285. package/dist/workflows/runner.d.ts +0 -63
  286. package/dist/workflows/runner.js +0 -381
  287. package/dist/workflows/runner.js.map +0 -1
  288. package/dist/workflows/schema.d.ts +0 -344
  289. package/dist/workflows/schema.js +0 -460
  290. package/dist/workflows/schema.js.map +0 -1
  291. package/dist/workflows/simple.d.ts +0 -90
  292. package/dist/workflows/simple.js +0 -393
  293. package/dist/workflows/simple.js.map +0 -1
  294. package/dist/workflows/target-policy.d.ts +0 -74
  295. package/dist/workflows/target-policy.js +0 -80
  296. package/dist/workflows/target-policy.js.map +0 -1
  297. package/dist/workflows/templates.d.ts +0 -88
  298. package/dist/workflows/templates.js +0 -127
  299. package/dist/workflows/templates.js.map +0 -1
  300. package/dist/workflows/triggers.d.ts +0 -21
  301. package/dist/workflows/triggers.js +0 -45
  302. package/dist/workflows/triggers.js.map +0 -1
  303. package/dist/workflows/verdict.d.ts +0 -19
  304. package/dist/workflows/verdict.js +0 -30
  305. package/dist/workflows/verdict.js.map +0 -1
  306. package/dist/worktree/manager.d.ts +0 -43
  307. package/dist/worktree/manager.js +0 -127
  308. package/dist/worktree/manager.js.map +0 -1
  309. package/docker-compose.yml +0 -274
  310. package/sandbox.Dockerfile +0 -68
  311. package/skills/README.md +0 -42
  312. package/skills/browser-qa/SKILL.md +0 -208
  313. package/skills/browser-qa/scripts/agent-browser.mjs +0 -531
  314. package/skills/building/SKILL.md +0 -95
  315. package/skills/chat/SKILL.md +0 -36
  316. package/skills/code-review/SKILL.md +0 -82
  317. package/skills/debug-production/SKILL.md +0 -108
  318. package/skills/demo/SKILL.md +0 -228
  319. package/skills/demo/scripts/compose-demo.sh +0 -243
  320. package/skills/issue-answer/SKILL.md +0 -91
  321. package/skills/issue-comment/SKILL.md +0 -49
  322. package/skills/issue-triage/SKILL.md +0 -149
  323. package/skills/issue-triage/references/AGENT-BRIEF.md +0 -65
  324. package/skills/pr-comment/SKILL.md +0 -64
  325. package/skills/pr-review/SKILL.md +0 -148
  326. package/skills/pr-review/references/findings-schema.md +0 -89
  327. package/skills/qa-test/SKILL.md +0 -97
  328. package/skills/repo-health/SKILL.md +0 -67
  329. package/skills/security-feedback/SKILL.md +0 -173
  330. package/skills/security-feedback/references/templates.md +0 -77
  331. package/skills/security-review/SKILL.md +0 -215
  332. package/skills/security-review/references/issue-format.md +0 -302
  333. package/skills/verify/SKILL.md +0 -118
  334. package/workflows/answer.yaml +0 -44
  335. package/workflows/build.yaml +0 -129
  336. package/workflows/cron-health.yaml +0 -6
  337. package/workflows/cron-review.yaml +0 -8
  338. package/workflows/cron-security.yaml +0 -6
  339. package/workflows/cron-triage.yaml +0 -8
  340. package/workflows/demo.yaml +0 -62
  341. package/workflows/examples/bash-smoke.yaml +0 -41
  342. package/workflows/examples/parallel-review.yaml +0 -36
  343. package/workflows/examples/tdd-loop.yaml +0 -17
  344. package/workflows/explore.yaml +0 -80
  345. package/workflows/issue-comment.yaml +0 -15
  346. package/workflows/issue-triage.yaml +0 -22
  347. package/workflows/pr-comment.yaml +0 -16
  348. package/workflows/pr-fix.yaml +0 -17
  349. package/workflows/pr-review.yaml +0 -50
  350. package/workflows/prompts/answer.md +0 -68
  351. package/workflows/prompts/architect.md +0 -44
  352. package/workflows/prompts/classifier.md +0 -60
  353. package/workflows/prompts/classify-adds-info.md +0 -17
  354. package/workflows/prompts/demo.md +0 -125
  355. package/workflows/prompts/executor.md +0 -42
  356. package/workflows/prompts/explore-ask.md +0 -99
  357. package/workflows/prompts/explore-publish.md +0 -31
  358. package/workflows/prompts/explore-read.md +0 -120
  359. package/workflows/prompts/explore-synthesize.md +0 -88
  360. package/workflows/prompts/fix.md +0 -18
  361. package/workflows/prompts/guardrails.md +0 -96
  362. package/workflows/prompts/pr-fix.md +0 -26
  363. package/workflows/prompts/pr.md +0 -39
  364. package/workflows/prompts/qa-browser.md +0 -95
  365. package/workflows/prompts/qa-synth.md +0 -44
  366. package/workflows/prompts/qa-test.md +0 -60
  367. package/workflows/prompts/re-reviewer.md +0 -33
  368. package/workflows/prompts/reviewer.md +0 -60
  369. package/workflows/prompts/verify-browser.md +0 -93
  370. package/workflows/prompts/verify-synth.md +0 -44
  371. package/workflows/prompts/verify.md +0 -63
  372. package/workflows/qa-test.yaml +0 -94
  373. package/workflows/repo-health.yaml +0 -15
  374. package/workflows/security-feedback.yaml +0 -25
  375. package/workflows/security-review.yaml +0 -37
  376. package/workflows/verify.yaml +0 -93
  377. /package/dist/{cli/cli-config.d.ts → cli-config.d.ts} +0 -0
  378. /package/dist/{cli/cli-config.js → cli-config.js} +0 -0
  379. /package/dist/{cli/cli-format.d.ts → cli-format.d.ts} +0 -0
  380. /package/dist/{cli/cli-format.js → cli-format.js} +0 -0
  381. /package/dist/{cli/cli-timeline.d.ts → cli-timeline.d.ts} +0 -0
  382. /package/dist/{cli/cli-timeline.js → cli-timeline.js} +0 -0
  383. /package/dist/{cli/cli.d.ts → cli.d.ts} +0 -0
  384. /package/dist/{cli/oauth-cli.d.ts → oauth-cli.d.ts} +0 -0
  385. /package/dist/{cli/setup.d.ts → setup.d.ts} +0 -0
package/dist/index.js DELETED
@@ -1,940 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync } from "fs";
2
- import { resolve } from "path";
3
- import { randomUUID } from "crypto";
4
- import { Hono } from "hono";
5
- import { serve } from "@hono/node-server";
6
- import { loadConfig, resolveModel, resolveVariant, resolveGithubAuth } from "./config/config.js";
7
- import { ConnectorRegistry, GitHubWebhookConnector, SlackConnector, SessionManager, MessageDeliveryService } from "./connectors/index.js";
8
- import { dispatch } from "./engine/dispatcher.js";
9
- import { MessageBatcher } from "./engine/chat/message-batcher.js";
10
- import { chatSystemSuffix, handleChatMessage, loadAgentContext } from "./engine/chat/chat.js";
11
- import { configureWorkflowAssets, validateAssets, getWorkflow } from "./workflows/loader.js";
12
- import { ChatRunner } from "./engine/chat/chat-runner.js";
13
- import { buildReadSkillTool, loadChatSkillCatalogue } from "./engine/chat/chat-skills.js";
14
- import { configureGitAuth } from "./engine/github/git-auth.js";
15
- import { StateDb } from "./state/db.js";
16
- import { CronScheduler } from "./cron/scheduler.js";
17
- import { getJobs } from "./cron/jobs.js";
18
- import { dispatchCronWorkflow } from "./cron/fanout.js";
19
- import { mountAdmin } from "./admin/index.js";
20
- import { cleanupOrphanedSandboxes } from "./sandbox/index.js";
21
- import { writeEgressFirewallConfigs, writeOtelCollectorConfig } from "./sandbox/egress-firewall-config.js";
22
- import { initTelemetry, shutdownTelemetry } from "./telemetry/index.js";
23
- import { authMiddleware, authIsEnabled } from "./admin/auth.js";
24
- import { GitHubClient } from "./engine/github/github.js";
25
- import { setInstallationRepos } from "./managed-repos.js";
26
- import { screenForInjection, flagPrefix } from "./engine/screen/screen.js";
27
- import { runSimpleWorkflow, PR_HEADREF_PREPOPULATE_WORKFLOWS } from "./workflows/simple.js";
28
- import { resumeOrphanedWorkflows, resumeSimpleRun } from "./workflows/resume.js";
29
- import { ProgressNotifier, GitHubTransport, SlackTransport, } from "./notify/index.js";
30
- /**
31
- * Pre-flight validation — checks that config is sane before starting any
32
- * services. Exits with code 78 (EX_CONFIG) on configuration errors so
33
- * Docker's restart policy doesn't loop forever on a misconfigured container.
34
- */
35
- function validateConfig(config) {
36
- const fatal = (msg) => {
37
- console.error(`\n[startup] FATAL: ${msg}`);
38
- console.error("[startup] Fix your .env and restart.\n");
39
- process.exit(78); // EX_CONFIG — sysexits.h convention
40
- };
41
- if (config.githubApp) {
42
- const { appId, privateKeyPath, installationId } = config.githubApp;
43
- if (!appId || !installationId) {
44
- fatal("GITHUB_APP_ID and GITHUB_APP_INSTALLATION_ID are required when the GitHub App is configured.");
45
- }
46
- if (!existsSync(resolve(privateKeyPath))) {
47
- fatal(`GITHUB_APP_PRIVATE_KEY_PATH points to "${privateKeyPath}" which does not exist.`);
48
- }
49
- try {
50
- const content = readFileSync(resolve(privateKeyPath), "utf8");
51
- if (!content.startsWith("-----BEGIN")) {
52
- fatal(`GITHUB_APP_PRIVATE_KEY_PATH ("${privateKeyPath}") does not look like a PEM file.`);
53
- }
54
- }
55
- catch (err) {
56
- fatal(`Cannot read GITHUB_APP_PRIVATE_KEY_PATH ("${privateKeyPath}"): ${err.message}`);
57
- }
58
- }
59
- if (!config.webhookSecret && config.githubApp) {
60
- console.warn("[startup] WEBHOOK_SECRET is not set — webhook signature verification is disabled.");
61
- }
62
- }
63
- async function main() {
64
- console.log("Last Light v2.0 — Agent SDK Harness");
65
- console.log("====================================");
66
- // Load and validate config + overlay assets before starting anything. These
67
- // throw on a broken/empty overlay, a cron targeting a missing workflow, or a
68
- // phase whose prompt/skill can't resolve — all unfixable by a restart, so we
69
- // exit 78 (EX_CONFIG) to stop Docker's restart policy from looping.
70
- let config;
71
- try {
72
- config = loadConfig();
73
- configureWorkflowAssets({
74
- builtInRoot: config.builtInRoot,
75
- overlayRoot: config.overlayDir,
76
- disabled: config.disabled,
77
- });
78
- validateAssets(config.routes);
79
- }
80
- catch (err) {
81
- console.error(`\n[startup] FATAL: ${err.message}`);
82
- console.error("[startup] Fix your config/overlay and restart.\n");
83
- process.exit(78); // EX_CONFIG — sysexits.h convention
84
- }
85
- validateConfig(config);
86
- const packageJson = JSON.parse(readFileSync(resolve("package.json"), "utf8"));
87
- await initTelemetry(config.otel, { packageVersion: packageJson.version });
88
- let telemetryShutdownStarted = false;
89
- console.log(config.otel.enabled
90
- ? `[otel] enabled service=${config.otel.serviceName} forwardToSandbox=${config.otel.forwardToSandbox} includeContent=${config.otel.includeContent}`
91
- : "[otel] disabled");
92
- console.log(`[config] Port: ${config.port}, Model: ${config.model}`);
93
- const modelOverrides = Object.entries(config.models).filter(([k]) => k !== "default");
94
- if (modelOverrides.length > 0) {
95
- console.log(`[config] Model overrides: ${modelOverrides.map(([k, v]) => `${k}=${v}`).join(", ")}`);
96
- }
97
- // Clean up any sandbox containers left over from a previous run
98
- cleanupOrphanedSandboxes();
99
- // Ensure state directory structure exists (mountable as Docker volume)
100
- for (const sub of ["sessions", "logs", "sandboxes"]) {
101
- mkdirSync(resolve(config.stateDir, sub), { recursive: true });
102
- }
103
- console.log(`[state] State dir: ${config.stateDir}`);
104
- console.log(`[state] Sessions dir: ${config.sessionsDir}`);
105
- console.log(`[config] Sandbox backend: ${config.sandbox}`);
106
- // Regenerate egress firewall configs (nginx ssl_preread + coredns) from
107
- // the allowlist source of truth. Only meaningful for the docker backend;
108
- // cheap enough to do unconditionally so a backend switch doesn't leave
109
- // stale configs on disk. The docker backend forwards sandbox telemetry
110
- // through the in-network OTEL collector (reached by IP), so the strict
111
- // SNI allowlist no longer needs collector hosts — that hop happens on
112
- // the collector's trusted outbound leg, not through the firewall.
113
- const proxyDir = writeEgressFirewallConfigs(config.stateDir);
114
- console.log(`[state] Egress firewall configs: ${proxyDir}`);
115
- // Generate the in-network OTEL collector config (docker backend). Derived
116
- // from the harness's OTEL_* backend env so the collector re-exports to the
117
- // same backend the harness uses — with auth headers that stay host-side.
118
- // Forwarding is gated on telemetry being active: when disabled (or sandbox
119
- // forwarding off) the collector renders an inert debug-only config so the
120
- // static collector IP can't be used as a sandbox exfil path.
121
- const collectorConfigPath = writeOtelCollectorConfig(config.stateDir, {
122
- active: config.otel.enabled && config.otel.forwardToSandbox,
123
- });
124
- console.log(`[state] OTEL collector config: ${collectorConfigPath} (forwarding ${config.otel.enabled && config.otel.forwardToSandbox ? "active" : "disabled"})`);
125
- // Initialize state database first — ChatRunner needs SessionManager
126
- // (DB-backed) at construction time.
127
- const db = new StateDb(config.dbPath);
128
- console.log(`[state] Database: ${config.dbPath}`);
129
- // Session manager for messaging connectors (shared across Slack, Discord, etc.)
130
- const sessionManager = new SessionManager(db.database);
131
- // In-process chat runner backs the messaging chat skill. One pi-ai
132
- // conversation per Slack/Discord thread; locked down to read-only
133
- // GitHub tools plus a `read_skill` tool that exposes the curated
134
- // chat skill catalogue. Skills are listed in the system prompt via
135
- // an XML <available_skills> block; the agent pulls full SKILL.md on
136
- // demand — same progressive-disclosure model the sandbox phases use.
137
- const chatSkills = loadChatSkillCatalogue();
138
- const readSkill = buildReadSkillTool(chatSkills.skills);
139
- // Resolve GitHub auth once (App wins; PAT fallback; else none). Drives the
140
- // chat GitHub tools, the harness client, and the chat prompt's tool section.
141
- const githubAuth = resolveGithubAuth(config);
142
- const chatGithubAuth = githubAuth?.kind === "app"
143
- ? {
144
- appId: githubAuth.appId,
145
- privateKeyPath: githubAuth.privateKeyPath,
146
- installationId: githubAuth.installationId,
147
- }
148
- : githubAuth?.kind === "token"
149
- ? { token: githubAuth.token }
150
- : undefined;
151
- const chatRunner = new ChatRunner({
152
- model: resolveModel(config.models, "chat"),
153
- thinking: resolveVariant(config.variants, "chat"),
154
- systemPrompt: loadAgentContext() +
155
- chatSystemSuffix(githubAuth !== undefined) +
156
- chatSkills.catalogueXml,
157
- github: chatGithubAuth,
158
- extraTools: chatSkills.skills.length > 0
159
- ? { tools: [readSkill.tool], execute: readSkill.execute }
160
- : undefined,
161
- }, sessionManager);
162
- if (chatSkills.skills.length > 0) {
163
- console.log(`[chat] Loaded ${chatSkills.skills.length} skill(s): ${chatSkills.skills.map((s) => s.name).join(", ")}`);
164
- }
165
- else {
166
- console.warn("[chat] No skills loaded — frontmatter missing or no matching SKILL.md found");
167
- }
168
- // Configure git with GitHub App credentials — agents can git clone/push natively.
169
- // Non-fatal: the token is refreshed before each agent execution anyway, so a
170
- // transient failure here (DNS, rate limit) doesn't block startup.
171
- if (config.githubApp) {
172
- try {
173
- await configureGitAuth({
174
- appId: config.githubApp.appId,
175
- privateKeyPath: config.githubApp.privateKeyPath,
176
- installationId: config.githubApp.installationId,
177
- botLogin: config.botLogin,
178
- });
179
- }
180
- catch (err) {
181
- console.warn(`[git-auth] Initial token mint failed (will retry per-execution): ${err.message}`);
182
- }
183
- }
184
- // GitHub API client for harness-level operations (posting comments, fetching
185
- // issues). App auth when configured; otherwise a PAT (read-only unless the
186
- // token carries write scope); null in chat-only mode.
187
- const github = config.githubApp
188
- ? new GitHubClient(config.githubApp)
189
- : config.githubToken
190
- ? GitHubClient.withToken(config.githubToken)
191
- : null;
192
- // Discover the repos the App installation can access and seed the managed-repo
193
- // list. When the overlay's `managedRepos` is empty this becomes the effective
194
- // allowlist (getManagedRepos falls back to it); a configured list still wins.
195
- // Kept live afterwards by installation webhooks (github-webhook.ts). Non-fatal:
196
- // on failure we fall back to whatever `managedRepos` config provides. Runs
197
- // before the HTTP listener opens, so the list is warm before the first event.
198
- if (github && config.githubApp) {
199
- try {
200
- const repos = await github.listInstallationRepos();
201
- setInstallationRepos(repos);
202
- console.log(`[github] Discovered ${repos.length} installation repos`);
203
- }
204
- catch (err) {
205
- console.warn(`[github] Installation repo discovery failed: ${err.message}`);
206
- }
207
- }
208
- /**
209
- * Dispatch a workflow by name. Used by webhook events, cron jobs, and the
210
- * /api/run endpoint. Every dispatch creates a workflow_run row visible in
211
- * the dashboard, regardless of whether it's a single-phase workflow (like
212
- * issue-triage) or a multi-phase one.
213
- *
214
- * The router still uses skill names for backwards compat — for the four
215
- * agent skills they're 1:1 with workflow names.
216
- */
217
- const dispatchWorkflow = async (workflowName, context, onRunStart) => {
218
- // Slack-initiated workflows (explore, /explore) carry a
219
- // `slack:{team}:{channel}:{thread}` triggerId and don't require a
220
- // managed `repo` — their postComment goes back to the Slack thread.
221
- const slackTriggerId = typeof context.triggerId === "string" && context.triggerId.startsWith("slack:")
222
- ? context.triggerId
223
- : undefined;
224
- const repoStr = context.repo;
225
- if (!repoStr && !slackTriggerId) {
226
- const msg = `dispatchWorkflow(${workflowName}): missing 'repo' in context`;
227
- console.error(`[dispatch] ${msg}`);
228
- return { success: false, error: msg };
229
- }
230
- const [owner, repo] = repoStr && repoStr.includes("/")
231
- ? repoStr.split("/")
232
- : repoStr
233
- ? ["", repoStr]
234
- : ["", ""];
235
- if (repoStr && (!owner || !repo)) {
236
- const msg = `dispatchWorkflow(${workflowName}): invalid repo format '${repoStr}'`;
237
- console.error(`[dispatch] ${msg}`);
238
- return { success: false, error: msg };
239
- }
240
- // Pluck the standard fields, leave the rest in `extra` for the workflow
241
- // template to consume.
242
- const { _triggerType, repo: _r, issueNumber, prNumber, title, body, labels, sender, commentBody, triggerId: _triggerId, channelId, threadId, prePopulateBranch: ctxPrePopulateBranch, branch: ctxBranch, ...rest } = context;
243
- // Preserve channelId/threadId in extra so they're stored on the
244
- // workflow run context — needed by boot-time resume to rebuild the
245
- // Slack postComment callback after a harness restart.
246
- const extra = { ...rest };
247
- if (typeof channelId === "string")
248
- extra.channelId = channelId;
249
- if (typeof threadId === "string")
250
- extra.threadId = threadId;
251
- // For PR-scoped read workflows, resolve the PR head ref and ask the
252
- // sandbox to pre-clone the repo at that branch. The agent then enters
253
- // a workspace that's already a checkout of the PR's actual code —
254
- // saves a redundant clone_repo MCP call inside the session.
255
- //
256
- // pr-fix already plumbs `branch` through context (line ~709 below)
257
- // because the architect/executor need the branch name to push to;
258
- // we honor that here as the pre-populate branch too.
259
- let prePopulateBranch = typeof ctxPrePopulateBranch === "string" ? ctxPrePopulateBranch : undefined;
260
- if (!prePopulateBranch && typeof ctxBranch === "string" && ctxBranch && workflowName === "pr-fix") {
261
- prePopulateBranch = ctxBranch;
262
- }
263
- // PR-scoped read workflows that benefit from a workspace pre-checked-out at
264
- // the PR's *real* head ref. Each one synthesizes a `lastlight/N-<title-slug>`
265
- // branch (see resolveRunBranch) that does NOT exist on the remote, so
266
- // prePopulateWorkspace's missing-branch fallback silently clones the
267
- // *default* branch — testing/demoing code that lacks the PR's changes
268
- // (a false-negative QA, or a before/after demo whose "after" matches
269
- // "before"). Resolving the head ref here pins the workspace to the actual
270
- // PR code. See `PR_HEADREF_PREPOPULATE_WORKFLOWS` for the per-workflow why.
271
- if (!prePopulateBranch &&
272
- PR_HEADREF_PREPOPULATE_WORKFLOWS.has(workflowName) &&
273
- typeof prNumber === "number" &&
274
- github &&
275
- owner &&
276
- repo) {
277
- try {
278
- const pr = await github.getPullRequest(owner, repo, prNumber);
279
- prePopulateBranch = pr.head.ref;
280
- // Surface the base ref so a before/after demo can fetch + check out the
281
- // baseline (the read-only pre-clone is shallow + single-branch at the
282
- // head ref, so `origin/<base>` isn't present until the agent fetches it).
283
- if (pr.base?.ref)
284
- extra.baseBranch = pr.base.ref;
285
- console.log(`[dispatch] ${workflowName}: pre-populating workspace at ${owner}/${repo}@${prePopulateBranch} ` +
286
- `(base ${pr.base?.ref ?? "?"})`);
287
- }
288
- catch (err) {
289
- const msg = err instanceof Error ? err.message : String(err);
290
- console.warn(`[dispatch] ${workflowName}: could not resolve PR head ref (${msg}); ` +
291
- `agent will need to clone via MCP`);
292
- }
293
- }
294
- // Base branch for scoping. PR-triggered runs set it from the PR's base ref
295
- // above; build/issue-triggered runs have no PR, so resolve the repo's real
296
- // default branch here. Without this, everything that diffs against the base
297
- // — notably the reviewer prompt's `git ... {{baseBranch}}..HEAD` — assumes
298
- // `main` and breaks on a `master`-default (or otherwise non-`main`) repo.
299
- // Best-effort: on failure fall back to `main` so the template still renders
300
- // a valid ref rather than an empty `..HEAD`.
301
- if (!extra.baseBranch && github && owner && repo) {
302
- try {
303
- extra.baseBranch = await github.getDefaultBranch(owner, repo);
304
- }
305
- catch (err) {
306
- const msg = err instanceof Error ? err.message : String(err);
307
- console.warn(`[dispatch] ${workflowName}: could not resolve default branch for ${owner}/${repo} ` +
308
- `(${msg}); assuming main`);
309
- extra.baseBranch = "main";
310
- }
311
- }
312
- const request = {
313
- owner,
314
- repo,
315
- issueNumber: typeof issueNumber === "number" ? issueNumber : undefined,
316
- prNumber: typeof prNumber === "number" ? prNumber : undefined,
317
- issueTitle: typeof title === "string" ? title : "",
318
- issueBody: typeof body === "string" ? body : "",
319
- issueLabels: Array.isArray(labels) ? labels : undefined,
320
- commentBody: typeof commentBody === "string" ? commentBody : undefined,
321
- sender: typeof sender === "string" ? sender : "unknown",
322
- triggerId: slackTriggerId,
323
- extra,
324
- prePopulateBranch,
325
- };
326
- // For workflows where the architect/agent needs to see the full issue
327
- // history (e.g. a build greenlit by "@last-light lets build this!" needs
328
- // the spec the explore phase wrote in earlier comments), fetch the real
329
- // issue body and the comment thread, combine them into a single context
330
- // blob, and screen the combined text in ONE SDK call.
331
- //
332
- // Why single-shot: screening per-comment fans out N concurrent SDK calls,
333
- // and on a busy issue (16+ comments) that exhausts memory and trips the
334
- // EventEmitter listener cap. The combined-context approach keeps screen
335
- // cost at exactly one haiku call regardless of thread length.
336
- //
337
- // For comment-triggered builds the envelope's `body` field is the
338
- // triggering comment, not the issue body — we explicitly fetch the
339
- // real issue body here so the architect sees both the spec (issue body
340
- // + thread) and the trigger (commentBody) cleanly separated.
341
- const ENRICH_WORKFLOWS = new Set(["build", "pr-fix", "explore"]);
342
- if (github &&
343
- ENRICH_WORKFLOWS.has(workflowName) &&
344
- request.issueNumber &&
345
- owner && repo) {
346
- try {
347
- const [trueIssueBody, comments] = await Promise.all([
348
- github.getIssueBody(owner, repo, request.issueNumber),
349
- github.listIssueComments(owner, repo, request.issueNumber),
350
- ]);
351
- const formattedComments = comments
352
- .filter((c) => c.body.trim())
353
- .map((c) => `--- @${c.user} (${c.createdAt}) ---\n${c.body}`)
354
- .join("\n\n");
355
- const combinedContext = [
356
- trueIssueBody ? `# Issue body\n\n${trueIssueBody}` : "",
357
- formattedComments ? `# Issue thread (oldest → newest)\n\n${formattedComments}` : "",
358
- ]
359
- .filter(Boolean)
360
- .join("\n\n");
361
- if (combinedContext) {
362
- // Single screening call over the entire combined context.
363
- const screen = await screenForInjection(combinedContext);
364
- const annotated = screen.flagged
365
- ? `${flagPrefix(screen.reason)}${combinedContext}`
366
- : combinedContext;
367
- (request.extra ||= {}).combinedContext = annotated;
368
- // Clear individual issueBody so simple.ts uses combinedContext
369
- // exclusively (avoids double-rendering the body).
370
- request.issueBody = "";
371
- if (screen.flagged) {
372
- console.warn(`[dispatch] Screener flagged combined issue context for ${owner}/${repo}#${request.issueNumber}: ${screen.reason || "no reason"}`);
373
- }
374
- }
375
- }
376
- catch (err) {
377
- const m = err instanceof Error ? err.message : String(err);
378
- console.warn(`[dispatch] Failed to fetch/screen issue context: ${m}`);
379
- // Non-fatal — workflow proceeds with whatever context the envelope had.
380
- }
381
- }
382
- const slackPost = slackTriggerId && slackConnector && typeof channelId === "string" && typeof threadId === "string"
383
- ? async (msg) => {
384
- try {
385
- await slackConnector.sendMessage(channelId, threadId, msg);
386
- }
387
- catch (err) {
388
- const m = err instanceof Error ? err.message : String(err);
389
- console.warn(`[dispatch] Failed to post to Slack thread: ${m}`);
390
- }
391
- }
392
- : undefined;
393
- // In-place "task list" progress checklist — opt-in per workflow via
394
- // `status_checklist: true` in the YAML. Build a transport for whichever
395
- // surface triggered the run (GitHub comment and/or Slack thread) and hand
396
- // the runner a ProgressNotifier instead of letting it post a comment per
397
- // phase. The notifier is created inside onRunStart because it needs the
398
- // workflow-run id (only known once simple.ts creates the row) to persist
399
- // its in-place update handles to scratch.notifier. better-sqlite3 is
400
- // synchronous and simple.ts invokes onRunStart synchronously before the
401
- // first reporter call, so the notifier is ready in time; the proxy guards
402
- // the brief window before assignment.
403
- // Which in-place surface(s) can the checklist edit? Knowable synchronously
404
- // (transport existence needs only github/issue or slack/channel/thread —
405
- // the run id is needed solely for persistence + resume handles).
406
- const ghChecklist = !!(github && typeof issueNumber === "number");
407
- const slackChecklist = !!(slackConnector && typeof channelId === "string" && typeof threadId === "string");
408
- let statusChecklist = false;
409
- try {
410
- // Only activate the checklist when the workflow opts in AND there's a
411
- // surface to render it on — otherwise leave `reporter` undefined so the
412
- // runner keeps its legacy per-phase comment behavior instead of going
413
- // silent.
414
- statusChecklist =
415
- getWorkflow(workflowName).status_checklist === true && (ghChecklist || slackChecklist);
416
- }
417
- catch {
418
- /* unknown workflow — surfaced downstream by runSimpleWorkflow */
419
- }
420
- let notifier;
421
- const reporterProxy = statusChecklist
422
- ? {
423
- start: (m) => notifier?.start(m) ?? Promise.resolve(),
424
- step: (k, s, d) => notifier?.step(k, s, d) ?? Promise.resolve(),
425
- insertStep: (st, b) => notifier?.insertStep(st, b) ?? Promise.resolve(),
426
- note: (m) => notifier?.note(m) ?? Promise.resolve(),
427
- noteApproval: (m, meta) => notifier?.noteApproval(m, meta) ?? Promise.resolve(),
428
- footer: (m) => notifier?.footer(m) ?? Promise.resolve(),
429
- noteTerminal: (m) => notifier?.noteTerminal(m) ?? Promise.resolve(),
430
- }
431
- : undefined;
432
- const notifierOnRunStart = statusChecklist
433
- ? (runId) => {
434
- try {
435
- const saved = ((db.runs.getRun(runId)?.scratch?.notifier) ?? {});
436
- const persist = (patch) => {
437
- const cur = ((db.runs.getRun(runId)?.scratch?.notifier) ?? {});
438
- db.runs.mergeScratch(runId, { notifier: { ...cur, ...patch } });
439
- };
440
- const transports = [];
441
- if (ghChecklist && github && typeof issueNumber === "number") {
442
- transports.push(new GitHubTransport({
443
- github,
444
- owner,
445
- repo,
446
- issueNumber,
447
- commentId: saved.githubCommentId,
448
- save: (id) => persist({ githubCommentId: id }),
449
- }));
450
- }
451
- if (slackChecklist && slackConnector && typeof channelId === "string" && typeof threadId === "string") {
452
- transports.push(new SlackTransport({
453
- slack: slackConnector,
454
- channel: channelId,
455
- thread: threadId,
456
- ts: saved.slackTs,
457
- save: (ts) => persist({ slackTs: ts, slackChannel: channelId, slackThread: threadId }),
458
- }));
459
- }
460
- if (transports.length > 0)
461
- notifier = new ProgressNotifier(transports);
462
- }
463
- catch (err) {
464
- const m = err instanceof Error ? err.message : String(err);
465
- console.warn(`[dispatch] notifier setup failed: ${m}`);
466
- }
467
- }
468
- : undefined;
469
- const callbacks = {
470
- reporter: reporterProxy,
471
- publicUrl: config.publicUrl,
472
- postComment: slackPost
473
- ?? (github && issueNumber
474
- ? async (msg) => {
475
- try {
476
- await github.postComment(owner, repo, issueNumber, msg);
477
- }
478
- catch (err) {
479
- const m = err instanceof Error ? err.message : String(err);
480
- console.warn(`[dispatch] Failed to post comment: ${m}`);
481
- }
482
- }
483
- : undefined),
484
- onPhaseStart: async (phase) => {
485
- console.log(`[dispatch] ▶ ${workflowName}/${phase}`);
486
- // Refresh the Slack thinking indicator so long-running phases
487
- // don't leave the thread looking dead. threadId doubles as both
488
- // the message anchor and the thread root for DM threads.
489
- if (slackPost && slackConnector && typeof channelId === "string" && typeof threadId === "string") {
490
- slackConnector.showTyping(channelId, threadId, threadId).catch(() => { });
491
- }
492
- },
493
- onPhaseEnd: async (phase, result) => console.log(`[dispatch] ◀ ${workflowName}/${phase}: ${result.success ? "OK" : "FAILED"}`),
494
- onRunStart: notifierOnRunStart
495
- ? async (runId) => {
496
- // Synchronous notifier setup must finish before simple.ts calls
497
- // reporter.start() (the next statement after it invokes this), so
498
- // run it first, then chain any caller-provided onRunStart.
499
- notifierOnRunStart(runId);
500
- if (onRunStart)
501
- await onRunStart(runId);
502
- }
503
- : onRunStart,
504
- };
505
- try {
506
- const result = await runSimpleWorkflow(workflowName, request, {
507
- model: config.model,
508
- maxTurns: config.maxTurns,
509
- stateDir: config.stateDir,
510
- sandboxDir: config.sandboxDir,
511
- sessionsDir: config.sessionsDir,
512
- sandbox: config.sandbox,
513
- buildAssets: config.buildAssets,
514
- buildAssetsDir: config.buildAssetsDir,
515
- otel: config.otel,
516
- }, callbacks, db, config.models, config.approval, config.bootstrapLabel, config.variants);
517
- const summary = result.phases.map((p) => `${p.phase}=${p.success ? "ok" : "fail"}`).join(", ");
518
- if (result.paused) {
519
- console.log(`[dispatch] ${workflowName} paused (${summary})`);
520
- }
521
- else if (result.success) {
522
- console.log(`[dispatch] ${workflowName} completed (${summary})`);
523
- }
524
- else {
525
- console.warn(`[dispatch] ${workflowName} failed (${summary})`);
526
- }
527
- return { success: result.success, paused: result.paused };
528
- }
529
- catch (err) {
530
- const msg = err instanceof Error ? err.message : String(err);
531
- console.error(`[dispatch] ${workflowName} threw: ${msg}`);
532
- return { success: false, error: msg };
533
- }
534
- };
535
- // Set up connector registry
536
- const registry = new ConnectorRegistry();
537
- // Message delivery service for cron output
538
- const delivery = new MessageDeliveryService();
539
- // Shared HTTP server — always boots, independent of GitHub. `main()` owns the
540
- // Hono app + serve() lifecycle that the webhook connector used to own, so the
541
- // `lastlight` CLI + admin dashboard + /api/* work even with no GitHub App
542
- // (chat-only / PAT modes). The root /health is what the CLI hits
543
- // (src/cli/cli.ts, cli-server.ts).
544
- const app = new Hono();
545
- app.get("/health", (c) => c.json({ status: "ok" }));
546
- // GitHub webhook connector (optional — requires both webhook secret and GitHub
547
- // App). It registers /webhooks/github onto the shared app; it no longer owns
548
- // the HTTP listener.
549
- let githubConnector = null;
550
- if (config.webhookSecret && config.githubApp) {
551
- githubConnector = new GitHubWebhookConnector({
552
- port: config.port,
553
- webhookSecret: config.webhookSecret,
554
- botLogin: config.botLogin,
555
- app,
556
- });
557
- registry.register(githubConnector);
558
- }
559
- // Slack connector (optional — only if SLACK_BOT_TOKEN is set)
560
- let slackConnector = null;
561
- if (config.slack) {
562
- slackConnector = new SlackConnector({
563
- botToken: config.slack.botToken,
564
- mode: config.slack.mode,
565
- appToken: config.slack.appToken,
566
- signingSecret: config.slack.signingSecret,
567
- // Webhook mode mounts /webhooks/slack on the shared HTTP server.
568
- honoApp: app,
569
- allowedUsers: config.slack.allowedUsers,
570
- deliveryChannel: config.slack.deliveryChannel,
571
- botIdentifier: "", // Will be resolved from Slack API on connect
572
- }, sessionManager);
573
- registry.register(slackConnector);
574
- // Register Slack as a delivery target for cron reports
575
- if (config.slack.deliveryChannel) {
576
- delivery.register("slack", (msg) => slackConnector.sendToDeliveryChannel(msg));
577
- }
578
- }
579
- // Construct the cron scheduler before mounting admin so the dashboard can
580
- // list/toggle/edit registered cron jobs. Jobs are registered further down
581
- // (after we know whether webhooks are enabled). The runner closes over
582
- // `dispatchWorkflow`, which is defined earlier in this file.
583
- const cron = new CronScheduler(db, async (workflowName, context) => {
584
- const { dispatched, failures } = await dispatchCronWorkflow(workflowName, context, dispatchWorkflow);
585
- if (failures > 0) {
586
- console.warn(`[cron] ${workflowName}: ${failures}/${dispatched} dispatches failed`);
587
- }
588
- });
589
- // Options for the ledger-driven resume machinery (`resumeSimpleRun`). Shared
590
- // by the boot-time orphan sweep (`resumeOrphanedWorkflows`, below) AND the
591
- // dashboard/CLI "retry a failed run" callback (`retryWorkflow`, in mountAdmin)
592
- // so both reconstruct context from the stored `workflow_runs` row identically.
593
- const resumeOpts = {
594
- db,
595
- github,
596
- config: {
597
- model: config.model,
598
- maxTurns: config.maxTurns,
599
- stateDir: config.stateDir,
600
- sandboxDir: config.sandboxDir,
601
- sessionsDir: config.sessionsDir,
602
- sandbox: config.sandbox,
603
- buildAssets: config.buildAssets,
604
- buildAssetsDir: config.buildAssetsDir,
605
- otel: config.otel,
606
- },
607
- models: config.models,
608
- variants: config.variants,
609
- approvalConfig: config.approval,
610
- bootstrapLabel: config.bootstrapLabel,
611
- publicUrl: config.publicUrl,
612
- slackPoster: slackConnector
613
- ? (channelId, threadId, msg) => slackConnector.sendMessage(channelId, threadId, msg).then(() => { })
614
- : undefined,
615
- };
616
- // Mount admin dashboard on the shared HTTP server (always available).
617
- {
618
- mountAdmin(app, db, {
619
- cronScheduler: cron,
620
- stateDir: config.stateDir,
621
- sessionsDir: config.sessionsDir,
622
- buildAssetsDir: config.buildAssetsDir,
623
- buildAssets: config.buildAssets,
624
- adminPassword: process.env.ADMIN_PASSWORD ?? "",
625
- adminSecret: process.env.ADMIN_SECRET ?? "lastlight-dev-secret",
626
- publicConfig: config.publicConfig,
627
- builtInRoot: config.builtInRoot,
628
- overlayDir: config.overlayDir,
629
- slackOAuthClientId: process.env.SLACK_OAUTH_CLIENT_ID,
630
- slackOAuthClientSecret: process.env.SLACK_OAUTH_CLIENT_SECRET,
631
- slackOAuthRedirectUri: process.env.SLACK_OAUTH_REDIRECT_URI,
632
- slackAllowedWorkspace: process.env.SLACK_ALLOWED_WORKSPACE,
633
- githubOAuthClientId: process.env.GITHUB_OAUTH_CLIENT_ID,
634
- githubOAuthClientSecret: process.env.GITHUB_OAUTH_CLIENT_SECRET,
635
- githubOAuthRedirectUri: process.env.GITHUB_OAUTH_REDIRECT_URI,
636
- githubAllowedOrg: process.env.GITHUB_ALLOWED_ORG,
637
- resumeWorkflow: async (workflowRun, sender) => {
638
- if (!github) {
639
- console.warn(`[admin] Cannot resume workflow ${workflowRun.id}: GitHub App not configured`);
640
- return;
641
- }
642
- // Derive owner/repo for the resume dispatch. Prefer the triggerId
643
- // (owner/repo#N) but fall back to the stored repo + context.owner so a
644
- // run keyed on a non-GitHub triggerId (e.g. a Slack-thread override)
645
- // still resumes from the dashboard/focused-approval flow.
646
- let [owner, repo] = workflowRun.triggerId.includes("/")
647
- ? workflowRun.triggerId.replace(/#\d+$/, "").split("/")
648
- : ["", ""];
649
- if (!owner || !repo) {
650
- const ctxOwner = workflowRun.context?.owner || "";
651
- const storedRepo = workflowRun.repo || "";
652
- owner = ctxOwner || (storedRepo.includes("/") ? storedRepo.split("/")[0] : "");
653
- repo = storedRepo.includes("/") ? storedRepo.split("/")[1] : storedRepo;
654
- }
655
- const issueNumber = workflowRun.issueNumber;
656
- if (!owner || !repo || !issueNumber) {
657
- console.warn(`[admin] Cannot resume workflow ${workflowRun.id}: missing owner/repo/issueNumber`);
658
- return;
659
- }
660
- db.runs.setRunning(workflowRun.id);
661
- console.log(`[admin] Resuming ${workflowRun.workflowName} for ${owner}/${repo}#${issueNumber} after dashboard approval by ${sender}`);
662
- dispatchWorkflow(workflowRun.workflowName, {
663
- repo: `${owner}/${repo}`,
664
- issueNumber,
665
- title: `Issue #${issueNumber}`,
666
- body: "",
667
- sender,
668
- _triggerType: "admin",
669
- }).catch((err) => console.error(`[admin] Resume failed:`, err));
670
- },
671
- // Retry a FAILED run, resuming from the phase that failed with the same
672
- // context. Unlike `resumeWorkflow` (approval-gate resume, which rebuilds a
673
- // lossy owner/repo/issueNumber context and bails on non-issue runs), this
674
- // re-enters via `resumeSimpleRun`, reconstructing the full context from the
675
- // stored `workflow_runs.context` + `scratch` — so it also retries
676
- // Slack-thread-scoped runs (e.g. an `explore` started from Slack). The
677
- // failed phase's ledger row is `success=0`, so it re-runs while
678
- // already-succeeded phases skip.
679
- retryWorkflow: async (workflowRun, sender) => {
680
- if (workflowRun.status !== "failed") {
681
- console.warn(`[admin] Cannot retry ${workflowRun.id}: status is '${workflowRun.status}', not 'failed'`);
682
- return;
683
- }
684
- // Compare-and-set: flip failed→running and clear the terminal markers.
685
- // If a racing retry already flipped it, changes===0 and we don't dispatch.
686
- const changed = db.runs.restartRun(workflowRun.id);
687
- if (changed !== 1) {
688
- console.warn(`[admin] Retry ${workflowRun.id}: run is no longer 'failed' (raced) — skipping dispatch`);
689
- return;
690
- }
691
- const fresh = db.runs.getRun(workflowRun.id);
692
- if (!fresh)
693
- return;
694
- console.log(`[admin] Retrying ${fresh.workflowName} run ${fresh.id} (was on phase=${workflowRun.currentPhase}) by ${sender}`);
695
- resumeSimpleRun(fresh, resumeOpts).catch((err) => console.error(`[admin] Retry ${fresh.id} failed:`, err));
696
- },
697
- });
698
- console.log(`[admin] Dashboard mounted at /admin`);
699
- }
700
- // Protect API endpoints with auth when any login method is configured
701
- // (password OR OAuth) — same gate as the dashboard, via the shared helper.
702
- const adminPassword = process.env.ADMIN_PASSWORD ?? "";
703
- const adminSecret = process.env.ADMIN_SECRET ?? "lastlight-dev-secret";
704
- const apiAuthEnabled = authIsEnabled({
705
- adminPassword,
706
- slackOAuthClientId: process.env.SLACK_OAUTH_CLIENT_ID,
707
- slackOAuthClientSecret: process.env.SLACK_OAUTH_CLIENT_SECRET,
708
- githubOAuthClientId: process.env.GITHUB_OAUTH_CLIENT_ID,
709
- githubOAuthClientSecret: process.env.GITHUB_OAUTH_CLIENT_SECRET,
710
- githubAllowedOrg: process.env.GITHUB_ALLOWED_ORG,
711
- });
712
- if (apiAuthEnabled) {
713
- app.use("/api/*", authMiddleware(apiAuthEnabled, adminSecret));
714
- console.log(`[api] API endpoints protected with auth`);
715
- }
716
- // API endpoint for CLI triggers
717
- app.post("/api/run", async (c) => {
718
- const body = await c.req.json();
719
- // Accept either `skill` (legacy) or `workflow` (preferred). They map 1:1
720
- // for the four agent skills (issue-triage, pr-review, repo-health,
721
- // issue-comment) which are now backed by single-phase YAML workflows.
722
- const workflowName = (body.workflow ?? body.skill);
723
- const context = (body.context ?? {});
724
- if (!workflowName) {
725
- return c.json({ error: "Missing 'workflow' (or 'skill') field" }, 400);
726
- }
727
- console.log(`[api] CLI triggered: workflow=${workflowName}`);
728
- // Run asynchronously — return immediately with a stable id the caller
729
- // can correlate with workflow_runs in the dashboard.
730
- const executionId = randomUUID();
731
- dispatchWorkflow(workflowName, { ...context, _triggerType: "api" }).catch((err) => {
732
- const msg = err instanceof Error ? err.message : String(err);
733
- console.error(`[api] workflow ${workflowName} failed: ${msg}`);
734
- });
735
- return c.json({ accepted: true, executionId, workflow: workflowName }, 202);
736
- });
737
- // API endpoint for build cycle triggers (issue URL)
738
- app.post("/api/build", async (c) => {
739
- const body = await c.req.json();
740
- const { owner, repo, issueNumber, issueTitle, issueBody, issueLabels, sender } = body;
741
- if (!owner || !repo || !issueNumber) {
742
- return c.json({ error: "Missing owner, repo, or issueNumber" }, 400);
743
- }
744
- console.log(`[api] CLI build triggered: ${owner}/${repo}#${issueNumber}`);
745
- // If labels weren't supplied, fetch them so the orchestrator can detect
746
- // bootstrap tasks (lastlight:bootstrap label) and skip the BLOCKED gate.
747
- let resolvedLabels = issueLabels;
748
- if (!resolvedLabels && github) {
749
- try {
750
- const issue = await github.getIssue(owner, repo, issueNumber);
751
- resolvedLabels = (issue.labels || []).map((l) => typeof l === "string" ? l : l.name).filter(Boolean);
752
- }
753
- catch { /* non-fatal */ }
754
- }
755
- // Run build cycle asynchronously via the generic dispatcher
756
- dispatchWorkflow("build", {
757
- repo: `${owner}/${repo}`,
758
- issueNumber,
759
- title: issueTitle || `Issue #${issueNumber}`,
760
- body: issueBody || "",
761
- labels: resolvedLabels,
762
- sender: sender || "cli",
763
- _triggerType: "api",
764
- }).catch((err) => {
765
- console.error(`[api] Build failed:`, err);
766
- });
767
- return c.json({ accepted: true, owner, repo, issueNumber }, 202);
768
- });
769
- // Handle events from any connector
770
- // Handle events from any connector. The dispatcher turns each EventEnvelope
771
- // into a workflow dispatch (or in-process handler run) through one testable
772
- // seam; every per-event branch lives in src/engine/dispatcher.ts. main()
773
- // only constructs the deps and relays the typed outcome.
774
- const dispatchDeps = {
775
- db,
776
- github,
777
- dispatchWorkflow,
778
- sessionManager,
779
- // One in-process chat turn. handleChatMessage manages session resume via
780
- // sessionManager internally; the dispatcher uses resumeAgentSessionId only
781
- // to decide whether to persist a newly-minted agent session id.
782
- runChat: (message, messagingSessionId, sender) => handleChatMessage(message, messagingSessionId, sender, sessionManager, { chatRunner, sessionsHomeDir: config.sessionsDir }, { model: resolveModel(config.models, "chat"), maxTurns: 10 }),
783
- reviewPostsCheck: config.reviewPostsCheck,
784
- publicUrl: config.publicUrl,
785
- };
786
- // Wire Slack approval buttons into the SAME approval-resolution path as the
787
- // `/approve` slash command. The connector verifies + parses the button click
788
- // (on /webhooks/slack/interactions) and hands us the workflow run id; we force
789
- // the route to `approval-response`, reusing the whole dispatcher/resume seam.
790
- if (slackConnector) {
791
- slackConnector.onApprovalAction(async ({ decision, workflowRunId, sender, envelope }) => {
792
- await dispatch(envelope, {
793
- ...dispatchDeps,
794
- route: async () => ({
795
- action: "handler",
796
- handler: "approval-response",
797
- context: { decision, workflowRunId, sender, source: "slack" },
798
- }),
799
- });
800
- });
801
- }
802
- // One chat turn over HTTP — `lastlight chat` without a messaging platform.
803
- // Routes through the SAME dispatcher seam Slack uses (forcing the chat
804
- // handler), so the executions row, agent-session resume, and telemetry are
805
- // recorded identically and the turn shows up in the dashboard Chat tab. The
806
- // synthetic envelope's reply() just captures the assistant text to return.
807
- app.post("/api/chat", async (c) => {
808
- const body = await c.req.json().catch(() => ({}));
809
- const message = typeof body.message === "string" ? body.message : "";
810
- if (!message.trim())
811
- return c.json({ error: "Missing 'message'" }, 400);
812
- const user = typeof body.user === "string" && body.user ? body.user : "cli";
813
- const threadId = typeof body.thread === "string" && body.thread ? body.thread : null;
814
- const session = sessionManager.getOrCreateSession({
815
- platform: "cli", channelId: user, threadId, userId: user,
816
- });
817
- let reply = "";
818
- const envelope = {
819
- id: session.id,
820
- source: "cli",
821
- type: "message",
822
- sender: user,
823
- senderIsBot: false,
824
- body: message,
825
- raw: { cli: true },
826
- reply: async (msg) => { reply = msg; },
827
- timestamp: new Date(),
828
- };
829
- const outcome = await dispatch(envelope, {
830
- ...dispatchDeps,
831
- route: async () => ({
832
- action: "handler",
833
- handler: "chat",
834
- context: { sessionId: session.id, message, sender: user },
835
- }),
836
- });
837
- return c.json({ text: reply, thread: threadId ?? session.id, sessionId: session.id, outcome: outcome.kind });
838
- });
839
- const handleEnvelope = async (envelope) => {
840
- console.log(`[event] ${envelope.source}:${envelope.type} from ${envelope.sender}${envelope.repo ? ` on ${envelope.repo}` : ""}`);
841
- try {
842
- const outcome = await dispatch(envelope, dispatchDeps);
843
- if (outcome.kind === "ignored") {
844
- console.log(`[event] Ignored: ${outcome.reason}`);
845
- }
846
- else if (outcome.kind === "skipped") {
847
- console.log(`[event] Skipped: ${outcome.reason}`);
848
- }
849
- }
850
- catch (err) {
851
- const msg = err instanceof Error ? err.message : String(err);
852
- console.error(`[event] dispatch threw: ${msg}`);
853
- }
854
- };
855
- // Batch bursty messaging input per session BEFORE routing: a rapid Slack
856
- // burst is collected, sorted into send order, and collapsed into ONE
857
- // envelope so it's classified once and answered as a single ordered turn.
858
- // Gated on `type === "message"` — only messaging connectors emit that here
859
- // (GitHub events have richer types; the CLI dispatches directly, not via the
860
- // registry). Tunable settle window via CHAT_BATCH_DEBOUNCE_MS (default 700ms;
861
- // 0 disables).
862
- const messageBatcher = new MessageBatcher({
863
- dispatch: handleEnvelope,
864
- debounceMs: Number.parseInt(process.env.CHAT_BATCH_DEBOUNCE_MS || "700", 10),
865
- });
866
- registry.onEvent(async (envelope) => {
867
- if (envelope.type === "message") {
868
- messageBatcher.submit(envelope);
869
- return;
870
- }
871
- await handleEnvelope(envelope);
872
- });
873
- // Cron jobs — fan out from each tick into one workflow run per managed repo
874
- // (see dispatchCronWorkflow). The scheduler itself was constructed earlier
875
- // so the admin dashboard could be wired with it. Every job (health/security
876
- // reports + issue/PR polling) drives a GitHub-scoped workflow, so skip
877
- // registration entirely without a GitHub client — a chat-only instance would
878
- // otherwise fire periodic no-op dispatch failures.
879
- const webhooksEnabled = !!(config.webhookSecret && config.githubApp);
880
- if (github) {
881
- const jobs = getJobs({ webhooksEnabled, db });
882
- for (const job of jobs) {
883
- cron.register(job);
884
- }
885
- if (webhooksEnabled) {
886
- console.log("[cron] Webhooks enabled — skipping issue/PR polling crons");
887
- }
888
- }
889
- else {
890
- console.log("[cron] No GitHub client — skipping all cron jobs (chat-only mode)");
891
- }
892
- // Start everything
893
- await registry.startAll();
894
- console.log("[main] All connectors started");
895
- console.log("[main] Cron jobs registered");
896
- // Open the shared HTTP listener. All routes (admin, /api/*, /health,
897
- // /webhooks/github, /webhooks/slack) are registered synchronously above, so
898
- // the port is ready the moment it opens. Always boots — chat-only, PAT, and
899
- // full GitHub App modes alike.
900
- const server = serve({ fetch: app.fetch, port: config.port, hostname: "0.0.0.0" });
901
- console.log(`[http] Listening on port ${config.port}`);
902
- // Chat runs in-process via pi-ai — no long-lived server to boot.
903
- // Boot-time recovery: any workflow_runs left in 'running' state from a
904
- // previous harness lifetime have already had their sandbox containers
905
- // killed by cleanupOrphanedSandboxes(). Mark their stale execution rows as
906
- // failed and re-dispatch each run so the runner can pick up after the last
907
- // completed phase. Skips 'paused' runs — those intentionally wait for a
908
- // human approval and are resumed via the dashboard / GitHub comment flow.
909
- resumeOrphanedWorkflows(resumeOpts).catch((err) => console.error("[main] Resume sweep failed:", err));
910
- console.log("[main] Ready to receive events");
911
- // Graceful shutdown
912
- const shutdown = async () => {
913
- console.log("\n[main] Shutting down...");
914
- cron.stopAll();
915
- await registry.stopAll();
916
- // The shared HTTP server is owned here now (no longer by the webhook
917
- // connector's stop()), so close it explicitly.
918
- server.close();
919
- if (!telemetryShutdownStarted) {
920
- telemetryShutdownStarted = true;
921
- await shutdownTelemetry();
922
- }
923
- db.close();
924
- process.exit(0);
925
- };
926
- process.on("SIGINT", shutdown);
927
- process.on("SIGTERM", shutdown);
928
- }
929
- main().catch((err) => {
930
- console.error("[main] Fatal error:", err);
931
- // Exit 78 (EX_CONFIG) to signal Docker restart policy that looping won't help.
932
- // Common causes: bad PEM, wrong App ID, missing env vars.
933
- const msg = err?.message || "";
934
- const isConfig = msg.includes("could not be decoded") ||
935
- msg.includes("not found") ||
936
- msg.includes("ENOENT") ||
937
- msg.includes("required");
938
- process.exit(isConfig ? 78 : 1);
939
- });
940
- //# sourceMappingURL=index.js.map