agent-relay-server 0.120.2 → 0.120.3

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 (141) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +3 -3
  3. package/public/assets/{MessageBubble-DazQ9Rn-.js → MessageBubble-Cqi3XsZ7.js} +2 -2
  4. package/public/assets/{MessageBubble-DazQ9Rn-.js.map → MessageBubble-Cqi3XsZ7.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-DpTiGCkz.js → PlanGraphPanel-D6Um05oW.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-DpTiGCkz.js.map → PlanGraphPanel-D6Um05oW.js.map} +1 -1
  7. package/public/assets/{activity-DReCE2mK.js → activity-B0LsQHEz.js} +2 -2
  8. package/public/assets/{activity-DReCE2mK.js.map → activity-B0LsQHEz.js.map} +1 -1
  9. package/public/assets/{agent-profiles-D3ba8Xpe.js → agent-profiles-O--pe5Ic.js} +2 -2
  10. package/public/assets/{agent-profiles-D3ba8Xpe.js.map → agent-profiles-O--pe5Ic.js.map} +1 -1
  11. package/public/assets/{agent-type-icon-BEz393ra.js → agent-type-icon-CeEoMouY.js} +2 -2
  12. package/public/assets/{agent-type-icon-BEz393ra.js.map → agent-type-icon-CeEoMouY.js.map} +1 -1
  13. package/public/assets/{agents-BcsIxadm.js → agents-Ii6XlVwR.js} +2 -2
  14. package/public/assets/{agents-BcsIxadm.js.map → agents-Ii6XlVwR.js.map} +1 -1
  15. package/public/assets/{analytics-0lOlkaib.js → analytics-BCzdYWRT.js} +2 -2
  16. package/public/assets/{analytics-0lOlkaib.js.map → analytics-BCzdYWRT.js.map} +1 -1
  17. package/public/assets/{automation-D66mthBn.js → automation-Crr3Xzui.js} +2 -2
  18. package/public/assets/{automation-D66mthBn.js.map → automation-Crr3Xzui.js.map} +1 -1
  19. package/public/assets/{badge-BhUZ1cAb.js → badge-DCIT3Irj.js} +2 -2
  20. package/public/assets/{badge-BhUZ1cAb.js.map → badge-DCIT3Irj.js.map} +1 -1
  21. package/public/assets/{branch-state-badge-NH-l2Eh3.js → branch-state-badge-Bhw2rloy.js} +2 -2
  22. package/public/assets/{branch-state-badge-NH-l2Eh3.js.map → branch-state-badge-Bhw2rloy.js.map} +1 -1
  23. package/public/assets/{button-C-V4oij1.js → button-CzdTkRIf.js} +2 -2
  24. package/public/assets/{button-C-V4oij1.js.map → button-CzdTkRIf.js.map} +1 -1
  25. package/public/assets/{card-Br1JLjTH.js → card-C3BGuVC6.js} +2 -2
  26. package/public/assets/{card-Br1JLjTH.js.map → card-C3BGuVC6.js.map} +1 -1
  27. package/public/assets/{channels-BlHLMQVk.js → channels-CKs9rvsn.js} +2 -2
  28. package/public/assets/{channels-BlHLMQVk.js.map → channels-CKs9rvsn.js.map} +1 -1
  29. package/public/assets/{chat-rwffRsPX.js → chat-BCwEUj5R.js} +2 -2
  30. package/public/assets/{chat-rwffRsPX.js.map → chat-BCwEUj5R.js.map} +1 -1
  31. package/public/assets/{connectors-CdsiaLD3.js → connectors-D_kQR_vw.js} +2 -2
  32. package/public/assets/{connectors-CdsiaLD3.js.map → connectors-D_kQR_vw.js.map} +1 -1
  33. package/public/assets/{copy-button-BStzXgtd.js → copy-button-B3o1jpLu.js} +2 -2
  34. package/public/assets/{copy-button-BStzXgtd.js.map → copy-button-B3o1jpLu.js.map} +1 -1
  35. package/public/assets/{dist-ChDX_t12.js → dist-B-T7lg0a.js} +2 -2
  36. package/public/assets/{dist-ChDX_t12.js.map → dist-B-T7lg0a.js.map} +1 -1
  37. package/public/assets/{dist-DV83j9tm.js → dist-B3e4B9pg.js} +2 -2
  38. package/public/assets/{dist-DV83j9tm.js.map → dist-B3e4B9pg.js.map} +1 -1
  39. package/public/assets/{dist-y03IwOrd.js → dist-B6woQ9Tb.js} +2 -2
  40. package/public/assets/{dist-y03IwOrd.js.map → dist-B6woQ9Tb.js.map} +1 -1
  41. package/public/assets/{dist-CQ0Z-TDi.js → dist-B9G83DXv.js} +2 -2
  42. package/public/assets/{dist-CQ0Z-TDi.js.map → dist-B9G83DXv.js.map} +1 -1
  43. package/public/assets/{dist-Cs8X0P1d.js → dist-BIjlN-xB.js} +2 -2
  44. package/public/assets/{dist-Cs8X0P1d.js.map → dist-BIjlN-xB.js.map} +1 -1
  45. package/public/assets/{dist-DyNJNwpi.js → dist-BInckSJC.js} +2 -2
  46. package/public/assets/{dist-DyNJNwpi.js.map → dist-BInckSJC.js.map} +1 -1
  47. package/public/assets/{dist-CmFeEXMe.js → dist-BlzVGRhh.js} +2 -2
  48. package/public/assets/{dist-CmFeEXMe.js.map → dist-BlzVGRhh.js.map} +1 -1
  49. package/public/assets/{dist-CDZZxxJq.js → dist-C7aTlosb.js} +2 -2
  50. package/public/assets/{dist-CDZZxxJq.js.map → dist-C7aTlosb.js.map} +1 -1
  51. package/public/assets/{dist-D2jvo5oA.js → dist-DGfp5dLz.js} +2 -2
  52. package/public/assets/{dist-D2jvo5oA.js.map → dist-DGfp5dLz.js.map} +1 -1
  53. package/public/assets/{dist-mm7c7nBt.js → dist-DT-OV6sQ.js} +2 -2
  54. package/public/assets/{dist-mm7c7nBt.js.map → dist-DT-OV6sQ.js.map} +1 -1
  55. package/public/assets/{dist-CBMWGhWc.js → dist-OUPTEP6q.js} +2 -2
  56. package/public/assets/{dist-CBMWGhWc.js.map → dist-OUPTEP6q.js.map} +1 -1
  57. package/public/assets/{es2015-VnZ2kVIz.js → es2015-CnKXYKpf.js} +2 -2
  58. package/public/assets/{es2015-VnZ2kVIz.js.map → es2015-CnKXYKpf.js.map} +1 -1
  59. package/public/assets/{formatted-body-impl-D_ZJQVNk.js → formatted-body-impl-CSRGuMcM.js} +2 -2
  60. package/public/assets/{formatted-body-impl-D_ZJQVNk.js.map → formatted-body-impl-CSRGuMcM.js.map} +1 -1
  61. package/public/assets/formatted-body-q_qzt1L-.js +3 -0
  62. package/public/assets/{formatted-body-C1AprxyU.js.map → formatted-body-q_qzt1L-.js.map} +1 -1
  63. package/public/assets/{index-DfPouNiV.js → index-Ceiwd02X.js} +6 -6
  64. package/public/assets/{index-DfPouNiV.js.map → index-Ceiwd02X.js.map} +1 -1
  65. package/public/assets/{input-CtLT7WXi.js → input-B6Q0NWCS.js} +2 -2
  66. package/public/assets/{input-CtLT7WXi.js.map → input-B6Q0NWCS.js.map} +1 -1
  67. package/public/assets/{insights-BJf1vwrf.js → insights-DUhXqZTZ.js} +2 -2
  68. package/public/assets/{insights-BJf1vwrf.js.map → insights-DUhXqZTZ.js.map} +1 -1
  69. package/public/assets/{integrations-BZSHasnY.js → integrations-lbg5CjtE.js} +2 -2
  70. package/public/assets/{integrations-BZSHasnY.js.map → integrations-lbg5CjtE.js.map} +1 -1
  71. package/public/assets/{lib-ByLQqcD1.js → lib-BHsWPMKB.js} +2 -2
  72. package/public/assets/{lib-ByLQqcD1.js.map → lib-BHsWPMKB.js.map} +1 -1
  73. package/public/assets/lucide-react-DuhlF2Sj.js +10 -0
  74. package/public/assets/lucide-react-DuhlF2Sj.js.map +1 -0
  75. package/public/assets/{maintenance-uEI1lArh.js → maintenance-CQgneSVf.js} +2 -2
  76. package/public/assets/{maintenance-uEI1lArh.js.map → maintenance-CQgneSVf.js.map} +1 -1
  77. package/public/assets/{managed-agents-mX3hHpOP.js → managed-agents-DsnYf6fm.js} +2 -2
  78. package/public/assets/{managed-agents-mX3hHpOP.js.map → managed-agents-DsnYf6fm.js.map} +1 -1
  79. package/public/assets/{markdown-preview-impl-ClNwMC48.js → markdown-preview-impl-C1ZtGJp6.js} +2 -2
  80. package/public/assets/{markdown-preview-impl-ClNwMC48.js.map → markdown-preview-impl-C1ZtGJp6.js.map} +1 -1
  81. package/public/assets/{memory-OfusEzGh.js → memory-SSLUTOI2.js} +2 -2
  82. package/public/assets/{memory-OfusEzGh.js.map → memory-SSLUTOI2.js.map} +1 -1
  83. package/public/assets/{messages-DsQGj0gU.js → messages-BY26OP4K.js} +2 -2
  84. package/public/assets/{messages-DsQGj0gU.js.map → messages-BY26OP4K.js.map} +1 -1
  85. package/public/assets/{orchestrators-BwWyl4mB.js → orchestrators-OY7yPzVI.js} +2 -2
  86. package/public/assets/{orchestrators-BwWyl4mB.js.map → orchestrators-OY7yPzVI.js.map} +1 -1
  87. package/public/assets/{overview-CED1SeTY.js → overview-Dj0u1tPG.js} +2 -2
  88. package/public/assets/{overview-CED1SeTY.js.map → overview-Dj0u1tPG.js.map} +1 -1
  89. package/public/assets/{pairs-BNu6ZWg4.js → pairs-CrUR50ou.js} +2 -2
  90. package/public/assets/{pairs-BNu6ZWg4.js.map → pairs-CrUR50ou.js.map} +1 -1
  91. package/public/assets/{projects-BmopRiUD.js → projects-B_AtxHtH.js} +2 -2
  92. package/public/assets/{projects-BmopRiUD.js.map → projects-B_AtxHtH.js.map} +1 -1
  93. package/public/assets/{prompt-settings-Ci2n1cFt.js → prompt-settings-CPtYSban.js} +2 -2
  94. package/public/assets/{prompt-settings-Ci2n1cFt.js.map → prompt-settings-CPtYSban.js.map} +1 -1
  95. package/public/assets/{react-dom-cLkVFBsS.js → react-dom-DWBmcawP.js} +2 -2
  96. package/public/assets/{react-dom-cLkVFBsS.js.map → react-dom-DWBmcawP.js.map} +1 -1
  97. package/public/assets/{registry-DwXnDIql.js → registry-BgUALWn5.js} +2 -2
  98. package/public/assets/{registry-DwXnDIql.js.map → registry-BgUALWn5.js.map} +1 -1
  99. package/public/assets/{security-AW5EbdOm.js → security-5lHfGk7g.js} +2 -2
  100. package/public/assets/{security-AW5EbdOm.js.map → security-5lHfGk7g.js.map} +1 -1
  101. package/public/assets/{select-CEeWX4hp.js → select-Cgx4QSLj.js} +2 -2
  102. package/public/assets/{select-CEeWX4hp.js.map → select-Cgx4QSLj.js.map} +1 -1
  103. package/public/assets/{settings-DYUdGz4G.js → settings-DFGgxyun.js} +2 -2
  104. package/public/assets/{settings-DYUdGz4G.js.map → settings-DFGgxyun.js.map} +1 -1
  105. package/public/assets/{store-BaslPLIP.js → store-BM4zM89B.js} +3 -3
  106. package/public/assets/store-BM4zM89B.js.map +1 -0
  107. package/public/assets/{task-chip-kxk4eNm1.js → task-chip-DXfvXBDx.js} +2 -2
  108. package/public/assets/{task-chip-kxk4eNm1.js.map → task-chip-DXfvXBDx.js.map} +1 -1
  109. package/public/assets/{tasks-DxCsrY4g.js → tasks-BvH5rluu.js} +2 -2
  110. package/public/assets/{tasks-DxCsrY4g.js.map → tasks-BvH5rluu.js.map} +1 -1
  111. package/public/assets/{teams-CF7vi-oM.js → teams-VZOIOdoE.js} +2 -2
  112. package/public/assets/{teams-CF7vi-oM.js.map → teams-VZOIOdoE.js.map} +1 -1
  113. package/public/assets/{terminal-viewer-impl-Dy3vqOnW.js → terminal-viewer-impl-DYY3Wmoe.js} +2 -2
  114. package/public/assets/{terminal-viewer-impl-Dy3vqOnW.js.map → terminal-viewer-impl-DYY3Wmoe.js.map} +1 -1
  115. package/public/assets/{use-keyboard-viewport-B06XtRUZ.js → use-keyboard-viewport-CP109w9y.js} +2 -2
  116. package/public/assets/{use-keyboard-viewport-B06XtRUZ.js.map → use-keyboard-viewport-CP109w9y.js.map} +1 -1
  117. package/public/assets/{user-avatar-DuwCFrER.js → user-avatar-Dw6mzWhv.js} +2 -2
  118. package/public/assets/{user-avatar-DuwCFrER.js.map → user-avatar-Dw6mzWhv.js.map} +1 -1
  119. package/public/assets/{work-queue-BPaGXrCE.js → work-queue-B6Xk7QNe.js} +2 -2
  120. package/public/assets/{work-queue-BPaGXrCE.js.map → work-queue-B6Xk7QNe.js.map} +1 -1
  121. package/public/assets/{workspaces-BLEXgnlj.js → workspaces-CPIzAHAe.js} +2 -2
  122. package/public/assets/{workspaces-BLEXgnlj.js.map → workspaces-CPIzAHAe.js.map} +1 -1
  123. package/public/index.html +23 -23
  124. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  125. package/src/branch-landed.ts +3 -7
  126. package/src/bus.ts +2 -2
  127. package/src/db/connection.ts +36 -12
  128. package/src/db/maintenance-worker.ts +23 -11
  129. package/src/db/message-reads.ts +17 -1
  130. package/src/maintenance/jobs.ts +18 -12
  131. package/src/maintenance/scheduler.ts +8 -3
  132. package/src/maintenance/types.ts +5 -1
  133. package/src/routes/sse.ts +2 -1
  134. package/src/services/git-process.ts +61 -0
  135. package/src/services/git-remote.ts +68 -17
  136. package/src/services/task-lifecycle.ts +2 -6
  137. package/src/sse.ts +110 -44
  138. package/public/assets/formatted-body-C1AprxyU.js +0 -3
  139. package/public/assets/lucide-react-Dc3ZwCBi.js +0 -9
  140. package/public/assets/lucide-react-Dc3ZwCBi.js.map +0 -1
  141. package/public/assets/store-BaslPLIP.js.map +0 -1
