@atolis-hq/wake 0.2.97 → 0.3.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 (442) hide show
  1. package/dist/src/activities/agent/agent-activity-definition.js +26 -0
  2. package/dist/src/activities/agent/agent-activity.js +171 -0
  3. package/dist/src/activities/agent/agent-result.js +149 -0
  4. package/dist/src/activities/contracts/activity.js +10 -0
  5. package/dist/src/activities/contracts/config.js +2 -0
  6. package/dist/src/activities/contracts/event-schema.js +302 -0
  7. package/dist/src/activities/contracts/events.js +42 -0
  8. package/dist/src/activities/contracts/identifiers.js +29 -0
  9. package/dist/src/activities/contracts/registry.js +71 -0
  10. package/dist/src/activities/contracts/streams.js +17 -0
  11. package/dist/src/activities/contracts/vocabulary.js +58 -0
  12. package/dist/src/activities/index.js +21 -0
  13. package/dist/src/activities/pr/activity-support.js +111 -0
  14. package/dist/src/activities/pr/application.js +158 -0
  15. package/dist/src/activities/pr/approve.js +85 -0
  16. package/dist/src/activities/pr/capability.js +11 -0
  17. package/dist/src/activities/pr/decision-claim.js +76 -0
  18. package/dist/src/activities/pr/event-drafts.js +66 -0
  19. package/dist/src/activities/pr/intent.js +37 -0
  20. package/dist/src/activities/pr/merge-authority-gate.js +14 -0
  21. package/dist/src/activities/pr/merge.js +110 -0
  22. package/dist/src/activities/pr/policy.js +135 -0
  23. package/dist/src/activities/pr/projection.js +130 -0
  24. package/dist/src/activities/pr/vocabulary.js +30 -0
  25. package/dist/src/activities/review/authorization.js +22 -0
  26. package/dist/src/activities/review/contracts.js +22 -0
  27. package/dist/src/activities/review/signals.js +3 -0
  28. package/dist/src/bootstrap/analytics-projection.js +26 -0
  29. package/dist/src/bootstrap/board-projection.js +337 -0
  30. package/dist/src/bootstrap/composition-root.js +382 -0
  31. package/dist/src/bootstrap/config/load-config.js +51 -0
  32. package/dist/src/bootstrap/config/root-schema.js +134 -0
  33. package/dist/src/bootstrap/external-ref.js +12 -0
  34. package/dist/src/bootstrap/fake-provider-files.js +44 -0
  35. package/dist/src/bootstrap/fake-scenarios.js +22 -0
  36. package/dist/src/bootstrap/index.js +48 -0
  37. package/dist/src/bootstrap/initialise.js +434 -0
  38. package/dist/src/bootstrap/paths.js +15 -0
  39. package/dist/src/bootstrap/projection-runtime.js +27 -0
  40. package/dist/src/bootstrap/runner-quota-reporter.js +21 -0
  41. package/dist/src/bootstrap/runner-registry.js +30 -0
  42. package/dist/src/bootstrap/schedule-checkpoint-store.js +49 -0
  43. package/dist/src/bootstrap/self-update-application.js +195 -0
  44. package/dist/src/bootstrap/self-update-failure-log.js +33 -0
  45. package/dist/src/bootstrap/source-update-port.js +47 -0
  46. package/dist/src/bootstrap/status-publish-activity.js +42 -0
  47. package/dist/src/bootstrap/surface-api-applications.js +325 -0
  48. package/dist/src/bootstrap/surface-api-execution-applications.js +95 -0
  49. package/dist/src/bootstrap/surface-api-metadata.js +12 -0
  50. package/dist/src/bootstrap/surface-api-projection-pages.js +22 -0
  51. package/dist/src/bootstrap/surface-api-run-context.js +5 -0
  52. package/dist/src/bootstrap/surface-api-transcripts.js +57 -0
  53. package/dist/src/bootstrap/surface-api-work-applications.js +199 -0
  54. package/dist/src/bootstrap/surface-applications.js +7 -0
  55. package/dist/src/bootstrap/surface-cli-applications.js +701 -0
  56. package/dist/src/bootstrap/update-ledger.js +61 -0
  57. package/dist/src/bootstrap/update-maintenance-lease.js +176 -0
  58. package/dist/src/{version.js → bootstrap/version.js} +35 -51
  59. package/dist/src/control-plane/application/advance-once.js +184 -0
  60. package/dist/src/control-plane/application/control-plane-projection.js +42 -0
  61. package/dist/src/control-plane/application/control-plane-service.js +42 -0
  62. package/dist/src/control-plane/application/intake-pipeline.js +29 -0
  63. package/dist/src/control-plane/application/runner-control-service.js +66 -0
  64. package/dist/src/control-plane/application/runner-pipeline.js +57 -0
  65. package/dist/src/control-plane/application/schedule-service.js +47 -0
  66. package/dist/src/control-plane/application/work-cancellation-policy.js +31 -0
  67. package/dist/src/control-plane/contracts/commands.js +10 -0
  68. package/dist/src/control-plane/contracts/config.js +22 -0
  69. package/dist/src/control-plane/contracts/events.js +81 -0
  70. package/dist/src/control-plane/contracts/streams.js +5 -0
  71. package/dist/src/control-plane/domain/dispatch-policy.js +15 -0
  72. package/dist/src/control-plane/domain/quota-policy.js +26 -0
  73. package/dist/src/control-plane/domain/runner-quota.js +35 -0
  74. package/dist/src/control-plane/domain/schedule-policy.js +46 -0
  75. package/dist/src/control-plane/index.js +20 -0
  76. package/dist/src/control-plane/infrastructure/intake-host.js +24 -0
  77. package/dist/src/control-plane/infrastructure/resident-host.js +45 -0
  78. package/dist/src/control-plane/infrastructure/tick-host.js +35 -0
  79. package/dist/src/execution/application/activation-claim.js +50 -0
  80. package/dist/src/execution/application/active-run-cancellation.js +14 -0
  81. package/dist/src/execution/application/execution-activity.js +97 -0
  82. package/dist/src/execution/application/execution-projection.js +17 -0
  83. package/dist/src/execution/application/execution-service.js +372 -0
  84. package/dist/src/execution/application/execution-validation.js +26 -0
  85. package/dist/src/execution/application/recovery-service.js +212 -0
  86. package/dist/src/execution/application/run-lifecycle.js +115 -0
  87. package/dist/src/execution/application/run-liveness-service.js +80 -0
  88. package/dist/src/execution/application/run-repository.js +27 -0
  89. package/dist/src/execution/contracts/commands.js +1 -0
  90. package/dist/src/execution/contracts/config.js +35 -0
  91. package/dist/src/execution/contracts/control-plane.js +1 -0
  92. package/dist/src/execution/contracts/event-factory.js +53 -0
  93. package/dist/src/execution/contracts/events.js +274 -0
  94. package/dist/src/execution/contracts/identifiers.js +5 -0
  95. package/dist/src/execution/contracts/liveness.js +1 -0
  96. package/dist/src/execution/contracts/results.js +1 -0
  97. package/dist/src/execution/contracts/runner.js +15 -0
  98. package/dist/src/execution/contracts/streams.js +10 -0
  99. package/dist/src/execution/contracts/views.js +1 -0
  100. package/dist/src/execution/contracts/vocabulary.js +43 -0
  101. package/dist/src/execution/contracts/workspace.js +1 -0
  102. package/dist/src/execution/domain/run-result.js +34 -0
  103. package/dist/src/execution/domain/run.js +142 -0
  104. package/dist/src/execution/index.js +31 -0
  105. package/dist/src/execution/infrastructure/agent-runner-adapter.js +114 -0
  106. package/dist/src/execution/infrastructure/process-execution.js +29 -0
  107. package/dist/src/execution/infrastructure/prompt-templates.js +42 -0
  108. package/dist/src/execution/infrastructure/runners/claude.js +120 -0
  109. package/dist/src/execution/infrastructure/runners/codex.js +143 -0
  110. package/dist/src/execution/infrastructure/runners/command.js +4 -0
  111. package/dist/src/execution/infrastructure/runners/cursor.js +69 -0
  112. package/dist/src/execution/infrastructure/runners/fake-scenarios.js +99 -0
  113. package/dist/src/execution/infrastructure/runners/fake.js +65 -0
  114. package/dist/src/execution/infrastructure/runners/registry.js +26 -0
  115. package/dist/src/execution/infrastructure/transcript-store.js +223 -0
  116. package/dist/src/execution/infrastructure/transcripts.js +11 -0
  117. package/dist/src/execution/infrastructure/workspace/fake-workspace.js +19 -0
  118. package/dist/src/execution/infrastructure/workspace/git-workspace.js +241 -0
  119. package/dist/src/integrations/application/agent-run-publication-reactor.js +111 -0
  120. package/dist/src/integrations/application/artifact-registration-reactor.js +159 -0
  121. package/dist/src/integrations/application/poll-service.js +25 -0
  122. package/dist/src/integrations/application/terminal-agent-run-report.js +40 -0
  123. package/dist/src/integrations/application/work-admission.js +31 -0
  124. package/dist/src/integrations/application/work-conclusion.js +19 -0
  125. package/dist/src/integrations/contracts/artifact-events.js +60 -0
  126. package/dist/src/integrations/contracts/artifact-vocabulary.js +9 -0
  127. package/dist/src/integrations/contracts/config.js +14 -0
  128. package/dist/src/integrations/contracts/identifiers.js +5 -0
  129. package/dist/src/integrations/contracts/intake-rules.js +14 -0
  130. package/dist/src/integrations/contracts/intake.js +1 -0
  131. package/dist/src/integrations/contracts/outcome-vocabulary.js +7 -0
  132. package/dist/src/integrations/contracts/provider.js +37 -0
  133. package/dist/src/integrations/contracts/streams.js +10 -0
  134. package/dist/src/integrations/delivery/application/delivery-outcome-reactor.js +75 -0
  135. package/dist/src/integrations/delivery/application/delivery-projector.js +165 -0
  136. package/dist/src/integrations/delivery/application/delivery-service.js +173 -0
  137. package/dist/src/integrations/delivery/contracts/commands.js +1 -0
  138. package/dist/src/integrations/delivery/contracts/config.js +1 -0
  139. package/dist/src/integrations/delivery/contracts/event-factory.js +18 -0
  140. package/dist/src/integrations/delivery/contracts/events.js +86 -0
  141. package/dist/src/integrations/delivery/contracts/intents.js +89 -0
  142. package/dist/src/integrations/delivery/contracts/views.js +1 -0
  143. package/dist/src/integrations/delivery/contracts/vocabulary.js +22 -0
  144. package/dist/src/integrations/fake/durable-delivery-provider.js +48 -0
  145. package/dist/src/integrations/fake/external-sink.js +9 -0
  146. package/dist/src/integrations/fake/external-source.js +44 -0
  147. package/dist/src/integrations/fake/inbound-translator.js +130 -0
  148. package/dist/src/integrations/fake/provider.js +95 -0
  149. package/dist/src/integrations/github/application/agent-context-reader.js +54 -0
  150. package/dist/src/integrations/github/application/agent-run-comment.js +94 -0
  151. package/dist/src/integrations/github/application/comment-history-reader.js +43 -0
  152. package/dist/src/integrations/github/application/inbound-context.js +10 -0
  153. package/dist/src/integrations/github/application/inbound-review-signals.js +159 -0
  154. package/dist/src/integrations/github/application/inbound-translator.js +231 -0
  155. package/dist/src/integrations/github/application/inbound-watch-gate-signals.js +84 -0
  156. package/dist/src/integrations/github/application/intake-policy.js +23 -0
  157. package/dist/src/integrations/github/application/outbound-translator.js +47 -0
  158. package/dist/src/integrations/github/application/poll-service.js +29 -0
  159. package/dist/src/integrations/github/application/pull-request-translation.js +10 -0
  160. package/dist/src/integrations/github/application/resource-links.js +8 -0
  161. package/dist/src/integrations/github/application/review-command-translator.js +16 -0
  162. package/dist/src/integrations/github/application/wake-labels.js +95 -0
  163. package/dist/src/integrations/github/contracts/config.js +47 -0
  164. package/dist/src/integrations/github/contracts/events.js +127 -0
  165. package/dist/src/integrations/github/contracts/external-key.js +19 -0
  166. package/dist/src/integrations/github/contracts/payloads.js +9 -0
  167. package/dist/src/integrations/github/contracts/vocabulary.js +55 -0
  168. package/dist/src/integrations/github/index.js +31 -0
  169. package/dist/src/integrations/github/infrastructure/client-reads.js +214 -0
  170. package/dist/src/integrations/github/infrastructure/client.js +132 -0
  171. package/dist/src/integrations/github/infrastructure/content-fingerprint.js +10 -0
  172. package/dist/src/integrations/github/infrastructure/delivery.js +31 -0
  173. package/dist/src/integrations/github/infrastructure/etag-cache.js +56 -0
  174. package/dist/src/integrations/github/infrastructure/gh-auth.js +19 -0
  175. package/dist/src/integrations/github/infrastructure/issue-source.js +109 -0
  176. package/dist/src/integrations/github/infrastructure/pr-source.js +186 -0
  177. package/dist/src/integrations/github/infrastructure/review-source.js +70 -0
  178. package/dist/src/integrations/github/infrastructure/source.js +108 -0
  179. package/dist/src/integrations/github/provider.js +96 -0
  180. package/dist/src/integrations/index.js +30 -0
  181. package/dist/src/kernel/contracts/checkpoint-store.js +1 -0
  182. package/dist/src/kernel/contracts/clock.js +1 -0
  183. package/dist/src/kernel/contracts/commands.js +1 -0
  184. package/dist/src/kernel/contracts/event-journal.js +2 -0
  185. package/dist/src/kernel/contracts/event-schema.js +54 -0
  186. package/dist/src/kernel/contracts/events.js +11 -0
  187. package/dist/src/kernel/contracts/id-generator.js +1 -0
  188. package/dist/src/kernel/contracts/identifiers.js +8 -0
  189. package/dist/src/kernel/contracts/projection-store.js +1 -0
  190. package/dist/src/kernel/contracts/relations.js +1 -0
  191. package/dist/src/kernel/contracts/schema.js +17 -0
  192. package/dist/src/kernel/contracts/vocabulary.js +6 -0
  193. package/dist/src/kernel/domain/event-envelope.js +29 -0
  194. package/dist/src/kernel/domain/match-mode.js +11 -0
  195. package/dist/src/kernel/domain/relation.js +10 -0
  196. package/dist/src/kernel/index.js +17 -0
  197. package/dist/src/{lib/clock.js → kernel/infrastructure/system-clock.js} +3 -3
  198. package/dist/src/kernel/infrastructure/ulid-id-generator.js +9 -0
  199. package/dist/src/main.js +137 -1030
  200. package/dist/src/orchestration/application/accept-activity-outcome.js +25 -0
  201. package/dist/src/orchestration/application/accept-signal.js +35 -0
  202. package/dist/src/orchestration/application/advance-workflow.js +160 -0
  203. package/dist/src/orchestration/application/coordination-claims.js +107 -0
  204. package/dist/src/orchestration/application/group-budget-recorder.js +43 -0
  205. package/dist/src/orchestration/application/orchestration-projection.js +20 -0
  206. package/dist/src/orchestration/application/orchestration-repository.js +36 -0
  207. package/dist/src/orchestration/application/orchestration-service.js +82 -0
  208. package/dist/src/orchestration/application/request-child.js +118 -0
  209. package/dist/src/orchestration/application/signal-reactor.js +17 -0
  210. package/dist/src/orchestration/application/start-workflow.js +51 -0
  211. package/dist/src/orchestration/application/watch-reactor.js +82 -0
  212. package/dist/src/orchestration/contracts/activity-outcome.js +18 -0
  213. package/dist/src/orchestration/contracts/commands.js +1 -0
  214. package/dist/src/orchestration/contracts/config.js +128 -0
  215. package/dist/src/orchestration/contracts/event-decoder.js +263 -0
  216. package/dist/src/orchestration/contracts/events.js +41 -0
  217. package/dist/src/orchestration/contracts/identifiers.js +35 -0
  218. package/dist/src/orchestration/contracts/streams.js +38 -0
  219. package/dist/src/orchestration/contracts/views.js +1 -0
  220. package/dist/src/orchestration/contracts/vocabulary.js +30 -0
  221. package/dist/src/orchestration/domain/activation-policy.js +55 -0
  222. package/dist/src/orchestration/domain/approval-defaults.js +17 -0
  223. package/dist/src/orchestration/domain/child-policy.js +63 -0
  224. package/dist/src/orchestration/domain/compiler.js +162 -0
  225. package/dist/src/orchestration/domain/coordination-events.js +25 -0
  226. package/dist/src/orchestration/domain/decision-events.js +47 -0
  227. package/dist/src/orchestration/domain/interpreter.js +49 -0
  228. package/dist/src/orchestration/domain/operator-retry-policy.js +34 -0
  229. package/dist/src/orchestration/domain/retry-policy.js +26 -0
  230. package/dist/src/orchestration/domain/signal-policy.js +120 -0
  231. package/dist/src/orchestration/domain/supplemental-policy.js +61 -0
  232. package/dist/src/orchestration/domain/transition.js +55 -0
  233. package/dist/src/orchestration/domain/workflow-graph.js +41 -0
  234. package/dist/src/orchestration/domain/workflow-instance-events.js +236 -0
  235. package/dist/src/orchestration/domain/workflow-instance.js +41 -0
  236. package/dist/src/orchestration/domain/workflow-selector.js +21 -0
  237. package/dist/src/orchestration/index.js +19 -0
  238. package/dist/src/persistence/application/projection-runner.js +79 -0
  239. package/dist/src/persistence/filesystem/file-checkpoint-store.js +35 -0
  240. package/dist/src/persistence/filesystem/file-event-journal.js +143 -0
  241. package/dist/src/persistence/filesystem/file-lock.js +91 -0
  242. package/dist/src/persistence/filesystem/file-projection-store.js +64 -0
  243. package/dist/src/persistence/index.js +8 -0
  244. package/dist/src/persistence/memory/in-memory-checkpoint-store.js +14 -0
  245. package/dist/src/persistence/memory/in-memory-event-journal.js +88 -0
  246. package/dist/src/persistence/memory/in-memory-projection-store.js +17 -0
  247. package/dist/src/resources/application/lookup-projections.js +48 -0
  248. package/dist/src/resources/application/resource-lookup.js +16 -0
  249. package/dist/src/resources/application/resource-projections.js +81 -0
  250. package/dist/src/resources/application/resource-repository.js +18 -0
  251. package/dist/src/resources/application/resource-service.js +83 -0
  252. package/dist/src/resources/contracts/commands.js +1 -0
  253. package/dist/src/resources/contracts/config.js +2 -0
  254. package/dist/src/resources/contracts/events.js +83 -0
  255. package/dist/src/resources/contracts/identifiers.js +15 -0
  256. package/dist/src/resources/contracts/streams.js +6 -0
  257. package/dist/src/resources/contracts/views.js +1 -0
  258. package/dist/src/resources/contracts/vocabulary.js +67 -0
  259. package/dist/src/resources/domain/correlation.js +8 -0
  260. package/dist/src/resources/domain/resource.js +58 -0
  261. package/dist/src/resources/index.js +14 -0
  262. package/dist/src/surfaces/api/contracts/activities.js +1 -0
  263. package/dist/src/surfaces/api/contracts/board.js +2 -0
  264. package/dist/src/surfaces/api/contracts/common.js +46 -0
  265. package/dist/src/surfaces/api/contracts/control-plane.js +5 -0
  266. package/dist/src/surfaces/api/contracts/events.js +1 -0
  267. package/dist/src/surfaces/api/contracts/execution.js +1 -0
  268. package/dist/src/surfaces/api/contracts/index.js +13 -0
  269. package/dist/src/surfaces/api/contracts/observability.js +1 -0
  270. package/dist/src/surfaces/api/contracts/orchestration.js +1 -0
  271. package/dist/src/surfaces/api/contracts/resources.js +1 -0
  272. package/dist/src/surfaces/api/contracts/status.js +1 -0
  273. package/dist/src/surfaces/api/contracts/system.js +1 -0
  274. package/dist/src/surfaces/api/contracts/transport-values.js +39 -0
  275. package/dist/src/surfaces/api/contracts/work.js +17 -0
  276. package/dist/src/surfaces/api/http-server.js +121 -0
  277. package/dist/src/surfaces/api/presenters/activities.js +1 -0
  278. package/dist/src/surfaces/api/presenters/board.js +3 -0
  279. package/dist/src/surfaces/api/presenters/control-plane.js +1 -0
  280. package/dist/src/surfaces/api/presenters/events.js +1 -0
  281. package/dist/src/surfaces/api/presenters/execution.js +59 -0
  282. package/dist/src/surfaces/api/presenters/observability.js +1 -0
  283. package/dist/src/surfaces/api/presenters/orchestration.js +19 -0
  284. package/dist/src/surfaces/api/presenters/resources.js +15 -0
  285. package/dist/src/surfaces/api/presenters/status.js +3 -0
  286. package/dist/src/surfaces/api/presenters/system.js +14 -0
  287. package/dist/src/surfaces/api/presenters/work.js +16 -0
  288. package/dist/src/surfaces/api/problem-details.js +9 -0
  289. package/dist/src/surfaces/api/router.js +13 -0
  290. package/dist/src/surfaces/api/routes/activities.js +1 -0
  291. package/dist/src/surfaces/api/routes/applications.js +1 -0
  292. package/dist/src/surfaces/api/routes/commands.js +82 -0
  293. package/dist/src/surfaces/api/routes/control-plane.js +6 -0
  294. package/dist/src/surfaces/api/routes/events.js +1 -0
  295. package/dist/src/surfaces/api/routes/execution.js +8 -0
  296. package/dist/src/surfaces/api/routes/index.js +18 -0
  297. package/dist/src/surfaces/api/routes/observability.js +1 -0
  298. package/dist/src/surfaces/api/routes/orchestration.js +1 -0
  299. package/dist/src/surfaces/api/routes/pagination.js +78 -0
  300. package/dist/src/surfaces/api/routes/read.js +135 -0
  301. package/dist/src/surfaces/api/routes/resources.js +1 -0
  302. package/dist/src/surfaces/api/routes/responses.js +67 -0
  303. package/dist/src/surfaces/api/routes/system.js +1 -0
  304. package/dist/src/surfaces/api/routes/work.js +9 -0
  305. package/dist/src/surfaces/cli/commands/api.js +1 -0
  306. package/dist/src/surfaces/cli/commands/audit.js +10 -0
  307. package/dist/src/surfaces/cli/commands/correlate.js +2 -0
  308. package/dist/src/surfaces/cli/commands/doctor.js +6 -0
  309. package/dist/src/surfaces/cli/commands/init.js +39 -0
  310. package/dist/src/surfaces/cli/commands/sandbox-entrypoint.js +33 -0
  311. package/dist/src/surfaces/cli/commands/sandbox-setup.js +29 -0
  312. package/dist/src/surfaces/cli/commands/sandbox.js +105 -0
  313. package/dist/src/surfaces/cli/commands/self-update.js +38 -0
  314. package/dist/src/surfaces/cli/commands/smoke.js +16 -0
  315. package/dist/src/surfaces/cli/commands/start.js +1 -0
  316. package/dist/src/surfaces/cli/commands/stop.js +13 -0
  317. package/dist/src/surfaces/cli/commands/tick.js +1 -0
  318. package/dist/src/surfaces/cli/commands/ui.js +2 -0
  319. package/dist/src/surfaces/cli/commands/validate-state.js +5 -0
  320. package/dist/src/surfaces/cli/infrastructure/docker-cli.js +221 -0
  321. package/dist/src/surfaces/cli/infrastructure/process-log.js +86 -0
  322. package/dist/src/surfaces/cli/main.js +140 -0
  323. package/dist/src/surfaces/cli/usage.js +33 -0
  324. package/dist/src/surfaces/contracts/config.js +34 -0
  325. package/dist/src/surfaces/index.js +47 -0
  326. package/dist/src/surfaces/web-assets/assets/index-Bfn4FwZG.css +1 -0
  327. package/dist/src/surfaces/web-assets/assets/index-ipB6JPGU.js +1090 -0
  328. package/dist/src/surfaces/web-assets/index.html +13 -0
  329. package/dist/src/surfaces/web-host/asset-source.js +1 -0
  330. package/dist/src/surfaces/web-host/packaged-assets.js +44 -0
  331. package/dist/src/work/application/work-projection.js +59 -0
  332. package/dist/src/work/application/work-repository.js +18 -0
  333. package/dist/src/work/application/work-service.js +117 -0
  334. package/dist/src/work/contracts/commands.js +1 -0
  335. package/dist/src/work/contracts/config.js +2 -0
  336. package/dist/src/work/contracts/events.js +103 -0
  337. package/dist/src/work/contracts/identifiers.js +5 -0
  338. package/dist/src/work/contracts/streams.js +6 -0
  339. package/dist/src/work/contracts/views.js +1 -0
  340. package/dist/src/work/contracts/vocabulary.js +6 -0
  341. package/dist/src/work/domain/work-item.js +89 -0
  342. package/dist/src/work/index.js +11 -0
  343. package/docker/Dockerfile +10 -6
  344. package/docker/Dockerfile.packaged +3 -3
  345. package/package.json +29 -4
  346. package/prompts/implement.md +2 -2
  347. package/templates/SETUP.md +7 -7
  348. package/dist/src/adapters/claude/claude-runner.js +0 -529
  349. package/dist/src/adapters/codex/codex-runner.js +0 -503
  350. package/dist/src/adapters/cursor/cursor-runner.js +0 -418
  351. package/dist/src/adapters/docker/docker-cli.js +0 -150
  352. package/dist/src/adapters/fake/fake-artifact-verifier.js +0 -14
  353. package/dist/src/adapters/fake/fake-github-pull-request-activity-source.js +0 -77
  354. package/dist/src/adapters/fake/fake-resource-index.js +0 -21
  355. package/dist/src/adapters/fake/fake-runner.js +0 -77
  356. package/dist/src/adapters/fake/fake-ticketing-system.js +0 -185
  357. package/dist/src/adapters/fake/fake-workspace-manager.js +0 -81
  358. package/dist/src/adapters/fs/resource-index.js +0 -126
  359. package/dist/src/adapters/fs/self-update-ledger.js +0 -17
  360. package/dist/src/adapters/fs/state-store.js +0 -775
  361. package/dist/src/adapters/git/git-workspace-manager.js +0 -289
  362. package/dist/src/adapters/github/github-artifact-verifier.js +0 -38
  363. package/dist/src/adapters/github/github-auth.js +0 -16
  364. package/dist/src/adapters/github/github-client.js +0 -249
  365. package/dist/src/adapters/github/github-etag-cache.js +0 -57
  366. package/dist/src/adapters/github/github-issues-work-source.js +0 -692
  367. package/dist/src/adapters/github/github-pull-request-activity-source.js +0 -584
  368. package/dist/src/adapters/github/github-pull-request-merge-actor.js +0 -45
  369. package/dist/src/adapters/http/ui-assets.js +0 -1188
  370. package/dist/src/adapters/http/ui-data.js +0 -1107
  371. package/dist/src/adapters/http/ui-server.js +0 -458
  372. package/dist/src/adapters/runner/cli-command.js +0 -73
  373. package/dist/src/adapters/runner/prompt-templates.js +0 -76
  374. package/dist/src/adapters/runner/runner-cli-adapter.js +0 -112
  375. package/dist/src/adapters/runner/runner-registry.js +0 -139
  376. package/dist/src/adapters/runner/runtime-events.js +0 -32
  377. package/dist/src/adapters/runner/stage-prompt.js +0 -268
  378. package/dist/src/adapters/runner/transcripts.js +0 -25
  379. package/dist/src/cli/audit-command.js +0 -36
  380. package/dist/src/cli/correlate-command.js +0 -62
  381. package/dist/src/cli/doctor-command.js +0 -48
  382. package/dist/src/cli/init-command.js +0 -18
  383. package/dist/src/cli/sandbox-command.js +0 -258
  384. package/dist/src/cli/sandbox-entrypoint-command.js +0 -88
  385. package/dist/src/cli/sandbox-exec-logging.js +0 -7
  386. package/dist/src/cli/sandbox-resume.js +0 -79
  387. package/dist/src/cli/sandbox-setup-command.js +0 -31
  388. package/dist/src/cli/scaffold-assets.js +0 -100
  389. package/dist/src/cli/self-update-command.js +0 -198
  390. package/dist/src/cli/startup-preflight.js +0 -134
  391. package/dist/src/cli/stop-command.js +0 -46
  392. package/dist/src/cli/ui-command.js +0 -27
  393. package/dist/src/config/defaults.js +0 -10
  394. package/dist/src/config/discover-config-files.js +0 -11
  395. package/dist/src/config/load-config.js +0 -69
  396. package/dist/src/config/split-config.js +0 -14
  397. package/dist/src/core/active-run-recovery.js +0 -44
  398. package/dist/src/core/approval-intents.js +0 -16
  399. package/dist/src/core/audit-events.js +0 -82
  400. package/dist/src/core/control-plane.js +0 -192
  401. package/dist/src/core/event-builders.js +0 -153
  402. package/dist/src/core/event-resolver.js +0 -249
  403. package/dist/src/core/lifecycle-service.js +0 -8
  404. package/dist/src/core/live-execution.js +0 -81
  405. package/dist/src/core/outbox.js +0 -198
  406. package/dist/src/core/policy-engine.js +0 -389
  407. package/dist/src/core/projection-updater.js +0 -693
  408. package/dist/src/core/quota-backoff.js +0 -69
  409. package/dist/src/core/run-lease.js +0 -38
  410. package/dist/src/core/scheduled-workflow-source.js +0 -199
  411. package/dist/src/core/sink-router.js +0 -107
  412. package/dist/src/core/stale-run-reconciler.js +0 -352
  413. package/dist/src/core/tick-runner.js +0 -2442
  414. package/dist/src/core/workspace-cleanup.js +0 -117
  415. package/dist/src/domain/branch-naming.js +0 -14
  416. package/dist/src/domain/custom-commands.js +0 -58
  417. package/dist/src/domain/event-types.js +0 -158
  418. package/dist/src/domain/manual-labels.js +0 -1
  419. package/dist/src/domain/resource-uri.js +0 -38
  420. package/dist/src/domain/runner-routing.js +0 -127
  421. package/dist/src/domain/runtime-events.js +0 -36
  422. package/dist/src/domain/schema.js +0 -1233
  423. package/dist/src/domain/sources.js +0 -16
  424. package/dist/src/domain/stages.js +0 -36
  425. package/dist/src/domain/work-item-labels.js +0 -48
  426. package/dist/src/domain/work-item-lifecycle.js +0 -10
  427. package/dist/src/domain/work-item-status.js +0 -33
  428. package/dist/src/domain/workflows.js +0 -159
  429. package/dist/src/lib/deep-merge.js +0 -14
  430. package/dist/src/lib/detached-process-logging.js +0 -46
  431. package/dist/src/lib/event-log.js +0 -21
  432. package/dist/src/lib/format.js +0 -41
  433. package/dist/src/lib/json-file.js +0 -23
  434. package/dist/src/lib/lock.js +0 -181
  435. package/dist/src/lib/log-rotation.js +0 -42
  436. package/dist/src/lib/paths.js +0 -52
  437. package/dist/src/lib/process-identity.js +0 -54
  438. package/dist/src/lib/state-health.js +0 -30
  439. package/dist/src/lib/work-id.js +0 -19
  440. package/dist/src/lib/yaml-file.js +0 -20
  441. /package/dist/src/{core → activities/pr}/contracts.js +0 -0
  442. /package/dist/src/{domain/types.js → control-plane/contracts/views.js} +0 -0
