@evermore.work/server 2026.511.0-canary.2 → 2026.513.0-canary.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 (160) hide show
  1. package/dist/adapters/builtin-adapter-types.d.ts.map +1 -1
  2. package/dist/adapters/builtin-adapter-types.js +1 -0
  3. package/dist/adapters/builtin-adapter-types.js.map +1 -1
  4. package/dist/adapters/registry.d.ts.map +1 -1
  5. package/dist/adapters/registry.js +20 -2
  6. package/dist/adapters/registry.js.map +1 -1
  7. package/dist/app.d.ts.map +1 -1
  8. package/dist/app.js +1 -3
  9. package/dist/app.js.map +1 -1
  10. package/dist/home-paths.d.ts +2 -4
  11. package/dist/home-paths.d.ts.map +1 -1
  12. package/dist/home-paths.js +8 -35
  13. package/dist/home-paths.js.map +1 -1
  14. package/dist/routes/agents.d.ts.map +1 -1
  15. package/dist/routes/agents.js +8 -2
  16. package/dist/routes/agents.js.map +1 -1
  17. package/dist/routes/issues.d.ts.map +1 -1
  18. package/dist/routes/issues.js +172 -11
  19. package/dist/routes/issues.js.map +1 -1
  20. package/dist/routes/plugins.d.ts.map +1 -1
  21. package/dist/routes/plugins.js +7 -21
  22. package/dist/routes/plugins.js.map +1 -1
  23. package/dist/services/environment-execution-target.d.ts.map +1 -1
  24. package/dist/services/environment-execution-target.js +2 -0
  25. package/dist/services/environment-execution-target.js.map +1 -1
  26. package/dist/services/environment-runtime.d.ts.map +1 -1
  27. package/dist/services/environment-runtime.js +30 -4
  28. package/dist/services/environment-runtime.js.map +1 -1
  29. package/dist/services/heartbeat.d.ts.map +1 -1
  30. package/dist/services/heartbeat.js +8 -3
  31. package/dist/services/heartbeat.js.map +1 -1
  32. package/dist/services/index.d.ts +1 -0
  33. package/dist/services/index.d.ts.map +1 -1
  34. package/dist/services/index.js +1 -0
  35. package/dist/services/index.js.map +1 -1
  36. package/dist/services/issue-recovery-actions.d.ts +40 -0
  37. package/dist/services/issue-recovery-actions.d.ts.map +1 -0
  38. package/dist/services/issue-recovery-actions.js +204 -0
  39. package/dist/services/issue-recovery-actions.js.map +1 -0
  40. package/dist/services/issues.d.ts +20 -0
  41. package/dist/services/issues.d.ts.map +1 -1
  42. package/dist/services/issues.js +160 -10
  43. package/dist/services/issues.js.map +1 -1
  44. package/dist/services/plugin-config-validator.js +1 -1
  45. package/dist/services/plugin-config-validator.js.map +1 -1
  46. package/dist/services/plugin-dev-watcher.d.ts.map +1 -1
  47. package/dist/services/plugin-dev-watcher.js +7 -2
  48. package/dist/services/plugin-dev-watcher.js.map +1 -1
  49. package/dist/services/plugin-environment-driver.d.ts +2 -0
  50. package/dist/services/plugin-environment-driver.d.ts.map +1 -1
  51. package/dist/services/plugin-environment-driver.js +2 -0
  52. package/dist/services/plugin-environment-driver.js.map +1 -1
  53. package/dist/services/plugin-host-services.d.ts.map +1 -1
  54. package/dist/services/plugin-host-services.js +31 -4
  55. package/dist/services/plugin-host-services.js.map +1 -1
  56. package/dist/services/plugin-loader.d.ts +2 -2
  57. package/dist/services/plugin-loader.js +3 -3
  58. package/dist/services/plugin-loader.js.map +1 -1
  59. package/dist/services/plugin-local-folders.d.ts +1 -0
  60. package/dist/services/plugin-local-folders.d.ts.map +1 -1
  61. package/dist/services/plugin-local-folders.js +45 -0
  62. package/dist/services/plugin-local-folders.js.map +1 -1
  63. package/dist/services/plugin-managed-agents.d.ts.map +1 -1
  64. package/dist/services/plugin-managed-agents.js +52 -9
  65. package/dist/services/plugin-managed-agents.js.map +1 -1
  66. package/dist/services/plugin-managed-skills.d.ts +14 -0
  67. package/dist/services/plugin-managed-skills.d.ts.map +1 -0
  68. package/dist/services/plugin-managed-skills.js +264 -0
  69. package/dist/services/plugin-managed-skills.js.map +1 -0
  70. package/dist/services/plugin-registry.d.ts +1 -1
  71. package/dist/services/plugin-registry.js +1 -1
  72. package/dist/services/recovery/service.d.ts.map +1 -1
  73. package/dist/services/recovery/service.js +225 -63
  74. package/dist/services/recovery/service.js.map +1 -1
  75. package/dist/services/recovery/successful-run-handoff.d.ts +2 -0
  76. package/dist/services/recovery/successful-run-handoff.d.ts.map +1 -1
  77. package/dist/services/recovery/successful-run-handoff.js +8 -1
  78. package/dist/services/recovery/successful-run-handoff.js.map +1 -1
  79. package/dist/services/recovery/successful-run-handoff.test.js +5 -2
  80. package/dist/services/recovery/successful-run-handoff.test.js.map +1 -1
  81. package/package.json +14 -13
  82. package/skills/evermore-converting-plans-to-tasks/SKILL.md +1 -1
  83. package/skills/evermore-create-plugin/SKILL.md +95 -42
  84. package/skills/terminal-bench-loop/SKILL.md +4 -4
  85. package/ui-dist/assets/{_basePickBy-CbmMN1RH.js → _basePickBy-k9wCTyNE.js} +1 -1
  86. package/ui-dist/assets/{_baseUniq-D5Spi0LU.js → _baseUniq-gqdJ9_M4.js} +1 -1
  87. package/ui-dist/assets/{arc-7n9-L5yV.js → arc-CZ5fTbi1.js} +1 -1
  88. package/ui-dist/assets/{architectureDiagram-VXUJARFQ-CaCkwTrk.js → architectureDiagram-VXUJARFQ-C1PmJabO.js} +1 -1
  89. package/ui-dist/assets/{blockDiagram-VD42YOAC-C_gMX5sE.js → blockDiagram-VD42YOAC-C31GBmZl.js} +1 -1
  90. package/ui-dist/assets/{c4Diagram-YG6GDRKO-DsIPHY3x.js → c4Diagram-YG6GDRKO-CXALeUm_.js} +1 -1
  91. package/ui-dist/assets/channel-BscBiA2B.js +1 -0
  92. package/ui-dist/assets/{chunk-4BX2VUAB-ConiONwq.js → chunk-4BX2VUAB-CI9ame_l.js} +1 -1
  93. package/ui-dist/assets/{chunk-55IACEB6-rN7J69fU.js → chunk-55IACEB6-X7CMs2bY.js} +1 -1
  94. package/ui-dist/assets/{chunk-B4BG7PRW-CWrv2W6l.js → chunk-B4BG7PRW-CRFGJGGj.js} +1 -1
  95. package/ui-dist/assets/{chunk-DI55MBZ5-DxMrfgRx.js → chunk-DI55MBZ5-pFj4tWf4.js} +1 -1
  96. package/ui-dist/assets/{chunk-FMBD7UC4-C3IEVrmU.js → chunk-FMBD7UC4-DwtEpJHS.js} +1 -1
  97. package/ui-dist/assets/{chunk-QN33PNHL-mY7dIxLL.js → chunk-QN33PNHL-O_GMkM_d.js} +1 -1
  98. package/ui-dist/assets/{chunk-QZHKN3VN-C57cpB6v.js → chunk-QZHKN3VN-DVQoxgmw.js} +1 -1
  99. package/ui-dist/assets/{chunk-TZMSLE5B-Bznk-429.js → chunk-TZMSLE5B-nYT9Oclu.js} +1 -1
  100. package/ui-dist/assets/classDiagram-2ON5EDUG-DRC1sToY.js +1 -0
  101. package/ui-dist/assets/classDiagram-v2-WZHVMYZB-DRC1sToY.js +1 -0
  102. package/ui-dist/assets/clone-djKk5h1v.js +1 -0
  103. package/ui-dist/assets/{cose-bilkent-S5V4N54A-WVNEjOkH.js → cose-bilkent-S5V4N54A-DrymQvjx.js} +1 -1
  104. package/ui-dist/assets/{dagre-6UL2VRFP-B5zFofAq.js → dagre-6UL2VRFP-BSMS-awI.js} +1 -1
  105. package/ui-dist/assets/{diagram-PSM6KHXK-B5uBquxq.js → diagram-PSM6KHXK-CKA4xpzT.js} +1 -1
  106. package/ui-dist/assets/{diagram-QEK2KX5R--vjH56tj.js → diagram-QEK2KX5R-D224Hw3l.js} +1 -1
  107. package/ui-dist/assets/{diagram-S2PKOQOG-BrRUS4fb.js → diagram-S2PKOQOG-QSLRBXq3.js} +1 -1
  108. package/ui-dist/assets/{erDiagram-Q2GNP2WA-DEz4OctV.js → erDiagram-Q2GNP2WA-BMb1ltru.js} +1 -1
  109. package/ui-dist/assets/{flowDiagram-NV44I4VS-B15VynOf.js → flowDiagram-NV44I4VS-DFO86CtG.js} +1 -1
  110. package/ui-dist/assets/{ganttDiagram-JELNMOA3-DMZlAptH.js → ganttDiagram-JELNMOA3-DqGmEMSm.js} +1 -1
  111. package/ui-dist/assets/{gitGraphDiagram-V2S2FVAM-CcxPbN5-.js → gitGraphDiagram-V2S2FVAM-C58nf2i-.js} +1 -1
  112. package/ui-dist/assets/{graph-BZIdisjp.js → graph-DosSwXzr.js} +1 -1
  113. package/ui-dist/assets/{index-ZU3cQRJ_.js → index-3RqTP3Wv.js} +1 -1
  114. package/ui-dist/assets/{index-CMFXrEKF.js → index-4nkrIR74.js} +154 -153
  115. package/ui-dist/assets/index-BNTUqCLB.css +1 -0
  116. package/ui-dist/assets/{index-JK3bQafC.js → index-BWnvh-op.js} +1 -1
  117. package/ui-dist/assets/{index-C_B62ZrS.js → index-BbNMaRBx.js} +1 -1
  118. package/ui-dist/assets/{index-D9_Jej4N.js → index-BfcmN5ZJ.js} +1 -1
  119. package/ui-dist/assets/{index-CLpI7eai.js → index-BgdsvI-o.js} +1 -1
  120. package/ui-dist/assets/{index-DPcKcWrz.js → index-BrTEhQHs.js} +1 -1
  121. package/ui-dist/assets/{index-CguyMMMe.js → index-C37UJrtl.js} +1 -1
  122. package/ui-dist/assets/{index-B8AK13gN.js → index-Ci5TGFP3.js} +1 -1
  123. package/ui-dist/assets/{index-CBsefjme.js → index-CqyFdYQi.js} +1 -1
  124. package/ui-dist/assets/{index-jrAeI9QO.js → index-Cu1tIzzo.js} +1 -1
  125. package/ui-dist/assets/{index-QcuAFhJE.js → index-CvQSsdq1.js} +1 -1
  126. package/ui-dist/assets/{index-CtbPMroJ.js → index-D6e8ieSZ.js} +1 -1
  127. package/ui-dist/assets/{index-Caiv3tFo.js → index-D9fEHHhi.js} +1 -1
  128. package/ui-dist/assets/{index-Br9DlaVa.js → index-DU3gaUtN.js} +1 -1
  129. package/ui-dist/assets/{index-B9gUF7Qo.js → index-DozqwUGT.js} +1 -1
  130. package/ui-dist/assets/{index-B2TuAYBk.js → index-DsbA9pOP.js} +1 -1
  131. package/ui-dist/assets/{index-BEgNCEf4.js → index-Dz0v0MS4.js} +1 -1
  132. package/ui-dist/assets/{index-DChtU7Y7.js → index-Oyl53tyz.js} +1 -1
  133. package/ui-dist/assets/{index-BXCWvVzU.js → index-ae2EPSbU.js} +1 -1
  134. package/ui-dist/assets/{index-C0ActS50.js → index-cxYbeASY.js} +1 -1
  135. package/ui-dist/assets/{index-CuiPTihA.js → index-gB4esTR_.js} +1 -1
  136. package/ui-dist/assets/{index-DZAJwG4j.js → index-uX_QygSO.js} +1 -1
  137. package/ui-dist/assets/{infoDiagram-HS3SLOUP-C2wYobHy.js → infoDiagram-HS3SLOUP-fH1KZK8Y.js} +1 -1
  138. package/ui-dist/assets/{journeyDiagram-XKPGCS4Q-BtfEQPBF.js → journeyDiagram-XKPGCS4Q-Bs6gBpT7.js} +1 -1
  139. package/ui-dist/assets/{kanban-definition-3W4ZIXB7-B4U_W3OQ.js → kanban-definition-3W4ZIXB7-CjXiZk7z.js} +1 -1
  140. package/ui-dist/assets/{layout-ZIWcjZTf.js → layout-BNsoNCJt.js} +1 -1
  141. package/ui-dist/assets/{linear-D7hp1mR1.js → linear-D4_nHGWF.js} +1 -1
  142. package/ui-dist/assets/{mermaid.core-npjWckTB.js → mermaid.core-QfFHliw3.js} +4 -4
  143. package/ui-dist/assets/{mindmap-definition-VGOIOE7T-Cno5l-kW.js → mindmap-definition-VGOIOE7T-B_dKQ4Qp.js} +1 -1
  144. package/ui-dist/assets/{pieDiagram-ADFJNKIX-Ce0R1e38.js → pieDiagram-ADFJNKIX-DflXWaiu.js} +1 -1
  145. package/ui-dist/assets/{quadrantDiagram-AYHSOK5B-BNZ-4uth.js → quadrantDiagram-AYHSOK5B-Bt3RB7oE.js} +1 -1
  146. package/ui-dist/assets/{requirementDiagram-UZGBJVZJ-BIdjI2CO.js → requirementDiagram-UZGBJVZJ-DxsHOEgt.js} +1 -1
  147. package/ui-dist/assets/{sankeyDiagram-TZEHDZUN-hYF-IP2X.js → sankeyDiagram-TZEHDZUN-DilTmstc.js} +1 -1
  148. package/ui-dist/assets/{sequenceDiagram-WL72ISMW-CBreF6um.js → sequenceDiagram-WL72ISMW-BYRLNxvZ.js} +1 -1
  149. package/ui-dist/assets/{stateDiagram-FKZM4ZOC-DOje8UoB.js → stateDiagram-FKZM4ZOC-D0jvEPoV.js} +1 -1
  150. package/ui-dist/assets/stateDiagram-v2-4FDKWEC3-D_kHbugj.js +1 -0
  151. package/ui-dist/assets/{timeline-definition-IT6M3QCI-Da_eKz_x.js → timeline-definition-IT6M3QCI-KyT1QpWq.js} +1 -1
  152. package/ui-dist/assets/{treemap-GDKQZRPO-BAFi4Uf3.js → treemap-GDKQZRPO-BEa3HnWS.js} +1 -1
  153. package/ui-dist/assets/{xychartDiagram-PRI3JC2R-Bc_vIoPh.js → xychartDiagram-PRI3JC2R-BUxqP2No.js} +1 -1
  154. package/ui-dist/index.html +2 -2
  155. package/ui-dist/assets/channel-zaN5fJQ5.js +0 -1
  156. package/ui-dist/assets/classDiagram-2ON5EDUG-CZ5AdJ2X.js +0 -1
  157. package/ui-dist/assets/classDiagram-v2-WZHVMYZB-CZ5AdJ2X.js +0 -1
  158. package/ui-dist/assets/clone-CgK10jEJ.js +0 -1
  159. package/ui-dist/assets/index-B7sbRkCH.css +0 -1
  160. package/ui-dist/assets/stateDiagram-v2-4FDKWEC3-BtMV8Rt1.js +0 -1
