@penclipai/server 2026.704.0 → 2026.714.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 (485) hide show
  1. package/dist/adapters/index.d.ts +1 -1
  2. package/dist/adapters/index.d.ts.map +1 -1
  3. package/dist/adapters/index.js.map +1 -1
  4. package/dist/adapters/process/execute.d.ts.map +1 -1
  5. package/dist/adapters/process/execute.js +1 -0
  6. package/dist/adapters/process/execute.js.map +1 -1
  7. package/dist/adapters/registry.d.ts.map +1 -1
  8. package/dist/adapters/registry.js +79 -51
  9. package/dist/adapters/registry.js.map +1 -1
  10. package/dist/adapters/utils.d.ts +5 -0
  11. package/dist/adapters/utils.d.ts.map +1 -1
  12. package/dist/adapters/utils.js.map +1 -1
  13. package/dist/agent-auth-jwt.d.ts +5 -1
  14. package/dist/agent-auth-jwt.d.ts.map +1 -1
  15. package/dist/agent-auth-jwt.js +72 -20
  16. package/dist/agent-auth-jwt.js.map +1 -1
  17. package/dist/app.d.ts +2 -0
  18. package/dist/app.d.ts.map +1 -1
  19. package/dist/app.js +10 -0
  20. package/dist/app.js.map +1 -1
  21. package/dist/built-ins/agents/reflection-coach/AGENTS.md +47 -0
  22. package/dist/built-ins/agents/reflection-coach/routines/recent-agent-reflection.md +77 -0
  23. package/dist/errors.d.ts +1 -1
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js +2 -2
  26. package/dist/errors.js.map +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +208 -116
  29. package/dist/index.js.map +1 -1
  30. package/dist/middleware/api-compression.d.ts +8 -0
  31. package/dist/middleware/api-compression.d.ts.map +1 -0
  32. package/dist/middleware/api-compression.js +207 -0
  33. package/dist/middleware/api-compression.js.map +1 -0
  34. package/dist/middleware/auth.d.ts.map +1 -1
  35. package/dist/middleware/auth.js +130 -3
  36. package/dist/middleware/auth.js.map +1 -1
  37. package/dist/middleware/error-handler.d.ts.map +1 -1
  38. package/dist/middleware/error-handler.js +27 -0
  39. package/dist/middleware/error-handler.js.map +1 -1
  40. package/dist/onboarding-assets/default/AGENTS.md +1 -0
  41. package/dist/redaction.d.ts.map +1 -1
  42. package/dist/redaction.js +11 -0
  43. package/dist/redaction.js.map +1 -1
  44. package/dist/routes/access.d.ts.map +1 -1
  45. package/dist/routes/access.js +4 -3
  46. package/dist/routes/access.js.map +1 -1
  47. package/dist/routes/adapters.d.ts.map +1 -1
  48. package/dist/routes/adapters.js +2 -0
  49. package/dist/routes/adapters.js.map +1 -1
  50. package/dist/routes/agents.d.ts.map +1 -1
  51. package/dist/routes/agents.js +222 -87
  52. package/dist/routes/agents.js.map +1 -1
  53. package/dist/routes/attention.d.ts +3 -0
  54. package/dist/routes/attention.d.ts.map +1 -0
  55. package/dist/routes/attention.js +24 -0
  56. package/dist/routes/attention.js.map +1 -0
  57. package/dist/routes/authz.d.ts.map +1 -1
  58. package/dist/routes/authz.js +29 -1
  59. package/dist/routes/authz.js.map +1 -1
  60. package/dist/routes/board-chat.d.ts.map +1 -1
  61. package/dist/routes/board-chat.js +4 -1
  62. package/dist/routes/board-chat.js.map +1 -1
  63. package/dist/routes/built-in-agents.d.ts +3 -0
  64. package/dist/routes/built-in-agents.d.ts.map +1 -0
  65. package/dist/routes/built-in-agents.js +267 -0
  66. package/dist/routes/built-in-agents.js.map +1 -0
  67. package/dist/routes/cases.d.ts +46 -0
  68. package/dist/routes/cases.d.ts.map +1 -0
  69. package/dist/routes/cases.js +1395 -0
  70. package/dist/routes/cases.js.map +1 -0
  71. package/dist/routes/companies.d.ts.map +1 -1
  72. package/dist/routes/companies.js +4 -1
  73. package/dist/routes/companies.js.map +1 -1
  74. package/dist/routes/company-skills.d.ts.map +1 -1
  75. package/dist/routes/company-skills.js +453 -33
  76. package/dist/routes/company-skills.js.map +1 -1
  77. package/dist/routes/environments.d.ts.map +1 -1
  78. package/dist/routes/environments.js +71 -8
  79. package/dist/routes/environments.js.map +1 -1
  80. package/dist/routes/execution-workspaces.d.ts.map +1 -1
  81. package/dist/routes/execution-workspaces.js +89 -3
  82. package/dist/routes/execution-workspaces.js.map +1 -1
  83. package/dist/routes/health.d.ts +2 -0
  84. package/dist/routes/health.d.ts.map +1 -1
  85. package/dist/routes/health.js +30 -0
  86. package/dist/routes/health.js.map +1 -1
  87. package/dist/routes/inbox-dismissals.d.ts.map +1 -1
  88. package/dist/routes/inbox-dismissals.js +80 -19
  89. package/dist/routes/inbox-dismissals.js.map +1 -1
  90. package/dist/routes/index.d.ts +2 -0
  91. package/dist/routes/index.d.ts.map +1 -1
  92. package/dist/routes/index.js +2 -0
  93. package/dist/routes/index.js.map +1 -1
  94. package/dist/routes/issues.d.ts +25 -0
  95. package/dist/routes/issues.d.ts.map +1 -1
  96. package/dist/routes/issues.js +1455 -46
  97. package/dist/routes/issues.js.map +1 -1
  98. package/dist/routes/openapi.d.ts.map +1 -1
  99. package/dist/routes/openapi.js +448 -11
  100. package/dist/routes/openapi.js.map +1 -1
  101. package/dist/routes/pipelines.js +2 -2
  102. package/dist/routes/pipelines.js.map +1 -1
  103. package/dist/routes/resource-memberships.d.ts.map +1 -1
  104. package/dist/routes/resource-memberships.js +13 -5
  105. package/dist/routes/resource-memberships.js.map +1 -1
  106. package/dist/routes/secrets.d.ts.map +1 -1
  107. package/dist/routes/secrets.js +261 -1
  108. package/dist/routes/secrets.js.map +1 -1
  109. package/dist/routes/sidebar-badges.d.ts.map +1 -1
  110. package/dist/routes/sidebar-badges.js +18 -2
  111. package/dist/routes/sidebar-badges.js.map +1 -1
  112. package/dist/routes/user-profiles.d.ts.map +1 -1
  113. package/dist/routes/user-profiles.js +5 -4
  114. package/dist/routes/user-profiles.js.map +1 -1
  115. package/dist/server-info.d.ts +2 -0
  116. package/dist/server-info.d.ts.map +1 -1
  117. package/dist/server-info.js +20 -5
  118. package/dist/server-info.js.map +1 -1
  119. package/dist/services/access.d.ts +4 -4
  120. package/dist/services/access.d.ts.map +1 -1
  121. package/dist/services/activity.d.ts +3 -3
  122. package/dist/services/activity.d.ts.map +1 -1
  123. package/dist/services/activity.js +5 -4
  124. package/dist/services/activity.js.map +1 -1
  125. package/dist/services/adapter-registry-bootstrap.reconcile.test.js +1 -0
  126. package/dist/services/adapter-registry-bootstrap.reconcile.test.js.map +1 -1
  127. package/dist/services/agent-secret-bindings.d.ts +15 -0
  128. package/dist/services/agent-secret-bindings.d.ts.map +1 -1
  129. package/dist/services/agent-secret-bindings.js +43 -0
  130. package/dist/services/agent-secret-bindings.js.map +1 -1
  131. package/dist/services/agents.d.ts +194 -133
  132. package/dist/services/agents.d.ts.map +1 -1
  133. package/dist/services/agents.js +107 -4
  134. package/dist/services/agents.js.map +1 -1
  135. package/dist/services/approvals.d.ts +10 -10
  136. package/dist/services/approvals.d.ts.map +1 -1
  137. package/dist/services/approvals.js +9 -1
  138. package/dist/services/approvals.js.map +1 -1
  139. package/dist/services/assets.d.ts +5 -5
  140. package/dist/services/attention.d.ts +11 -0
  141. package/dist/services/attention.d.ts.map +1 -0
  142. package/dist/services/attention.js +1024 -0
  143. package/dist/services/attention.js.map +1 -0
  144. package/dist/services/authorization.d.ts +15 -2
  145. package/dist/services/authorization.d.ts.map +1 -1
  146. package/dist/services/authorization.js +324 -8
  147. package/dist/services/authorization.js.map +1 -1
  148. package/dist/services/board-auth.d.ts +2 -2
  149. package/dist/services/budgets.d.ts.map +1 -1
  150. package/dist/services/budgets.js +8 -7
  151. package/dist/services/budgets.js.map +1 -1
  152. package/dist/services/built-in-agent-metadata.d.ts +9 -0
  153. package/dist/services/built-in-agent-metadata.d.ts.map +1 -0
  154. package/dist/services/built-in-agent-metadata.js +39 -0
  155. package/dist/services/built-in-agent-metadata.js.map +1 -0
  156. package/dist/services/built-in-agents.d.ts +394 -0
  157. package/dist/services/built-in-agents.d.ts.map +1 -0
  158. package/dist/services/built-in-agents.js +1426 -0
  159. package/dist/services/built-in-agents.js.map +1 -0
  160. package/dist/services/change-consent-gate.d.ts +18 -0
  161. package/dist/services/change-consent-gate.d.ts.map +1 -0
  162. package/dist/services/change-consent-gate.js +170 -0
  163. package/dist/services/change-consent-gate.js.map +1 -0
  164. package/dist/services/companies.d.ts +14 -8
  165. package/dist/services/companies.d.ts.map +1 -1
  166. package/dist/services/companies.js +6 -1
  167. package/dist/services/companies.js.map +1 -1
  168. package/dist/services/company-artifacts.d.ts +4 -1
  169. package/dist/services/company-artifacts.d.ts.map +1 -1
  170. package/dist/services/company-artifacts.js +9 -1
  171. package/dist/services/company-artifacts.js.map +1 -1
  172. package/dist/services/company-member-roles.d.ts.map +1 -1
  173. package/dist/services/company-member-roles.js +2 -0
  174. package/dist/services/company-member-roles.js.map +1 -1
  175. package/dist/services/company-portability.d.ts +1 -1
  176. package/dist/services/company-portability.d.ts.map +1 -1
  177. package/dist/services/company-portability.js +113 -8
  178. package/dist/services/company-portability.js.map +1 -1
  179. package/dist/services/company-search.d.ts.map +1 -1
  180. package/dist/services/company-search.js +843 -294
  181. package/dist/services/company-search.js.map +1 -1
  182. package/dist/services/company-skills.d.ts +64 -3
  183. package/dist/services/company-skills.d.ts.map +1 -1
  184. package/dist/services/company-skills.js +1205 -20
  185. package/dist/services/company-skills.js.map +1 -1
  186. package/dist/services/costs.d.ts +9 -8
  187. package/dist/services/costs.d.ts.map +1 -1
  188. package/dist/services/costs.js +9 -2
  189. package/dist/services/costs.js.map +1 -1
  190. package/dist/services/dashboard.d.ts +2 -0
  191. package/dist/services/dashboard.d.ts.map +1 -1
  192. package/dist/services/dashboard.js +60 -17
  193. package/dist/services/dashboard.js.map +1 -1
  194. package/dist/services/database-backup-health.d.ts +34 -0
  195. package/dist/services/database-backup-health.d.ts.map +1 -0
  196. package/dist/services/database-backup-health.js +104 -0
  197. package/dist/services/database-backup-health.js.map +1 -0
  198. package/dist/services/document-annotations.d.ts +169 -1
  199. package/dist/services/document-annotations.d.ts.map +1 -1
  200. package/dist/services/document-annotations.js +272 -1
  201. package/dist/services/document-annotations.js.map +1 -1
  202. package/dist/services/documents.d.ts +368 -0
  203. package/dist/services/documents.d.ts.map +1 -1
  204. package/dist/services/documents.js +2 -2
  205. package/dist/services/documents.js.map +1 -1
  206. package/dist/services/environment-config.d.ts.map +1 -1
  207. package/dist/services/environment-config.js +6 -0
  208. package/dist/services/environment-config.js.map +1 -1
  209. package/dist/services/environment-custom-image-runtime.d.ts +28 -0
  210. package/dist/services/environment-custom-image-runtime.d.ts.map +1 -1
  211. package/dist/services/environment-custom-image-runtime.js +106 -7
  212. package/dist/services/environment-custom-image-runtime.js.map +1 -1
  213. package/dist/services/environment-custom-images.d.ts +31 -1
  214. package/dist/services/environment-custom-images.d.ts.map +1 -1
  215. package/dist/services/environment-custom-images.js +110 -4
  216. package/dist/services/environment-custom-images.js.map +1 -1
  217. package/dist/services/environment-execution-target.d.ts.map +1 -1
  218. package/dist/services/environment-execution-target.js +1 -3
  219. package/dist/services/environment-execution-target.js.map +1 -1
  220. package/dist/services/environment-probe.d.ts +1 -0
  221. package/dist/services/environment-probe.d.ts.map +1 -1
  222. package/dist/services/environment-probe.js +99 -5
  223. package/dist/services/environment-probe.js.map +1 -1
  224. package/dist/services/environments.d.ts +3 -1
  225. package/dist/services/environments.d.ts.map +1 -1
  226. package/dist/services/environments.js +98 -3
  227. package/dist/services/environments.js.map +1 -1
  228. package/dist/services/execution-workspace-policy.d.ts +29 -3
  229. package/dist/services/execution-workspace-policy.d.ts.map +1 -1
  230. package/dist/services/execution-workspace-policy.js +44 -1
  231. package/dist/services/execution-workspace-policy.js.map +1 -1
  232. package/dist/services/execution-workspaces.d.ts +64 -1
  233. package/dist/services/execution-workspaces.d.ts.map +1 -1
  234. package/dist/services/execution-workspaces.js +634 -3
  235. package/dist/services/execution-workspaces.js.map +1 -1
  236. package/dist/services/external-objects.d.ts +26 -26
  237. package/dist/services/feedback.d.ts +2 -2
  238. package/dist/services/finance.d.ts +5 -5
  239. package/dist/services/goals.d.ts +8 -8
  240. package/dist/services/heartbeat-stop-metadata.d.ts +2 -2
  241. package/dist/services/heartbeat-stop-metadata.d.ts.map +1 -1
  242. package/dist/services/heartbeat-stop-metadata.js +4 -0
  243. package/dist/services/heartbeat-stop-metadata.js.map +1 -1
  244. package/dist/services/heartbeat-stop-metadata.test.js +9 -0
  245. package/dist/services/heartbeat-stop-metadata.test.js.map +1 -1
  246. package/dist/services/heartbeat.d.ts +133 -28
  247. package/dist/services/heartbeat.d.ts.map +1 -1
  248. package/dist/services/heartbeat.js +1963 -239
  249. package/dist/services/heartbeat.js.map +1 -1
  250. package/dist/services/inbox-dismissals.d.ts +26 -0
  251. package/dist/services/inbox-dismissals.d.ts.map +1 -1
  252. package/dist/services/inbox-dismissals.js +33 -18
  253. package/dist/services/inbox-dismissals.js.map +1 -1
  254. package/dist/services/index.d.ts +3 -1
  255. package/dist/services/index.d.ts.map +1 -1
  256. package/dist/services/index.js +3 -1
  257. package/dist/services/index.js.map +1 -1
  258. package/dist/services/instance-settings.d.ts +25 -1
  259. package/dist/services/instance-settings.d.ts.map +1 -1
  260. package/dist/services/instance-settings.js +103 -5
  261. package/dist/services/instance-settings.js.map +1 -1
  262. package/dist/services/issue-continuation-summary.js +1 -1
  263. package/dist/services/issue-continuation-summary.js.map +1 -1
  264. package/dist/services/issue-recovery-actions.d.ts +3 -0
  265. package/dist/services/issue-recovery-actions.d.ts.map +1 -1
  266. package/dist/services/issue-recovery-actions.js +9 -0
  267. package/dist/services/issue-recovery-actions.js.map +1 -1
  268. package/dist/services/issue-rewake-throttle.d.ts +92 -0
  269. package/dist/services/issue-rewake-throttle.d.ts.map +1 -0
  270. package/dist/services/issue-rewake-throttle.js +139 -0
  271. package/dist/services/issue-rewake-throttle.js.map +1 -0
  272. package/dist/services/issue-thread-interactions.d.ts +8 -1
  273. package/dist/services/issue-thread-interactions.d.ts.map +1 -1
  274. package/dist/services/issue-thread-interactions.js +231 -16
  275. package/dist/services/issue-thread-interactions.js.map +1 -1
  276. package/dist/services/issue-visibility.d.ts +4 -0
  277. package/dist/services/issue-visibility.d.ts.map +1 -0
  278. package/dist/services/issue-visibility.js +9 -0
  279. package/dist/services/issue-visibility.js.map +1 -0
  280. package/dist/services/issues.d.ts +225 -105
  281. package/dist/services/issues.d.ts.map +1 -1
  282. package/dist/services/issues.js +540 -18
  283. package/dist/services/issues.js.map +1 -1
  284. package/dist/services/local-service-supervisor.d.ts +3 -0
  285. package/dist/services/local-service-supervisor.d.ts.map +1 -1
  286. package/dist/services/local-service-supervisor.js +51 -0
  287. package/dist/services/local-service-supervisor.js.map +1 -1
  288. package/dist/services/pipeline-case-outputs.d.ts.map +1 -1
  289. package/dist/services/pipeline-case-outputs.js +2 -1
  290. package/dist/services/pipeline-case-outputs.js.map +1 -1
  291. package/dist/services/pipelines-aggregation.d.ts.map +1 -1
  292. package/dist/services/pipelines-aggregation.js +3 -2
  293. package/dist/services/pipelines-aggregation.js.map +1 -1
  294. package/dist/services/pipelines.d.ts +165 -165
  295. package/dist/services/pipelines.d.ts.map +1 -1
  296. package/dist/services/pipelines.js +6 -4
  297. package/dist/services/pipelines.js.map +1 -1
  298. package/dist/services/plugin-database.d.ts +2 -2
  299. package/dist/services/plugin-environment-driver.d.ts +1 -1
  300. package/dist/services/plugin-host-services.d.ts.map +1 -1
  301. package/dist/services/plugin-host-services.js +3 -1
  302. package/dist/services/plugin-host-services.js.map +1 -1
  303. package/dist/services/plugin-managed-routines.d.ts +1 -0
  304. package/dist/services/plugin-managed-routines.d.ts.map +1 -1
  305. package/dist/services/plugin-registry.d.ts +21 -21
  306. package/dist/services/productivity-review.d.ts +1 -0
  307. package/dist/services/productivity-review.d.ts.map +1 -1
  308. package/dist/services/productivity-review.js +6 -5
  309. package/dist/services/productivity-review.js.map +1 -1
  310. package/dist/services/projects.d.ts +9 -9
  311. package/dist/services/recovery/service.d.ts +82 -54
  312. package/dist/services/recovery/service.d.ts.map +1 -1
  313. package/dist/services/recovery/service.js +200 -35
  314. package/dist/services/recovery/service.js.map +1 -1
  315. package/dist/services/recovery/successful-run-handoff.d.ts +1 -0
  316. package/dist/services/recovery/successful-run-handoff.d.ts.map +1 -1
  317. package/dist/services/recovery/successful-run-handoff.js +2 -0
  318. package/dist/services/recovery/successful-run-handoff.js.map +1 -1
  319. package/dist/services/recovery/successful-run-handoff.test.js +20 -0
  320. package/dist/services/recovery/successful-run-handoff.test.js.map +1 -1
  321. package/dist/services/resource-memberships.d.ts +13 -10
  322. package/dist/services/resource-memberships.d.ts.map +1 -1
  323. package/dist/services/resource-memberships.js +89 -20
  324. package/dist/services/resource-memberships.js.map +1 -1
  325. package/dist/services/responsible-user-denial-run-outcomes.d.ts +60 -0
  326. package/dist/services/responsible-user-denial-run-outcomes.d.ts.map +1 -0
  327. package/dist/services/responsible-user-denial-run-outcomes.js +56 -0
  328. package/dist/services/responsible-user-denial-run-outcomes.js.map +1 -0
  329. package/dist/services/responsible-user-denial-run-outcomes.test.d.ts +2 -0
  330. package/dist/services/responsible-user-denial-run-outcomes.test.d.ts.map +1 -0
  331. package/dist/services/responsible-user-denial-run-outcomes.test.js +75 -0
  332. package/dist/services/responsible-user-denial-run-outcomes.test.js.map +1 -0
  333. package/dist/services/routines.d.ts +30 -8
  334. package/dist/services/routines.d.ts.map +1 -1
  335. package/dist/services/routines.js +241 -18
  336. package/dist/services/routines.js.map +1 -1
  337. package/dist/services/run-liveness.d.ts.map +1 -1
  338. package/dist/services/run-liveness.js +21 -0
  339. package/dist/services/run-liveness.js.map +1 -1
  340. package/dist/services/run-log-store.d.ts +1 -0
  341. package/dist/services/run-log-store.d.ts.map +1 -1
  342. package/dist/services/run-log-store.js +4 -0
  343. package/dist/services/run-log-store.js.map +1 -1
  344. package/dist/services/run-scratch.d.ts +42 -0
  345. package/dist/services/run-scratch.d.ts.map +1 -0
  346. package/dist/services/run-scratch.js +111 -0
  347. package/dist/services/run-scratch.js.map +1 -0
  348. package/dist/services/run-scratch.test.d.ts +2 -0
  349. package/dist/services/run-scratch.test.d.ts.map +1 -0
  350. package/dist/services/run-scratch.test.js +98 -0
  351. package/dist/services/run-scratch.test.js.map +1 -0
  352. package/dist/services/secrets.d.ts +1261 -64
  353. package/dist/services/secrets.d.ts.map +1 -1
  354. package/dist/services/secrets.js +1089 -96
  355. package/dist/services/secrets.js.map +1 -1
  356. package/dist/services/task-watchdogs.d.ts +1 -0
  357. package/dist/services/task-watchdogs.d.ts.map +1 -1
  358. package/dist/services/task-watchdogs.js +78 -9
  359. package/dist/services/task-watchdogs.js.map +1 -1
  360. package/dist/services/work-timeline.d.ts.map +1 -1
  361. package/dist/services/work-timeline.js +34 -2
  362. package/dist/services/work-timeline.js.map +1 -1
  363. package/dist/services/workspace-runtime-read-model.d.ts +30 -29
  364. package/dist/services/workspace-runtime-read-model.d.ts.map +1 -1
  365. package/dist/services/workspace-runtime-read-model.js.map +1 -1
  366. package/dist/services/workspace-runtime.d.ts +59 -13
  367. package/dist/services/workspace-runtime.d.ts.map +1 -1
  368. package/dist/services/workspace-runtime.js +1121 -102
  369. package/dist/services/workspace-runtime.js.map +1 -1
  370. package/dist/skills-catalog/catalog/bundled/paperclip-operations/reflection-coach/SKILL.md +202 -0
  371. package/dist/skills-catalog/catalog/bundled/product/paperclip-capsules/SKILL.md +1 -1
  372. package/dist/skills-catalog/catalog/bundled/product/wireframe/SKILL.md +1 -1
  373. package/dist/skills-catalog/catalog/optional/content/release-announcement/SKILL.md +90 -0
  374. package/dist/skills-catalog/catalog/optional/finance/ramp/SKILL.md +98 -0
  375. package/dist/skills-catalog/generated/catalog.json +84 -12
  376. package/dist/ui-branding.d.ts +7 -0
  377. package/dist/ui-branding.d.ts.map +1 -1
  378. package/dist/ui-branding.js +8 -2
  379. package/dist/ui-branding.js.map +1 -1
  380. package/dist/version.d.ts +14 -0
  381. package/dist/version.d.ts.map +1 -1
  382. package/dist/version.js +114 -3
  383. package/dist/version.js.map +1 -1
  384. package/package.json +19 -20
  385. package/skills/paperclip/SKILL.md +45 -10
  386. package/skills/paperclip/references/api-reference.md +246 -1
  387. package/skills/paperclip/references/cases.md +295 -0
  388. package/skills/paperclip/references/company-skills.md +1 -1
  389. package/skills/paperclip-board/SKILL.md +3 -4
  390. package/skills/paperclip-converting-plans-to-tasks/SKILL.md +3 -7
  391. package/skills/para-memory-files/SKILL.md +3 -7
  392. package/ui-dist/assets/{abnfDiagram-VRR7QNED-BsFz2IiG.js → abnfDiagram-VRR7QNED-Dtgfy1PK.js} +1 -1
  393. package/ui-dist/assets/{arc-HZhAwbOs.js → arc-BGdvNvs8.js} +1 -1
  394. package/ui-dist/assets/{architectureDiagram-ZJ3FMSHR-BteMjuOn.js → architectureDiagram-ZJ3FMSHR-CK19SQDx.js} +1 -1
  395. package/ui-dist/assets/{blockDiagram-677ZJIJ3-Iq66uZjf.js → blockDiagram-677ZJIJ3-Kh4N3XG4.js} +1 -1
  396. package/ui-dist/assets/{browser-ponyfill-BBsuyZJt.js → browser-ponyfill-B3hXtEFW.js} +1 -1
  397. package/ui-dist/assets/{c4Diagram-LMCZKHZV-_nlglj7I.js → c4Diagram-LMCZKHZV-CBHpGrgU.js} +1 -1
  398. package/ui-dist/assets/channel-4fjB8e8N.js +1 -0
  399. package/ui-dist/assets/{chunk-2Q5K7J3B-Cvz7WwSQ.js → chunk-2Q5K7J3B-BdHbqi7k.js} +1 -1
  400. package/ui-dist/assets/{chunk-32BRIVSS-LLubniCS.js → chunk-32BRIVSS-DxtzZzpt.js} +1 -1
  401. package/ui-dist/assets/{chunk-5VM5RSS4-BOst5hcr.js → chunk-5VM5RSS4-DMKxBbgu.js} +1 -1
  402. package/ui-dist/assets/{chunk-EX3LRPZG-D5WNShVm.js → chunk-EX3LRPZG-BaIOPi4v.js} +1 -1
  403. package/ui-dist/assets/{chunk-JWPE2WC7-CA07x4av.js → chunk-JWPE2WC7-DGo3MPmr.js} +1 -1
  404. package/ui-dist/assets/{chunk-MOJQB5TN-Clfi83rK.js → chunk-MOJQB5TN-BEhQg0uD.js} +1 -1
  405. package/ui-dist/assets/{chunk-RYQCIY6F-BhZ6p7YJ.js → chunk-RYQCIY6F-HPJ6zRpJ.js} +1 -1
  406. package/ui-dist/assets/{chunk-V7JOEXUC-CdexJGoV.js → chunk-V7JOEXUC-Bl_6cAJD.js} +1 -1
  407. package/ui-dist/assets/{chunk-VR4S4FIN-8vjmBhr1.js → chunk-VR4S4FIN-KMjgGCsw.js} +1 -1
  408. package/ui-dist/assets/{chunk-XXDRQBXY-CqmCaplj.js → chunk-XXDRQBXY-oWGAIKjr.js} +1 -1
  409. package/ui-dist/assets/classDiagram-OUVF2IWQ-CTXxO5l9.js +1 -0
  410. package/ui-dist/assets/classDiagram-v2-EOCWNBFH-CTXxO5l9.js +1 -0
  411. package/ui-dist/assets/{cose-bilkent-JH36ORCC-CY-zAygY.js → cose-bilkent-JH36ORCC-Ckqse1pH.js} +1 -1
  412. package/ui-dist/assets/{cynefin-VYW2F7L2-BQSQ1HKr.js → cynefin-VYW2F7L2-DfZ8Hzqi.js} +1 -1
  413. package/ui-dist/assets/{cynefinDiagram-TSTJHNR4-NJCuFrZL.js → cynefinDiagram-TSTJHNR4-BkDQo9oi.js} +1 -1
  414. package/ui-dist/assets/{dagre-VKFMJZFB-DAUPIc8g.js → dagre-VKFMJZFB-ClghXQ6E.js} +1 -1
  415. package/ui-dist/assets/{diagram-FQU43EPY-DvMwJlCi.js → diagram-FQU43EPY-D5a0CwpV.js} +1 -1
  416. package/ui-dist/assets/{diagram-G47NLZAW-DPY0Jz_y.js → diagram-G47NLZAW-hVjYR_xl.js} +1 -1
  417. package/ui-dist/assets/{diagram-NH7WQ7WH-BpopeEhp.js → diagram-NH7WQ7WH-BlDPgD5Z.js} +1 -1
  418. package/ui-dist/assets/{diagram-OA4YK3LP-Qpu05z9X.js → diagram-OA4YK3LP-D0iZKIS3.js} +1 -1
  419. package/ui-dist/assets/{diagram-WEI45ONY-DHnvw9XU.js → diagram-WEI45ONY-m6hrfD0J.js} +1 -1
  420. package/ui-dist/assets/{ebnfDiagram-CCIWWBDH-D61S54_7.js → ebnfDiagram-CCIWWBDH-D4SQYLod.js} +1 -1
  421. package/ui-dist/assets/{erDiagram-Q63AITRT-pplAN_x2.js → erDiagram-Q63AITRT-Byzb6-I5.js} +1 -1
  422. package/ui-dist/assets/{flowDiagram-23GEKE2U-BaBcbyDq.js → flowDiagram-23GEKE2U-CaTUkFO6.js} +1 -1
  423. package/ui-dist/assets/{ganttDiagram-NO4QXBWP-BOJSII2b.js → ganttDiagram-NO4QXBWP-DmdMQ3ES.js} +1 -1
  424. package/ui-dist/assets/{gitGraphDiagram-IHSO6WYX-aYAaO4a2.js → gitGraphDiagram-IHSO6WYX-Bt-1g2CS.js} +1 -1
  425. package/ui-dist/assets/{index-CZxYajxF.js → index-2Dk9yO0W.js} +1 -1
  426. package/ui-dist/assets/{index-B8H0SHw1.js → index-8jLpgFNU.js} +1 -1
  427. package/ui-dist/assets/{index-3EKATXgc.js → index-AP_iimxs.js} +1 -1
  428. package/ui-dist/assets/{index-C2JgdjPa.js → index-BNJasQY9.js} +1 -1
  429. package/ui-dist/assets/{index-BaiskB3o.js → index-BO-aNmyX.js} +1 -1
  430. package/ui-dist/assets/{index-B5YenxAQ.js → index-BPCzCn1Y.js} +1 -1
  431. package/ui-dist/assets/{index-Da2kqT1M.js → index-BSLnLTb2.js} +1 -1
  432. package/ui-dist/assets/{index-By0oeEvz.js → index-CCZ2qgHN.js} +1 -1
  433. package/ui-dist/assets/{index-bUFCSEgv.js → index-CLZXR1k9.js} +1 -1
  434. package/ui-dist/assets/{index-CJlw4l9L.js → index-CTNfrE1s.js} +1 -1
  435. package/ui-dist/assets/{index-TmpY6Xh_.js → index-ChNd25s7.js} +1 -1
  436. package/ui-dist/assets/{index-BRgdHUNR.js → index-CjHiMOEO.js} +1 -1
  437. package/ui-dist/assets/{index-DrL6pHSJ.js → index-CmeJQpXZ.js} +1 -1
  438. package/ui-dist/assets/index-CyuYtn7U.css +1 -0
  439. package/ui-dist/assets/{index-CKKytnOd.js → index-CzZjM8eh.js} +1 -1
  440. package/ui-dist/assets/{index-Dceamza_.js → index-D-6T0Aft.js} +1 -1
  441. package/ui-dist/assets/{index-CjDqB1SX.js → index-DG_7V1pG.js} +1 -1
  442. package/ui-dist/assets/index-DIKcgxCt.js +676 -0
  443. package/ui-dist/assets/{index-BKVeJ3Jh.js → index-DiUerHmi.js} +1 -1
  444. package/ui-dist/assets/{index-C-nKjFZh.js → index-NZ7v5Fc2.js} +1 -1
  445. package/ui-dist/assets/{index-CvPPuNs1.js → index-Py70EvBg.js} +1 -1
  446. package/ui-dist/assets/{index-LHkZdu0h.js → index-R3aYn0qM.js} +1 -1
  447. package/ui-dist/assets/{index-BT8b79Bd.js → index-YXpm7gW-.js} +1 -1
  448. package/ui-dist/assets/{index-CemenVOf.js → index-n4q3gzki.js} +1 -1
  449. package/ui-dist/assets/{index-D3ODiUup.js → index-vlZ3GoYY.js} +1 -1
  450. package/ui-dist/assets/{infoDiagram-FWYZ7A6U-C7_90qGq.js → infoDiagram-FWYZ7A6U-jdi5ciVg.js} +1 -1
  451. package/ui-dist/assets/{ishikawaDiagram-FXEZZL3T-T-nPPdTt.js → ishikawaDiagram-FXEZZL3T-CMz5VvYp.js} +1 -1
  452. package/ui-dist/assets/{journeyDiagram-5HDEW3XC-C3hqQN0R.js → journeyDiagram-5HDEW3XC-DK6dk2R-.js} +1 -1
  453. package/ui-dist/assets/{kanban-definition-HUTT4EX6-CPoD9DUm.js → kanban-definition-HUTT4EX6-DX5cZL-D.js} +1 -1
  454. package/ui-dist/assets/{linear-D-S8yr3Z.js → linear-CI96ABKp.js} +1 -1
  455. package/ui-dist/assets/{mermaid.core-DhvH8shK.js → mermaid.core-DnsEVfbd.js} +4 -4
  456. package/ui-dist/assets/{mindmap-definition-LN4V7U3C-CxCIKrKo.js → mindmap-definition-LN4V7U3C-BdDCY7hf.js} +1 -1
  457. package/ui-dist/assets/{pegDiagram-2B236MQR-D4G-PC6N.js → pegDiagram-2B236MQR-DuY4TF-f.js} +1 -1
  458. package/ui-dist/assets/{pieDiagram-ENE6RG2P-N4X8UvqY.js → pieDiagram-ENE6RG2P-bgFw7dm3.js} +1 -1
  459. package/ui-dist/assets/{quadrantDiagram-ABIIQ3AL-CZohoDln.js → quadrantDiagram-ABIIQ3AL-BCZVWyWS.js} +1 -1
  460. package/ui-dist/assets/{railroadDiagram-RFXS5EU6-fMGQVhwF.js → railroadDiagram-RFXS5EU6-D9V5_Cgc.js} +1 -1
  461. package/ui-dist/assets/{requirementDiagram-TGXJPOKE-CK0IuUmk.js → requirementDiagram-TGXJPOKE-CItsya2r.js} +1 -1
  462. package/ui-dist/assets/{sankeyDiagram-HTMAVEWB-pSfb3CqG.js → sankeyDiagram-HTMAVEWB-Cz9IHsZO.js} +1 -1
  463. package/ui-dist/assets/{sequenceDiagram-DBY2YBRQ-CbhkqIeq.js → sequenceDiagram-DBY2YBRQ-hMSqTiwK.js} +1 -1
  464. package/ui-dist/assets/{sizeCapture-X5ZJPWSS-B6vqqPx-.js → sizeCapture-X5ZJPWSS-0g9M5wMf.js} +1 -1
  465. package/ui-dist/assets/{stateDiagram-2N3HPSRC-DIJf9h5C.js → stateDiagram-2N3HPSRC-F8_YhgWl.js} +1 -1
  466. package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-fZImUBtq.js +1 -0
  467. package/ui-dist/assets/{swimlanes-5IMT3BWC-DIX_LV8O.js → swimlanes-5IMT3BWC-CCZqPbpE.js} +2 -2
  468. package/ui-dist/assets/swimlanesDiagram-G3AALYLV-9C1UT51R.js +8 -0
  469. package/ui-dist/assets/{timeline-definition-FHXFAJF6-CD-Pxb_g.js → timeline-definition-FHXFAJF6-BeUIr18x.js} +1 -1
  470. package/ui-dist/assets/{vennDiagram-L72KCM5P-BAZsWL0R.js → vennDiagram-L72KCM5P-D3d5h_G4.js} +1 -1
  471. package/ui-dist/assets/{wardleyDiagram-EHGQE667-CnNyfwX5.js → wardleyDiagram-EHGQE667-Bzt-As9A.js} +1 -1
  472. package/ui-dist/assets/{xychartDiagram-FW5EYKEG-j9YgsD3Y.js → xychartDiagram-FW5EYKEG-C2mly0xf.js} +1 -1
  473. package/ui-dist/fonts/InterVariable-Italic.woff2 +0 -0
  474. package/ui-dist/fonts/InterVariable.woff2 +0 -0
  475. package/ui-dist/fonts/NOTICE.md +17 -0
  476. package/ui-dist/index.html +2 -2
  477. package/ui-dist/locales/en/common.json +1142 -5
  478. package/ui-dist/locales/zh-CN/common.json +1140 -3
  479. package/ui-dist/assets/channel-CsiI2LYw.js +0 -1
  480. package/ui-dist/assets/classDiagram-OUVF2IWQ-CBn1Jqo8.js +0 -1
  481. package/ui-dist/assets/classDiagram-v2-EOCWNBFH-CBn1Jqo8.js +0 -1
  482. package/ui-dist/assets/index-BsL8K1vL.css +0 -1
  483. package/ui-dist/assets/index-D__fTsb2.js +0 -643
  484. package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-DpwLD7YL.js +0 -1
  485. package/ui-dist/assets/swimlanesDiagram-G3AALYLV-Cklauutg.js +0 -8
