@penclipai/server 2026.426.0 → 2026.505.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 (253) hide show
  1. package/dist/adapters/builtin-adapter-types.d.ts.map +1 -1
  2. package/dist/adapters/builtin-adapter-types.js +3 -0
  3. package/dist/adapters/builtin-adapter-types.js.map +1 -1
  4. package/dist/adapters/index.d.ts +2 -2
  5. package/dist/adapters/index.d.ts.map +1 -1
  6. package/dist/adapters/index.js +1 -1
  7. package/dist/adapters/index.js.map +1 -1
  8. package/dist/adapters/registry.d.ts +2 -1
  9. package/dist/adapters/registry.d.ts.map +1 -1
  10. package/dist/adapters/registry.js +76 -6
  11. package/dist/adapters/registry.js.map +1 -1
  12. package/dist/adapters/types.d.ts +1 -1
  13. package/dist/adapters/types.d.ts.map +1 -1
  14. package/dist/adapters/utils.d.ts.map +1 -1
  15. package/dist/adapters/utils.js +2 -1
  16. package/dist/adapters/utils.js.map +1 -1
  17. package/dist/attachment-types.d.ts +1 -16
  18. package/dist/attachment-types.d.ts.map +1 -1
  19. package/dist/attachment-types.js +7 -0
  20. package/dist/attachment-types.js.map +1 -1
  21. package/dist/auth/better-auth.d.ts +3 -1
  22. package/dist/auth/better-auth.d.ts.map +1 -1
  23. package/dist/auth/better-auth.js +8 -2
  24. package/dist/auth/better-auth.js.map +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +27 -13
  27. package/dist/index.js.map +1 -1
  28. package/dist/middleware/auth.d.ts.map +1 -1
  29. package/dist/middleware/auth.js +143 -2
  30. package/dist/middleware/auth.js.map +1 -1
  31. package/dist/onboarding-assets/ceo/AGENTS.md +1 -1
  32. package/dist/onboarding-assets/ceo/HEARTBEAT.md +5 -5
  33. package/dist/redaction.d.ts.map +1 -1
  34. package/dist/redaction.js +30 -12
  35. package/dist/redaction.js.map +1 -1
  36. package/dist/routes/access.d.ts.map +1 -1
  37. package/dist/routes/access.js +10 -0
  38. package/dist/routes/access.js.map +1 -1
  39. package/dist/routes/activity.d.ts.map +1 -1
  40. package/dist/routes/activity.js +4 -2
  41. package/dist/routes/activity.js.map +1 -1
  42. package/dist/routes/adapters.d.ts.map +1 -1
  43. package/dist/routes/adapters.js +1 -0
  44. package/dist/routes/adapters.js.map +1 -1
  45. package/dist/routes/agents.d.ts.map +1 -1
  46. package/dist/routes/agents.js +317 -56
  47. package/dist/routes/agents.js.map +1 -1
  48. package/dist/routes/costs.d.ts.map +1 -1
  49. package/dist/routes/costs.js +21 -2
  50. package/dist/routes/costs.js.map +1 -1
  51. package/dist/routes/instance-settings.d.ts.map +1 -1
  52. package/dist/routes/instance-settings.js +37 -2
  53. package/dist/routes/instance-settings.js.map +1 -1
  54. package/dist/routes/issue-tree-control.d.ts.map +1 -1
  55. package/dist/routes/issue-tree-control.js +3 -1
  56. package/dist/routes/issue-tree-control.js.map +1 -1
  57. package/dist/routes/issues.d.ts.map +1 -1
  58. package/dist/routes/issues.js +257 -32
  59. package/dist/routes/issues.js.map +1 -1
  60. package/dist/routes/projects.d.ts.map +1 -1
  61. package/dist/routes/projects.js +10 -3
  62. package/dist/routes/projects.js.map +1 -1
  63. package/dist/routes/routines.d.ts.map +1 -1
  64. package/dist/routes/routines.js +6 -1
  65. package/dist/routes/routines.js.map +1 -1
  66. package/dist/routes/workspace-command-authz.d.ts +1 -1
  67. package/dist/routes/workspace-command-authz.d.ts.map +1 -1
  68. package/dist/routes/workspace-command-authz.js +2 -2
  69. package/dist/routes/workspace-command-authz.js.map +1 -1
  70. package/dist/runtime-api.d.ts +4 -0
  71. package/dist/runtime-api.d.ts.map +1 -1
  72. package/dist/runtime-api.js +38 -10
  73. package/dist/runtime-api.js.map +1 -1
  74. package/dist/services/companies.d.ts +6 -0
  75. package/dist/services/companies.d.ts.map +1 -1
  76. package/dist/services/companies.js +1 -0
  77. package/dist/services/companies.js.map +1 -1
  78. package/dist/services/company-portability.d.ts.map +1 -1
  79. package/dist/services/company-portability.js +16 -15
  80. package/dist/services/company-portability.js.map +1 -1
  81. package/dist/services/costs.d.ts +9 -0
  82. package/dist/services/costs.d.ts.map +1 -1
  83. package/dist/services/costs.js +45 -1
  84. package/dist/services/costs.js.map +1 -1
  85. package/dist/services/environment-execution-target.d.ts.map +1 -1
  86. package/dist/services/environment-execution-target.js +7 -13
  87. package/dist/services/environment-execution-target.js.map +1 -1
  88. package/dist/services/environment-run-orchestrator.d.ts.map +1 -1
  89. package/dist/services/environment-run-orchestrator.js +56 -0
  90. package/dist/services/environment-run-orchestrator.js.map +1 -1
  91. package/dist/services/environment-runtime.d.ts +2 -0
  92. package/dist/services/environment-runtime.d.ts.map +1 -1
  93. package/dist/services/environment-runtime.js +80 -39
  94. package/dist/services/environment-runtime.js.map +1 -1
  95. package/dist/services/heartbeat-stop-metadata.d.ts +2 -1
  96. package/dist/services/heartbeat-stop-metadata.d.ts.map +1 -1
  97. package/dist/services/heartbeat-stop-metadata.js +10 -1
  98. package/dist/services/heartbeat-stop-metadata.js.map +1 -1
  99. package/dist/services/heartbeat-stop-metadata.test.js +24 -0
  100. package/dist/services/heartbeat-stop-metadata.test.js.map +1 -1
  101. package/dist/services/heartbeat.d.ts +156 -5
  102. package/dist/services/heartbeat.d.ts.map +1 -1
  103. package/dist/services/heartbeat.js +1384 -112
  104. package/dist/services/heartbeat.js.map +1 -1
  105. package/dist/services/index.d.ts +1 -0
  106. package/dist/services/index.d.ts.map +1 -1
  107. package/dist/services/index.js +1 -0
  108. package/dist/services/index.js.map +1 -1
  109. package/dist/services/instance-settings.d.ts.map +1 -1
  110. package/dist/services/instance-settings.js +4 -1
  111. package/dist/services/instance-settings.js.map +1 -1
  112. package/dist/services/issue-execution-policy.d.ts +56 -1
  113. package/dist/services/issue-execution-policy.d.ts.map +1 -1
  114. package/dist/services/issue-execution-policy.js +400 -2
  115. package/dist/services/issue-execution-policy.js.map +1 -1
  116. package/dist/services/issue-thread-interactions.d.ts +5 -1
  117. package/dist/services/issue-thread-interactions.d.ts.map +1 -1
  118. package/dist/services/issue-thread-interactions.js +44 -1
  119. package/dist/services/issue-thread-interactions.js.map +1 -1
  120. package/dist/services/issue-tree-control.d.ts +1 -0
  121. package/dist/services/issue-tree-control.d.ts.map +1 -1
  122. package/dist/services/issue-tree-control.js +84 -4
  123. package/dist/services/issue-tree-control.js.map +1 -1
  124. package/dist/services/issues.d.ts +10 -1
  125. package/dist/services/issues.d.ts.map +1 -1
  126. package/dist/services/issues.js +452 -48
  127. package/dist/services/issues.js.map +1 -1
  128. package/dist/services/plugin-environment-driver.d.ts +4 -0
  129. package/dist/services/plugin-environment-driver.d.ts.map +1 -1
  130. package/dist/services/plugin-environment-driver.js +18 -1
  131. package/dist/services/plugin-environment-driver.js.map +1 -1
  132. package/dist/services/productivity-review.d.ts +83 -0
  133. package/dist/services/productivity-review.d.ts.map +1 -0
  134. package/dist/services/productivity-review.js +650 -0
  135. package/dist/services/productivity-review.js.map +1 -0
  136. package/dist/services/recovery/index.d.ts +1 -1
  137. package/dist/services/recovery/index.d.ts.map +1 -1
  138. package/dist/services/recovery/index.js +1 -1
  139. package/dist/services/recovery/index.js.map +1 -1
  140. package/dist/services/recovery/issue-graph-liveness.d.ts +13 -1
  141. package/dist/services/recovery/issue-graph-liveness.d.ts.map +1 -1
  142. package/dist/services/recovery/issue-graph-liveness.js +212 -92
  143. package/dist/services/recovery/issue-graph-liveness.js.map +1 -1
  144. package/dist/services/recovery/origins.d.ts +2 -0
  145. package/dist/services/recovery/origins.d.ts.map +1 -1
  146. package/dist/services/recovery/origins.js +4 -0
  147. package/dist/services/recovery/origins.js.map +1 -1
  148. package/dist/services/recovery/run-liveness-continuations.d.ts.map +1 -1
  149. package/dist/services/recovery/run-liveness-continuations.js.map +1 -1
  150. package/dist/services/recovery/service.d.ts +20 -2
  151. package/dist/services/recovery/service.d.ts.map +1 -1
  152. package/dist/services/recovery/service.js +405 -63
  153. package/dist/services/recovery/service.js.map +1 -1
  154. package/dist/services/routines.d.ts +5 -2
  155. package/dist/services/routines.d.ts.map +1 -1
  156. package/dist/services/routines.js +47 -3
  157. package/dist/services/routines.js.map +1 -1
  158. package/dist/worktree-config.d.ts.map +1 -1
  159. package/dist/worktree-config.js +2 -5
  160. package/dist/worktree-config.js.map +1 -1
  161. package/package.json +16 -15
  162. package/skills/diagnose-why-work-stopped/SKILL.md +161 -0
  163. package/skills/paperclip/SKILL.md +37 -26
  164. package/skills/paperclip/references/api-reference.md +6 -2
  165. package/skills/paperclip-converting-plans-to-tasks/SKILL.md +42 -0
  166. package/skills/paperclip-create-agent/SKILL.md +3 -2
  167. package/skills/paperclip-create-agent/references/agent-instruction-templates.md +1 -1
  168. package/skills/paperclip-create-agent/references/api-reference.md +7 -2
  169. package/skills/paperclip-create-agent/references/baseline-role-guide.md +1 -1
  170. package/skills/paperclip-create-agent/references/draft-review-checklist.md +2 -2
  171. package/skills/paperclip-dev/SKILL.md +267 -0
  172. package/skills/terminal-bench-loop/SKILL.md +236 -0
  173. package/ui-dist/assets/{_basePickBy-BRqa7PJ5.js → _basePickBy-BS0Fg_DB.js} +1 -1
  174. package/ui-dist/assets/{_baseUniq-DhE2yrXC.js → _baseUniq-Dtnt_4SE.js} +1 -1
  175. package/ui-dist/assets/{arc-7qnikTQ3.js → arc-BCoOPxh5.js} +1 -1
  176. package/ui-dist/assets/{architectureDiagram-VXUJARFQ-CH0wVUOM.js → architectureDiagram-VXUJARFQ-C6eX2QUo.js} +1 -1
  177. package/ui-dist/assets/{blockDiagram-VD42YOAC-CeeRyJQX.js → blockDiagram-VD42YOAC-aUueUD4B.js} +1 -1
  178. package/ui-dist/assets/browser-ponyfill-BlAfsWm_.js +2 -0
  179. package/ui-dist/assets/{c4Diagram-YG6GDRKO-C_cV0CGo.js → c4Diagram-YG6GDRKO-CfPWRlOF.js} +1 -1
  180. package/ui-dist/assets/channel-ChNSCFJf.js +1 -0
  181. package/ui-dist/assets/{chunk-4BX2VUAB-DQ6pxPVT.js → chunk-4BX2VUAB-BTD1apA4.js} +1 -1
  182. package/ui-dist/assets/{chunk-55IACEB6-L8pS0IoX.js → chunk-55IACEB6-BXXF_ClN.js} +1 -1
  183. package/ui-dist/assets/{chunk-B4BG7PRW-BZKGE88E.js → chunk-B4BG7PRW-hAZeWGP8.js} +1 -1
  184. package/ui-dist/assets/{chunk-DI55MBZ5-CefSoZ_K.js → chunk-DI55MBZ5-cOH3UoEl.js} +1 -1
  185. package/ui-dist/assets/{chunk-FMBD7UC4-Bc3qTTHB.js → chunk-FMBD7UC4-Cu2yZOcl.js} +1 -1
  186. package/ui-dist/assets/{chunk-QN33PNHL-CjWBr5bI.js → chunk-QN33PNHL-0DNN5aRU.js} +1 -1
  187. package/ui-dist/assets/{chunk-QZHKN3VN-C0JUdmmz.js → chunk-QZHKN3VN-B9_bhK2n.js} +1 -1
  188. package/ui-dist/assets/{chunk-TZMSLE5B-D4d4I82z.js → chunk-TZMSLE5B-Cr5xwxio.js} +1 -1
  189. package/ui-dist/assets/classDiagram-2ON5EDUG-4aK1QZU3.js +1 -0
  190. package/ui-dist/assets/classDiagram-v2-WZHVMYZB-4aK1QZU3.js +1 -0
  191. package/ui-dist/assets/clone-C8lk5Qbc.js +1 -0
  192. package/ui-dist/assets/{cose-bilkent-S5V4N54A-B09h9XGZ.js → cose-bilkent-S5V4N54A-6_Dw6gpQ.js} +1 -1
  193. package/ui-dist/assets/{dagre-6UL2VRFP-CA02PXuX.js → dagre-6UL2VRFP-CFBhlh5H.js} +1 -1
  194. package/ui-dist/assets/{diagram-PSM6KHXK-DaT9cnrY.js → diagram-PSM6KHXK-C88ftcah.js} +1 -1
  195. package/ui-dist/assets/{diagram-QEK2KX5R-Drwc3gBw.js → diagram-QEK2KX5R-9EUupcuH.js} +1 -1
  196. package/ui-dist/assets/{diagram-S2PKOQOG-CpsGCaT6.js → diagram-S2PKOQOG-Dsml0wWh.js} +1 -1
  197. package/ui-dist/assets/{erDiagram-Q2GNP2WA-CVkBh9TY.js → erDiagram-Q2GNP2WA-sM-XdfHS.js} +1 -1
  198. package/ui-dist/assets/{flowDiagram-NV44I4VS-De9sXvPR.js → flowDiagram-NV44I4VS-qll7oaoW.js} +1 -1
  199. package/ui-dist/assets/{ganttDiagram-JELNMOA3-CSFa0gXS.js → ganttDiagram-JELNMOA3-VWnJMcjC.js} +1 -1
  200. package/ui-dist/assets/{gitGraphDiagram-V2S2FVAM-DEJaChxa.js → gitGraphDiagram-V2S2FVAM-DFnocrfl.js} +1 -1
  201. package/ui-dist/assets/{graph-D2R4DCtu.js → graph-nq3Qye4Z.js} +1 -1
  202. package/ui-dist/assets/{index-DEG-9CFs.js → index-3Owzaheh.js} +1 -1
  203. package/ui-dist/assets/{index-DHnKx9xX.js → index-B2A-a635.js} +1 -1
  204. package/ui-dist/assets/{index-C1I0SGDm.js → index-BGFrRiqa.js} +1 -1
  205. package/ui-dist/assets/{index-B44EtLRv.js → index-BVC5UhRK.js} +1 -1
  206. package/ui-dist/assets/{index-C_dAXwxT.js → index-BrP1U_Hy.js} +1 -1
  207. package/ui-dist/assets/{index-flZjKn_n.js → index-CXXHGqM8.js} +1 -1
  208. package/ui-dist/assets/{index-ssM_UKPW.js → index-CgyPAauR.js} +1 -1
  209. package/ui-dist/assets/{index-Ct1AraKR.js → index-CksQ4Ytv.js} +1 -1
  210. package/ui-dist/assets/{index-DQ6I_vpd.js → index-CrNzj2vZ.js} +1 -1
  211. package/ui-dist/assets/{index-DzZID5RY.js → index-CxbZBH3M.js} +1 -1
  212. package/ui-dist/assets/{index-Cn6_RRY5.js → index-D-dSSrf-.js} +1 -1
  213. package/ui-dist/assets/{index-CVa2OHgx.js → index-D6uZ_7Vh.js} +1 -1
  214. package/ui-dist/assets/{index-BzjWQd50.js → index-D7JGmxas.js} +1 -1
  215. package/ui-dist/assets/{index-CnT1_9UF.js → index-DDqO9GAq.js} +1 -1
  216. package/ui-dist/assets/index-DEUtmlPm.js +513 -0
  217. package/ui-dist/assets/{index-D2fEhyQg.js → index-DF5RDSoK.js} +1 -1
  218. package/ui-dist/assets/{index-CZGNe8K3.js → index-DfI92epU.js} +1 -1
  219. package/ui-dist/assets/{index-ByamXtyB.js → index-Dukb9MDQ.js} +1 -1
  220. package/ui-dist/assets/index-HP73_6Vr.css +1 -0
  221. package/ui-dist/assets/{index-BJS4rvUh.js → index-NXDTW2n4.js} +1 -1
  222. package/ui-dist/assets/{index-Bad5Hy7e.js → index-SxPPG9ig.js} +1 -1
  223. package/ui-dist/assets/{index-CC51mhhA.js → index-lC4Yz3Gw.js} +1 -1
  224. package/ui-dist/assets/{index-BFzkl36p.js → index-q2RXGI2V.js} +1 -1
  225. package/ui-dist/assets/{index-40icqWwg.js → index-qjfdrS96.js} +1 -1
  226. package/ui-dist/assets/{infoDiagram-HS3SLOUP-CJcjzWkM.js → infoDiagram-HS3SLOUP-CTrK5xoS.js} +1 -1
  227. package/ui-dist/assets/{journeyDiagram-XKPGCS4Q-ByITI00s.js → journeyDiagram-XKPGCS4Q-YFC7FykG.js} +1 -1
  228. package/ui-dist/assets/{kanban-definition-3W4ZIXB7-DvEjKke-.js → kanban-definition-3W4ZIXB7-B3dlyva0.js} +1 -1
  229. package/ui-dist/assets/{layout-CZcd66hi.js → layout-DefunPTK.js} +1 -1
  230. package/ui-dist/assets/{linear-jTUy3iHu.js → linear-CIPvzeMv.js} +1 -1
  231. package/ui-dist/assets/{mermaid.core-DECSZPbJ.js → mermaid.core-zKYhmnnR.js} +4 -4
  232. package/ui-dist/assets/{mindmap-definition-VGOIOE7T-Twtu17_c.js → mindmap-definition-VGOIOE7T-BlU-ebRa.js} +1 -1
  233. package/ui-dist/assets/{pieDiagram-ADFJNKIX-DlbgZ010.js → pieDiagram-ADFJNKIX-Ceto4LXH.js} +1 -1
  234. package/ui-dist/assets/{quadrantDiagram-AYHSOK5B-CMAa3qAT.js → quadrantDiagram-AYHSOK5B-C6M6hkuE.js} +1 -1
  235. package/ui-dist/assets/{requirementDiagram-UZGBJVZJ-CXRTfJOe.js → requirementDiagram-UZGBJVZJ-B-bcG938.js} +1 -1
  236. package/ui-dist/assets/{sankeyDiagram-TZEHDZUN-DeyO4fer.js → sankeyDiagram-TZEHDZUN-CIqty6Qi.js} +1 -1
  237. package/ui-dist/assets/{sequenceDiagram-WL72ISMW-Ch8wlJIL.js → sequenceDiagram-WL72ISMW-CIt2R5tk.js} +1 -1
  238. package/ui-dist/assets/{stateDiagram-FKZM4ZOC-BgL_AAl9.js → stateDiagram-FKZM4ZOC-BC1RFlfg.js} +1 -1
  239. package/ui-dist/assets/stateDiagram-v2-4FDKWEC3-Iy6tYSSw.js +1 -0
  240. package/ui-dist/assets/{timeline-definition-IT6M3QCI-D1QWd7TQ.js → timeline-definition-IT6M3QCI-DZqvoU94.js} +1 -1
  241. package/ui-dist/assets/{treemap-GDKQZRPO-B5RkmUv8.js → treemap-GDKQZRPO-CSeKauwA.js} +1 -1
  242. package/ui-dist/assets/{xychartDiagram-PRI3JC2R-WtDhjZfk.js → xychartDiagram-PRI3JC2R-Ut3mCiEd.js} +1 -1
  243. package/ui-dist/index.html +2 -2
  244. package/ui-dist/locales/en/common.json +137 -1
  245. package/ui-dist/locales/zh-CN/common.json +111 -1
  246. package/ui-dist/assets/browser-ponyfill-Ct3hGqsr.js +0 -2
  247. package/ui-dist/assets/channel-pHFjGZL-.js +0 -1
  248. package/ui-dist/assets/classDiagram-2ON5EDUG-X4ZksqXl.js +0 -1
  249. package/ui-dist/assets/classDiagram-v2-WZHVMYZB-X4ZksqXl.js +0 -1
  250. package/ui-dist/assets/clone-DZzimpfG.js +0 -1
  251. package/ui-dist/assets/index-C1oE3J7o.css +0 -1
  252. package/ui-dist/assets/index-fSIlEIHr.js +0 -510
  253. package/ui-dist/assets/stateDiagram-v2-4FDKWEC3-gnLzrhSv.js +0 -1