package/public/index.html CHANGED
@@ -12,34 +12,34 @@
12
12
  <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%2309090b'/%3E%3Ccircle cx='16' cy='16' r='4.5' fill='%2358a6ff'/%3E%3Ccircle cx='6' cy='8' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='26' cy='8' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='6' cy='24' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='26' cy='24' r='2.5' fill='%233fb950'/%3E%3Cline x1='8' y1='9.5' x2='13' y2='14' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='24' y1='9.5' x2='19' y2='14' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='8' y1='22.5' x2='13' y2='18' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='24' y1='22.5' x2='19' y2='18' stroke='%2330363d' stroke-width='1.5'/%3E%3C/svg%3E">
13
13
  <link rel="manifest" href="manifest.webmanifest">
14
14
  <link rel="apple-touch-icon" href="icons/agent-relay-192.png">
15
- <script type="module" crossorigin src="./assets/index-DfPouNiV.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-Ceiwd02X.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="./assets/chunk-CilyBKbf.js">
17
17
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-C_JMBXql.js">
18
- <link rel="modulepreload" crossorigin href="./assets/lucide-react-Dc3ZwCBi.js">
19
- <link rel="modulepreload" crossorigin href="./assets/dist-ChDX_t12.js">
20
- <link rel="modulepreload" crossorigin href="./assets/dist-Cs8X0P1d.js">
21
- <link rel="modulepreload" crossorigin href="./assets/react-dom-cLkVFBsS.js">
22
- <link rel="modulepreload" crossorigin href="./assets/dist-CQ0Z-TDi.js">
23
- <link rel="modulepreload" crossorigin href="./assets/dist-mm7c7nBt.js">
24
- <link rel="modulepreload" crossorigin href="./assets/dist-CDZZxxJq.js">
25
- <link rel="modulepreload" crossorigin href="./assets/dist-DV83j9tm.js">
18
+ <link rel="modulepreload" crossorigin href="./assets/lucide-react-DuhlF2Sj.js">
19
+ <link rel="modulepreload" crossorigin href="./assets/dist-B-T7lg0a.js">
20
+ <link rel="modulepreload" crossorigin href="./assets/dist-BIjlN-xB.js">
21
+ <link rel="modulepreload" crossorigin href="./assets/react-dom-DWBmcawP.js">
22
+ <link rel="modulepreload" crossorigin href="./assets/dist-B9G83DXv.js">
23
+ <link rel="modulepreload" crossorigin href="./assets/dist-DT-OV6sQ.js">
24
+ <link rel="modulepreload" crossorigin href="./assets/dist-C7aTlosb.js">
25
+ <link rel="modulepreload" crossorigin href="./assets/dist-B3e4B9pg.js">
26
26
  <link rel="modulepreload" crossorigin href="./assets/dist-BiLuYU6G.js">