@@ -1,15 +1,15 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { Router } from "express";
3
3
  import multer from "multer";
4
4
  import { z } from "zod";
5
5
  import { and, asc, desc, eq, inArray, isNull, notInArray } from "drizzle-orm";
6
6
  import { activityLog, agents, documents, executionWorkspaces, heartbeatRuns, issueComments, issueDocuments, issueExecutionDecisions, issueRelations, issues as issueRows, issueWorkProducts, pipelineCaseIssueLinks, pipelineCases, pipelineStages, pipelines, projectWorkspaces, } from "@penclipai/db";
7
- import { addIssueCommentSchema, acceptIssueThreadInteractionSchema, attachmentArtifactWorkProductMetadataSchema, cancelIssueThreadInteractionSchema, companySearchQuerySchema, createIssueAttachmentMetadataSchema, createIssueThreadInteractionSchema, createIssueWorkProductSchema, createIssueLabelSchema, createAcceptedPlanDecompositionSchema, checkoutIssueSchema, createDocumentAnnotationCommentSchema, createDocumentAnnotationThreadSchema, createChildIssueSchema, createIssueSchema, resolveCreateIssueStatusDefault, resolveIssueRecoveryActionSchema, feedbackTargetTypeSchema, feedbackTraceStatusSchema, feedbackVoteValueSchema, upsertIssueFeedbackVoteSchema, upsertIssueWatchdogSchema, linkIssueApprovalSchema, issueDocumentKeySchema, ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, ISSUE_WATCHDOG_DISCOVERY_KINDS, TASK_WATCHDOG_PRODUCT_BUG_ORIGIN_KIND, rejectIssueThreadInteractionSchema, restoreIssueDocumentRevisionSchema, respondIssueThreadInteractionSchema, updateIssueWorkProductSchema, updateDocumentAnnotationThreadSchema, upsertIssueDocumentSchema, updateIssueSchema, getClosedIsolatedExecutionWorkspaceMessage, isClosedIsolatedExecutionWorkspace, isUuidLike, normalizeIssueIdentifier as normalizeIssueReferenceIdentifier, } from "@penclipai/shared";
7
+ import { addIssueCommentSchema, acceptIssueThreadInteractionSchema, attachmentArtifactWorkProductMetadataSchema, cancelIssueThreadInteractionSchema, companySearchQuerySchema, createIssueAttachmentMetadataSchema, createIssueThreadInteractionSchema, createIssueWorkProductSchema, createIssueLabelSchema, createAcceptedPlanDecompositionSchema, checkoutIssueSchema, createDocumentAnnotationCommentSchema, createDocumentAnnotationThreadSchema, createChildIssueSchema, createIssueSchema, resolveCreateIssueStatusDefault, resolveIssueRecoveryActionSchema, feedbackTargetTypeSchema, feedbackTraceStatusSchema, feedbackVoteValueSchema, upsertIssueFeedbackVoteSchema, upsertIssueWatchdogSchema, linkIssueApprovalSchema, issueDocumentKeySchema, ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, ISSUE_WATCHDOG_DISCOVERY_KINDS, TASK_WATCHDOG_PRODUCT_BUG_ORIGIN_KIND, rejectIssueThreadInteractionSchema, restoreIssueDocumentRevisionSchema, respondIssueThreadInteractionSchema, submitIssueThreadInteractionVerdictsSchema, updateIssueWorkProductSchema, updateDocumentAnnotationThreadSchema, upsertIssueDocumentSchema, updateIssueSchema, getClosedIsolatedExecutionWorkspaceMessage, isClosedIsolatedExecutionWorkspace, isUuidLike, normalizeIssueIdentifier as normalizeIssueReferenceIdentifier, } from "@penclipai/shared";
8
8
  import { trackAgentTaskCompleted } from "@penclipai/shared/telemetry";
