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
@@ -0,0 +1,691 @@
1
+ import { randomUUID } from "crypto";
2
+ import { execSync } from "child_process";
3
+ import { executeAgent } from "../engine/agent-executor.js";
4
+ import { phaseSkillNames } from "./schema.js";
5
+ import { loadPromptTemplate, resolveSkillPaths } from "./loader.js";
6
+ import { renderTemplate } from "./templates.js";
7
+ import { evalUntilExpression } from "./loop-eval.js";
8
+ import { parseReviewerVerdict } from "./verdict.js";
9
+ import { PhaseRef } from "./phase-ref.js";
10
+ import { recordError, recordExecutionMetrics, withSpan } from "../telemetry/index.js";
11
+ import { listRunningContainers } from "../admin/docker.js";
12
+ // ── Module-level helpers (moved here from runner.ts) ─────────────────────────
13
+ /**
14
+ * Reject shell commands containing mustache template markers to prevent
15
+ * accidental template injection into until_bash values.
16
+ */
17
+ function validateShellCommand(cmd) {
18
+ if (cmd.includes("{{")) {
19
+ throw new Error(`until_bash command rejected: contains template marker '{{'. Render templates before passing to shell.`);
20
+ }
21
+ }
22
+ /**
23
+ * Build the agent prompt for a phase, handling both `prompt:` (template file)
24
+ * and `skill:`/`skills:` (skill references) phase definitions.
25
+ */
26
+ export function buildPhasePrompt(phase, ctx, extraCtx) {
27
+ const fullCtx = extraCtx ? { ...ctx, ...extraCtx } : ctx;
28
+ if (phase.prompt) {
29
+ const template = loadPromptTemplate(phase.prompt);
30
+ return renderTemplate(template, fullCtx);
31
+ }
32
+ const skills = phaseSkillNames(phase);
33
+ if (skills.length) {
34
+ const [primary, ...rest] = skills;
35
+ const contextLines = Object.entries(fullCtx)
36
+ .filter(([, v]) => v !== undefined && v !== null)
37
+ .map(([k, v]) => `${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`)
38
+ .join("\n");
39
+ const others = rest.length
40
+ ? `Other skills available if you need them: ${rest.join(", ")}.`
41
+ : "";
42
+ return [
43
+ `Use the **${primary}** skill to handle this request.`,
44
+ others,
45
+ "",
46
+ "Context:",
47
+ contextLines,
48
+ ]
49
+ .filter((line) => line !== "")
50
+ .join("\n");
51
+ }
52
+ throw new Error(`Phase "${phase.name}" has neither prompt: nor skills: — cannot build prompt`);
53
+ }
54
+ /**
55
+ * Overlay per-phase executor config fields (`unrestricted_egress`,
56
+ * `web_search`, `sandbox_image`, resolved skill paths) onto the run-level
57
+ * config.
58
+ */
59
+ export function phaseConfigFor(config, phase) {
60
+ const skills = phaseSkillNames(phase);
61
+ const skillPaths = skills.length ? resolveSkillPaths(skills) : undefined;
62
+ if (phase.unrestricted_egress === undefined &&
63
+ phase.web_search === undefined &&
64
+ phase.sandbox_image === undefined &&
65
+ !skillPaths) {
66
+ return config;
67
+ }
68
+ const next = { ...config };
69
+ if (phase.unrestricted_egress !== undefined) {
70
+ next.unrestrictedEgress = phase.unrestricted_egress;
71
+ }
72
+ if (phase.web_search !== undefined) {
73
+ next.webSearch = phase.web_search;
74
+ }
75
+ if (phase.sandbox_image !== undefined) {
76
+ next.sandboxImage = phase.sandbox_image;
77
+ }
78
+ if (skillPaths) {
79
+ next.skillPaths = skillPaths;
80
+ }
81
+ return next;
82
+ }
83
+ /** Check if an error was caused by manual termination (OOM/cancel/kill). */
84
+ export function isTerminated(error) {
85
+ if (!error)
86
+ return false;
87
+ const lower = error.toLowerCase();
88
+ return (lower.includes("terminated") ||
89
+ lower.includes("killed") ||
90
+ lower.includes("exit undefined") ||
91
+ (lower.includes("container") && lower.includes("not running")));
92
+ }
93
+ function pickResult(r) {
94
+ return { success: r.success, output: r.output, error: r.error };
95
+ }
96
+ function issueNumberFromTrigger(triggerId) {
97
+ const m = triggerId.match(/#(\d+)$/);
98
+ return m ? Number(m[1]) : undefined;
99
+ }
100
+ /** Check if a sandbox container is actually running for a given taskId prefix. */
101
+ async function isContainerAlive(taskId) {
102
+ try {
103
+ const containers = await listRunningContainers();
104
+ return containers.some((c) => c.taskId === taskId);
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ }
110
+ /**
111
+ * Run a single agent phase with DB-tracked deduplication. Moved verbatim from
112
+ * runner.ts — the dedup ledger (`shouldRunPhase`) is the single source of
113
+ * truth for resume, so re-running from the top skips completed phases here.
114
+ */
115
+ export async function runPhase(workflowName, phaseName, taskId, triggerId, prompt, config, db, modelOverride, workflowRunId, githubAccess, variantOverride) {
116
+ const dedupKey = `${workflowName}:${phaseName}`;
117
+ const attrs = {
118
+ "workflow.name": workflowName,
119
+ "phase.name": phaseName,
120
+ "workflow.run_id": workflowRunId,
121
+ "trigger.id": triggerId,
122
+ "task.id": taskId,
123
+ repo: githubAccess?.repo,
124
+ "issue.number": issueNumberFromTrigger(triggerId),
125
+ "sandbox.backend": config.sandbox,
126
+ model: modelOverride || config.model,
127
+ };
128
+ return withSpan("lastlight.workflow.phase", attrs, async (span) => {
129
+ if (db) {
130
+ const status = db.executions.shouldRunPhase(dedupKey, triggerId, workflowRunId);
131
+ if (status === "running") {
132
+ const alive = await isContainerAlive(taskId);
133
+ if (alive) {
134
+ console.log(`[runner] Phase ${phaseName} is already running (container alive) — skipping`);
135
+ span?.addEvent("lastlight.workflow.phase.skipped", { reason: "running" });
136
+ return { skipped: true, reason: "running" };
137
+ }
138
+ console.log(`[runner] Phase ${phaseName} was running but container is dead — cleaning up`);
139
+ db.executions.markStaleAsFailed(dedupKey, triggerId, workflowRunId);
140
+ }
141
+ else if (status === "done") {
142
+ console.log(`[runner] Phase ${phaseName} already completed successfully — skipping`);
143
+ span?.addEvent("lastlight.workflow.phase.skipped", { reason: "done" });
144
+ return { skipped: true, reason: "done" };
145
+ }
146
+ const executionId = randomUUID();
147
+ db.executions.recordStart({
148
+ id: executionId,
149
+ triggerType: "webhook",
150
+ triggerId,
151
+ skill: dedupKey,
152
+ repo: githubAccess?.repo,
153
+ issueNumber: issueNumberFromTrigger(triggerId),
154
+ startedAt: new Date().toISOString(),
155
+ workflowRunId,
156
+ });
157
+ const baseConfig = modelOverride ? { ...config, model: modelOverride } : config;
158
+ const phaseConfigBase = variantOverride ? { ...baseConfig, variant: variantOverride } : baseConfig;
159
+ const phaseConfig = {
160
+ ...phaseConfigBase,
161
+ telemetry: { workflowName, phaseName, triggerId, workflowRunId },
162
+ };
163
+ try {
164
+ const result = await executeAgent(prompt, phaseConfig, {
165
+ taskId,
166
+ githubAccess,
167
+ onSessionId: (sessionId) => {
168
+ try {
169
+ db.executions.recordSessionId(executionId, sessionId);
170
+ }
171
+ catch (err) {
172
+ console.warn(`[runner] Failed to persist session id mid-run for ${phaseName}:`, err);
173
+ }
174
+ },
175
+ });
176
+ db.executions.recordFinish(executionId, {
177
+ success: result.success,
178
+ error: result.error,
179
+ turns: result.turns,
180
+ durationMs: result.durationMs,
181
+ sessionId: result.sessionId,
182
+ costUsd: result.costUsd,
183
+ inputTokens: result.inputTokens,
184
+ cacheCreationInputTokens: result.cacheCreationInputTokens,
185
+ cacheReadInputTokens: result.cacheReadInputTokens,
186
+ outputTokens: result.outputTokens,
187
+ apiDurationMs: result.apiDurationMs,
188
+ stopReason: result.stopReason,
189
+ extensionStatus: result.extensions ? JSON.stringify(result.extensions) : undefined,
190
+ skillsStatus: result.skills ? JSON.stringify(result.skills) : undefined,
191
+ });
192
+ span?.setAttributes({ success: result.success, stop_reason: result.stopReason ?? "unknown" });
193
+ recordExecutionMetrics("phase", { ...attrs, success: result.success, stop_reason: result.stopReason, durationMs: result.durationMs, costUsd: result.costUsd, inputTokens: result.inputTokens, outputTokens: result.outputTokens });
194
+ return { result, executionId, skipped: false };
195
+ }
196
+ catch (err) {
197
+ recordError("phase", err, attrs);
198
+ throw err;
199
+ }
200
+ }
201
+ const baseConfig = modelOverride ? { ...config, model: modelOverride } : config;
202
+ const phaseConfigBase = variantOverride ? { ...baseConfig, variant: variantOverride } : baseConfig;
203
+ const phaseConfig = { ...phaseConfigBase, telemetry: { workflowName, phaseName, triggerId, workflowRunId } };
204
+ const result = await executeAgent(prompt, phaseConfig, { taskId, githubAccess });
205
+ recordExecutionMetrics("phase", { ...attrs, success: result.success, stop_reason: result.stopReason, durationMs: result.durationMs, costUsd: result.costUsd, inputTokens: result.inputTokens, outputTokens: result.outputTokens });
206
+ return { result, skipped: false };
207
+ });
208
+ }
209
+ // ── PhaseExecutor ────────────────────────────────────────────────────────────
210
+ const MAX_PREV_OUTPUT_BYTES = 10 * 1024; // cap accumulated generic-loop output at 10KB
211
+ export class PhaseExecutor {
212
+ run;
213
+ reporter;
214
+ resolver;
215
+ constructor(run, reporter, resolver) {
216
+ this.run = run;
217
+ this.reporter = reporter;
218
+ this.resolver = resolver;
219
+ }
220
+ /**
221
+ * Execute one DAG node and return its delta. The scheduler accumulates
222
+ * `results` into `phases[]`, merges `outputVars` into the shared outputs
223
+ * map, and maps `status`/`paused`/`aborted` onto node state + run control.
224
+ */
225
+ async execute(node, outputs) {
226
+ const phase = this.run.definition.phases.find((p) => p.name === node.name);
227
+ if (!phase) {
228
+ // Unknown node — shouldn't happen; treat as a no-op success.
229
+ return { results: [{ phase: node.name, success: true, output: "" }], status: "succeeded" };
230
+ }
231
+ const phaseType = phase.type ?? "agent";
232
+ if (phaseType === "context")
233
+ return this.runContext(phase);
234
+ if (!phase.prompt && phaseSkillNames(phase).length === 0) {
235
+ console.warn(`[runner] Phase "${phase.name}" has type=agent but neither prompt: nor skills: — skipping`);
236
+ return { results: [], status: "succeeded" };
237
+ }
238
+ if (phase.loop)
239
+ return this.runReviewerLoop(phase, outputs);
240
+ if (phase.generic_loop)
241
+ return this.runGenericLoop(phase, outputs);
242
+ return this.runStandard(phase, outputs);
243
+ }
244
+ // ── Per-phase bodies ───────────────────────────────────────────────────────
245
+ async runContext(phase) {
246
+ await this.reporter.onStart(phase.name);
247
+ const result = { phase: phase.name, success: true, output: "Context assembled" };
248
+ // Persist a phase_history entry so the dashboard marks context phases done.
249
+ this.reporter.persistPhase(phase.name, "Context assembled");
250
+ await this.reporter.onEnd(phase.name, result);
251
+ return { results: [result], status: "succeeded" };
252
+ }
253
+ /** Resolve model + variant for a phase/task, honouring YAML templates first. */
254
+ resolveModelVariant(template, variantTemplate, taskName, fallbackTask) {
255
+ const ctx = this.run.ctx;
256
+ const modelRaw = template ? renderTemplate(template, ctx) : undefined;
257
+ const model = modelRaw || this.resolver.modelFor(taskName)
258
+ || (fallbackTask ? this.resolver.modelFor(fallbackTask) : undefined);
259
+ const variantRaw = variantTemplate ? renderTemplate(variantTemplate, ctx) : undefined;
260
+ const variant = variantRaw || this.resolver.variantFor(taskName)
261
+ || (fallbackTask ? this.resolver.variantFor(fallbackTask) : undefined);
262
+ return { model, variant };
263
+ }
264
+ async runPhaseCall(label, prompt, phase, model, variant) {
265
+ const { definition, config, db, workflowId, githubAccess, taskId, triggerId } = this.run;
266
+ return runPhase(definition.name, label, taskId, triggerId, prompt, phaseConfigFor(config, phase), db, model, workflowId, githubAccess, variant);
267
+ }
268
+ async runStandard(phase, outputs) {
269
+ const phaseName = phase.name;
270
+ await this.reporter.onStart(phaseName);
271
+ await this.reporter.step(phaseName, "running", phase.messages?.on_start);
272
+ const { model, variant } = this.resolveModelVariant(phase.model, phase.variant, phaseName);
273
+ const prompt = buildPhasePrompt(phase, this.run.ctx, { phaseOutputs: outputs });
274
+ const pr = await this.runPhaseCall(phaseName, prompt, phase, model, variant);
275
+ if (pr.skipped) {
276
+ if (pr.reason === "running") {
277
+ await this.reporter.message(phase.messages?.on_skipped_done);
278
+ return { results: [], status: "failed", aborted: true };
279
+ }
280
+ const result = { phase: phaseName, success: true, output: "Already completed" };
281
+ this.reporter.persistPhase(phaseName, "Already completed (deduplicated)");
282
+ await this.reporter.onEnd(phaseName, result);
283
+ await this.reporter.step(phaseName, "done", phase.messages?.on_skipped_done);
284
+ return { results: [result], status: "succeeded" };
285
+ }
286
+ const result = { phase: phaseName, ...pickResult(pr.result) };
287
+ await this.reporter.onEnd(phaseName, result);
288
+ const rawOutput = pr.result.output ?? "";
289
+ const outputVars = { [phaseName]: rawOutput };
290
+ if (phase.output_var)
291
+ outputVars[phase.output_var] = rawOutput;
292
+ if (!pr.result.success) {
293
+ if (!isTerminated(pr.result.error)) {
294
+ await this.reporter.step(phaseName, "failed", phase.messages?.on_failure);
295
+ }
296
+ this.reporter.failWorkflow(pr.result.error);
297
+ return { results: [result], status: "failed", outputVars };
298
+ }
299
+ // on_output rules (BLOCKED).
300
+ if (phase.on_output) {
301
+ const blocked = await this.evaluateBlocked(phase, pr.result.output ?? "");
302
+ if (blocked === "fail") {
303
+ const failResult = {
304
+ phase: phaseName,
305
+ success: false,
306
+ output: pr.result.output ?? "",
307
+ error: "BLOCKED",
308
+ };
309
+ return { results: [failResult], status: "failed", outputVars };
310
+ }
311
+ }
312
+ // Approval gate.
313
+ if (phase.approval_gate && this.resolver.gateEnabled(phase.approval_gate) && this.run.db && this.run.workflowId) {
314
+ await this.pauseForApproval(phaseName, phase.approval_gate, `${phaseName} complete — awaiting ${phase.approval_gate} approval.`, "approve", phase.approval_gate_message, { gateKey: phase.approval_gate }, undefined, phase.approval_artifact);
315
+ return { results: [result], status: "succeeded", paused: true, outputVars };
316
+ }
317
+ this.reporter.persistPhase(phaseName);
318
+ // Make THIS phase's own output available to its on_success message. The
319
+ // scheduler only merges `outputVars` into the shared outputs map after
320
+ // execute() returns, so without this a phase referencing its own output
321
+ // (e.g. answer's `on_success: "{{answerResult}}"`, explore's publishResult)
322
+ // would render empty. Merge the just-produced vars into the render context.
323
+ await this.reporter.step(phaseName, "done", phase.messages?.on_success, {
324
+ phaseOutputs: { ...outputs, ...outputVars },
325
+ });
326
+ return { results: [result], status: "succeeded", outputVars };
327
+ }
328
+ /**
329
+ * Evaluate the `contains_BLOCKED` rule. Returns "fail" when the workflow
330
+ * should fail, or "continue" when the marker is absent or bypassed.
331
+ */
332
+ async evaluateBlocked(phase, output) {
333
+ const rule = phase.on_output?.contains_BLOCKED;
334
+ if (!rule)
335
+ return "continue";
336
+ if (!output.toUpperCase().includes("BLOCKED"))
337
+ return "continue";
338
+ const ctx = this.run.ctx;
339
+ const hasUnlessLabel = rule.unless_label && ctx.issueLabels.includes(rule.unless_label);
340
+ const titleMatches = (() => {
341
+ if (!rule.unless_title_matches)
342
+ return false;
343
+ if (rule.unless_title_matches.length > 200)
344
+ return false;
345
+ if (/[+*]\{0,\}.*[+*]/.test(rule.unless_title_matches) || /(\([^)]*[+*][^)]*\))[+*?]/.test(rule.unless_title_matches))
346
+ return false;
347
+ try {
348
+ return new RegExp(rule.unless_title_matches, "i").test(ctx.issueTitle || "");
349
+ }
350
+ catch {
351
+ return false;
352
+ }
353
+ })();
354
+ if (hasUnlessLabel || titleMatches) {
355
+ await this.reporter.message(rule.bypass_message || phase.messages?.on_blocked_bypassed);
356
+ return "continue";
357
+ }
358
+ if (rule.action === "fail") {
359
+ this.run.db?.executions.markLatestAsFailed(`${this.run.definition.name}:${phase.name}`, this.run.triggerId, rule.message || "BLOCKED", this.run.workflowId);
360
+ this.reporter.failWorkflow(rule.message || "BLOCKED");
361
+ const blockedTemplate = rule.message || phase.messages?.on_blocked;
362
+ if (blockedTemplate)
363
+ await this.reporter.step(phase.name, "blocked", blockedTemplate);
364
+ return "fail";
365
+ }
366
+ return "continue";
367
+ }
368
+ /**
369
+ * Persist + pause for an approval/reply gate, atomically. The approval
370
+ * insert, the `waiting_approval` phase marker, the optional scratch persist
371
+ * (loops record their iteration/cycle state here), and the run pause all
372
+ * happen in one transaction via `db.runs.pauseForApproval` — a partial
373
+ * failure can't leave the run paused without its gate, or vice versa.
374
+ */
375
+ async pauseForApproval(stepKey, gate, summary, kind, message, extraCtx, scratchPatch, artifact) {
376
+ const { db, workflowId, ctx } = this.run;
377
+ if (!db || !workflowId)
378
+ return;
379
+ const approvalId = randomUUID();
380
+ db.runs.pauseForApproval(workflowId, {
381
+ id: approvalId,
382
+ workflowRunId: workflowId,
383
+ gate,
384
+ summary,
385
+ kind,
386
+ artifact,
387
+ requestedBy: ctx.sender,
388
+ createdAt: new Date().toISOString(),
389
+ }, {
390
+ phase: "waiting_approval",
391
+ summary: `Waiting for ${kind === "reply" ? "reply" : "approval"}: ${gate} (${approvalId})`,
392
+ }, scratchPatch);
393
+ // Expose `approvalId` to the gate message template so it can deep-link to
394
+ // the focused approval view via `{{approvalUrl}}`.
395
+ await this.reporter.step(stepKey, "awaiting", message, { ...extraCtx, approvalId }, { alsoNote: true });
396
+ }
397
+ async runReviewerLoop(phase, outputs) {
398
+ const loop = phase.loop;
399
+ const phaseName = phase.name;
400
+ const MAX_CYCLES = loop.max_cycles;
401
+ const { db, workflowId, triggerId, scratch } = this.run;
402
+ const wf = this.run.definition.name;
403
+ const results = [];
404
+ let approved = false;
405
+ let fixCycles = 0;
406
+ // Loop resume state. When the loop pauses at `loop.approval_gate` after a
407
+ // REQUEST_CHANGES verdict, we persist the cycle we paused at so that on
408
+ // resume we can tell "approved — continue with the fix cycle" apart from
409
+ // "review approved → done". Without this, a dedup-`done` review on resume
410
+ // would be misread as APPROVED and skip the required fix. (#94 follow-up)
411
+ const loopKey = `rloop:${phaseName}`;
412
+ const slot = scratch[loopKey] ?? {};
413
+ const pausedAtCycle = typeof slot.pausedAtCycle === "number" ? slot.pausedAtCycle : undefined;
414
+ while (!approved && fixCycles <= MAX_CYCLES) {
415
+ const reviewLabel = fixCycles === 0
416
+ ? PhaseRef.review(phaseName).format()
417
+ : PhaseRef.recheck(phaseName, fixCycles).format();
418
+ await this.reporter.onStart(reviewLabel);
419
+ await this.reporter.step(reviewLabel, "running", loop.messages?.on_cycle_start, { cycle: fixCycles + 1, maxCycles: MAX_CYCLES }, fixCycles === 0
420
+ ? undefined
421
+ : { insert: true, label: `${phase.label ?? phaseName} (cycle ${fixCycles + 1})` });
422
+ const reviewPrompt = fixCycles === 0
423
+ ? buildPhasePrompt(phase, this.run.ctx, { phaseOutputs: outputs, fixCycle: fixCycles })
424
+ : this.resolver.renderPrompt(loop.on_request_changes.re_review_prompt, { phaseOutputs: outputs, fixCycle: fixCycles });
425
+ const { model, variant } = this.resolveModelVariant(phase.model, phase.variant, phaseName);
426
+ const rr = await this.runPhaseCall(reviewLabel, reviewPrompt, phase, model, variant);
427
+ // Derive this review's verdict. A dedup-`done` review (resume) is NOT
428
+ // assumed approved — re-parse the verdict from its persisted output.
429
+ let verdict;
430
+ const reviewRan = !rr.skipped;
431
+ if (rr.skipped) {
432
+ if (rr.reason === "running") {
433
+ await this.reporter.message(phase.messages?.on_skipped_done);
434
+ return { results, status: "failed", aborted: true };
435
+ }
436
+ const prevOutput = db?.executions.getPhaseOutput(`${wf}:${reviewLabel}`, triggerId, workflowId) ?? "";
437
+ verdict = parseReviewerVerdict(prevOutput).verdict;
438
+ results.push({ phase: reviewLabel, success: true, output: "Already completed" });
439
+ }
440
+ else {
441
+ results.push({ phase: reviewLabel, ...pickResult(rr.result) });
442
+ await this.reporter.onEnd(reviewLabel, results[results.length - 1]);
443
+ const reviewerOutput = (rr.result.output || "").trim();
444
+ const parsed = parseReviewerVerdict(reviewerOutput);
445
+ verdict = parsed.verdict;
446
+ if (parsed.viaFallback) {
447
+ console.warn(`[runner] Reviewer output missing VERDICT: marker — using fallback detection (isApproved=${verdict === "APPROVED"})`);
448
+ }
449
+ // Persist the review output so a resumed run can re-derive this verdict.
450
+ const execId = "executionId" in rr ? rr.executionId : undefined;
451
+ if (execId && db)
452
+ db.executions.recordOutputText(execId, reviewerOutput);
453
+ }
454
+ const isApproved = verdict === "APPROVED";
455
+ if (isApproved) {
456
+ approved = true;
457
+ if (reviewRan)
458
+ this.reporter.persistPhase(reviewLabel, "APPROVED");
459
+ await this.reporter.step(reviewLabel, "done", loop.messages?.on_approved, { cycle: fixCycles + 1 });
460
+ }
461
+ else if (fixCycles < MAX_CYCLES) {
462
+ fixCycles++;
463
+ if (reviewRan)
464
+ this.reporter.persistPhase(reviewLabel, "REQUEST_CHANGES");
465
+ // Approval gate before the fix loop. Pause only on a *fresh* gate hit:
466
+ // not when the fix for this cycle has already run (gate was approved in
467
+ // a prior entry), and not when we're resuming right after approving
468
+ // exactly this cycle's gate.
469
+ if (this.resolver.gateEnabled(loop.approval_gate) && db && workflowId) {
470
+ const fixLabel = PhaseRef.fix(phaseName, fixCycles).format();
471
+ const fixAlreadyDone = db.executions.shouldRunPhase(`${wf}:${fixLabel}`, triggerId, workflowId) === "done";
472
+ const resumingThisGate = pausedAtCycle === fixCycles;
473
+ if (!fixAlreadyDone && !resumingThisGate) {
474
+ scratch[loopKey] = { ...slot, pausedAtCycle: fixCycles };
475
+ await this.pauseForApproval(reviewLabel, loop.approval_gate, `Reviewer requested changes (cycle ${fixCycles}/${MAX_CYCLES}) on phase ${phaseName}.`, "approve", loop.messages?.on_pause_for_approval, { cycle: fixCycles, maxCycles: MAX_CYCLES, gateKey: loop.approval_gate }, { [loopKey]: scratch[loopKey] }, loop.approval_artifact);
476
+ return { results, status: "succeeded", paused: true };
477
+ }
478
+ }
479
+ await this.reporter.step(reviewLabel, "done", loop.messages?.on_request_changes, {
480
+ cycle: fixCycles,
481
+ maxCycles: MAX_CYCLES,
482
+ });
483
+ // Fix phase.
484
+ const fixLabel = PhaseRef.fix(phaseName, fixCycles).format();
485
+ await this.reporter.onStart(fixLabel);
486
+ await this.reporter.step(fixLabel, "running", loop.messages?.on_fix_start, { cycle: fixCycles, maxCycles: MAX_CYCLES }, { insert: true, label: `Fix (cycle ${fixCycles})` });
487
+ const { model: fixModel, variant: fixVariant } = this.resolveModelVariant(loop.on_request_changes.fix_model, loop.on_request_changes.fix_variant, `${phaseName}_fix`, phaseName);
488
+ const fixPromptRendered = this.resolver.renderPrompt(loop.on_request_changes.fix_prompt, {
489
+ phaseOutputs: outputs,
490
+ fixCycle: fixCycles,
491
+ });
492
+ const fr = await this.runPhaseCall(fixLabel, fixPromptRendered, phase, fixModel, fixVariant);
493
+ if (fr.skipped) {
494
+ if (fr.reason === "running") {
495
+ await this.reporter.message(phase.messages?.on_skipped_done);
496
+ return { results, status: "failed", aborted: true };
497
+ }
498
+ results.push({ phase: fixLabel, success: true, output: "Already completed" });
499
+ }
500
+ else {
501
+ results.push({ phase: fixLabel, ...pickResult(fr.result) });
502
+ await this.reporter.onEnd(fixLabel, results[results.length - 1]);
503
+ if (!fr.result.success) {
504
+ if (!isTerminated(fr.result.error)) {
505
+ await this.reporter.step(fixLabel, "failed", loop.messages?.on_fix_failed, {
506
+ cycle: fixCycles,
507
+ maxCycles: MAX_CYCLES,
508
+ });
509
+ }
510
+ break;
511
+ }
512
+ this.reporter.persistPhase(fixLabel);
513
+ await this.reporter.step(fixLabel, "done");
514
+ }
515
+ }
516
+ else {
517
+ this.reporter.persistPhase(reviewLabel, "REQUEST_CHANGES — max cycles reached");
518
+ await this.reporter.step(reviewLabel, "blocked", loop.messages?.on_max_cycles, {
519
+ cycle: fixCycles,
520
+ maxCycles: MAX_CYCLES,
521
+ }, { alsoNote: true });
522
+ break;
523
+ }
524
+ }
525
+ const outputVars = {};
526
+ if (phase.output_var)
527
+ outputVars[phase.output_var] = { approved, cycles: fixCycles };
528
+ // The loop node itself succeeds as a scheduling unit — even at max cycles
529
+ // the linear runner continued to the next phase. Individual review/fix
530
+ // results carry their own success flags for the run-level rollup.
531
+ return { results, status: "succeeded", outputVars };
532
+ }
533
+ async runGenericLoop(phase, outputs) {
534
+ const loop = phase.generic_loop;
535
+ const phaseName = phase.name;
536
+ const MAX_ITER = loop.max_iterations;
537
+ const { db, workflowId, scratch, config } = this.run;
538
+ const results = [];
539
+ // Reply-gate loops resume mid-flight: read the saved iteration from scratch.
540
+ const scratchKey = loop.scratch_key;
541
+ const scratchSlot = (scratchKey
542
+ ? scratch[scratchKey] ?? {}
543
+ : {});
544
+ const resumeFromIter = loop.gate_kind === "reply" && typeof scratchSlot.iteration === "number"
545
+ ? Math.min(scratchSlot.iteration, MAX_ITER)
546
+ : 0;
547
+ let iteration = resumeFromIter;
548
+ let complete = false;
549
+ let previousOutput = (scratchSlot.lastOutputExecutionId && db
550
+ ? db.executions.getExecutionOutput(scratchSlot.lastOutputExecutionId) ?? ""
551
+ : scratchSlot.lastOutput ?? "");
552
+ await this.reporter.step(phaseName, "running");
553
+ while (!complete && iteration < MAX_ITER) {
554
+ iteration++;
555
+ const iterLabel = PhaseRef.iter(phaseName, iteration).format();
556
+ await this.reporter.onStart(iterLabel);
557
+ const iterCtx = {
558
+ iteration,
559
+ maxIterations: MAX_ITER,
560
+ previousOutput: loop.fresh_context ? "" : previousOutput,
561
+ phaseOutputs: outputs,
562
+ scratch,
563
+ };
564
+ const prompt = buildPhasePrompt(phase, this.run.ctx, iterCtx);
565
+ const { model, variant } = this.resolveModelVariant(phase.model, phase.variant, phaseName);
566
+ const ir = await this.runPhaseCall(iterLabel, prompt, phase, model, variant);
567
+ if (ir.skipped) {
568
+ if (ir.reason === "running") {
569
+ await this.reporter.message(phase.messages?.on_skipped_done, { iteration });
570
+ return { results, status: "failed", aborted: true };
571
+ }
572
+ results.push({ phase: iterLabel, success: true, output: "Already completed" });
573
+ complete = true;
574
+ break;
575
+ }
576
+ results.push({ phase: iterLabel, ...pickResult(ir.result) });
577
+ await this.reporter.onEnd(iterLabel, results[results.length - 1]);
578
+ if (!ir.result.success) {
579
+ if (!isTerminated(ir.result.error)) {
580
+ await this.reporter.step(phaseName, "failed", phase.messages?.on_failure, { iteration });
581
+ }
582
+ this.reporter.failWorkflow(ir.result.error);
583
+ const outputVars = phase.output_var
584
+ ? { [phase.output_var]: { completed: false, iterations: iteration } }
585
+ : undefined;
586
+ return { results, status: "failed", outputVars };
587
+ }
588
+ const iterOutput = ir.result.output || "";
589
+ if (!loop.fresh_context) {
590
+ const combined = previousOutput ? `${previousOutput}\n${iterOutput}` : iterOutput;
591
+ previousOutput = combined.length > MAX_PREV_OUTPUT_BYTES ? combined.slice(-MAX_PREV_OUTPUT_BYTES) : combined;
592
+ }
593
+ let conditionMet = false;
594
+ if (loop.until) {
595
+ conditionMet = evalUntilExpression(loop.until, {
596
+ output: iterOutput,
597
+ scratch,
598
+ ...Object.fromEntries(Object.entries(this.run.ctx)
599
+ .filter(([, v]) => typeof v === "string")
600
+ .map(([k, v]) => [k, v])),
601
+ });
602
+ }
603
+ if (!conditionMet && loop.until_bash) {
604
+ try {
605
+ validateShellCommand(loop.until_bash);
606
+ execSync(loop.until_bash, { timeout: 30_000, stdio: "pipe", cwd: config.sandboxDir ?? config.cwd });
607
+ conditionMet = true;
608
+ }
609
+ catch {
610
+ conditionMet = false;
611
+ }
612
+ }
613
+ const iterExecutionId = "executionId" in ir ? ir.executionId : undefined;
614
+ if (iterExecutionId && db)
615
+ db.executions.recordOutputText(iterExecutionId, iterOutput);
616
+ if (conditionMet) {
617
+ complete = true;
618
+ if (scratchKey && db && workflowId) {
619
+ const slot = { ...scratchSlot, iteration, ready: true };
620
+ if (iterExecutionId)
621
+ slot.lastOutputExecutionId = iterExecutionId;
622
+ delete slot.lastOutput;
623
+ db.runs.mergeScratch(workflowId, { [scratchKey]: slot });
624
+ scratch[scratchKey] = slot;
625
+ }
626
+ this.reporter.persistPhase(iterLabel, `iteration ${iteration} — condition met`);
627
+ await this.reporter.step(phaseName, "done");
628
+ break;
629
+ }
630
+ // Interactive gate between iterations.
631
+ if (loop.interactive && !complete && db && workflowId) {
632
+ const isReply = loop.gate_kind === "reply";
633
+ const gateMsg = loop.gate_message
634
+ ? renderTemplate(loop.gate_message, { ...this.run.ctx, phaseOutputs: outputs, iteration, maxIterations: MAX_ITER, scratch })
635
+ : `Loop iteration ${iteration}/${MAX_ITER} complete.`;
636
+ let gateScratchPatch;
637
+ if (scratchKey) {
638
+ const slot = {
639
+ ...scratch[scratchKey],
640
+ iteration,
641
+ };
642
+ if (iterExecutionId)
643
+ slot.lastOutputExecutionId = iterExecutionId;
644
+ delete slot.lastOutput;
645
+ scratch[scratchKey] = slot;
646
+ gateScratchPatch = { [scratchKey]: slot };
647
+ }
648
+ const approvalId = randomUUID();
649
+ // One transaction: persist the iteration scratch, create the pending
650
+ // gate, append the waiting_approval marker, and pause the run.
651
+ db.runs.pauseForApproval(workflowId, {
652
+ id: approvalId,
653
+ workflowRunId: workflowId,
654
+ gate: iterLabel,
655
+ summary: gateMsg,
656
+ kind: isReply ? "reply" : "approve",
657
+ requestedBy: this.run.ctx.sender,
658
+ createdAt: new Date().toISOString(),
659
+ }, {
660
+ phase: "waiting_approval",
661
+ summary: `Waiting for ${isReply ? "reply" : "approval"}: ${iterLabel} (${approvalId})`,
662
+ }, gateScratchPatch);
663
+ await this.reporter.step(phaseName, "awaiting");
664
+ if (isReply) {
665
+ const parts = [iterOutput.trim(), gateMsg.trim()].filter(Boolean);
666
+ if (parts.length > 0)
667
+ await this.reporter.postNote(parts.join("\n\n---\n\n"));
668
+ }
669
+ else {
670
+ await this.reporter.postNote(`**${phaseName} iteration ${iteration}/${MAX_ITER} complete** — approval required to continue.\n\n` +
671
+ `${gateMsg}\n\n` +
672
+ `**To continue:** comment \`@last-light approve\`\n` +
673
+ `**To abort:** comment \`@last-light reject [reason]\``);
674
+ }
675
+ return { results, status: "succeeded", paused: true };
676
+ }
677
+ this.reporter.persistPhase(iterLabel);
678
+ }
679
+ if (!complete) {
680
+ await this.reporter.step(phaseName, "failed", phase.messages?.on_failure, {
681
+ iteration,
682
+ maxIterations: MAX_ITER,
683
+ });
684
+ }
685
+ const outputVars = phase.output_var
686
+ ? { [phase.output_var]: { completed: complete, iterations: iteration } }
687
+ : undefined;
688
+ return { results, status: "succeeded", outputVars };
689
+ }
690
+ }
691
+ //# sourceMappingURL=phase-executor.js.map