27
- <link rel="modulepreload" crossorigin href="./assets/dist-DyNJNwpi.js">
28
- <link rel="modulepreload" crossorigin href="./assets/dist-D2jvo5oA.js">
29
- <link rel="modulepreload" crossorigin href="./assets/es2015-VnZ2kVIz.js">
30
- <link rel="modulepreload" crossorigin href="./assets/dist-CmFeEXMe.js">
31
- <link rel="modulepreload" crossorigin href="./assets/dist-CBMWGhWc.js">
32
- <link rel="modulepreload" crossorigin href="./assets/dist-y03IwOrd.js">
27
+ <link rel="modulepreload" crossorigin href="./assets/dist-BInckSJC.js">
28
+ <link rel="modulepreload" crossorigin href="./assets/dist-DGfp5dLz.js">
29
+ <link rel="modulepreload" crossorigin href="./assets/es2015-CnKXYKpf.js">
30
+ <link rel="modulepreload" crossorigin href="./assets/dist-BlzVGRhh.js">
31
+ <link rel="modulepreload" crossorigin href="./assets/dist-OUPTEP6q.js">
32
+ <link rel="modulepreload" crossorigin href="./assets/dist-B6woQ9Tb.js">
33
33
  <link rel="modulepreload" crossorigin href="./assets/types-uVOXgvMT.js">