@@ -1,14 +1,18 @@
1
1
  import { Buffer } from "node:buffer";
2
- import { and, asc, desc, eq, inArray, isNull, ne, or, sql } from "drizzle-orm";
3
- import { activityLog, agentWakeupRequests, agents, assets, companies, companyMemberships, documents, goals, heartbeatRuns, executionWorkspaces, issueAttachments, issueInboxArchives, issueLabels, issueRelations, issueComments, issueDocuments, issueReadStates, issues, labels, projectWorkspaces, projects, } from "@penclipai/db";
4
- import { extractAgentMentionIds, extractProjectMentionIds, isUuidLike } from "@penclipai/shared";
2
+ import { and, asc, desc, eq, gt, inArray, isNull, lt, ne, notInArray, or, sql } from "drizzle-orm";
3
+ import { activityLog, agentWakeupRequests, agents, approvals, assets, companies, companyMemberships, documents, goals, heartbeatRuns, executionWorkspaces, issueApprovals, issueAttachments, issueInboxArchives, issueLabels, issueRelations, issueComments, issueDocuments, issueReadStates, issueThreadInteractions, issues, labels, projectWorkspaces, projects, } from "@penclipai/db";
4
+ import { clampIssueRequestDepth, extractAgentMentionIds, extractProjectMentionIds, isUuidLike, normalizeIssueIdentifier as normalizeIssueReferenceIdentifier, } from "@penclipai/shared";
5
5
  import { conflict, notFound, unprocessable } from "../errors.js";