@@ -2,14 +2,14 @@ import { randomUUID } from "node:crypto";
2
2
  import { Router } from "express";
3
3
  import multer from "multer";
4
4
  import { z } from "zod";
5
- import { and, desc, eq, inArray } from "drizzle-orm";
6
- import { activityLog, executionWorkspaces, issueExecutionDecisions, projectWorkspaces } from "@evermore.work/db";
7
- import { addIssueCommentSchema, acceptIssueThreadInteractionSchema, cancelIssueThreadInteractionSchema, companySearchQuerySchema, createIssueAttachmentMetadataSchema, createIssueThreadInteractionSchema, createIssueWorkProductSchema, createIssueLabelSchema, checkoutIssueSchema, createChildIssueSchema, createIssueSchema, resolveCreateIssueStatusDefault, feedbackTargetTypeSchema, feedbackTraceStatusSchema, feedbackVoteValueSchema, upsertIssueFeedbackVoteSchema, linkIssueApprovalSchema, issueDocumentKeySchema, ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, rejectIssueThreadInteractionSchema, restoreIssueDocumentRevisionSchema, respondIssueThreadInteractionSchema, updateIssueWorkProductSchema, upsertIssueDocumentSchema, updateIssueSchema, getClosedIsolatedExecutionWorkspaceMessage, isClosedIsolatedExecutionWorkspace, normalizeIssueIdentifier as normalizeIssueReferenceIdentifier, } from "@evermore.work/shared";
5
+ import { and, desc, eq, inArray, notInArray } from "drizzle-orm";
6
+ import { activityLog, executionWorkspaces, issueExecutionDecisions, issueRelations, issues as issueRows, projectWorkspaces, } from "@evermore.work/db";
7
+ import { addIssueCommentSchema, acceptIssueThreadInteractionSchema, cancelIssueThreadInteractionSchema, companySearchQuerySchema, createIssueAttachmentMetadataSchema, createIssueThreadInteractionSchema, createIssueWorkProductSchema, createIssueLabelSchema, checkoutIssueSchema, createChildIssueSchema, createIssueSchema, resolveCreateIssueStatusDefault, resolveIssueRecoveryActionSchema, feedbackTargetTypeSchema, feedbackTraceStatusSchema, feedbackVoteValueSchema, upsertIssueFeedbackVoteSchema, linkIssueApprovalSchema, issueDocumentKeySchema, ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, rejectIssueThreadInteractionSchema, restoreIssueDocumentRevisionSchema, respondIssueThreadInteractionSchema, updateIssueWorkProductSchema, upsertIssueDocumentSchema, updateIssueSchema, getClosedIsolatedExecutionWorkspaceMessage, isClosedIsolatedExecutionWorkspace, normalizeIssueIdentifier as normalizeIssueReferenceIdentifier, } from "@evermore.work/shared";
8
8
  import { trackAgentTaskCompleted } from "@evermore.work/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, issueThreadInteractionService, ISSUE_LIST_DEFAULT_LIMIT, ISSUE_LIST_MAX_LIMIT, issueReferenceService, issueService, clampIssueListLimit, documentService, logActivity, projectService, routineService, workProductService, } 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, logActivity, projectService, routineService, workProductService, } from "../services/index.js";