34
34
  <link rel="modulepreload" crossorigin href="./assets/display-exQDWum3.js">
35
- <link rel="modulepreload" crossorigin href="./assets/store-BaslPLIP.js">
36
- <link rel="modulepreload" crossorigin href="./assets/agent-type-icon-BEz393ra.js">
37
- <link rel="modulepreload" crossorigin href="./assets/button-C-V4oij1.js">
38
- <link rel="modulepreload" crossorigin href="./assets/copy-button-BStzXgtd.js">
39
- <link rel="modulepreload" crossorigin href="./assets/formatted-body-C1AprxyU.js">
40
- <link rel="modulepreload" crossorigin href="./assets/task-chip-kxk4eNm1.js">
41
- <link rel="modulepreload" crossorigin href="./assets/badge-BhUZ1cAb.js">
42
- <link rel="modulepreload" crossorigin href="./assets/input-CtLT7WXi.js">
35
+ <link rel="modulepreload" crossorigin href="./assets/store-BM4zM89B.js">
36
+ <link rel="modulepreload" crossorigin href="./assets/agent-type-icon-CeEoMouY.js">
37
+ <link rel="modulepreload" crossorigin href="./assets/button-CzdTkRIf.js">
38
+ <link rel="modulepreload" crossorigin href="./assets/copy-button-B3o1jpLu.js">
39
+ <link rel="modulepreload" crossorigin href="./assets/formatted-body-q_qzt1L-.js">
40
+ <link rel="modulepreload" crossorigin href="./assets/task-chip-DXfvXBDx.js">
41
+ <link rel="modulepreload" crossorigin href="./assets/badge-DCIT3Irj.js">
42
+ <link rel="modulepreload" crossorigin href="./assets/input-B6Q0NWCS.js">
43
43
  <link rel="modulepreload" crossorigin href="./assets/themes-DjSwmAZN.js">
44
44
  <link rel="stylesheet" crossorigin href="./assets/index-DcmG5gLR.css">
45
45
  </head>
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.120.2",
4
+ "version": "0.120.3",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -8,6 +8,7 @@ import { resolveCoordinatorAgentId } from "./steward-identity";
8
8
  import { stewardFallbackTarget } from "./config";
9
9
  import { handleLandedIssueLifecycle } from "./services/issue-lifecycle";
10
10
  import { handleLandedTaskLifecycle } from "./services/task-lifecycle";
11
+ import { runGit } from "./services/git-process";
11
12
  import type { WorkspaceMergePreview, WorkspaceRecord } from "./types";
12
13
 
13
14
  // #444 — branch.landed is the first rule-driven type: the on-main "merged" fan-out audience comes