6
+ import { parseObject } from "../adapters/utils.js";
6
7
  import { defaultIssueExecutionWorkspaceSettingsForProject, gateProjectExecutionWorkspacePolicy, issueExecutionWorkspaceModeForPersistedWorkspace, parseIssueExecutionWorkspaceSettings, parseProjectExecutionWorkspacePolicy, } from "./execution-workspace-policy.js";
8
+ import { mergeExecutionWorkspaceConfig } from "./execution-workspaces.js";
9
+ import { buildInitialIssueMonitorFields, normalizeIssueExecutionPolicy } from "./issue-execution-policy.js";
7
10
  import { instanceSettingsService } from "./instance-settings.js";
8
11
  import { redactCurrentUserText } from "../log-redaction.js";
9
12
  import { resolveIssueGoalId, resolveNextIssueGoalId } from "./issue-goal-fallback.js";
10
13
  import { getDefaultCompanyGoal } from "./goals.js";
11
14
  import { isVerifiedIssueTreeControlInteractionWake, issueTreeControlService, } from "./issue-tree-control.js";
15
+ import { parseIssueGraphLivenessIncidentKey } from "./recovery/origins.js";
12
16
  const ALL_ISSUE_STATUSES = ["backlog", "todo", "in_progress", "in_review", "blocked", "done", "cancelled"];
13
17
  const MAX_ISSUE_COMMENT_PAGE_LIMIT = 500;
14
18
  export const ISSUE_LIST_DEFAULT_LIMIT = 500;
@@ -44,6 +48,14 @@ function readStringFromRecord(record, key) {
44
48
  const value = record[key];
45
49
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
46
50
  }