@@ -1,2442 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { readFile } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
- import { createLifecycleService } from './lifecycle-service.js';
5
- import { createPolicyEngine } from './policy-engine.js';
6
- import { createProjectionUpdater } from './projection-updater.js';
7
- import { acquireFileLock } from '../lib/lock.js';
8
- import { CORRELATION_REGISTERED_EVENT, CORRELATION_PRIMARY_CONFLICT_EVENT, PR_AUTO_MERGE_ENABLED_EVENT, PR_REVIEW_APPROVED_EVENT, PUBLISH_INTENT_REQUESTED_EVENT, RUN_CLAIMED_EVENT, RUN_COMPLETED_EVENT, } from '../domain/event-types.js';
9
- import { parseRunnerArtifacts, parseRunnerResult } from '../domain/schema.js';
10
- import { maxConfiguredRunnerTimeoutMs, resolveRunnerRouting } from '../domain/runner-routing.js';
11
- import { FROZEN_WORK_ITEM_LABEL, isWorkItemDeleted, isWorkItemRunnable, } from '../domain/work-item-lifecycle.js';
12
- import { isMeaningfulRuntimeEvent } from '../domain/runtime-events.js';
13
- import { chooseAction as chooseWorkflowAction, entryStage as workflowEntryStage, isKnownWorkflowStage, workflowChangedBlockReason, workflowForProjection, workflowNameForProjection, } from '../domain/workflows.js';
14
- import { labelsForWorkItem, SCHEDULED_WORKFLOW_LABEL, } from '../domain/work-item-labels.js';
15
- import { createEventEnvelope } from '../lib/event-log.js';
16
- import { branchNameForIssue } from '../domain/branch-naming.js';
17
- import { customCommandWorkspace, isCustomCommandAction } from '../domain/custom-commands.js';
18
- import { resolveQuotaPauseUntil } from './quota-backoff.js';
19
- import { createLabelsEvent, createPublishIntentEvent } from './event-builders.js';
20
- import { previousMatchingSlot } from './scheduled-workflow-source.js';
21
- import { createOutbox } from './outbox.js';
22
- import { createEventResolver } from './event-resolver.js';
23
- import { createStaleRunReconciler } from './stale-run-reconciler.js';
24
- import { createWorkspaceCleanup } from './workspace-cleanup.js';
25
- import { createAgentExecution } from './live-execution.js';
26
- import { createAutonomousDecisionAuditEvent, workflowRevision as computeWorkflowRevision, } from './audit-events.js';
27
- import { createRunLease, isRunLeaseExpired, renewRunLease, runLeaseRenewalIntervalMs, } from './run-lease.js';
28
- import { currentProcessIdentity, processIdentityMatches } from '../lib/process-identity.js';
29
- import { readJsonFile, writeJsonFile } from '../lib/json-file.js';
30
- import { isMissingPathError } from '../lib/state-health.js';
31
- const prReviewApprovalMarker = '<!-- wake:pr-review-approved -->';
32
- const prReviewChangesMarker = '<!-- wake:pr-review-changes-requested -->';
33
- const activeRunSourceRefreshIntervalMs = 1_000;
34
- function latestHumanCommentId(candidate) {
35
- const human = candidate.comments.filter((c) => !c.isBotAuthored);
36
- return human.at(-1)?.id;
37
- }
38
- // Matched as a token at the start of a (trimmed) line, mirroring the
39
- // /approved and /changes commands in policy-engine.ts.
40
- const interruptCommandPattern = /^\/interrupt\b/i;
41
- // Plain comments during a run are additional context for the next turn, not
42
- // a signal to abandon the current attempt - only an explicit /interrupt
43
- // should cancel an in-flight run, per the PR #411 follow-up discussion.
44
- function newHumanCommentsSince(snapshot, refreshed) {
45
- const knownIds = new Set(snapshot.comments.map((comment) => comment.id));
46
- return refreshed.comments.filter((comment) => !comment.isBotAuthored && !knownIds.has(comment.id));
47
- }
48
- function requestsInterrupt(comments) {
49
- return comments.some((comment) => comment.body.split(/\r?\n/).some((line) => interruptCommandPattern.test(line.trim())));
50
- }
51
- function latestActionableCommentId(candidate) {
52
- const handledCommentId = typeof candidate.context.lastHandledCommentId === 'string'
53
- ? candidate.context.lastHandledCommentId
54
- : undefined;
55
- const lastBotIndex = candidate.comments.reduce((acc, comment, index) => {
56
- return comment.isBotAuthored ? index : acc;
57
- }, -1);
58
- const latestComment = candidate.comments.slice(lastBotIndex).at(-1);
59
- if (latestComment?.id !== handledCommentId &&
60
- latestComment?.isBotAuthored === true &&
61
- latestComment.resourceUri !== undefined &&
62
- latestComment.body.includes(prReviewChangesMarker)) {
63
- return latestComment.id;
64
- }
65
- return latestHumanCommentId(candidate);
66
- }
67
- function projectedSourceRevision(projection) {
68
- const latestCommentUpdatedAt = projection.comments
69
- .map((comment) => comment.updatedAt)
70
- .sort()
71
- .at(-1);
72
- return latestCommentUpdatedAt === undefined
73
- ? `${projection.issue.repo}#${projection.issue.number}@${projection.issue.updatedAt}`
74
- : `${projection.issue.repo}#${projection.issue.number}@${projection.issue.updatedAt};comments@${latestCommentUpdatedAt}`;
75
- }
76
- function stableJson(value) {
77
- if (value === null || typeof value !== 'object') {
78
- return JSON.stringify(value);
79
- }
80
- if (Array.isArray(value)) {
81
- return `[${value.map((entry) => stableJson(entry)).join(',')}]`;
82
- }
83
- const record = value;
84
- return `{${Object.keys(record)
85
- .sort()
86
- .map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
87
- .join(',')}}`;
88
- }
89
- function sha256(value) {
90
- return `sha256:${createHash('sha256').update(stableJson(value)).digest('hex')}`;
91
- }
92
- function isLateralReadOnlyAction(action, config) {
93
- return isCustomCommandAction(action, config);
94
- }
95
- function isEventFromWatcherRun(event) {
96
- return (typeof event.payload === 'object' &&
97
- event.payload !== null &&
98
- 'watcherRun' in event.payload &&
99
- event.payload.watcherRun === true);
100
- }
101
- function shouldPublishRunResult(input) {
102
- if (input.failureClass === 'quota') {
103
- return false;
104
- }
105
- if (input.failureClass === undefined || input.failureClass === 'task') {
106
- return true;
107
- }
108
- return input.failureClass !== input.previousFailureClass;
109
- }
110
- function hasConfirmedExternalSideEffect(projection) {
111
- return projection.correlatedResources.some((resource) => /^[a-z0-9-]+:pr:/.test(resource.resourceUri));
112
- }
113
- function failurePhaseForRecord(record) {
114
- if (record.metadata?.failureSource === 'wake-workspace-validation') {
115
- return 'workspace-validation';
116
- }
117
- if (record.agentPid !== undefined || record.agentProcessStartedAt !== undefined) {
118
- return 'running';
119
- }
120
- if (record.lifecycle === 'PROCESS_STARTING' || record.lifecycle === 'RUNNING') {
121
- return 'process-starting';
122
- }
123
- if (record.lifecycle === 'PREPARING') {
124
- return 'workspace-prep';
125
- }
126
- return 'unknown';
127
- }
128
- function isWorkspaceValidationFailure(error) {
129
- return (error !== null &&
130
- typeof error === 'object' &&
131
- 'failureSource' in error &&
132
- error.failureSource === 'wake-workspace-validation');
133
- }
134
- function classifyFailedRun(input) {
135
- const processStarted = input.record.agentPid !== undefined || input.record.agentProcessStartedAt !== undefined;
136
- const workspaceChanged = input.workspacePath !== undefined;
137
- const externalSideEffects = hasConfirmedExternalSideEffect(input.projection)
138
- ? 'confirmed'
139
- : processStarted
140
- ? 'unknown'
141
- : 'none';
142
- const failurePhase = input.failurePhase ??
143
- ((input.envelope === 'degraded' || input.envelope === 'missing') && input.sentinel === 'FAILED'
144
- ? 'result-parsing'
145
- : failurePhaseForRecord(input.record));
146
- let retrySafety;
147
- if (input.failureClass === 'task') {
148
- retrySafety = 'NOT_RETRYABLE';
149
- }
150
- else if (externalSideEffects === 'confirmed' || workspaceChanged) {
151
- retrySafety = 'REQUIRES_RECONCILIATION';
152
- }
153
- else if (processStarted) {
154
- retrySafety = 'SAFE_TO_RESUME';
155
- }
156
- else {
157
- retrySafety = 'SAFE_TO_RETRY';
158
- }
159
- return {
160
- failurePhase,
161
- processStarted,
162
- workspaceChanged,
163
- externalSideEffects,
164
- retrySafety,
165
- };
166
- }
167
- export function createTickRunner(deps) {
168
- const policy = createPolicyEngine();
169
- const lifecycle = createLifecycleService();
170
- const projectionUpdater = createProjectionUpdater({
171
- stateStore: deps.stateStore,
172
- resourceIndex: deps.resourceIndex,
173
- config: deps.config,
174
- });
175
- const { deliverOutboundEvent, retryUnconfirmedDeliveries, suppressOutboundEvent } = createOutbox({
176
- clock: deps.clock,
177
- stateStore: deps.stateStore,
178
- projectionUpdater,
179
- ...(deps.outboundSink === undefined ? {} : { outboundSink: deps.outboundSink }),
180
- });
181
- const { ingestInboundEvents } = createEventResolver({
182
- clock: deps.clock,
183
- config: deps.config,
184
- stateStore: deps.stateStore,
185
- resourceIndex: deps.resourceIndex,
186
- projectionUpdater,
187
- qualifiesForMint: policy.qualifiesForMint,
188
- });
189
- const { reconcileStaleRunningRecords } = createStaleRunReconciler({
190
- config: deps.config,
191
- stateStore: deps.stateStore,
192
- projectionUpdater,
193
- runnerTimeoutMs,
194
- isRunningRecordActive,
195
- deliverOutboundEvent,
196
- });
197
- const { cleanupClosedIssueWorkspaces, sweepExpiredTranscriptDirs } = createWorkspaceCleanup({
198
- clock: deps.clock,
199
- config: deps.config,
200
- stateStore: deps.stateStore,
201
- workspaceManager: deps.workspaceManager,
202
- projectionUpdater,
203
- });
204
- const ownerInstanceId = `instance-${process.pid}-${Date.now()}`;
205
- function isAwaitingApproval(projection) {
206
- // pendingApprovalAction persists through a changes-requested review round
207
- // within the same gated cycle (that fold path never touches it), so it's
208
- // the faithful "is this item currently gated on human sign-off" signal —
209
- // context.status alone flips between 'awaiting-approval' and
210
- // 'changes-requested' within the same gated cycle and can't be used here.
211
- // The lastRunSentinel fallback tolerates a projection folded before
212
- // pendingApprovalAction was reliably set on every gated DONE.
213
- return (typeof projection.context.pendingApprovalAction === 'string' ||
214
- projection.context.lastRunSentinel === 'AWAITING_APPROVAL');
215
- }
216
- // Always reads the current projection at write time (never a threaded
217
- // local like a stale `workflowName`/`claimedStage`), so a mid-run
218
- // WORKFLOW_SELECTED_EVENT or freeze/schedule change can never be
219
- // stamped with a label reflecting a snapshot from before it took effect.
220
- async function currentLabelsForWorkItem(workItemKey, fallback) {
221
- const projection = (await deps.stateStore.readIssueState(workItemKey)) ?? fallback;
222
- return labelsForWorkItem(projection, deps.config);
223
- }
224
- function hasLabel(projection, label) {
225
- return projection.issue.labels.includes(label);
226
- }
227
- function approvedMergePolicyForStage(stage) {
228
- // Schema defaults materialize a disabled merge block whenever onSuccess is
229
- // set (e.g. approve-only watchers); treat it as no merge policy at all.
230
- const merge = stage?.watch?.find((watch) => watch.onSuccess?.merge !== undefined)?.onSuccess?.merge ?? null;
231
- return merge !== null && (merge.approve || merge.autoMerge) ? merge : null;
232
- }
233
- function escapeRegex(input) {
234
- return input.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
235
- }
236
- function globPatternMatches(pattern, path) {
237
- const regex = new RegExp(`^${pattern.split('*').map(escapeRegex).join('.*')}$`);
238
- return regex.test(path);
239
- }
240
- function reviewerMessageFromApprovalComment(body) {
241
- return body
242
- .replaceAll(prReviewApprovalMarker, '')
243
- .replaceAll('<!-- wake:pr-review-changes-requested -->', '')
244
- .trim();
245
- }
246
- async function evaluatePrMergeRisk(input) {
247
- const incumbent = await deps.resourceIndex.resolve(input.targetResourceUri);
248
- if (incumbent !== undefined && incumbent !== input.projection.workItemKey) {
249
- return {
250
- passed: false,
251
- reason: `Merge policy blocked this PR because ${input.targetResourceUri} is primary-correlated to ${incumbent}, not ${input.projection.workItemKey}.`,
252
- };
253
- }
254
- const blockedLabel = input.policy.blockedLabels.find((label) => input.projection.issue.labels.includes(label));
255
- if (blockedLabel !== undefined) {
256
- return {
257
- passed: false,
258
- reason: `Merge policy blocked this PR because the work item has blocked label "${blockedLabel}".`,
259
- };
260
- }
261
- if (deps.prMergeActor === undefined) {
262
- return {
263
- passed: false,
264
- reason: 'Merge policy blocked this PR because no pull request merge actor is configured.',
265
- };
266
- }
267
- const filesChanged = await deps.prMergeActor.listChangedFiles(input.targetResourceUri);
268
- if (input.policy.maxFilesChanged !== undefined &&
269
- filesChanged.length > input.policy.maxFilesChanged) {
270
- return {
271
- passed: false,
272
- reason: `Merge policy blocked this PR because it changes ${filesChanged.length} files, exceeding maxFilesChanged ${input.policy.maxFilesChanged}.`,
273
- };
274
- }
275
- for (const pattern of input.policy.blockedPaths) {
276
- const blockedPath = filesChanged.find((path) => globPatternMatches(pattern, path));
277
- if (blockedPath !== undefined) {
278
- return {
279
- passed: false,
280
- reason: `Merge policy blocked this PR because changed path "${blockedPath}" matches blockedPaths pattern "${pattern}".`,
281
- };
282
- }
283
- }
284
- return { passed: true, filesChanged };
285
- }
286
- async function publishPrMergePolicyBlock(input) {
287
- const commentId = input.approvalResolution.triggeringCommentId;
288
- const targetResourceUri = input.approvalResolution.targetResourceUri;
289
- if (commentId === undefined || targetResourceUri === undefined) {
290
- return { status: 'idle' };
291
- }
292
- const runId = `merge-gate-blocked-${commentId.replace(/[^a-z0-9]+/gi, '-')}`;
293
- const occurredAt = eventStampNow();
294
- const blockedEvent = createEventEnvelope({
295
- eventId: `${runId}-completed`,
296
- workItemKey: input.projection.workItemKey,
297
- streamScope: 'work-item',
298
- direction: 'internal',
299
- sourceSystem: 'wake',
300
- sourceEventType: RUN_COMPLETED_EVENT,
301
- sourceRefs: {
302
- repo: input.projection.issue.repo,
303
- issueNumber: input.projection.issue.number,
304
- runId,
305
- resourceUri: targetResourceUri,
306
- },
307
- occurredAt,
308
- ingestedAt: occurredAt,
309
- trigger: 'immediate',
310
- payload: {
311
- action: input.approvalResolution.pendingAction,
312
- sentinel: 'BLOCKED',
313
- runId,
314
- reason: 'pr-review-merge-policy-blocked',
315
- blockReason: 'pr-review-merge-policy-blocked',
316
- handledCommentId: commentId,
317
- body: input.reason,
318
- },
319
- });
320
- const appended = await deps.stateStore.appendEventEnvelope(blockedEvent);
321
- await projectionUpdater.rebuildFromEvents([appended]);
322
- const reviewerMessage = reviewerMessageFromApprovalComment(input.approvalResolution.triggeringCommentBody ?? '');
323
- const body = [reviewerMessage, input.reason].filter((part) => part.length > 0).join('\n\n');
324
- await deliverOutboundEvent(createEventEnvelope({
325
- eventId: `${runId}-publish-intent`,
326
- workItemKey: input.projection.workItemKey,
327
- streamScope: 'work-item',
328
- direction: 'outbound',
329
- sourceSystem: 'wake',
330
- sourceEventType: PUBLISH_INTENT_REQUESTED_EVENT,
331
- sourceRefs: {
332
- repo: input.projection.issue.repo,
333
- issueNumber: input.projection.issue.number,
334
- runId,
335
- resourceUri: targetResourceUri,
336
- },
337
- occurredAt,
338
- ingestedAt: occurredAt,
339
- trigger: 'context-only',
340
- payload: {
341
- kind: 'question',
342
- origin: input.projection.origin ?? 'github',
343
- body,
344
- action: input.approvalResolution.pendingAction,
345
- sentinel: 'BLOCKED',
346
- runId,
347
- idempotencyKey: `${commentId}:pr-review-merge-policy-blocked`,
348
- deliveryState: 'PENDING',
349
- },
350
- derivedHints: { stage: input.projection.wake.stage },
351
- }));
352
- await deliverOutboundEvent(createLabelsEvent({
353
- projection: input.projection,
354
- runId,
355
- ...(await currentLabelsForWorkItem(input.projection.workItemKey, input.projection)),
356
- occurredAt,
357
- }));
358
- return {
359
- status: 'processed',
360
- runId,
361
- sentinel: 'BLOCKED',
362
- nextStage: null,
363
- };
364
- }
365
- function describeMergeActorError(error) {
366
- return error instanceof Error ? error.message : String(error);
367
- }
368
- async function performApprovedPrMergeActions(input) {
369
- if (deps.prMergeActor === undefined ||
370
- input.approvalResolution.targetResourceUri === undefined ||
371
- input.approvalResolution.triggeringCommentId === undefined) {
372
- return { blocked: false };
373
- }
374
- const targetResourceUri = input.approvalResolution.targetResourceUri;
375
- const commentId = input.approvalResolution.triggeringCommentId.replace(/[^a-z0-9]+/gi, '-');
376
- const reviewBody = reviewerMessageFromApprovalComment(input.approvalResolution.triggeringCommentBody ?? '');
377
- // approve and autoMerge are independent: one failing doesn't stop the
378
- // other from being attempted. If autoMerge succeeds, a failed approval is
379
- // not a policy block on its own; GitHub can reject self-approval while
380
- // still accepting auto-merge for repos that do not require review.
381
- const approvalBlockedReasons = [];
382
- const autoMergeBlockedReasons = [];
383
- const approvedEventId = `pr-merge-approved-${commentId}`;
384
- if (input.mergePolicy.approve &&
385
- (await deps.stateStore.readEventEnvelope(approvedEventId)) === null) {
386
- try {
387
- await deps.prMergeActor.approve(targetResourceUri, reviewBody);
388
- const occurredAt = eventStampNow();
389
- await deps.stateStore.appendEventEnvelope(createEventEnvelope({
390
- eventId: approvedEventId,
391
- workItemKey: input.projection.workItemKey,
392
- streamScope: 'work-item',
393
- direction: 'internal',
394
- sourceSystem: 'wake',
395
- sourceEventType: PR_REVIEW_APPROVED_EVENT,
396
- sourceRefs: {
397
- resourceUri: targetResourceUri,
398
- commentId: input.approvalResolution.triggeringCommentId,
399
- },
400
- occurredAt,
401
- ingestedAt: occurredAt,
402
- trigger: 'context-only',
403
- payload: {
404
- idempotencyKey: `${input.approvalResolution.triggeringCommentId}:pr-review-approval`,
405
- },
406
- }));
407
- }
408
- catch (error) {
409
- approvalBlockedReasons.push(`Merge policy blocked the approval step: ${describeMergeActorError(error)}`);
410
- }
411
- }
412
- const autoMergeEventId = `pr-auto-merge-enabled-${commentId}`;
413
- let autoMergeSucceeded = false;
414
- if (input.mergePolicy.autoMerge &&
415
- (await deps.stateStore.readEventEnvelope(autoMergeEventId)) === null) {
416
- try {
417
- await deps.prMergeActor.enableAutoMerge(targetResourceUri, input.mergePolicy.mergeMethod);
418
- const occurredAt = eventStampNow();
419
- await deps.stateStore.appendEventEnvelope(createEventEnvelope({
420
- eventId: autoMergeEventId,
421
- workItemKey: input.projection.workItemKey,
422
- streamScope: 'work-item',
423
- direction: 'internal',
424
- sourceSystem: 'wake',
425
- sourceEventType: PR_AUTO_MERGE_ENABLED_EVENT,
426
- sourceRefs: {
427
- resourceUri: targetResourceUri,
428
- commentId: input.approvalResolution.triggeringCommentId,
429
- },
430
- occurredAt,
431
- ingestedAt: occurredAt,
432
- trigger: 'context-only',
433
- payload: {
434
- idempotencyKey: `${input.approvalResolution.triggeringCommentId}:pr-auto-merge`,
435
- },
436
- }));
437
- autoMergeSucceeded = true;
438
- }
439
- catch (error) {
440
- autoMergeBlockedReasons.push(`Merge policy blocked the auto-merge step: ${describeMergeActorError(error)}`);
441
- }
442
- }
443
- else if (input.mergePolicy.autoMerge) {
444
- autoMergeSucceeded = true;
445
- }
446
- const blockedReasons = autoMergeSucceeded && input.mergePolicy.autoMerge
447
- ? autoMergeBlockedReasons
448
- : [...approvalBlockedReasons, ...autoMergeBlockedReasons];
449
- if (blockedReasons.length > 0) {
450
- return { blocked: true, reason: blockedReasons.join(' ') };
451
- }
452
- // Approval failed but auto-merge covered it (e.g. GitHub rejecting
453
- // self-approval on the bot's own PR) - not a policy block, but the
454
- // failure should still leave a visible trail instead of vanishing
455
- // silently, since it may also be a real permissions/config problem.
456
- if (approvalBlockedReasons.length > 0) {
457
- const occurredAt = eventStampNow();
458
- await deliverOutboundEvent(createEventEnvelope({
459
- eventId: `pr-merge-approval-note-${commentId}`,
460
- workItemKey: input.projection.workItemKey,
461
- streamScope: 'work-item',
462
- direction: 'outbound',
463
- sourceSystem: 'wake',
464
- sourceEventType: PUBLISH_INTENT_REQUESTED_EVENT,
465
- sourceRefs: {
466
- repo: input.projection.issue.repo,
467
- issueNumber: input.projection.issue.number,
468
- resourceUri: targetResourceUri,
469
- },
470
- occurredAt,
471
- ingestedAt: occurredAt,
472
- trigger: 'context-only',
473
- payload: {
474
- kind: 'status-update',
475
- origin: input.projection.origin ?? 'github',
476
- body: `Approval step did not block the merge, but did not succeed on its own: ${approvalBlockedReasons.join(' ')}`,
477
- idempotencyKey: `${commentId}:pr-merge-approval-note`,
478
- deliveryState: 'PENDING',
479
- },
480
- derivedHints: { stage: input.projection.wake.stage },
481
- }));
482
- }
483
- return { blocked: false };
484
- }
485
- async function applyApprovedPrMergePolicy(input) {
486
- if (input.approvalResolution.targetResourceUri === undefined || input.mergePolicy === null) {
487
- return null;
488
- }
489
- const risk = await evaluatePrMergeRisk({
490
- projection: input.projection,
491
- targetResourceUri: input.approvalResolution.targetResourceUri,
492
- policy: input.mergePolicy,
493
- });
494
- if (!risk.passed) {
495
- return await publishPrMergePolicyBlock({
496
- projection: input.projection,
497
- approvalResolution: input.approvalResolution,
498
- reason: risk.reason,
499
- workflowName: input.workflowName,
500
- });
501
- }
502
- const mergeActionsResult = await performApprovedPrMergeActions({
503
- projection: input.projection,
504
- approvalResolution: input.approvalResolution,
505
- mergePolicy: input.mergePolicy,
506
- });
507
- if (mergeActionsResult.blocked) {
508
- return await publishPrMergePolicyBlock({
509
- projection: input.projection,
510
- approvalResolution: input.approvalResolution,
511
- reason: mergeActionsResult.reason,
512
- workflowName: input.workflowName,
513
- });
514
- }
515
- return null;
516
- }
517
- // The one deterministic approval transition: /approved, wake:auto, and a
518
- // watcher child's onSuccess.approve all resolve a pending approval through
519
- // this same event shape, so replay folds them identically.
520
- async function applyApprovalTransition(input) {
521
- const nextStage = lifecycle.nextStageFromSentinel(input.projection.wake.stage, 'DONE', input.workflow);
522
- if (nextStage === null) {
523
- return null;
524
- }
525
- const approvalCompletedEvent = createEventEnvelope({
526
- eventId: `${input.approvalId}-completed`,
527
- workItemKey: input.projection.workItemKey,
528
- streamScope: 'work-item',
529
- direction: 'internal',
530
- sourceSystem: 'wake',
531
- sourceEventType: RUN_COMPLETED_EVENT,
532
- sourceRefs: {
533
- repo: input.projection.issue.repo,
534
- issueNumber: input.projection.issue.number,
535
- runId: input.approvalId,
536
- },
537
- occurredAt: input.approvedAt,
538
- ingestedAt: input.approvedAt,
539
- trigger: 'immediate',
540
- payload: {
541
- action: input.pendingAction,
542
- sentinel: 'DONE',
543
- nextStage,
544
- runId: input.approvalId,
545
- reason: input.reason,
546
- ...input.payloadExtras,
547
- },
548
- });
549
- await deps.stateStore.appendEventEnvelope(approvalCompletedEvent);
550
- await projectionUpdater.rebuildFromEvents([approvalCompletedEvent]);
551
- await deliverOutboundEvent(createLabelsEvent({
552
- projection: input.projection,
553
- runId: input.approvalId,
554
- ...(await currentLabelsForWorkItem(input.projection.workItemKey, input.projection)),
555
- occurredAt: input.approvedAt,
556
- }));
557
- return { nextStage };
558
- }
559
- // Folds a human /changes reply onto the parent's context via the same
560
- // `changesRequested` RUN_COMPLETED payload shape a rejecting review watcher
561
- // uses (projection-updater.ts) — same fold, same bounding against
562
- // config.retry.maxChangesRequestedRetries, so a human bouncing /changes
563
- // repeatedly escalates to 'blocked' exactly like an auto-revise loop would.
564
- async function applyChangesRequestedTransition(input) {
565
- const currentCount = typeof input.projection.context.changesRequestedCount === 'number' &&
566
- Number.isInteger(input.projection.context.changesRequestedCount)
567
- ? input.projection.context.changesRequestedCount
568
- : 0;
569
- const escalated = currentCount + 1 > deps.config.retry.maxChangesRequestedRetries;
570
- const event = createEventEnvelope({
571
- eventId: `${input.requestId}-changes-requested`,
572
- workItemKey: input.projection.workItemKey,
573
- streamScope: 'work-item',
574
- direction: 'internal',
575
- sourceSystem: 'wake',
576
- sourceEventType: RUN_COMPLETED_EVENT,
577
- sourceRefs: {
578
- repo: input.projection.issue.repo,
579
- issueNumber: input.projection.issue.number,
580
- runId: input.requestId,
581
- },
582
- occurredAt: input.requestedAt,
583
- ingestedAt: input.requestedAt,
584
- trigger: 'immediate',
585
- payload: {
586
- changesRequested: true,
587
- runId: input.requestId,
588
- reason: 'human:changes-requested',
589
- ...(input.feedbackBody === undefined ? {} : { reviewFeedbackBody: input.feedbackBody }),
590
- ...(input.triggeringCommentId === undefined
591
- ? {}
592
- : { handledCommentId: input.triggeringCommentId }),
593
- },
594
- });
595
- await deps.stateStore.appendEventEnvelope(event);
596
- await projectionUpdater.rebuildFromEvents([event]);
597
- return { escalated };
598
- }
599
- // Closes the loop on #82's review feedback: rather than scrape a PR link
600
- // out of the agent's free text, the agent emits a `wake-artifacts` fence
601
- // (domain/schema.ts's parseRunnerArtifacts) and Wake verifies each claim
602
- // against the real provider before trusting it. No verifier configured (no
603
- // GitHub source) means no claim is ever trusted — this is a no-op, not a
604
- // silent "believe the agent" fallback.
605
- //
606
- // Uses a plain appendEventEnvelope + rebuildFromEvents pair, not
607
- // deliverOutboundEvent: wake.correlation.registered is not a publish-intent
608
- // type, so attemptDelivery's outbound sink call would be a no-op anyway —
609
- // this matches the same plain-append pattern used elsewhere for
610
- // internal-only events (see buildOriginCorrelationEvents' callers).
611
- async function registerReportedArtifacts(input) {
612
- if (deps.artifactVerifier === undefined) {
613
- return;
614
- }
615
- const { artifacts } = parseRunnerArtifacts(input.runnerResult.result);
616
- for (const artifact of artifacts) {
617
- const verified = await deps.artifactVerifier.verify(artifact, {
618
- branch: input.branch,
619
- repo: input.projection.issue.repo,
620
- });
621
- if (verified === null) {
622
- continue;
623
- }
624
- const event = createEventEnvelope({
625
- eventId: `${input.runId}-artifact-${artifact.kind}-${verified.resourceUri.replace(/[^a-z0-9]+/gi, '-')}`,
626
- workItemKey: input.projection.workItemKey,
627
- streamScope: 'work-item',
628
- direction: 'internal',
629
- sourceSystem: 'wake',
630
- sourceEventType: CORRELATION_REGISTERED_EVENT,
631
- sourceRefs: { runId: input.runId },
632
- occurredAt: input.occurredAt,
633
- ingestedAt: input.occurredAt,
634
- trigger: 'context-only',
635
- payload: {
636
- resourceUri: verified.resourceUri,
637
- role: 'implementation',
638
- relation: 'primary',
639
- provenance: 'agent-reported',
640
- registeredBy: input.runId,
641
- idempotencyKey: `${input.runId}:artifact-registration:${verified.resourceUri}`,
642
- },
643
- });
644
- const appended = await deps.stateStore.appendEventEnvelope(event);
645
- await projectionUpdater.rebuildFromEvents([appended]);
646
- }
647
- }
648
- // Events are stamped by reading the clock at the moment of stamping, never
649
- // from a frozen tick-start snapshot. `tickStartedAt` is the tick's *decision*
650
- // clock (policy/staleness), and reusing it to date events inverts them
651
- // against the work source's own poll-time ingestedAt — pollEvents() runs
652
- // after tickStartedAt is captured, so in production every polled upsert is
653
- // LATER than the tick's start. An event dated before the upsert that creates
654
- // the projection it folds into sorts ahead of it in rebuildFromEvents' global
655
- // replay, folds against `current === null`, and is silently dropped. Reading
656
- // per event also stays correct once ticks work items in parallel, where a
657
- // shared per-tick snapshot would tie every concurrent event on ingestedAt and
658
- // leave append order as the only discriminator.
659
- function eventStampNow() {
660
- return deps.clock.now().toISOString();
661
- }
662
- async function appendAuditEvent(input) {
663
- await deps.stateStore.appendEventEnvelope(createAutonomousDecisionAuditEvent(input));
664
- }
665
- // The watchlist is every resource currently correlated to an open work
666
- // item, deduplicated by exact resourceUri. It is derived once per tick from
667
- // the pre-poll projection snapshot (see runTick's ordering note) and handed
668
- // to every configured WorkSource. Core never interprets these URIs — it's
669
- // each source's own job to recognize and filter down to the resourceUri
670
- // shapes it understands (e.g. the GitHub PR source only polls entries
671
- // starting with `github:pr:`, ignoring everything else, including its own
672
- // review-thread correlations) and never core's (CLAUDE.md: "Core compares
673
- // resourceUri strings for equality and never parses a locator").
674
- function deriveWatchlist(projections) {
675
- const seen = new Set();
676
- const watch = [];
677
- for (const projection of projections) {
678
- if (projection.issue.state !== 'open' || isWorkItemDeleted(projection)) {
679
- continue;
680
- }
681
- for (const resource of projection.correlatedResources) {
682
- if (seen.has(resource.resourceUri)) {
683
- continue;
684
- }
685
- seen.add(resource.resourceUri);
686
- watch.push({ resourceUri: resource.resourceUri });
687
- }
688
- }
689
- return watch;
690
- }
691
- // Runs against every open, non-deleted item every tick (not just ones with
692
- // an eligible next action) so a stale label self-heals even on a parked
693
- // item nothing else would otherwise re-check — this is also where
694
- // wake:frozen/wake:scheduled-workflow get their only reconciliation
695
- // coverage, since freeze/unfreeze no longer writes labels inline (§6).
696
- async function markPendingActionableIssues(projections) {
697
- const activeRunWorkItemKeys = new Set();
698
- const now = deps.clock.now();
699
- for (const record of await deps.stateStore.listRunRecordSummaries()) {
700
- if (record.status === 'running' && (await isRunningRecordActive(record, now))) {
701
- activeRunWorkItemKeys.add(record.workItemKey);
702
- }
703
- }
704
- for (const projection of projections) {
705
- if (isWorkItemDeleted(projection) || activeRunWorkItemKeys.has(projection.workItemKey)) {
706
- continue;
707
- }
708
- const labels = labelsForWorkItem(projection, deps.config);
709
- const matchesDesired = hasLabel(projection, labels.statusLabel) &&
710
- hasLabel(projection, labels.stageLabel) &&
711
- hasLabel(projection, labels.workflowLabel) &&
712
- (labels.frozenLabel === undefined || hasLabel(projection, labels.frozenLabel)) &&
713
- (labels.scheduledLabel === undefined || hasLabel(projection, labels.scheduledLabel));
714
- // A toggle label (frozen/scheduled) whose desired state is "absent"
715
- // still needs to be checked for stray presence — the family-prefix
716
- // labels above don't need this since removing them is folded into
717
- // "does the current one match" by construction.
718
- const hasStrayToggleLabel = (labels.frozenLabel === undefined && hasLabel(projection, FROZEN_WORK_ITEM_LABEL)) ||
719
- (labels.scheduledLabel === undefined && hasLabel(projection, SCHEDULED_WORKFLOW_LABEL));
720
- if (matchesDesired && !hasStrayToggleLabel) {
721
- continue;
722
- }
723
- await deliverOutboundEvent(createLabelsEvent({
724
- projection,
725
- runId: `pending-${projection.workItemKey}-${deps.clock.now().getTime()}`,
726
- ...labels,
727
- occurredAt: eventStampNow(),
728
- }));
729
- }
730
- }
731
- function runnerTimeoutMs() {
732
- return maxConfiguredRunnerTimeoutMs(deps.config);
733
- }
734
- async function promptHashForAction(action) {
735
- const promptsRoot = deps.config.paths.promptsRoot;
736
- if (promptsRoot === undefined) {
737
- return sha256({ action, status: 'not-configured' });
738
- }
739
- for (const suffix of ['.md', '.start.md', '.resume.md']) {
740
- const path = join(promptsRoot, `${action}${suffix}`);
741
- try {
742
- return `sha256:${createHash('sha256')
743
- .update(await readFile(path, 'utf8'))
744
- .digest('hex')}`;
745
- }
746
- catch {
747
- // Try the next supported prompt template name.
748
- }
749
- }
750
- return sha256({ action, status: 'missing' });
751
- }
752
- function triggerEventIdForRun(input) {
753
- if (input.watcherTrigger?.kind === 'event') {
754
- return input.watcherTrigger.eventId;
755
- }
756
- return input.recentEvents.at(-1)?.eventId;
757
- }
758
- async function createRunInputSnapshot(input) {
759
- const validation = input.workspaceValidation !== null &&
760
- typeof input.workspaceValidation === 'object' &&
761
- !Array.isArray(input.workspaceValidation)
762
- ? input.workspaceValidation
763
- : undefined;
764
- const repositoryHead = typeof validation?.baseRevision === 'string' ? validation.baseRevision : undefined;
765
- const workspaceHead = typeof validation?.headRevision === 'string' ? validation.headRevision : undefined;
766
- const triggerEventId = triggerEventIdForRun({
767
- ...(input.watcherTrigger === undefined ? {} : { watcherTrigger: input.watcherTrigger }),
768
- recentEvents: input.recentEvents,
769
- });
770
- return deps.stateStore.writeRunInputSnapshot({
771
- schemaVersion: 1,
772
- snapshotId: `${input.runId}-input`,
773
- runId: input.runId,
774
- createdAt: input.createdAt,
775
- action: input.action,
776
- workflowName: input.workflowName,
777
- claimedStage: input.claimedStage,
778
- projectionVersion: input.projection.wake.syncedAt,
779
- sourceUpdatedAt: input.projection.issue.updatedAt,
780
- sourceRevision: input.sourceRevision,
781
- ...(triggerEventId === undefined ? {} : { triggerEventId }),
782
- ...(input.recentEvents.at(-1)?.eventId === undefined
783
- ? {}
784
- : { handledThroughEventId: input.recentEvents.at(-1).eventId }),
785
- workflowHash: await computeWorkflowRevision({
786
- config: deps.config,
787
- workflowName: input.workflowName,
788
- workflow: input.workflow,
789
- action: input.action,
790
- }),
791
- promptHash: await promptHashForAction(input.action),
792
- ...(repositoryHead === undefined ? {} : { repositoryHead }),
793
- ...(workspaceHead === undefined ? {} : { workspaceHead }),
794
- runnerConfigurationHash: sha256({
795
- routing: input.routing,
796
- runner: deps.config.runners[input.routing.runnerName],
797
- }),
798
- projection: input.projection,
799
- recentEvents: input.recentEvents,
800
- });
801
- }
802
- // Counted from durable run records (never an in-memory counter, per the
803
- // "tick is a pure function of durable state" invariant), so this holds
804
- // across process restarts and is a backstop independent of any specific
805
- // dispatch-eligibility bug (stuck watcher, misconfigured cron, a dedupe gap
806
- // not yet caught) - it caps total damage rather than preventing a cause.
807
- async function exceedsDispatchRateLimit(now) {
808
- const { windowMs, maxDispatches } = deps.config.scheduler.dispatchRateLimit;
809
- const windowStartMs = now.getTime() - windowMs;
810
- const runRecords = await deps.stateStore.listRunRecordSummaries();
811
- const recentCount = runRecords.reduce((count, record) => {
812
- const startedAtMs = Date.parse(record.startedAt);
813
- return Number.isFinite(startedAtMs) &&
814
- startedAtMs >= windowStartMs &&
815
- startedAtMs <= now.getTime()
816
- ? count + 1
817
- : count;
818
- }, 0);
819
- return recentCount >= maxDispatches;
820
- }
821
- function cancellationReasonForIneligibleProjection(projection) {
822
- if (projection === null || projection.issue.state !== 'open') {
823
- return 'CANCELED_BY_SOURCE_CLOSED';
824
- }
825
- const requiredAssignees = deps.config.sources.github.policy.requiredAssignees;
826
- if (requiredAssignees.length > 0 &&
827
- !requiredAssignees.some((login) => projection.issue.assignees.includes(login))) {
828
- return 'CANCELED_BY_UNASSIGNMENT';
829
- }
830
- const workflow = workflowForProjection(projection, deps.config);
831
- if (workflow === null || !isKnownWorkflowStage(projection.wake.stage, workflow)) {
832
- return 'CANCELED_BY_WORKFLOW_CHANGE';
833
- }
834
- return 'CANCELED_BY_SUPERSEDING_EVENT';
835
- }
836
- async function isRunningRecordActive(record, now) {
837
- if (record.lease !== undefined && !isRunLeaseExpired(record, now)) {
838
- return true;
839
- }
840
- return (processIdentityMatches({
841
- pid: record.agentPid,
842
- processStartedAt: record.agentProcessStartedAt,
843
- }) ||
844
- processIdentityMatches({
845
- pid: record.workerPid,
846
- processStartedAt: record.workerProcessStartedAt,
847
- }));
848
- }
849
- async function hasSchedulerCapacity(now) {
850
- const runRecords = await deps.stateStore.listRunRecordSummaries();
851
- for (const record of runRecords) {
852
- if (record.status !== 'running') {
853
- continue;
854
- }
855
- if (await isRunningRecordActive(record, now)) {
856
- return false;
857
- }
858
- }
859
- return true;
860
- }
861
- async function parkConfigDriftedProjections(projections) {
862
- let parked = false;
863
- for (const projection of projections) {
864
- if (projection.wake.blockReason === workflowChangedBlockReason) {
865
- continue;
866
- }
867
- const workflow = workflowForProjection(projection, deps.config);
868
- if (workflow !== null && isKnownWorkflowStage(projection.wake.stage, workflow)) {
869
- continue;
870
- }
871
- const occurredAt = eventStampNow();
872
- const eventId = `workflow-changed-${projection.workItemKey}-${deps.clock.now().getTime()}`;
873
- const event = createEventEnvelope({
874
- eventId,
875
- workItemKey: projection.workItemKey,
876
- streamScope: 'work-item',
877
- direction: 'internal',
878
- sourceSystem: 'wake',
879
- sourceEventType: RUN_COMPLETED_EVENT,
880
- sourceRefs: {
881
- repo: projection.issue.repo,
882
- issueNumber: projection.issue.number,
883
- runId: eventId,
884
- },
885
- occurredAt,
886
- ingestedAt: occurredAt,
887
- trigger: 'immediate',
888
- payload: {
889
- sentinel: 'BLOCKED',
890
- runId: eventId,
891
- reason: workflowChangedBlockReason,
892
- blockReason: workflowChangedBlockReason,
893
- body: `Workflow configuration changed; stored workflow or stage is no longer configured.`,
894
- },
895
- });
896
- await deps.stateStore.appendEventEnvelope(event);
897
- await projectionUpdater.rebuildFromEvents([event]);
898
- await deliverOutboundEvent(createLabelsEvent({
899
- projection,
900
- runId: eventId,
901
- ...(await currentLabelsForWorkItem(projection.workItemKey, projection)),
902
- occurredAt,
903
- }));
904
- parked = true;
905
- }
906
- return parked;
907
- }
908
- async function runIntakeTick() {
909
- const lock = await acquireFileLock(deps.stateStore.paths.tickLockFile, {
910
- staleAfterMs: Math.min(runnerTimeoutMs(), 5 * 60 * 1000),
911
- });
912
- if (!lock.acquired) {
913
- return { status: 'locked' };
914
- }
915
- try {
916
- const tickStartedAt = deps.clock.now();
917
- await reconcileStaleRunningRecords(tickStartedAt);
918
- await retryUnconfirmedDeliveries();
919
- const watchlistProjections = await deps.stateStore.listIssueStates();
920
- const inboundEvents = await ingestInboundEvents(await deps.workSource.pollEvents({ watch: deriveWatchlist(watchlistProjections) }));
921
- for (const event of inboundEvents) {
922
- const trigger = event.payload.trigger;
923
- const workflowName = typeof event.payload.workflow === 'string' ? event.payload.workflow : undefined;
924
- const workflow = workflowName === undefined ? undefined : deps.config.workflows[workflowName];
925
- if (trigger?.kind !== 'schedule' || workflowName === undefined || workflow === undefined) {
926
- continue;
927
- }
928
- const timestamp = eventStampNow();
929
- await appendAuditEvent({
930
- eventId: `${event.eventId}-audit-trigger-fired`,
931
- decisionType: 'trigger.fired',
932
- workItemKey: event.workItemKey,
933
- runId: event.eventId,
934
- workflowRevision: await computeWorkflowRevision({
935
- config: deps.config,
936
- workflowName,
937
- workflow,
938
- }),
939
- inputsConsidered: {
940
- workflow: workflowName,
941
- schedule: workflow.trigger?.schedule,
942
- trigger,
943
- sourceEventId: event.eventId,
944
- },
945
- outcome: {
946
- fired: true,
947
- sourceEventType: event.sourceEventType,
948
- },
949
- timestamp,
950
- sourceRefs: event.sourceRefs,
951
- });
952
- }
953
- const projections = await deps.stateStore.listIssueStates();
954
- await cleanupClosedIssueWorkspaces(projections);
955
- await sweepExpiredTranscriptDirs();
956
- // Runs every tick, not only when this tick happened to poll a fresh
957
- // inbound event — a stale label on a parked item (nothing else ever
958
- // re-checks it once there's no next action) otherwise never self-heals.
959
- await markPendingActionableIssues(projections);
960
- return {
961
- status: inboundEvents.length > 0 ? 'processed' : 'idle',
962
- };
963
- }
964
- finally {
965
- await lock.release();
966
- }
967
- }
968
- function watcherStatus(projection) {
969
- const sentinel = projection.context.lastRunSentinel;
970
- if (isAwaitingApproval(projection))
971
- return 'awaiting-approval';
972
- if (sentinel === 'REJECTED')
973
- return 'rejected';
974
- if (sentinel === 'BLOCKED')
975
- return 'blocked';
976
- if (sentinel === 'FAILED')
977
- return 'failed';
978
- if (sentinel === 'DONE')
979
- return 'done';
980
- return 'pending';
981
- }
982
- function watcherKey(input) {
983
- return [
984
- input.workItemKey,
985
- input.parentWorkflowName,
986
- input.parentStage,
987
- String(input.watcherIndex),
988
- ]
989
- .join('__')
990
- .replace(/[^A-Za-z0-9._-]/g, '_');
991
- }
992
- function watcherStateFile(key) {
993
- return join(deps.stateStore.paths.dataRoot, 'watchers', `${key}.json`);
994
- }
995
- async function readWatcherState(key) {
996
- try {
997
- const raw = await readJsonFile(watcherStateFile(key));
998
- if (raw === null || typeof raw !== 'object')
999
- return null;
1000
- const record = raw;
1001
- return record.schemaVersion === 1 &&
1002
- record.key === key &&
1003
- typeof record.updatedAt === 'string'
1004
- ? {
1005
- schemaVersion: 1,
1006
- key,
1007
- ...(typeof record.lastDispatchedEventId === 'string'
1008
- ? { lastDispatchedEventId: record.lastDispatchedEventId }
1009
- : {}),
1010
- ...(typeof record.lastDispatchedSlot === 'string'
1011
- ? { lastDispatchedSlot: record.lastDispatchedSlot }
1012
- : {}),
1013
- ...(typeof record.failureCount === 'number' &&
1014
- Number.isInteger(record.failureCount) &&
1015
- record.failureCount >= 0
1016
- ? { failureCount: record.failureCount }
1017
- : {}),
1018
- updatedAt: record.updatedAt,
1019
- }
1020
- : null;
1021
- }
1022
- catch (error) {
1023
- if (isMissingPathError(error))
1024
- return null;
1025
- throw error;
1026
- }
1027
- }
1028
- async function writeWatcherState(key, patch) {
1029
- const current = await readWatcherState(key);
1030
- await writeJsonFile(watcherStateFile(key), {
1031
- schemaVersion: 1,
1032
- key,
1033
- ...(current?.lastDispatchedEventId === undefined
1034
- ? {}
1035
- : { lastDispatchedEventId: current.lastDispatchedEventId }),
1036
- ...(current?.lastDispatchedSlot === undefined
1037
- ? {}
1038
- : { lastDispatchedSlot: current.lastDispatchedSlot }),
1039
- ...(current?.failureCount === undefined ? {} : { failureCount: current.failureCount }),
1040
- ...patch,
1041
- updatedAt: deps.clock.now().toISOString(),
1042
- });
1043
- }
1044
- function watcherCursorPatch(trigger) {
1045
- return {
1046
- ...(trigger.kind === 'event'
1047
- ? { lastDispatchedEventId: trigger.eventId }
1048
- : { lastDispatchedSlot: trigger.slot }),
1049
- failureCount: 0,
1050
- };
1051
- }
1052
- function isRetryEligibleWatcherFailure(input) {
1053
- return (input.sentinel === 'FAILED' &&
1054
- (input.retrySafety === 'SAFE_TO_RETRY' || input.retrySafety === 'SAFE_TO_RESUME'));
1055
- }
1056
- async function updateWatcherStateAfterCompletion(input) {
1057
- if (input.key === undefined || input.trigger === undefined)
1058
- return;
1059
- if (isRetryEligibleWatcherFailure(input)) {
1060
- const current = await readWatcherState(input.key);
1061
- const failureCount = (current?.failureCount ?? 0) + 1;
1062
- if (failureCount < deps.config.retry.maxFailureRetries) {
1063
- await writeWatcherState(input.key, { failureCount });
1064
- return;
1065
- }
1066
- }
1067
- await writeWatcherState(input.key, watcherCursorPatch(input.trigger));
1068
- }
1069
- async function nextWatcherDispatch(projections, now) {
1070
- for (const projection of projections) {
1071
- // A closed issue can still carry a stale status label (e.g. squash-merged
1072
- // outside Wake's own merge gate, before label reconciliation caught up),
1073
- // so this must be checked explicitly - unlike deriveWatchlist and
1074
- // cancellationReasonForIneligibleProjection, watcher dispatch has no
1075
- // other path that excludes closed issues. Without this, a closed issue
1076
- // whose last-seen local status matches `watcher.while.status` (e.g.
1077
- // awaiting-approval) re-fires its watcher workflow every tick forever.
1078
- if (projection.issue.state !== 'open' || !isWorkItemRunnable(projection))
1079
- continue;
1080
- const parentWorkflow = workflowForProjection(projection, deps.config);
1081
- if (parentWorkflow === null)
1082
- continue;
1083
- const parentWorkflowName = workflowNameForProjection(projection, deps.config);
1084
- const stage = parentWorkflow.stages[projection.wake.stage];
1085
- if (stage === undefined)
1086
- continue;
1087
- for (const [watcherIndex, watcher] of (stage.watch ?? []).entries()) {
1088
- if (!watcher.while.status.includes(watcherStatus(projection)))
1089
- continue;
1090
- const key = watcherKey({
1091
- workItemKey: projection.workItemKey,
1092
- parentWorkflowName,
1093
- parentStage: projection.wake.stage,
1094
- watcherIndex,
1095
- });
1096
- const state = await readWatcherState(key);
1097
- if (watcher.on !== undefined) {
1098
- const events = await deps.stateStore.listRecentEventEnvelopes({
1099
- workItemKey: projection.workItemKey,
1100
- direction: 'internal',
1101
- limit: 200,
1102
- });
1103
- const matchingEvents = events
1104
- .filter((event) => watcher.on.event.includes(event.sourceEventType))
1105
- .filter((event) => !isEventFromWatcherRun(event))
1106
- .sort((left, right) => left.ingestedAt.localeCompare(right.ingestedAt));
1107
- const cursorIndex = state?.lastDispatchedEventId === undefined
1108
- ? -1
1109
- : matchingEvents.findIndex((event) => event.eventId === state.lastDispatchedEventId);
1110
- const undispatched = matchingEvents.slice(cursorIndex + 1);
1111
- const next = (state?.failureCount ?? 0) > 0 ? undispatched.at(0) : undispatched.at(-1);
1112
- if (next !== undefined) {
1113
- return {
1114
- projection,
1115
- parentWorkflowName,
1116
- parentStage: projection.wake.stage,
1117
- watcherIndex,
1118
- targetWorkflowName: watcher.workflow,
1119
- trigger: { kind: 'event', eventId: next.eventId },
1120
- };
1121
- }
1122
- }
1123
- if (watcher.schedule !== undefined) {
1124
- const slot = previousMatchingSlot({
1125
- cron: watcher.schedule.cron,
1126
- now,
1127
- ...(state?.lastDispatchedSlot === undefined ? {} : { after: state.lastDispatchedSlot }),
1128
- });
1129
- if (slot !== null) {
1130
- return {
1131
- projection,
1132
- parentWorkflowName,
1133
- parentStage: projection.wake.stage,
1134
- watcherIndex,
1135
- targetWorkflowName: watcher.workflow,
1136
- trigger: { kind: 'schedule', slot },
1137
- };
1138
- }
1139
- }
1140
- }
1141
- }
1142
- return null;
1143
- }
1144
- async function resolvePrReviewTarget(input) {
1145
- if (deps.artifactVerifier === undefined) {
1146
- return null;
1147
- }
1148
- const { artifacts } = parseRunnerArtifacts(input.runnerResult.result);
1149
- for (const artifact of artifacts) {
1150
- const verified = await deps.artifactVerifier.verify(artifact, {
1151
- branch: branchNameForIssue(input.projection.issue.number),
1152
- repo: input.projection.issue.repo,
1153
- });
1154
- if (verified === null) {
1155
- continue;
1156
- }
1157
- const incumbent = await deps.resourceIndex.resolve(verified.resourceUri);
1158
- if (incumbent !== undefined && incumbent !== input.projection.workItemKey) {
1159
- const conflict = createEventEnvelope({
1160
- eventId: `${input.runId}-pr-review-primary-conflict`,
1161
- workItemKey: input.projection.workItemKey,
1162
- streamScope: 'work-item',
1163
- direction: 'internal',
1164
- sourceSystem: 'wake',
1165
- sourceEventType: CORRELATION_PRIMARY_CONFLICT_EVENT,
1166
- sourceRefs: { runId: input.runId },
1167
- occurredAt: input.occurredAt,
1168
- ingestedAt: input.occurredAt,
1169
- trigger: 'context-only',
1170
- payload: {
1171
- resourceUri: verified.resourceUri,
1172
- incumbentWorkItemKey: incumbent,
1173
- },
1174
- });
1175
- const appended = await deps.stateStore.appendEventEnvelope(conflict);
1176
- await projectionUpdater.rebuildFromEvents([appended]);
1177
- return null;
1178
- }
1179
- if (incumbent === undefined) {
1180
- const event = createEventEnvelope({
1181
- eventId: `${input.runId}-pr-review-artifact-${verified.resourceUri.replace(/[^a-z0-9]+/gi, '-')}`,
1182
- workItemKey: input.projection.workItemKey,
1183
- streamScope: 'work-item',
1184
- direction: 'internal',
1185
- sourceSystem: 'wake',
1186
- sourceEventType: CORRELATION_REGISTERED_EVENT,
1187
- sourceRefs: { runId: input.runId },
1188
- occurredAt: input.occurredAt,
1189
- ingestedAt: input.occurredAt,
1190
- trigger: 'context-only',
1191
- payload: {
1192
- resourceUri: verified.resourceUri,
1193
- role: 'implementation',
1194
- relation: 'primary',
1195
- provenance: 'agent-reported',
1196
- registeredBy: input.runId,
1197
- idempotencyKey: `${input.runId}:pr-review-artifact-registration:${verified.resourceUri}`,
1198
- },
1199
- });
1200
- const appended = await deps.stateStore.appendEventEnvelope(event);
1201
- await projectionUpdater.rebuildFromEvents([appended]);
1202
- }
1203
- return verified.resourceUri;
1204
- }
1205
- return null;
1206
- }
1207
- async function runRunnerTick() {
1208
- const lock = await acquireFileLock(deps.stateStore.paths.runnerLockFile, {
1209
- staleAfterMs: Math.min(runnerTimeoutMs(), 5 * 60 * 1000),
1210
- });
1211
- if (!lock.acquired) {
1212
- return { status: 'locked' };
1213
- }
1214
- try {
1215
- // The tick's *decision* clock: one consistent "now" for staleness and
1216
- // policy, so a single tick's decisions can't disagree with themselves.
1217
- // It is deliberately NOT an event-stamping clock — see eventStampNow().
1218
- // Its only remaining uses are the run records' startedAt (run records
1219
- // are not events and take no part in the rebuild fold).
1220
- const tickStartedAt = deps.clock.now();
1221
- const nowIso = tickStartedAt.toISOString();
1222
- await reconcileStaleRunningRecords(tickStartedAt);
1223
- const projections = await deps.stateStore.listIssueStates();
1224
- if (await parkConfigDriftedProjections(projections)) {
1225
- return { status: 'processed' };
1226
- }
1227
- let candidate = projections.find((issue) => isWorkItemRunnable(issue) &&
1228
- policy.resolveNextEligibleAction(issue, deps.config) !== null);
1229
- const watcherDispatch = candidate === undefined ? await nextWatcherDispatch(projections, tickStartedAt) : null;
1230
- candidate ??= watcherDispatch?.projection;
1231
- let watcherStateKeyForRun;
1232
- let watcherTriggerForRun;
1233
- const watcherRun = watcherDispatch !== null;
1234
- if (candidate === undefined) {
1235
- return { status: 'idle' };
1236
- }
1237
- if (await exceedsDispatchRateLimit(tickStartedAt)) {
1238
- const rateLimitedWorkflowName = workflowNameForProjection(candidate, deps.config);
1239
- const rateLimitedWorkflow = workflowForProjection(candidate, deps.config);
1240
- const blockedAt = eventStampNow();
1241
- await appendAuditEvent({
1242
- eventId: `dispatch-rate-limited-${candidate.workItemKey}-${tickStartedAt.getTime()}`,
1243
- decisionType: 'dispatch.rate-limited',
1244
- workItemKey: candidate.workItemKey,
1245
- runId: `dispatch-rate-limited-${candidate.workItemKey}-${tickStartedAt.getTime()}`,
1246
- workflowRevision: rateLimitedWorkflow === null
1247
- ? 'sha256:unavailable'
1248
- : await computeWorkflowRevision({
1249
- config: deps.config,
1250
- workflowName: rateLimitedWorkflowName,
1251
- workflow: rateLimitedWorkflow,
1252
- }),
1253
- inputsConsidered: {
1254
- windowMs: deps.config.scheduler.dispatchRateLimit.windowMs,
1255
- maxDispatches: deps.config.scheduler.dispatchRateLimit.maxDispatches,
1256
- watcherRun,
1257
- },
1258
- outcome: { dispatched: false, reason: 'dispatch-rate-limit-exceeded' },
1259
- timestamp: blockedAt,
1260
- sourceRefs: { repo: candidate.issue.repo, issueNumber: candidate.issue.number },
1261
- });
1262
- return { status: 'idle' };
1263
- }
1264
- let sourceRevision = projectedSourceRevision(candidate);
1265
- let refresh;
1266
- try {
1267
- refresh = await deps.workSource.refreshForDispatch?.({ projection: candidate });
1268
- }
1269
- catch {
1270
- return { status: 'idle' };
1271
- }
1272
- if (refresh !== undefined && refresh !== null) {
1273
- if (refresh.sourceExists === false) {
1274
- return { status: 'idle' };
1275
- }
1276
- sourceRevision = refresh.sourceRevision;
1277
- if (refresh.events.length > 0) {
1278
- await ingestInboundEvents(refresh.events);
1279
- candidate = (await deps.stateStore.readIssueState(candidate.workItemKey)) ?? candidate;
1280
- }
1281
- }
1282
- if (!watcherRun && policy.resolveNextEligibleAction(candidate, deps.config) === null) {
1283
- return { status: 'idle' };
1284
- }
1285
- const workflow = workflowForProjection(candidate, deps.config);
1286
- if (workflow === null) {
1287
- return { status: 'idle' };
1288
- }
1289
- let workflowName = workflowNameForProjection(candidate, deps.config);
1290
- let action;
1291
- let command;
1292
- let claimedStage = candidate.wake.stage;
1293
- let workspaceMode = 'none';
1294
- let promptContextOverrides;
1295
- if (watcherDispatch !== null) {
1296
- const targetWorkflow = deps.config.workflows[watcherDispatch.targetWorkflowName];
1297
- if (targetWorkflow === undefined) {
1298
- return { status: 'idle' };
1299
- }
1300
- const entryStageName = workflowEntryStage(targetWorkflow);
1301
- const entryStage = targetWorkflow.stages[entryStageName];
1302
- if (entryStage === undefined) {
1303
- return { status: 'idle' };
1304
- }
1305
- action = entryStage.action ?? entryStageName;
1306
- claimedStage = entryStageName;
1307
- workspaceMode = entryStage.workspace;
1308
- // The triggering event's own body (e.g. a refine plan comment) is
1309
- // durable and already local the moment this fires — projection.comments
1310
- // only gets it once a later GitHub poll echoes it back, which a watcher
1311
- // dispatched off the same-tick completion event can easily outrace.
1312
- const triggeringEvent = watcherDispatch.trigger.kind === 'event'
1313
- ? await deps.stateStore.readEventEnvelope(watcherDispatch.trigger.eventId)
1314
- : null;
1315
- const triggeringBody = triggeringEvent?.payload.body;
1316
- promptContextOverrides = {
1317
- ...entryStage.promptContext,
1318
- ...(typeof triggeringBody === 'string'
1319
- ? { parentPendingReviewBody: triggeringBody }
1320
- : {}),
1321
- };
1322
- workflowName = watcherDispatch.targetWorkflowName;
1323
- watcherStateKeyForRun = watcherKey({
1324
- workItemKey: watcherDispatch.projection.workItemKey,
1325
- parentWorkflowName: watcherDispatch.parentWorkflowName,
1326
- parentStage: watcherDispatch.parentStage,
1327
- watcherIndex: watcherDispatch.watcherIndex,
1328
- });
1329
- watcherTriggerForRun = watcherDispatch.trigger;
1330
- }
1331
- else if (isAwaitingApproval(candidate)) {
1332
- const customCommandRequest = policy.resolveCustomCommandRequest(candidate, deps.config);
1333
- if (customCommandRequest !== null) {
1334
- action = customCommandRequest.action;
1335
- command = customCommandRequest.command;
1336
- claimedStage = candidate.wake.stage;
1337
- workspaceMode = customCommandRequest.workspace;
1338
- }
1339
- else {
1340
- const approvalResolution = policy.resolveApprovalTransition(candidate);
1341
- if (approvalResolution === null) {
1342
- const changesRequestedAction = policy.resolveChangesRequestedAction(candidate);
1343
- if (changesRequestedAction !== null) {
1344
- action = changesRequestedAction;
1345
- const workflowAction = chooseWorkflowAction(candidate, workflow);
1346
- claimedStage = workflowAction?.stage ?? candidate.wake.stage;
1347
- workspaceMode = workflowAction?.workspace ?? 'none';
1348
- promptContextOverrides = {
1349
- ...workflowAction?.promptContext,
1350
- ...(typeof candidate.context.changesRequestedFeedback === 'string'
1351
- ? { parentPendingReviewBody: candidate.context.changesRequestedFeedback }
1352
- : {}),
1353
- };
1354
- }
1355
- else {
1356
- const reviewAction = policy.resolvePendingReviewFeedback(candidate);
1357
- if (reviewAction === null) {
1358
- return { status: 'idle' };
1359
- }
1360
- action = reviewAction;
1361
- const workflowAction = chooseWorkflowAction(candidate, workflow);
1362
- claimedStage = workflowAction?.stage ?? candidate.wake.stage;
1363
- workspaceMode = workflowAction?.workspace ?? 'none';
1364
- promptContextOverrides = workflowAction?.promptContext;
1365
- }
1366
- }
1367
- else if (approvalResolution.approved) {
1368
- const mergePolicyBlock = await applyApprovedPrMergePolicy({
1369
- projection: candidate,
1370
- approvalResolution,
1371
- workflowName,
1372
- mergePolicy: approvedMergePolicyForStage(workflow.stages[candidate.wake.stage]),
1373
- });
1374
- if (mergePolicyBlock !== null) {
1375
- return mergePolicyBlock;
1376
- }
1377
- const approvalId = `approval-${candidate.issue.number}-${deps.clock.now().getTime()}`;
1378
- const approvedAt = deps.clock.now().toISOString();
1379
- const automaticApproval = approvalResolution.automatic === true;
1380
- const applied = await applyApprovalTransition({
1381
- projection: candidate,
1382
- pendingAction: approvalResolution.pendingAction,
1383
- approvalId,
1384
- approvedAt,
1385
- reason: automaticApproval ? 'auto:approved' : 'human:approved',
1386
- workflow,
1387
- workflowName,
1388
- payloadExtras: automaticApproval
1389
- ? {
1390
- autoResolution: {
1391
- kind: 'awaiting-approval',
1392
- classification: 'auto-approval',
1393
- reasoning: approvalResolution.reason,
1394
- },
1395
- }
1396
- : {
1397
- handledCommentId: approvalResolution.triggeringCommentId ?? latestHumanCommentId(candidate),
1398
- },
1399
- });
1400
- if (applied === null) {
1401
- return { status: 'idle' };
1402
- }
1403
- if (automaticApproval) {
1404
- await appendAuditEvent({
1405
- eventId: `${approvalId}-audit-auto-resolution`,
1406
- decisionType: 'approval.auto-resolved',
1407
- workItemKey: candidate.workItemKey,
1408
- runId: approvalId,
1409
- workflowRevision: await computeWorkflowRevision({
1410
- config: deps.config,
1411
- workflowName,
1412
- workflow,
1413
- action: approvalResolution.pendingAction,
1414
- }),
1415
- inputsConsidered: {
1416
- labels: candidate.issue.labels,
1417
- pendingAction: approvalResolution.pendingAction,
1418
- pendingApprovalAllowAutoApproval: candidate.context.pendingApprovalAllowAutoApproval === true,
1419
- },
1420
- outcome: {
1421
- approved: true,
1422
- nextStage: applied.nextStage,
1423
- reason: approvalResolution.reason,
1424
- },
1425
- timestamp: approvedAt,
1426
- sourceRefs: {
1427
- repo: candidate.issue.repo,
1428
- issueNumber: candidate.issue.number,
1429
- },
1430
- });
1431
- }
1432
- return {
1433
- status: 'processed',
1434
- runId: approvalId,
1435
- sentinel: 'DONE',
1436
- nextStage: applied.nextStage,
1437
- };
1438
- }
1439
- else {
1440
- const changesRequestId = `changes-requested-${candidate.issue.number}-${deps.clock.now().getTime()}`;
1441
- const requestedAt = deps.clock.now().toISOString();
1442
- const changesRequestedTriggeringCommentId = approvalResolution.triggeringCommentId ?? latestHumanCommentId(candidate);
1443
- const { escalated } = await applyChangesRequestedTransition({
1444
- projection: candidate,
1445
- requestId: changesRequestId,
1446
- requestedAt,
1447
- ...(approvalResolution.triggeringCommentBody === undefined
1448
- ? {}
1449
- : { feedbackBody: approvalResolution.triggeringCommentBody }),
1450
- ...(changesRequestedTriggeringCommentId === undefined
1451
- ? {}
1452
- : { triggeringCommentId: changesRequestedTriggeringCommentId }),
1453
- });
1454
- if (escalated) {
1455
- return { status: 'idle' };
1456
- }
1457
- action = approvalResolution.pendingAction;
1458
- const workflowAction = chooseWorkflowAction(candidate, workflow);
1459
- claimedStage = workflowAction?.stage ?? candidate.wake.stage;
1460
- workspaceMode = workflowAction?.workspace ?? 'none';
1461
- promptContextOverrides = {
1462
- ...workflowAction?.promptContext,
1463
- ...(approvalResolution.changesRequested === true &&
1464
- approvalResolution.triggeringCommentBody !== undefined
1465
- ? { parentPendingReviewBody: approvalResolution.triggeringCommentBody }
1466
- : {}),
1467
- };
1468
- }
1469
- }
1470
- }
1471
- else {
1472
- const workflowAction = chooseWorkflowAction(candidate, workflow);
1473
- // Retry takes priority over the stage's fresh default action.
1474
- // chooseWorkflowAction almost always returns a non-null action for
1475
- // any valid stage (e.g. 'implement'), so checking it first would
1476
- // silently discard chooseRetryActionAfterHumanReply's decision
1477
- // whenever a FAILED/BLOCKED run left a lateral action (like
1478
- // `revise`) unfinished with a fresh human reply waiting — instead
1479
- // of resuming that action, it would restart the stage from
1480
- // scratch (#258 follow-up incident: a FAILED `revise` run fell
1481
- // back to a full fresh `implement` run and lost the PR-feedback
1482
- // context).
1483
- const nextAction = policy.resolveCustomCommandRequest(candidate, deps.config)?.action ??
1484
- policy.chooseRetryActionAfterHumanReply(candidate, workflow) ??
1485
- workflowAction?.action ??
1486
- null;
1487
- if (nextAction === null) {
1488
- return { status: 'idle' };
1489
- }
1490
- action = nextAction;
1491
- command = policy.resolveCustomCommandRequest(candidate, deps.config)?.command;
1492
- claimedStage = workflowAction?.stage ?? candidate.wake.stage;
1493
- workspaceMode =
1494
- customCommandWorkspace(action, deps.config) ?? workflowAction?.workspace ?? 'none';
1495
- promptContextOverrides = workflowAction?.promptContext;
1496
- }
1497
- // Resolve routing (with sideways fallback across quota-paused runners,
1498
- // #67) before claiming a run, so a fully-paused tier costs nothing more
1499
- // than an idle tick instead of a claimed-but-doomed run record.
1500
- const ledgerAtStart = await deps.stateStore.readLedger();
1501
- const routing = resolveRunnerRouting({
1502
- config: deps.config,
1503
- stage: claimedStage,
1504
- action,
1505
- workflowName,
1506
- ...(command === undefined ? {} : { command }),
1507
- now: tickStartedAt,
1508
- ...(ledgerAtStart === null ? {} : { ledger: ledgerAtStart }),
1509
- });
1510
- if (routing === null) {
1511
- return { status: 'idle' };
1512
- }
1513
- if (!(await hasSchedulerCapacity(deps.clock.now()))) {
1514
- return { status: 'idle' };
1515
- }
1516
- const runId = `run-${candidate.issue.number}-${deps.clock.now().getTime()}`;
1517
- const lease = createRunLease({ clock: deps.clock, ownerInstanceId });
1518
- const workerIdentity = currentProcessIdentity();
1519
- const runningRecord = {
1520
- schemaVersion: 1,
1521
- runId,
1522
- workItemKey: candidate.workItemKey,
1523
- repo: candidate.issue.repo,
1524
- issueNumber: candidate.issue.number,
1525
- action,
1526
- lifecycle: 'CREATED',
1527
- status: 'running',
1528
- startedAt: nowIso,
1529
- routing,
1530
- lease,
1531
- workerPid: workerIdentity.pid,
1532
- workerProcessStartedAt: workerIdentity.processStartedAt,
1533
- metadata: {
1534
- sourceRevision,
1535
- ...(watcherRun
1536
- ? {
1537
- watcher: true,
1538
- watcherWorkflow: workflowName,
1539
- watcherStateKey: watcherStateKeyForRun,
1540
- watcherTrigger: watcherTriggerForRun,
1541
- }
1542
- : {}),
1543
- },
1544
- };
1545
- await deps.stateStore.writeRunRecord(runningRecord);
1546
- async function transitionRunLifecycle(lifecycle) {
1547
- await deps.stateStore.writeRunRecord({
1548
- ...(await deps.stateStore.readRunRecord(runId)),
1549
- lifecycle,
1550
- });
1551
- }
1552
- async function appendRuntimeEvent(event) {
1553
- const timestamp = event.timestamp ?? deps.clock.now().toISOString();
1554
- await deps.stateStore.updateRunRecordIf(runId, {
1555
- expect: (record) => record.status === 'running' &&
1556
- record.lease?.leaseId === lease.leaseId &&
1557
- record.lease.ownerInstanceId === ownerInstanceId,
1558
- update: (record) => {
1559
- const runtimeEvents = record.runtimeEvents ?? [];
1560
- const sequence = event.sequence ?? runtimeEvents.length;
1561
- const normalizedEvent = {
1562
- ...event,
1563
- timestamp,
1564
- sequence,
1565
- };
1566
- return {
1567
- ...record,
1568
- runtimeEvents: [...runtimeEvents, normalizedEvent],
1569
- ...(isMeaningfulRuntimeEvent(normalizedEvent)
1570
- ? { lastMeaningfulActivityAt: normalizedEvent.timestamp }
1571
- : {}),
1572
- };
1573
- },
1574
- });
1575
- }
1576
- const claimedAt = eventStampNow();
1577
- const claimedEvent = createEventEnvelope({
1578
- eventId: `${runId}-claimed`,
1579
- workItemKey: candidate.workItemKey,
1580
- streamScope: 'work-item',
1581
- direction: 'internal',
1582
- sourceSystem: 'wake',
1583
- sourceEventType: RUN_CLAIMED_EVENT,
1584
- sourceRefs: {
1585
- repo: candidate.issue.repo,
1586
- issueNumber: candidate.issue.number,
1587
- runId,
1588
- },
1589
- occurredAt: claimedAt,
1590
- ingestedAt: claimedAt,
1591
- trigger: 'immediate',
1592
- payload: {
1593
- action,
1594
- priorStage: candidate.wake.stage,
1595
- ...(watcherRun ? {} : { claimedStage }),
1596
- sourceRevision,
1597
- ...(watcherRun ? { watcherRun: true, watcherTrigger: watcherTriggerForRun } : {}),
1598
- },
1599
- });
1600
- await deps.stateStore.appendEventEnvelope(claimedEvent);
1601
- if (watcherRun && watcherDispatch !== null && watcherTriggerForRun !== undefined) {
1602
- await appendAuditEvent({
1603
- eventId: `${runId}-audit-watcher-dispatched`,
1604
- decisionType: 'watcher.dispatched',
1605
- workItemKey: candidate.workItemKey,
1606
- runId,
1607
- workflowRevision: await computeWorkflowRevision({
1608
- config: deps.config,
1609
- workflowName,
1610
- workflow: deps.config.workflows[workflowName] ?? workflow,
1611
- action,
1612
- }),
1613
- inputsConsidered: {
1614
- parentWorkflowName: watcherDispatch.parentWorkflowName,
1615
- parentStage: watcherDispatch.parentStage,
1616
- watcherIndex: watcherDispatch.watcherIndex,
1617
- watcherStatus: watcherStatus(candidate),
1618
- trigger: watcherTriggerForRun,
1619
- },
1620
- outcome: {
1621
- dispatched: true,
1622
- targetWorkflowName: workflowName,
1623
- action,
1624
- claimedStage,
1625
- },
1626
- timestamp: claimedAt,
1627
- sourceRefs: {
1628
- repo: candidate.issue.repo,
1629
- issueNumber: candidate.issue.number,
1630
- },
1631
- });
1632
- }
1633
- await transitionRunLifecycle('CLAIMED');
1634
- await projectionUpdater.rebuildFromEvents([claimedEvent]);
1635
- // Watcher runs execute a *different* workflow's stage (e.g. plan-review)
1636
- // against the same parent projection, not a stage transition of the
1637
- // parent's own workflow — writing labels here would stamp the child
1638
- // workflow's stage/workflow onto the parent's GitHub issue. The parent's
1639
- // labels must only ever reflect the parent's own action (see the mirrored
1640
- // guard on the completion path below).
1641
- if (!watcherRun) {
1642
- await deliverOutboundEvent(createLabelsEvent({
1643
- projection: candidate,
1644
- runId,
1645
- ...(await currentLabelsForWorkItem(candidate.workItemKey, candidate)),
1646
- occurredAt: eventStampNow(),
1647
- }));
1648
- }
1649
- let leaseRenewalTimer;
1650
- function startLeaseRenewal() {
1651
- leaseRenewalTimer = setInterval(() => {
1652
- void renewRunLease({
1653
- stateStore: deps.stateStore,
1654
- runId,
1655
- leaseId: lease.leaseId,
1656
- ownerInstanceId,
1657
- clock: deps.clock,
1658
- });
1659
- }, runLeaseRenewalIntervalMs);
1660
- }
1661
- function delayActiveRunRefresh() {
1662
- return new Promise((resolve) => {
1663
- const timer = setTimeout(resolve, activeRunSourceRefreshIntervalMs);
1664
- timer.unref?.();
1665
- });
1666
- }
1667
- const recentEvents = await deps.stateStore.listEventEnvelopesForWorkItem(candidate.workItemKey, 6);
1668
- const activeCandidate = candidate;
1669
- const runnerProjection = watcherRun
1670
- ? {
1671
- ...activeCandidate,
1672
- wake: {
1673
- ...activeCandidate.wake,
1674
- sessionId: undefined,
1675
- sessionCli: undefined,
1676
- },
1677
- }
1678
- : activeCandidate;
1679
- const inputSnapshot = await createRunInputSnapshot({
1680
- runId,
1681
- createdAt: deps.clock.now().toISOString(),
1682
- action,
1683
- workflowName,
1684
- workflow: deps.config.workflows[workflowName] ?? workflow,
1685
- claimedStage,
1686
- projection: runnerProjection,
1687
- recentEvents,
1688
- sourceRevision,
1689
- routing,
1690
- ...(watcherTriggerForRun === undefined ? {} : { watcherTrigger: watcherTriggerForRun }),
1691
- });
1692
- await deps.stateStore.writeRunRecord({
1693
- ...(await deps.stateStore.readRunRecord(runId)),
1694
- inputSnapshotId: inputSnapshot.snapshotId,
1695
- });
1696
- try {
1697
- await transitionRunLifecycle('PREPARING');
1698
- const prepareResult = workspaceMode === 'branch'
1699
- ? await deps.workspaceManager.prepareWorkspace({
1700
- workId: candidate.workItemKey,
1701
- repo: candidate.issue.repo,
1702
- issueNumber: candidate.issue.number,
1703
- })
1704
- : workspaceMode === 'read-only'
1705
- ? await deps.workspaceManager.prepareReadOnlyClone({
1706
- repo: candidate.issue.repo,
1707
- })
1708
- : {};
1709
- const { workspacePath } = prepareResult;
1710
- const mergeConflictDetected = 'mergeConflictDetected' in prepareResult ? prepareResult.mergeConflictDetected : false;
1711
- const upstreamChanges = 'upstreamChanges' in prepareResult && typeof prepareResult.upstreamChanges === 'string'
1712
- ? prepareResult.upstreamChanges
1713
- : undefined;
1714
- const preExistingUncommittedChanges = 'preExistingUncommittedChanges' in prepareResult
1715
- ? prepareResult.preExistingUncommittedChanges
1716
- : false;
1717
- const preparedRecord = (await deps.stateStore.readRunRecord(runId));
1718
- await deps.stateStore.writeRunRecord({
1719
- ...preparedRecord,
1720
- lifecycle: 'PROCESS_STARTING',
1721
- metadata: {
1722
- ...preparedRecord.metadata,
1723
- ...(workspacePath === undefined ? {} : { workspacePath }),
1724
- workspaceMode,
1725
- inputSnapshotId: inputSnapshot.snapshotId,
1726
- ...(prepareResult.validation === undefined
1727
- ? {}
1728
- : { workspaceValidation: prepareResult.validation }),
1729
- },
1730
- });
1731
- await transitionRunLifecycle('RUNNING');
1732
- startLeaseRenewal();
1733
- let executionFinished = false;
1734
- let cancellationReason = null;
1735
- const runnerInput = {
1736
- action,
1737
- projection: runnerProjection,
1738
- recentEvents,
1739
- config: deps.config,
1740
- runId,
1741
- routing,
1742
- workspaceMode,
1743
- ...(promptContextOverrides === undefined ? {} : { promptContextOverrides }),
1744
- ...(workspacePath === undefined ? {} : { workspacePath }),
1745
- ...(mergeConflictDetected ? { mergeConflictDetected: true } : {}),
1746
- ...(upstreamChanges === undefined ? {} : { upstreamChanges }),
1747
- ...(preExistingUncommittedChanges ? { preExistingUncommittedChanges: true } : {}),
1748
- onProcessStart: async (identity) => {
1749
- await deps.stateStore.updateRunRecordIf(runId, {
1750
- expect: (record) => record.status === 'running' &&
1751
- record.lease?.leaseId === lease.leaseId &&
1752
- record.lease.ownerInstanceId === ownerInstanceId,
1753
- update: (record) => ({
1754
- ...record,
1755
- agentPid: identity.pid,
1756
- agentProcessStartedAt: identity.processStartedAt,
1757
- }),
1758
- });
1759
- },
1760
- onRuntimeEvent: appendRuntimeEvent,
1761
- };
1762
- const execution = deps.runner.start === undefined
1763
- ? createAgentExecution(runnerInput, (liveInput) => deps.runner.run(liveInput))
1764
- : await deps.runner.start(runnerInput);
1765
- async function persistCancellationRequest(reason) {
1766
- const requestedAt = deps.clock.now().toISOString();
1767
- await deps.stateStore.updateRunRecordIf(runId, {
1768
- expect: (record) => record.status === 'running' &&
1769
- record.lease?.leaseId === lease.leaseId &&
1770
- record.lease.ownerInstanceId === ownerInstanceId,
1771
- update: (record) => ({
1772
- ...record,
1773
- metadata: {
1774
- ...record.metadata,
1775
- cancellation: {
1776
- reason,
1777
- requestedAt,
1778
- source: 'active-source-reconciliation',
1779
- },
1780
- },
1781
- }),
1782
- });
1783
- }
1784
- async function superviseActiveSource() {
1785
- if (deps.workSource.refreshForDispatch === undefined) {
1786
- return null;
1787
- }
1788
- while (!executionFinished) {
1789
- await delayActiveRunRefresh();
1790
- if (executionFinished) {
1791
- return null;
1792
- }
1793
- let activeRefresh;
1794
- try {
1795
- activeRefresh = await deps.workSource.refreshForDispatch({
1796
- projection: activeCandidate,
1797
- });
1798
- }
1799
- catch {
1800
- continue;
1801
- }
1802
- if (activeRefresh === null) {
1803
- continue;
1804
- }
1805
- if (activeRefresh.events.length > 0) {
1806
- await ingestInboundEvents(activeRefresh.events);
1807
- }
1808
- const refreshedProjection = (await deps.stateStore.readIssueState(activeCandidate.workItemKey)) ??
1809
- activeCandidate;
1810
- const newHumanComments = newHumanCommentsSince(inputSnapshot.projection, refreshedProjection);
1811
- if (requestsInterrupt(newHumanComments)) {
1812
- const reason = 'CANCELED_BY_SUPERSEDING_EVENT';
1813
- cancellationReason = reason;
1814
- await persistCancellationRequest(reason);
1815
- await execution.cancel(reason);
1816
- return reason;
1817
- }
1818
- const ineligible = activeRefresh.sourceExists === false ||
1819
- !policy.isEligible(refreshedProjection, deps.config);
1820
- if (!ineligible) {
1821
- continue;
1822
- }
1823
- const reason = cancellationReasonForIneligibleProjection(activeRefresh.sourceExists === false ? null : refreshedProjection);
1824
- cancellationReason = reason;
1825
- await persistCancellationRequest(reason);
1826
- await execution.cancel(reason);
1827
- return reason;
1828
- }
1829
- return null;
1830
- }
1831
- const runnerResult = await Promise.race([
1832
- execution.result.finally(() => {
1833
- executionFinished = true;
1834
- }),
1835
- superviseActiveSource().then(async (reason) => {
1836
- if (reason === null) {
1837
- return execution.result;
1838
- }
1839
- return execution.result;
1840
- }),
1841
- ]);
1842
- clearInterval(leaseRenewalTimer);
1843
- leaseRenewalTimer = undefined;
1844
- const parsedRunnerResult = parseRunnerResult(runnerResult.result);
1845
- const sentinel = parsedRunnerResult.status;
1846
- // The approval gate is pure policy, never the agent's word choice —
1847
- // a DONE on a stage configured with skipApproval: false is gated
1848
- // regardless of what the agent wrote (ADR 0002).
1849
- const skipApproval = runnerResult.metadata?.skipApproval;
1850
- const approvalGated = sentinel === 'DONE' && skipApproval === false;
1851
- // A canceled run must not advance the stage regardless of what the
1852
- // runner echoed back — the snapshot it acted on was superseded.
1853
- const nextStage = cancellationReason !== null
1854
- ? null
1855
- : approvalGated
1856
- ? null
1857
- : isLateralReadOnlyAction(action, deps.config) && sentinel === 'DONE'
1858
- ? null
1859
- : lifecycle.nextStageFromSentinel(claimedStage, sentinel, workflow);
1860
- const finishedAt = deps.clock.now().toISOString();
1861
- let workspaceBookkeeping;
1862
- if (workspacePath !== undefined) {
1863
- try {
1864
- workspaceBookkeeping = {
1865
- status: 'recorded',
1866
- result: await deps.workspaceManager.recordWorkspaceBookkeeping({ workspacePath }),
1867
- };
1868
- }
1869
- catch (error) {
1870
- workspaceBookkeeping = {
1871
- status: 'failed',
1872
- failureSource: 'wake-workspace-bookkeeping',
1873
- error: error instanceof Error ? error.message : String(error),
1874
- };
1875
- }
1876
- }
1877
- let prReviewTargetResourceUri = null;
1878
- if (watcherRun) {
1879
- // Artifact correlation is intentionally scoped to any watcher-dispatched
1880
- // run, not to an action literally named "pr-review".
1881
- prReviewTargetResourceUri = await resolvePrReviewTarget({
1882
- projection: candidate,
1883
- runId,
1884
- runnerResult,
1885
- occurredAt: finishedAt,
1886
- });
1887
- await appendAuditEvent({
1888
- eventId: `${runId}-audit-review-verdict`,
1889
- decisionType: 'review.verdict',
1890
- workItemKey: candidate.workItemKey,
1891
- runId,
1892
- workflowRevision: await computeWorkflowRevision({
1893
- config: deps.config,
1894
- workflowName,
1895
- workflow: deps.config.workflows[workflowName] ?? workflow,
1896
- action,
1897
- }),
1898
- inputsConsidered: {
1899
- sourceRevision,
1900
- watcherTrigger: watcherTriggerForRun,
1901
- verifiedTargetResourceUri: prReviewTargetResourceUri,
1902
- },
1903
- outcome: {
1904
- sentinel,
1905
- verdict: prReviewTargetResourceUri === null
1906
- ? 'uncertain'
1907
- : sentinel === 'DONE'
1908
- ? 'approved'
1909
- : sentinel === 'REJECTED'
1910
- ? 'changes-requested'
1911
- : 'uncertain',
1912
- reasoning: parsedRunnerResult.body,
1913
- runner: {
1914
- model: runnerResult.model,
1915
- cli: runnerResult.cli,
1916
- },
1917
- },
1918
- timestamp: finishedAt,
1919
- sourceRefs: {
1920
- repo: candidate.issue.repo,
1921
- issueNumber: candidate.issue.number,
1922
- },
1923
- });
1924
- }
1925
- else if (workspaceMode === 'branch') {
1926
- await registerReportedArtifacts({
1927
- projection: candidate,
1928
- runId,
1929
- runnerResult,
1930
- branch: branchNameForIssue(candidate.issue.number),
1931
- occurredAt: finishedAt,
1932
- });
1933
- }
1934
- const failedRunnerName = runnerResult.routing?.runnerName ?? routing.runnerName;
1935
- const existingRunners = ledgerAtStart?.runners ?? {};
1936
- if (runnerResult.failureClass === 'quota') {
1937
- const failureCount = (existingRunners[failedRunnerName]?.failureCount ?? 0) + 1;
1938
- const quotaPause = resolveQuotaPauseUntil({
1939
- result: runnerResult.result,
1940
- now: new Date(finishedAt),
1941
- failureCount,
1942
- });
1943
- await deps.stateStore.writeLedger({
1944
- schemaVersion: 1,
1945
- runners: {
1946
- ...existingRunners,
1947
- [failedRunnerName]: {
1948
- pausedUntil: quotaPause.pausedUntil,
1949
- pausedUntilSource: quotaPause.source,
1950
- failureCount,
1951
- lastFailureAt: finishedAt,
1952
- },
1953
- },
1954
- });
1955
- }
1956
- else if ((existingRunners[failedRunnerName]?.failureCount ?? 0) > 0) {
1957
- await deps.stateStore.writeLedger({
1958
- schemaVersion: 1,
1959
- runners: {
1960
- ...existingRunners,
1961
- [failedRunnerName]: { failureCount: 0 },
1962
- },
1963
- });
1964
- }
1965
- const resultMetadata = {
1966
- ...runnerResult.metadata,
1967
- envelope: parsedRunnerResult.envelope,
1968
- ...(cancellationReason === null ? {} : { cancellationReason }),
1969
- ...(runnerResult.failureClass === undefined
1970
- ? {}
1971
- : { failureClass: runnerResult.failureClass }),
1972
- ...(runnerResult.routing === undefined ? {} : { routing: runnerResult.routing }),
1973
- };
1974
- const executionOutcome = cancellationReason !== null
1975
- ? cancellationReason
1976
- : runnerResult.failureClass === 'quota'
1977
- ? 'QUOTA_EXHAUSTED'
1978
- : runnerResult.failureClass === 'infra'
1979
- ? 'PROCESS_FAILED'
1980
- : 'COMPLETED';
1981
- // Canceled runs don't produce a meaningful workflow outcome; the input
1982
- // they acted on was superseded so the sentinel is not authoritative.
1983
- const workflowOutcome = cancellationReason !== null
1984
- ? undefined
1985
- : sentinel === 'DONE'
1986
- ? approvalGated
1987
- ? 'AWAITING_APPROVAL'
1988
- : 'DONE'
1989
- : sentinel === 'REJECTED'
1990
- ? 'CHANGES_REQUESTED'
1991
- : sentinel === 'BLOCKED'
1992
- ? 'BLOCKED'
1993
- : undefined;
1994
- await transitionRunLifecycle('FINALISING');
1995
- const finalisingRecord = (await deps.stateStore.readRunRecord(runId));
1996
- const failureContext = sentinel === 'FAILED'
1997
- ? classifyFailedRun({
1998
- projection: candidate,
1999
- record: finalisingRecord,
2000
- failureClass: runnerResult.failureClass ?? 'task',
2001
- ...(workspacePath === undefined ? {} : { workspacePath }),
2002
- envelope: parsedRunnerResult.envelope,
2003
- sentinel,
2004
- })
2005
- : undefined;
2006
- await deps.stateStore.writeRunRecord({
2007
- ...finalisingRecord,
2008
- lifecycle: 'TERMINAL',
2009
- status: sentinel === 'DONE'
2010
- ? approvalGated
2011
- ? 'awaiting-approval'
2012
- : 'completed'
2013
- : sentinel === 'REJECTED'
2014
- ? 'rejected'
2015
- : sentinel === 'BLOCKED'
2016
- ? 'blocked'
2017
- : 'failed',
2018
- finishedAt,
2019
- sessionId: runnerResult.session_id,
2020
- sentinel,
2021
- executionOutcome,
2022
- ...(workflowOutcome !== undefined ? { workflowOutcome } : {}),
2023
- ...(failureContext === undefined ? {} : failureContext),
2024
- summary: parsedRunnerResult.body,
2025
- ...(runnerResult.routing === undefined ? {} : { routing: runnerResult.routing }),
2026
- ...(runnerResult.tokenUsage === undefined ? {} : { tokenUsage: runnerResult.tokenUsage }),
2027
- metadata: {
2028
- ...finalisingRecord.metadata,
2029
- ...(workspaceBookkeeping === undefined ? {} : { workspaceBookkeeping }),
2030
- ...resultMetadata,
2031
- },
2032
- });
2033
- // Computed before the payload so a rejecting review verdict (either
2034
- // the PR-artifact-verified path or a plain onSuccess.approve watcher
2035
- // with no PR) can fold context.status = 'changes-requested' on the
2036
- // parent via the same RUN_COMPLETED event, instead of requiring a
2037
- // separate marker comment round-trip (§5).
2038
- const watcherSuccessPolicy = watcherRun && watcherDispatch !== null
2039
- ? deps.config.workflows[watcherDispatch.parentWorkflowName]?.stages[watcherDispatch.parentStage]?.watch?.[watcherDispatch.watcherIndex]?.onSuccess
2040
- : undefined;
2041
- const isReviewRejection = watcherRun &&
2042
- sentinel === 'REJECTED' &&
2043
- (prReviewTargetResourceUri !== null || watcherSuccessPolicy?.approve === true);
2044
- const runCompletedEvent = createEventEnvelope({
2045
- eventId: `${runId}-completed`,
2046
- workItemKey: candidate.workItemKey,
2047
- streamScope: 'work-item',
2048
- direction: 'internal',
2049
- sourceSystem: 'wake',
2050
- sourceEventType: RUN_COMPLETED_EVENT,
2051
- sourceRefs: {
2052
- repo: candidate.issue.repo,
2053
- issueNumber: candidate.issue.number,
2054
- runId,
2055
- },
2056
- occurredAt: finishedAt,
2057
- ingestedAt: finishedAt,
2058
- trigger: 'immediate',
2059
- payload: {
2060
- action,
2061
- sentinel,
2062
- approvalGated,
2063
- allowAutoApproval: runnerResult.metadata?.allowAutoApproval === true,
2064
- ...(nextStage !== null ? { nextStage } : {}),
2065
- runId,
2066
- sessionId: runnerResult.session_id,
2067
- sessionCli: runnerResult.cli,
2068
- workspacePath,
2069
- reason: cancellationReason === null
2070
- ? `runner:${sentinel.toLowerCase()}`
2071
- : `runner:${String(cancellationReason).toLowerCase()}`,
2072
- ...(runnerResult.routing === undefined ? {} : { routing: runnerResult.routing }),
2073
- ...(runnerResult.failureClass === undefined
2074
- ? {}
2075
- : { failureClass: runnerResult.failureClass }),
2076
- ...(failureContext === undefined ? {} : failureContext),
2077
- // Only mark the triggering comment handled when the run reached the
2078
- // agent and produced a real outcome. Quota/infra failures are transient
2079
- // blips; canceled runs acted on a superseded snapshot — in both cases
2080
- // leave handledCommentId unset so the next tick can retry (S9).
2081
- ...(runnerResult.failureClass === 'quota' ||
2082
- runnerResult.failureClass === 'infra' ||
2083
- cancellationReason !== null
2084
- ? {}
2085
- : { handledCommentId: latestActionableCommentId(inputSnapshot.projection) }),
2086
- body: parsedRunnerResult.body,
2087
- envelope: parsedRunnerResult.envelope,
2088
- executionOutcome,
2089
- ...(workflowOutcome !== undefined ? { workflowOutcome } : {}),
2090
- ...(watcherRun ? { watcherRun: true, watcherTrigger: watcherTriggerForRun } : {}),
2091
- ...(isReviewRejection
2092
- ? { changesRequested: true, reviewFeedbackBody: parsedRunnerResult.body }
2093
- : {}),
2094
- },
2095
- });
2096
- await deps.stateStore.appendEventEnvelope(runCompletedEvent);
2097
- await projectionUpdater.rebuildFromEvents([runCompletedEvent]);
2098
- await updateWatcherStateAfterCompletion({
2099
- key: watcherStateKeyForRun,
2100
- trigger: watcherTriggerForRun,
2101
- sentinel,
2102
- retrySafety: failureContext?.retrySafety,
2103
- });
2104
- if (!watcherRun) {
2105
- await deliverOutboundEvent(createLabelsEvent({
2106
- projection: candidate,
2107
- runId,
2108
- ...(await currentLabelsForWorkItem(candidate.workItemKey, candidate)),
2109
- occurredAt: finishedAt,
2110
- }));
2111
- }
2112
- const publishIntent = createPublishIntentEvent({
2113
- projection: candidate,
2114
- runId,
2115
- action,
2116
- runnerResult,
2117
- parsedRunnerResult,
2118
- sentinel,
2119
- approvalGated,
2120
- occurredAt: finishedAt,
2121
- startedAt: nowIso,
2122
- ...(workspacePath === undefined ? {} : { workspacePath }),
2123
- ...(typeof candidate.context.lastFailureClass === 'string'
2124
- ? { previousFailureClass: candidate.context.lastFailureClass }
2125
- : {}),
2126
- });
2127
- if (watcherRun) {
2128
- // Watcher-dispatched runs own PR verdict delivery and correlation
2129
- // registration regardless of the target workflow/action name.
2130
- const pendingApprovalAction = candidate.context.pendingApprovalAction;
2131
- if (prReviewTargetResourceUri !== null &&
2132
- (sentinel === 'DONE' || sentinel === 'REJECTED')) {
2133
- await deliverOutboundEvent({
2134
- ...publishIntent,
2135
- sourceRefs: {
2136
- ...publishIntent.sourceRefs,
2137
- resourceUri: prReviewTargetResourceUri,
2138
- },
2139
- payload: {
2140
- ...publishIntent.payload,
2141
- kind: 'status-update',
2142
- body: sentinel === 'DONE'
2143
- ? `${parsedRunnerResult.body}\n\n${prReviewApprovalMarker}`
2144
- : `${parsedRunnerResult.body}\n\n<!-- wake:pr-review-changes-requested -->`,
2145
- idempotencyKey: `${runId}:pr-review-verdict-comment`,
2146
- },
2147
- });
2148
- const approvalId = `${runId}-parent-approval`;
2149
- const parentWorkflow = watcherDispatch === null
2150
- ? undefined
2151
- : deps.config.workflows[watcherDispatch.parentWorkflowName];
2152
- if (sentinel === 'DONE' &&
2153
- watcherDispatch !== null &&
2154
- isAwaitingApproval(candidate) &&
2155
- typeof pendingApprovalAction === 'string' &&
2156
- parentWorkflow !== undefined &&
2157
- (await deps.stateStore.readEventEnvelope(`${approvalId}-completed`)) === null) {
2158
- const approvedAt = eventStampNow();
2159
- const approvalResolution = {
2160
- approved: true,
2161
- pendingAction: pendingApprovalAction,
2162
- targetResourceUri: prReviewTargetResourceUri,
2163
- triggeringCommentId: approvalId,
2164
- triggeringCommentBody: parsedRunnerResult.body,
2165
- };
2166
- const mergePolicyBlock = await applyApprovedPrMergePolicy({
2167
- projection: candidate,
2168
- approvalResolution,
2169
- workflowName: watcherDispatch.parentWorkflowName,
2170
- mergePolicy: approvedMergePolicyForStage(parentWorkflow.stages[watcherDispatch.parentStage]),
2171
- });
2172
- if (mergePolicyBlock !== null) {
2173
- return mergePolicyBlock;
2174
- }
2175
- const applied = await applyApprovalTransition({
2176
- projection: candidate,
2177
- pendingAction: pendingApprovalAction,
2178
- approvalId,
2179
- approvedAt,
2180
- reason: 'watcher:approved',
2181
- workflow: parentWorkflow,
2182
- workflowName: watcherDispatch.parentWorkflowName,
2183
- });
2184
- if (applied !== null) {
2185
- await appendAuditEvent({
2186
- eventId: `${approvalId}-audit-watcher-approval`,
2187
- decisionType: 'approval.watcher-resolved',
2188
- workItemKey: candidate.workItemKey,
2189
- runId,
2190
- workflowRevision: await computeWorkflowRevision({
2191
- config: deps.config,
2192
- workflowName: watcherDispatch.parentWorkflowName,
2193
- workflow: parentWorkflow,
2194
- action: pendingApprovalAction,
2195
- }),
2196
- inputsConsidered: {
2197
- watcherWorkflow: watcherDispatch.targetWorkflowName,
2198
- pendingAction: pendingApprovalAction,
2199
- childRunId: runId,
2200
- childSentinel: sentinel,
2201
- verifiedTargetResourceUri: prReviewTargetResourceUri,
2202
- },
2203
- outcome: { approved: true, nextStage: applied.nextStage },
2204
- timestamp: approvedAt,
2205
- sourceRefs: {
2206
- repo: candidate.issue.repo,
2207
- issueNumber: candidate.issue.number,
2208
- resourceUri: prReviewTargetResourceUri,
2209
- },
2210
- });
2211
- }
2212
- }
2213
- }
2214
- else if (watcherDispatch !== null &&
2215
- watcherSuccessPolicy?.approve === true &&
2216
- (sentinel === 'DONE' ||
2217
- sentinel === 'REJECTED' ||
2218
- sentinel === 'FAILED' ||
2219
- sentinel === 'BLOCKED')) {
2220
- // No PR surface to carry the verdict comment: the child's sentinel
2221
- // is its verdict. Publish the review body for every verdict so the
2222
- // human sees why; only an ungated DONE resolves the parent's
2223
- // pending gate (checked below via isAwaitingApproval).
2224
- await deliverOutboundEvent(publishIntent);
2225
- const approvalId = `${runId}-parent-approval`;
2226
- const parentWorkflow = deps.config.workflows[watcherDispatch.parentWorkflowName];
2227
- if (sentinel === 'DONE' &&
2228
- isAwaitingApproval(candidate) &&
2229
- typeof pendingApprovalAction === 'string' &&
2230
- parentWorkflow !== undefined &&
2231
- (await deps.stateStore.readEventEnvelope(`${approvalId}-completed`)) === null) {
2232
- const approvedAt = eventStampNow();
2233
- const applied = await applyApprovalTransition({
2234
- projection: candidate,
2235
- pendingAction: pendingApprovalAction,
2236
- approvalId,
2237
- approvedAt,
2238
- reason: 'watcher:approved',
2239
- workflow: parentWorkflow,
2240
- workflowName: watcherDispatch.parentWorkflowName,
2241
- });
2242
- if (applied !== null) {
2243
- await appendAuditEvent({
2244
- eventId: `${approvalId}-audit-watcher-approval`,
2245
- decisionType: 'approval.watcher-resolved',
2246
- workItemKey: candidate.workItemKey,
2247
- runId,
2248
- workflowRevision: await computeWorkflowRevision({
2249
- config: deps.config,
2250
- workflowName: watcherDispatch.parentWorkflowName,
2251
- workflow: parentWorkflow,
2252
- action: pendingApprovalAction,
2253
- }),
2254
- inputsConsidered: {
2255
- watcherWorkflow: watcherDispatch.targetWorkflowName,
2256
- pendingAction: pendingApprovalAction,
2257
- childRunId: runId,
2258
- childSentinel: sentinel,
2259
- },
2260
- outcome: { approved: true, nextStage: applied.nextStage },
2261
- timestamp: approvedAt,
2262
- sourceRefs: {
2263
- repo: candidate.issue.repo,
2264
- issueNumber: candidate.issue.number,
2265
- },
2266
- });
2267
- }
2268
- }
2269
- }
2270
- else {
2271
- await suppressOutboundEvent(publishIntent, {
2272
- suppressedPublishReason: 'pr-review-no-actionable-verdict',
2273
- });
2274
- }
2275
- }
2276
- else if (shouldPublishRunResult({
2277
- failureClass: runnerResult.failureClass,
2278
- previousFailureClass: candidate.context.lastFailureClass,
2279
- })) {
2280
- await deliverOutboundEvent(publishIntent);
2281
- }
2282
- else {
2283
- await suppressOutboundEvent(publishIntent, {
2284
- suppressedPublishReason: runnerResult.failureClass === 'quota' ? 'quota-failure' : 'repeated-infra-failure',
2285
- });
2286
- }
2287
- return {
2288
- status: 'processed',
2289
- runId,
2290
- sentinel,
2291
- nextStage,
2292
- };
2293
- }
2294
- catch (err) {
2295
- clearInterval(leaseRenewalTimer);
2296
- const finishedAt = deps.clock.now().toISOString();
2297
- const sentinel = 'FAILED';
2298
- const failedRecord = (await deps.stateStore.readRunRecord(runId)) ?? runningRecord;
2299
- const failedRecordMetadata = failedRecord.metadata;
2300
- const failedRecordWorkspacePath = typeof failedRecordMetadata?.workspacePath === 'string'
2301
- ? failedRecordMetadata.workspacePath
2302
- : undefined;
2303
- const failureContext = classifyFailedRun({
2304
- projection: candidate,
2305
- record: failedRecord,
2306
- failureClass: 'infra',
2307
- ...(isWorkspaceValidationFailure(err)
2308
- ? { failurePhase: 'workspace-validation' }
2309
- : {}),
2310
- ...(failedRecordWorkspacePath === undefined
2311
- ? {}
2312
- : { workspacePath: failedRecordWorkspacePath }),
2313
- });
2314
- await deps.stateStore.writeRunRecord({
2315
- ...failedRecord,
2316
- lifecycle: 'TERMINAL',
2317
- status: 'failed',
2318
- finishedAt,
2319
- sentinel,
2320
- executionOutcome: 'PROCESS_FAILED',
2321
- ...failureContext,
2322
- summary: err instanceof Error ? err.message : String(err),
2323
- metadata: {
2324
- ...failedRecord.metadata,
2325
- failureClass: 'infra',
2326
- ...(isWorkspaceValidationFailure(err)
2327
- ? { failureSource: 'wake-workspace-validation' }
2328
- : {}),
2329
- ...failureContext,
2330
- },
2331
- });
2332
- const runCompletedEvent = createEventEnvelope({
2333
- eventId: `${runId}-completed`,
2334
- workItemKey: candidate.workItemKey,
2335
- streamScope: 'work-item',
2336
- direction: 'internal',
2337
- sourceSystem: 'wake',
2338
- sourceEventType: RUN_COMPLETED_EVENT,
2339
- sourceRefs: {
2340
- repo: candidate.issue.repo,
2341
- issueNumber: candidate.issue.number,
2342
- runId,
2343
- },
2344
- occurredAt: finishedAt,
2345
- ingestedAt: finishedAt,
2346
- trigger: 'immediate',
2347
- payload: {
2348
- action,
2349
- sentinel,
2350
- runId,
2351
- reason: 'runner:infrastructure-error',
2352
- failureClass: 'infra',
2353
- ...failureContext,
2354
- executionOutcome: 'PROCESS_FAILED',
2355
- // Deliberately omit handledCommentId: an infra blip (CLI crash, timeout,
2356
- // network error) never reached the agent, so it isn't an answer to the
2357
- // triggering comment. Leaving it unset lets the next tick retry the same
2358
- // request instead of silently eating it (S9).
2359
- // Mirrors the happy-path completion payload below: without this flag,
2360
- // projection-updater.ts folds sentinel/workflowOutcome onto the
2361
- // *parent's* context, even though this failure belongs to a watcher's
2362
- // own child-workflow run, not the parent's own action.
2363
- ...(watcherRun ? { watcherRun: true, watcherTrigger: watcherTriggerForRun } : {}),
2364
- },
2365
- });
2366
- await deps.stateStore.appendEventEnvelope(runCompletedEvent);
2367
- await projectionUpdater.rebuildFromEvents([runCompletedEvent]);
2368
- await updateWatcherStateAfterCompletion({
2369
- key: watcherStateKeyForRun,
2370
- trigger: watcherTriggerForRun,
2371
- sentinel,
2372
- retrySafety: failureContext.retrySafety,
2373
- });
2374
- // See the matching guard on the claim path above: a watcher run's
2375
- // failure is not the parent's own action failing, so it must not
2376
- // stamp the child workflow's stage/workflow onto the parent's labels.
2377
- if (!watcherRun) {
2378
- await deliverOutboundEvent(createLabelsEvent({
2379
- projection: candidate,
2380
- runId,
2381
- ...(await currentLabelsForWorkItem(candidate.workItemKey, candidate)),
2382
- occurredAt: finishedAt,
2383
- }));
2384
- }
2385
- const errorMessage = err instanceof Error ? err.message : String(err);
2386
- const infraFailureResult = {
2387
- result: errorMessage,
2388
- model: 'unknown',
2389
- cli: 'unknown',
2390
- failureClass: 'infra',
2391
- };
2392
- const parsedInfraFailureResult = parseRunnerResult(infraFailureResult.result);
2393
- const failurePublishIntent = createPublishIntentEvent({
2394
- projection: candidate,
2395
- runId,
2396
- action,
2397
- runnerResult: infraFailureResult,
2398
- parsedRunnerResult: parsedInfraFailureResult,
2399
- sentinel,
2400
- occurredAt: finishedAt,
2401
- startedAt: nowIso,
2402
- ...(typeof candidate.context.lastFailureClass === 'string'
2403
- ? { previousFailureClass: candidate.context.lastFailureClass }
2404
- : {}),
2405
- });
2406
- if (shouldPublishRunResult({
2407
- failureClass: 'infra',
2408
- previousFailureClass: candidate.context.lastFailureClass,
2409
- })) {
2410
- await deliverOutboundEvent(failurePublishIntent);
2411
- }
2412
- else {
2413
- await suppressOutboundEvent(failurePublishIntent, {
2414
- suppressedPublishReason: 'repeated-infra-failure',
2415
- });
2416
- }
2417
- return {
2418
- status: 'processed',
2419
- runId,
2420
- sentinel,
2421
- nextStage: null,
2422
- };
2423
- }
2424
- }
2425
- finally {
2426
- await lock.release();
2427
- }
2428
- }
2429
- return {
2430
- runIntakeTick,
2431
- runRunnerTick,
2432
- async runTick() {
2433
- await deps.stateStore.assertStateHealthy();
2434
- const intakeResult = await runIntakeTick();
2435
- if (intakeResult.status === 'locked') {
2436
- return intakeResult;
2437
- }
2438
- const runnerResult = await runRunnerTick();
2439
- return runnerResult;
2440
- },
2441
- };
2442
- }