@@ -67,14 +68,9 @@ async function landedCommitSubjects(input: BranchLandedInput, log: BranchLandedL
67
68
  const tipSha = cleanRef(input.mergedSha);
68
69
  if (!repoRoot || !baseSha || !tipSha || baseSha === tipSha) return undefined;
69
70
  try {
70
- const proc = Bun.spawn(["git", "-C", repoRoot, "log", "--format=%B%x00", `${baseSha}..${tipSha}`], {
71
- stdin: "ignore",
72
- stdout: "pipe",
73
- stderr: "pipe",
74
- });
75
- const [stdout, stderr, exitCode] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
71
+ const { stdout, stderr, exitCode, timedOut } = await runGit(["-C", repoRoot, "log", "--format=%B%x00", `${baseSha}..${tipSha}`]);
76
72
  if (exitCode !== 0) {
77
- const error = stderr.trim() || `exit ${exitCode}`;
73
+ const error = timedOut ? "timed out" : stderr.trim() || `exit ${exitCode}`;
78
74
  log(`issue lifecycle commit range scan failed for ${input.workspace.id} (${baseSha.slice(0, 12)}..${tipSha.slice(0, 12)}): ${error}; falling back to tip subject`);
79
75
  return undefined;
80
76
  }
package/src/bus.ts CHANGED
@@ -528,6 +528,7 @@ function fanoutAgent(agentId: string, cache: Map<string, AgentCard | null>): Age
528
528
 
529
529
  function connectionWantsEvent(conn: BusConnection, event: RelayEvent, agentCache: Map<string, AgentCard | null>): boolean {
530
530
  if (!matchesSubscription(conn.subscriptions, event.type)) return false;
531
+ if (event.type === "agent.status" && conn.agentId) return event.subject === conn.agentId || (typeof event.data.id === "string" && event.data.id === conn.agentId);
531
532
  if (event.type === "message.new" && conn.agentId) {
532
533
  return messageMatchesAgent(event.data as unknown as Message, conn.agentId, fanoutAgent(conn.agentId, agentCache));
533
534
  }
@@ -538,8 +539,7 @@ function connectionWantsEvent(conn: BusConnection, event: RelayEvent, agentCache
538
539
  if (event.type.startsWith("command.")) {
539
540
  const command = isRecord(event.data.command) ? event.data.command : undefined;
540
541
  if (!command) return true;
541
- const target = typeof command.target === "string" ? command.target : "";
542
- const source = typeof command.source === "string" ? command.source : "";
542
+ const target = typeof command.target === "string" ? command.target : "", source = typeof command.source === "string" ? command.source : "";
543
543
  return target === conn.componentId || target === conn.agentId || source === conn.componentId || source === conn.agentId;
544
544
  }
545
545
  return true;
@@ -74,24 +74,48 @@ export function runDbMaintenance(options: { vacuum?: boolean } = {}): {
74
74
  return { analyzed: true, checkpointed: true, vacuumed };
75
75
  }
76
76
 
77
- export async function runDbMaintenanceAsync(options: { vacuum?: boolean } = {}): Promise<{
77
+ export async function runDbMaintenanceWorker<T>(operation: string, args: string[], signal?: AbortSignal): Promise<T> {
78
+ if (!dbPath || dbPath === ":memory:") {
79
+ throw new Error("database worker requires a file-backed database");
80
+ }
81
+ if (signal?.aborted) throw new Error("database worker aborted");
82
+ const proc = Bun.spawn([process.execPath, join(import.meta.dir, "maintenance-worker.ts"), operation, dbPath, ...args], {
83
+ stdin: "ignore",
84
+ stdout: "pipe",
85
+ stderr: "pipe",
86
+ });
87
+ let killTimer: Timer | undefined;
88
+ const abort = () => {
89
+ proc.kill("SIGTERM");
90
+ killTimer = setTimeout(() => proc.kill("SIGKILL"), 2_000);
91
+ };
92
+ signal?.addEventListener("abort", abort, { once: true });
93
+ try {
94
+ const [stdout, stderr, exitCode] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
95
+ if (signal?.aborted) throw new Error("database worker aborted");
96
+ if (exitCode !== 0) {
97
+ const detail = stderr.trim() || stdout.trim() || `exit ${exitCode}`;
98
+ throw new Error(`database worker ${operation} failed: ${detail}`);
99
+ }
100
+ return JSON.parse(stdout) as T;
101
+ } finally {
102
+ signal?.removeEventListener("abort", abort);
103
+ if (killTimer) clearTimeout(killTimer);
104
+ }
105
+ }
106
+
107
+ export async function runDbMaintenanceAsync(options: { vacuum?: boolean; signal?: AbortSignal } = {}): Promise<{
78
108
  analyzed: boolean;
79
109
  checkpointed: boolean;
80
110
  vacuumed: boolean;
81
111
  offThread: boolean;
82
112
  }> {
83
113
  if (!dbPath || dbPath === ":memory:") return { ...runDbMaintenance(options), offThread: false };
84
- const proc = Bun.spawn([process.execPath, join(import.meta.dir, "maintenance-worker.ts"), dbPath, options.vacuum ? "vacuum" : "no-vacuum"], {
85
- stdin: "ignore",
86
- stdout: "pipe",
87
- stderr: "pipe",
88
- });
89
- const [stdout, stderr, exitCode] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
90
- if (exitCode !== 0) {
91
- const detail = stderr.trim() || stdout.trim() || `exit ${exitCode}`;
92
- throw new Error(`db maintenance worker failed: ${detail}`);
93
- }
94
- const result = JSON.parse(stdout) as { analyzed: boolean; checkpointed: boolean; vacuumed: boolean };
114
+ const result = await runDbMaintenanceWorker<{ analyzed: boolean; checkpointed: boolean; vacuumed: boolean }>(
115
+ "db-maintenance",
116
+ [options.vacuum ? "vacuum" : "no-vacuum"],
117
+ options.signal,
118
+ );
95
119
  return { ...result, offThread: true };
96
120
  }
97
121
 
@@ -1,25 +1,37 @@
1
1
  import { Database } from "bun:sqlite";
2
2
  import { applyConnectionPragmas } from "./connection";
3
3
 
4
- const [, , dbPath, mode] = Bun.argv;
4
+ const [, , operation, dbPath, arg] = Bun.argv;
5
5
 
6
- if (!dbPath) {
7
- console.error("db path is required");
6
+ if (!operation || !dbPath) {
7
+ console.error("operation and db path are required");
8
8
  process.exit(2);
9
9
  }
10
10
 
11
11
  const db = new Database(dbPath, { create: true });
12
12
  try {
13
13
  applyConnectionPragmas(db);
14
- db.run("ANALYZE");
15
- db.run("PRAGMA optimize");
16
- db.run("PRAGMA wal_checkpoint(TRUNCATE)");
17
- let vacuumed = false;
18
- if (mode === "vacuum") {
19
- db.run("VACUUM");
20
- vacuumed = true;
14
+ if (operation === "db-maintenance") {
15
+ db.run("ANALYZE");
16
+ db.run("PRAGMA optimize");
17
+ db.run("PRAGMA wal_checkpoint(TRUNCATE)");
18
+ let vacuumed = false;
19
+ if (arg === "vacuum") {
20
+ db.run("VACUUM");
21
+ vacuumed = true;
22
+ }
23
+ console.log(JSON.stringify({ analyzed: true, checkpointed: true, vacuumed }));
24
+ } else if (operation === "old-message-prune") {
25
+ const cutoff = Number(arg);
26
+ if (!Number.isFinite(cutoff)) throw new Error("old-message-prune requires a cutoff timestamp");
27
+ const prunedMessages = db.transaction(() => {
28
+ db.query("UPDATE messages SET reply_to = NULL WHERE reply_to IN (SELECT id FROM messages WHERE created_at < ?)").run(cutoff);
29
+ return db.query("DELETE FROM messages WHERE created_at < ?").run(cutoff).changes;
30
+ })();
31
+ console.log(JSON.stringify({ prunedMessages }));
32
+ } else {
33
+ throw new Error(`unknown maintenance operation: ${operation}`);
21
34
  }
22
- console.log(JSON.stringify({ analyzed: true, checkpointed: true, vacuumed }));
23
35
  } finally {
24
36
  db.close();
25
37
  }
@@ -17,7 +17,7 @@ import { matchAgents } from "../agent-ref";
17
17
  import { getAgent } from "./agents.ts";
18
18
  import { sendMessageWithResult } from "./messages.ts";
19
19
  import { deleteArtifactLinksForEntity } from "./artifacts.ts";
20
- import { getDb, timedQuery } from "./connection.ts";
20
+ import { getDb, getDbPath, runDbMaintenanceWorker, timedQuery } from "./connection.ts";
21
21
  import { legacyChannelTargets, isChannelAgentId } from "./delivery.ts";
22
22
  import { messageTaskClosed } from "./claim-sql.ts";
23
23
  import { MSG_SELECT, rowToMessage } from "./mappers.ts";
@@ -630,6 +630,10 @@ export function getLatestMessageId(): number {
630
630
 
631
631
  export function pruneOldMessages(maxAgeMs: number): number {
632
632
  const cutoff = Date.now() - maxAgeMs;
633
+ return pruneOldMessagesBefore(cutoff);
634
+ }
635
+
636
+ function pruneOldMessagesBefore(cutoff: number): number {
633
637
  return getDb().transaction(() => {
634
638
  getDb()
635
639
  .query("UPDATE messages SET reply_to = NULL WHERE reply_to IN (SELECT id FROM messages WHERE created_at < ?)")
@@ -639,3 +643,15 @@ export function pruneOldMessages(maxAgeMs: number): number {
639
643
  .run(cutoff).changes;
640
644
  })();
641
645
  }
646
+
647
+ export async function pruneOldMessagesAsync(maxAgeMs: number, options: { signal?: AbortSignal } = {}): Promise<{
648
+ prunedMessages: number;
649
+ offThread: boolean;
650
+ }> {
651
+ const cutoff = Date.now() - maxAgeMs;
652
+ if (!getDbPath() || getDbPath() === ":memory:") {
653
+ return { prunedMessages: pruneOldMessagesBefore(cutoff), offThread: false };
654
+ }
655
+ const result = await runDbMaintenanceWorker<{ prunedMessages: number }>("old-message-prune", [String(cutoff)], options.signal);
656
+ return { ...result, offThread: true };
657
+ }
@@ -13,7 +13,7 @@ import {
13
13
  listStaleQueuedDirectTargets,
14
14
  pruneOrphanedSharedWorkspaces,
15
15
  pruneOfflineAgents,
16
- pruneOldMessages,
16
+ pruneOldMessagesAsync,
17
17
  pruneProviderQuotaHistory,
18
18
  reapStaleAgents,
19
19
  reapStaleOrchestrators,
@@ -72,6 +72,10 @@ import type { MaintenanceJobDefinition } from "./types";
72
72
 
73
73
  let dbMaintenanceRuns = 0;
74
74
 
75
+ function isFleetIdleForVacuum(): boolean {
76
+ return !listAgents({ status: "busy" }).some((agent) => agent.kind === "provider" && agent.id !== "user" && agent.id !== "system");
77
+ }
78
+
75
79
  function isManagedBusyWatchCandidate(agent: ReturnType<typeof listAgents>[number]): boolean {
76
80
  if (agent.kind !== "provider") return false;
77
81
  if (agent.status !== "busy") return false;
@@ -315,17 +319,17 @@ export const definitions: MaintenanceJobDefinition[] = [
315
319
  description: "Stop spawned sessions whose relay agent is offline/gone but whose process the orchestrator still reports running, after a grace period for self-heal.",
316
320
  intervalMs: ORPHAN_REAPER_INTERVAL_MS,
317
321
  runOnStart: false,
318
- handler: reapOrphanedSessions,
322
+ handler: () => reapOrphanedSessions(),
319
323
  },
320
- { id: "transient-agent-reaper", title: "Transient agent reaper", description: "Cleanly shut down transient spawned agents once they are idle and any isolated branch work has landed.", intervalMs: REAP_INTERVAL_MS, runOnStart: false, handler: reapTransientAgents },
321
- { id: "rate-limit-resume", title: "Rate-limit resume", description: "Auto-resume agents held on a provider usage/rate-limit stall once their reset window has passed (#286).", intervalMs: REAP_INTERVAL_MS, runOnStart: false, handler: resumeRateLimitedAgents },
324
+ { id: "transient-agent-reaper", title: "Transient agent reaper", description: "Cleanly shut down transient spawned agents once they are idle and any isolated branch work has landed.", intervalMs: REAP_INTERVAL_MS, runOnStart: false, handler: () => reapTransientAgents() },
325
+ { id: "rate-limit-resume", title: "Rate-limit resume", description: "Auto-resume agents held on a provider usage/rate-limit stall once their reset window has passed (#286).", intervalMs: REAP_INTERVAL_MS, runOnStart: false, handler: () => resumeRateLimitedAgents() },
322
326
  {
323
327
  id: "stuck-busy-watchdog",
324
328
  title: "Stuck busy watchdog",
325
329
  description: "Warn when a managed provider stays busy past the threshold without provider progress (#53).",
326
330
  intervalMs: REAP_INTERVAL_MS,
327
331
  runOnStart: false,
328
- handler: runStuckBusyWatchdog,
332
+ handler: () => runStuckBusyWatchdog(),
329
333
  },
330
334
  {
331
335
  id: "team-reap",
@@ -341,7 +345,7 @@ export const definitions: MaintenanceJobDefinition[] = [
341
345
  description: "Gently ping a team's current coordinator once when the team has had no busy members for the configured threshold.",
342
346
  intervalMs: REAP_INTERVAL_MS,
343
347
  runOnStart: false,
344
- handler: runTeamIdleWatchdog,
348
+ handler: () => runTeamIdleWatchdog(),
345
349
  },
346
350
  {
347
351
  id: "orchestrator-reaper",
@@ -373,9 +377,9 @@ export const definitions: MaintenanceJobDefinition[] = [
373
377
  description: "Delete messages outside the configured retention window.",
374
378
  intervalMs: DAY_MS,
375
379
  runOnStart: false,
376
- handler() {
380
+ handler(context) {
377
381
  const retentionDays = Number(process.env.RETENTION_DAYS) || 30;
378
- return { prunedMessages: pruneOldMessages(retentionDays * DAY_MS) };
382
+ return pruneOldMessagesAsync(retentionDays * DAY_MS, { signal: context?.signal });
379
383
  },
380
384
  },
381
385
  {
@@ -385,10 +389,12 @@ export const definitions: MaintenanceJobDefinition[] = [
385
389
  intervalMs: DB_MAINTENANCE_INTERVAL_MS,
386
390
  runOnStart: false,
387
391
  timeoutMs: 5 * 60 * 1000,
388
- async handler() {
392
+ async handler(context) {
389
393
  dbMaintenanceRuns += 1;
390
- const vacuum = DB_VACUUM_EVERY > 0 && dbMaintenanceRuns % DB_VACUUM_EVERY === 0;
391
- return runDbMaintenanceAsync({ vacuum });
394
+ const vacuumDue = DB_VACUUM_EVERY > 0 && dbMaintenanceRuns % DB_VACUUM_EVERY === 0;
395
+ const vacuum = vacuumDue && isFleetIdleForVacuum();
396
+ const result = await runDbMaintenanceAsync({ vacuum, signal: context?.signal });
397
+ return vacuumDue && !vacuum ? { ...result, vacuumSkipped: "fleet-busy" } : result;
392
398
  },
393
399
  },
394
400
  {
@@ -492,7 +498,7 @@ export const definitions: MaintenanceJobDefinition[] = [
492
498
  intervalMs: 60 * 1000,
493
499
  runOnStart: false,
494
500
  timeoutMs: 10 * 1000,
495
- handler: runAutoMergeWatchdog,
501
+ handler: () => runAutoMergeWatchdog(),
496
502
  },
497
503
  {
498
504
  id: "workspace-idle-refresh",
@@ -118,8 +118,9 @@ async function runJob(definition: MaintenanceJobDefinition, options: { force?: b
118
118
  return { id: definition.id, status: "skipped", startedAt, finishedAt: Date.now(), durationMs: 0, result: { reason: "not due or already running", state } };
119
119
  }
120
120
 
121
+ const abortController = new AbortController();
121
122
  try {
122
- const result = await withTimeout(Promise.resolve(definition.handler()), timeoutMs);
123
+ const result = await withTimeout(Promise.resolve(definition.handler({ signal: abortController.signal })), timeoutMs, abortController);
123
124
  const finishedAt = Date.now();
124
125
  const durationMs = finishedAt - startedAt;
125
126
  db.query(`
@@ -145,10 +146,14 @@ async function runJob(definition: MaintenanceJobDefinition, options: { force?: b
145
146
  }
146
147
  }
147
148
 
148
- async function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
149
+ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, abortController?: AbortController): Promise<T> {
149
150
  let timeout: Timer | undefined;
150
151
  const timeoutPromise = new Promise<never>((_, reject) => {
151
- timeout = setTimeout(() => reject(new Error(`maintenance job timed out after ${timeoutMs}ms`)), timeoutMs);
152
+ timeout = setTimeout(() => {
153
+ const error = new Error(`maintenance job timed out after ${timeoutMs}ms`);
154
+ abortController?.abort(error);
155
+ reject(error);
156
+ }, timeoutMs);
152
157
  });
153
158
  try {
154
159
  return await Promise.race([promise, timeoutPromise]);
@@ -1,5 +1,9 @@
1
1
  import type { MaintenanceJob } from "../types";
2
2
 
3
+ export interface MaintenanceJobContext {
4
+ signal: AbortSignal;
5
+ }
6
+
3
7
  export interface MaintenanceJobDefinition {
4
8
  id: string;
5
9
  title: string;
@@ -7,7 +11,7 @@ export interface MaintenanceJobDefinition {
7
11
  intervalMs: number;
8
12
  timeoutMs?: number;
9
13
  runOnStart?: boolean;
10
- handler: () => Promise<Record<string, unknown>> | Record<string, unknown>;
14
+ handler: (context?: MaintenanceJobContext) => Promise<Record<string, unknown>> | Record<string, unknown>;
11
15
  }
12
16
 
13
17
  export interface MaintenanceJobRow {
package/src/routes/sse.ts CHANGED
@@ -5,5 +5,6 @@ import { type Handler } from "./_shared";
5
5
  export const getEvents: Handler = (req) => {
6
6
  const url = new URL(req.url);
7
7
  const agentId = url.searchParams.get("for") || null;
8
- return createSSEStream(agentId);
8
+ const lastEventId = Number(req.headers.get("last-event-id") ?? "");
9
+ return createSSEStream(agentId, Number.isSafeInteger(lastEventId) && lastEventId > 0 ? lastEventId : 0);
9
10
  };
@@ -0,0 +1,61 @@
1
+ export const LAND_GIT_TIMEOUT_MS = 2_000;
2
+
3
+ export interface GitProcessResult {
4
+ stdout: string;
5
+ stderr: string;
6
+ exitCode: number;
7
+ timedOut: boolean;
8
+ }
9
+
10
+ export async function runGit(args: string[], timeoutMs = LAND_GIT_TIMEOUT_MS): Promise<GitProcessResult> {
11
+ const proc = Bun.spawn(["git", ...args], {
12
+ stdin: "ignore",
13
+ stdout: "pipe",
14
+ stderr: "pipe",
15
+ });
16
+ const stdoutPromise = streamText(proc.stdout);
17
+ const stderrPromise = streamText(proc.stderr);
18
+ const timedOut = await waitForExitOrTimeout(proc, timeoutMs);
19
+ const [stdout, stderr] = timedOut
20
+ ? await Promise.all([settleText(stdoutPromise), settleText(stderrPromise)])
21
+ : await Promise.all([stdoutPromise, stderrPromise]);
22
+ return { stdout, stderr, exitCode: proc.exitCode ?? 1, timedOut };
23
+ }
24
+
25
+ async function streamText(stream: ReadableStream<Uint8Array> | null): Promise<string> {
26
+ if (!stream) return "";
27
+ try {
28
+ return await new Response(stream).text();
29
+ } catch {
30
+ return "";
31
+ }
32
+ }
33
+
34
+ async function settleText(promise: Promise<string>): Promise<string> {
35
+ return Promise.race([promise, new Promise<string>((resolve) => setTimeout(() => resolve(""), 25))]);
36
+ }
37
+
38
+ async function waitForExitOrTimeout(proc: Bun.Subprocess<"ignore", "pipe", "pipe">, timeoutMs: number | undefined): Promise<boolean> {
39
+ if (!timeoutMs || timeoutMs <= 0) {
40
+ await proc.exited;
41
+ return false;
42
+ }
43
+ let timedOut = false;
44
+ let kill: ReturnType<typeof setTimeout> | undefined;
45
+ const timeout = setTimeout(() => {
46
+ timedOut = true;
47
+ try { proc.kill("SIGTERM"); } catch {}
48
+ kill = setTimeout(() => {
49
+ try { proc.kill("SIGKILL"); } catch {}
50
+ }, 1_000);
51
+ kill.unref?.();
52
+ }, timeoutMs);
53
+ timeout.unref?.();
54
+ try {
55
+ await proc.exited;
56
+ } finally {
57
+ clearTimeout(timeout);
58
+ if (kill) clearTimeout(kill);
59
+ }
60
+ return timedOut;
61
+ }
@@ -3,6 +3,9 @@
3
3
  // not drift: issue-lifecycle land-close (which repo a bare `#NNN` in a commit subject
4
4
  // closes) AND project auto-seed (a project's `issueRepo` for chat #NNN linkify). Promoted
5
5
  // out of issue-lifecycle.ts where it was private — see the DRY rule in CLAUDE.md.
6
+ import { runGit } from "./git-process";
7
+ import { existsSync, readFileSync, statSync } from "node:fs";
8
+ import { dirname, isAbsolute, join, resolve } from "node:path";
6
9
 
7
10
  /**
8
11
  * `owner/repo` out of any git remote URL form: `git@github.com:owner/repo.git`,
@@ -24,12 +27,7 @@ export async function remoteUrlFromGitRemoteAsync(repoRoot: string | undefined):
24
27
  if (remoteUrlCache.has(repoRoot)) return remoteUrlCache.get(repoRoot);
25
28
  let url: string | undefined;
26
29
  try {
27
- const proc = Bun.spawn(["git", "-C", repoRoot, "remote", "get-url", "origin"], {
28
- stdin: "ignore",
29
- stdout: "pipe",
30
- stderr: "ignore",
31
- });
32
- const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
30
+ const { stdout, exitCode } = await runGit(["-C", repoRoot, "remote", "get-url", "origin"]);
33
31
  if (exitCode === 0) url = stdout.trim() || undefined;
34
32
  } catch {
35
33
  url = undefined;
@@ -42,17 +40,7 @@ export function remoteUrlFromGitRemote(repoRoot: string | undefined): string | u
42
40
  if (!repoRoot) return undefined;
43
41
  if (!remoteUrlCache) remoteUrlCache = new Map();
44
42
  if (remoteUrlCache.has(repoRoot)) return remoteUrlCache.get(repoRoot);
45
- let url: string | undefined;
46
- try {
47
- const proc = Bun.spawnSync(["git", "-C", repoRoot, "remote", "get-url", "origin"], {
48
- stdin: "ignore",
49
- stdout: "pipe",
50
- stderr: "ignore",
51
- });
52
- if (proc.exitCode === 0) url = Buffer.from(proc.stdout).toString("utf8").trim() || undefined;
53
- } catch {
54
- url = undefined;
55
- }
43
+ const url = remoteUrlFromGitConfig(repoRoot);
56
44
  remoteUrlCache.set(repoRoot, url);
57
45
  return url;
58
46
  }
@@ -84,3 +72,66 @@ export function __resetRemoteRepoCacheForTests(): void {
84
72
  remoteRepoCache = undefined;
85
73
  remoteUrlCache = undefined;
86
74
  }
75
+
76
+ function remoteUrlFromGitConfig(repoRoot: string): string | undefined {
77
+ const gitDir = findGitDir(repoRoot);
78
+ if (!gitDir) return undefined;
79
+ return originUrlFromConfig(join(gitDir, "config")) ?? originUrlFromConfig(join(commonGitDir(gitDir) ?? gitDir, "config"));
80
+ }
81
+
82
+ function findGitDir(start: string): string | undefined {
83
+ let dir = resolve(start);
84
+ while (true) {
85
+ const marker = join(dir, ".git");
86
+ try {
87
+ const stat = statSync(marker);
88
+ if (stat.isDirectory()) return marker;
89
+ if (stat.isFile()) return gitDirFromFile(marker, dir);
90
+ } catch {}
91
+ const parent = dirname(dir);
92
+ if (parent === dir) return undefined;
93
+ dir = parent;
94
+ }
95
+ }
96
+
97
+ function gitDirFromFile(marker: string, worktreeRoot: string): string | undefined {
98
+ try {
99
+ const content = readFileSync(marker, "utf8").trim();
100
+ const match = content.match(/^gitdir:\s*(.+)$/i);
101
+ if (!match?.[1]) return undefined;
102
+ return isAbsolute(match[1]) ? match[1] : resolve(worktreeRoot, match[1]);
103
+ } catch {
104
+ return undefined;
105
+ }
106
+ }
107
+
108
+ function commonGitDir(gitDir: string): string | undefined {
109
+ const commonDirFile = join(gitDir, "commondir");
110
+ if (!existsSync(commonDirFile)) return undefined;
111
+ try {
112
+ const common = readFileSync(commonDirFile, "utf8").trim();
113
+ if (!common) return undefined;
114
+ return isAbsolute(common) ? common : resolve(gitDir, common);
115
+ } catch {
116
+ return undefined;
117
+ }
118
+ }
119
+
120
+ function originUrlFromConfig(configPath: string): string | undefined {
121
+ let inOrigin = false;
122
+ try {
123
+ for (const line of readFileSync(configPath, "utf8").split(/\r?\n/)) {
124
+ const section = line.match(/^\s*\[(.+)]\s*$/);
125
+ if (section) {
126
+ inOrigin = /^remote\s+"origin"$/.test(section[1]!.trim());
127
+ continue;
128
+ }
129
+ if (!inOrigin) continue;
130
+ const url = line.match(/^\s*url\s*=\s*(.+?)\s*$/);
131
+ if (url?.[1]) return url[1].trim() || undefined;
132
+ }
133
+ } catch {
134
+ return undefined;
135
+ }
136
+ return undefined;
137
+ }
@@ -38,6 +38,7 @@ import { routeNotification } from "../notification-router";
38
38
  import { resolveCoordinatorAgentId } from "../steward-identity";
39
39
  import { isAgentExitDoneWhen, stageSatisfiesDoneWhen } from "./task-done-when";
40
40
  import { emitTaskSemanticNotification, type TaskSemanticTransition } from "./task-semantic-events";
41
+ import { runGit } from "./git-process";
41
42
  import type { IssueRef, TaskDeliverableStage, TaskGateState, WorkspaceRecord } from "../types";
42
43
  import type { IssueTrackerAdapter } from "../issue-tracker";
43
44
 
@@ -107,12 +108,7 @@ async function landedDiffSummary(input: LandedTaskLifecycleInput, log: LogFn): P
107
108
  const tipSha = input.mergedSha?.trim();
108
109
  if (!repoRoot || !baseSha || !tipSha || baseSha === tipSha) return undefined;
109
110
  try {
110
- const proc = Bun.spawn(["git", "-C", repoRoot, "diff", "--shortstat", `${baseSha}..${tipSha}`], {
111
- stdin: "ignore",
112
- stdout: "pipe",
113
- stderr: "pipe",
114
- });
115
- const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
111
+ const { stdout, exitCode } = await runGit(["-C", repoRoot, "diff", "--shortstat", `${baseSha}..${tipSha}`]);
116
112
  if (exitCode !== 0) return undefined;
117
113
  const out = stdout.trim();
118
114
  return out || undefined;