9
9
  import { getTelemetryClient } from "../telemetry.js";
10
10
  import { validate } from "../middleware/validate.js";
11
11
  import * as serviceIndex from "../services/index.js";
12
- import { accessService, agentService, companyService, companySearchService, goalService, heartbeatService, issueApprovalService, issueRecoveryActionService, issueThreadInteractionService, ISSUE_LIST_DEFAULT_LIMIT, ISSUE_LIST_MAX_LIMIT, issueReferenceService, issueService, clampIssueListLimit, documentService, documentAnnotationService, logActivity, projectService, routineService, workProductService, } from "../services/index.js";
12
+ import { accessService, agentService, companySkillService, companyService, companySearchService, goalService, heartbeatService, issueApprovalService, issueRecoveryActionService, issueThreadInteractionService, ISSUE_LIST_DEFAULT_LIMIT, ISSUE_LIST_MAX_LIMIT, issueReferenceService, issueService, clampIssueListLimit, documentService, documentAnnotationService, logActivity, projectService, routineService, workProductService, } from "../services/index.js";
13
13
  import { buildPlanReviewContext } from "../services/plan-review-context.js";
14
14
  import { TASK_WATCHDOG_ORIGIN_KIND, resolveTaskWatchdogMutationScope, taskWatchdogScopeAllowsIssueMutation, } from "../services/task-watchdog-scope.js";
15
15
  import { logger } from "../middleware/logger.js";
@@ -24,7 +24,8 @@ import { assertEnvironmentSelectionForCompany } from "./environment-selection.js
24
24
  import { executionWorkspaceService as executionWorkspaceServiceDirect } from "../services/execution-workspaces.js";
25
25
  import { feedbackService } from "../services/feedback.js";
26
26
  import { instanceSettingsService } from "../services/instance-settings.js";
27
- import { readAcceptedPlanConfirmationTarget } from "../services/issues.js";
27
+ import { ISSUE_BLOCKER_DIAGNOSTICS_MAX_BLOCKERS, ISSUE_WAKE_DIAGNOSTICS_LOOKBACK_DAYS, ISSUE_WAKE_DIAGNOSTICS_MAX_ACTIVITY_RECORDS, ISSUE_WAKE_DIAGNOSTICS_MAX_WAKE_REQUESTS, readAcceptedPlanConfirmationTarget, } from "../services/issues.js";
28
+ import { authorizationDeniedDetails } from "../services/authorization.js";
28
29
  import { environmentService } from "../services/environments.js";
29
30
  import { environmentRuntimeService } from "../services/environment-runtime.js";
30
31
  import { redactSensitiveText } from "../redaction.js";
