agent-relay-server 0.107.0 → 0.109.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 (217) hide show
  1. package/docs/openapi.json +589 -1
  2. package/package.json +4 -4
  3. package/public/assets/MessageBubble-XgN61quY.js +4 -0
  4. package/public/assets/{MessageBubble-PEdvVh_m.js.map → MessageBubble-XgN61quY.js.map} +1 -1
  5. package/public/assets/PlanGraphPanel-LY-msOsr.js +2 -0
  6. package/public/assets/PlanGraphPanel-LY-msOsr.js.map +1 -0
  7. package/public/assets/activity-D0EotVU9.js +2 -0
  8. package/public/assets/activity-D0EotVU9.js.map +1 -0
  9. package/public/assets/{agent-profiles-CuTQtmV6.js → agent-profiles-CAGvryb9.js} +2 -2
  10. package/public/assets/{agent-profiles-CuTQtmV6.js.map → agent-profiles-CAGvryb9.js.map} +1 -1
  11. package/public/assets/agents-CXXZ_sU9.js +2 -0
  12. package/public/assets/{agents-CgRFJyks.js.map → agents-CXXZ_sU9.js.map} +1 -1
  13. package/public/assets/{analytics-C3OTFIXo.js → analytics-Q6Dl_CE9.js} +3 -3
  14. package/public/assets/{analytics-C3OTFIXo.js.map → analytics-Q6Dl_CE9.js.map} +1 -1
  15. package/public/assets/{automation-CH9YGYoE.js → automation-zig0cp23.js} +2 -2
  16. package/public/assets/{automation-CH9YGYoE.js.map → automation-zig0cp23.js.map} +1 -1
  17. package/public/assets/{badge-D8fABzMs.js → badge-kdQDWO6e.js} +2 -2
  18. package/public/assets/{badge-D8fABzMs.js.map → badge-kdQDWO6e.js.map} +1 -1
  19. package/public/assets/{branch-state-badge-CcLZ_u86.js → branch-state-badge-BRjUWyMT.js} +2 -2
  20. package/public/assets/{branch-state-badge-CcLZ_u86.js.map → branch-state-badge-BRjUWyMT.js.map} +1 -1
  21. package/public/assets/{button-KASZcscA.js → button-BKFvNXa3.js} +2 -2
  22. package/public/assets/{button-KASZcscA.js.map → button-BKFvNXa3.js.map} +1 -1
  23. package/public/assets/card-B0y1i_2D.js +2 -0
  24. package/public/assets/{card-Cds0MWME.js.map → card-B0y1i_2D.js.map} +1 -1
  25. package/public/assets/{channels-CcSnLzCG.js → channels-DOYB1iiA.js} +2 -2
  26. package/public/assets/{channels-CcSnLzCG.js.map → channels-DOYB1iiA.js.map} +1 -1
  27. package/public/assets/chat-C3QlsFBd.js +2 -0
  28. package/public/assets/{chat-BLcm2Aht.js.map → chat-C3QlsFBd.js.map} +1 -1
  29. package/public/assets/{connectors-DbPOlQhL.js → connectors-C5CeK-2M.js} +2 -2
  30. package/public/assets/{connectors-DbPOlQhL.js.map → connectors-C5CeK-2M.js.map} +1 -1
  31. package/public/assets/{copy-button-d9DFNX9v.js → copy-button-aLaiAkv2.js} +2 -2
  32. package/public/assets/{copy-button-d9DFNX9v.js.map → copy-button-aLaiAkv2.js.map} +1 -1
  33. package/public/assets/display-_5kFrHJh.js +3 -0
  34. package/public/assets/display-_5kFrHJh.js.map +1 -0
  35. package/public/assets/{dist-DOAjFOS4.js → dist-BDYU37NZ.js} +2 -2
  36. package/public/assets/{dist-DOAjFOS4.js.map → dist-BDYU37NZ.js.map} +1 -1
  37. package/public/assets/dist-BnfqT2cl.js +2 -0
  38. package/public/assets/{dist-DC-TqTz2.js.map → dist-BnfqT2cl.js.map} +1 -1
  39. package/public/assets/dist-CF28Y5wI.js +2 -0
  40. package/public/assets/{dist-CBVZk9U8.js.map → dist-CF28Y5wI.js.map} +1 -1
  41. package/public/assets/{dist-DTXKbk2K.js → dist-DDh8a_SK.js} +2 -2
  42. package/public/assets/{dist-DTXKbk2K.js.map → dist-DDh8a_SK.js.map} +1 -1
  43. package/public/assets/{dist-BbUJ1Vht.js → dist-DKoRwEZq.js} +2 -2
  44. package/public/assets/{dist-BbUJ1Vht.js.map → dist-DKoRwEZq.js.map} +1 -1
  45. package/public/assets/{dist-UhEAFdvZ.js → dist-DQPz52Zx.js} +2 -2
  46. package/public/assets/{dist-UhEAFdvZ.js.map → dist-DQPz52Zx.js.map} +1 -1
  47. package/public/assets/{dist-CgqfdHSt.js → dist-D_qcpfbI.js} +2 -2
  48. package/public/assets/{dist-CgqfdHSt.js.map → dist-D_qcpfbI.js.map} +1 -1
  49. package/public/assets/{dist-DybScJmm.js → dist-DnyQI0qt.js} +2 -2
  50. package/public/assets/{dist-DybScJmm.js.map → dist-DnyQI0qt.js.map} +1 -1
  51. package/public/assets/{dist-BwwlmyBq.js → dist-EO8jbi2F.js} +2 -2
  52. package/public/assets/{dist-BwwlmyBq.js.map → dist-EO8jbi2F.js.map} +1 -1
  53. package/public/assets/{dist-BlZvUZZB.js → dist-avJPT3qF.js} +2 -2
  54. package/public/assets/{dist-BlZvUZZB.js.map → dist-avJPT3qF.js.map} +1 -1
  55. package/public/assets/dist-cyag2tir.js +2 -0
  56. package/public/assets/{dist-DreO5AcZ.js.map → dist-cyag2tir.js.map} +1 -1
  57. package/public/assets/{es2015-DUjFyjDp.js → es2015-C9FSEGd7.js} +2 -2
  58. package/public/assets/{es2015-DUjFyjDp.js.map → es2015-C9FSEGd7.js.map} +1 -1
  59. package/public/assets/{formatted-body-impl-BJTthVHg.js → formatted-body-impl-DCQSVyWB.js} +2 -2
  60. package/public/assets/{formatted-body-impl-BJTthVHg.js.map → formatted-body-impl-DCQSVyWB.js.map} +1 -1
  61. package/public/assets/index-D6hQkte1.css +2 -0
  62. package/public/assets/index-RCbIyk26.js +23 -0
  63. package/public/assets/index-RCbIyk26.js.map +1 -0
  64. package/public/assets/{input-CtHfLn1i.js → input-DfN6sUbX.js} +2 -2
  65. package/public/assets/{input-CtHfLn1i.js.map → input-DfN6sUbX.js.map} +1 -1
  66. package/public/assets/{insights-DklrIhb6.js → insights-CsLs6NbL.js} +2 -2
  67. package/public/assets/{insights-DklrIhb6.js.map → insights-CsLs6NbL.js.map} +1 -1
  68. package/public/assets/{integrations-hkSW8CzM.js → integrations-CztQfrI7.js} +2 -2
  69. package/public/assets/{integrations-hkSW8CzM.js.map → integrations-CztQfrI7.js.map} +1 -1
  70. package/public/assets/{lib-l-JkMVR9.js → lib-DRRHEqRZ.js} +2 -2
  71. package/public/assets/{lib-l-JkMVR9.js.map → lib-DRRHEqRZ.js.map} +1 -1
  72. package/public/assets/{lucide-react-C4eXYCwx.js → lucide-react-DmbfpRvJ.js} +2 -2
  73. package/public/assets/{lucide-react-C4eXYCwx.js.map → lucide-react-DmbfpRvJ.js.map} +1 -1
  74. package/public/assets/{maintenance-FAAMGpuZ.js → maintenance-BN5F0Lnc.js} +2 -2
  75. package/public/assets/{maintenance-FAAMGpuZ.js.map → maintenance-BN5F0Lnc.js.map} +1 -1
  76. package/public/assets/managed-agents-R_LI9qS5.js +4 -0
  77. package/public/assets/{managed-agents-CVca-s3t.js.map → managed-agents-R_LI9qS5.js.map} +1 -1
  78. package/public/assets/markdown-preview-impl-DQlmK_ev.js +2 -0
  79. package/public/assets/{markdown-preview-impl-BPW4OQo9.js.map → markdown-preview-impl-DQlmK_ev.js.map} +1 -1
  80. package/public/assets/memory-kn4bgMXC.js +2 -0
  81. package/public/assets/{memory-BzuDTHC5.js.map → memory-kn4bgMXC.js.map} +1 -1
  82. package/public/assets/{messages-_W24PNvT.js → messages-Dv3Tv7zC.js} +2 -2
  83. package/public/assets/{messages-_W24PNvT.js.map → messages-Dv3Tv7zC.js.map} +1 -1
  84. package/public/assets/{orchestrators-Du6I_n8i.js → orchestrators-DcNLRMzk.js} +2 -2
  85. package/public/assets/{orchestrators-Du6I_n8i.js.map → orchestrators-DcNLRMzk.js.map} +1 -1
  86. package/public/assets/overview-BOLzPSSC.js +2 -0
  87. package/public/assets/{overview-Dyvm1qpT.js.map → overview-BOLzPSSC.js.map} +1 -1
  88. package/public/assets/{pairs-B8x1RCNd.js → pairs-B4r0egpx.js} +2 -2
  89. package/public/assets/{pairs-B8x1RCNd.js.map → pairs-B4r0egpx.js.map} +1 -1
  90. package/public/assets/projects-cOeuiMjl.js +2 -0
  91. package/public/assets/{projects-vt63bwHN.js.map → projects-cOeuiMjl.js.map} +1 -1
  92. package/public/assets/prompt-settings-E7-6Osj4.js +2 -0
  93. package/public/assets/{prompt-settings-xVXmvxB5.js.map → prompt-settings-E7-6Osj4.js.map} +1 -1
  94. package/public/assets/{react-dom-BMF4hNzG.js → react-dom-jQ4Vs380.js} +2 -2
  95. package/public/assets/{react-dom-BMF4hNzG.js.map → react-dom-jQ4Vs380.js.map} +1 -1
  96. package/public/assets/registry-CTN3jOnb.js +2 -0
  97. package/public/assets/{registry-ypiICauZ.js.map → registry-CTN3jOnb.js.map} +1 -1
  98. package/public/assets/{security-BfOQ6T5r.js → security-BctIcBZb.js} +2 -2
  99. package/public/assets/{security-BfOQ6T5r.js.map → security-BctIcBZb.js.map} +1 -1
  100. package/public/assets/{select-Dx8Ro-A3.js → select-BndvVb0A.js} +2 -2
  101. package/public/assets/{select-Dx8Ro-A3.js.map → select-BndvVb0A.js.map} +1 -1
  102. package/public/assets/settings-B_tRq-P4.js +6 -0
  103. package/public/assets/{settings-87clTx4N.js.map → settings-B_tRq-P4.js.map} +1 -1
  104. package/public/assets/store-DDWg67Pg.js +9 -0
  105. package/public/assets/store-DDWg67Pg.js.map +1 -0
  106. package/public/assets/tasks-BgTTxVET.js +2 -0
  107. package/public/assets/tasks-BgTTxVET.js.map +1 -0
  108. package/public/assets/teams-DJiLBEgF.js +2 -0
  109. package/public/assets/{teams-DJWp4AKi.js.map → teams-DJiLBEgF.js.map} +1 -1
  110. package/public/assets/{terminal-viewer-impl-DgMuRAe_.js → terminal-viewer-impl-BLdiyCNR.js} +3 -3
  111. package/public/assets/{terminal-viewer-impl-DgMuRAe_.js.map → terminal-viewer-impl-BLdiyCNR.js.map} +1 -1
  112. package/public/assets/types-DSwhOFcJ.js +2 -0
  113. package/public/assets/types-DSwhOFcJ.js.map +1 -0
  114. package/public/assets/{use-keyboard-viewport-RYMALvu1.js → use-keyboard-viewport-Dw2CHZ50.js} +2 -2
  115. package/public/assets/{use-keyboard-viewport-RYMALvu1.js.map → use-keyboard-viewport-Dw2CHZ50.js.map} +1 -1
  116. package/public/assets/user-avatar-rt59cEsc.js +2 -0
  117. package/public/assets/{user-avatar-CfAUS3PZ.js.map → user-avatar-rt59cEsc.js.map} +1 -1
  118. package/public/assets/{work-queue-D-Otxd9u.js → work-queue-BYRdBuLR.js} +2 -2
  119. package/public/assets/{work-queue-D-Otxd9u.js.map → work-queue-BYRdBuLR.js.map} +1 -1
  120. package/public/assets/workspaces-DCRT3l9H.js +3 -0
  121. package/public/assets/workspaces-DCRT3l9H.js.map +1 -0
  122. package/public/index.html +23 -23
  123. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  124. package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +6 -0
  125. package/src/branch-landed.ts +17 -3
  126. package/src/config-store.ts +12 -1
  127. package/src/db/agents.ts +3 -2
  128. package/src/db/continuations.ts +13 -3
  129. package/src/db/index.ts +3 -0
  130. package/src/db/issue-lifecycle.ts +12 -0
  131. package/src/db/mappers.ts +17 -0
  132. package/src/db/messages.ts +9 -4
  133. package/src/db/migrations.ts +112 -2
  134. package/src/db/notification-subscriptions.ts +1 -1
  135. package/src/db/prompt-templates.ts +19 -0
  136. package/src/db/schema.ts +74 -2
  137. package/src/db/stats.ts +3 -2
  138. package/src/db/task-comment-through.ts +112 -0
  139. package/src/db/task-core.ts +14 -1
  140. package/src/db/task-entity.ts +629 -0
  141. package/src/db/task-maintenance.ts +4 -3
  142. package/src/db/task-memory.ts +180 -0
  143. package/src/db/tasks.ts +24 -10
  144. package/src/issue-tracker/adapter.ts +15 -0
  145. package/src/issue-tracker/github.ts +40 -2
  146. package/src/issue-tracker/index.ts +1 -0
  147. package/src/mcp/index.ts +12 -1
  148. package/src/mcp/tools-spawn.ts +4 -0
  149. package/src/mcp-dispatch-tools.ts +193 -0
  150. package/src/mcp-notification-subscription-tools.ts +62 -4
  151. package/src/mcp-plan-tools.ts +30 -3
  152. package/src/mcp-release-tools.ts +33 -0
  153. package/src/mcp-task-memory-tools.ts +115 -0
  154. package/src/mcp-task-tools.ts +232 -0
  155. package/src/mcp-task-weave-tools.ts +66 -0
  156. package/src/notification-router.ts +24 -2
  157. package/src/notification-rules.ts +4 -0
  158. package/src/notification-types.ts +23 -0
  159. package/src/prompt-resolver.ts +66 -3
  160. package/src/routes/agents-spawn.ts +10 -0
  161. package/src/routes/index.ts +17 -1
  162. package/src/routes/messages.ts +6 -6
  163. package/src/routes/plans.ts +10 -2
  164. package/src/routes/tasks.ts +335 -2
  165. package/src/routing-policy-store.ts +147 -2
  166. package/src/security.ts +4 -0
  167. package/src/self-resume.ts +13 -2
  168. package/src/services/issue-lifecycle.ts +9 -1
  169. package/src/services/plan-graph.ts +107 -9
  170. package/src/services/release-readiness.ts +142 -0
  171. package/src/services/send-message.ts +31 -0
  172. package/src/services/spawn-agent.ts +81 -4
  173. package/src/services/task-context-hydration.ts +361 -0
  174. package/src/services/task-entity.ts +149 -0
  175. package/src/services/task-issue-weave.ts +558 -0
  176. package/src/services/task-lifecycle.ts +465 -0
  177. package/src/services/task-plan-view.ts +178 -0
  178. package/src/services/task-semantic-events.ts +137 -0
  179. package/src/services/transient-agent-reaper.ts +55 -22
  180. package/src/spawn-targets.ts +27 -2
  181. package/src/task-authz.ts +52 -0
  182. package/src/task-dispatcher.ts +461 -0
  183. package/src/upgrade-install-verify.ts +20 -1
  184. package/public/assets/MessageBubble-PEdvVh_m.js +0 -4
  185. package/public/assets/PlanGraphPanel-DSts7Rtq.js +0 -2
  186. package/public/assets/PlanGraphPanel-DSts7Rtq.js.map +0 -1
  187. package/public/assets/activity-BcYf8YNO.js +0 -2
  188. package/public/assets/activity-BcYf8YNO.js.map +0 -1
  189. package/public/assets/agents-CgRFJyks.js +0 -2
  190. package/public/assets/card-Cds0MWME.js +0 -2
  191. package/public/assets/chat-BLcm2Aht.js +0 -2
  192. package/public/assets/display-BWi8DQCp.js +0 -3
  193. package/public/assets/display-BWi8DQCp.js.map +0 -1
  194. package/public/assets/dist-CBVZk9U8.js +0 -2
  195. package/public/assets/dist-DC-TqTz2.js +0 -2
  196. package/public/assets/dist-DreO5AcZ.js +0 -2
  197. package/public/assets/index-3ydeNO0c.js +0 -23
  198. package/public/assets/index-3ydeNO0c.js.map +0 -1
  199. package/public/assets/index-BjjzI2LT.css +0 -2
  200. package/public/assets/managed-agents-CVca-s3t.js +0 -4
  201. package/public/assets/markdown-preview-impl-BPW4OQo9.js +0 -2
  202. package/public/assets/memory-BzuDTHC5.js +0 -2
  203. package/public/assets/overview-Dyvm1qpT.js +0 -2
  204. package/public/assets/projects-vt63bwHN.js +0 -2
  205. package/public/assets/prompt-settings-xVXmvxB5.js +0 -2
  206. package/public/assets/registry-ypiICauZ.js +0 -2
  207. package/public/assets/settings-87clTx4N.js +0 -6
  208. package/public/assets/store-DUfrGvOE.js +0 -9
  209. package/public/assets/store-DUfrGvOE.js.map +0 -1
  210. package/public/assets/tasks-1Kvp53wh.js +0 -2
  211. package/public/assets/tasks-1Kvp53wh.js.map +0 -1
  212. package/public/assets/teams-DJWp4AKi.js +0 -2
  213. package/public/assets/types-DorDDvEb.js +0 -2
  214. package/public/assets/types-DorDDvEb.js.map +0 -1
  215. package/public/assets/user-avatar-CfAUS3PZ.js +0 -2
  216. package/public/assets/workspaces-CIsQqQby.js +0 -3
  217. package/public/assets/workspaces-CIsQqQby.js.map +0 -1