51
+ function buildReusedExecutionWorkspaceConfigPatchFromIssueSettings(settings) {
52
+ return {
53
+ environmentId: settings?.environmentId ?? null,
54
+ provisionCommand: settings?.workspaceStrategy?.provisionCommand ?? null,
55
+ teardownCommand: settings?.workspaceStrategy?.teardownCommand ?? null,
56
+ workspaceRuntime: settings?.workspaceRuntime ?? null,
57
+ };
58
+ }
47
59
  function sameRunLock(checkoutRunId, actorRunId) {
48
60
  if (actorRunId)
49
61
  return checkoutRunId === actorRunId;
@@ -463,6 +475,21 @@ async function withIssueLabels(dbOrTx, rows) {
463
475
  const ACTIVE_RUN_STATUSES = ["queued", "running"];
464
476
  const BLOCKER_ATTENTION_ACTIVE_RUN_STATUSES = ["queued", "running"];
465
477
  const BLOCKER_ATTENTION_ACTIVE_WAKE_STATUSES = ["queued", "deferred_issue_execution"];
478
+ const BLOCKER_ATTENTION_PENDING_INTERACTION_STATUSES = ["pending"];
479
+ const BLOCKER_ATTENTION_PENDING_APPROVAL_STATUSES = ["pending", "revision_requested"];
480
+ const BLOCKER_ATTENTION_OPEN_RECOVERY_ORIGIN_KIND = "harness_liveness_escalation";
481
+ const PRODUCTIVITY_REVIEW_ORIGIN_KIND = "issue_productivity_review";
482
+ const PRODUCTIVITY_REVIEW_TERMINAL_STATUSES = ["done", "cancelled"];
483
+ const PRODUCTIVITY_REVIEW_ACTIVITY_ACTIONS = [
484
+ "issue.productivity_review_created",
485
+ "issue.productivity_review_updated",
486
+ ];
487
+ const PRODUCTIVITY_REVIEW_TRIGGERS = [
488
+ "no_comment_streak",
489
+ "long_active_duration",
490
+ "high_churn",
491
+ ];
492
+ const BLOCKER_ATTENTION_OPEN_RECOVERY_TERMINAL_STATUSES = ["done", "cancelled"];
466
493
  const BLOCKER_ATTENTION_MAX_DEPTH = 8;
467
494
  const BLOCKER_ATTENTION_MAX_NODES = 2000;
468
495
  const BLOCKER_ATTENTION_INVOKABLE_AGENT_STATUSES = new Set(["active", "idle", "running", "error"]);
@@ -499,8 +526,10 @@ function createIssueBlockerAttention(input = {}) {
499
526
  reason: input.reason ?? null,
500
527
  unresolvedBlockerCount: input.unresolvedBlockerCount ?? 0,
501
528
  coveredBlockerCount: input.coveredBlockerCount ?? 0,
529
+ stalledBlockerCount: input.stalledBlockerCount ?? 0,
502
530
  attentionBlockerCount: input.attentionBlockerCount ?? 0,
503
531
  sampleBlockerIdentifier: input.sampleBlockerIdentifier ?? null,
532
+ sampleStalledBlockerIdentifier: input.sampleStalledBlockerIdentifier ?? null,
504
533
  };
505
534
  }
506
535
  function blockerSampleIdentifier(node) {
@@ -594,6 +623,81 @@ async function terminalExplicitBlockersByRoot(companyId, roots, dbOrTx) {
594
623
  }
595
624
  return terminalByRoot;
596
625
  }
626
+ function readProductivityReviewTrigger(value) {
627
+ if (typeof value !== "string")
628
+ return null;
629
+ return PRODUCTIVITY_REVIEW_TRIGGERS.includes(value)
630
+ ? value
631
+ : null;
632
+ }
633
+ function readProductivityReviewStreak(value) {
634
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
635
+ return null;
636
+ return Math.floor(value);
637
+ }
638
+ async function listIssueProductivityReviewMap(dbOrTx, companyId, sourceIssueIds) {
639
+ const map = new Map();
640
+ if (sourceIssueIds.length === 0)
641
+ return map;
642
+ const reviewRows = [];
643
+ for (const chunk of chunkList([...new Set(sourceIssueIds)], ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
644
+ const rows = await dbOrTx
645
+ .select({
646
+ sourceIssueId: issues.originId,
647
+ reviewIssueId: issues.id,
648
+ reviewIdentifier: issues.identifier,
649
+ status: issues.status,
650
+ priority: issues.priority,
651
+ createdAt: issues.createdAt,
652
+ updatedAt: issues.updatedAt,
653
+ })
654
+ .from(issues)
655
+ .where(and(eq(issues.companyId, companyId), eq(issues.originKind, PRODUCTIVITY_REVIEW_ORIGIN_KIND), inArray(issues.originId, chunk), isNull(issues.hiddenAt), notInArray(issues.status, PRODUCTIVITY_REVIEW_TERMINAL_STATUSES)))
656
+ .orderBy(desc(issues.createdAt), desc(issues.id));
657
+ reviewRows.push(...rows);
658
+ }
659
+ if (reviewRows.length === 0)
660
+ return map;
661
+ const reviewIssueIds = reviewRows.map((row) => row.reviewIssueId);
662
+ const triggerByReviewIssueId = new Map();
663
+ for (const chunk of chunkList(reviewIssueIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
664
+ const detailRows = await dbOrTx
665
+ .select({
666
+ entityId: activityLog.entityId,
667
+ details: activityLog.details,
668
+ createdAt: activityLog.createdAt,
669
+ })
670
+ .from(activityLog)
671
+ .where(and(eq(activityLog.companyId, companyId), eq(activityLog.entityType, "issue"), inArray(activityLog.entityId, chunk), inArray(activityLog.action, PRODUCTIVITY_REVIEW_ACTIVITY_ACTIONS)))
672
+ .orderBy(desc(activityLog.createdAt));
673
+ for (const row of detailRows) {
674
+ if (triggerByReviewIssueId.has(row.entityId))
675
+ continue;
676
+ triggerByReviewIssueId.set(row.entityId, {
677
+ trigger: readProductivityReviewTrigger(row.details?.trigger),
678
+ noCommentStreak: readProductivityReviewStreak(row.details?.noCommentStreak),
679
+ });
680
+ }
681
+ }
682
+ for (const row of reviewRows) {
683
+ if (!row.sourceIssueId)
684
+ continue;
685
+ if (map.has(row.sourceIssueId))
686
+ continue;
687
+ const detail = triggerByReviewIssueId.get(row.reviewIssueId);
688
+ map.set(row.sourceIssueId, {
689
+ reviewIssueId: row.reviewIssueId,
690
+ reviewIdentifier: row.reviewIdentifier,
691
+ status: row.status,
692
+ priority: row.priority,
693
+ trigger: detail?.trigger ?? null,
694
+ noCommentStreak: detail?.noCommentStreak ?? null,
695
+ createdAt: row.createdAt,
696
+ updatedAt: row.updatedAt,
697
+ });
698
+ }
699
+ return map;
700
+ }
597
701
  async function listIssueBlockerAttentionMap(dbOrTx, companyId, issueRows) {
598
702
  const roots = issueRows.filter((row) => row.companyId === companyId && row.status === "blocked");
599
703
  const attentionMap = new Map();
@@ -719,6 +823,42 @@ async function listIssueBlockerAttentionMap(dbOrTx, companyId, issueRows) {
719
823
  activeIssueIds.add(row.issueId);
720
824
  }
721
825
  }
826
+ const explicitWaitCandidateIds = [...nodesById.values()]
827
+ .filter((node) => node.status !== "done")
828
+ .map((node) => node.id);
829
+ const explicitWaitingIssueIds = new Set();
830
+ if (explicitWaitCandidateIds.length > 0) {
831
+ for (const chunk of chunkList(explicitWaitCandidateIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
832
+ const interactionRows = await dbOrTx
833
+ .select({ issueId: issueThreadInteractions.issueId })
834
+ .from(issueThreadInteractions)
835
+ .where(and(eq(issueThreadInteractions.companyId, companyId), inArray(issueThreadInteractions.status, BLOCKER_ATTENTION_PENDING_INTERACTION_STATUSES), inArray(issueThreadInteractions.issueId, chunk)));
836
+ for (const row of interactionRows)
837
+ explicitWaitingIssueIds.add(row.issueId);
838
+ const approvalRows = await dbOrTx
839
+ .select({ issueId: issueApprovals.issueId })
840
+ .from(issueApprovals)
841
+ .innerJoin(approvals, eq(issueApprovals.approvalId, approvals.id))
842
+ .where(and(eq(issueApprovals.companyId, companyId), inArray(approvals.status, BLOCKER_ATTENTION_PENDING_APPROVAL_STATUSES), inArray(issueApprovals.issueId, chunk)));
843
+ for (const row of approvalRows)
844
+ explicitWaitingIssueIds.add(row.issueId);
845
+ }
846
+ // Recovery rows are intentionally company-wide: a liveness escalation for
847
+ // the same leaf blocker represents an active waiting path even when that
848
+ // blocker is reached through another blocked graph.
849
+ const recoveryRows = await dbOrTx
850
+ .select({ id: issues.id, originId: issues.originId })
851
+ .from(issues)
852
+ .where(and(eq(issues.companyId, companyId), eq(issues.originKind, BLOCKER_ATTENTION_OPEN_RECOVERY_ORIGIN_KIND), isNull(issues.hiddenAt), notInArray(issues.status, BLOCKER_ATTENTION_OPEN_RECOVERY_TERMINAL_STATUSES)));
853
+ for (const row of recoveryRows) {
854
+ const parsed = parseIssueGraphLivenessIncidentKey(row.originId);
855
+ if (!parsed || parsed.companyId !== companyId)
856
+ continue;
857
+ explicitWaitingIssueIds.add(row.id);
858
+ explicitWaitingIssueIds.add(parsed.issueId);
859
+ explicitWaitingIssueIds.add(parsed.leafIssueId);
860
+ }
861
+ }
722
862
  const agentRows = agentIds.size > 0
723
863
  ? await dbOrTx
724
864
  .select({
@@ -731,37 +871,73 @@ async function listIssueBlockerAttentionMap(dbOrTx, companyId, issueRows) {
731
871
  : [];
732
872
  const agentsById = new Map(agentRows.map((agent) => [agent.id, agent]));
733
873
  const classifyPath = (nodeId, seen) => {
734
- if (truncated || seen.has(nodeId))
735
- return { covered: false, sampleBlockerIdentifier: blockerSampleIdentifier(nodesById.get(nodeId)) };
874
+ const sample = blockerSampleIdentifier(nodesById.get(nodeId));
875
+ if (truncated || seen.has(nodeId)) {
876
+ return { covered: false, stalled: false, sampleBlockerIdentifier: sample, sampleStalledBlockerIdentifier: null };
877
+ }
736
878
  const node = nodesById.get(nodeId);
737
- if (!node || node.companyId !== companyId)
738
- return { covered: false, sampleBlockerIdentifier: nodeId };
739
- if (node.status === "done")
740
- return { covered: true, sampleBlockerIdentifier: blockerSampleIdentifier(node) };
741
- if (activeIssueIds.has(node.id))
742
- return { covered: true, sampleBlockerIdentifier: blockerSampleIdentifier(node) };
743
- if (node.status === "cancelled")
744
- return { covered: false, sampleBlockerIdentifier: blockerSampleIdentifier(node) };
879
+ if (!node || node.companyId !== companyId) {
880
+ return { covered: false, stalled: false, sampleBlockerIdentifier: nodeId, sampleStalledBlockerIdentifier: null };
881
+ }
882
+ const nodeSample = blockerSampleIdentifier(node);
883
+ if (node.status === "done") {
884
+ return { covered: true, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
885
+ }
886
+ if (explicitWaitingIssueIds.has(node.id)) {
887
+ return { covered: true, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
888
+ }
889
+ if (node.status === "in_review") {
890
+ const hasWaitingPath = activeIssueIds.has(node.id) || Boolean(node.assigneeUserId);
891
+ if (hasWaitingPath) {
892
+ return { covered: true, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
893
+ }
894
+ return { covered: false, stalled: true, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: nodeSample };
895
+ }
896
+ if (activeIssueIds.has(node.id)) {
897
+ return { covered: true, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
898
+ }
899
+ if (node.status === "cancelled") {
900
+ return { covered: false, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
901
+ }
745
902
  const downstream = (edgesByIssueId.get(node.id) ?? []).filter((edge) => nodesById.get(edge.blockerIssueId)?.status !== "done");
746
903
  if (downstream.length > 0) {
747
904
  const nextSeen = new Set(seen);
748
905
  nextSeen.add(nodeId);
749
906
  const classified = downstream.map((edge) => classifyPath(edge.blockerIssueId, nextSeen));
750
- const attention = classified.find((result) => !result.covered);
751
- if (attention)
752
- return attention;
907
+ const stalledChild = classified.find((result) => result.stalled || result.sampleStalledBlockerIdentifier);
908
+ const sampleStalled = stalledChild?.sampleStalledBlockerIdentifier ?? null;
909
+ const hardAttention = classified.find((result) => !result.covered && !result.stalled);
910
+ if (hardAttention) {
911
+ return {
912
+ covered: false,
913
+ stalled: false,
914
+ sampleBlockerIdentifier: hardAttention.sampleBlockerIdentifier,
915
+ sampleStalledBlockerIdentifier: sampleStalled,
916
+ };
917
+ }
918
+ const stalledEntry = classified.find((result) => result.stalled);
919
+ if (stalledEntry) {
920
+ return {
921
+ covered: false,
922
+ stalled: true,
923
+ sampleBlockerIdentifier: stalledEntry.sampleBlockerIdentifier,
924
+ sampleStalledBlockerIdentifier: sampleStalled,
925
+ };
926
+ }
753
927
  return {
754
928
  covered: true,
755
- sampleBlockerIdentifier: classified[0]?.sampleBlockerIdentifier ?? blockerSampleIdentifier(node),
929
+ stalled: false,
930
+ sampleBlockerIdentifier: classified[0]?.sampleBlockerIdentifier ?? nodeSample,
931
+ sampleStalledBlockerIdentifier: null,
756
932
  };
757
933
  }
758
934
  if (node.assigneeAgentId) {
759
935
  const assignee = agentsById.get(node.assigneeAgentId);
760
936
  if (!assignee || assignee.companyId !== companyId || !BLOCKER_ATTENTION_INVOKABLE_AGENT_STATUSES.has(assignee.status)) {
761
- return { covered: false, sampleBlockerIdentifier: blockerSampleIdentifier(node) };
937
+ return { covered: false, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
762
938
  }
763
939
  }
764
- return { covered: false, sampleBlockerIdentifier: blockerSampleIdentifier(node) };
940
+ return { covered: false, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
765
941
  };
766
942
  for (const root of roots) {
767
943
  const topLevelEdges = (edgesByIssueId.get(root.id) ?? []).filter((edge) => nodesById.get(edge.blockerIssueId)?.status !== "done");
@@ -777,21 +953,40 @@ async function listIssueBlockerAttentionMap(dbOrTx, companyId, issueRows) {
777
953
  result: classifyPath(edge.blockerIssueId, new Set([root.id])),
778
954
  }));
779
955
  const coveredBlockerCount = classified.filter((entry) => entry.result.covered).length;
780
- const attentionBlockerCount = classified.length - coveredBlockerCount;
781
- const attentionEntry = classified.find((entry) => !entry.result.covered);
782
- const sampleEntry = attentionEntry ?? classified[0] ?? null;
956
+ const stalledBlockerCount = classified.filter((entry) => entry.result.stalled).length;
957
+ const attentionBlockerCount = classified.length - coveredBlockerCount - stalledBlockerCount;
958
+ const hardAttentionEntry = classified.find((entry) => !entry.result.covered && !entry.result.stalled);
959
+ const stalledEntry = classified.find((entry) => entry.result.stalled);
960
+ const sampleEntry = hardAttentionEntry ?? stalledEntry ?? classified[0] ?? null;
783
961
  const sampleNode = sampleEntry ? nodesById.get(sampleEntry.edge.blockerIssueId) : null;
962
+ const sampleStalledFromChain = classified
963
+ .map((entry) => entry.result.sampleStalledBlockerIdentifier)
964
+ .find((value) => value);
965
+ let state;
966
+ let reason;
967
+ if (attentionBlockerCount > 0) {
968
+ state = "needs_attention";
969
+ reason = "attention_required";
970
+ }
971
+ else if (stalledBlockerCount > 0) {
972
+ state = "stalled";
973
+ reason = "stalled_review";
974
+ }
975
+ else {
976
+ state = "covered";
977
+ reason = topLevelEdges.every((edge) => nodesById.get(edge.blockerIssueId)?.parentId === root.id)
978
+ ? "active_child"
979
+ : "active_dependency";
980
+ }
784
981
  attentionMap.set(root.id, createIssueBlockerAttention({
785
- state: attentionBlockerCount === 0 ? "covered" : "needs_attention",
786
- reason: attentionBlockerCount === 0
787
- ? topLevelEdges.every((edge) => nodesById.get(edge.blockerIssueId)?.parentId === root.id)
788
- ? "active_child"
789
- : "active_dependency"
790
- : "attention_required",
982
+ state,
983
+ reason,
791
984
  unresolvedBlockerCount: topLevelEdges.length,
792
985
  coveredBlockerCount,
986
+ stalledBlockerCount,
793
987
  attentionBlockerCount,
794
988
  sampleBlockerIdentifier: sampleEntry?.result.sampleBlockerIdentifier ?? blockerSampleIdentifier(sampleNode),
989
+ sampleStalledBlockerIdentifier: stalledEntry?.result.sampleStalledBlockerIdentifier ?? sampleStalledFromChain ?? null,
795
990
  }));
796
991
  }
797
992
  return attentionMap;
@@ -837,6 +1032,12 @@ const issueListSelect = {
837
1032
  assigneeAdapterOverrides: issues.assigneeAdapterOverrides,
838
1033
  executionPolicy: sql `null`,
839
1034
  executionState: sql `null`,
1035
+ monitorNextCheckAt: issues.monitorNextCheckAt,
1036
+ monitorWakeRequestedAt: issues.monitorWakeRequestedAt,
1037
+ monitorLastTriggeredAt: issues.monitorLastTriggeredAt,
1038
+ monitorAttemptCount: issues.monitorAttemptCount,
1039
+ monitorNotes: issues.monitorNotes,
1040
+ monitorScheduledBy: issues.monitorScheduledBy,
840
1041
  executionWorkspaceId: issues.executionWorkspaceId,
841
1042
  executionWorkspacePreference: issues.executionWorkspacePreference,
842
1043
  executionWorkspaceSettings: sql `null`,
@@ -935,6 +1136,49 @@ async function lastActivityStatsForIssues(dbOrTx, companyId, issueIds) {
935
1136
  }
936
1137
  return [...byIssueId.values()];
937
1138
  }
1139
+ async function blockedByMapForIssues(dbOrTx, companyId, issueIds) {
1140
+ const map = new Map();
1141
+ const uniqueIssueIds = [...new Set(issueIds)];
1142
+ if (uniqueIssueIds.length === 0)
1143
+ return map;
1144
+ for (const issueId of uniqueIssueIds) {
1145
+ map.set(issueId, []);
1146
+ }
1147
+ for (const issueIdChunk of chunkList(uniqueIssueIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
1148
+ const rows = await dbOrTx
1149
+ .select({
1150
+ currentIssueId: issueRelations.relatedIssueId,
1151
+ relatedId: issues.id,
1152
+ identifier: issues.identifier,
1153
+ title: issues.title,
1154
+ status: issues.status,
1155
+ priority: issues.priority,
1156
+ assigneeAgentId: issues.assigneeAgentId,
1157
+ assigneeUserId: issues.assigneeUserId,
1158
+ })
1159
+ .from(issueRelations)
1160
+ .innerJoin(issues, eq(issueRelations.issueId, issues.id))
1161
+ .where(and(eq(issueRelations.companyId, companyId), eq(issueRelations.type, "blocks"), inArray(issueRelations.relatedIssueId, issueIdChunk)));
1162
+ for (const row of rows) {
1163
+ const blockedBy = map.get(row.currentIssueId);
1164
+ if (!blockedBy)
1165
+ continue;
1166
+ blockedBy.push({
1167
+ id: row.relatedId,
1168
+ identifier: row.identifier,
1169
+ title: row.title,
1170
+ status: row.status,
1171
+ priority: row.priority,
1172
+ assigneeAgentId: row.assigneeAgentId,
1173
+ assigneeUserId: row.assigneeUserId,
1174
+ });
1175
+ }
1176
+ }
1177
+ for (const blockedBy of map.values()) {
1178
+ blockedBy.sort((a, b) => a.title.localeCompare(b.title));
1179
+ }
1180
+ return map;
1181
+ }
938
1182
  export function issueService(db) {
939
1183
  const instanceSettings = instanceSettingsService(db);
940
1184
  const treeControlSvc = issueTreeControlService(db);
@@ -1298,10 +1542,14 @@ export function issueService(db) {
1298
1542
  const limit = typeof filters?.limit === "number" && Number.isFinite(filters.limit)
1299
1543
  ? Math.max(1, Math.floor(filters.limit))
1300
1544
  : undefined;
1545
+ const offset = typeof filters?.offset === "number" && Number.isFinite(filters.offset)
1546
+ ? Math.max(0, Math.floor(filters.offset))
1547
+ : 0;
1301
1548
  const touchedByUserId = filters?.touchedByUserId?.trim() || undefined;
1302
1549
  const inboxArchivedByUserId = filters?.inboxArchivedByUserId?.trim() || undefined;
1303
1550
  const unreadForUserId = filters?.unreadForUserId?.trim() || undefined;
1304
1551
  const contextUserId = unreadForUserId ?? touchedByUserId ?? inboxArchivedByUserId;
1552
+ const includeBlockedBy = filters?.includeBlockedBy === true;
1305
1553
  const rawSearch = filters?.q?.trim() ?? "";
1306
1554
  const hasSearch = rawSearch.length > 0;
1307
1555
  const escapedSearch = hasSearch ? escapeLikePattern(rawSearch) : "";
@@ -1408,8 +1656,11 @@ export function issueService(db) {
1408
1656
  .select(issueListSelect)
1409
1657
  .from(issues)
1410
1658
  .where(and(...conditions))
1411
- .orderBy(hasSearch ? asc(searchOrder) : asc(priorityOrder), asc(priorityOrder), desc(canonicalLastActivityAt), desc(issues.updatedAt));
1412
- const rows = (limit === undefined ? await baseQuery : await baseQuery.limit(limit)).map((row) => ({
1659
+ .orderBy(hasSearch ? asc(searchOrder) : asc(priorityOrder), asc(priorityOrder), desc(canonicalLastActivityAt), desc(issues.updatedAt), desc(issues.id));
1660
+ const pageQuery = offset > 0
1661
+ ? (limit === undefined ? baseQuery.offset(offset) : baseQuery.limit(limit).offset(offset))
1662
+ : (limit === undefined ? baseQuery : baseQuery.limit(limit));
1663
+ const rows = (await pageQuery).map((row) => ({
1413
1664
  ...row,
1414
1665
  description: decodeDatabaseTextPreview(row.description, ISSUE_LIST_DESCRIPTION_MAX_CHARS),
1415
1666
  }));
@@ -1420,7 +1671,7 @@ export function issueService(db) {
1420
1671
  return withRuns;
1421
1672
  }
1422
1673
  const issueIds = withRuns.map((row) => row.id);
1423
- const [statsRows, readRows, lastActivityRows] = await Promise.all([
1674
+ const [statsRows, readRows, lastActivityRows, blockedByMap] = await Promise.all([
1424
1675
  contextUserId
1425
1676
  ? userCommentStatsForIssues(db, companyId, contextUserId, issueIds)
1426
1677
  : Promise.resolve([]),
@@ -1428,18 +1679,28 @@ export function issueService(db) {
1428
1679
  ? userReadStatsForIssues(db, companyId, contextUserId, issueIds)
1429
1680
  : Promise.resolve([]),
1430
1681
  lastActivityStatsForIssues(db, companyId, issueIds),
1682
+ includeBlockedBy
1683
+ ? blockedByMapForIssues(db, companyId, issueIds)
1684
+ : Promise.resolve(new Map()),
1431
1685
  ]);
1432
1686
  const statsByIssueId = new Map(statsRows.map((row) => [row.issueId, row]));
1433
1687
  const lastActivityByIssueId = new Map(lastActivityRows.map((row) => [row.issueId, row]));
1434
- const blockerAttentionByIssueId = await listIssueBlockerAttentionMap(db, companyId, withRuns);
1688
+ const [blockerAttentionByIssueId, productivityReviewByIssueId] = await Promise.all([
1689
+ listIssueBlockerAttentionMap(db, companyId, withRuns),
1690
+ listIssueProductivityReviewMap(db, companyId, issueIds),
1691
+ ]);
1435
1692
  if (!contextUserId) {
1436
1693
  return withRuns.map((row) => {
1437
1694
  const activity = lastActivityByIssueId.get(row.id);
1438
1695
  const lastActivityAt = latestIssueActivityAt(row.updatedAt, activity?.latestCommentAt ?? null, activity?.latestLogAt ?? null) ?? row.updatedAt;
1439
1696
  return {
1440
1697
  ...row,
1698
+ ...(includeBlockedBy ? { blockedBy: blockedByMap.get(row.id) ?? [] } : {}),
1441
1699
  lastActivityAt,
1442
1700
  ...(blockerAttentionByIssueId.has(row.id) ? { blockerAttention: blockerAttentionByIssueId.get(row.id) } : {}),
1701
+ ...(productivityReviewByIssueId.has(row.id)
1702
+ ? { productivityReview: productivityReviewByIssueId.get(row.id) }
1703
+ : {}),
1443
1704
  };
1444
1705
  });
1445
1706
  }
@@ -1449,8 +1710,12 @@ export function issueService(db) {
1449
1710
  const lastActivityAt = latestIssueActivityAt(row.updatedAt, activity?.latestCommentAt ?? null, activity?.latestLogAt ?? null) ?? row.updatedAt;
1450
1711
  return {
1451
1712
  ...row,
1713
+ ...(includeBlockedBy ? { blockedBy: blockedByMap.get(row.id) ?? [] } : {}),
1452
1714
  lastActivityAt,
1453
1715
  ...(blockerAttentionByIssueId.has(row.id) ? { blockerAttention: blockerAttentionByIssueId.get(row.id) } : {}),
1716
+ ...(productivityReviewByIssueId.has(row.id)
1717
+ ? { productivityReview: productivityReviewByIssueId.get(row.id) }
1718
+ : {}),
1454
1719
  ...deriveIssueUserContext(row, contextUserId, {
1455
1720
  myLastCommentAt: statsByIssueId.get(row.id)?.myLastCommentAt ?? null,
1456
1721
  myLastReadAt: readByIssueId.get(row.id) ?? null,
@@ -1538,8 +1803,9 @@ export function issueService(db) {
1538
1803
  },
1539
1804
  getById: async (raw) => {
1540
1805
  const id = raw.trim();
1541
- if (/^[A-Z]+-\d+$/i.test(id)) {
1542
- return getIssueByIdentifier(id);
1806
+ const identifier = normalizeIssueReferenceIdentifier(id);
1807
+ if (identifier) {
1808
+ return getIssueByIdentifier(identifier);
1543
1809
  }
1544
1810
  if (!isUuidLike(id)) {
1545
1811
  return null;
@@ -1577,6 +1843,9 @@ export function issueService(db) {
1577
1843
  listBlockerAttention: async (companyId, issueRows, dbOrTx = db) => {
1578
1844
  return listIssueBlockerAttentionMap(dbOrTx, companyId, issueRows);
1579
1845
  },
1846
+ listProductivityReviews: async (companyId, sourceIssueIds, dbOrTx = db) => {
1847
+ return listIssueProductivityReviewMap(dbOrTx, companyId, sourceIssueIds);
1848
+ },
1580
1849
  listWakeableBlockedDependents: async (blockerIssueId) => {
1581
1850
  const blockerIssue = await db
1582
1851
  .select({ id: issues.id, companyId: issues.companyId })
@@ -1715,7 +1984,7 @@ export function issueService(db) {
1715
1984
  parentId: parent.id,
1716
1985
  projectId: issueData.projectId ?? parent.projectId,
1717
1986
  goalId: issueData.goalId ?? parent.goalId,
1718
- requestDepth: Math.max(parent.requestDepth + 1, issueData.requestDepth ?? 0),
1987
+ requestDepth: clampIssueRequestDepth(Math.max(clampIssueRequestDepth(parent.requestDepth) + 1, issueData.requestDepth ?? 0)),
1719
1988
  description: appendAcceptanceCriteriaToDescription(issueData.description, acceptanceCriteria),
1720
1989
  inheritExecutionWorkspaceFromIssueId: parent.id,
1721
1990
  });
@@ -1788,16 +2057,60 @@ export function issueService(db) {
1788
2057
  }
1789
2058
  }
1790
2059
  }
1791
- if (executionWorkspaceSettings == null &&
1792
- executionWorkspaceId == null &&
1793
- issueData.projectId) {
1794
- const project = await tx
2060
+ // Cache the project policy lookup for this insert. Both the
2061
+ // default-settings block and the assignee-environment-promotion block
2062
+ // need the same row; without caching they'd issue two round-trips.
2063
+ let projectPolicyCached = null;
2064
+ let projectPolicyLoaded = false;
2065
+ const loadProjectPolicyOnce = async () => {
2066
+ if (projectPolicyLoaded)
2067
+ return projectPolicyCached;
2068
+ projectPolicyLoaded = true;
2069
+ if (!issueData.projectId)
2070
+ return null;
2071
+ const projectRow = await tx
1795
2072
  .select({ executionWorkspacePolicy: projects.executionWorkspacePolicy })
1796
2073
  .from(projects)
1797
2074
  .where(and(eq(projects.id, issueData.projectId), eq(projects.companyId, companyId)))
1798
2075
  .then((rows) => rows[0] ?? null);
2076
+ projectPolicyCached = parseProjectExecutionWorkspacePolicy(projectRow?.executionWorkspacePolicy);
2077
+ return projectPolicyCached;
2078
+ };
2079
+ if (executionWorkspaceSettings == null &&
2080
+ executionWorkspaceId == null &&
2081
+ issueData.projectId) {
1799
2082
  executionWorkspaceSettings =
1800
- defaultIssueExecutionWorkspaceSettingsForProject(gateProjectExecutionWorkspacePolicy(parseProjectExecutionWorkspacePolicy(project?.executionWorkspacePolicy), isolatedWorkspacesEnabled));
2083
+ defaultIssueExecutionWorkspaceSettingsForProject(gateProjectExecutionWorkspacePolicy(await loadProjectPolicyOnce(), isolatedWorkspacesEnabled));
2084
+ }
2085
+ if (data.assigneeAgentId && isolatedWorkspacesEnabled) {
2086
+ const currentWorkspaceSettings = executionWorkspaceSettings == null
2087
+ ? {}
2088
+ : parseObject(executionWorkspaceSettings);
2089
+ const issueHasEnvironmentSelection = Object.prototype.hasOwnProperty.call(currentWorkspaceSettings, "environmentId");
2090
+ // Don't promote the assignee agent's defaultEnvironmentId if either
2091
+ // the issue or the project policy already specifies an environment.
2092
+ // resolveExecutionWorkspaceEnvironmentId treats issue settings as
2093
+ // higher priority than project policy, so promoting the agent's
2094
+ // default to issue settings would invert the documented priority
2095
+ // (project policy must win over agent default when explicitly set).
2096
+ let projectHasEnvironmentSelection = false;
2097
+ if (!issueHasEnvironmentSelection && issueData.projectId) {
2098
+ const projectPolicy = await loadProjectPolicyOnce();
2099
+ projectHasEnvironmentSelection = projectPolicy?.environmentId !== undefined;
2100
+ }
2101
+ if (!issueHasEnvironmentSelection && !projectHasEnvironmentSelection) {
2102
+ const assigneeAgent = await tx
2103
+ .select({ defaultEnvironmentId: agents.defaultEnvironmentId })
2104
+ .from(agents)
2105
+ .where(and(eq(agents.id, data.assigneeAgentId), eq(agents.companyId, companyId)))
2106
+ .then((rows) => rows[0] ?? null);
2107
+ if (typeof assigneeAgent?.defaultEnvironmentId === "string" && assigneeAgent.defaultEnvironmentId.length > 0) {
2108
+ executionWorkspaceSettings = {
2109
+ ...currentWorkspaceSettings,
2110
+ environmentId: assigneeAgent.defaultEnvironmentId,
2111
+ };
2112
+ }
2113
+ }
1801
2114
  }
1802
2115
  if (!projectWorkspaceId && issueData.projectId) {
1803
2116
  const project = await tx
@@ -1842,6 +2155,7 @@ export function issueService(db) {
1842
2155
  const identifier = `${company.issuePrefix}-${issueNumber}`;
1843
2156
  const values = {
1844
2157
  ...issueData,
2158
+ requestDepth: clampIssueRequestDepth(issueData.requestDepth),
1845
2159
  originKind: issueData.originKind ?? "manual",
1846
2160
  goalId: resolveIssueGoalId({
1847
2161
  projectId: issueData.projectId,
@@ -1866,6 +2180,12 @@ export function issueService(db) {
1866
2180
  if (values.status === "cancelled") {
1867
2181
  values.cancelledAt = new Date();
1868
2182
  }
2183
+ Object.assign(values, buildInitialIssueMonitorFields({
2184
+ policy: normalizeIssueExecutionPolicy(issueData.executionPolicy ?? null),
2185
+ status: values.status ?? "backlog",
2186
+ assigneeAgentId: values.assigneeAgentId ?? null,
2187
+ assigneeUserId: values.assigneeUserId ?? null,
2188
+ }));
1869
2189
  const [issue] = await tx.insert(issues).values(values).returning();
1870
2190
  if (inputLabelIds) {
1871
2191
  await syncIssueLabels(issue.id, companyId, inputLabelIds, tx);
@@ -1902,6 +2222,9 @@ export function issueService(db) {
1902
2222
  ...issueData,
1903
2223
  updatedAt: new Date(),
1904
2224
  };
2225
+ if (issueData.requestDepth !== undefined) {
2226
+ patch.requestDepth = clampIssueRequestDepth(issueData.requestDepth);
2227
+ }
1905
2228
  const nextAssigneeAgentId = issueData.assigneeAgentId !== undefined ? issueData.assigneeAgentId : existing.assigneeAgentId;
1906
2229
  const nextAssigneeUserId = issueData.assigneeUserId !== undefined ? issueData.assigneeUserId : existing.assigneeUserId;
1907
2230
  if (nextAssigneeAgentId && nextAssigneeUserId) {
@@ -1927,6 +2250,12 @@ export function issueService(db) {
1927
2250
  const nextProjectId = issueData.projectId !== undefined ? issueData.projectId : existing.projectId;
1928
2251
  const nextProjectWorkspaceId = issueData.projectWorkspaceId !== undefined ? issueData.projectWorkspaceId : existing.projectWorkspaceId;
1929
2252
  const nextExecutionWorkspaceId = issueData.executionWorkspaceId !== undefined ? issueData.executionWorkspaceId : existing.executionWorkspaceId;
2253
+ const nextExecutionWorkspacePreference = issueData.executionWorkspacePreference !== undefined
2254
+ ? issueData.executionWorkspacePreference
2255
+ : existing.executionWorkspacePreference;
2256
+ const nextExecutionWorkspaceSettings = issueData.executionWorkspaceSettings !== undefined
2257
+ ? parseIssueExecutionWorkspaceSettings(issueData.executionWorkspaceSettings)
2258
+ : parseIssueExecutionWorkspaceSettings(existing.executionWorkspaceSettings);
1930
2259
  if (nextProjectWorkspaceId) {
1931
2260
  await assertValidProjectWorkspace(existing.companyId, nextProjectId, nextProjectWorkspaceId);
1932
2261
  }
@@ -1961,6 +2290,66 @@ export function issueService(db) {
1961
2290
  getProjectDefaultGoalId(tx, existing.companyId, existing.projectId),
1962
2291
  getProjectDefaultGoalId(tx, existing.companyId, issueData.projectId !== undefined ? issueData.projectId : existing.projectId),
1963
2292
  ]);
2293
+ // Mirror the create() path: when the assignee changes to a non-null
2294
+ // agent, default the issue's executionWorkspaceSettings.environmentId
2295
+ // to the new agent's defaultEnvironmentId. Skip when:
2296
+ // - this update explicitly sets executionWorkspaceSettings.environmentId
2297
+ // (caller is making a deliberate override; respect it), OR
2298
+ // - the project policy already specifies an environmentId (project
2299
+ // policy must win over agent default per the documented priority
2300
+ // order in resolveExecutionWorkspaceEnvironmentId), OR
2301
+ // - the issue already has an environmentId that was *not* the prior
2302
+ // assignee's default (i.e., the operator set it explicitly in an
2303
+ // earlier update; preserve their choice). When the existing
2304
+ // environmentId matches the prior assignee's default, treat it as
2305
+ // auto-promoted and refresh it to the new assignee's default.
2306
+ const assigneeChanged = issueData.assigneeAgentId !== undefined &&
2307
+ issueData.assigneeAgentId !== null &&
2308
+ issueData.assigneeAgentId !== existing.assigneeAgentId;
2309
+ const explicitEnvInThisUpdate = issueData.executionWorkspaceSettings !== undefined &&
2310
+ Object.prototype.hasOwnProperty.call(parseObject(issueData.executionWorkspaceSettings), "environmentId");
2311
+ if (assigneeChanged && isolatedWorkspacesEnabled && !explicitEnvInThisUpdate) {
2312
+ let projectHasEnvironmentSelection = false;
2313
+ if (nextProjectId) {
2314
+ const projectRow = await tx
2315
+ .select({ executionWorkspacePolicy: projects.executionWorkspacePolicy })
2316
+ .from(projects)
2317
+ .where(and(eq(projects.id, nextProjectId), eq(projects.companyId, existing.companyId)))
2318
+ .then((rows) => rows[0] ?? null);
2319
+ const projectPolicy = parseProjectExecutionWorkspacePolicy(projectRow?.executionWorkspacePolicy);
2320
+ projectHasEnvironmentSelection = projectPolicy?.environmentId !== undefined;
2321
+ }
2322
+ if (!projectHasEnvironmentSelection) {
2323
+ const baseSettings = nextExecutionWorkspaceSettings == null
2324
+ ? {}
2325
+ : parseObject(nextExecutionWorkspaceSettings);
2326
+ const existingEnvId = typeof baseSettings.environmentId === "string"
2327
+ ? baseSettings.environmentId
2328
+ : null;
2329
+ const agentRows = await tx
2330
+ .select({ id: agents.id, defaultEnvironmentId: agents.defaultEnvironmentId })
2331
+ .from(agents)
2332
+ .where(and(eq(agents.companyId, existing.companyId), inArray(agents.id, [issueData.assigneeAgentId, existing.assigneeAgentId].filter((value) => typeof value === "string"))));
2333
+ const newAssignee = agentRows.find((row) => row.id === issueData.assigneeAgentId);
2334
+ const previousAssignee = existing.assigneeAgentId
2335
+ ? agentRows.find((row) => row.id === existing.assigneeAgentId)
2336
+ : null;
2337
+ const newDefaultEnvId = typeof newAssignee?.defaultEnvironmentId === "string" && newAssignee.defaultEnvironmentId.length > 0
2338
+ ? newAssignee.defaultEnvironmentId
2339
+ : null;
2340
+ const previousDefaultEnvId = typeof previousAssignee?.defaultEnvironmentId === "string" && previousAssignee.defaultEnvironmentId.length > 0
2341
+ ? previousAssignee.defaultEnvironmentId
2342
+ : null;
2343
+ const existingEnvWasAutoPromoted = existingEnvId === null ||
2344
+ (previousDefaultEnvId !== null && existingEnvId === previousDefaultEnvId);
2345
+ if (newDefaultEnvId && existingEnvWasAutoPromoted) {
2346
+ patch.executionWorkspaceSettings = {
2347
+ ...baseSettings,
2348
+ environmentId: newDefaultEnvId,
2349
+ };
2350
+ }
2351
+ }
2352
+ }
1964
2353
  patch.goalId = resolveNextIssueGoalId({
1965
2354
  currentProjectId: existing.projectId,
1966
2355
  currentGoalId: existing.goalId,
@@ -1987,6 +2376,27 @@ export function issueService(db) {
1987
2376
  userId: actorUserId ?? null,
1988
2377
  }, tx);
1989
2378
  }
2379
+ if (issueData.executionWorkspaceSettings !== undefined &&
2380
+ nextExecutionWorkspaceId &&
2381
+ nextExecutionWorkspacePreference === "reuse_existing") {
2382
+ const workspace = await tx
2383
+ .select({
2384
+ id: executionWorkspaces.id,
2385
+ metadata: executionWorkspaces.metadata,
2386
+ })
2387
+ .from(executionWorkspaces)
2388
+ .where(and(eq(executionWorkspaces.id, nextExecutionWorkspaceId), eq(executionWorkspaces.companyId, existing.companyId)))
2389
+ .then((rows) => rows[0] ?? null);
2390
+ if (workspace) {
2391
+ await tx
2392
+ .update(executionWorkspaces)
2393
+ .set({
2394
+ metadata: mergeExecutionWorkspaceConfig(workspace.metadata ?? null, buildReusedExecutionWorkspaceConfigPatchFromIssueSettings(nextExecutionWorkspaceSettings)),
2395
+ updatedAt: new Date(),
2396
+ })
2397
+ .where(eq(executionWorkspaces.id, workspace.id));
2398
+ }
2399
+ }
1990
2400
  const [enriched] = await withIssueLabels(tx, [updated]);
1991
2401
  return enriched;
1992
2402
  };
@@ -2359,14 +2769,8 @@ export function issueService(db) {
2359
2769
  if (!anchor)
2360
2770
  return [];
2361
2771
  conditions.push(order === "asc"
2362
- ? sql `(
2363
- ${issueComments.createdAt} > ${anchor.createdAt}
2364
- OR (${issueComments.createdAt} = ${anchor.createdAt} AND ${issueComments.id} > ${anchor.id})
2365
- )`
2366
- : sql `(
2367
- ${issueComments.createdAt} < ${anchor.createdAt}
2368
- OR (${issueComments.createdAt} = ${anchor.createdAt} AND ${issueComments.id} < ${anchor.id})
2369
- )`);
2772
+ ? or(gt(issueComments.createdAt, anchor.createdAt), and(eq(issueComments.createdAt, anchor.createdAt), gt(issueComments.id, anchor.id)))
2773
+ : or(lt(issueComments.createdAt, anchor.createdAt), and(eq(issueComments.createdAt, anchor.createdAt), lt(issueComments.id, anchor.id))));
2370
2774
  }
2371
2775
  const query = db
2372
2776
  .select()