@@ -198,6 +199,67 @@ function readNonEmptyString(value) {
198
199
  function readObject(value) {
199
200
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
200
201
  }
202
+ function hasOwn(record, key) {
203
+ return Object.prototype.hasOwnProperty.call(record, key);
204
+ }
205
+ async function auditAgentIssueCreateAttributionSpoof(input) {
206
+ const actor = getActorInfo(input.req);
207
+ await logActivity(input.db, {
208
+ companyId: input.companyId,
209
+ actorType: actor.actorType,
210
+ actorId: actor.actorId,
211
+ agentId: actor.agentId,
212
+ runId: actor.runId,
213
+ action: input.action === "rejected"
214
+ ? "issue.attribution_spoof_rejected"
215
+ : "issue.attribution_spoof_stripped",
216
+ entityType: input.entityId ? "issue" : "company",
217
+ entityId: input.entityId ?? input.companyId,
218
+ details: {
219
+ surface: input.surface,
220
+ field: input.field,
221
+ requestedValue: input.requestedValue,
222
+ derivedFrom: "authenticated_actor",
223
+ },
224
+ });
225
+ }
226
+ async function sanitizeIssueCreateAttribution(db, req, res, companyId, input, options) {
227
+ const sanitized = { ...input };
228
+ if (req.actor.type !== "agent")
229
+ return sanitized;
230
+ if (hasOwn(sanitized, "responsibleUserId") && sanitized.responsibleUserId != null) {
231
+ await auditAgentIssueCreateAttributionSpoof({
232
+ db,
233
+ req,
234
+ companyId,
235
+ entityId: options.entityId,
236
+ surface: options.surface,
237
+ field: "responsibleUserId",
238
+ action: "rejected",
239
+ requestedValue: readNonEmptyString(sanitized.responsibleUserId),
240
+ });
241
+ res.status(422).json({ error: "Agent-created issues cannot set responsibleUserId" });
242
+ return null;
243
+ }
244
+ if (hasOwn(sanitized, "createdByUserId") && sanitized.createdByUserId != null) {
245
+ await auditAgentIssueCreateAttributionSpoof({
246
+ db,
247
+ req,
248
+ companyId,
249
+ entityId: options.entityId,
250
+ surface: options.surface,
251
+ field: "createdByUserId",
252
+ action: "stripped",
253
+ requestedValue: readNonEmptyString(sanitized.createdByUserId),
254
+ });
255
+ delete sanitized.createdByUserId;
256
+ }
257
+ delete sanitized.responsibleUserId;
258
+ return sanitized;
259
+ }
260
+ function authenticatedActorResponsibleUserId(req) {
261
+ return req.actor.type === "agent" ? req.actor.onBehalfOfUserId ?? null : null;
262
+ }
201
263
  function readPlanConfirmationTargetForIssue(payload, issueId) {
202
264
  const target = readObject(readObject(payload).target);
203
265
  if (target.type !== "issue_document" || target.key !== "plan")
@@ -250,7 +312,7 @@ function emptyWorkspaceNameMaps() {
250
312
  };
251
313
  }
252
314
  function summarizeIssueWorkspaceForActivity(issue, names) {
253
- const settings = parseIssueExecutionWorkspaceSettings(issue.executionWorkspaceSettings);
315
+ const settings = parseIssueExecutionWorkspaceSettings(issue.executionWorkspaceSettings, { includeEnvironmentId: true });
254
316
  const mode = settings?.mode ?? issue.executionWorkspacePreference ?? null;
255
317
  const executionWorkspaceId = issue.executionWorkspaceId ?? null;
256
318
  const projectWorkspaceId = issue.projectWorkspaceId ?? null;
@@ -411,6 +473,469 @@ function withRecoveryActionsOnRelationSummaries(relations, recoveryActionByIssue
411
473
  blocks: relations.blocks.map(augment),
412
474
  };
413
475
  }
476
+ function toIssueBlockerDiagnosticSummary(issue) {
477
+ return {
478
+ id: issue.id,
479
+ identifier: issue.identifier,
480
+ title: issue.title,
481
+ status: issue.status,
482
+ priority: issue.priority,
483
+ assigneeAgentId: issue.assigneeAgentId,
484
+ assigneeUserId: issue.assigneeUserId,
485
+ };
486
+ }
487
+ function blockerDiagnosticLabel(issue) {
488
+ return issue.identifier ?? issue.title;
489
+ }
490
+ function buildIssueBlockerDiagnosticsResponse(input) {
491
+ const issue = toIssueBlockerDiagnosticSummary(input.issue);
492
+ const visibleBlockerIds = new Set(input.visibleBlockers.map((blocker) => blocker.id));
493
+ const omittedUnauthorizedBlockerCount = input.blockers.filter((blocker) => !visibleBlockerIds.has(blocker.id)).length;
494
+ const completeVisibleSet = !input.truncated && omittedUnauthorizedBlockerCount === 0;
495
+ const unresolvedIds = new Set(input.readiness.unresolvedBlockerIssueIds);
496
+ const pendingFinalizeIds = new Set(input.readiness.pendingFinalizeBlockerIssueIds);
497
+ const blockers = input.visibleBlockers.map((blockerRow) => {
498
+ const blocker = toIssueBlockerDiagnosticSummary(blockerRow);
499
+ const isPendingFinalize = pendingFinalizeIds.has(blocker.id);
500
+ const isUnresolved = unresolvedIds.has(blocker.id);
501
+ const flags = [];
502
+ if (issue.status === "blocked" && blocker.status === "done")
503
+ flags.push("done_but_blocking");
504
+ if (blocker.status === "cancelled")
505
+ flags.push("cancelled_blocker_in_set");
506
+ if (isPendingFinalize)
507
+ flags.push("workspace_finalize_pending");
508
+ return {
509
+ ...blocker,
510
+ isUnresolved,
511
+ isPendingFinalize,
512
+ isDependencyReady: blocker.status === "done" && !isPendingFinalize,
513
+ flags,
514
+ };
515
+ });
516
+ const readiness = completeVisibleSet
517
+ ? {
518
+ allBlockersDone: input.readiness.allBlockersDone,
519
+ isDependencyReady: input.readiness.isDependencyReady,
520
+ unresolvedBlockerCount: input.readiness.unresolvedBlockerIssueIds.length,
521
+ pendingFinalizeBlockerCount: input.readiness.pendingFinalizeBlockerIssueIds.length,
522
+ }
523
+ : null;
524
+ const reportedOmittedUnauthorizedBlockerCount = input.truncated
525
+ ? null
526
+ : omittedUnauthorizedBlockerCount;
527
+ return {
528
+ issue,
529
+ diagnosis: buildIssueBlockerDiagnosis({
530
+ issue,
531
+ blockers,
532
+ readiness,
533
+ omittedUnauthorizedBlockerCount: reportedOmittedUnauthorizedBlockerCount,
534
+ truncated: input.truncated,
535
+ maxBlockers: input.maxBlockers ?? ISSUE_BLOCKER_DIAGNOSTICS_MAX_BLOCKERS,
536
+ }),
537
+ readiness,
538
+ blockers,
539
+ omittedUnauthorizedBlockerCount: reportedOmittedUnauthorizedBlockerCount,
540
+ truncated: input.truncated,
541
+ caps: {
542
+ maxBlockers: input.maxBlockers ?? ISSUE_BLOCKER_DIAGNOSTICS_MAX_BLOCKERS,
543
+ },
544
+ };
545
+ }
546
+ function buildIssueBlockerDiagnosis(input) {
547
+ if (input.truncated) {
548
+ return `Blocker diagnostics for ${blockerDiagnosticLabel(input.issue)} are truncated at ${input.maxBlockers} blockers, so readiness is not reported.`;
549
+ }
550
+ const omittedUnauthorizedBlockerCount = input.omittedUnauthorizedBlockerCount ?? 0;
551
+ if (omittedUnauthorizedBlockerCount > 0) {
552
+ return `One or more blockers for ${blockerDiagnosticLabel(input.issue)} are outside this actor's authorization boundary, so this diagnosis only covers visible blockers.`;
553
+ }
554
+ if (input.blockers.length === 0) {
555
+ return input.issue.status === "blocked"
556
+ ? `${blockerDiagnosticLabel(input.issue)} is blocked but has no first-class blocker relations.`
557
+ : null;
558
+ }
559
+ const pendingFinalize = input.blockers.find((blocker) => blocker.isPendingFinalize);
560
+ if (pendingFinalize) {
561
+ return `${blockerDiagnosticLabel(input.issue)} is waiting for ${blockerDiagnosticLabel(pendingFinalize)} to finish workspace finalization.`;
562
+ }
563
+ const cancelled = input.blockers.find((blocker) => blocker.status === "cancelled");
564
+ if (cancelled) {
565
+ return `${blockerDiagnosticLabel(input.issue)} is blocked by ${blockerDiagnosticLabel(cancelled)}, which is cancelled; cancelled blockers do not resolve until the blocker relation is removed or replaced.`;
566
+ }
567
+ const unresolved = input.blockers.find((blocker) => blocker.isUnresolved);
568
+ if (unresolved) {
569
+ return `${blockerDiagnosticLabel(input.issue)} is blocked by ${blockerDiagnosticLabel(unresolved)}, which is ${unresolved.status}.`;
570
+ }
571
+ if (input.readiness?.isDependencyReady && input.issue.status === "blocked") {
572
+ return `All blockers for ${blockerDiagnosticLabel(input.issue)} are resolved, but the issue is still blocked; this is likely a stale blocker hold.`;
573
+ }
574
+ if (input.readiness?.isDependencyReady) {
575
+ return `All blockers for ${blockerDiagnosticLabel(input.issue)} are resolved.`;
576
+ }
577
+ return null;
578
+ }
579
+ const ISSUE_WAKE_DIAGNOSTIC_KNOWN_SOURCES = new Set([
580
+ "timer",
581
+ "assignment",
582
+ "on_demand",
583
+ "automation",
584
+ ]);
585
+ const ISSUE_WAKE_DIAGNOSTIC_KNOWN_REASONS = new Set([
586
+ "issue_assigned",
587
+ "issue_blockers_resolved",
588
+ "issue_commented",
589
+ "issue_comment_mentioned",
590
+ "issue_dependencies_blocked",
591
+ "issue_tree_hold_active",
592
+ "missing_issue_comment",
593
+ "process_lost_retry",
594
+ "run_liveness_continuation",
595
+ "heartbeat.disabled",
596
+ "heartbeat.timer.no_actionable_work",
597
+ "heartbeat.wakeOnDemand.disabled",
598
+ ]);
599
+ const ISSUE_WAKE_DIAGNOSTIC_KNOWN_STATUSES = new Set([
600
+ "queued",
601
+ "claimed",
602
+ "coalesced",
603
+ "skipped",
604
+ "completed",
605
+ "failed",
606
+ "cancelled",
607
+ "deferred_issue_execution",
608
+ ]);
609
+ function dateToIso(value) {
610
+ if (!value)
611
+ return null;
612
+ return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
613
+ }
614
+ function projectWakeDiagnosticSource(value) {
615
+ if (!value)
616
+ return null;
617
+ return ISSUE_WAKE_DIAGNOSTIC_KNOWN_SOURCES.has(value) ? value : "other";
618
+ }
619
+ function projectWakeDiagnosticReason(value) {
620
+ if (!value)
621
+ return null;
622
+ return ISSUE_WAKE_DIAGNOSTIC_KNOWN_REASONS.has(value) ? value : "other";
623
+ }
624
+ function projectWakeDiagnosticStatus(value) {
625
+ return ISSUE_WAKE_DIAGNOSTIC_KNOWN_STATUSES.has(value) ? value : "other";
626
+ }
627
+ function wakeFailureClass(status, rawError) {
628
+ if (status === "failed" || rawError)
629
+ return "failed";
630
+ if (status === "cancelled")
631
+ return "cancelled";
632
+ if (status === "skipped")
633
+ return "skipped";
634
+ return null;
635
+ }
636
+ function projectIssueWakeRequest(row, options) {
637
+ const status = projectWakeDiagnosticStatus(row.status);
638
+ return {
639
+ kind: "wake_request",
640
+ agentId: options.includeInternalIds ? row.agentId : null,
641
+ source: projectWakeDiagnosticSource(row.source) ?? "other",
642
+ reason: projectWakeDiagnosticReason(row.reason),
643
+ status,
644
+ coalescedCount: row.coalescedCount,
645
+ runId: options.includeInternalIds ? row.runId : null,
646
+ requestedAt: dateToIso(row.requestedAt),
647
+ claimedAt: dateToIso(row.claimedAt),
648
+ finishedAt: dateToIso(row.finishedAt),
649
+ failureClass: wakeFailureClass(status, row.error),
650
+ };
651
+ }
652
+ function wakeDiagnosticActivityAction(action) {
653
+ return action === "issue.tree_hold_wakeup_deferred" ? action : "other";
654
+ }
655
+ function wakeDiagnosticActivityEntityType(entityType) {
656
+ return entityType === "issue" || entityType === "agent_wakeup_request" ? entityType : "other";
657
+ }
658
+ function projectIssueWakeActivityRecord(row, issueId, options) {
659
+ const details = row.details && typeof row.details === "object" ? row.details : {};
660
+ const action = wakeDiagnosticActivityAction(row.action);
661
+ const rootIssueId = readNonEmptyString(details["rootIssueId"]);
662
+ const detailIssueId = readNonEmptyString(details["issueId"]);
663
+ const projectedRootIssueId = rootIssueId === issueId || detailIssueId === issueId || (row.entityType === "issue" && row.entityId === issueId)
664
+ ? issueId
665
+ : null;
666
+ return {
667
+ kind: "activity",
668
+ action,
669
+ entityType: wakeDiagnosticActivityEntityType(row.entityType),
670
+ agentId: options.includeInternalIds ? row.agentId ?? readNonEmptyString(details["agentId"]) : null,
671
+ runId: options.includeInternalIds ? row.runId : null,
672
+ createdAt: dateToIso(row.createdAt),
673
+ source: projectWakeDiagnosticSource(readNonEmptyString(details["source"])),
674
+ requestedReason: projectWakeDiagnosticReason(readNonEmptyString(details["requestedReason"])),
675
+ previousReason: projectWakeDiagnosticReason(readNonEmptyString(details["previousReason"])),
676
+ rootIssueId: projectedRootIssueId,
677
+ holdId: options.includeInternalIds ? readNonEmptyString(details["holdId"]) : null,
678
+ summary: action === "issue.tree_hold_wakeup_deferred"
679
+ ? "Wake was deferred because an active issue-tree hold was present."
680
+ : "Wake-related activity was recorded.",
681
+ };
682
+ }
683
+ function issueWakeDiagnosticEventTimestamp(event) {
684
+ const timestamp = event.kind === "wake_request" ? event.requestedAt : event.createdAt;
685
+ return new Date(timestamp).getTime();
686
+ }
687
+ function wakeDiagnosticReasonPhrase(reason) {
688
+ return reason ? ` for ${reason}` : "";
689
+ }
690
+ function buildIssueWakeDiagnosis(input) {
691
+ if (input.truncated) {
692
+ return `Wake diagnostics for ${blockerDiagnosticLabel(input.issue)} are truncated to ${input.maxWakeRequests} wake requests and ${input.maxActivityRecords} activity records over ${input.lookbackDays} days, so the diagnosis only covers returned records.`;
693
+ }
694
+ const latest = input.events[0];
695
+ if (latest?.kind === "activity" && latest.action === "issue.tree_hold_wakeup_deferred") {
696
+ return `The most recent wake-related activity for ${blockerDiagnosticLabel(input.issue)} was deferred by an active issue-tree hold.`;
697
+ }
698
+ if (latest?.kind === "wake_request") {
699
+ if (latest.status === "deferred_issue_execution") {
700
+ return `The most recent wake for ${blockerDiagnosticLabel(input.issue)} is deferred${wakeDiagnosticReasonPhrase(latest.reason)}.`;
701
+ }
702
+ if (latest.status === "failed") {
703
+ return `The most recent wake for ${blockerDiagnosticLabel(input.issue)} failed${wakeDiagnosticReasonPhrase(latest.reason)}; raw error text is withheld.`;
704
+ }
705
+ if (latest.status === "skipped" || latest.status === "cancelled" || latest.status === "coalesced") {
706
+ const coalesced = latest.coalescedCount > 0 ? ` and coalesced ${latest.coalescedCount} additional request(s)` : "";
707
+ return `The most recent wake for ${blockerDiagnosticLabel(input.issue)} was ${latest.status}${wakeDiagnosticReasonPhrase(latest.reason)}${coalesced}.`;
708
+ }
709
+ if (latest.status === "queued" || latest.status === "claimed") {
710
+ return `The most recent wake for ${blockerDiagnosticLabel(input.issue)} is currently ${latest.status}${wakeDiagnosticReasonPhrase(latest.reason)}.`;
711
+ }
712
+ if (latest.status === "completed") {
713
+ return `The most recent wake for ${blockerDiagnosticLabel(input.issue)} completed${wakeDiagnosticReasonPhrase(latest.reason)}.`;
714
+ }
715
+ }
716
+ if (input.events.length > 0)
717
+ return null;
718
+ const blockerDiagnostics = input.blockerDiagnostics;
719
+ if (blockerDiagnostics.truncated) {
720
+ return `No wake rows are visible for ${blockerDiagnosticLabel(input.issue)} in the bounded window, and blocker diagnostics are truncated, so no wake cause is inferred.`;
721
+ }
722
+ if ((blockerDiagnostics.omittedUnauthorizedBlockerCount ?? 0) > 0) {
723
+ return `No wake rows are visible for ${blockerDiagnosticLabel(input.issue)} in the bounded window, and one or more blockers are outside this actor's authorization boundary.`;
724
+ }
725
+ if (input.issue.status !== "blocked" || blockerDiagnostics.blockers.length === 0)
726
+ return null;
727
+ const pendingFinalize = blockerDiagnostics.blockers.find((blocker) => blocker.isPendingFinalize);
728
+ if (pendingFinalize) {
729
+ return `No wake row exists for ${blockerDiagnosticLabel(input.issue)} in the bounded window. ${blockerDiagnosticLabel(input.issue)} is waiting for ${blockerDiagnosticLabel(pendingFinalize)} to finish workspace finalization, so issue_blockers_resolved has not fired.`;
730
+ }
731
+ const cancelled = blockerDiagnostics.blockers.find((blocker) => blocker.status === "cancelled");
732
+ if (cancelled) {
733
+ return `No wake row exists for ${blockerDiagnosticLabel(input.issue)} in the bounded window. ${blockerDiagnosticLabel(input.issue)} is blocked by ${blockerDiagnosticLabel(cancelled)}, which is cancelled; cancelled blockers do not fire issue_blockers_resolved.`;
734
+ }
735
+ const unresolved = blockerDiagnostics.blockers.find((blocker) => blocker.isUnresolved);
736
+ if (unresolved) {
737
+ return `No wake row exists for ${blockerDiagnosticLabel(input.issue)} in the bounded window. ${blockerDiagnosticLabel(input.issue)} is blocked by ${blockerDiagnosticLabel(unresolved)}, which is ${unresolved.status}, so issue_blockers_resolved has not fired.`;
738
+ }
739
+ if (blockerDiagnostics.readiness?.isDependencyReady) {
740
+ return `No wake row exists for ${blockerDiagnosticLabel(input.issue)} in the bounded window. All visible blockers are resolved, but the issue is still blocked; this is likely a stale blocker hold or an older wake outside the lookback window.`;
741
+ }
742
+ return null;
743
+ }
744
+ function buildIssueWakeDiagnosticsResponse(input) {
745
+ const issue = toIssueBlockerDiagnosticSummary(input.issue);
746
+ const events = [
747
+ ...input.wakeRequests.map((record) => projectIssueWakeRequest(record, { includeInternalIds: input.includeInternalIds })),
748
+ ...input.activityRecords.map((record) => projectIssueWakeActivityRecord(record, issue.id, { includeInternalIds: input.includeInternalIds })),
749
+ ].sort((left, right) => issueWakeDiagnosticEventTimestamp(right) - issueWakeDiagnosticEventTimestamp(left));
750
+ const truncated = input.truncatedWakeRequests || input.truncatedActivityRecords;
751
+ const maxWakeRequests = input.maxWakeRequests ?? ISSUE_WAKE_DIAGNOSTICS_MAX_WAKE_REQUESTS;
752
+ const maxActivityRecords = input.maxActivityRecords ?? ISSUE_WAKE_DIAGNOSTICS_MAX_ACTIVITY_RECORDS;
753
+ const lookbackDays = input.lookbackDays ?? ISSUE_WAKE_DIAGNOSTICS_LOOKBACK_DAYS;
754
+ const diagnosis = buildIssueWakeDiagnosis({
755
+ issue,
756
+ events,
757
+ blockerDiagnostics: input.blockerDiagnostics,
758
+ truncated,
759
+ maxWakeRequests,
760
+ maxActivityRecords,
761
+ lookbackDays,
762
+ });
763
+ return {
764
+ issue,
765
+ diagnosis,
766
+ likelyReason: diagnosis,
767
+ events,
768
+ wakeRequestCount: input.wakeRequests.length,
769
+ activityRecordCount: input.activityRecords.length,
770
+ truncated,
771
+ truncatedSections: {
772
+ wakeRequests: input.truncatedWakeRequests,
773
+ activityRecords: input.truncatedActivityRecords,
774
+ },
775
+ caps: {
776
+ maxWakeRequests,
777
+ maxActivityRecords,
778
+ lookbackDays,
779
+ },
780
+ };
781
+ }
782
+ function groupByIssueId(rows) {
783
+ const map = new Map();
784
+ for (const row of rows) {
785
+ const issueRows = map.get(row.issueId) ?? [];
786
+ issueRows.push(row);
787
+ map.set(row.issueId, issueRows);
788
+ }
789
+ return map;
790
+ }
791
+ function groupBlockersByBlockedIssueId(rows) {
792
+ const map = new Map();
793
+ for (const row of rows) {
794
+ const issueRows = map.get(row.blockedIssueId) ?? [];
795
+ issueRows.push(row);
796
+ map.set(row.blockedIssueId, issueRows);
797
+ }
798
+ return map;
799
+ }
800
+ function issueSubtreeEdgeTimestamp(edge) {
801
+ return edge.timestamp ? new Date(edge.timestamp).getTime() : 0;
802
+ }
803
+ function buildIssueSubtreeDiagnosis(input) {
804
+ if (input.truncated) {
805
+ return `Subtree diagnostics for ${blockerDiagnosticLabel(input.issue)} are bounded to depth ${input.caps.maxDepth} and ${input.caps.maxNodes} nodes, so the diagnosis only covers returned visible nodes.`;
806
+ }
807
+ if ((input.omittedUnauthorizedNodeCount ?? 0) > 0) {
808
+ return `One or more subtree nodes under ${blockerDiagnosticLabel(input.issue)} are outside this actor's authorization boundary, so this diagnosis only covers visible nodes.`;
809
+ }
810
+ const blockedNodeWithDiagnosis = input.nodes.find((node) => node.issue.status === "blocked" && node.diagnosis);
811
+ const firstNodeWithDiagnosis = blockedNodeWithDiagnosis ?? input.nodes.find((node) => node.diagnosis);
812
+ if (!firstNodeWithDiagnosis?.diagnosis)
813
+ return null;
814
+ return `${blockerDiagnosticLabel(firstNodeWithDiagnosis.issue)} appears to be the subtree stall point: ${firstNodeWithDiagnosis.diagnosis}`;
815
+ }
816
+ function buildIssueSubtreeDiagnosticsResponse(input) {
817
+ const issue = toIssueBlockerDiagnosticSummary(input.issue);
818
+ const visibleNodeIds = new Set(input.visibleNodes.map((node) => node.id));
819
+ const visibleBlockerIdsByIssueId = groupBlockersByBlockedIssueId(input.visibleBlockers);
820
+ const omittedUnauthorizedNodeCount = input.truncatedNodes || input.truncatedDepth
821
+ ? null
822
+ : input.nodes.filter((node) => !visibleNodeIds.has(node.id)).length;
823
+ const nodeResponses = [];
824
+ const edges = [];
825
+ for (const node of input.visibleNodes) {
826
+ const rawBlockers = input.blockersByIssueId.get(node.id) ?? [];
827
+ const visibleBlockers = visibleBlockerIdsByIssueId.get(node.id) ?? [];
828
+ const blockerResponse = buildIssueBlockerDiagnosticsResponse({
829
+ issue: node,
830
+ blockers: rawBlockers,
831
+ visibleBlockers,
832
+ readiness: input.readinessByIssueId.get(node.id) ?? {
833
+ allBlockersDone: true,
834
+ isDependencyReady: true,
835
+ unresolvedBlockerIssueIds: [],
836
+ pendingFinalizeBlockerIssueIds: [],
837
+ },
838
+ truncated: input.truncatedBlockerIssueIds.has(node.id),
839
+ maxBlockers: input.caps.maxBlockersPerNode,
840
+ });
841
+ const wakeResponse = buildIssueWakeDiagnosticsResponse({
842
+ issue: node,
843
+ wakeRequests: input.wakeRequestsByIssueId.get(node.id) ?? [],
844
+ activityRecords: input.activityRecordsByIssueId.get(node.id) ?? [],
845
+ blockerDiagnostics: blockerResponse,
846
+ truncatedWakeRequests: input.truncatedWakeIssueIds.has(node.id),
847
+ truncatedActivityRecords: input.truncatedActivityIssueIds.has(node.id),
848
+ includeInternalIds: input.includeInternalIds,
849
+ maxWakeRequests: input.caps.maxWakeRequestsPerNode,
850
+ maxActivityRecords: input.caps.maxActivityRecordsPerNode,
851
+ lookbackDays: input.caps.lookbackDays,
852
+ });
853
+ const nodeDiagnosis = wakeResponse.diagnosis ?? blockerResponse.diagnosis;
854
+ if (node.parentId && visibleNodeIds.has(node.parentId)) {
855
+ edges.push({
856
+ kind: "parent",
857
+ fromIssueId: node.parentId,
858
+ toIssueId: node.id,
859
+ timestamp: dateToIso(node.createdAt),
860
+ });
861
+ }
862
+ for (const blocker of visibleBlockers) {
863
+ edges.push({
864
+ kind: "blocks",
865
+ fromIssueId: blocker.id,
866
+ toIssueId: node.id,
867
+ timestamp: dateToIso(blocker.relationCreatedAt),
868
+ });
869
+ }
870
+ for (const event of wakeResponse.events) {
871
+ if (event.kind === "wake_request") {
872
+ edges.push({
873
+ kind: "wake_request",
874
+ issueId: node.id,
875
+ agentId: event.agentId,
876
+ reason: event.reason,
877
+ status: event.status,
878
+ timestamp: event.requestedAt,
879
+ });
880
+ }
881
+ else {
882
+ edges.push({
883
+ kind: "activity",
884
+ issueId: node.id,
885
+ action: event.action,
886
+ timestamp: event.createdAt,
887
+ });
888
+ }
889
+ }
890
+ nodeResponses.push({
891
+ issue: toIssueBlockerDiagnosticSummary(node),
892
+ parentId: node.parentId && visibleNodeIds.has(node.parentId) ? node.parentId : null,
893
+ depth: node.depth,
894
+ diagnosis: nodeDiagnosis,
895
+ likelyReason: nodeDiagnosis,
896
+ blockers: blockerResponse.blockers,
897
+ blockerReadiness: blockerResponse.readiness,
898
+ omittedUnauthorizedBlockerCount: blockerResponse.omittedUnauthorizedBlockerCount,
899
+ wakeEvents: wakeResponse.events,
900
+ wakeRequestCount: wakeResponse.wakeRequestCount,
901
+ activityRecordCount: wakeResponse.activityRecordCount,
902
+ truncated: blockerResponse.truncated || wakeResponse.truncated,
903
+ truncatedSections: {
904
+ blockers: blockerResponse.truncated,
905
+ wakeRequests: wakeResponse.truncatedSections.wakeRequests,
906
+ activityRecords: wakeResponse.truncatedSections.activityRecords,
907
+ },
908
+ });
909
+ }
910
+ edges.sort((left, right) => issueSubtreeEdgeTimestamp(right) - issueSubtreeEdgeTimestamp(left));
911
+ const truncatedSections = {
912
+ nodes: input.truncatedNodes,
913
+ depth: input.truncatedDepth,
914
+ blockers: input.truncatedBlockerIssueIds.size > 0,
915
+ wakeRequests: input.truncatedWakeIssueIds.size > 0,
916
+ activityRecords: input.truncatedActivityIssueIds.size > 0,
917
+ };
918
+ const truncated = Object.values(truncatedSections).some(Boolean);
919
+ const diagnosis = buildIssueSubtreeDiagnosis({
920
+ issue,
921
+ nodes: nodeResponses,
922
+ omittedUnauthorizedNodeCount,
923
+ truncated,
924
+ caps: input.caps,
925
+ });
926
+ return {
927
+ issue,
928
+ diagnosis,
929
+ likelyReason: diagnosis,
930
+ nodes: nodeResponses,
931
+ edges,
932
+ nodeCount: nodeResponses.length,
933
+ omittedUnauthorizedNodeCount,
934
+ truncated,
935
+ truncatedSections,
936
+ caps: input.caps,
937
+ };
938
+ }
414
939
  const ACTIVE_REVIEW_APPROVAL_STATUSES = new Set(["pending", "revision_requested"]);
415
940
  const INVALID_AGENT_IN_REVIEW_DISPOSITION_MESSAGE = "invalid_issue_disposition: Agent-authored updates that move an issue to in_review must include a real review path. " +
416
941
  "This request would leave the issue in_review without anyone or anything owning the next action. " +
@@ -513,7 +1038,7 @@ async function assertCanManageIssueMonitor(accessSvc, req, companyId, assigneeAg
513
1038
  resource: { type: "company", companyId },
514
1039
  });
