lastlight 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (478) hide show
  1. package/README.md +265 -118
  2. package/agent-context/rules.md +62 -9
  3. package/agent-context/security.md +69 -0
  4. package/config/default.yaml +89 -0
  5. package/deploy/.env.production.example +20 -2
  6. package/deploy/entrypoint.sh +16 -17
  7. package/deploy/native/README.md +160 -0
  8. package/deploy/native/install.sh +110 -0
  9. package/deploy/native/lastlight.env.example +95 -0
  10. package/deploy/native/lastlight.service +50 -0
  11. package/deploy/sandbox-entrypoint.sh +59 -42
  12. package/dist/admin/auth.d.ts +1 -1
  13. package/dist/admin/auth.js +3 -1
  14. package/dist/admin/auth.js.map +1 -1
  15. package/dist/admin/chat-session-reader.d.ts +11 -14
  16. package/dist/admin/chat-session-reader.js +27 -51
  17. package/dist/admin/chat-session-reader.js.map +1 -1
  18. package/dist/admin/docker.d.ts +37 -0
  19. package/dist/admin/docker.js +86 -1
  20. package/dist/admin/docker.js.map +1 -1
  21. package/dist/admin/index.js +38 -3
  22. package/dist/admin/index.js.map +1 -1
  23. package/dist/admin/log-search.test.d.ts +1 -0
  24. package/dist/admin/log-search.test.js +78 -0
  25. package/dist/admin/log-search.test.js.map +1 -0
  26. package/dist/admin/routes.d.ts +52 -3
  27. package/dist/admin/routes.js +780 -63
  28. package/dist/admin/routes.js.map +1 -1
  29. package/dist/admin/routes.test.js +680 -14
  30. package/dist/admin/routes.test.js.map +1 -1
  31. package/dist/admin/server-logs.test.d.ts +1 -0
  32. package/dist/admin/server-logs.test.js +58 -0
  33. package/dist/admin/server-logs.test.js.map +1 -0
  34. package/dist/admin/sessions.d.ts +26 -39
  35. package/dist/admin/sessions.js +86 -310
  36. package/dist/admin/sessions.js.map +1 -1
  37. package/dist/admin/sessions.test.d.ts +1 -0
  38. package/dist/admin/sessions.test.js +78 -0
  39. package/dist/admin/sessions.test.js.map +1 -0
  40. package/dist/admin/version.d.ts +15 -0
  41. package/dist/admin/version.js +73 -0
  42. package/dist/admin/version.js.map +1 -0
  43. package/dist/admin/version.test.d.ts +1 -0
  44. package/dist/admin/version.test.js +71 -0
  45. package/dist/admin/version.test.js.map +1 -0
  46. package/dist/cli-config.d.ts +55 -0
  47. package/dist/cli-config.js +119 -0
  48. package/dist/cli-config.js.map +1 -0
  49. package/dist/cli-config.test.d.ts +1 -0
  50. package/dist/cli-config.test.js +92 -0
  51. package/dist/cli-config.test.js.map +1 -0
  52. package/dist/cli-format.d.ts +19 -0
  53. package/dist/cli-format.js +107 -0
  54. package/dist/cli-format.js.map +1 -0
  55. package/dist/cli-server.d.ts +73 -0
  56. package/dist/cli-server.js +347 -0
  57. package/dist/cli-server.js.map +1 -0
  58. package/dist/cli-server.test.d.ts +1 -0
  59. package/dist/cli-server.test.js +38 -0
  60. package/dist/cli-server.test.js.map +1 -0
  61. package/dist/cli-timeline.d.ts +35 -0
  62. package/dist/cli-timeline.js +373 -0
  63. package/dist/cli-timeline.js.map +1 -0
  64. package/dist/cli-timeline.test.d.ts +1 -0
  65. package/dist/cli-timeline.test.js +104 -0
  66. package/dist/cli-timeline.test.js.map +1 -0
  67. package/dist/cli.d.ts +0 -19
  68. package/dist/cli.js +910 -194
  69. package/dist/cli.js.map +1 -1
  70. package/dist/config-overlay.test.d.ts +1 -0
  71. package/dist/config-overlay.test.js +112 -0
  72. package/dist/config-overlay.test.js.map +1 -0
  73. package/dist/config-resolve.d.ts +28 -0
  74. package/dist/config-resolve.js +47 -0
  75. package/dist/config-resolve.js.map +1 -0
  76. package/dist/config-resolve.test.d.ts +1 -0
  77. package/dist/config-resolve.test.js +68 -0
  78. package/dist/config-resolve.test.js.map +1 -0
  79. package/dist/config.d.ts +62 -49
  80. package/dist/config.js +452 -76
  81. package/dist/config.js.map +1 -1
  82. package/dist/config.test.js +201 -32
  83. package/dist/config.test.js.map +1 -1
  84. package/dist/connectors/github-webhook.js +83 -5
  85. package/dist/connectors/github-webhook.js.map +1 -1
  86. package/dist/connectors/github-webhook.test.d.ts +1 -0
  87. package/dist/connectors/github-webhook.test.js +156 -0
  88. package/dist/connectors/github-webhook.test.js.map +1 -0
  89. package/dist/connectors/messaging/session-manager.d.ts +18 -0
  90. package/dist/connectors/messaging/session-manager.js +83 -2
  91. package/dist/connectors/messaging/session-manager.js.map +1 -1
  92. package/dist/connectors/messaging/session-manager.test.d.ts +1 -0
  93. package/dist/connectors/messaging/session-manager.test.js +144 -0
  94. package/dist/connectors/messaging/session-manager.test.js.map +1 -0
  95. package/dist/connectors/slack/connector.d.ts +9 -0
  96. package/dist/connectors/slack/connector.js +15 -0
  97. package/dist/connectors/slack/connector.js.map +1 -1
  98. package/dist/connectors/slack/mrkdwn.js +81 -0
  99. package/dist/connectors/slack/mrkdwn.js.map +1 -1
  100. package/dist/connectors/slack/mrkdwn.test.js +49 -0
  101. package/dist/connectors/slack/mrkdwn.test.js.map +1 -1
  102. package/dist/connectors/types.d.ts +1 -1
  103. package/dist/cron/fanout.d.ts +33 -0
  104. package/dist/cron/fanout.js +55 -0
  105. package/dist/cron/fanout.js.map +1 -0
  106. package/dist/cron/fanout.test.d.ts +1 -0
  107. package/dist/cron/fanout.test.js +73 -0
  108. package/dist/cron/fanout.test.js.map +1 -0
  109. package/dist/cron/jobs.d.ts +5 -0
  110. package/dist/cron/jobs.js +10 -3
  111. package/dist/cron/jobs.js.map +1 -1
  112. package/dist/cron/scheduler.d.ts +12 -0
  113. package/dist/cron/scheduler.js +27 -1
  114. package/dist/cron/scheduler.js.map +1 -1
  115. package/dist/engine/agent-executor.d.ts +158 -0
  116. package/dist/engine/agent-executor.js +1140 -0
  117. package/dist/engine/agent-executor.js.map +1 -0
  118. package/dist/engine/agent-executor.test.d.ts +1 -0
  119. package/dist/engine/agent-executor.test.js +395 -0
  120. package/dist/engine/agent-executor.test.js.map +1 -0
  121. package/dist/engine/chat-runner.d.ts +123 -0
  122. package/dist/engine/chat-runner.js +379 -0
  123. package/dist/engine/chat-runner.js.map +1 -0
  124. package/dist/engine/chat-runner.test.d.ts +1 -0
  125. package/dist/engine/chat-runner.test.js +104 -0
  126. package/dist/engine/chat-runner.test.js.map +1 -0
  127. package/dist/engine/chat-skills.d.ts +45 -0
  128. package/dist/engine/chat-skills.js +184 -0
  129. package/dist/engine/chat-skills.js.map +1 -0
  130. package/dist/engine/chat-skills.test.d.ts +1 -0
  131. package/dist/engine/chat-skills.test.js +20 -0
  132. package/dist/engine/chat-skills.test.js.map +1 -0
  133. package/dist/engine/chat.d.ts +20 -23
  134. package/dist/engine/chat.js +251 -155
  135. package/dist/engine/chat.js.map +1 -1
  136. package/dist/engine/chat.test.d.ts +1 -0
  137. package/dist/engine/chat.test.js +42 -0
  138. package/dist/engine/chat.test.js.map +1 -0
  139. package/dist/engine/classifier.d.ts +35 -2
  140. package/dist/engine/classifier.js +170 -31
  141. package/dist/engine/classifier.js.map +1 -1
  142. package/dist/engine/classifier.test.d.ts +1 -0
  143. package/dist/engine/classifier.test.js +115 -0
  144. package/dist/engine/classifier.test.js.map +1 -0
  145. package/dist/engine/dispatcher.d.ts +60 -0
  146. package/dist/engine/dispatcher.js +601 -0
  147. package/dist/engine/dispatcher.js.map +1 -0
  148. package/dist/engine/dispatcher.test.d.ts +1 -0
  149. package/dist/engine/dispatcher.test.js +511 -0
  150. package/dist/engine/dispatcher.test.js.map +1 -0
  151. package/dist/engine/event-shim.d.ts +117 -0
  152. package/dist/engine/event-shim.js +435 -0
  153. package/dist/engine/event-shim.js.map +1 -0
  154. package/dist/engine/event-shim.test.d.ts +1 -0
  155. package/dist/engine/event-shim.test.js +194 -0
  156. package/dist/engine/event-shim.test.js.map +1 -0
  157. package/dist/engine/git-auth.d.ts +13 -17
  158. package/dist/engine/git-auth.js +99 -42
  159. package/dist/engine/git-auth.js.map +1 -1
  160. package/dist/engine/git-auth.test.d.ts +1 -0
  161. package/dist/engine/git-auth.test.js +117 -0
  162. package/dist/engine/git-auth.test.js.map +1 -0
  163. package/dist/engine/github-app-client.d.ts +7 -0
  164. package/dist/engine/github-app-client.js +16 -0
  165. package/dist/engine/github-app-client.js.map +1 -0
  166. package/dist/engine/github-app-client.test.d.ts +1 -0
  167. package/dist/engine/github-app-client.test.js +44 -0
  168. package/dist/engine/github-app-client.test.js.map +1 -0
  169. package/dist/engine/github-tools.d.ts +12 -0
  170. package/dist/engine/github-tools.js +261 -0
  171. package/dist/engine/github-tools.js.map +1 -0
  172. package/dist/engine/github.d.ts +1027 -43
  173. package/dist/engine/github.js +153 -14
  174. package/dist/engine/github.js.map +1 -1
  175. package/dist/engine/llm.d.ts +47 -0
  176. package/dist/engine/llm.js +221 -0
  177. package/dist/engine/llm.js.map +1 -0
  178. package/dist/engine/llm.test.d.ts +1 -0
  179. package/dist/engine/llm.test.js +189 -0
  180. package/dist/engine/llm.test.js.map +1 -0
  181. package/dist/engine/profiles.d.ts +249 -0
  182. package/dist/engine/profiles.js +42 -0
  183. package/dist/engine/profiles.js.map +1 -0
  184. package/dist/engine/router.d.ts +12 -6
  185. package/dist/engine/router.js +339 -81
  186. package/dist/engine/router.js.map +1 -1
  187. package/dist/engine/router.test.js +428 -78
  188. package/dist/engine/router.test.js.map +1 -1
  189. package/dist/engine/screen.d.ts +41 -0
  190. package/dist/engine/screen.js +93 -0
  191. package/dist/engine/screen.js.map +1 -0
  192. package/dist/engine/screen.test.d.ts +1 -0
  193. package/dist/engine/screen.test.js +82 -0
  194. package/dist/engine/screen.test.js.map +1 -0
  195. package/dist/index.js +423 -437
  196. package/dist/index.js.map +1 -1
  197. package/dist/managed-repos.d.ts +7 -9
  198. package/dist/managed-repos.js +12 -15
  199. package/dist/managed-repos.js.map +1 -1
  200. package/dist/managed-repos.test.js +24 -19
  201. package/dist/managed-repos.test.js.map +1 -1
  202. package/dist/notify/index.d.ts +15 -0
  203. package/dist/notify/index.js +6 -0
  204. package/dist/notify/index.js.map +1 -0
  205. package/dist/notify/model.d.ts +57 -0
  206. package/dist/notify/model.js +89 -0
  207. package/dist/notify/model.js.map +1 -0
  208. package/dist/notify/model.test.d.ts +1 -0
  209. package/dist/notify/model.test.js +109 -0
  210. package/dist/notify/model.test.js.map +1 -0
  211. package/dist/notify/notifier.d.ts +17 -0
  212. package/dist/notify/notifier.js +87 -0
  213. package/dist/notify/notifier.js.map +1 -0
  214. package/dist/notify/notifier.test.d.ts +1 -0
  215. package/dist/notify/notifier.test.js +100 -0
  216. package/dist/notify/notifier.test.js.map +1 -0
  217. package/dist/notify/render.d.ts +21 -0
  218. package/dist/notify/render.js +59 -0
  219. package/dist/notify/render.js.map +1 -0
  220. package/dist/notify/render.test.d.ts +1 -0
  221. package/dist/notify/render.test.js +65 -0
  222. package/dist/notify/render.test.js.map +1 -0
  223. package/dist/notify/transports/github.d.ts +26 -0
  224. package/dist/notify/transports/github.js +26 -0
  225. package/dist/notify/transports/github.js.map +1 -0
  226. package/dist/notify/transports/slack.d.ts +26 -0
  227. package/dist/notify/transports/slack.js +28 -0
  228. package/dist/notify/transports/slack.js.map +1 -0
  229. package/dist/notify/transports.test.d.ts +1 -0
  230. package/dist/notify/transports.test.js +71 -0
  231. package/dist/notify/transports.test.js.map +1 -0
  232. package/dist/notify/types.d.ts +95 -0
  233. package/dist/notify/types.js +16 -0
  234. package/dist/notify/types.js.map +1 -0
  235. package/dist/sandbox/docker-compose.test.d.ts +1 -0
  236. package/dist/sandbox/docker-compose.test.js +128 -0
  237. package/dist/sandbox/docker-compose.test.js.map +1 -0
  238. package/dist/sandbox/docker.d.ts +97 -9
  239. package/dist/sandbox/docker.js +192 -41
  240. package/dist/sandbox/docker.js.map +1 -1
  241. package/dist/sandbox/docker.test.d.ts +1 -0
  242. package/dist/sandbox/docker.test.js +137 -0
  243. package/dist/sandbox/docker.test.js.map +1 -0
  244. package/dist/sandbox/egress-allowlist.d.ts +69 -0
  245. package/dist/sandbox/egress-allowlist.js +164 -0
  246. package/dist/sandbox/egress-allowlist.js.map +1 -0
  247. package/dist/sandbox/egress-allowlist.test.d.ts +1 -0
  248. package/dist/sandbox/egress-allowlist.test.js +65 -0
  249. package/dist/sandbox/egress-allowlist.test.js.map +1 -0
  250. package/dist/sandbox/egress-firewall-config.d.ts +127 -0
  251. package/dist/sandbox/egress-firewall-config.js +434 -0
  252. package/dist/sandbox/egress-firewall-config.js.map +1 -0
  253. package/dist/sandbox/egress-firewall-config.test.d.ts +1 -0
  254. package/dist/sandbox/egress-firewall-config.test.js +244 -0
  255. package/dist/sandbox/egress-firewall-config.test.js.map +1 -0
  256. package/dist/sandbox/images.d.ts +23 -0
  257. package/dist/sandbox/images.js +55 -0
  258. package/dist/sandbox/images.js.map +1 -0
  259. package/dist/sandbox/index.d.ts +82 -3
  260. package/dist/sandbox/index.js +223 -93
  261. package/dist/sandbox/index.js.map +1 -1
  262. package/dist/sandbox/index.test.d.ts +1 -0
  263. package/dist/sandbox/index.test.js +157 -0
  264. package/dist/sandbox/index.test.js.map +1 -0
  265. package/dist/session-log.d.ts +63 -0
  266. package/dist/session-log.js +290 -0
  267. package/dist/session-log.js.map +1 -0
  268. package/dist/session-log.test.d.ts +1 -0
  269. package/dist/session-log.test.js +85 -0
  270. package/dist/session-log.test.js.map +1 -0
  271. package/dist/setup.d.ts +31 -12
  272. package/dist/setup.js +516 -174
  273. package/dist/setup.js.map +1 -1
  274. package/dist/setup.test.js +138 -6
  275. package/dist/setup.test.js.map +1 -1
  276. package/dist/state/approval-store.d.ts +86 -0
  277. package/dist/state/approval-store.js +140 -0
  278. package/dist/state/approval-store.js.map +1 -0
  279. package/dist/state/build-assets.d.ts +50 -0
  280. package/dist/state/build-assets.js +154 -0
  281. package/dist/state/build-assets.js.map +1 -0
  282. package/dist/state/build-assets.test.d.ts +1 -0
  283. package/dist/state/build-assets.test.js +106 -0
  284. package/dist/state/build-assets.test.js.map +1 -0
  285. package/dist/state/db.d.ts +65 -333
  286. package/dist/state/db.js +112 -835
  287. package/dist/state/db.js.map +1 -1
  288. package/dist/state/db.test.js +127 -91
  289. package/dist/state/db.test.js.map +1 -1
  290. package/dist/state/execution-store.d.ts +257 -0
  291. package/dist/state/execution-store.js +527 -0
  292. package/dist/state/execution-store.js.map +1 -0
  293. package/dist/state/migrate.d.ts +15 -0
  294. package/dist/state/migrate.js +175 -0
  295. package/dist/state/migrate.js.map +1 -0
  296. package/dist/state/workflow-run-store.d.ts +195 -0
  297. package/dist/state/workflow-run-store.js +363 -0
  298. package/dist/state/workflow-run-store.js.map +1 -0
  299. package/dist/state/workflow-run-store.test.d.ts +1 -0
  300. package/dist/state/workflow-run-store.test.js +264 -0
  301. package/dist/state/workflow-run-store.test.js.map +1 -0
  302. package/dist/telemetry/index.d.ts +38 -0
  303. package/dist/telemetry/index.js +253 -0
  304. package/dist/telemetry/index.js.map +1 -0
  305. package/dist/telemetry/index.test.d.ts +1 -0
  306. package/dist/telemetry/index.test.js +73 -0
  307. package/dist/telemetry/index.test.js.map +1 -0
  308. package/dist/telemetry/pi-events.d.ts +12 -0
  309. package/dist/telemetry/pi-events.js +134 -0
  310. package/dist/telemetry/pi-events.js.map +1 -0
  311. package/dist/telemetry/pi-events.test.d.ts +1 -0
  312. package/dist/telemetry/pi-events.test.js +69 -0
  313. package/dist/telemetry/pi-events.test.js.map +1 -0
  314. package/dist/workflows/dag.d.ts +13 -1
  315. package/dist/workflows/dag.js +7 -3
  316. package/dist/workflows/dag.js.map +1 -1
  317. package/dist/workflows/dag.test.js +22 -0
  318. package/dist/workflows/dag.test.js.map +1 -1
  319. package/dist/workflows/golden-build.test.d.ts +1 -0
  320. package/dist/workflows/golden-build.test.js +45 -0
  321. package/dist/workflows/golden-build.test.js.map +1 -0
  322. package/dist/workflows/loader-overlay.test.d.ts +1 -0
  323. package/dist/workflows/loader-overlay.test.js +101 -0
  324. package/dist/workflows/loader-overlay.test.js.map +1 -0
  325. package/dist/workflows/loader.d.ts +36 -11
  326. package/dist/workflows/loader.js +311 -87
  327. package/dist/workflows/loader.js.map +1 -1
  328. package/dist/workflows/loader.test.js +114 -10
  329. package/dist/workflows/loader.test.js.map +1 -1
  330. package/dist/workflows/loop-eval.js +2 -0
  331. package/dist/workflows/loop-eval.js.map +1 -1
  332. package/dist/workflows/loop-eval.test.js +14 -0
  333. package/dist/workflows/loop-eval.test.js.map +1 -1
  334. package/dist/workflows/phase-executor.d.ts +145 -0
  335. package/dist/workflows/phase-executor.js +691 -0
  336. package/dist/workflows/phase-executor.js.map +1 -0
  337. package/dist/workflows/phase-executor.test.d.ts +1 -0
  338. package/dist/workflows/phase-executor.test.js +434 -0
  339. package/dist/workflows/phase-executor.test.js.map +1 -0
  340. package/dist/workflows/phase-ref.d.ts +44 -0
  341. package/dist/workflows/phase-ref.js +78 -0
  342. package/dist/workflows/phase-ref.js.map +1 -0
  343. package/dist/workflows/phase-ref.test.d.ts +1 -0
  344. package/dist/workflows/phase-ref.test.js +24 -0
  345. package/dist/workflows/phase-ref.test.js.map +1 -0
  346. package/dist/workflows/resume.d.ts +5 -11
  347. package/dist/workflows/resume.js +88 -7
  348. package/dist/workflows/resume.js.map +1 -1
  349. package/dist/workflows/runner.d.ts +34 -27
  350. package/dist/workflows/runner.js +252 -930
  351. package/dist/workflows/runner.js.map +1 -1
  352. package/dist/workflows/runner.test.js +346 -35
  353. package/dist/workflows/runner.test.js.map +1 -1
  354. package/dist/workflows/schema.d.ts +52 -10
  355. package/dist/workflows/schema.js +147 -7
  356. package/dist/workflows/schema.js.map +1 -1
  357. package/dist/workflows/simple.d.ts +67 -3
  358. package/dist/workflows/simple.js +248 -87
  359. package/dist/workflows/simple.js.map +1 -1
  360. package/dist/workflows/simple.test.d.ts +1 -0
  361. package/dist/workflows/simple.test.js +107 -0
  362. package/dist/workflows/simple.test.js.map +1 -0
  363. package/dist/workflows/templates.d.ts +29 -0
  364. package/dist/workflows/templates.js +42 -4
  365. package/dist/workflows/templates.js.map +1 -1
  366. package/dist/workflows/templates.test.js +103 -0
  367. package/dist/workflows/templates.test.js.map +1 -1
  368. package/dist/workflows/triggers.d.ts +21 -0
  369. package/dist/workflows/triggers.js +44 -0
  370. package/dist/workflows/triggers.js.map +1 -0
  371. package/dist/workflows/verdict.d.ts +19 -0
  372. package/dist/workflows/verdict.js +30 -0
  373. package/dist/workflows/verdict.js.map +1 -0
  374. package/dist/workflows/verdict.test.d.ts +1 -0
  375. package/dist/workflows/verdict.test.js +44 -0
  376. package/dist/workflows/verdict.test.js.map +1 -0
  377. package/dist/worktree/manager.d.ts +1 -1
  378. package/dist/worktree/manager.js +14 -14
  379. package/dist/worktree/manager.js.map +1 -1
  380. package/dist/worktree/manager.test.d.ts +1 -0
  381. package/dist/worktree/manager.test.js +87 -0
  382. package/dist/worktree/manager.test.js.map +1 -0
  383. package/docker-compose.yml +206 -5
  384. package/package.json +19 -7
  385. package/sandbox.Dockerfile +97 -18
  386. package/skills/README.md +40 -0
  387. package/skills/browser-qa/SKILL.md +193 -0
  388. package/skills/browser-qa/scripts/agent-browser.mjs +380 -0
  389. package/skills/building/SKILL.md +95 -0
  390. package/skills/chat/SKILL.md +30 -11
  391. package/skills/code-review/SKILL.md +59 -0
  392. package/skills/debug-production/SKILL.md +108 -0
  393. package/skills/demo/SKILL.md +194 -0
  394. package/skills/demo/scripts/compose-demo.sh +205 -0
  395. package/skills/issue-answer/SKILL.md +90 -0
  396. package/skills/issue-comment/SKILL.md +38 -24
  397. package/skills/issue-triage/SKILL.md +103 -37
  398. package/skills/issue-triage/references/AGENT-BRIEF.md +61 -0
  399. package/skills/pr-comment/SKILL.md +64 -0
  400. package/skills/pr-review/SKILL.md +65 -49
  401. package/skills/qa-test/SKILL.md +97 -0
  402. package/skills/repo-health/SKILL.md +48 -45
  403. package/skills/security-feedback/SKILL.md +173 -0
  404. package/skills/security-feedback/references/templates.md +77 -0
  405. package/skills/security-review/SKILL.md +213 -0
  406. package/skills/security-review/references/issue-format.md +302 -0
  407. package/skills/verify/SKILL.md +118 -0
  408. package/workflows/answer.yaml +37 -0
  409. package/workflows/build.yaml +32 -22
  410. package/workflows/cron-security.yaml +6 -0
  411. package/workflows/demo.yaml +55 -0
  412. package/workflows/explore.yaml +9 -0
  413. package/workflows/pr-comment.yaml +16 -0
  414. package/workflows/pr-fix.yaml +3 -0
  415. package/workflows/pr-review.yaml +4 -1
  416. package/workflows/prompts/answer.md +67 -0
  417. package/workflows/prompts/architect.md +24 -7
  418. package/workflows/prompts/demo.md +100 -0
  419. package/workflows/prompts/executor.md +29 -20
  420. package/workflows/prompts/explore-ask.md +51 -22
  421. package/workflows/prompts/explore-publish.md +2 -2
  422. package/workflows/prompts/explore-read.md +24 -10
  423. package/workflows/prompts/explore-synthesize.md +15 -8
  424. package/workflows/prompts/fix.md +9 -10
  425. package/workflows/prompts/guardrails.md +26 -8
  426. package/workflows/prompts/pr-fix.md +6 -7
  427. package/workflows/prompts/pr.md +11 -10
  428. package/workflows/prompts/qa-browser.md +95 -0
  429. package/workflows/prompts/qa-synth.md +44 -0
  430. package/workflows/prompts/qa-test.md +60 -0
  431. package/workflows/prompts/re-reviewer.md +8 -9
  432. package/workflows/prompts/reviewer.md +10 -9
  433. package/workflows/prompts/verify-browser.md +93 -0
  434. package/workflows/prompts/verify-synth.md +44 -0
  435. package/workflows/prompts/verify.md +63 -0
  436. package/workflows/qa-test.yaml +87 -0
  437. package/workflows/security-feedback.yaml +25 -0
  438. package/workflows/security-review.yaml +29 -0
  439. package/workflows/verify.yaml +85 -0
  440. package/deploy/mcp-config.tmpl.json +0 -14
  441. package/dist/cron/rate-limits.d.ts +0 -26
  442. package/dist/cron/rate-limits.js +0 -193
  443. package/dist/cron/rate-limits.js.map +0 -1
  444. package/dist/engine/executor.d.ts +0 -75
  445. package/dist/engine/executor.js +0 -292
  446. package/dist/engine/executor.js.map +0 -1
  447. package/mcp-github-app/package.json +0 -18
  448. package/mcp-github-app/src/auth.js +0 -66
  449. package/mcp-github-app/src/github.js +0 -371
  450. package/mcp-github-app/src/index.js +0 -534
  451. package/skills/devops/webhook-subscriptions/SKILL.md +0 -180
  452. package/skills/github/DESCRIPTION.md +0 -3
  453. package/skills/github/codebase-inspection/SKILL.md +0 -115
  454. package/skills/github/github-auth/SKILL.md +0 -54
  455. package/skills/github/github-auth/scripts/gh-env.sh +0 -66
  456. package/skills/github/github-code-review/SKILL.md +0 -480
  457. package/skills/github/github-code-review/references/review-output-template.md +0 -74
  458. package/skills/github/github-issues/SKILL.md +0 -369
  459. package/skills/github/github-issues/templates/bug-report.md +0 -35
  460. package/skills/github/github-issues/templates/feature-request.md +0 -31
  461. package/skills/github/github-pr-workflow/SKILL.md +0 -395
  462. package/skills/github/github-pr-workflow/references/ci-troubleshooting.md +0 -183
  463. package/skills/github/github-pr-workflow/references/conventional-commits.md +0 -71
  464. package/skills/github/github-pr-workflow/templates/pr-body-bugfix.md +0 -35
  465. package/skills/github/github-pr-workflow/templates/pr-body-feature.md +0 -33
  466. package/skills/github/github-repo-management/SKILL.md +0 -515
  467. package/skills/github/github-repo-management/references/github-api-cheatsheet.md +0 -161
  468. package/skills/github-orchestrator/SKILL.md +0 -103
  469. package/skills/mcp/DESCRIPTION.md +0 -3
  470. package/skills/mcp/mcporter/SKILL.md +0 -122
  471. package/skills/mcp/native-mcp/SKILL.md +0 -356
  472. package/skills/software-development/architect/SKILL.md +0 -154
  473. package/skills/software-development/assure-guardrails/SKILL.md +0 -239
  474. package/skills/software-development/plan/SKILL.md +0 -64
  475. package/skills/software-development/requesting-code-review/SKILL.md +0 -291
  476. package/skills/software-development/subagent-driven-development/SKILL.md +0 -433
  477. package/skills/software-development/systematic-debugging/SKILL.md +0 -366
  478. package/skills/software-development/test-driven-development/SKILL.md +0 -342