13
13
  import { logger } from "../middleware/logger.js";
14
14
  import { conflict, forbidden, HttpError, notFound, unauthorized, unprocessable } from "../errors.js";
15
15
  import { assertBoard, assertCompanyAccess, getActorInfo } from "./authz.js";
@@ -235,6 +235,34 @@ async function listSuccessfulRunHandoffStates(db, companyId, issueIds) {
235
235
  }
236
236
  return states;
237
237
  }
238
+ async function relationRecoveryActionMap(recoveryActionsSvc, companyId, relations) {
239
+ const candidates = [];
240
+ const visit = (summary) => {
241
+ candidates.push(summary);
242
+ for (const terminal of summary.terminalBlockers ?? []) {
243
+ visit(terminal);
244
+ }
245
+ };
246
+ for (const blocker of relations.blockedBy)
247
+ visit(blocker);
248
+ for (const blocking of relations.blocks)
249
+ visit(blocking);
250
+ if (candidates.length === 0)
251
+ return new Map();
252
+ const ids = [...new Set(candidates.map((summary) => summary.id))];
253
+ return recoveryActionsSvc.listActiveForIssues(companyId, ids);
254
+ }
255
+ function withRecoveryActionsOnRelationSummaries(relations, recoveryActionByIssueId) {
256
+ const augment = (summary) => ({
257
+ ...summary,
258
+ activeRecoveryAction: recoveryActionByIssueId.get(summary.id) ?? summary.activeRecoveryAction ?? null,
259
+ terminalBlockers: summary.terminalBlockers?.map(augment),
260
+ });
261
+ return {
262
+ blockedBy: relations.blockedBy.map(augment),
263
+ blocks: relations.blocks.map(augment),
264
+ };
265
+ }
238
266
  const ACTIVE_REVIEW_APPROVAL_STATUSES = new Set(["pending", "revision_requested"]);