515
1040
  if (!runtimeDecision.allowed) {
516
- throw forbidden(runtimeDecision.explanation);
1041
+ throw forbidden(runtimeDecision.explanation, authorizationDeniedDetails(runtimeDecision));
517
1042
  }
518
1043
  if (req.actor.type === "agent" && req.actor.agentId && req.actor.agentId === assigneeAgentId)
519
1044
  return;
@@ -601,6 +1126,12 @@ function isAssigneeSelfCommentOnTerminalIssue(input) {
601
1126
  return false;
602
1127
  return input.actorId === input.assigneeAgentId;
603
1128
  }
1129
+ const REQUEST_ITEM_VERDICTS_WAKE_COALESCE_WINDOW_MS = 2_000;
1130
+ function buildRequestItemVerdictsWakeIdempotencyKey(args) {
1131
+ const now = args.at ?? new Date();
1132
+ const bucket = Math.floor(now.getTime() / REQUEST_ITEM_VERDICTS_WAKE_COALESCE_WINDOW_MS);
1133
+ return `request_item_verdicts:${args.issueId}:${args.interactionId}:${bucket}`;
1134
+ }
604
1135
  function queueResolvedInteractionContinuationWakeup(input) {
605
1136
  if (input.interaction.continuationPolicy !== "wake_assignee"
606
1137
  && input.interaction.continuationPolicy !== "wake_assignee_on_accept")
@@ -617,6 +1148,13 @@ function queueResolvedInteractionContinuationWakeup(input) {
617
1148
  const planTarget = readPlanConfirmationTargetForIssue(input.interaction.payload, input.issue.id);
618
1149
  const interactionResult = readConfirmationResultForWake(input.interaction.result);
619
1150
  const checkboxSelection = readCheckboxSelectionForWake(input.interaction);
1151
+ const newlyResolvedItemIds = input.newlyResolvedItemIds?.filter((value) => value.length > 0) ?? [];
1152
+ const itemVerdicts = newlyResolvedItemIds.length > 0
1153
+ ? {
1154
+ newlyResolvedItemIds,
1155
+ coalesceWindowMs: REQUEST_ITEM_VERDICTS_WAKE_COALESCE_WINDOW_MS,
1156
+ }
1157
+ : null;
620
1158
  const planReviewInteraction = planTarget && input.interaction.kind === "request_confirmation"
621
1159
  ? {
622
1160
  id: input.interaction.id,
@@ -640,8 +1178,10 @@ function queueResolvedInteractionContinuationWakeup(input) {
640
1178
  sourceRunId: input.interaction.sourceRunId ?? null,
641
1179
  ...(planReviewInteraction ? { planReviewInteraction } : {}),
642
1180
  ...(checkboxSelection ? { checkboxSelection } : {}),
1181
+ ...(itemVerdicts ? { itemVerdicts, newlyResolvedItemIds } : {}),
643
1182
  mutation: "interaction",
644
1183
  },
1184
+ idempotencyKey: input.idempotencyKey ?? null,
645
1185
  requestedByActorType: input.actor.actorType,
646
1186
  requestedByActorId: input.actor.actorId,
647
1187
  contextSnapshot: {
@@ -654,6 +1194,7 @@ function queueResolvedInteractionContinuationWakeup(input) {
654
1194
  sourceRunId: input.interaction.sourceRunId ?? null,
655
1195
  ...(planReviewInteraction ? { planReviewInteraction } : {}),
656
1196
  ...(checkboxSelection ? { checkboxSelection } : {}),
1197
+ ...(itemVerdicts ? { itemVerdicts, newlyResolvedItemIds } : {}),
657
1198
  wakeReason: "issue_commented",
658
1199
  source: input.source,
659
1200
  ...(forceFreshSession ? { forceFreshSession: true } : {}),
@@ -802,6 +1343,329 @@ class AutoApprovalIssueMissingError extends Error {
802
1343
  this.name = "AutoApprovalIssueMissingError";
803
1344
  }
804
1345
  }
1346
+ function toCompactIssue(issue) {
1347
+ return {
1348
+ id: issue.id,
1349
+ companyId: issue.companyId,
1350
+ projectId: issue.projectId,
1351
+ projectWorkspaceId: issue.projectWorkspaceId,
1352
+ goalId: issue.goalId,
1353
+ parentId: issue.parentId,
1354
+ title: issue.title,
1355
+ description: issue.description,
1356
+ status: issue.status,
1357
+ workMode: issue.workMode,
1358
+ priority: issue.priority,
1359
+ assigneeAgentId: issue.assigneeAgentId,
1360
+ assigneeUserId: issue.assigneeUserId,
1361
+ checkoutRunId: issue.checkoutRunId,
1362
+ executionRunId: issue.executionRunId,
1363
+ executionAgentNameKey: issue.executionAgentNameKey,
1364
+ executionLockedAt: issue.executionLockedAt,
1365
+ createdByAgentId: issue.createdByAgentId,
1366
+ createdByUserId: issue.createdByUserId,
1367
+ issueNumber: issue.issueNumber,
1368
+ identifier: issue.identifier,
1369
+ originKind: issue.originKind,
1370
+ originId: issue.originId,
1371
+ originRunId: issue.originRunId,
1372
+ requestDepth: issue.requestDepth,
1373
+ billingCode: issue.billingCode,
1374
+ executionWorkspaceId: issue.executionWorkspaceId,
1375
+ startedAt: issue.startedAt,
1376
+ completedAt: issue.completedAt,
1377
+ cancelledAt: issue.cancelledAt,
1378
+ createdAt: issue.createdAt,
1379
+ updatedAt: issue.updatedAt,
1380
+ ...(issue.labelIds ? { labelIds: issue.labelIds } : {}),
1381
+ ...(issue.labels ? { labels: issue.labels } : {}),
1382
+ ...(issue.blockedBy ? { blockedBy: issue.blockedBy } : {}),
1383
+ ...(issue.blockerAttention ? { blockerAttention: issue.blockerAttention } : {}),
1384
+ ...(issue.blockedInboxAttention !== undefined ? { blockedInboxAttention: issue.blockedInboxAttention } : {}),
1385
+ ...(issue.productivityReview ? { productivityReview: issue.productivityReview } : {}),
1386
+ ...(issue.scheduledRetry ? { scheduledRetry: issue.scheduledRetry } : {}),
1387
+ ...(issue.liveDescendantCount !== undefined ? { liveDescendantCount: issue.liveDescendantCount } : {}),
1388
+ ...(issue.myLastTouchAt !== undefined ? { myLastTouchAt: issue.myLastTouchAt } : {}),
1389
+ ...(issue.lastExternalCommentAt !== undefined ? { lastExternalCommentAt: issue.lastExternalCommentAt } : {}),
1390
+ ...(issue.lastActivityAt !== undefined ? { lastActivityAt: issue.lastActivityAt } : {}),
1391
+ ...(issue.isUnreadForMe !== undefined ? { isUnreadForMe: issue.isUnreadForMe } : {}),
1392
+ activeRecoveryAction: issue.activeRecoveryAction ?? null,
1393
+ successfulRunHandoff: issue.successfulRunHandoff ?? null,
1394
+ };
1395
+ }
1396
+ function compactIssueListEtag(issues) {
1397
+ const hash = createHash("sha256")
1398
+ .update(JSON.stringify(issues))
1399
+ .digest("base64url");
1400
+ return `"compact-issues:${hash}"`;
1401
+ }
1402
+ function requestMatchesEtag(ifNoneMatchHeader, etag) {
1403
+ if (!ifNoneMatchHeader)
1404
+ return false;
1405
+ return ifNoneMatchHeader
1406
+ .split(",")
1407
+ .map((value) => value.trim())
1408
+ .some((value) => value === "*" || value === etag || value === `W/${etag}`);
1409
+ }
1410
+ const ISSUE_LIST_SERVER_CACHE_TTL_MS = 2_000;
1411
+ const ISSUE_LIST_SERVER_CACHE_STALE_MS = 5_000;
1412
+ export const ISSUE_LIST_SERVER_CACHE_MAX_ENTRIES = 256;
1413
+ const ISSUE_LIST_STORM_WINDOW_MS = 500;
1414
+ const ISSUE_LIST_STORM_THRESHOLD = 4;
1415
+ const ISSUE_LIST_MAX_ACTOR_CLIENT_INFLIGHT = 8;
1416
+ const issueListResponseCache = new Map();
1417
+ const issueListInflight = new Map();
1418
+ const issueListActorClientInflight = new Map();
1419
+ export function __getIssueListResponseCacheSizeForTests() {
1420
+ return issueListResponseCache.size;
1421
+ }
1422
+ export function __clearIssueListResponseCacheForTests() {
1423
+ issueListResponseCache.clear();
1424
+ }
1425
+ function shortHash(value) {
1426
+ return createHash("sha256").update(value).digest("base64url").slice(0, 16);
1427
+ }
1428
+ function stableJson(value) {
1429
+ if (value === null || typeof value !== "object")
1430
+ return JSON.stringify(value);
1431
+ if (Array.isArray(value))
1432
+ return `[${value.map(stableJson).join(",")}]`;
1433
+ const record = value;
1434
+ return `{${Object.keys(record)
1435
+ .sort()
1436
+ .filter((key) => record[key] !== undefined)
1437
+ .map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
1438
+ .join(",")}}`;
1439
+ }
1440
+ function normalizeIssueListCacheValue(value) {
1441
+ if (value === undefined)
1442
+ return undefined;
1443
+ if (value === null)
1444
+ return null;
1445
+ if (Array.isArray(value)) {
1446
+ return value.map(normalizeIssueListCacheValue).sort((a, b) => stableJson(a).localeCompare(stableJson(b)));
1447
+ }
1448
+ if (typeof value === "object") {
1449
+ const normalized = {};
1450
+ for (const [key, nestedValue] of Object.entries(value)) {
1451
+ const next = normalizeIssueListCacheValue(nestedValue);
1452
+ if (next !== undefined)
1453
+ normalized[key] = next;
1454
+ }
1455
+ return normalized;
1456
+ }
1457
+ return value;
1458
+ }
1459
+ function issueListActorIdentity(req, companyId) {
1460
+ if (req.actor.type === "agent") {
1461
+ const onBehalfMembership = req.actor.onBehalfOfUserId
1462
+ ? req.actor.onBehalfOfMemberships?.find((membership) => membership.companyId === companyId) ?? null
1463
+ : null;
1464
+ const key = [
1465
+ "agent",
1466
+ companyId,
1467
+ req.actor.agentId ?? "unknown-agent",
1468
+ req.actor.keyId ?? req.actor.source ?? "agent-auth",
1469
+ req.actor.onBehalfOfUserId ?? "no-responsible-user",
1470
+ onBehalfMembership?.status ?? "no-responsible-user-status",
1471
+ onBehalfMembership?.membershipRole ?? "no-responsible-user-role",
1472
+ ].join(":");
1473
+ return { actorType: "agent", key, hash: shortHash(key) };
1474
+ }
1475
+ if (req.actor.type === "board") {
1476
+ const sessionPart = req.actor.source === "session"
1477
+ ? `cookie:${shortHash(String(req.headers.cookie ?? "no-cookie"))}`
1478
+ : req.actor.keyId ?? req.actor.source ?? "board";
1479
+ const key = [
1480
+ "board",
1481
+ companyId,
1482
+ req.actor.source ?? "board",
1483
+ req.actor.userId ?? "unknown-user",
1484
+ sessionPart,
1485
+ ].join(":");
1486
+ return { actorType: "board", key, hash: shortHash(key) };
1487
+ }
1488
+ const key = ["none", companyId, req.actor.source ?? "none"].join(":");
1489
+ return { actorType: "none", key, hash: shortHash(key) };
1490
+ }
1491
+ function issueListClientIdentity(req) {
1492
+ const forwardedFor = Array.isArray(req.headers["x-forwarded-for"])
1493
+ ? req.headers["x-forwarded-for"][0]
1494
+ : req.headers["x-forwarded-for"];
1495
+ const client = [
1496
+ String(forwardedFor ?? req.ip ?? "unknown-ip").split(",")[0]?.trim() ?? "unknown-ip",
1497
+ req.header("user-agent") ?? "unknown-agent",
1498
+ ].join(":");
1499
+ return { key: client, hash: shortHash(client) };
1500
+ }
1501
+ function safeRefererPath(req) {
1502
+ const referer = req.header("referer");
1503
+ if (!referer)
1504
+ return null;
1505
+ try {
1506
+ return new URL(referer).pathname;
1507
+ }
1508
+ catch {
1509
+ return referer.split("?")[0]?.slice(0, 160) ?? null;
1510
+ }
1511
+ }
1512
+ function issueListRequestKey(input) {
1513
+ const route = "GET /api/companies/:companyId/issues";
1514
+ const actor = issueListActorIdentity(input.req, input.companyId);
1515
+ const client = issueListClientIdentity(input.req);
1516
+ const normalizedQuery = normalizeIssueListCacheValue(input.normalizedQuery);
1517
+ const queryKeys = Object.keys(normalizedQuery).sort();
1518
+ const key = stableJson({
1519
+ actor: actor.key,
1520
+ companyId: input.companyId,
1521
+ query: normalizedQuery,
1522
+ route,
1523
+ });
1524
+ return {
1525
+ actor,
1526
+ client,
1527
+ key,
1528
+ keyHash: shortHash(key),
1529
+ queryKeys,
1530
+ route,
1531
+ };
1532
+ }
1533
+ function pruneIssueListResponseCache(now) {
1534
+ for (const [key, entry] of issueListResponseCache) {
1535
+ if (entry.staleUntil <= now)
1536
+ issueListResponseCache.delete(key);
1537
+ }
1538
+ }
1539
+ function touchIssueListResponseCacheEntry(key, entry) {
1540
+ issueListResponseCache.delete(key);
1541
+ issueListResponseCache.set(key, entry);
1542
+ }
1543
+ function trimIssueListResponseCache() {
1544
+ while (issueListResponseCache.size > ISSUE_LIST_SERVER_CACHE_MAX_ENTRIES) {
1545
+ const oldestKey = issueListResponseCache.keys().next().value;
1546
+ if (oldestKey === undefined)
1547
+ return;
1548
+ issueListResponseCache.delete(oldestKey);
1549
+ }
1550
+ }
1551
+ function setIssueListResponseCacheEntry(key, entry) {
1552
+ touchIssueListResponseCacheEntry(key, entry);
1553
+ trimIssueListResponseCache();
1554
+ }
1555
+ function decrementIssueListActorClientInflight(actorClientKey) {
1556
+ const next = (issueListActorClientInflight.get(actorClientKey) ?? 1) - 1;
1557
+ if (next <= 0)
1558
+ issueListActorClientInflight.delete(actorClientKey);
1559
+ else
1560
+ issueListActorClientInflight.set(actorClientKey, next);
1561
+ }
1562
+ async function coordinateIssueListGet(input) {
1563
+ const now = Date.now();
1564
+ pruneIssueListResponseCache(now);
1565
+ const cached = input.allowTtlCache ? issueListResponseCache.get(input.requestKey.key) : undefined;
1566
+ if (cached && cached.expiresAt > now) {
1567
+ touchIssueListResponseCacheEntry(input.requestKey.key, cached);
1568
+ return { response: cached.response, cacheStatus: "hit", identicalInFlightCount: 0 };
1569
+ }
1570
+ const existing = issueListInflight.get(input.requestKey.key);
1571
+ if (existing) {
1572
+ existing.waiterCount += 1;
1573
+ const identicalInFlightCount = existing.waiterCount + 1;
1574
+ if (!existing.stormLogged &&
1575
+ identicalInFlightCount >= ISSUE_LIST_STORM_THRESHOLD &&
1576
+ now - existing.startedAt <= ISSUE_LIST_STORM_WINDOW_MS) {
1577
+ existing.stormLogged = true;
1578
+ const event = {
1579
+ event: "request_storm_detected",
1580
+ route: input.requestKey.route,
1581
+ companyId: input.companyId,
1582
+ actorType: input.requestKey.actor.actorType,
1583
+ actorIdentityHash: input.requestKey.actor.hash,
1584
+ clientHash: input.requestKey.client.hash,
1585
+ cacheKeyHash: input.requestKey.keyHash,
1586
+ queryKeys: input.requestKey.queryKeys,
1587
+ identicalInFlightCount,
1588
+ windowMs: now - existing.startedAt,
1589
+ referer: safeRefererPath(input.req),
1590
+ visibilityHint: input.req.header("x-paperclip-tab-visible") ?? null,
1591
+ };
1592
+ logger.warn(event, "request_storm_detected");
1593
+ input.diagnostics?.onStormDetected?.(event);
1594
+ }
1595
+ const response = await existing.promise;
1596
+ return { response, cacheStatus: "coalesced", identicalInFlightCount };
1597
+ }
1598
+ const actorClientKey = `${input.requestKey.actor.key}:${input.requestKey.client.key}`;
1599
+ const actorClientInflight = issueListActorClientInflight.get(actorClientKey) ?? 0;
1600
+ if (actorClientInflight >= ISSUE_LIST_MAX_ACTOR_CLIENT_INFLIGHT) {
1601
+ if (cached && cached.staleUntil > now) {
1602
+ touchIssueListResponseCacheEntry(input.requestKey.key, cached);
1603
+ return { response: cached.response, cacheStatus: "stale", identicalInFlightCount: 0 };
1604
+ }
1605
+ return { response: null, cacheStatus: "retry", identicalInFlightCount: 0, retryAfterSeconds: 1 };
1606
+ }
1607
+ issueListActorClientInflight.set(actorClientKey, actorClientInflight + 1);
1608
+ const promise = (async () => {
1609
+ await input.diagnostics?.onComputeStart?.({
1610
+ companyId: input.companyId,
1611
+ cacheKeyHash: input.requestKey.keyHash,
1612
+ });
1613
+ return input.compute();
1614
+ })();
1615
+ const inflightEntry = {
1616
+ promise,
1617
+ startedAt: now,
1618
+ waiterCount: 0,
1619
+ stormLogged: false,
1620
+ };
1621
+ issueListInflight.set(input.requestKey.key, inflightEntry);
1622
+ try {
1623
+ const response = await promise;
1624
+ if (input.allowTtlCache) {
1625
+ setIssueListResponseCacheEntry(input.requestKey.key, {
1626
+ response,
1627
+ expiresAt: Date.now() + ISSUE_LIST_SERVER_CACHE_TTL_MS,
1628
+ staleUntil: Date.now() + ISSUE_LIST_SERVER_CACHE_STALE_MS,
1629
+ });
1630
+ }
1631
+ return { response, cacheStatus: "miss", identicalInFlightCount: 1 };
1632
+ }
1633
+ finally {
1634
+ if (issueListInflight.get(input.requestKey.key) === inflightEntry) {
1635
+ issueListInflight.delete(input.requestKey.key);
1636
+ }
1637
+ decrementIssueListActorClientInflight(actorClientKey);
1638
+ }
1639
+ }
1640
+ function estimatedJsonBytes(value) {
1641
+ return Buffer.byteLength(JSON.stringify(value), "utf8");
1642
+ }
1643
+ function logIssueListRequest(input) {
1644
+ input.res.once("finish", () => {
1645
+ const contentEncoding = input.res.getHeader("content-encoding");
1646
+ const contentLength = Number(input.res.getHeader("content-length"));
1647
+ logger.debug({
1648
+ event: "safe_get_request_observed",
1649
+ route: input.requestKey.route,
1650
+ companyId: input.companyId,
1651
+ actorType: input.requestKey.actor.actorType,
1652
+ actorIdentityHash: input.requestKey.actor.hash,
1653
+ clientHash: input.requestKey.client.hash,
1654
+ cacheKeyHash: input.requestKey.keyHash,
1655
+ queryKeys: input.requestKey.queryKeys,
1656
+ requestCount: input.identicalInFlightCount,
1657
+ durationMs: Date.now() - input.startedAt,
1658
+ statusCode: input.res.statusCode,
1659
+ responseBytes: input.bodyBytes,
1660
+ compressedBytes: contentEncoding && Number.isFinite(contentLength) ? contentLength : null,
1661
+ contentEncoding: contentEncoding ? String(contentEncoding) : null,
1662
+ cacheStatus: input.cacheStatus,
1663
+ etagOutcome: input.etagOutcome,
1664
+ referer: safeRefererPath(input.req),
1665
+ visibilityHint: input.req.header("x-paperclip-tab-visible") ?? null,
1666
+ }, "safe authenticated GET observed");
1667
+ });
1668
+ }
805
1669
  export function issueRoutes(db, storage, opts = {}) {
806
1670
  const router = Router();
807
1671
  const svc = issueService(db);
@@ -826,6 +1690,7 @@ export function issueRoutes(db, storage, opts = {}) {
826
1690
  const executionWorkspacesSvc = executionWorkspaceServiceDirect(db);
827
1691
  const workProductsSvc = workProductService(db);
828
1692
  const documentsSvc = documentService(db);
1693
+ const companySkillsSvc = companySkillService(db);
829
1694
  const documentAnnotationsSvc = documentAnnotationService(db);
830
1695
  const issueReferencesSvc = issueReferenceService(db);
831
1696
  const issueThreadInteractionsSvc = issueThreadInteractionService(db);
@@ -1430,11 +2295,14 @@ export function issueRoutes(db, storage, opts = {}) {
1430
2295
  });
1431
2296
  if (decision.allowed)
1432
2297
  return;
1433
- throw forbidden(decision.explanation);
2298
+ throw forbidden(decision.explanation, authorizationDeniedDetails(decision));
1434
2299
  }
1435
2300
  function isTaskBridgeKeyActor(req) {
1436
2301
  return req.actor.type === "agent" && req.actor.source === "agent_key" && req.actor.keyScope?.kind === "task_bridge";
1437
2302
  }
2303
+ function isSkillTestScopedActor(req) {
2304
+ return req.actor.type === "agent" && req.actor.keyScope?.kind === "skill_test";
2305
+ }
1438
2306
  function taskBridgeOriginForActor(req) {
1439
2307
  return isTaskBridgeKeyActor(req) && req.actor.keyId
1440
2308
  ? { originKind: "task_bridge", originId: req.actor.keyId }
@@ -2268,6 +3136,136 @@ export function issueRoutes(db, storage, opts = {}) {
2268
3136
  }
2269
3137
  return { project, goal: null };
2270
3138
  }
3139
+ function compactIssueProjectWorkspace(workspace) {
3140
+ if (!workspace)
3141
+ return null;
3142
+ return {
3143
+ id: workspace.id,
3144
+ companyId: workspace.companyId,
3145
+ projectId: workspace.projectId,
3146
+ name: workspace.name,
3147
+ sourceType: workspace.sourceType,
3148
+ cwd: workspace.cwd,
3149
+ repoUrl: workspace.repoUrl,
3150
+ repoRef: workspace.repoRef,
3151
+ defaultRef: workspace.defaultRef,
3152
+ visibility: workspace.visibility,
3153
+ setupCommand: workspace.setupCommand,
3154
+ cleanupCommand: workspace.cleanupCommand,
3155
+ remoteProvider: workspace.remoteProvider,
3156
+ remoteWorkspaceRef: workspace.remoteWorkspaceRef,
3157
+ sharedWorkspaceKey: workspace.sharedWorkspaceKey,
3158
+ runtimeConfig: workspace.runtimeConfig,
3159
+ isPrimary: workspace.isPrimary,
3160
+ createdAt: workspace.createdAt,
3161
+ updatedAt: workspace.updatedAt,
3162
+ };
3163
+ }
3164
+ function compactIssueProject(project) {
3165
+ if (!project)
3166
+ return null;
3167
+ return {
3168
+ id: project.id,
3169
+ companyId: project.companyId,
3170
+ urlKey: project.urlKey,
3171
+ goalId: project.goalId,
3172
+ goalIds: project.goalIds,
3173
+ goals: project.goals,
3174
+ name: project.name,
3175
+ description: project.description,
3176
+ status: project.status,
3177
+ leadAgentId: project.leadAgentId,
3178
+ targetDate: project.targetDate,
3179
+ color: project.color,
3180
+ icon: project.icon,
3181
+ env: null,
3182
+ pauseReason: project.pauseReason,
3183
+ pausedAt: project.pausedAt,
3184
+ executionWorkspacePolicy: project.executionWorkspacePolicy,
3185
+ codebase: project.codebase,
3186
+ workspaces: (project.workspaces ?? []).map(compactIssueProjectWorkspace),
3187
+ primaryWorkspace: compactIssueProjectWorkspace(project.primaryWorkspace),
3188
+ managedByPlugin: project.managedByPlugin ?? null,
3189
+ taskCount: project.taskCount,
3190
+ budget: project.budget,
3191
+ archivedAt: project.archivedAt,
3192
+ createdAt: project.createdAt,
3193
+ updatedAt: project.updatedAt,
3194
+ };
3195
+ }
3196
+ function compactIssueRuntimeService(service) {
3197
+ return {
3198
+ id: service.id,
3199
+ companyId: service.companyId,
3200
+ projectId: service.projectId,
3201
+ projectWorkspaceId: service.projectWorkspaceId,
3202
+ executionWorkspaceId: service.executionWorkspaceId,
3203
+ issueId: service.issueId,
3204
+ scopeType: service.scopeType,
3205
+ scopeId: service.scopeId,
3206
+ serviceName: service.serviceName,
3207
+ status: service.status,
3208
+ lifecycle: service.lifecycle,
3209
+ reuseKey: service.reuseKey,
3210
+ command: service.command,
3211
+ cwd: service.cwd,
3212
+ port: service.port,
3213
+ url: service.url,
3214
+ provider: service.provider,
3215
+ providerRef: service.providerRef,
3216
+ ownerAgentId: service.ownerAgentId,
3217
+ startedByRunId: service.startedByRunId,
3218
+ lastUsedAt: service.lastUsedAt,
3219
+ startedAt: service.startedAt,
3220
+ stoppedAt: service.stoppedAt,
3221
+ healthStatus: service.healthStatus,
3222
+ configIndex: service.configIndex ?? null,
3223
+ };
3224
+ }
3225
+ function compactIssueExecutionWorkspace(workspace) {
3226
+ if (!workspace)
3227
+ return null;
3228
+ return {
3229
+ id: workspace.id,
3230
+ companyId: workspace.companyId,
3231
+ projectId: workspace.projectId,
3232
+ projectWorkspaceId: workspace.projectWorkspaceId,
3233
+ sourceIssueId: workspace.sourceIssueId,
3234
+ mode: workspace.mode,
3235
+ strategyType: workspace.strategyType,
3236
+ name: workspace.name,
3237
+ status: workspace.status,
3238
+ cwd: workspace.cwd,
3239
+ repoUrl: workspace.repoUrl,
3240
+ baseRef: workspace.baseRef,
3241
+ branchName: workspace.branchName,
3242
+ providerType: workspace.providerType,
3243
+ providerRef: workspace.providerRef,
3244
+ derivedFromExecutionWorkspaceId: workspace.derivedFromExecutionWorkspaceId,
3245
+ lastUsedAt: workspace.lastUsedAt,
3246
+ openedAt: workspace.openedAt,
3247
+ closedAt: workspace.closedAt,
3248
+ cleanupEligibleAt: workspace.cleanupEligibleAt,
3249
+ cleanupReason: workspace.cleanupReason,
3250
+ config: workspace.config
3251
+ ? {
3252
+ environmentId: workspace.config.environmentId,
3253
+ provisionCommand: workspace.config.provisionCommand,
3254
+ teardownCommand: workspace.config.teardownCommand,
3255
+ cleanupCommand: workspace.config.cleanupCommand,
3256
+ workspaceRuntime: workspace.config.workspaceRuntime,
3257
+ desiredState: workspace.config.desiredState,
3258
+ serviceStates: workspace.config.serviceStates,
3259
+ }
3260
+ : null,
3261
+ metadata: null,
3262
+ runtimeServices: (workspace.runtimeServices ?? [])
3263
+ .filter((service) => service.status === "starting" || service.status === "running")
3264
+ .map(compactIssueRuntimeService),
3265
+ createdAt: workspace.createdAt,
3266
+ updatedAt: workspace.updatedAt,
3267
+ };
3268
+ }
2271
3269
  // Resolve issue identifiers (e.g. "PAP-39") to UUIDs for all /issues/:id routes
2272
3270
  router.param("id", async (req, res, next, rawId) => {
2273
3271
  try {
@@ -2306,7 +3304,21 @@ export function issueRoutes(db, storage, opts = {}) {
2306
3304
  res.status(403).json({ error: "Company search is outside this actor's authorization boundary" });
2307
3305
  return;
2308
3306
  }
2309
- const query = companySearchQuerySchema.parse(req.query);
3307
+ const parsedQuery = companySearchQuerySchema.safeParse(req.query);
3308
+ if (!parsedQuery.success) {
3309
+ res.status(400).json({
3310
+ error: parsedQuery.error.issues[0]?.message ?? "Invalid search query",
3311
+ });
3312
+ return;
3313
+ }
3314
+ let query = parsedQuery.data;
3315
+ if (query.assigneeUserId === "me") {
3316
+ if (req.actor.type !== "board" || !req.actor.userId) {
3317
+ res.status(403).json({ error: "assigneeUserId=me requires board authentication" });
3318
+ return;
3319
+ }
3320
+ query = { ...query, assigneeUserId: req.actor.userId };
3321
+ }
2310
3322
  const rateLimit = searchRateLimiter.consume(companySearchRateLimitActor(req, companyId));
2311
3323
  res.setHeader("X-RateLimit-Limit", String(rateLimit.limit));
2312
3324
  res.setHeader("X-RateLimit-Remaining", String(rateLimit.remaining));
@@ -2322,6 +3334,7 @@ export function issueRoutes(db, storage, opts = {}) {
2322
3334
  res.json(result);
2323
3335
  });
2324
3336
  router.get("/companies/:companyId/issues", async (req, res) => {
3337
+ const startedAt = Date.now();
2325
3338
  const companyId = req.params.companyId;
2326
3339
  assertCompanyAccess(req, companyId);
2327
3340
  if (isTaskBridgeKeyActor(req)) {
@@ -2356,6 +3369,8 @@ export function issueRoutes(db, storage, opts = {}) {
2356
3369
  const attention = req.query.attention;
2357
3370
  const sortField = req.query.sortField;
2358
3371
  const sortDir = req.query.sortDir;
3372
+ const view = req.query.view;
3373
+ const compactView = view === "compact";
2359
3374
  const hasPlanDocument = parseOptionalBooleanQuery(req.query.hasPlanDocument);
2360
3375
  const includeLiveDescendantSummary = parseOptionalBooleanQuery(req.query.includeLiveDescendantSummary);
2361
3376
  const assigneeAgentFilterRaw = req.query.assigneeAgentId;
@@ -2380,6 +3395,10 @@ export function issueRoutes(db, storage, opts = {}) {
2380
3395
  res.status(400).json({ error: "attention must be 'blocked' when provided" });
2381
3396
  return;
2382
3397
  }
3398
+ if (view !== undefined && view !== "compact") {
3399
+ res.status(400).json({ error: "view must be 'compact' when provided" });
3400
+ return;
3401
+ }
2383
3402
  if (rawLimit !== undefined && (parsedLimit === null || !Number.isInteger(parsedLimit) || parsedLimit <= 0)) {
2384
3403
  res.status(400).json({ error: `limit must be a positive integer up to ${ISSUE_LIST_MAX_LIMIT}` });
2385
3404
  return;
@@ -2425,7 +3444,7 @@ export function issueRoutes(db, storage, opts = {}) {
2425
3444
  }
2426
3445
  }
2427
3446
  const offset = parsedOffset ?? 0;
2428
- const rawResult = await svc.list(companyId, {
3447
+ const listFilters = {
2429
3448
  attention: attention === "blocked" ? "blocked" : undefined,
2430
3449
  status: req.query.status,
2431
3450
  assigneeAgentId,
@@ -2455,36 +3474,145 @@ export function issueRoutes(db, storage, opts = {}) {
2455
3474
  offset,
2456
3475
  sortField: sortField === "updated" ? "updated" : undefined,
2457
3476
  sortDir: sortDir === "asc" || sortDir === "desc" ? sortDir : undefined,
3477
+ };
3478
+ const requestKey = issueListRequestKey({
3479
+ req,
3480
+ companyId,
3481
+ normalizedQuery: {
3482
+ ...listFilters,
3483
+ view: compactView ? "compact" : undefined,
3484
+ },
2458
3485
  });
2459
- const result = await actorCanReadCompanyScope(req, companyId)
2460
- ? rawResult
2461
- : await filterIssuesForActor(req, rawResult);
2462
- const issueIds = result.map((issue) => issue.id);
2463
- const [handoffStates, recoveryActionByIssue] = await Promise.all([
2464
- listSuccessfulRunHandoffStates(db, companyId, issueIds),
2465
- recoveryActionsSvc.listActiveForIssues(companyId, issueIds),
2466
- ]);
2467
- const actor = getActorInfo(req);
2468
- await Promise.all(result.map(async (issue) => {
2469
- const activeRecoveryAction = recoveryActionByIssue.get(issue.id) ?? null;
2470
- if (!activeRecoveryAction)
2471
- return;
2472
- const revalidated = await revalidateActiveSourceRecoveryForRead({
2473
- issue,
2474
- trigger: "read_projection",
2475
- actor,
2476
- activeRecoveryAction,
3486
+ const coordinated = await coordinateIssueListGet({
3487
+ req,
3488
+ companyId,
3489
+ requestKey,
3490
+ allowTtlCache: compactView,
3491
+ diagnostics: opts.issueListDiagnostics,
3492
+ compute: async () => {
3493
+ const rawResult = await svc.list(companyId, listFilters);
3494
+ const result = await actorCanReadCompanyScope(req, companyId)
3495
+ ? rawResult
3496
+ : await filterIssuesForActor(req, rawResult);
3497
+ const issueIds = result.map((issue) => issue.id);
3498
+ if (compactView) {
3499
+ const [handoffStates, recoveryActionByIssue] = await Promise.all([
3500
+ listSuccessfulRunHandoffStates(db, companyId, issueIds),
3501
+ recoveryActionsSvc.listActiveForIssues(companyId, issueIds),
3502
+ ]);
3503
+ const actor = getActorInfo(req);
3504
+ await Promise.all(result.map(async (issue) => {
3505
+ const activeRecoveryAction = recoveryActionByIssue.get(issue.id) ?? null;
3506
+ if (!activeRecoveryAction)
3507
+ return;
3508
+ const revalidated = await revalidateActiveSourceRecoveryForRead({
3509
+ issue,
3510
+ trigger: "read_projection",
3511
+ actor,
3512
+ activeRecoveryAction,
3513
+ });
3514
+ if (revalidated)
3515
+ recoveryActionByIssue.set(issue.id, revalidated);
3516
+ else
3517
+ recoveryActionByIssue.delete(issue.id);
3518
+ }));
3519
+ const compactResult = result.map((issue) => toCompactIssue({
3520
+ ...issue,
3521
+ activeRecoveryAction: recoveryActionByIssue.get(issue.id) ?? null,
3522
+ successfulRunHandoff: handoffStates.get(issue.id) ?? null,
3523
+ }));
3524
+ return {
3525
+ kind: "compact",
3526
+ body: compactResult,
3527
+ etag: compactIssueListEtag(compactResult),
3528
+ cacheControl: "private, must-revalidate",
3529
+ };
3530
+ }
3531
+ const [handoffStates, recoveryActionByIssue] = await Promise.all([
3532
+ listSuccessfulRunHandoffStates(db, companyId, issueIds),
3533
+ recoveryActionsSvc.listActiveForIssues(companyId, issueIds),
3534
+ ]);
3535
+ const actor = getActorInfo(req);
3536
+ await Promise.all(result.map(async (issue) => {
3537
+ const activeRecoveryAction = recoveryActionByIssue.get(issue.id) ?? null;
3538
+ if (!activeRecoveryAction)
3539
+ return;
3540
+ const revalidated = await revalidateActiveSourceRecoveryForRead({
3541
+ issue,
3542
+ trigger: "read_projection",
3543
+ actor,
3544
+ activeRecoveryAction,
3545
+ });
3546
+ if (revalidated)
3547
+ recoveryActionByIssue.set(issue.id, revalidated);
3548
+ else
3549
+ recoveryActionByIssue.delete(issue.id);
3550
+ }));
3551
+ return {
3552
+ kind: "full",
3553
+ body: result.map((issue) => ({
3554
+ ...issue,
3555
+ successfulRunHandoff: handoffStates.get(issue.id) ?? null,
3556
+ activeRecoveryAction: recoveryActionByIssue.get(issue.id) ?? null,
3557
+ })),
3558
+ };
3559
+ },
3560
+ });
3561
+ res.setHeader("X-Paperclip-Request-Cache", coordinated.cacheStatus);
3562
+ if (!coordinated.response) {
3563
+ const body = {
3564
+ error: "Too many concurrent issue-list requests for this actor/client",
3565
+ retryAfterSeconds: coordinated.retryAfterSeconds ?? 1,
3566
+ };
3567
+ res.setHeader("Retry-After", String(body.retryAfterSeconds));
3568
+ logIssueListRequest({
3569
+ req,
3570
+ res,
3571
+ companyId,
3572
+ requestKey,
3573
+ startedAt,
3574
+ cacheStatus: "retry",
3575
+ bodyBytes: estimatedJsonBytes(body),
3576
+ etagOutcome: "none",
3577
+ identicalInFlightCount: coordinated.identicalInFlightCount,
2477
3578
  });
2478
- if (revalidated)
2479
- recoveryActionByIssue.set(issue.id, revalidated);
2480
- else
2481
- recoveryActionByIssue.delete(issue.id);
2482
- }));
2483
- res.json(result.map((issue) => ({
2484
- ...issue,
2485
- successfulRunHandoff: handoffStates.get(issue.id) ?? null,
2486
- activeRecoveryAction: recoveryActionByIssue.get(issue.id) ?? null,
2487
- })));
3579
+ res.status(429).json(body);
3580
+ return;
3581
+ }
3582
+ if (coordinated.response.kind === "compact") {
3583
+ res.setHeader("Cache-Control", coordinated.response.cacheControl);
3584
+ res.setHeader("ETag", coordinated.response.etag);
3585
+ const etagMatched = requestMatchesEtag(req.header("if-none-match"), coordinated.response.etag);
3586
+ logIssueListRequest({
3587
+ req,
3588
+ res,
3589
+ companyId,
3590
+ requestKey,
3591
+ startedAt,
3592
+ cacheStatus: coordinated.cacheStatus,
3593
+ bodyBytes: etagMatched ? 0 : estimatedJsonBytes(coordinated.response.body),
3594
+ etagOutcome: etagMatched ? "not_modified" : "fresh",
3595
+ identicalInFlightCount: coordinated.identicalInFlightCount,
3596
+ });
3597
+ if (etagMatched) {
3598
+ res.status(304).end();
3599
+ return;
3600
+ }
3601
+ res.json(coordinated.response.body);
3602
+ return;
3603
+ }
3604
+ logIssueListRequest({
3605
+ req,
3606
+ res,
3607
+ companyId,
3608
+ requestKey,
3609
+ startedAt,
3610
+ cacheStatus: coordinated.cacheStatus,
3611
+ bodyBytes: estimatedJsonBytes(coordinated.response.body),
3612
+ etagOutcome: "none",
3613
+ identicalInFlightCount: coordinated.identicalInFlightCount,
3614
+ });
3615
+ res.json(coordinated.response.body);
2488
3616
  });
2489
3617
  router.get("/companies/:companyId/issues/count", async (req, res) => {
2490
3618
  const companyId = req.params.companyId;
@@ -2745,6 +3873,126 @@ export function issueRoutes(db, storage, opts = {}) {
2745
3873
  currentExecutionWorkspace,
2746
3874
  });
2747
3875
  });
3876
+ router.get("/issues/:id/diagnostics/blockers", async (req, res) => {
3877
+ const id = req.params.id;
3878
+ const issue = await svc.getById(id);
3879
+ if (!issue) {
3880
+ res.status(404).json({ error: "Issue not found" });
3881
+ return;
3882
+ }
3883
+ assertCompanyAccess(req, issue.companyId);
3884
+ if (!(await assertIssueReadAllowed(req, res, issue)))
3885
+ return;
3886
+ const diagnostic = await svc.getBlockerDiagnostics(issue.id);
3887
+ const visibleBlockers = await filterIssuesForActor(req, diagnostic.blockers);
3888
+ const response = buildIssueBlockerDiagnosticsResponse({
3889
+ issue,
3890
+ blockers: diagnostic.blockers,
3891
+ visibleBlockers,
3892
+ readiness: diagnostic.readiness,
3893
+ truncated: diagnostic.truncated,
3894
+ });
3895
+ logger.info({
3896
+ companyId: issue.companyId,
3897
+ issueId: issue.id,
3898
+ actorType: req.actor.type,
3899
+ visibleBlockerCount: response.blockers.length,
3900
+ omittedUnauthorizedBlockerCount: response.omittedUnauthorizedBlockerCount,
3901
+ truncated: response.truncated,
3902
+ }, "issue blocker diagnostics read");
3903
+ res.json(response);
3904
+ });
3905
+ router.get("/issues/:id/diagnostics/wakes", async (req, res) => {
3906
+ const id = req.params.id;
3907
+ const issue = await svc.getById(id);
3908
+ if (!issue) {
3909
+ res.status(404).json({ error: "Issue not found" });
3910
+ return;
3911
+ }
3912
+ assertCompanyAccess(req, issue.companyId);
3913
+ if (!(await assertIssueReadAllowed(req, res, issue)))
3914
+ return;
3915
+ const [wakeDiagnostic, blockerDiagnostic, includeInternalIds] = await Promise.all([
3916
+ svc.getWakeDiagnostics(issue.id),
3917
+ svc.getBlockerDiagnostics(issue.id),
3918
+ actorCanReadCompanyScope(req, issue.companyId),
3919
+ ]);
3920
+ const visibleBlockers = await filterIssuesForActor(req, blockerDiagnostic.blockers);
3921
+ const blockerResponse = buildIssueBlockerDiagnosticsResponse({
3922
+ issue,
3923
+ blockers: blockerDiagnostic.blockers,
3924
+ visibleBlockers,
3925
+ readiness: blockerDiagnostic.readiness,
3926
+ truncated: blockerDiagnostic.truncated,
3927
+ });
3928
+ const response = buildIssueWakeDiagnosticsResponse({
3929
+ issue,
3930
+ wakeRequests: wakeDiagnostic.wakeRequests,
3931
+ activityRecords: wakeDiagnostic.activityRecords,
3932
+ blockerDiagnostics: blockerResponse,
3933
+ truncatedWakeRequests: wakeDiagnostic.truncatedWakeRequests,
3934
+ truncatedActivityRecords: wakeDiagnostic.truncatedActivityRecords,
3935
+ includeInternalIds,
3936
+ });
3937
+ logger.info({
3938
+ companyId: issue.companyId,
3939
+ issueId: issue.id,
3940
+ actorType: req.actor.type,
3941
+ wakeRequestCount: response.wakeRequestCount,
3942
+ activityRecordCount: response.activityRecordCount,
3943
+ internalIdsIncluded: includeInternalIds,
3944
+ truncated: response.truncated,
3945
+ }, "issue wake diagnostics read");
3946
+ res.json(response);
3947
+ });
3948
+ router.get("/issues/:id/diagnostics/subtree", async (req, res) => {
3949
+ const id = req.params.id;
3950
+ const issue = await svc.getById(id);
3951
+ if (!issue) {
3952
+ res.status(404).json({ error: "Issue not found" });
3953
+ return;
3954
+ }
3955
+ assertCompanyAccess(req, issue.companyId);
3956
+ if (!(await assertIssueReadAllowed(req, res, issue)))
3957
+ return;
3958
+ const [diagnostic, includeInternalIds] = await Promise.all([
3959
+ svc.getSubtreeDiagnostics(issue.id),
3960
+ actorCanReadCompanyScope(req, issue.companyId),
3961
+ ]);
3962
+ const allBlockers = [...diagnostic.blockersByIssueId.values()].flat();
3963
+ const [visibleNodes, visibleBlockers] = await Promise.all([
3964
+ filterIssuesForActor(req, diagnostic.nodes),
3965
+ filterIssuesForActor(req, allBlockers),
3966
+ ]);
3967
+ const response = buildIssueSubtreeDiagnosticsResponse({
3968
+ issue,
3969
+ nodes: diagnostic.nodes,
3970
+ visibleNodes,
3971
+ blockersByIssueId: diagnostic.blockersByIssueId,
3972
+ visibleBlockers,
3973
+ readinessByIssueId: diagnostic.readinessByIssueId,
3974
+ wakeRequestsByIssueId: diagnostic.wakeRequestsByIssueId,
3975
+ activityRecordsByIssueId: diagnostic.activityRecordsByIssueId,
3976
+ truncatedNodes: diagnostic.truncatedNodes,
3977
+ truncatedDepth: diagnostic.truncatedDepth,
3978
+ truncatedBlockerIssueIds: diagnostic.truncatedBlockerIssueIds,
3979
+ truncatedWakeIssueIds: diagnostic.truncatedWakeIssueIds,
3980
+ truncatedActivityIssueIds: diagnostic.truncatedActivityIssueIds,
3981
+ includeInternalIds,
3982
+ caps: diagnostic.caps,
3983
+ });
3984
+ logger.info({
3985
+ companyId: issue.companyId,
3986
+ issueId: issue.id,
3987
+ actorType: req.actor.type,
3988
+ nodeCount: response.nodeCount,
3989
+ omittedUnauthorizedNodeCount: response.omittedUnauthorizedNodeCount,
3990
+ edgeCount: response.edges.length,
3991
+ internalIdsIncluded: includeInternalIds,
3992
+ truncated: response.truncated,
3993
+ }, "issue subtree diagnostics read");
3994
+ res.json(response);
3995
+ });
2748
3996
  router.get("/issues/:id", async (req, res) => {
2749
3997
  const id = req.params.id;
2750
3998
  const issue = await svc.getById(id);
@@ -2798,10 +4046,10 @@ export function issueRoutes(db, storage, opts = {}) {
2798
4046
  relatedWork: referenceSummary,
2799
4047
  referencedIssueIdentifiers: referenceSummary.outbound.map((item) => item.issue.identifier ?? item.issue.id),
2800
4048
  ...documentPayload,
2801
- project: project ?? null,
4049
+ project: compactIssueProject(project),
2802
4050
  goal: goal ?? null,
2803
4051
  mentionedProjects,
2804
- currentExecutionWorkspace,
4052
+ currentExecutionWorkspace: compactIssueExecutionWorkspace(currentExecutionWorkspace),
2805
4053
  workProducts,
2806
4054
  linkedCases,
2807
4055
  });
@@ -2914,6 +4162,8 @@ export function issueRoutes(db, storage, opts = {}) {
2914
4162
  return;
2915
4163
  }
2916
4164
  assertCompanyAccess(req, issue.companyId);
4165
+ if (!(await assertIssueReadAllowed(req, res, issue)))
4166
+ return;
2917
4167
  const active = await revalidateActiveSourceRecoveryForRead({
2918
4168
  issue,
2919
4169
  trigger: "read_projection",
@@ -3077,6 +4327,8 @@ export function issueRoutes(db, storage, opts = {}) {
3077
4327
  return;
3078
4328
  }
3079
4329
  assertCompanyAccess(req, issue.companyId);
4330
+ if (!(await assertIssueReadAllowed(req, res, issue)))
4331
+ return;
3080
4332
  const objects = await externalObjectsSvc.listForIssue(issue.id);
3081
4333
  res.json(objects);
3082
4334
  });
@@ -3088,13 +4340,31 @@ export function issueRoutes(db, storage, opts = {}) {
3088
4340
  return;
3089
4341
  }
3090
4342
  assertCompanyAccess(req, issue.companyId);
4343
+ if (!(await assertIssueReadAllowed(req, res, issue)))
4344
+ return;
3091
4345
  const summary = await externalObjectsSvc.getIssueSummary(issue.id);
3092
4346
  res.json(summary);
3093
4347
  });
3094
4348
  router.post("/companies/:companyId/issues/external-object-summaries", validate(externalObjectSummariesSchema), async (req, res) => {
3095
4349
  const companyId = req.params.companyId;
3096
4350
  assertCompanyAccess(req, companyId);
3097
- const summaries = await externalObjectsSvc.getIssueSummaries(companyId, req.body.issueIds);
4351
+ const requestedIssueIds = [...new Set(req.body.issueIds)];
4352
+ const candidateIssues = requestedIssueIds.length > 0
4353
+ ? await db
4354
+ .select({
4355
+ id: issueRows.id,
4356
+ companyId: issueRows.companyId,
4357
+ projectId: issueRows.projectId,
4358
+ parentId: issueRows.parentId,
4359
+ assigneeAgentId: issueRows.assigneeAgentId,
4360
+ assigneeUserId: issueRows.assigneeUserId,
4361
+ status: issueRows.status,
4362
+ })
4363
+ .from(issueRows)
4364
+ .where(and(eq(issueRows.companyId, companyId), inArray(issueRows.id, requestedIssueIds)))
4365
+ : [];
4366
+ const readableIssueIds = (await filterIssuesForActor(req, candidateIssues)).map((issue) => issue.id);
4367
+ const summaries = await externalObjectsSvc.getIssueSummaries(companyId, readableIssueIds);
3098
4368
  res.json({ summaries: Object.fromEntries(summaries) });
3099
4369
  });
3100
4370
  router.post("/issues/:id/external-objects/refresh", validate(refreshExternalObjectsSchema), async (req, res) => {
@@ -3182,6 +4452,8 @@ export function issueRoutes(db, storage, opts = {}) {
3182
4452
  return;
3183
4453
  }
3184
4454
  assertCompanyAccess(req, issue.companyId);
4455
+ if (!(await assertIssueReadAllowed(req, res, issue)))
4456
+ return;
3185
4457
  const keyParsed = issueDocumentKeySchema.safeParse(String(req.params.key ?? "").trim().toLowerCase());
3186
4458
  if (!keyParsed.success) {
3187
4459
  res.status(400).json({ error: "Invalid document key", details: keyParsed.error.issues });
@@ -3251,6 +4523,8 @@ export function issueRoutes(db, storage, opts = {}) {
3251
4523
  return;
3252
4524
  }
3253
4525
  assertCompanyAccess(req, issue.companyId);
4526
+ if (!(await assertIssueReadAllowed(req, res, issue)))
4527
+ return;
3254
4528
  const keyParsed = issueDocumentKeySchema.safeParse(String(req.params.key ?? "").trim().toLowerCase());
3255
4529
  if (!keyParsed.success) {
3256
4530
  res.status(400).json({ error: "Invalid document key", details: keyParsed.error.issues });
@@ -3555,6 +4829,8 @@ export function issueRoutes(db, storage, opts = {}) {
3555
4829
  return;
3556
4830
  }
3557
4831
  assertCompanyAccess(req, issue.companyId);
4832
+ if (!(await assertIssueReadAllowed(req, res, issue)))
4833
+ return;
3558
4834
  const keyParsed = issueDocumentKeySchema.safeParse(String(req.params.key ?? "").trim().toLowerCase());
3559
4835
  if (!keyParsed.success) {
3560
4836
  res.status(400).json({ error: "Invalid document key", details: keyParsed.error.issues });
@@ -4164,6 +5440,8 @@ export function issueRoutes(db, storage, opts = {}) {
4164
5440
  assertCompanyAccess(req, issue.companyId);
4165
5441
  if (await assertLowTrustControlPlaneDenied(req, res, issue.companyId, issue))
4166
5442
  return;
5443
+ if (!(await assertIssueReadAllowed(req, res, issue)))
5444
+ return;
4167
5445
  const approvals = await issueApprovalsSvc.listApprovalsForIssue(id);
4168
5446
  res.json(approvals);
4169
5447
  });
@@ -4233,10 +5511,25 @@ export function issueRoutes(db, storage, opts = {}) {
4233
5511
  router.post("/companies/:companyId/issues", applyCreateIssueStatusDefault, validate(createIssueSchema), async (req, res) => {
4234
5512
  const companyId = req.params.companyId;
4235
5513
  assertCompanyAccess(req, companyId);
5514
+ if (isSkillTestScopedActor(req)) {
5515
+ res.status(403).json({
5516
+ error: "Skill-test run tokens cannot create issues.",
5517
+ details: {
5518
+ scopedIssueId: req.actor.keyScope?.kind === "skill_test" ? req.actor.keyScope.issueId : null,
5519
+ securityPrinciples: ["Least Privilege", "Complete Mediation", "Fail Securely"],
5520
+ },
5521
+ });
5522
+ return;
5523
+ }
4236
5524
  if (await assertLowTrustControlPlaneDenied(req, res, companyId, null))
4237
5525
  return;
4238
5526
  assertNoAgentHostWorkspaceCommandMutation(req, collectIssueWorkspaceCommandPaths(req.body));
4239
- const { watchdogDiscovery: rawWatchdogDiscovery, ...rawCreateBody } = req.body;
5527
+ const sanitizedBody = await sanitizeIssueCreateAttribution(db, req, res, companyId, req.body, {
5528
+ surface: "issues.create",
5529
+ });
5530
+ if (!sanitizedBody)
5531
+ return;
5532
+ const { watchdogDiscovery: rawWatchdogDiscovery, ...rawCreateBody } = sanitizedBody;
4240
5533
  const watchdogDiscovery = normalizeWatchdogDiscovery(rawWatchdogDiscovery);
4241
5534
  const watchdogProductBugFollowUp = await resolveTaskWatchdogProductBugFollowUp(req, res, companyId, watchdogDiscovery);
4242
5535
  if (watchdogProductBugFollowUp === false)
@@ -4336,6 +5629,9 @@ export function issueRoutes(db, storage, opts = {}) {
4336
5629
  ...(sourceTrust ? { sourceTrust } : {}),
4337
5630
  createdByAgentId: actor.agentId,
4338
5631
  createdByUserId: actor.actorType === "user" ? actor.actorId : null,
5632
+ actorRunId: actor.runId,
5633
+ actorResponsibleUserId: authenticatedActorResponsibleUserId(req),
5634
+ trustExplicitResponsibleUserId: actor.actorType === "user",
4339
5635
  watchdogActorRunId: actor.runId,
4340
5636
  });
4341
5637
  await issueReferencesSvc.syncIssue(issue.id);
@@ -4447,9 +5743,15 @@ export function issueRoutes(db, storage, opts = {}) {
4447
5743
  if (await assertLowTrustControlPlaneDenied(req, res, parent.companyId, parent))
4448
5744
  return;
4449
5745
  assertNoAgentHostWorkspaceCommandMutation(req, collectIssueWorkspaceCommandPaths(req.body));
4450
- const normalizedAssigneeAgentId = await normalizeIssueAssigneeAgentReference(parent.companyId, req.body.assigneeAgentId);
5746
+ const sanitizedBody = await sanitizeIssueCreateAttribution(db, req, res, parent.companyId, req.body, {
5747
+ surface: "issues.children.create",
5748
+ entityId: parent.id,
5749
+ });
5750
+ if (!sanitizedBody)
5751
+ return;
5752
+ const normalizedAssigneeAgentId = await normalizeIssueAssigneeAgentReference(parent.companyId, sanitizedBody.assigneeAgentId);
4451
5753
  const createBody = {
4452
- ...req.body,
5754
+ ...sanitizedBody,
4453
5755
  ...(normalizedAssigneeAgentId !== undefined ? { assigneeAgentId: normalizedAssigneeAgentId } : {}),
4454
5756
  };
4455
5757
  if (!(await assertCheapRecoveryIssueAssigneeProfileAllowed(req, res, parent, createBody)))
@@ -4461,7 +5763,7 @@ export function issueRoutes(db, storage, opts = {}) {
4461
5763
  assigneeUserId: createBody.assigneeUserId ?? null,
4462
5764
  };
4463
5765
  await assertTaskBridgeCreateAllowed(req, parent.companyId, childAssignmentScope);
4464
- if (req.body.assigneeAgentId || req.body.assigneeUserId) {
5766
+ if (sanitizedBody.assigneeAgentId || sanitizedBody.assigneeUserId) {
4465
5767
  await assertCanAssignTasks(req, parent.companyId, childAssignmentScope);
4466
5768
  }
4467
5769
  await assertIssueEnvironmentSelection(parent.companyId, createBody.executionWorkspaceSettings?.environmentId);
@@ -4493,6 +5795,9 @@ export function issueRoutes(db, storage, opts = {}) {
4493
5795
  ...(sourceTrust ? { sourceTrust } : {}),
4494
5796
  createdByAgentId: actor.agentId,
4495
5797
  createdByUserId: actor.actorType === "user" ? actor.actorId : null,
5798
+ actorRunId: actor.runId,
5799
+ actorResponsibleUserId: authenticatedActorResponsibleUserId(req),
5800
+ trustExplicitResponsibleUserId: actor.actorType === "user",
4496
5801
  actorAgentId: actor.agentId,
4497
5802
  actorUserId: actor.actorType === "user" ? actor.actorId : null,
4498
5803
  watchdogActorRunId: actor.runId,
@@ -4608,9 +5913,15 @@ export function issueRoutes(db, storage, opts = {}) {
4608
5913
  return;
4609
5914
  const requestedChildren = [];
4610
5915
  for (const child of req.body.children) {
4611
- const normalizedAssigneeAgentId = await normalizeIssueAssigneeAgentReference(sourceIssue.companyId, child.assigneeAgentId);
5916
+ const sanitizedChild = await sanitizeIssueCreateAttribution(db, req, res, sourceIssue.companyId, child, {
5917
+ surface: "issues.accepted_plan_decomposition",
5918
+ entityId: sourceIssue.id,
5919
+ });
5920
+ if (!sanitizedChild)
5921
+ return;
5922
+ const normalizedAssigneeAgentId = await normalizeIssueAssigneeAgentReference(sourceIssue.companyId, sanitizedChild.assigneeAgentId);
4612
5923
  const childBody = {
4613
- ...child,
5924
+ ...sanitizedChild,
4614
5925
  ...(normalizedAssigneeAgentId !== undefined ? { assigneeAgentId: normalizedAssigneeAgentId } : {}),
4615
5926
  };
4616
5927
  requestedChildren.push(childBody);
@@ -4646,6 +5957,9 @@ export function issueRoutes(db, storage, opts = {}) {
4646
5957
  ...(sourceTrust ? { sourceTrust } : {}),
4647
5958
  createdByAgentId: actor.agentId,
4648
5959
  createdByUserId: actor.actorType === "user" ? actor.actorId : null,
5960
+ actorRunId: actor.runId,
5961
+ actorResponsibleUserId: authenticatedActorResponsibleUserId(req),
5962
+ trustExplicitResponsibleUserId: actor.actorType === "user",
4649
5963
  actorAgentId: actor.agentId,
4650
5964
  actorUserId: actor.actorType === "user" ? actor.actorId : null,
4651
5965
  });
@@ -5449,6 +6763,33 @@ export function issueRoutes(db, storage, opts = {}) {
5449
6763
  }
5450
6764
  }
5451
6765
  }
6766
+ if (issue.harnessKind === "skill_test" &&
6767
+ existing.status !== issue.status &&
6768
+ (issue.status === "done" || issue.status === "cancelled")) {
6769
+ const completedRun = await companySkillsSvc.completeTestRunForIssue({
6770
+ companyId: issue.companyId,
6771
+ issueId: issue.id,
6772
+ outcome: issue.status === "done" ? "succeeded" : "cancelled",
6773
+ error: issue.status === "cancelled" ? "Harness issue was cancelled" : null,
6774
+ });
6775
+ if (completedRun) {
6776
+ await logActivity(db, {
6777
+ companyId: issue.companyId,
6778
+ actorType: actor.actorType,
6779
+ actorId: actor.actorId,
6780
+ agentId: actor.agentId,
6781
+ runId: actor.runId,
6782
+ action: "company.skill_test_run_completed",
6783
+ entityType: "company_skill_test_run",
6784
+ entityId: completedRun.id,
6785
+ details: {
6786
+ issueId: issue.id,
6787
+ status: completedRun.status,
6788
+ outputDocumentKey: completedRun.outputDocumentKey,
6789
+ },
6790
+ });
6791
+ }
6792
+ }
5452
6793
  let comment = null;
5453
6794
  if (commentBody) {
5454
6795
  const commentReferenceSummaryBefore = updateReferenceSummaryAfter
@@ -5883,6 +7224,9 @@ export function issueRoutes(db, storage, opts = {}) {
5883
7224
  return;
5884
7225
  const updated = await svc.checkout(id, req.body.agentId, req.body.expectedStatuses, checkoutRunId);
5885
7226
  const actor = getActorInfo(req);
7227
+ if (updated?.harnessKind === "skill_test") {
7228
+ await companySkillsSvc.markTestRunRunning(updated.companyId, updated.id);
7229
+ }
5886
7230
  await logActivity(db, {
5887
7231
  companyId: issue.companyId,
5888
7232
  actorType: actor.actorType,
@@ -6273,6 +7617,62 @@ export function issueRoutes(db, storage, opts = {}) {
6273
7617
  });
6274
7618
  res.json(interaction);
6275
7619
  });
7620
+ router.post("/issues/:id/interactions/:interactionId/verdicts", validate(submitIssueThreadInteractionVerdictsSchema), async (req, res) => {
7621
+ const id = req.params.id;
7622
+ const interactionId = req.params.interactionId;
7623
+ const issue = await svc.getById(id);
7624
+ if (!issue) {
7625
+ res.status(404).json({ error: "Issue not found" });
7626
+ return;
7627
+ }
7628
+ assertCompanyAccess(req, issue.companyId);
7629
+ if (await rejectAgentIssueThreadInteractionResolution(req, res, issue))
7630
+ return;
7631
+ assertBoard(req);
7632
+ const actor = getActorInfo(req);
7633
+ const { interaction, newlyResolvedItemIds } = await issueThreadInteractionService(db).submitItemVerdicts(issue, interactionId, req.body, {
7634
+ agentId: actor.agentId,
7635
+ userId: actor.actorType === "user" ? actor.actorId : null,
7636
+ });
7637
+ await logActivity(db, {
7638
+ companyId: issue.companyId,
7639
+ actorType: actor.actorType,
7640
+ actorId: actor.actorId,
7641
+ agentId: actor.agentId,
7642
+ runId: actor.runId,
7643
+ action: interaction.status === "expired"
7644
+ ? "issue.thread_interaction_expired"
7645
+ : "issue.thread_interaction_item_verdicts_submitted",
7646
+ entityType: "issue",
7647
+ entityId: issue.id,
7648
+ details: {
7649
+ interactionId: interaction.id,
7650
+ interactionKind: interaction.kind,
7651
+ interactionStatus: interaction.status,
7652
+ submittedVerdictCount: Array.isArray(req.body?.verdicts) ? req.body.verdicts.length : 0,
7653
+ newlyResolvedItemCount: newlyResolvedItemIds.length,
7654
+ newlyResolvedItemIds,
7655
+ complete: interaction.kind === "request_item_verdicts"
7656
+ ? (interaction.result?.complete ?? false)
7657
+ : false,
7658
+ },
7659
+ });
7660
+ if (newlyResolvedItemIds.length > 0) {
7661
+ queueResolvedInteractionContinuationWakeup({
7662
+ heartbeat,
7663
+ issue,
7664
+ interaction,
7665
+ actor,
7666
+ source: "issue.interaction.verdicts",
7667
+ newlyResolvedItemIds,
7668
+ idempotencyKey: buildRequestItemVerdictsWakeIdempotencyKey({
7669
+ issueId: issue.id,
7670
+ interactionId: interaction.id,
7671
+ }),
7672
+ });
7673
+ }
7674
+ res.json(interaction);
7675
+ });
6276
7676
  router.post("/issues/:id/interactions/:interactionId/cancel", validate(cancelIssueThreadInteractionSchema), async (req, res) => {
6277
7677
  const id = req.params.id;
6278
7678
  const interactionId = req.params.interactionId;
@@ -7205,6 +8605,8 @@ export function issueRoutes(db, storage, opts = {}) {
7205
8605
  return;
7206
8606
  }
7207
8607
  assertCompanyAccess(req, issue.companyId);
8608
+ if (!(await assertIssueReadAllowed(req, res, issue)))
8609
+ return;
7208
8610
  const attachments = await svc.listAttachments(issueId);
7209
8611
  res.json(attachments.map(withContentPath));
7210
8612
  });
@@ -7302,6 +8704,13 @@ export function issueRoutes(db, storage, opts = {}) {
7302
8704
  return;
7303
8705
  }
7304
8706
  assertCompanyAccess(req, attachment.companyId);
8707
+ const issue = await svc.getById(attachment.issueId);
8708
+ if (!issue) {
8709
+ res.status(404).json({ error: "Issue not found" });
8710
+ return;
8711
+ }
8712
+ if (!(await assertIssueReadAllowed(req, res, issue)))
8713
+ return;
7305
8714
  const contentLength = attachment.byteSize;
7306
8715
  const range = parseAttachmentRangeHeader(typeof req.headers.range === "string" ? req.headers.range : undefined, contentLength);
7307
8716
  res.setHeader("Accept-Ranges", "bytes");