@@ -0,0 +1,1140 @@
1
+ import { resolve, basename, join, relative } from "path";
2
+ import { appendFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "fs";
3
+ import { randomUUID } from "crypto";
4
+ import { createTaskSandbox, setupTaskWorktree, SANDBOX_IMAGE_QA, } from "../sandbox/index.js";
5
+ import { refreshGitAuth } from "./git-auth.js";
6
+ import { AGENTIC_PROFILE_FOR, GITHUB_PERMISSION_PROFILES, loadAgentContext, } from "./profiles.js";
7
+ import { AgenticShim, truncateForLog, safeStringify } from "./event-shim.js";
8
+ import { projectSlugForCwd } from "../session-log.js";
9
+ import { BuildAssetStore } from "../state/build-assets.js";
10
+ import { ALLOW_ALL_SENTINEL, DEFAULT_ALLOWLIST, mergeAllowlist } from "../sandbox/egress-allowlist.js";
11
+ import { getDockerSandboxOtelEnv, getOtelEnvForSandbox, recordError, recordExecutionMetrics, safeSpanAttributes, withSpan } from "../telemetry/index.js";
12
+ import { recordPiEvent } from "../telemetry/pi-events.js";
13
+ const DEFAULT_MODEL = "anthropic/claude-sonnet-4-6";
14
+ const DOCKER_WORKSPACE_DIR = "/home/agent/workspace";
15
+ // Directory holding one skill bundle per phase. Deliberately NOT named
16
+ // `.agents/skills` (pi's auto-discovery path): we map each phase's bundle
17
+ // explicitly via --skill / skillPaths so two phases sharing a workspace —
18
+ // sequential today, parallel via worktrees tomorrow — can never clobber each
19
+ // other's catalogue. The agent keeps cwd = the repo (no `cd` preamble on
20
+ // every command); the bundle is staged at the workspace ROOT — a sibling of
21
+ // the repo, never in its git tree — and reached by an absolute path. On
22
+ // docker/none that root is genuinely outside the repo. gondolin mounts only
23
+ // cwd, so there the bundle is staged under the repo and added to the
24
+ // checkout's local `.git/info/exclude` (never committed; see `excludeFromGit`).
25
+ const SKILL_BUNDLE_ROOT = ".lastlight-skills";
26
+ const THINKING_LEVELS = new Set([
27
+ "off", "minimal", "low", "medium", "high", "xhigh",
28
+ ]);
29
+ /**
30
+ * Stage this phase's declared skills into a per-phase bundle directory at
31
+ * `<workspaceRoot>/.lastlight-skills/<phaseKey>/<basename>/` and return the
32
+ * staged skill dirs, so the caller can point pi at them explicitly (via
33
+ * `--skill` for docker or `skillPaths` for the in-process backends). Each
34
+ * skill is a directory containing SKILL.md plus any `scripts/` / `references/`
35
+ * / `assets/` — the whole tree comes along.
36
+ *
37
+ * Keyed by phase so concurrent phases in one workspace never touch each
38
+ * other's bundle: only the phase's own `<phaseKey>` subtree is cleared, so a
39
+ * clean slate per phase doesn't disturb a sibling phase mid-run.
40
+ *
41
+ * `mode` controls how each skill lands:
42
+ * - "symlink": one symlink per skill → host source. gondolin/none, where pi
43
+ * reads skill files host-side / through the cwd mount. Zero-copy.
44
+ * - "copy": recursive copy. docker, where the agent's tools run inside the
45
+ * container and host symlink targets wouldn't resolve; the copy lands
46
+ * under the bind-mounted workspace.
47
+ *
48
+ * Returns `undefined` when the phase declares no skills (after clearing its
49
+ * bundle), so a phase with no `skills:` gets no catalogue at all.
50
+ */
51
+ export function stageSkillBundle(workspaceRoot, phaseKey, skillPaths, mode) {
52
+ const bundleDir = join(workspaceRoot, SKILL_BUNDLE_ROOT, phaseKey);
53
+ if (existsSync(bundleDir)) {
54
+ rmSync(bundleDir, { recursive: true, force: true });
55
+ }
56
+ if (!skillPaths?.length)
57
+ return undefined;
58
+ mkdirSync(bundleDir, { recursive: true });
59
+ const staged = [];
60
+ for (const hostPath of skillPaths) {
61
+ const dest = join(bundleDir, basename(hostPath));
62
+ if (mode === "symlink") {
63
+ symlinkSync(hostPath, dest, "dir");
64
+ }
65
+ else {
66
+ cpSync(hostPath, dest, { recursive: true, dereference: true });
67
+ }
68
+ staged.push(dest);
69
+ }
70
+ return staged;
71
+ }
72
+ /**
73
+ * Sanitized per-phase key for the skill bundle directory. Phase name first
74
+ * (unique even for loop iterations like `reviewer_fix_1`), then workflow name,
75
+ * then a constant fallback — so the bundle is always isolated per phase.
76
+ */
77
+ function skillBundleKey(config) {
78
+ const raw = config.telemetry?.phaseName || config.telemetry?.workflowName || "phase";
79
+ return raw.replace(/[^A-Za-z0-9_-]/g, "_") || "phase";
80
+ }
81
+ /**
82
+ * Add `entry` to a checkout's local `.git/info/exclude` (idempotent) so the
83
+ * agent's own `git add`/`commit` can never pick it up. This file lives inside
84
+ * `.git/` — it is never tracked, committed, or pushed, and it leaves the repo's
85
+ * real `.gitignore` untouched; the exclusion applies only to this ephemeral
86
+ * sandbox checkout. Used for the gondolin backend, where the skill bundle must
87
+ * be staged under cwd (the only mounted dir) rather than as an out-of-repo
88
+ * sibling. No-op when `repoDir` isn't a git checkout (e.g. the workspace root).
89
+ */
90
+ export function excludeFromGit(repoDir, entry) {
91
+ const gitDir = join(repoDir, ".git");
92
+ if (!existsSync(gitDir))
93
+ return; // not a checkout — nothing to exclude
94
+ const infoDir = join(gitDir, "info");
95
+ const excludeFile = join(infoDir, "exclude");
96
+ const line = `/${entry}/`;
97
+ let current = "";
98
+ try {
99
+ current = readFileSync(excludeFile, "utf8");
100
+ }
101
+ catch { /* may not exist yet */ }
102
+ if (current.split(/\r?\n/).includes(line))
103
+ return;
104
+ mkdirSync(infoDir, { recursive: true });
105
+ appendFileSync(excludeFile, `${current.length && !current.endsWith("\n") ? "\n" : ""}${line}\n`);
106
+ }
107
+ // ── Server-mode build assets ────────────────────────────────────────
108
+ //
109
+ // In "server" mode the per-phase handoff docs live in the Last Light store
110
+ // rather than being committed into the target repo. The seam is symmetric to
111
+ // the skill bundle but bidirectional: stage the run's stored docs into the
112
+ // repo's `.lastlight/<issueKey>/` before the agent runs (so a later phase /
113
+ // resumed run sees prior context), and harvest whatever the phase wrote back
114
+ // to the store afterwards. The directory is the SAME relative path as repo
115
+ // mode (`{{issueDir}}`), so prompts are unchanged except for gating their
116
+ // `git add .lastlight/ && commit` off — the dir is git-excluded here too as a
117
+ // backstop against the agent's `git add -A` feature commit sweeping it in.
118
+ const ARTIFACT_DIR_ROOT = ".lastlight";
119
+ /**
120
+ * Resolve the server-mode artifact context for a run, or undefined when not in
121
+ * server mode (the default — the whole seam is then skipped and behaviour is
122
+ * byte-for-byte repo mode). `hostRepoDir` is the host-visible repo checkout
123
+ * (for docker that's the bind-mounted workspace path, not the in-container one).
124
+ */
125
+ function serverArtifacts(config, hostRepoDir) {
126
+ if (config.buildAssets !== "server" || !config.buildAssetsDir || !config.buildAssetsKey) {
127
+ return undefined;
128
+ }
129
+ const store = new BuildAssetStore(config.buildAssetsDir);
130
+ const ref = config.buildAssetsKey;
131
+ return { store, ref, dir: join(hostRepoDir, ARTIFACT_DIR_ROOT, ref.issueKey), repoDir: hostRepoDir };
132
+ }
133
+ /** Stage stored docs into the workspace + exclude the dir from git (server mode). */
134
+ function stageArtifactsIn(art) {
135
+ if (!art)
136
+ return;
137
+ try {
138
+ // Stage the run's stored docs into `<repoDir>/.lastlight/<issueKey>/`. This
139
+ // is safe for every workflow shape here: pre-cloned workflows (build, pr-*)
140
+ // already have the checkout at `repoDir`, and non-pre-cloned ones (explore)
141
+ // clone the repo into a *subdir* — so `repoDir` is the workspace root and a
142
+ // `.lastlight/` there never collides with the agent's clone. (A future
143
+ // workflow that `git clone … .` into cwd would be the one exception.)
144
+ art.store.stageInto(art.ref, art.dir);
145
+ // Backstop the prompt-level commit gate: when this is a real checkout, keep
146
+ // the docs out of the agent's `git add -A` feature commit. No-op at the
147
+ // workspace root (no `.git`), where the docs sit outside the repo subtree
148
+ // and are never in the repo's git tree anyway.
149
+ excludeFromGit(art.repoDir, ARTIFACT_DIR_ROOT);
150
+ }
151
+ catch (err) {
152
+ const msg = err instanceof Error ? err.message : String(err);
153
+ console.warn(`[executor] Could not stage build assets: ${msg}`);
154
+ }
155
+ }
156
+ /** Persist docs the phase wrote back to the store (server mode). */
157
+ function harvestArtifactsOut(art) {
158
+ if (!art)
159
+ return;
160
+ try {
161
+ art.store.harvestFrom(art.ref, art.dir);
162
+ }
163
+ catch (err) {
164
+ const msg = err instanceof Error ? err.message : String(err);
165
+ console.warn(`[executor] Could not harvest build assets: ${msg}`);
166
+ }
167
+ }
168
+ /**
169
+ * Execute one workflow-phase agent task via agentic-pi.
170
+ *
171
+ * Sandbox backend (decided per call):
172
+ * 1. gondolin — agentic-pi's QEMU micro-VM. cwd is the host worktree,
173
+ * mounted at /workspace inside the VM.
174
+ * 2. docker — the legacy container path (`src/sandbox/docker.ts`). The
175
+ * entrypoint inside the container execs `agentic-pi run --sandbox none`.
176
+ * 3. none — agentic-pi in-process with cwd = the host worktree.
177
+ */
178
+ export async function executeAgent(prompt, config, opts) {
179
+ const taskId = opts?.taskId || `task-${randomUUID().slice(0, 8)}`;
180
+ const stateDir = config.stateDir || resolve("data");
181
+ const backend = config.sandbox ?? "gondolin";
182
+ // Mint a scoped GitHub App token. Same flow as the legacy executor —
183
+ // defense in depth so a downstream tool gating regression can't burn
184
+ // more access than the profile allowed.
185
+ //
186
+ // GITHUB_APP_* env vars are forwarded to agentic-pi *only* when the access
187
+ // profile opts into App PEM access via `allowMcpAppAuth`. That is currently
188
+ // never set (see gitSandboxAccessForWorkflow): the github extension can't
189
+ // read the PEM in the sandbox and skips rather than falling back, so we keep
190
+ // the App key out entirely and every run uses just the minted `GITHUB_TOKEN`
191
+ // below — which also stops agents minting elevated tokens themselves. The
192
+ // branch is retained so per-profile App auth can be re-enabled if the
193
+ // sandbox-side PEM is ever materialized.
194
+ const ghEnv = {};
195
+ let mintedToken;
196
+ const access = opts?.githubAccess;
197
+ const allowAppAuth = access?.allowMcpAppAuth === true;
198
+ if (process.env.GITHUB_APP_ID && allowAppAuth) {
199
+ ghEnv.GITHUB_APP_ID = process.env.GITHUB_APP_ID;
200
+ if (process.env.GITHUB_APP_INSTALLATION_ID) {
201
+ ghEnv.GITHUB_APP_INSTALLATION_ID = process.env.GITHUB_APP_INSTALLATION_ID;
202
+ }
203
+ if (process.env.GITHUB_APP_PRIVATE_KEY_PATH) {
204
+ ghEnv.GITHUB_APP_PRIVATE_KEY_PATH = process.env.GITHUB_APP_PRIVATE_KEY_PATH;
205
+ }
206
+ }
207
+ else {
208
+ // Suppress for in-process runs that inherit our env. Empty strings
209
+ // override the inherited value via `applyEnv()`.
210
+ ghEnv.GITHUB_APP_ID = "";
211
+ ghEnv.GITHUB_APP_INSTALLATION_ID = "";
212
+ ghEnv.GITHUB_APP_PRIVATE_KEY_PATH = "";
213
+ }
214
+ if (process.env.GITHUB_APP_ID && access) {
215
+ try {
216
+ const permissions = GITHUB_PERMISSION_PROFILES[access.profile];
217
+ const repositories = access.repo ? [access.repo] : undefined;
218
+ console.log(`[executor] Minting git token: profile=${access.profile}, ` +
219
+ `repo=${access.repo || "(unscoped)"}, permissions=${permissions ? Object.keys(permissions).join(",") : "all"}`);
220
+ const { token } = await refreshGitAuth({
221
+ appId: process.env.GITHUB_APP_ID,
222
+ privateKeyPath: process.env.GITHUB_APP_PRIVATE_KEY_PATH || "",
223
+ installationId: process.env.GITHUB_APP_INSTALLATION_ID || "",
224
+ permissions,
225
+ repositories,
226
+ });
227
+ mintedToken = token;
228
+ ghEnv.GITHUB_TOKEN = token;
229
+ ghEnv.GIT_TOKEN = token;
230
+ }
231
+ catch (err) {
232
+ const msg = err instanceof Error ? err.message : String(err);
233
+ console.warn(`[executor] Could not mint git token (repo=${access.repo || "none"}, ` +
234
+ `profile=${access.profile}): ${msg}`);
235
+ }
236
+ }
237
+ // Provider API keys.
238
+ if (process.env.OPENAI_API_KEY)
239
+ ghEnv.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
240
+ if (process.env.ANTHROPIC_API_KEY)
241
+ ghEnv.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
242
+ if (process.env.OPENROUTER_API_KEY)
243
+ ghEnv.OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY;
244
+ if (process.env.FIREWORKS_API_KEY)
245
+ ghEnv.FIREWORKS_API_KEY = process.env.FIREWORKS_API_KEY;
246
+ // Web-search provider keys. Forwarded only when the workflow opted into
247
+ // web search (scoped to explore today; see webSearchEnabledForWorkflow
248
+ // in workflows/runner.ts). agentic-pi auto-detects the provider from
249
+ // whichever key is present (Tavily > Exa > Brave by default).
250
+ if (config.webSearch) {
251
+ if (process.env.TAVILY_API_KEY)
252
+ ghEnv.TAVILY_API_KEY = process.env.TAVILY_API_KEY;
253
+ if (process.env.BRAVE_SEARCH_API_KEY)
254
+ ghEnv.BRAVE_SEARCH_API_KEY = process.env.BRAVE_SEARCH_API_KEY;
255
+ if (process.env.EXA_API_KEY)
256
+ ghEnv.EXA_API_KEY = process.env.EXA_API_KEY;
257
+ }
258
+ // OTEL config for the agent runtime itself. On docker the agent runs
259
+ // inside the container, so it reads this (the container env) and is
260
+ // pointed at the in-network collector — never the real backend or its
261
+ // auth headers. On gondolin/none the agent runs in the harness process
262
+ // and inherits the harness SDK; forwarding the host's OTEL_* here just
263
+ // re-affirms that config for any child processes.
264
+ if (config.otel?.enabled && config.otel.forwardToSandbox) {
265
+ Object.assign(ghEnv, backend === "docker" ? getDockerSandboxOtelEnv() : getOtelEnvForSandbox());
266
+ }
267
+ const prePopulate = access?.prePopulateBranch && mintedToken
268
+ ? {
269
+ owner: access.owner,
270
+ repo: access.repo,
271
+ branch: access.prePopulateBranch,
272
+ token: mintedToken,
273
+ runId: access.runId,
274
+ shallow: access.shallow,
275
+ }
276
+ : undefined;
277
+ const spanAttrs = safeSpanAttributes({
278
+ "agent.runtime": "agentic-pi",
279
+ "sandbox.backend": backend,
280
+ "task.id": taskId,
281
+ repo: access?.repo,
282
+ "github.profile": access?.profile,
283
+ model: config.model || DEFAULT_MODEL,
284
+ variant: config.variant,
285
+ "web_search.enabled": config.webSearch === true,
286
+ unrestricted_egress: config.unrestrictedEgress === true,
287
+ "workflow.name": config.telemetry?.workflowName,
288
+ "phase.name": config.telemetry?.phaseName,
289
+ });
290
+ if (backend === "docker") {
291
+ return withSpan("lastlight.agent.execute", spanAttrs, () => executeDocker(prompt, config, {
292
+ taskId,
293
+ stateDir,
294
+ env: ghEnv,
295
+ prePopulate,
296
+ access,
297
+ onSessionId: opts?.onSessionId,
298
+ }));
299
+ }
300
+ const workDir = setupTaskWorktree({
301
+ taskId,
302
+ stateDir,
303
+ sandboxDir: config.sandboxDir,
304
+ prePopulate,
305
+ });
306
+ // Drop AGENTS.md into the workspace — agentic-pi reads it as the
307
+ // system context (same convention as the previous opencode setup).
308
+ try {
309
+ const md = loadAgentContext(config.agentContextDir);
310
+ if (md)
311
+ writeFileSync(join(workDir, "AGENTS.md"), md);
312
+ }
313
+ catch (err) {
314
+ const msg = err instanceof Error ? err.message : String(err);
315
+ console.warn(`[executor] Could not write AGENTS.md: ${msg}`);
316
+ }
317
+ return withSpan("lastlight.agent.execute", spanAttrs, () => executeInProcess(prompt, config, {
318
+ backend,
319
+ taskId,
320
+ workDir,
321
+ stateDir,
322
+ env: ghEnv,
323
+ access,
324
+ onSessionId: opts?.onSessionId,
325
+ }));
326
+ }
327
+ // ── In-process path (gondolin / none) ───────────────────────────────
328
+ async function executeInProcess(prompt, config, ctx) {
329
+ const startTime = Date.now();
330
+ console.log(` [executor] Running in-process (task: ${ctx.taskId}, sandbox: ${ctx.backend})`);
331
+ const model = config.model || DEFAULT_MODEL;
332
+ const thinking = coerceThinking(config.variant);
333
+ const profile = ctx.access ? AGENTIC_PROFILE_FOR[ctx.access.profile] : undefined;
334
+ const sessionsDir = resolveSessionsDir(config);
335
+ // When the harness pre-cloned the target repo, cwd is the checkout
336
+ // (`<workDir>/<repo>/`) so the agent's commands run inside the repo with no
337
+ // `cd` preamble; otherwise cwd is the workspace root and the agent clones in.
338
+ const agentCwd = ctx.access?.prePopulateBranch
339
+ ? join(ctx.workDir, ctx.access.repo)
340
+ : ctx.workDir;
341
+ // Stage this phase's skills into its own bundle and point pi at the staged
342
+ // dirs explicitly via `skillPaths` below. The bundle lives at the workspace
343
+ // root — a sibling of the repo, outside its git tree — for `none` (the host
344
+ // FS is fully visible in-process). gondolin mounts only cwd, so its bundle
345
+ // is staged under the repo and added to the checkout's local
346
+ // `.git/info/exclude` so the agent can't commit it.
347
+ const gondolin = ctx.backend === "gondolin";
348
+ const skillRoot = gondolin ? agentCwd : ctx.workDir;
349
+ let stagedSkillDirs;
350
+ try {
351
+ stagedSkillDirs = stageSkillBundle(skillRoot, skillBundleKey(config), config.skillPaths, "symlink");
352
+ if (stagedSkillDirs && gondolin)
353
+ excludeFromGit(agentCwd, SKILL_BUNDLE_ROOT);
354
+ }
355
+ catch (err) {
356
+ const msg = err instanceof Error ? err.message : String(err);
357
+ console.warn(`[executor] Could not stage skills: ${msg}`);
358
+ }
359
+ // Server-mode build assets: agentCwd is the host-visible repo checkout for
360
+ // the in-process backends (gondolin's cwd mount / none's host FS), so stage
361
+ // and harvest operate on it directly.
362
+ const artifacts = serverArtifacts(config, agentCwd);
363
+ stageArtifactsIn(artifacts);
364
+ const shim = new AgenticShim({
365
+ homeDir: sessionsDir,
366
+ projectSlug: projectSlugForCwd(agentCwd),
367
+ model,
368
+ initialPrompt: prompt,
369
+ });
370
+ // agentic-pi reads its own env (provider keys, App PEM, etc.) from
371
+ // process.env. Splice in our scoped values for the duration of the call,
372
+ // then restore.
373
+ const restore = applyEnv(ctx.env);
374
+ // Env forwarded INTO the sandbox so the agent's `bash` calls see it.
375
+ // agentic-pi auto-injects GITHUB_TOKEN/GH_TOKEN when --profile is set;
376
+ // git identity is set here so `git commit` works without extra setup.
377
+ //
378
+ // Notes per backend:
379
+ // - gondolin: the VM's user inherits HOME from the agentic-pi process
380
+ // (so the host's HOME — e.g. /Users/clifton — leaks in). We force
381
+ // HOME=/root so `git config --global` and `gh auth status` write
382
+ // to a real path inside the VM.
383
+ // - none: agent runs on the host; do NOT override HOME (would mess
384
+ // with the harness user's real config).
385
+ // - docker (handled separately): the container's `agent` user has
386
+ // its own HOME=/home/agent baked in by the image; no override.
387
+ const baseSandboxEnv = {
388
+ GIT_AUTHOR_NAME: "last-light[bot]",
389
+ GIT_AUTHOR_EMAIL: "last-light[bot]@users.noreply.github.com",
390
+ GIT_COMMITTER_NAME: "last-light[bot]",
391
+ GIT_COMMITTER_EMAIL: "last-light[bot]@users.noreply.github.com",
392
+ // /workspace is a bind mount from the host (host UID) into a VM running
393
+ // as a different UID; git refuses to operate without an explicit
394
+ // safe-directory. Setting it via GIT_CONFIG_* avoids needing HOME and
395
+ // a writeable ~/.gitconfig.
396
+ GIT_CONFIG_COUNT: "1",
397
+ GIT_CONFIG_KEY_0: "safe.directory",
398
+ GIT_CONFIG_VALUE_0: "*",
399
+ };
400
+ const otelSandboxEnv = config.otel?.enabled && config.otel.forwardToSandbox ? getOtelEnvForSandbox() : {};
401
+ const sandboxEnv = ctx.backend === "gondolin"
402
+ ? { ...otelSandboxEnv, ...baseSandboxEnv, HOME: "/root", USER: "root", LOGNAME: "root" }
403
+ : { ...otelSandboxEnv, ...baseSandboxEnv };
404
+ let notifiedSessionId = false;
405
+ let result;
406
+ const acc = new RunResultAccumulator();
407
+ try {
408
+ // HTTP egress allowlist. lastlight owns the policy (rather than relying
409
+ // on agentic-pi's bundled default) so a single source — `egress-allowlist.ts`
410
+ // — covers both backends. `unrestrictedEgress` opts a phase out via the
411
+ // `"*"` sentinel; gondolin (post the upstream allow-all patch) treats it
412
+ // as "allow every host".
413
+ const extraHosts = config.otel?.enabled && config.otel.forwardToSandbox ? config.otel.collectorHosts : [];
414
+ const allowedHttpHosts = config.unrestrictedEgress
415
+ ? [ALLOW_ALL_SENTINEL]
416
+ : mergeAllowlist(DEFAULT_ALLOWLIST, extraHosts);
417
+ // Loaded lazily: agentic-pi transitively imports pi-coding-agent, whose
418
+ // bundled undici writes a v8 Agent onto `Symbol.for('undici.globalDispatcher.1')`
419
+ // the moment its `lib/global.js` evaluates. Node's built-in fetch reads
420
+ // from the same symbol, so eager-loading here would poison every fetch
421
+ // in the harness — breaking arctic's OAuth code exchange (strict
422
+ // content-length validation). Dynamic import keeps the harness on
423
+ // Node's clean dispatcher unless an in-process sandbox actually runs.
424
+ const { run: agenticRun } = await import("agentic-pi");
425
+ result = await agenticRun({
426
+ model,
427
+ prompt,
428
+ thinking,
429
+ profile,
430
+ sandbox: ctx.backend === "gondolin" ? "gondolin" : "none",
431
+ sandboxEnv,
432
+ cwd: agentCwd,
433
+ noSession: true,
434
+ // Explicit per-phase skill bundle (staged above). pi loads these
435
+ // additively; nothing is written into the repo for the agent to commit.
436
+ skillPaths: stagedSkillDirs,
437
+ allowedHttpHosts,
438
+ // Explicit boolean — without it, agentic-pi auto-enables web search
439
+ // when any provider key is in process.env. We forwarded those keys
440
+ // above only for opted-in workflows, but `process.env` on the harness
441
+ // host carries them regardless, so we must opt-out explicitly.
442
+ webSearch: config.webSearch === true,
443
+ webSearchProvider: config.webSearchProvider,
444
+ onEvent: (record) => {
445
+ acc.feed(record);
446
+ shim.feed(record);
447
+ recordPiEvent(record, {
448
+ includeContent: config.otel?.includeContent === true,
449
+ surface: "agent",
450
+ workflowName: config.telemetry?.workflowName,
451
+ phaseName: config.telemetry?.phaseName,
452
+ model,
453
+ });
454
+ if (!notifiedSessionId && ctx.onSessionId && record.type === "session" && typeof record.id === "string") {
455
+ notifiedSessionId = true;
456
+ ctx.onSessionId(record.id);
457
+ }
458
+ },
459
+ onWarn: (msg) => console.warn(`[agentic] ${msg}`),
460
+ });
461
+ }
462
+ catch (err) {
463
+ restore();
464
+ // Harvest even on failure so a partial plan/summary still reaches the store.
465
+ harvestArtifactsOut(artifacts);
466
+ const msg = err instanceof Error ? err.message : String(err);
467
+ const durationMs = Date.now() - startTime;
468
+ recordError("agent", err, { "sandbox.backend": ctx.backend, model, success: false, stop_reason: "error_executor", "workflow.name": config.telemetry?.workflowName, "phase.name": config.telemetry?.phaseName });
469
+ recordExecutionMetrics("agent", { "sandbox.backend": ctx.backend, model, success: false, stop_reason: "error_executor", durationMs });
470
+ const fallbackId = `exec-${basename(ctx.taskId)}`;
471
+ const synthesizedId = await shim
472
+ .finalizeWithFallback(emptyResult("error_executor", durationMs), fallbackId, msg)
473
+ .catch(() => null);
474
+ return {
475
+ success: false,
476
+ output: "",
477
+ turns: 0,
478
+ error: msg,
479
+ durationMs,
480
+ sessionId: synthesizedId ?? undefined,
481
+ stopReason: "error_executor",
482
+ };
483
+ }
484
+ restore();
485
+ harvestArtifactsOut(artifacts);
486
+ // agentic-pi's in-process `stats` is the same compaction-blind
487
+ // `usage_snapshot`. Prefer our per-message accumulation when it carries
488
+ // token data (see RunResultAccumulator.bestStats).
489
+ const better = acc.bestStats();
490
+ if (better && (better.tokens?.total ?? 0) > 0)
491
+ result.stats = better;
492
+ const finalResult = await finalizeFromRunResult(result, prompt, shim, startTime, acc.extensions(), acc.skills(), acc.toolError(), acc.endedOnToolCall());
493
+ recordExecutionMetrics("agent", {
494
+ "sandbox.backend": ctx.backend,
495
+ model,
496
+ success: finalResult.success,
497
+ stop_reason: finalResult.stopReason,
498
+ durationMs: finalResult.durationMs,
499
+ costUsd: finalResult.costUsd,
500
+ inputTokens: finalResult.inputTokens,
501
+ outputTokens: finalResult.outputTokens,
502
+ "workflow.name": config.telemetry?.workflowName,
503
+ "phase.name": config.telemetry?.phaseName,
504
+ });
505
+ return finalResult;
506
+ }
507
+ // ── Docker path ─────────────────────────────────────────────────────
508
+ async function executeDocker(prompt, config, ctx) {
509
+ // HTTP egress routing — docker analog of the gondolin allowlist.
510
+ // Sandbox is wired to coredns-strict by default (allowlist enforced via
511
+ // DNS sinkhole + nginx ssl_preread); a phase that set
512
+ // `unrestricted_egress: true` points at coredns-open instead.
513
+ // The IPs here match the static assignments in docker-compose.yml
514
+ // (see src/sandbox/egress-firewall-config.ts for the constants).
515
+ const dnsIp = config.unrestrictedEgress
516
+ ? (process.env.LASTLIGHT_DNS_OPEN || "172.30.0.11")
517
+ : (process.env.LASTLIGHT_DNS_STRICT || "172.30.0.10");
518
+ // A phase can opt into the browser-QA image (Playwright + Chromium baked in)
519
+ // with `sandbox_image: qa`. The runner only schedules such a phase when the
520
+ // image is actually present (see `qaImageAvailable`), so by here it's safe to
521
+ // request; pass undefined otherwise to use the lean default image.
522
+ const imageName = config.sandboxImage === "qa" ? SANDBOX_IMAGE_QA : undefined;
523
+ const sbx = await createTaskSandbox({
524
+ taskId: ctx.taskId,
525
+ stateDir: ctx.stateDir,
526
+ sandboxDir: config.sandboxDir,
527
+ env: ctx.env,
528
+ prePopulate: ctx.prePopulate,
529
+ dnsIp,
530
+ imageName,
531
+ });
532
+ if (!sbx) {
533
+ throw new Error("LASTLIGHT_SANDBOX=docker but no docker sandbox was available. " +
534
+ "Install Docker and build the sandbox image, or set LASTLIGHT_SANDBOX=gondolin / none.");
535
+ }
536
+ try {
537
+ const md = loadAgentContext(config.agentContextDir);
538
+ if (md)
539
+ writeFileSync(join(sbx.workDir, "AGENTS.md"), md);
540
+ }
541
+ catch (err) {
542
+ const msg = err instanceof Error ? err.message : String(err);
543
+ console.warn(`[executor] Could not write docker AGENTS.md: ${msg}`);
544
+ }
545
+ const startTime = Date.now();
546
+ console.log(` [executor] Running in docker sandbox (task: ${ctx.taskId})`);
547
+ const model = config.model || DEFAULT_MODEL;
548
+ const thinking = coerceThinking(config.variant);
549
+ const profile = ctx.access ? AGENTIC_PROFILE_FOR[ctx.access.profile] : undefined;
550
+ const sessionsDir = resolveSessionsDir(config);
551
+ // When the harness pre-cloned the repo, cwd is the checkout
552
+ // (`<workspace>/<repo>/`) so the agent runs inside the repo with no `cd`
553
+ // preamble; otherwise it's the workspace root.
554
+ const agentCwd = ctx.prePopulate
555
+ ? `${DOCKER_WORKSPACE_DIR}/${ctx.prePopulate.repo}`
556
+ : DOCKER_WORKSPACE_DIR;
557
+ // Stage this phase's skills into its own bundle at the workspace ROOT — a
558
+ // sibling of any repo subdir, never inside its git tree. docker bind-mounts
559
+ // the WHOLE workspace, so the agent reaches the bundle by an absolute
560
+ // `--skill` path even though cwd is the repo. Copy (not symlink) because the
561
+ // agent's tools run inside the container and host symlink targets don't
562
+ // resolve there. Map the host dests to their in-container paths for `--skill`.
563
+ let skillDirsInContainer = [];
564
+ try {
565
+ const staged = stageSkillBundle(sbx.workDir, skillBundleKey(config), config.skillPaths, "copy");
566
+ if (staged) {
567
+ skillDirsInContainer = staged.map((d) => `${DOCKER_WORKSPACE_DIR}/${relative(sbx.workDir, d)}`);
568
+ }
569
+ }
570
+ catch (err) {
571
+ const msg = err instanceof Error ? err.message : String(err);
572
+ console.warn(`[executor] Could not stage skills in docker workspace: ${msg}`);
573
+ }
574
+ // Server-mode build assets. The host-visible repo checkout is the
575
+ // bind-mounted workspace path (`sbx.workDir[/<repo>]`), NOT the in-container
576
+ // `agentCwd`. Stage now; harvest before each `sbx.cleanup()` below (cleanup
577
+ // removes the workspace, taking the docs with it).
578
+ const hostRepoDir = ctx.prePopulate ? join(sbx.workDir, ctx.prePopulate.repo) : sbx.workDir;
579
+ const artifacts = serverArtifacts(config, hostRepoDir);
580
+ stageArtifactsIn(artifacts);
581
+ // The dashboard reads from <sessionsDir>/projects/<slug>/. Use the same
582
+ // resolved cwd for the slug so live tails land in the right project dir.
583
+ const shim = new AgenticShim({
584
+ homeDir: sessionsDir,
585
+ projectSlug: projectSlugForCwd(agentCwd),
586
+ model,
587
+ initialPrompt: prompt,
588
+ });
589
+ const acc = new RunResultAccumulator();
590
+ let notifiedSessionId = false;
591
+ // Identity forwarded into the sandboxed agentic-pi run inside the container.
592
+ // The container's `agent` user already has HOME=/home/agent set up, so we
593
+ // do NOT override HOME here — only git identity + safe.directory (for the
594
+ // host-UID bind mount). GITHUB_TOKEN/GH_TOKEN are auto-injected by
595
+ // agentic-pi when --profile is set.
596
+ const sandboxEnv = {
597
+ // Inner-run env for the agent's child shells. Points at the in-network
598
+ // collector (IP-only, no secret headers) so any OTLP a script emits is
599
+ // tunnelled the same way the agent's own telemetry is.
600
+ ...(config.otel?.enabled && config.otel.forwardToSandbox ? getDockerSandboxOtelEnv() : {}),
601
+ GIT_AUTHOR_NAME: "last-light[bot]",
602
+ GIT_AUTHOR_EMAIL: "last-light[bot]@users.noreply.github.com",
603
+ GIT_COMMITTER_NAME: "last-light[bot]",
604
+ GIT_COMMITTER_EMAIL: "last-light[bot]@users.noreply.github.com",
605
+ GIT_CONFIG_COUNT: "1",
606
+ GIT_CONFIG_KEY_0: "safe.directory",
607
+ GIT_CONFIG_VALUE_0: "*",
608
+ };
609
+ try {
610
+ await sbx.sandbox.runAgent(ctx.taskId, prompt, {
611
+ model,
612
+ thinking,
613
+ profile,
614
+ sandboxEnv,
615
+ agentCwd,
616
+ skillDirs: skillDirsInContainer,
617
+ webSearch: config.webSearch === true,
618
+ webSearchProvider: config.webSearchProvider,
619
+ onLine: (line) => {
620
+ if (!line.startsWith("{"))
621
+ return;
622
+ let record;
623
+ try {
624
+ record = JSON.parse(line);
625
+ }
626
+ catch {
627
+ return;
628
+ }
629
+ acc.feed(record);
630
+ shim.feed(record);
631
+ recordPiEvent(record, {
632
+ includeContent: config.otel?.includeContent === true,
633
+ surface: "agent",
634
+ workflowName: config.telemetry?.workflowName,
635
+ phaseName: config.telemetry?.phaseName,
636
+ model,
637
+ });
638
+ if (!notifiedSessionId && ctx.onSessionId && record.type === "session" && typeof record.id === "string") {
639
+ notifiedSessionId = true;
640
+ ctx.onSessionId(record.id);
641
+ }
642
+ },
643
+ });
644
+ }
645
+ catch (err) {
646
+ const msg = err instanceof Error ? err.message : String(err);
647
+ const durationMs = Date.now() - startTime;
648
+ recordError("agent", err, { "sandbox.backend": "docker", model, success: false, stop_reason: "error_sandbox", "workflow.name": config.telemetry?.workflowName, "phase.name": config.telemetry?.phaseName });
649
+ recordExecutionMetrics("agent", { "sandbox.backend": "docker", model, success: false, stop_reason: "error_sandbox", durationMs });
650
+ const fallbackId = `exec-${basename(ctx.taskId)}`;
651
+ const synthesizedId = await shim
652
+ .finalizeWithFallback(emptyResult("error_sandbox", durationMs), fallbackId, msg)
653
+ .catch(() => null);
654
+ harvestArtifactsOut(artifacts);
655
+ await sbx.cleanup();
656
+ return {
657
+ success: false,
658
+ output: "",
659
+ turns: 0,
660
+ error: msg,
661
+ durationMs,
662
+ sessionId: synthesizedId ?? undefined,
663
+ stopReason: "error_sandbox",
664
+ };
665
+ }
666
+ harvestArtifactsOut(artifacts);
667
+ await sbx.cleanup();
668
+ const finalResult = await finalizeFromRunResult(acc.build(0), prompt, shim, startTime, acc.extensions(), acc.skills(), acc.toolError(), acc.endedOnToolCall());
669
+ recordExecutionMetrics("agent", {
670
+ "sandbox.backend": "docker",
671
+ model,
672
+ success: finalResult.success,
673
+ stop_reason: finalResult.stopReason,
674
+ durationMs: finalResult.durationMs,
675
+ costUsd: finalResult.costUsd,
676
+ inputTokens: finalResult.inputTokens,
677
+ outputTokens: finalResult.outputTokens,
678
+ "workflow.name": config.telemetry?.workflowName,
679
+ "phase.name": config.telemetry?.phaseName,
680
+ });
681
+ return finalResult;
682
+ }
683
+ /**
684
+ * Build a RunResult-shaped tally from the JSONL event stream emitted by
685
+ * `agentic-pi run` inside the docker sandbox. Mirrors what agentic-pi's
686
+ * own `run()` function does in-process — minimum viable subset of
687
+ * fields the executor cares about.
688
+ *
689
+ * Usage accounting accumulates each assistant `message_end`'s `usage`
690
+ * rather than trusting the terminal `usage_snapshot`. pi's snapshot is
691
+ * derived from `getSessionStats()`, which recomputes from the *current*
692
+ * in-memory message window — auto-compaction replaces those messages with
693
+ * a summary, so the snapshot reports zero tokens/cost/turns the moment a
694
+ * phase compacts. The per-message events fire at finalization (before any
695
+ * compaction rebuild), so summing them is compaction-proof. `bestStats()`
696
+ * prefers the accumulation and falls back to the snapshot only when no
697
+ * per-message usage was observed.
698
+ */
699
+ export class RunResultAccumulator {
700
+ sessionId;
701
+ finalText = "";
702
+ agentEnded = false;
703
+ toolErrors = false;
704
+ lastToolError;
705
+ // True iff the last assistant turn ended with a tool call — i.e. the agent
706
+ // asked for a tool and the loop terminated before it could respond to the
707
+ // result. That's a truncated run (pi hit its internal step cap mid-task),
708
+ // not a finished one. See `finalizeFromRunResult`'s truncation guard.
709
+ lastAssistantHadToolCall = false;
710
+ fatalError;
711
+ snapshotStats;
712
+ messages = [];
713
+ // Per-message usage accumulation (the compaction-proof source).
714
+ assistantMessages = 0;
715
+ userMessages = 0;
716
+ toolCalls = 0;
717
+ toolResults = 0;
718
+ msgInput = 0;
719
+ msgOutput = 0;
720
+ msgCacheRead = 0;
721
+ msgCacheWrite = 0;
722
+ msgCost = 0;
723
+ // Extension status events (file-search / github / web-search), keyed by
724
+ // extension name. Emitted once each at run start; we keep the raw payload
725
+ // (minus type/extension) so build() can map them onto the RunResult.
726
+ ext = {};
727
+ // Skill-loading status. agentic-pi emits a single (gated) `skills_status`
728
+ // event at run start; we keep the raw payload (minus type) so skills()
729
+ // can normalize it onto the RunResult, mirroring `ext` above for tools.
730
+ skillsRaw;
731
+ feed(r) {
732
+ switch (r.type) {
733
+ case "session":
734
+ if (typeof r.id === "string")
735
+ this.sessionId = r.id;
736
+ break;
737
+ case "extension_status": {
738
+ if (typeof r.extension === "string") {
739
+ const { type: _t, extension: _e, ...rest } = r;
740
+ this.ext[r.extension] = rest;
741
+ }
742
+ break;
743
+ }
744
+ case "skills_status": {
745
+ const { type: _t, ...rest } = r;
746
+ this.skillsRaw = rest;
747
+ break;
748
+ }
749
+ case "message_end": {
750
+ const m = r.message;
751
+ if (m?.role === "assistant" && Array.isArray(m.content)) {
752
+ const text = m.content
753
+ .filter((c) => c.type === "text" && typeof c.text === "string")
754
+ .map((c) => c.text)
755
+ .join("");
756
+ if (text)
757
+ this.finalText = text;
758
+ this.assistantMessages += 1;
759
+ const toolCallsInTurn = m.content.filter((c) => c.type === "toolCall").length;
760
+ this.toolCalls += toolCallsInTurn;
761
+ // Track whether the *latest* assistant turn requested a tool. If the
762
+ // run ends here (no synthesis turn follows the tool result), the
763
+ // agent was cut off mid-task.
764
+ this.lastAssistantHadToolCall = toolCallsInTurn > 0;
765
+ this.accumulateUsage(m.usage);
766
+ }
767
+ else if (m?.role === "user") {
768
+ this.userMessages += 1;
769
+ }
770
+ break;
771
+ }
772
+ case "tool_execution_end":
773
+ this.toolResults += 1;
774
+ if (r.isError === true) {
775
+ this.toolErrors = true;
776
+ // Keep the actual failure text (not just a boolean) so a run that
777
+ // ends in `error_tool` can report which tool failed and why —
778
+ // e.g. a provider `insufficient_quota` surfaced through an MCP
779
+ // call, or a bash command's stderr. Last error wins (it's the
780
+ // one that ended the run). truncate: tool output can be huge.
781
+ const raw = r.error ?? r.result ?? r.output;
782
+ const message = truncateForLog(typeof raw === "string" ? raw : safeStringify(raw), 4096);
783
+ const tool = typeof r.tool === "string"
784
+ ? r.tool
785
+ : typeof r.toolName === "string"
786
+ ? r.toolName
787
+ : undefined;
788
+ if (message)
789
+ this.lastToolError = { tool, message };
790
+ }
791
+ break;
792
+ case "agent_end":
793
+ this.agentEnded = true;
794
+ if (Array.isArray(r.messages))
795
+ this.messages = r.messages;
796
+ break;
797
+ case "usage_snapshot":
798
+ this.snapshotStats = r.stats;
799
+ break;
800
+ case "fatal_error":
801
+ this.fatalError = r.error;
802
+ break;
803
+ }
804
+ }
805
+ accumulateUsage(usage) {
806
+ if (!usage || typeof usage !== "object")
807
+ return;
808
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
809
+ this.msgInput += num(usage.input);
810
+ this.msgOutput += num(usage.output);
811
+ this.msgCacheRead += num(usage.cacheRead);
812
+ this.msgCacheWrite += num(usage.cacheWrite);
813
+ const cost = usage.cost;
814
+ if (cost && typeof cost === "object")
815
+ this.msgCost += num(cost.total);
816
+ }
817
+ /** Stats summed from per-message usage, or undefined if none was seen. */
818
+ accumulatedStats() {
819
+ const total = this.msgInput + this.msgOutput + this.msgCacheRead + this.msgCacheWrite;
820
+ if (this.assistantMessages === 0 && total === 0)
821
+ return undefined;
822
+ return {
823
+ userMessages: this.userMessages,
824
+ assistantMessages: this.assistantMessages,
825
+ toolCalls: this.toolCalls,
826
+ toolResults: this.toolResults,
827
+ tokens: {
828
+ input: this.msgInput,
829
+ output: this.msgOutput,
830
+ cacheRead: this.msgCacheRead,
831
+ cacheWrite: this.msgCacheWrite,
832
+ total,
833
+ },
834
+ cost: this.msgCost,
835
+ };
836
+ }
837
+ /**
838
+ * Prefer the per-message accumulation (compaction-proof) over pi's
839
+ * terminal `usage_snapshot`. Fall back to the snapshot only when the
840
+ * accumulation carries no token data — e.g. a provider that doesn't
841
+ * report per-message usage — so a non-compacted snapshot still wins.
842
+ */
843
+ bestStats() {
844
+ const acc = this.accumulatedStats();
845
+ if (acc && acc.tokens.total > 0)
846
+ return acc;
847
+ return this.snapshotStats ?? acc;
848
+ }
849
+ build(exitCode) {
850
+ return {
851
+ exitCode: exitCode,
852
+ ok: exitCode === 0 && !this.fatalError,
853
+ agentEnded: this.agentEnded,
854
+ toolErrors: this.toolErrors,
855
+ fatalError: this.fatalError,
856
+ sessionId: this.sessionId,
857
+ finalText: this.finalText,
858
+ messages: this.messages,
859
+ stats: this.bestStats(),
860
+ records: [],
861
+ warnings: [],
862
+ };
863
+ }
864
+ /**
865
+ * Normalized extension status captured from `extension_status` events
866
+ * (file-search / github / web-search), or undefined if none reported.
867
+ * Decoupled from agentic-pi's `RunResult` type, which lags the runtime —
868
+ * the docker sandbox's agentic-pi emits file-search even when the harness's
869
+ * pinned `RunResult` type doesn't yet declare it.
870
+ */
871
+ extensions() {
872
+ const out = {};
873
+ for (const [name, v] of Object.entries(this.ext)) {
874
+ if (v && typeof v.status === "string") {
875
+ out[name] = {
876
+ status: v.status,
877
+ ...(typeof v.mode === "string" ? { mode: v.mode } : {}),
878
+ ...(typeof v.provider === "string" ? { provider: v.provider } : {}),
879
+ ...(typeof v.toolCount === "number" ? { toolCount: v.toolCount } : {}),
880
+ ...(typeof v.reason === "string" ? { reason: v.reason } : {}),
881
+ };
882
+ }
883
+ }
884
+ return Object.keys(out).length > 0 ? out : undefined;
885
+ }
886
+ /**
887
+ * Normalized skill-loading status from the gated `skills_status` event, or
888
+ * undefined if none was reported (agentic-pi suppresses it on a run that
889
+ * configured no skills and discovered none). The skill-loading counterpart
890
+ * to {@link extensions}.
891
+ */
892
+ skills() {
893
+ const s = this.skillsRaw;
894
+ if (!s || typeof s.status !== "string")
895
+ return undefined;
896
+ const skills = Array.isArray(s.skills)
897
+ ? s.skills
898
+ .filter((x) => !!x && typeof x === "object")
899
+ .map((x) => ({
900
+ name: typeof x.name === "string" ? x.name : "",
901
+ source: typeof x.source === "string" ? x.source : "",
902
+ modelInvocable: x.modelInvocable === true,
903
+ }))
904
+ : [];
905
+ return {
906
+ status: s.status,
907
+ discovered: typeof s.discovered === "number" ? s.discovered : skills.length,
908
+ skills,
909
+ mappedPaths: Array.isArray(s.mappedPaths)
910
+ ? s.mappedPaths.filter((p) => typeof p === "string")
911
+ : [],
912
+ noSkills: s.noSkills === true,
913
+ };
914
+ }
915
+ /**
916
+ * The last tool result that came back with `isError: true`, including the
917
+ * failure text — or undefined if no tool errored. This is what turns a
918
+ * bare `error_tool` stop reason into a human-readable cause.
919
+ */
920
+ toolError() {
921
+ return this.lastToolError;
922
+ }
923
+ /**
924
+ * True iff the run's final assistant turn ended on a tool call — meaning
925
+ * the agent intended to keep going but the loop stopped before it could
926
+ * respond to the tool result and write its answer. The signal a run was
927
+ * truncated (step-limit) rather than genuinely finished.
928
+ */
929
+ endedOnToolCall() {
930
+ return this.lastAssistantHadToolCall;
931
+ }
932
+ }
933
+ // ── Helpers ─────────────────────────────────────────────────────────
934
+ /** Provider account/billing/auth failures pi may surface as error text. */
935
+ const ACCOUNT_ERROR_MARKERS = [
936
+ "credit balance",
937
+ "insufficient_quota",
938
+ "insufficient quota",
939
+ "rate limit",
940
+ "unauthorized",
941
+ "invalid_api_key",
942
+ ];
943
+ /**
944
+ * Detect a provider account error (out of credit, quota, rate-limited, bad key)
945
+ * that pi may have surfaced as plain text rather than a hard failure.
946
+ *
947
+ * Critically, on a **successful** run we scan ONLY the genuine provider-error
948
+ * channel (`agentErrorMessage`, which `extractAgentError` already turns into a
949
+ * non-success stopReason when set — so it's empty here). The agent's own output
950
+ * (`finalText`) and tool results (`fatalError`, `toolError`) are NOT scanned on
951
+ * success: a legitimate `verify`/`qa-test` report or a `curl` probing a 401
952
+ * endpoint routinely contains "unauthorized" / "rate limit" as part of the task
953
+ * itself, and folding that in would wrongly fail a genuinely successful run
954
+ * (and drop its report). Only on a failed run do we fold those in to label why.
955
+ */
956
+ export function detectAccountError(opts) {
957
+ const combined = [
958
+ opts.success ? undefined : opts.fatalErrorMessage,
959
+ opts.agentErrorMessage,
960
+ opts.success ? undefined : opts.finalText,
961
+ opts.success ? undefined : opts.toolErrorText,
962
+ ]
963
+ .filter((s) => typeof s === "string" && s.length > 0)
964
+ .join("\n")
965
+ .toLowerCase();
966
+ return ACCOUNT_ERROR_MARKERS.some((m) => combined.includes(m));
967
+ }
968
+ function finalizeFromRunResult(result, _prompt, shim, startTime, extensions, skills, toolError, endedOnToolCall = false) {
969
+ const durationMs = Date.now() - startTime;
970
+ const stats = result.stats;
971
+ // pi-agent-core swallows provider API errors (insufficient_quota, rate
972
+ // limit, auth) inside `handleRunFailure`: it synthesizes an assistant
973
+ // message with stopReason "error" + errorMessage and emits a clean
974
+ // agent_end. Without this check the run would map to "success" and the
975
+ // workflow would silently advance with no output. See message scan below.
976
+ const agentError = extractAgentError(result);
977
+ let stopReason = agentError?.stopReason ?? mapStopReason(result);
978
+ // Truncation guard. A run that *would* map to "success" but whose final
979
+ // assistant turn ended on a tool call was cut off mid-task: the agent
980
+ // asked for a tool and the loop stopped before it could read the result
981
+ // and synthesize an answer (pi hit its internal step cap — agentic-pi
982
+ // v0.2.7 exposes no maxSteps knob to lift it). In that state `finalText`
983
+ // is the agent's "let me just check X" preamble, NOT an answer. Reclassify
984
+ // as a failure so the workflow fires `on_failure` instead of delivering
985
+ // the fragment as if it were the result.
986
+ const truncated = stopReason === "success" && !agentError && endedOnToolCall;
987
+ if (truncated)
988
+ stopReason = "error_truncated";
989
+ const success = stopReason === "success";
990
+ const truncationMessage = "Agent stopped mid-task before producing a final answer (hit the agent " +
991
+ "step limit). No answer was delivered.";
992
+ // A bare `error_tool` stop reason is useless on its own. Surface the
993
+ // failing tool's actual error text so the executions row and dashboard
994
+ // show *why* the run died (e.g. "Tool `bash` failed: insufficient_quota").
995
+ const toolErrorText = toolError
996
+ ? toolError.tool
997
+ ? `Tool \`${toolError.tool}\` failed: ${toolError.message}`
998
+ : toolError.message
999
+ : undefined;
1000
+ const inputTokens = stats?.tokens.input ?? 0;
1001
+ const outputTokens = stats?.tokens.output ?? 0;
1002
+ const cacheRead = stats?.tokens.cacheRead ?? 0;
1003
+ const cacheWrite = stats?.tokens.cacheWrite ?? 0;
1004
+ const costUsd = stats?.cost ?? 0;
1005
+ const turns = stats?.assistantMessages ?? 0;
1006
+ const costStr = costUsd > 0 ? `, $${costUsd.toFixed(4)}` : "";
1007
+ console.log(` [executor] Result: ${stopReason} (${turns} turns, ${Math.round(durationMs / 1000)}s${costStr})` +
1008
+ `${result.sessionId ? ` [session ${result.sessionId}]` : ""}`);
1009
+ shim.finalize({
1010
+ finalText: result.finalText,
1011
+ turns,
1012
+ costUsd,
1013
+ inputTokens,
1014
+ outputTokens,
1015
+ cacheReadInputTokens: cacheRead,
1016
+ cacheCreationInputTokens: cacheWrite,
1017
+ stopReason,
1018
+ durationMs,
1019
+ apiErrorMessage: agentError?.errorMessage ??
1020
+ (success ? undefined : (toolErrorText ?? (truncated ? truncationMessage : undefined))),
1021
+ });
1022
+ void shim.flush();
1023
+ const accountError = detectAccountError({
1024
+ success,
1025
+ fatalErrorMessage: result.fatalError?.message,
1026
+ agentErrorMessage: agentError?.errorMessage,
1027
+ finalText: result.finalText,
1028
+ toolErrorText,
1029
+ });
1030
+ const errorText = result.fatalError?.message ||
1031
+ agentError?.errorMessage ||
1032
+ toolErrorText ||
1033
+ (truncated ? truncationMessage : result.finalText) ||
1034
+ stopReason;
1035
+ if (!success || accountError) {
1036
+ if (accountError)
1037
+ console.error(` [executor] Account error: ${errorText}`);
1038
+ else
1039
+ console.error(` [executor] Run failed (${stopReason}): ${errorText}`);
1040
+ }
1041
+ return {
1042
+ success: success && !accountError,
1043
+ output: result.finalText,
1044
+ turns,
1045
+ error: success && !accountError ? undefined : errorText,
1046
+ durationMs,
1047
+ sessionId: result.sessionId,
1048
+ costUsd: costUsd > 0 ? costUsd : undefined,
1049
+ inputTokens: inputTokens || undefined,
1050
+ outputTokens: outputTokens || undefined,
1051
+ cacheReadInputTokens: cacheRead || undefined,
1052
+ cacheCreationInputTokens: cacheWrite || undefined,
1053
+ stopReason,
1054
+ extensions,
1055
+ skills,
1056
+ };
1057
+ }
1058
+ function mapStopReason(result) {
1059
+ if (result.fatalError)
1060
+ return "error_fatal";
1061
+ if (result.toolErrors && result.finalText.length === 0)
1062
+ return "error_tool";
1063
+ if (!result.ok)
1064
+ return `error_exit_${result.exitCode}`;
1065
+ if (result.agentEnded || result.finalText.length > 0)
1066
+ return "success";
1067
+ return "unknown";
1068
+ }
1069
+ /**
1070
+ * Scan agent_end's messages for a failed assistant turn. pi-agent-core's
1071
+ * `handleRunFailure` catches provider errors, synthesizes an assistant
1072
+ * message with stopReason "error"/"aborted" + errorMessage, and emits a
1073
+ * normal agent_end — so the only signal that the run actually failed
1074
+ * lives on the last assistant message.
1075
+ */
1076
+ function extractAgentError(result) {
1077
+ if (!Array.isArray(result.messages))
1078
+ return undefined;
1079
+ for (let i = result.messages.length - 1; i >= 0; i--) {
1080
+ const m = result.messages[i];
1081
+ if (m?.role !== "assistant")
1082
+ continue;
1083
+ if (m.stopReason === "error" || m.stopReason === "aborted") {
1084
+ return {
1085
+ stopReason: m.stopReason === "aborted" ? "error_aborted" : "error_agent",
1086
+ errorMessage: m.errorMessage || m.stopReason,
1087
+ };
1088
+ }
1089
+ return undefined;
1090
+ }
1091
+ return undefined;
1092
+ }
1093
+ function coerceThinking(raw) {
1094
+ if (!raw)
1095
+ return undefined;
1096
+ const v = raw.trim().toLowerCase();
1097
+ if (!THINKING_LEVELS.has(v)) {
1098
+ console.warn(`[executor] Ignoring unknown thinking level "${raw}" — must be one of: ${[...THINKING_LEVELS].join(", ")}`);
1099
+ return undefined;
1100
+ }
1101
+ return v;
1102
+ }
1103
+ function resolveSessionsDir(config) {
1104
+ if (config.sessionsDir)
1105
+ return resolve(config.sessionsDir);
1106
+ const stateDir = config.stateDir || resolve("data");
1107
+ return process.env.LASTLIGHT_SESSIONS_DIR
1108
+ ? resolve(process.env.LASTLIGHT_SESSIONS_DIR)
1109
+ : resolve(stateDir, "agent-sessions");
1110
+ }
1111
+ function emptyResult(stopReason, durationMs) {
1112
+ return {
1113
+ finalText: "",
1114
+ turns: 0,
1115
+ costUsd: 0,
1116
+ inputTokens: 0,
1117
+ outputTokens: 0,
1118
+ cacheReadInputTokens: 0,
1119
+ cacheCreationInputTokens: 0,
1120
+ stopReason,
1121
+ durationMs,
1122
+ };
1123
+ }
1124
+ /** Splice values into process.env for the duration of a sync block. */
1125
+ function applyEnv(env) {
1126
+ const saved = {};
1127
+ for (const [k, v] of Object.entries(env)) {
1128
+ saved[k] = process.env[k];
1129
+ process.env[k] = v;
1130
+ }
1131
+ return () => {
1132
+ for (const [k, v] of Object.entries(saved)) {
1133
+ if (v === undefined)
1134
+ delete process.env[k];
1135
+ else
1136
+ process.env[k] = v;
1137
+ }
1138
+ };
1139
+ }
1140
+ //# sourceMappingURL=agent-executor.js.map