239
267
  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. " +
240
268
  "This request would leave the issue in_review without anyone or anything owning the next action. " +
@@ -512,6 +540,7 @@ export function issueRoutes(db, storage, opts = {}) {
512
540
  const projectsSvc = projectService(db);
513
541
  const goalsSvc = goalService(db);
514
542
  const issueApprovalsSvc = issueApprovalService(db);
543
+ const recoveryActionsSvc = issueRecoveryActionService(db);
515
544
  const executionWorkspacesSvc = executionWorkspaceServiceDirect(db);
516
545
  const workProductsSvc = workProductService(db);
517
546
  const documentsSvc = documentService(db);
@@ -1082,10 +1111,15 @@ export function issueRoutes(db, storage, opts = {}) {
1082
1111
  limit,
1083
1112
  offset,
1084
1113
  });
1085
- const handoffStates = await listSuccessfulRunHandoffStates(db, companyId, result.map((issue) => issue.id));
1114
+ const issueIds = result.map((issue) => issue.id);
1115
+ const [handoffStates, recoveryActionByIssue] = await Promise.all([
1116
+ listSuccessfulRunHandoffStates(db, companyId, issueIds),
1117
+ recoveryActionsSvc.listActiveForIssues(companyId, issueIds),
1118
+ ]);
1086
1119
  res.json(result.map((issue) => ({
1087
1120
  ...issue,
1088
1121
  successfulRunHandoff: handoffStates.get(issue.id) ?? null,
1122
+ activeRecoveryAction: recoveryActionByIssue.get(issue.id) ?? null,
1089
1123
  })));