@@ -0,0 +1,180 @@
1
+ // #832 — Task-bound memory: task-local slice of the #815 scoped-knowledge hierarchy.
2
+ //
3
+ // This deliberately mirrors the self-resume continuation envelope primitive:
4
+ // replaceable working state, append-only ruled-out ledger, append-only decisions.
5
+ // When #815 lands, this module is the adapter from durable Task ids into the
6
+ // session/task scope in that chain; do not grow a second memory system beside it.
7
+ import { parseJson } from "../utils";
8
+ import { getDb, ValidationError } from "./connection.ts";
9
+ import { getTaskDetail } from "./task-entity.ts";
10
+ import { isContinuationDecisionEntry, isContinuationLedgerEntry } from "./continuations.ts";
11
+ import type { ContinuationBudget, ContinuationDecisionEntry, ContinuationEnvelope, ContinuationLedgerEntry, TaskDetail } from "../types";
12
+
13
+ interface TaskMemoryRow {
14
+ task_id: number;
15
+ objective: string;
16
+ generation: number;
17
+ ledger: string;
18
+ decisions: string;
19
+ working_state: string;
20
+ archive_refs: string;
21
+ budget: string;
22
+ created_at: number;
23
+ updated_at: number;
24
+ }
25
+
26
+ export type TaskMemoryEnvelope = ContinuationEnvelope & { taskId: number };
27
+
28
+ export function getTaskMemoryEnvelope(taskId: number): TaskMemoryEnvelope | null {
29
+ const row = getDb().query("SELECT * FROM task_memories WHERE task_id = ?").get(taskId) as TaskMemoryRow | undefined;
30
+ return row ? rowToTaskMemoryEnvelope(row) : null;
31
+ }
32
+
33
+ export function readTaskMemoryEnvelope(taskId: number): TaskMemoryEnvelope {
34
+ const task = requireTask(taskId);
35
+ return getTaskMemoryEnvelope(taskId) ?? emptyTaskMemoryEnvelope(task, Date.now());
36
+ }
37
+
38
+ export function replaceTaskMemoryWorkingState(input: { taskId: number; workingState: string; now?: number }): TaskMemoryEnvelope {
39
+ const existing = ensureTaskMemoryEnvelope(input.taskId, input.now);
40
+ const workingState = input.workingState.trim();
41
+ if (!workingState) throw new ValidationError("workingState required");
42
+ const now = input.now ?? Date.now();
43
+ getDb().query(`
44
+ UPDATE task_memories
45
+ SET generation = ?, working_state = ?, budget = ?, updated_at = ?
46
+ WHERE task_id = ?
47
+ `).run(existing.generation + 1, workingState, JSON.stringify(nextBudget(existing, now)), now, input.taskId);
48
+ return readTaskMemoryEnvelope(input.taskId);
49
+ }
50
+
51
+ export function appendTaskMemoryRuledOut(input: {
52
+ taskId: number;
53
+ entries: Array<string | { ruledOut: string; why?: string }>;
54
+ now?: number;
55
+ }): TaskMemoryEnvelope {
56
+ const existing = ensureTaskMemoryEnvelope(input.taskId, input.now);
57
+ if (!Array.isArray(input.entries) || input.entries.length === 0) throw new ValidationError("ruledOut must contain at least one entry");
58
+ const now = input.now ?? Date.now();
59
+ const generation = existing.generation + 1;
60
+ const entries = input.entries.map((entry, index) => ruledOutEntry(entry, index, generation, now));
61
+ getDb().query(`
62
+ UPDATE task_memories
63
+ SET generation = ?, ledger = ?, budget = ?, updated_at = ?
64
+ WHERE task_id = ?
65
+ `).run(generation, JSON.stringify([...existing.ledger, ...entries]), JSON.stringify(nextBudget(existing, now)), now, input.taskId);
66
+ return readTaskMemoryEnvelope(input.taskId);
67
+ }
68
+
69
+ export function appendTaskMemoryDecision(input: {
70
+ taskId: number;
71
+ decision: string;
72
+ rationale?: string;
73
+ by?: string;
74
+ now?: number;
75
+ }): TaskMemoryEnvelope {
76
+ const existing = ensureTaskMemoryEnvelope(input.taskId, input.now);
77
+ const decision = input.decision.trim();
78
+ if (!decision) throw new ValidationError("decision required");
79
+ const rationale = input.rationale?.trim() ?? "";
80
+ const now = input.now ?? Date.now();
81
+ const entry: ContinuationDecisionEntry = {
82
+ gen: existing.generation + 1,
83
+ decision,
84
+ rationale,
85
+ ...(input.by?.trim() ? { by: input.by.trim() } : {}),
86
+ at: now,
87
+ };
88
+ getDb().query(`
89
+ UPDATE task_memories
90
+ SET generation = ?, decisions = ?, budget = ?, updated_at = ?
91
+ WHERE task_id = ?
92
+ `).run(entry.gen, JSON.stringify([...existing.decisions, entry]), JSON.stringify(nextBudget(existing, now)), now, input.taskId);
93
+ return readTaskMemoryEnvelope(input.taskId);
94
+ }
95
+
96
+ function ensureTaskMemoryEnvelope(taskId: number, now = Date.now()): TaskMemoryEnvelope {
97
+ const existing = getTaskMemoryEnvelope(taskId);
98
+ if (existing) return existing;
99
+ const task = requireTask(taskId);
100
+ const empty = emptyTaskMemoryEnvelope(task, now);
101
+ getDb().query(`
102
+ INSERT INTO task_memories (task_id, objective, generation, ledger, decisions, working_state, archive_refs, budget, created_at, updated_at)
103
+ VALUES (?, ?, 0, '[]', '[]', '', '[]', ?, ?, ?)
104
+ `).run(taskId, empty.objective, JSON.stringify(empty.budget), now, now);
105
+ return readTaskMemoryEnvelope(taskId);
106
+ }
107
+
108
+ function requireTask(taskId: number): TaskDetail {
109
+ const task = getTaskDetail(taskId);
110
+ if (!task) throw new ValidationError(`task ${taskId} not found or not a deliverable task`);
111
+ return task;
112
+ }
113
+
114
+ function emptyTaskMemoryEnvelope(task: TaskDetail, now: number): TaskMemoryEnvelope {
115
+ return {
116
+ taskId: task.id,
117
+ agentId: `task:${task.id}`,
118
+ objective: `Task #${task.id}: ${task.title}`,
119
+ generation: 0,
120
+ ledger: [],
121
+ decisions: [],
122
+ workingState: "",
123
+ archiveRefs: [],
124
+ budget: emptyBudget(),
125
+ createdAt: now,
126
+ updatedAt: now,
127
+ };
128
+ }
129
+
130
+ function rowToTaskMemoryEnvelope(row: TaskMemoryRow): TaskMemoryEnvelope {
131
+ return {
132
+ taskId: row.task_id,
133
+ agentId: `task:${row.task_id}`,
134
+ objective: row.objective,
135
+ generation: row.generation,
136
+ ledger: parseJson<ContinuationLedgerEntry[]>(row.ledger, []).filter(isContinuationLedgerEntry),
137
+ decisions: parseJson<ContinuationDecisionEntry[]>(row.decisions, []).filter(isContinuationDecisionEntry),
138
+ workingState: row.working_state,
139
+ archiveRefs: parseJson<string[]>(row.archive_refs, []).filter((value): value is string => typeof value === "string"),
140
+ budget: parseJson<ContinuationBudget>(row.budget, emptyBudget()),
141
+ createdAt: row.created_at,
142
+ updatedAt: row.updated_at,
143
+ };
144
+ }
145
+
146
+ function ruledOutEntry(
147
+ value: string | { ruledOut: string; why?: string },
148
+ index: number,
149
+ generation: number,
150
+ now: number,
151
+ ): ContinuationLedgerEntry {
152
+ if (typeof value !== "string" && (typeof value !== "object" || value === null || Array.isArray(value))) {
153
+ throw new ValidationError(`ruledOut[${index}] must be a string or object`);
154
+ }
155
+ const ruledOut = typeof value === "string" ? value.trim() : value.ruledOut?.trim();
156
+ if (!ruledOut) throw new ValidationError(`ruledOut[${index}] required`);
157
+ const why = typeof value === "string" ? "" : value.why?.trim() ?? "";
158
+ return { gen: generation, ruledOut, why, at: now };
159
+ }
160
+
161
+ function nextBudget(existing: TaskMemoryEnvelope, now: number): ContinuationBudget {
162
+ return {
163
+ ...existing.budget,
164
+ spent: {
165
+ ...existing.budget.spent,
166
+ generations: existing.generation + 1,
167
+ wallClockMs: Math.max(0, existing.budget.spent.wallClockMs) + Math.max(0, now - existing.updatedAt),
168
+ },
169
+ };
170
+ }
171
+
172
+ function emptyBudget(): ContinuationBudget {
173
+ return {
174
+ maxGenerations: 0,
175
+ maxWallClockMs: 0,
176
+ maxTokens: 0,
177
+ hardStopMultiplier: 1,
178
+ spent: { generations: 0, wallClockMs: 0, tokens: 0 },
179
+ };
180
+ }
package/src/db/tasks.ts CHANGED
@@ -19,7 +19,7 @@ import { messageTaskClosed } from "./claim-sql.ts";
19
19
  import { ClaimError, getDb } from "./connection.ts";