1090
1124
  });
1091
1125
  router.get("/companies/:companyId/labels", async (req, res) => {
@@ -1153,7 +1187,7 @@ export function issueRoutes(db, storage, opts = {}) {
1153
1187
  const currentExecutionWorkspacePromise = issue.executionWorkspaceId
1154
1188
  ? executionWorkspacesSvc.getById(issue.executionWorkspaceId)
1155
1189
  : Promise.resolve(null);
1156
- const [{ project, goal }, ancestors, commentCursor, wakeComment, relations, blockerAttention, productivityReview, scheduledRetry, attachments, continuationSummary, currentExecutionWorkspace,] = await Promise.all([
1190
+ const [{ project, goal }, ancestors, commentCursor, wakeComment, relations, blockerAttention, productivityReview, scheduledRetry, attachments, continuationSummary, currentExecutionWorkspace, activeRecoveryAction,] = await Promise.all([
1157
1191
  resolveIssueProjectAndGoal(issue),
1158
1192
  svc.getAncestors(issue.id),
1159
1193
  svc.getCommentCursor(issue.id),
@@ -1165,7 +1199,10 @@ export function issueRoutes(db, storage, opts = {}) {
1165
1199
  svc.listAttachments(issue.id),
1166
1200
  documentsSvc.getIssueDocumentByKey(issue.id, ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY),
1167
1201
  currentExecutionWorkspacePromise,
1202
+ recoveryActionsSvc.getActiveForIssue(issue.companyId, issue.id),
1168
1203
  ]);
1204
+ const recoveryActionsByRelationIssue = await relationRecoveryActionMap(recoveryActionsSvc, issue.companyId, relations);
1205
+ const relationsWithRecoveryActions = withRecoveryActionsOnRelationSummaries(relations, recoveryActionsByRelationIssue);
1169
1206
  res.json({
1170
1207
  issue: {
1171
1208
  id: issue.id,
@@ -1177,12 +1214,13 @@ export function issueRoutes(db, storage, opts = {}) {
1177
1214
  ...(blockerAttention ? { blockerAttention } : {}),
1178
1215
  productivityReview,
1179
1216
  scheduledRetry,
1217
+ activeRecoveryAction,
1180
1218
  priority: issue.priority,
1181
1219
  projectId: issue.projectId,
1182
1220
  goalId: goal?.id ?? issue.goalId,
1183
1221
  parentId: issue.parentId,
1184
- blockedBy: relations.blockedBy,
1185
- blocks: relations.blocks,
1222
+ blockedBy: relationsWithRecoveryActions.blockedBy,
1223
+ blocks: relationsWithRecoveryActions.blocks,
1186
1224
  assigneeAgentId: issue.assigneeAgentId,
1187
1225
  assigneeUserId: issue.assigneeUserId,
1188
1226
  originKind: issue.originKind,
@@ -1246,7 +1284,7 @@ export function issueRoutes(db, storage, opts = {}) {
1246
1284
  return;
1247
1285
  }
1248
1286
  assertCompanyAccess(req, issue.companyId);
1249
- const [{ project, goal }, ancestors, mentionedProjectIds, documentPayload, relations, blockerAttention, productivityReview, referenceSummary, successfulRunHandoffStates, scheduledRetry,] = await Promise.all([
1287
+ const [{ project, goal }, ancestors, mentionedProjectIds, documentPayload, relations, blockerAttention, productivityReview, referenceSummary, successfulRunHandoffStates, scheduledRetry, activeRecoveryAction,] = await Promise.all([
1250
1288
  resolveIssueProjectAndGoal(issue),
1251
1289
  svc.getAncestors(issue.id),
1252
1290
  svc.findMentionedProjectIds(issue.id, { includeCommentBodies: false }),
@@ -1257,7 +1295,10 @@ export function issueRoutes(db, storage, opts = {}) {
1257
1295
  issueReferencesSvc.listIssueReferenceSummary(issue.id),
1258
1296
  listSuccessfulRunHandoffStates(db, issue.companyId, [issue.id]),
1259
1297
  svc.getCurrentScheduledRetry(issue.id),
1298
+ recoveryActionsSvc.getActiveForIssue(issue.companyId, issue.id),
1260
1299
  ]);
1300
+ const recoveryActionsByRelationIssue = await relationRecoveryActionMap(recoveryActionsSvc, issue.companyId, relations);
1301
+ const relationsWithRecoveryActions = withRecoveryActionsOnRelationSummaries(relations, recoveryActionsByRelationIssue);
1261
1302
  const mentionedProjects = mentionedProjectIds.length > 0
1262
1303
  ? await projectsSvc.listByIds(issue.companyId, mentionedProjectIds)
1263
1304
  : [];
@@ -1273,8 +1314,9 @@ export function issueRoutes(db, storage, opts = {}) {
1273
1314
  productivityReview,
1274
1315
  successfulRunHandoff: successfulRunHandoffStates.get(issue.id) ?? null,
1275
1316
  scheduledRetry,
1276
- blockedBy: relations.blockedBy,
1277
- blocks: relations.blocks,
1317
+ activeRecoveryAction,
1318
+ blockedBy: relationsWithRecoveryActions.blockedBy,
1319
+ blocks: relationsWithRecoveryActions.blocks,
1278
1320
  relatedWork: referenceSummary,
1279
1321
  referencedIssueIdentifiers: referenceSummary.outbound.map((item) => item.issue.identifier ?? item.issue.id),
1280
1322
  ...documentPayload,
@@ -1285,6 +1327,125 @@ export function issueRoutes(db, storage, opts = {}) {
1285
1327
  workProducts,
1286
1328
  });
1287
1329
  });
1330
+ router.get("/issues/:id/recovery-actions", async (req, res) => {
1331
+ const id = req.params.id;
1332
+ const issue = await svc.getById(id);
1333
+ if (!issue) {
1334
+ res.status(404).json({ error: "Issue not found" });
1335
+ return;
1336
+ }
1337
+ assertCompanyAccess(req, issue.companyId);
1338
+ const active = await recoveryActionsSvc.getActiveForIssue(issue.companyId, issue.id);
1339
+ res.json({
1340
+ active,
1341
+ actions: active ? [active] : [],
1342
+ });
1343
+ });
1344
+ router.post("/issues/:id/recovery-actions/resolve", validate(resolveIssueRecoveryActionSchema), async (req, res) => {
1345
+ const id = req.params.id;
1346
+ const existing = await svc.getById(id);
1347
+ if (!existing) {
1348
+ res.status(404).json({ error: "Issue not found" });
1349
+ return;
1350
+ }
1351
+ assertCompanyAccess(req, existing.companyId);
1352
+ if (!(await assertAgentIssueMutationAllowed(req, res, existing)))
1353
+ return;
1354
+ const { actionId, outcome, sourceIssueStatus, resolutionNote } = req.body;
1355
+ if (outcome === "false_positive" || outcome === "cancelled") {
1356
+ assertBoard(req);
1357
+ }
1358
+ const actor = getActorInfo(req);
1359
+ const updateFields = sourceIssueStatus ? { status: sourceIssueStatus } : {};
1360
+ await assertAgentInReviewReviewPath({
1361
+ existing,
1362
+ updateFields,
1363
+ actorType: req.actor.type,
1364
+ });
1365
+ const actionStatus = outcome === "cancelled" ? "cancelled" : "resolved";
1366
+ const result = await db.transaction(async (tx) => {
1367
+ let issue = existing;
1368
+ if (outcome === "blocked") {
1369
+ const unresolvedBlockers = await tx
1370
+ .select({ id: issueRows.id })
1371
+ .from(issueRelations)
1372
+ .innerJoin(issueRows, eq(issueRelations.issueId, issueRows.id))
1373
+ .where(and(eq(issueRelations.companyId, existing.companyId), eq(issueRelations.relatedIssueId, existing.id), eq(issueRelations.type, "blocks"), notInArray(issueRows.status, ["done", "cancelled"])))
1374
+ .limit(1);
1375
+ if (unresolvedBlockers.length === 0) {
1376
+ throw unprocessable("Blocked recovery resolution requires an unresolved first-class blocker on the source issue");
1377
+ }
1378
+ }
1379
+ if (sourceIssueStatus) {
1380
+ const updatedIssue = await svc.update(id, {
1381
+ status: sourceIssueStatus,
1382
+ actorAgentId: actor.agentId ?? null,
1383
+ actorUserId: actor.actorType === "user" ? actor.actorId : null,
1384
+ }, tx);
1385
+ if (!updatedIssue)
1386
+ throw notFound("Issue not found");
1387
+ issue = updatedIssue;
1388
+ }
1389
+ const recoveryAction = await recoveryActionsSvc.resolveActiveForIssue({
1390
+ companyId: existing.companyId,
1391
+ sourceIssueId: existing.id,
1392
+ actionId: actionId ?? null,
1393
+ status: actionStatus,
1394
+ outcome,
1395
+ resolutionNote: resolutionNote ?? null,
1396
+ }, tx);
1397
+ if (!recoveryAction)
1398
+ throw notFound("Active recovery action not found");
1399
+ return { issue, recoveryAction };
1400
+ });
1401
+ await routinesSvc.syncRunStatusForIssue(result.issue.id);
1402
+ if (sourceIssueStatus && existing.status !== result.issue.status) {
1403
+ await logActivity(db, {
1404
+ companyId: result.issue.companyId,
1405
+ actorType: actor.actorType,
1406
+ actorId: actor.actorId,
1407
+ agentId: actor.agentId,
1408
+ runId: actor.runId,
1409
+ action: "issue.updated",
1410
+ entityType: "issue",
1411
+ entityId: result.issue.id,
1412
+ details: {
1413
+ identifier: result.issue.identifier,
1414
+ status: result.issue.status,
1415
+ source: "recovery_action_resolution",
1416
+ recoveryActionId: result.recoveryAction.id,
1417
+ _previous: {
1418
+ status: existing.status,
1419
+ },
1420
+ },
1421
+ });
1422
+ }
1423
+ await logActivity(db, {
1424
+ companyId: result.issue.companyId,
1425
+ actorType: actor.actorType,
1426
+ actorId: actor.actorId,
1427
+ agentId: actor.agentId,
1428
+ runId: actor.runId,
1429
+ action: "issue.recovery_action_resolved",
1430
+ entityType: "issue",
1431
+ entityId: result.issue.id,
1432
+ details: {
1433
+ identifier: result.issue.identifier,
1434
+ recoveryActionId: result.recoveryAction.id,
1435
+ recoveryActionStatus: result.recoveryAction.status,
1436
+ outcome: result.recoveryAction.outcome,
1437
+ sourceIssueStatus: sourceIssueStatus ?? null,
1438
+ resolutionNote: result.recoveryAction.resolutionNote,
1439
+ },
1440
+ });
1441
+ res.json({
1442
+ issue: {
1443
+ ...result.issue,
1444
+ activeRecoveryAction: null,
1445
+ },
1446
+ recoveryAction: result.recoveryAction,
1447
+ });
1448
+ });
1288
1449
  router.get("/issues/:id/work-products", async (req, res) => {
1289
1450
  const id = req.params.id;
1290
1451
  const issue = await svc.getById(id);