20
20
  import { rowToTask, rowToTaskEvent } from "./mappers.ts";
21
21
  import { claimMessageRow } from "./message-claims.ts";
22
- import { TASK_SELECT, insertTaskEvent } from "./task-core.ts";
22
+ import { TASK_SELECT, insertTaskEvent, claimTaskWhere, DELIVERABLE_TASK_KIND } from "./task-core.ts";
23
23
  import { settleSingleTargetOnDemandTasks } from "./task-maintenance.ts";
24
24
  import { renderTemplate } from "../prompt-resolver";
25
25
  import type {
@@ -85,7 +85,7 @@ import type {
85
85
  WorkspaceStatus,
86
86
  } from "../types";
87
87
 
88
- export { TASK_SELECT, insertTaskEvent } from "./task-core.ts";
88
+ export { TASK_SELECT, insertTaskEvent, claimTaskWhere, CLAIM_TASK_KIND, DELIVERABLE_TASK_KIND } from "./task-core.ts";
89
89
 
90
90
  export function findOpenTaskByDedupe(source: string, dedupeKey: string): Task | null {
91
91
  const row = getDb()
@@ -112,9 +112,16 @@ export function getTask(id: number): Task | null {
112
112
  return row ? rowToTask(row) : null;
113
113
  }
114
114
 
115
- export function listTasks(filter?: { status?: string; source?: string; target?: string; limit?: number }): Task[] {
115
+ export function listTasks(filter?: { status?: string; source?: string; target?: string; kind?: "claim" | "deliverable" | "all"; limit?: number }): Task[] {
116
116
  const conditions: string[] = [];
117
117
  const params: any[] = [];
118
+ // Default to the claim lane so legacy queues/views (dashboard work queue, GET /api/tasks)
119
+ // never get polluted by durable deliverable Tasks (#834). Pass kind:'all' to span both.
120
+ const kind = filter?.kind ?? "claim";
121
+ if (kind !== "all") {
122
+ conditions.push("kind = ?");
123
+ params.push(kind);
124
+ }
118
125
  if (filter?.status) {
119
126
  conditions.push("status = ?");
120
127
  params.push(filter.status);
@@ -165,7 +172,7 @@ export function releaseExpiredClaims(now: number = Date.now()): { messageIds: nu
165
172
  .query(`SELECT id FROM messages WHERE ${releasableMessageClaim}`)
166
173
  .all(now) as any[];
167
174
  const taskRows = getDb()
168
- .query(`${TASK_SELECT} WHERE claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND status IN ('claimed', 'in_progress', 'blocked')`)
175
+ .query(`${TASK_SELECT} WHERE ${claimTaskWhere()} AND claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND status IN ('claimed', 'in_progress', 'blocked')`)
169
176
  .all(now) as any[];
170
177
 
171
178
  if (messageRows.length > 0) {
@@ -176,7 +183,7 @@ export function releaseExpiredClaims(now: number = Date.now()): { messageIds: nu
176
183
 
177
184
  if (taskRows.length > 0) {
178
185
  getDb()
179
- .query("UPDATE tasks SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ? WHERE claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND status IN ('claimed', 'in_progress', 'blocked')")
186
+ .query(`UPDATE tasks SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ? WHERE ${claimTaskWhere()} AND claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND status IN ('claimed', 'in_progress', 'blocked')`)
180
187
  .run(now, now);
181
188
 
182
189
  for (const row of taskRows) {
@@ -201,14 +208,14 @@ export function releaseExpiredClaims(now: number = Date.now()): { messageIds: nu
201
208
  export function orphanTasksForAgent(agentId: string, now: number = Date.now()): Task[] {
202
209
  return getDb().transaction(() => {
203
210
  const rows = getDb()
204
- .query(`${TASK_SELECT} WHERE claimed_by = ? AND status IN ('claimed', 'in_progress')`)
211
+ .query(`${TASK_SELECT} WHERE ${claimTaskWhere()} AND claimed_by = ? AND status IN ('claimed', 'in_progress')`)
205
212
  .all(agentId) as any[];
206
213
  if (rows.length === 0) return [];
207
214
 
208
215
  getDb().query(`
209
216
  UPDATE tasks
210
217
  SET status = 'orphaned', updated_at = ?, last_seen_at = ?
211
- WHERE claimed_by = ? AND status IN ('claimed', 'in_progress')
218
+ WHERE ${claimTaskWhere()} AND claimed_by = ? AND status IN ('claimed', 'in_progress')
212
219
  `).run(now, now, agentId);
213
220
 
214
221
  for (const row of rows) {
@@ -231,14 +238,14 @@ export function releaseOrphanedTasks(graceMs = 120_000, now: number = Date.now()
231
238
  const cutoff = now - graceMs;
232
239
  settleSingleTargetOnDemandTasks("status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?", [cutoff], now, "orphan-grace-elapsed");
233
240
  const rows = getDb()
234
- .query(`${TASK_SELECT} WHERE status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?`)
241
+ .query(`${TASK_SELECT} WHERE ${claimTaskWhere()} AND status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?`)
235
242
  .all(cutoff) as any[];
236
243
  if (rows.length === 0) return [];
237
244
 
238
245
  getDb().query(`
239
246
  UPDATE tasks
240
247
  SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ?
241
- WHERE status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?
248
+ WHERE ${claimTaskWhere()} AND status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?
242
249
  `).run(now, cutoff);
243
250
 
244
251
  for (const row of rows) {
@@ -265,6 +272,8 @@ export function claimTask(taskId: number, agentId: string, guard?: AgentSessionG
265
272
  if (agent.status === "offline") return { ok: false, error: "claiming agent is offline" };
266
273
  const task = getTask(taskId);
267
274
  if (!task) return { ok: false, error: "task not found" };
275
+ // Fence: deliverable Tasks are never claimable through the claim-lease lane (#834).
276
+ if (task.kind === DELIVERABLE_TASK_KIND) return { ok: false, error: "task is not claimable" };
268
277
  if (!["open", "blocked"].includes(task.status)) return { ok: false, error: `task is ${task.status}` };
269
278
 
270
279
  try {
@@ -273,7 +282,7 @@ export function claimTask(taskId: number, agentId: string, guard?: AgentSessionG
273
282
  const expiresAt = now + CLAIM_LEASE_MS;
274
283
  const result = getDb().query(`
275
284
  UPDATE tasks SET status = 'claimed', claimed_by = ?, claimed_at = ?, claim_expires_at = ?, updated_at = ?
276
- WHERE id = ? AND status IN ('open', 'blocked')
285
+ WHERE id = ? AND ${claimTaskWhere()} AND status IN ('open', 'blocked')
277
286
  `).run(agentId, now, expiresAt, now, taskId);
278
287
  if (result.changes === 0) return { ok: false, error: "claim race — already claimed" };
279
288
  if (task.messageId) {
@@ -305,6 +314,7 @@ export function renewTaskClaim(taskId: number, agentId: string, guard?: AgentSes
305
314
  if (agent.status === "offline") return { ok: false, error: "claiming agent is offline" };
306
315
  const task = getTask(taskId);
307
316
  if (!task) return { ok: false, error: "task not found" };
317
+ if (task.kind === DELIVERABLE_TASK_KIND) return { ok: false, error: "task is not claimable" };
308
318
  if (task.claimedBy !== agentId) return { ok: false, error: task.claimedBy ? `claimed by ${task.claimedBy}` : "task is not claimed" };
309
319
  if (!["claimed", "in_progress", "blocked"].includes(task.status)) return { ok: false, error: `task is ${task.status}` };
310
320
 
@@ -320,6 +330,10 @@ export function renewTaskClaim(taskId: number, agentId: string, guard?: AgentSes
320
330
  export function updateTaskStatus(taskId: number, input: TaskStatusInput): { ok: boolean; error?: string; task?: Task; event?: TaskEvent } {
321
331
  const task = getTask(taskId);
322
332
  if (!task) return { ok: false, error: "task not found" };
333
+ // Fence: the claim-lane status updater writes `claimed_by`; deliverable Tasks must never
334
+ // be mutated here. They transition through setTaskStatus (src/db/task-entity.ts), which
335
+ // never touches claimed_by. (#834)
336
+ if (task.kind === DELIVERABLE_TASK_KIND) return { ok: false, error: "deliverable task: use the task-entity status API" };
323
337
  const now = Date.now();
324
338
  const agentId = input.agentId ?? task.claimedBy ?? null;
325
339
  if (agentId) {
@@ -5,6 +5,15 @@ export interface IssueTrackerStatus {
5
5
  title: string;
6
6
  }
7
7
 
8
+ // #838 — the linked-issue spec read used by relay to hydrate a spawned Task agent's
9
+ // first-turn context server-side ONCE (so the worker never burns its own context reading
10
+ // the issue tracker). Extends the status read with the issue BODY (intent/spec/acceptance).
11
+ export interface IssueTrackerIssueDetail {
12
+ status: IssueRefStatus;
13
+ title: string;
14
+ body: string;
15
+ }
16
+
8
17
  export interface IssueTrackerCloseOptions {
9
18
  comment?: string;
10
19
  labels?: string[];
@@ -26,9 +35,15 @@ export interface IssueTrackerIssueSummary {
26
35
  export interface IssueTrackerAdapter {
27
36
  resolveLink(ref: IssueRef): string;
28
37
  getStatus(ref: IssueRef): Promise<IssueTrackerStatus>;
38
+ /** #838 — read the issue's full spec (title + body) for relay-side Task hydration. Optional:
39
+ * adapters that cannot fetch a body degrade gracefully (hydration falls back to title+url). */
40
+ getDetail?(ref: IssueRef): Promise<IssueTrackerIssueDetail>;
29
41
  listOpenSubIssues?(ref: IssueRef): Promise<IssueTrackerIssueSummary[]>;
30
42
  close(ref: IssueRef, options?: IssueTrackerCloseOptions): Promise<void>;
31
43
  reopen(ref: IssueRef, options?: IssueTrackerReopenOptions): Promise<void>;
32
44
  comment(ref: IssueRef, body: string): Promise<void>;
45
+ // List existing comment bodies on the issue. Used to RECONCILE a Task comment-through after
46
+ // a post-before-finalize crash (#833): match the relay marker rather than blindly re-post.
47
+ listComments?(ref: IssueRef): Promise<string[]>;
33
48
  applyLabels(ref: IssueRef, labels: string[]): Promise<void>;
34
49
  }
@@ -1,8 +1,10 @@
1
1
  import type { IssueRef, IssueRefStatus } from "../types";
2
- import type { IssueTrackerAdapter, IssueTrackerCloseOptions, IssueTrackerIssueSummary, IssueTrackerReopenOptions, IssueTrackerStatus } from "./adapter";
2
+ import type { IssueTrackerAdapter, IssueTrackerCloseOptions, IssueTrackerIssueDetail, IssueTrackerIssueSummary, IssueTrackerReopenOptions, IssueTrackerStatus } from "./adapter";
3
3
 
4
4
  export const GITHUB_FIXED_LABEL = "fixed";
5
5
  const GITHUB_CREDENTIAL_REQUIREMENT = "GitHub issue tracker adapter requires gh authenticated with a token that has issues:write scope";
6
+ // #838 — hard bound on the spawn-time issue read so hydration never stalls the spawn.
7
+ const ISSUE_DETAIL_READ_TIMEOUT_MS = 10_000;
6
8
 
7
9
  type GitHubApiMethod = "GET" | "PATCH" | "POST";
8
10
 
@@ -10,6 +12,10 @@ export interface GitHubApiRequest {
10
12
  method: GitHubApiMethod;
11
13
  path: string;
12
14
  body?: Record<string, unknown>;
15
+ /** #838 — bound the spawned `gh` subprocess so a hung/slow call can't stall the caller
16
+ * (e.g. relay_spawn_agent hydration). On timeout the process is killed and the call throws,
17
+ * which the hydration path catches and degrades to partial "spec unavailable". */
18
+ timeoutMs?: number;
13
19
  }
14
20
 
15
21
  type GitHubApi = (request: GitHubApiRequest) => Promise<unknown>;
@@ -18,6 +24,7 @@ interface GitHubIssuePayload {
18
24
  state?: unknown;
19
25
  title?: unknown;
20
26
  html_url?: unknown;
27
+ body?: unknown;
21
28
  }
22
29
 
23
30
  interface GitHubSubIssueNode {
@@ -76,16 +83,20 @@ async function ghApi(request: GitHubApiRequest): Promise<unknown> {
76
83
  // auth backed by a GitHub token with `issues:write` scope.
77
84
  const args = ["gh", "api", "--method", request.method, request.path, "--header", "Accept: application/vnd.github+json"];
78
85
  appendBodyArgs(args, request.body);
86
+ // `Bun.spawnSync` blocks the event loop synchronously, so a JS-level timeout race can't preempt
87
+ // a hung `gh`; the subprocess `timeout` (kills with SIGTERM on expiry) is the only effective bound.
79
88
  const proc = Bun.spawnSync(args, {
80
89
  stdin: "ignore",
81
90
  stdout: "pipe",
82
91
  stderr: "pipe",
83
92
  env: process.env,
93
+ ...(request.timeoutMs ? { timeout: request.timeoutMs } : {}),
84
94
  });
85
95
  const stdout = textFromBytes(proc.stdout).trim();
86
96
  if (proc.exitCode !== 0) {
87
97
  const stderr = textFromBytes(proc.stderr).trim();
88
- throw new Error(`${GITHUB_CREDENTIAL_REQUIREMENT}; gh api failed: ${stderr || stdout || `exit ${proc.exitCode}`}`);
98
+ const timedOut = request.timeoutMs && (proc.signalCode === "SIGTERM" || proc.exitCode === null);
99
+ throw new Error(`${GITHUB_CREDENTIAL_REQUIREMENT}; gh api ${timedOut ? `timed out after ${request.timeoutMs}ms` : "failed"}: ${stderr || stdout || `exit ${proc.exitCode}`}`);
89
100
  }
90
101
  if (!stdout) return {};
91
102
  try {
@@ -152,6 +163,19 @@ class GitHubIssueTrackerAdapter implements IssueTrackerAdapter {
152
163
  return { status: statusFromState(issue.state), title: titleFromIssue(issue) };
153
164
  }
154
165
 
166
+ // #838 — same GET as getStatus, but also surfaces the issue body (intent/spec/acceptance) so
167
+ // relay can hydrate a spawned Task agent server-side. ONE issue read at spawn time; the worker
168
+ // never touches the issue tracker itself. Bounded by a timeout so a hung `gh` degrades the
169
+ // hydration to partial "spec unavailable" rather than stalling relay_spawn_agent.
170
+ async getDetail(ref: IssueRef): Promise<IssueTrackerIssueDetail> {
171
+ const issue = await this.api({ method: "GET", path: githubIssuePath(ref), timeoutMs: ISSUE_DETAIL_READ_TIMEOUT_MS }) as GitHubIssuePayload;
172
+ return {
173
+ status: statusFromState(issue.state),
174
+ title: titleFromIssue(issue),
175
+ body: typeof issue.body === "string" ? issue.body.trim() : "",
176
+ };
177
+ }
178
+
155
179
  async listOpenSubIssues(ref: IssueRef): Promise<IssueTrackerIssueSummary[]> {
156
180
  assertGitHubIssueRef(ref);
157
181
  const [owner, repo] = ref.repo.split("/") as [string, string];
@@ -203,6 +227,20 @@ class GitHubIssueTrackerAdapter implements IssueTrackerAdapter {
203
227
  await this.api({ method: "POST", path: `${githubIssuePath(ref)}/comments`, body: { body: trimmed } });
204
228
  }
205
229
 
230
+ async listComments(ref: IssueRef): Promise<string[]> {
231
+ const out: string[] = [];
232
+ for (let page = 1; page <= 10; page++) {
233
+ const batch = await this.api({ method: "GET", path: `${githubIssuePath(ref)}/comments?per_page=100&page=${page}` });
234
+ if (!Array.isArray(batch) || batch.length === 0) break;
235
+ for (const node of batch) {
236
+ const body = node && typeof node === "object" ? (node as { body?: unknown }).body : undefined;
237
+ if (typeof body === "string") out.push(body);
238
+ }
239
+ if (batch.length < 100) break;
240
+ }
241
+ return out;
242
+ }
243
+
206
244
  async applyLabels(ref: IssueRef, labels: string[]): Promise<void> {
207
245
  const cleaned = uniqueLabels(labels);
208
246
  if (cleaned.length === 0) return;
@@ -1,6 +1,7 @@
1
1
  export type {
2
2
  IssueTrackerAdapter,
3
3
  IssueTrackerCloseOptions,
4
+ IssueTrackerIssueDetail,
4
5
  IssueTrackerIssueSummary,
5
6
  IssueTrackerReopenOptions,
6
7
  IssueTrackerStatus,
package/src/mcp/index.ts CHANGED
@@ -5,6 +5,11 @@ import { spawnCapablePrimer } from "../spawn-targets";
5
5
  import { McpAuthError, McpNotFoundError } from "../mcp-errors";
6
6
  import { createActivityEvent, ValidationError } from "../db";
7
7
  import { PLAN_TOOLS, relayPlanTool } from "../mcp-plan-tools";
8
+ import { TASK_TOOLS, relayTaskTool } from "../mcp-task-tools";
9
+ import { TASK_MEMORY_TOOLS, relayTaskMemoryTool } from "../mcp-task-memory-tools";
10
+ import { DISPATCH_TOOLS, relayDispatchTool } from "../mcp-dispatch-tools";
11
+ import { TASK_WEAVE_TOOLS, relayTaskWeaveTool } from "../mcp-task-weave-tools";
12
+ import { RELEASE_TOOLS, relayReleaseTool } from "../mcp-release-tools";
8
13
  import { TEAM_TOOLS, relayTeamTool } from "../mcp-team-tools";
9
14
  import { BLACKBOARD_TOOLS, relayBlackboardTool } from "../mcp-blackboard-tools";
10
15
  import { PROJECT_TOOLS, relayProjectTool } from "../mcp-project-tools"; import { MEMORY_TOOLS, relayMemoryTool } from "../mcp-memory-tools"; import { ISSUE_REF_TOOLS, relayIssueRefTool } from "../mcp-issue-ref-tools"; import { ISSUE_ASSOCIATION_TOOLS, relayIssueAssociationTool } from "../mcp-issue-association-tools"; import { ISSUE_ACTIVITY_TOOLS, relayIssueActivityTool } from "../mcp-issue-activity-tools"; import { ISSUE_LIFECYCLE_TOOLS, relayIssueLifecycleTool } from "../mcp-issue-lifecycle-tools";
@@ -32,7 +37,7 @@ const TOOLS: ToolDefinition[] = [
32
37
  AGENT_ACTION_TOOL,
33
38
  ...WORKSPACE_TOOLS,
34
39
  ...MERGE_TOOLS, ...TEAM_TOOLS,
35
- ...PLAN_TOOLS, ...ISSUE_REF_TOOLS, ...ISSUE_ASSOCIATION_TOOLS, ...ISSUE_ACTIVITY_TOOLS, ...ISSUE_LIFECYCLE_TOOLS,
40
+ ...PLAN_TOOLS, ...TASK_TOOLS, ...TASK_MEMORY_TOOLS, ...DISPATCH_TOOLS, ...TASK_WEAVE_TOOLS, ...RELEASE_TOOLS, ...ISSUE_REF_TOOLS, ...ISSUE_ASSOCIATION_TOOLS, ...ISSUE_ACTIVITY_TOOLS, ...ISSUE_LIFECYCLE_TOOLS,
36
41
  ...BLACKBOARD_TOOLS,
37
42
  ...PROJECT_TOOLS,
38
43
  ...MEMORY_TOOLS,
@@ -50,6 +55,7 @@ export const RELAY_TOOL_GROUPS: Record<string, readonly string[]> = {
50
55
  workspace: ["relay_workspace_status", "relay_workspace_ready", "relay_workspace_deps", "relay_workspace_list", "relay_workspace_claim", "relay_workspace_release", "relay_workspace_land", "relay_merge_pause", "relay_merge_pauses", "relay_merge_renew", "relay_merge_resume"],
51
56
  "spawn-lifecycle": ["relay_spawn_agent", "relay_spawn_targets", "relay_my_agents", "relay_shutdown_agent", "relay_agent_action", "relay_agent_status", "relay_agent_crash_report", "relay_schedule_timer", "relay_list_timers", "relay_cancel_timer"],
52
57
  plans: ["relay_plan_create", "relay_plan_add_node", "relay_plan_update_node", "relay_plan_add_edge", "relay_plan_remove_node", "relay_plan_remove_edge", "relay_plan_delete", "relay_plan_get"],
58
+ tasks: ["relay_task_create", "relay_task_get", "relay_task_list", "relay_task_update", "relay_task_set_status", "relay_task_set_stage", "relay_task_record_gate", "relay_task_add_dependency", "relay_task_remove_dependency", "relay_task_add_ref", "relay_task_remove_ref", "relay_task_assign_agent", "relay_task_history", "relay_task_memory_read", "relay_task_memory_replace_working_state", "relay_task_memory_append_ruled_out", "relay_task_memory_append_decision", "relay_task_dispatch", "relay_task_comment", "relay_task_close_bridge", "relay_release_readiness"],
53
59
  teams: ["relay_team_get", "relay_team_set_brief", "relay_team_claim", "relay_team_dissolve"],
54
60
  blackboard: ["relay_blackboard_write", "relay_blackboard_get", "relay_blackboard_read", "relay_blackboard_search", "relay_blackboard_retract"],
55
61
  project: ["relay_project_list", "relay_project_get", "relay_project_add_root", "relay_project_write_entry", "relay_project_read", "relay_project_search", "relay_project_retract", "relay_project_add_edge", "relay_project_remove_edge", "relay_project_promote_entry", "relay_project_ingest_claudemd"],
@@ -167,6 +173,11 @@ async function callTool(auth: McpAuthContext, params: unknown): Promise<Record<s
167
173
  else if (name === "relay_workspace_land") result = relayWorkspaceMutation(auth, "merge", args);
168
174
  else if (name.startsWith("relay_merge_")) result = relayMergeTool(auth, name, args); else if (name.startsWith("relay_team_")) result = await relayTeamTool(auth, name, args);
169
175
  else if (name.startsWith("relay_plan_")) result = relayPlanTool(auth, name, args);
176
+ else if (name.startsWith("relay_task_memory_")) result = relayTaskMemoryTool(auth, name, args);
177
+ else if (name === "relay_task_dispatch") result = relayDispatchTool(auth, name, args);
178
+ else if (name === "relay_task_comment" || name === "relay_task_close_bridge") result = await relayTaskWeaveTool(auth, name, args);
179
+ else if (name === "relay_release_readiness") result = relayReleaseTool(name, args);
180
+ else if (name.startsWith("relay_task_")) result = relayTaskTool(auth, name, args);
170
181
  else if (name.startsWith("relay_blackboard_")) result = relayBlackboardTool(auth, name, args);
171
182
  else if (name.startsWith("relay_project_")) result = relayProjectTool(auth, name, args); else if (name.startsWith("relay_memory_")) result = relayMemoryTool(auth, name, args); else if (name.startsWith("relay_issue_ref_")) result = relayIssueRefTool(auth, name, args); else if (name.startsWith("relay_issue_association_")) result = relayIssueAssociationTool(auth, name, args); else if (name.startsWith("relay_issue_activity_")) result = relayIssueActivityTool(auth, name, args); else if (name.startsWith("relay_issue_lifecycle_")) result = await relayIssueLifecycleTool(auth, name, args);
172
183
  else if (name === "relay_subscribe" || name === "relay_unsubscribe" || name === "relay_list_subscriptions" || name === "relay_why_notified" || name === "relay_notification_preview") result = relayNotificationSubscriptionTool(auth, name, args);
@@ -37,6 +37,8 @@ export const SPAWN_TOOLS: ToolDefinition[] = [
37
37
  effort: { type: "string", enum: VALID_EFFORTS },
38
38
  approvalMode: { type: "string", enum: APPROVAL_MODES, description: "Permission posture for the worker. Defaults to YOUR OWN approval mode when omitted (a headless `guarded` worker wedges on the first approval prompt), so a coordinator running `open` spawns workers that can act autonomously. Pass an explicit value to narrow a child (e.g. `read-only` reviewer)." },
39
39
  prompt: { type: "string", description: "Initial task/message delivered to the agent on launch — spawn and hand it its first instruction in one call (no separate follow-up message needed)." },
40
+ taskId: { type: "integer", minimum: 1, description: "#838 — spawn the worker bound to a durable Task. Relay hydrates the worker's FIRST-TURN context FROM the Task (intent + linked-issue spec/acceptance + recorded decisions + history + attribution), resolved server-side ONCE, so you do NOT hand-write a long brief and the worker does NOT read the issue tracker. Records the spawn on the Task for attribution. Pair with `steer` for the one-line actionable instruction. When model/effort are omitted, relay MAY resolve them from the Task's dispatch work-shape." },
41
+ steer: { type: "string", description: "#838 — the latest one-line steer: the actionable instruction layered over the hydrated Task context (used with `taskId`). Overrides the Task's stored latest steer for this spawn." },
40
42
  expectReply: { type: "boolean", description: "Legacy compatibility flag. Spawned workers now report up automatically: each turn-final response is routed to the spawner by lineage, without a reply obligation or idle nag." },
41
43
  systemPromptAppend: { type: "string" },
42
44
  projectInstructions: { type: "object", description: "#409 — project instruction projection (default OFF). When set, relay composes a knowledge preamble from the spawn cwd's resolved project (own + inherited entries) and prepends it to systemPromptAppend, instead of relying on host CLAUDE.md. `policy: \"all\"` includes ingested host content; `\"vanilla\"` excludes it (manual+promoted only). Omit to keep current behavior.", properties: { policy: { type: "string", enum: PROJECT_INSTRUCTION_POLICY_NAMES } }, additionalProperties: false },
@@ -143,6 +145,8 @@ export async function relaySpawnAgent(auth: McpAuthContext, args: Record<string,
143
145
  policyName: optionalString(args.policyName, "policyName", 120),
144
146
  profile: optionalString(args.profile, "profile", 120),
145
147
  prompt: optionalString(args.prompt, "prompt", 16_000),
148
+ taskId: optionalPositiveInt(args.taskId, "taskId"),
149
+ steer: optionalString(args.steer, "steer", 16_000),
146
150
  expectReply: typeof args.expectReply === "boolean" ? args.expectReply : undefined,
147
151
  systemPromptAppend: optionalString(args.systemPromptAppend, "systemPromptAppend", 64_000),
148
152
  projectInstructions: parseProjectInstructions(args.projectInstructions, (m) => new ValidationError(m)),