agent-relay-server 0.126.0 → 0.127.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 (75) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +3 -3
  3. package/public/assets/{MessageBubble-B6YgulnX.js → MessageBubble-D2cnxyTy.js} +2 -2
  4. package/public/assets/{MessageBubble-B6YgulnX.js.map → MessageBubble-D2cnxyTy.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel--DNxHn1T.js → PlanGraphPanel-Cnxt-x7R.js} +2 -2
  6. package/public/assets/{PlanGraphPanel--DNxHn1T.js.map → PlanGraphPanel-Cnxt-x7R.js.map} +1 -1
  7. package/public/assets/{activity-Fo5aLYD6.js → activity-BEm1BQB1.js} +2 -2
  8. package/public/assets/{activity-Fo5aLYD6.js.map → activity-BEm1BQB1.js.map} +1 -1
  9. package/public/assets/{agent-profiles-C3p0xclG.js → agent-profiles-C8V_oCiM.js} +2 -2
  10. package/public/assets/{agent-profiles-C3p0xclG.js.map → agent-profiles-C8V_oCiM.js.map} +1 -1
  11. package/public/assets/{agents-_GBQyF5b.js → agents-D8mc9X1j.js} +2 -2
  12. package/public/assets/{agents-_GBQyF5b.js.map → agents-D8mc9X1j.js.map} +1 -1
  13. package/public/assets/{automation-Cf_Ewi9f.js → automation-BEhJZkNQ.js} +2 -2
  14. package/public/assets/{automation-Cf_Ewi9f.js.map → automation-BEhJZkNQ.js.map} +1 -1
  15. package/public/assets/{chat-BdbIWZpQ.js → chat-BAH9bEPx.js} +2 -2
  16. package/public/assets/chat-BAH9bEPx.js.map +1 -0
  17. package/public/assets/{formatted-body-jjhFXTap.js → formatted-body-Rv9RDPsQ.js} +3 -3
  18. package/public/assets/{formatted-body-jjhFXTap.js.map → formatted-body-Rv9RDPsQ.js.map} +1 -1
  19. package/public/assets/{formatted-body-impl-gOLBKeyW.js → formatted-body-impl-XFU4tPeT.js} +2 -2
  20. package/public/assets/{formatted-body-impl-gOLBKeyW.js.map → formatted-body-impl-XFU4tPeT.js.map} +1 -1
  21. package/public/assets/{index-CgH1BZwq.js → index-Bhy_jnX4.js} +6 -6
  22. package/public/assets/{index-CgH1BZwq.js.map → index-Bhy_jnX4.js.map} +1 -1
  23. package/public/assets/{insights-CTW85An9.js → insights-DpXPAEwH.js} +2 -2
  24. package/public/assets/{insights-CTW85An9.js.map → insights-DpXPAEwH.js.map} +1 -1
  25. package/public/assets/{maintenance-D5PEygZz.js → maintenance-BrkqfGwx.js} +2 -2
  26. package/public/assets/{maintenance-D5PEygZz.js.map → maintenance-BrkqfGwx.js.map} +1 -1
  27. package/public/assets/{managed-agents-pkpAzZy8.js → managed-agents-CLANxOAN.js} +2 -2
  28. package/public/assets/{managed-agents-pkpAzZy8.js.map → managed-agents-CLANxOAN.js.map} +1 -1
  29. package/public/assets/{markdown-preview-impl-DMzFE-Lo.js → markdown-preview-impl-CcZFkjn-.js} +2 -2
  30. package/public/assets/{markdown-preview-impl-DMzFE-Lo.js.map → markdown-preview-impl-CcZFkjn-.js.map} +1 -1
  31. package/public/assets/{memory-BnquVYQz.js → memory-BoQw8q-i.js} +2 -2
  32. package/public/assets/{memory-BnquVYQz.js.map → memory-BoQw8q-i.js.map} +1 -1
  33. package/public/assets/{messages-DgxLlndC.js → messages-DfKNuph2.js} +2 -2
  34. package/public/assets/{messages-DgxLlndC.js.map → messages-DfKNuph2.js.map} +1 -1
  35. package/public/assets/{orchestrators-BaWxeAlK.js → orchestrators-sQh8e7-V.js} +2 -2
  36. package/public/assets/{orchestrators-BaWxeAlK.js.map → orchestrators-sQh8e7-V.js.map} +1 -1
  37. package/public/assets/{overview-Kvs-j2gg.js → overview-B0z8ZNtp.js} +2 -2
  38. package/public/assets/{overview-Kvs-j2gg.js.map → overview-B0z8ZNtp.js.map} +1 -1
  39. package/public/assets/{pairs-eDzbqcER.js → pairs-B3FhGmn-.js} +2 -2
  40. package/public/assets/{pairs-eDzbqcER.js.map → pairs-B3FhGmn-.js.map} +1 -1
  41. package/public/assets/{prompt-settings-CjcqBG_r.js → prompt-settings-CCj_s008.js} +2 -2
  42. package/public/assets/{prompt-settings-CjcqBG_r.js.map → prompt-settings-CCj_s008.js.map} +1 -1
  43. package/public/assets/{registry-C1DSnjjg.js → registry-mD_y06Ru.js} +2 -2
  44. package/public/assets/{registry-C1DSnjjg.js.map → registry-mD_y06Ru.js.map} +1 -1
  45. package/public/assets/{security-VbEe_jwm.js → security-CTSA9niL.js} +2 -2
  46. package/public/assets/{security-VbEe_jwm.js.map → security-CTSA9niL.js.map} +1 -1
  47. package/public/assets/{select-CVfwgfKl.js → select-gEQEHCIf.js} +2 -2
  48. package/public/assets/{select-CVfwgfKl.js.map → select-gEQEHCIf.js.map} +1 -1
  49. package/public/assets/{settings-ujA9hZW1.js → settings-ByAR-TYj.js} +2 -2
  50. package/public/assets/{settings-ujA9hZW1.js.map → settings-ByAR-TYj.js.map} +1 -1
  51. package/public/assets/{tasks-BtbKaa-b.js → tasks-CZyJX881.js} +2 -2
  52. package/public/assets/{tasks-BtbKaa-b.js.map → tasks-CZyJX881.js.map} +1 -1
  53. package/public/assets/{teams-CrVDQ99v.js → teams-BrL4sEL7.js} +2 -2
  54. package/public/assets/{teams-CrVDQ99v.js.map → teams-BrL4sEL7.js.map} +1 -1
  55. package/public/assets/{terminal-viewer-impl-Aays_e7-.js → terminal-viewer-impl-LjIph8ql.js} +2 -2
  56. package/public/assets/{terminal-viewer-impl-Aays_e7-.js.map → terminal-viewer-impl-LjIph8ql.js.map} +1 -1
  57. package/public/assets/types-BqeSkWrv.js.map +1 -1
  58. package/public/assets/{work-queue-DDSGZO4Z.js → work-queue-CSu5kRsw.js} +2 -2
  59. package/public/assets/{work-queue-DDSGZO4Z.js.map → work-queue-CSu5kRsw.js.map} +1 -1
  60. package/public/index.html +2 -2
  61. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  62. package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +2 -2
  63. package/runner/src/relay-mcp-proxy.ts +1 -3
  64. package/src/db/index.ts +1 -0
  65. package/src/db/task-deliverables.ts +206 -0
  66. package/src/db/task-entity.ts +4 -8
  67. package/src/mcp/index.ts +4 -4
  68. package/src/mcp/tools-workspace.ts +3 -36
  69. package/src/mcp-task-deliver-tools.ts +143 -0
  70. package/src/notification-rules.ts +1 -0
  71. package/src/notification-types.ts +2 -0
  72. package/src/prompt-resolver.ts +6 -6
  73. package/src/services/task-delivery.ts +275 -0
  74. package/src/workspace-phase.ts +5 -5
  75. package/public/assets/chat-BdbIWZpQ.js.map +0 -1
@@ -1,2 +1,2 @@
1
- import{a as e,gt as t,jn as n,rr as r,yn as i}from"./lucide-react-NXHPIMsX.js";import{p as a,t as o}from"./store-D-Frk_EC.js";import{K as s,ct as c,d as l}from"./display-aTnwsqQh.js";import{t as u}from"./badge-BCvp7LK3.js";import{t as d}from"./button-DyHjsHY3.js";import{At as f,G as p,wt as m}from"./index-CgH1BZwq.js";import{r as h,t as g}from"./card-BoqYxpVW.js";var _=r(),v={open:`bg-blue-500/10 text-blue-400 border-blue-500/20`,blocked:`bg-yellow-500/10 text-yellow-400 border-yellow-500/20`,claimed:`bg-emerald-500/10 text-emerald-400 border-emerald-500/20`,in_progress:`bg-purple-500/10 text-purple-400 border-purple-500/20`};function y(){let e=a(),n=o(e=>e.agentsById),r=o(e=>e.compose),i=o(e=>e.set),s=o(e=>e.doClaimTask),c=o(e=>e.doClaim),d=o(e=>e.openTaskEvents),h=o(e=>e.showError),g=o(e=>e.openConfirm),v=o(e=>e.doDeleteMessage),y=o(e=>e.doUpdateTaskStatus),x=m(),S=f(),C=S.filter(e=>e.claimable).length;function w(e){if(!r.from){h(`Validation`,`Select a "Claim as" agent first.`);return}e.sourceType===`task`&&e.task?s(e.task.id):e.sourceType===`message`&&e.message&&c(e.message.id)}function T(e){g(`Cancel Item`,`Cancel "${e.title||(e.sourceType===`task`?`T${e.id}`:`Message #${e.id}`)}"? This cannot be undone.`,()=>{e.sourceType===`task`&&e.task?y(e.task,`canceled`):e.sourceType===`message`&&e.message&&v(e.message.id)})}function E(e){e.task&&d(e.task)}return(0,_.jsxs)(`div`,{className:`space-y-4`,children:[(0,_.jsxs)(`div`,{className:`flex items-center justify-between flex-wrap gap-2`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(t,{className:`w-5 h-5 text-muted-foreground`}),(0,_.jsx)(`h2`,{className:`text-lg font-semibold`,children:`Work Queue`}),C>0&&(0,_.jsxs)(u,{className:`bg-orange-500/20 text-orange-400 border-orange-500/30 border`,children:[C,` claimable`]})]}),(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(`label`,{className:`text-xs text-muted-foreground shrink-0`,children:`Claim as`}),(0,_.jsxs)(`select`,{className:`rounded-md border border-input bg-background px-3 py-1.5 text-sm`,value:r.from,onChange:e=>i({compose:{...r,from:e.target.value}}),children:[(0,_.jsx)(`option`,{value:``,children:`Select agent...`}),x.map(e=>(0,_.jsx)(`option`,{value:e.id,children:l(e)},e.id))]})]})]}),(0,_.jsx)(p,{className:`h-[calc(100dvh-11rem)]`,children:(0,_.jsxs)(`div`,{className:`space-y-2 pr-2`,children:[S.length===0&&(0,_.jsx)(`div`,{className:`text-center text-muted-foreground py-16 text-sm`,children:`Work queue is empty`}),S.map(t=>(0,_.jsx)(b,{item:t,now:e,agentName:e=>n[e]?l(n[e]):e.slice(-10),onClaim:()=>w(t),onCancel:()=>T(t),onEvents:t.task?()=>E(t):void 0},t.id))]})})]})}function b({item:t,now:r,agentName:a,onClaim:o,onCancel:l,onEvents:f}){let p=c[t.severity]||c.info,m=v[t.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:t.claimable?`ring-1 ring-orange-500/30`:``,children:(0,_.jsx)(h,{className:`p-3`,children:(0,_.jsxs)(`div`,{className:`flex items-start gap-3`,children:[(0,_.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap mb-1`,children:[(0,_.jsx)(`span`,{className:`font-medium text-sm truncate`,children:t.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:t.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:t.status}),t.sourceType===`message`&&(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400`,children:`msg`})]}),t.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:t.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[t.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(i,{className:`w-3 h-3 inline mr-1`}),a(t.owner)]}),t.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:t.source}),(0,_.jsxs)(`span`,{title:String(t.updatedAt),children:[(0,_.jsx)(n,{className:`w-3 h-3 inline mr-1`}),s(r,t.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[t.claimable&&(0,_.jsxs)(d,{size:`sm`,className:`h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white`,onClick:o,children:[(0,_.jsx)(i,{className:`w-3.5 h-3.5 mr-1`}),` Claim`]}),f&&(0,_.jsx)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs`,onClick:f,children:`Events`}),(0,_.jsxs)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20`,onClick:l,children:[(0,_.jsx)(e,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
- //# sourceMappingURL=work-queue-DDSGZO4Z.js.map
1
+ import{a as e,gt as t,jn as n,rr as r,yn as i}from"./lucide-react-NXHPIMsX.js";import{p as a,t as o}from"./store-D-Frk_EC.js";import{K as s,ct as c,d as l}from"./display-aTnwsqQh.js";import{t as u}from"./badge-BCvp7LK3.js";import{t as d}from"./button-DyHjsHY3.js";import{At as f,G as p,wt as m}from"./index-Bhy_jnX4.js";import{r as h,t as g}from"./card-BoqYxpVW.js";var _=r(),v={open:`bg-blue-500/10 text-blue-400 border-blue-500/20`,blocked:`bg-yellow-500/10 text-yellow-400 border-yellow-500/20`,claimed:`bg-emerald-500/10 text-emerald-400 border-emerald-500/20`,in_progress:`bg-purple-500/10 text-purple-400 border-purple-500/20`};function y(){let e=a(),n=o(e=>e.agentsById),r=o(e=>e.compose),i=o(e=>e.set),s=o(e=>e.doClaimTask),c=o(e=>e.doClaim),d=o(e=>e.openTaskEvents),h=o(e=>e.showError),g=o(e=>e.openConfirm),v=o(e=>e.doDeleteMessage),y=o(e=>e.doUpdateTaskStatus),x=m(),S=f(),C=S.filter(e=>e.claimable).length;function w(e){if(!r.from){h(`Validation`,`Select a "Claim as" agent first.`);return}e.sourceType===`task`&&e.task?s(e.task.id):e.sourceType===`message`&&e.message&&c(e.message.id)}function T(e){g(`Cancel Item`,`Cancel "${e.title||(e.sourceType===`task`?`T${e.id}`:`Message #${e.id}`)}"? This cannot be undone.`,()=>{e.sourceType===`task`&&e.task?y(e.task,`canceled`):e.sourceType===`message`&&e.message&&v(e.message.id)})}function E(e){e.task&&d(e.task)}return(0,_.jsxs)(`div`,{className:`space-y-4`,children:[(0,_.jsxs)(`div`,{className:`flex items-center justify-between flex-wrap gap-2`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(t,{className:`w-5 h-5 text-muted-foreground`}),(0,_.jsx)(`h2`,{className:`text-lg font-semibold`,children:`Work Queue`}),C>0&&(0,_.jsxs)(u,{className:`bg-orange-500/20 text-orange-400 border-orange-500/30 border`,children:[C,` claimable`]})]}),(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(`label`,{className:`text-xs text-muted-foreground shrink-0`,children:`Claim as`}),(0,_.jsxs)(`select`,{className:`rounded-md border border-input bg-background px-3 py-1.5 text-sm`,value:r.from,onChange:e=>i({compose:{...r,from:e.target.value}}),children:[(0,_.jsx)(`option`,{value:``,children:`Select agent...`}),x.map(e=>(0,_.jsx)(`option`,{value:e.id,children:l(e)},e.id))]})]})]}),(0,_.jsx)(p,{className:`h-[calc(100dvh-11rem)]`,children:(0,_.jsxs)(`div`,{className:`space-y-2 pr-2`,children:[S.length===0&&(0,_.jsx)(`div`,{className:`text-center text-muted-foreground py-16 text-sm`,children:`Work queue is empty`}),S.map(t=>(0,_.jsx)(b,{item:t,now:e,agentName:e=>n[e]?l(n[e]):e.slice(-10),onClaim:()=>w(t),onCancel:()=>T(t),onEvents:t.task?()=>E(t):void 0},t.id))]})})]})}function b({item:t,now:r,agentName:a,onClaim:o,onCancel:l,onEvents:f}){let p=c[t.severity]||c.info,m=v[t.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:t.claimable?`ring-1 ring-orange-500/30`:``,children:(0,_.jsx)(h,{className:`p-3`,children:(0,_.jsxs)(`div`,{className:`flex items-start gap-3`,children:[(0,_.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap mb-1`,children:[(0,_.jsx)(`span`,{className:`font-medium text-sm truncate`,children:t.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:t.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:t.status}),t.sourceType===`message`&&(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400`,children:`msg`})]}),t.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:t.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[t.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(i,{className:`w-3 h-3 inline mr-1`}),a(t.owner)]}),t.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:t.source}),(0,_.jsxs)(`span`,{title:String(t.updatedAt),children:[(0,_.jsx)(n,{className:`w-3 h-3 inline mr-1`}),s(r,t.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[t.claimable&&(0,_.jsxs)(d,{size:`sm`,className:`h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white`,onClick:o,children:[(0,_.jsx)(i,{className:`w-3.5 h-3.5 mr-1`}),` Claim`]}),f&&(0,_.jsx)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs`,onClick:f,children:`Events`}),(0,_.jsxs)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20`,onClick:l,children:[(0,_.jsx)(e,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
+ //# sourceMappingURL=work-queue-CSu5kRsw.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"work-queue-DDSGZO4Z.js","names":[],"sources":["../../dashboard/src/components/views/work-queue.tsx"],"sourcesContent":["import { useRelayStore, useNow } from '@/store'\nimport { useWorkQueueItems, useComposeAgents } from '@/hooks/use-selectors'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { ListChecks, CheckCircle2, Calendar, X } from 'lucide-react'\nimport { displayName, timeAgo } from '@/lib/display'\nimport { SEVERITY_COLORS } from '@/lib/constants'\nimport type { WorkQueueItem } from '@/types'\n\nconst STATUS_COLORS: Record<string, string> = {\n open: 'bg-blue-500/10 text-blue-400 border-blue-500/20',\n blocked: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20',\n claimed: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',\n in_progress: 'bg-purple-500/10 text-purple-400 border-purple-500/20',\n}\n\nexport function WorkQueueView() {\n const now = useNow()\n const agentsById = useRelayStore((s) => s.agentsById)\n const compose = useRelayStore((s) => s.compose)\n const set = useRelayStore((s) => s.set)\n const doClaimTask = useRelayStore((s) => s.doClaimTask)\n const doClaim = useRelayStore((s) => s.doClaim)\n const openTaskEvents = useRelayStore((s) => s.openTaskEvents)\n const showError = useRelayStore((s) => s.showError)\n const openConfirm = useRelayStore((s) => s.openConfirm)\n const doDeleteMessage = useRelayStore((s) => s.doDeleteMessage)\n const doUpdateTaskStatus = useRelayStore((s) => s.doUpdateTaskStatus)\n\n const composeAgents = useComposeAgents()\n const items = useWorkQueueItems()\n\n const claimableCount = items.filter((i) => i.claimable).length\n\n function handleClaim(item: WorkQueueItem) {\n if (!compose.from) { showError('Validation', 'Select a \"Claim as\" agent first.'); return }\n if (item.sourceType === 'task' && item.task) doClaimTask(item.task.id)\n else if (item.sourceType === 'message' && item.message) doClaim(item.message.id)\n }\n\n function handleCancel(item: WorkQueueItem) {\n const label = item.title || (item.sourceType === 'task' ? `T${item.id}` : `Message #${item.id}`)\n openConfirm('Cancel Item', `Cancel \"${label}\"? This cannot be undone.`, () => {\n if (item.sourceType === 'task' && item.task) doUpdateTaskStatus(item.task, 'canceled')\n else if (item.sourceType === 'message' && item.message) doDeleteMessage(item.message.id)\n })\n }\n\n function handleEvents(item: WorkQueueItem) {\n if (item.task) openTaskEvents(item.task)\n }\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between flex-wrap gap-2\">\n <div className=\"flex items-center gap-2\">\n <ListChecks className=\"w-5 h-5 text-muted-foreground\" />\n <h2 className=\"text-lg font-semibold\">Work Queue</h2>\n {claimableCount > 0 && (\n <Badge className=\"bg-orange-500/20 text-orange-400 border-orange-500/30 border\">\n {claimableCount} claimable\n </Badge>\n )}\n </div>\n <div className=\"flex items-center gap-2\">\n <label className=\"text-xs text-muted-foreground shrink-0\">Claim as</label>\n <select\n className=\"rounded-md border border-input bg-background px-3 py-1.5 text-sm\"\n value={compose.from}\n onChange={(e) => set({ compose: { ...compose, from: e.target.value } })}\n >\n <option value=\"\">Select agent...</option>\n {composeAgents.map((a) => <option key={a.id} value={a.id}>{displayName(a)}</option>)}\n </select>\n </div>\n </div>\n\n <ScrollArea className=\"h-[calc(100dvh-11rem)]\">\n <div className=\"space-y-2 pr-2\">\n {items.length === 0 && (\n <div className=\"text-center text-muted-foreground py-16 text-sm\">Work queue is empty</div>\n )}\n {items.map((item) => (\n <WorkQueueCard\n key={item.id}\n item={item}\n now={now}\n agentName={(id) => agentsById[id] ? displayName(agentsById[id]) : id.slice(-10)}\n onClaim={() => handleClaim(item)}\n onCancel={() => handleCancel(item)}\n onEvents={item.task ? () => handleEvents(item) : undefined}\n />\n ))}\n </div>\n </ScrollArea>\n </div>\n )\n}\n\nfunction WorkQueueCard({\n item, now, agentName, onClaim, onCancel, onEvents,\n}: {\n item: WorkQueueItem\n now: number\n agentName: (id: string) => string\n onClaim: () => void\n onCancel: () => void\n onEvents?: () => void\n}) {\n const severityClass = SEVERITY_COLORS[item.severity] || SEVERITY_COLORS.info\n const statusClass = STATUS_COLORS[item.status] || 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20'\n\n return (\n <Card className={item.claimable ? 'ring-1 ring-orange-500/30' : ''}>\n <CardContent className=\"p-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap mb-1\">\n <span className=\"font-medium text-sm truncate\">{item.title}</span>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${severityClass}`}>{item.severity}</Badge>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${statusClass}`}>{item.status}</Badge>\n {item.sourceType === 'message' && (\n <Badge variant=\"outline\" className=\"text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400\">msg</Badge>\n )}\n </div>\n {item.body && (\n <p className=\"text-xs text-muted-foreground line-clamp-2 mb-2\">{item.body}</p>\n )}\n <div className=\"flex items-center gap-3 text-xs text-muted-foreground flex-wrap\">\n {item.owner && (\n <span title=\"Owner\">\n <CheckCircle2 className=\"w-3 h-3 inline mr-1\" />{agentName(item.owner)}\n </span>\n )}\n {item.source && (\n <span className=\"font-mono\">{item.source}</span>\n )}\n <span title={String(item.updatedAt)}>\n <Calendar className=\"w-3 h-3 inline mr-1\" />{timeAgo(now, item.updatedAt)}\n </span>\n </div>\n </div>\n <div className=\"flex flex-col gap-1 shrink-0\">\n {item.claimable && (\n <Button size=\"sm\" className=\"h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white\" onClick={onClaim}>\n <CheckCircle2 className=\"w-3.5 h-3.5 mr-1\" /> Claim\n </Button>\n )}\n {onEvents && (\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs\" onClick={onEvents}>\n Events\n </Button>\n )}\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20\" onClick={onCancel}>\n <X className=\"w-3.5 h-3.5 mr-1\" /> Cancel\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"work-queue-CSu5kRsw.js","names":[],"sources":["../../dashboard/src/components/views/work-queue.tsx"],"sourcesContent":["import { useRelayStore, useNow } from '@/store'\nimport { useWorkQueueItems, useComposeAgents } from '@/hooks/use-selectors'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { ListChecks, CheckCircle2, Calendar, X } from 'lucide-react'\nimport { displayName, timeAgo } from '@/lib/display'\nimport { SEVERITY_COLORS } from '@/lib/constants'\nimport type { WorkQueueItem } from '@/types'\n\nconst STATUS_COLORS: Record<string, string> = {\n open: 'bg-blue-500/10 text-blue-400 border-blue-500/20',\n blocked: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20',\n claimed: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',\n in_progress: 'bg-purple-500/10 text-purple-400 border-purple-500/20',\n}\n\nexport function WorkQueueView() {\n const now = useNow()\n const agentsById = useRelayStore((s) => s.agentsById)\n const compose = useRelayStore((s) => s.compose)\n const set = useRelayStore((s) => s.set)\n const doClaimTask = useRelayStore((s) => s.doClaimTask)\n const doClaim = useRelayStore((s) => s.doClaim)\n const openTaskEvents = useRelayStore((s) => s.openTaskEvents)\n const showError = useRelayStore((s) => s.showError)\n const openConfirm = useRelayStore((s) => s.openConfirm)\n const doDeleteMessage = useRelayStore((s) => s.doDeleteMessage)\n const doUpdateTaskStatus = useRelayStore((s) => s.doUpdateTaskStatus)\n\n const composeAgents = useComposeAgents()\n const items = useWorkQueueItems()\n\n const claimableCount = items.filter((i) => i.claimable).length\n\n function handleClaim(item: WorkQueueItem) {\n if (!compose.from) { showError('Validation', 'Select a \"Claim as\" agent first.'); return }\n if (item.sourceType === 'task' && item.task) doClaimTask(item.task.id)\n else if (item.sourceType === 'message' && item.message) doClaim(item.message.id)\n }\n\n function handleCancel(item: WorkQueueItem) {\n const label = item.title || (item.sourceType === 'task' ? `T${item.id}` : `Message #${item.id}`)\n openConfirm('Cancel Item', `Cancel \"${label}\"? This cannot be undone.`, () => {\n if (item.sourceType === 'task' && item.task) doUpdateTaskStatus(item.task, 'canceled')\n else if (item.sourceType === 'message' && item.message) doDeleteMessage(item.message.id)\n })\n }\n\n function handleEvents(item: WorkQueueItem) {\n if (item.task) openTaskEvents(item.task)\n }\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between flex-wrap gap-2\">\n <div className=\"flex items-center gap-2\">\n <ListChecks className=\"w-5 h-5 text-muted-foreground\" />\n <h2 className=\"text-lg font-semibold\">Work Queue</h2>\n {claimableCount > 0 && (\n <Badge className=\"bg-orange-500/20 text-orange-400 border-orange-500/30 border\">\n {claimableCount} claimable\n </Badge>\n )}\n </div>\n <div className=\"flex items-center gap-2\">\n <label className=\"text-xs text-muted-foreground shrink-0\">Claim as</label>\n <select\n className=\"rounded-md border border-input bg-background px-3 py-1.5 text-sm\"\n value={compose.from}\n onChange={(e) => set({ compose: { ...compose, from: e.target.value } })}\n >\n <option value=\"\">Select agent...</option>\n {composeAgents.map((a) => <option key={a.id} value={a.id}>{displayName(a)}</option>)}\n </select>\n </div>\n </div>\n\n <ScrollArea className=\"h-[calc(100dvh-11rem)]\">\n <div className=\"space-y-2 pr-2\">\n {items.length === 0 && (\n <div className=\"text-center text-muted-foreground py-16 text-sm\">Work queue is empty</div>\n )}\n {items.map((item) => (\n <WorkQueueCard\n key={item.id}\n item={item}\n now={now}\n agentName={(id) => agentsById[id] ? displayName(agentsById[id]) : id.slice(-10)}\n onClaim={() => handleClaim(item)}\n onCancel={() => handleCancel(item)}\n onEvents={item.task ? () => handleEvents(item) : undefined}\n />\n ))}\n </div>\n </ScrollArea>\n </div>\n )\n}\n\nfunction WorkQueueCard({\n item, now, agentName, onClaim, onCancel, onEvents,\n}: {\n item: WorkQueueItem\n now: number\n agentName: (id: string) => string\n onClaim: () => void\n onCancel: () => void\n onEvents?: () => void\n}) {\n const severityClass = SEVERITY_COLORS[item.severity] || SEVERITY_COLORS.info\n const statusClass = STATUS_COLORS[item.status] || 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20'\n\n return (\n <Card className={item.claimable ? 'ring-1 ring-orange-500/30' : ''}>\n <CardContent className=\"p-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap mb-1\">\n <span className=\"font-medium text-sm truncate\">{item.title}</span>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${severityClass}`}>{item.severity}</Badge>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${statusClass}`}>{item.status}</Badge>\n {item.sourceType === 'message' && (\n <Badge variant=\"outline\" className=\"text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400\">msg</Badge>\n )}\n </div>\n {item.body && (\n <p className=\"text-xs text-muted-foreground line-clamp-2 mb-2\">{item.body}</p>\n )}\n <div className=\"flex items-center gap-3 text-xs text-muted-foreground flex-wrap\">\n {item.owner && (\n <span title=\"Owner\">\n <CheckCircle2 className=\"w-3 h-3 inline mr-1\" />{agentName(item.owner)}\n </span>\n )}\n {item.source && (\n <span className=\"font-mono\">{item.source}</span>\n )}\n <span title={String(item.updatedAt)}>\n <Calendar className=\"w-3 h-3 inline mr-1\" />{timeAgo(now, item.updatedAt)}\n </span>\n </div>\n </div>\n <div className=\"flex flex-col gap-1 shrink-0\">\n {item.claimable && (\n <Button size=\"sm\" className=\"h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white\" onClick={onClaim}>\n <CheckCircle2 className=\"w-3.5 h-3.5 mr-1\" /> Claim\n </Button>\n )}\n {onEvents && (\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs\" onClick={onEvents}>\n Events\n </Button>\n )}\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20\" onClick={onCancel}>\n <X className=\"w-3.5 h-3.5 mr-1\" /> Cancel\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n"],"mappings":"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"}
package/public/index.html CHANGED
@@ -12,7 +12,7 @@
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-CgH1BZwq.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-Bhy_jnX4.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
18
  <link rel="modulepreload" crossorigin href="./assets/lucide-react-NXHPIMsX.js">
@@ -36,7 +36,7 @@
36
36
  <link rel="modulepreload" crossorigin href="./assets/agent-type-icon-BmBR-9CH.js">
37
37
  <link rel="modulepreload" crossorigin href="./assets/button-DyHjsHY3.js">
38
38
  <link rel="modulepreload" crossorigin href="./assets/copy-button-C67FUG3E.js">
39
- <link rel="modulepreload" crossorigin href="./assets/formatted-body-jjhFXTap.js">
39
+ <link rel="modulepreload" crossorigin href="./assets/formatted-body-Rv9RDPsQ.js">
40
40
  <link rel="modulepreload" crossorigin href="./assets/task-chip-CeujtXYT.js">
41
41
  <link rel="modulepreload" crossorigin href="./assets/badge-BCvp7LK3.js">
42
42
  <link rel="modulepreload" crossorigin href="./assets/input-Rc4oWv4q.js">
@@ -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.126.0",
4
+ "version": "0.127.0",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -56,8 +56,8 @@ var RUNNER_PROMPT_TEMPLATE_DEFAULTS = [
56
56
  category: "runner",
57
57
  title: "Runner Workspace Lifecycle Note",
58
58
  description: "Lifecycle note injected by the runner for isolated workspaces.",
59
- template: lines("[agent-relay] Isolated workspace: you are in a git worktree on branch {{branch}}, based on {{baseRef}} \u2014 NOT the main checkout. Other agents may work in parallel and land to {{baseRef}}, so {{baseRef}} will move under you. That is expected; don't fight it.", "Do NOT push this branch yourself \u2014 not with `git push`, not with `tl push` or any other push wrapper, and do not manually rebase or merge. A steward may be auto-rebasing this branch in the background; pushing concurrently races it and can leave the worktree mid-rebase. Just commit your work here. When the task is done, run `agent-relay workspace ready` \u2014 Relay rebases onto the latest {{baseRef}}, lands your work, and pushes for you. If the installed `agent-relay` binary is stale and says the workspace command is unknown, run the repo-local fallback: `bun src/index.ts workspace ready`.", "After `ready`, the status becomes `review_requested` \u2014 this is the NORMAL, healthy hand-off state, NOT an escalation or a stall. Relay auto-merges clean rebases roughly every 2 minutes; a steward agent is spawned (after a short delay) ONLY if it can't land deterministically, so seeing no steward means it's working, not stuck. Stop your turn after `ready` and go idle; do NOT wait or poll the steward queue. Relay wakes you with `landed-success` when your branch lands and refreshes, or `landed-failure` if landing fatally fails. On success you'll be on a fresh rebased branch whose name gains a `--N` suffix \u2014 expected, keep working there. Never `cd` into the main checkout, and never merge/push/resolve conflicts yourself \u2014 Relay and the steward own all of that. `agent-relay workspace status` anytime shows your current state and the exact next step."),
60
- defaultTemplate: lines("[agent-relay] Isolated workspace: you are in a git worktree on branch {{branch}}, based on {{baseRef}} \u2014 NOT the main checkout. Other agents may work in parallel and land to {{baseRef}}, so {{baseRef}} will move under you. That is expected; don't fight it.", "Do NOT push this branch yourself \u2014 not with `git push`, not with `tl push` or any other push wrapper, and do not manually rebase or merge. A steward may be auto-rebasing this branch in the background; pushing concurrently races it and can leave the worktree mid-rebase. Just commit your work here. When the task is done, run `agent-relay workspace ready` \u2014 Relay rebases onto the latest {{baseRef}}, lands your work, and pushes for you. If the installed `agent-relay` binary is stale and says the workspace command is unknown, run the repo-local fallback: `bun src/index.ts workspace ready`.", "After `ready`, the status becomes `review_requested` \u2014 this is the NORMAL, healthy hand-off state, NOT an escalation or a stall. Relay auto-merges clean rebases roughly every 2 minutes; a steward agent is spawned (after a short delay) ONLY if it can't land deterministically, so seeing no steward means it's working, not stuck. Stop your turn after `ready` and go idle; do NOT wait or poll the steward queue. Relay wakes you with `landed-success` when your branch lands and refreshes, or `landed-failure` if landing fatally fails. On success you'll be on a fresh rebased branch whose name gains a `--N` suffix \u2014 expected, keep working there. Never `cd` into the main checkout, and never merge/push/resolve conflicts yourself \u2014 Relay and the steward own all of that. `agent-relay workspace status` anytime shows your current state and the exact next step."),
59
+ template: lines("[agent-relay] Isolated workspace: you are in a git worktree on branch {{branch}}, based on {{baseRef}} \u2014 NOT the main checkout. Other agents may work in parallel and land to {{baseRef}}, so {{baseRef}} will move under you. That is expected; don't fight it.", "Do NOT push this branch yourself \u2014 not with `git push`, not with `tl push` or any other push wrapper, and do not manually rebase or merge. A steward may be auto-rebasing this branch in the background; pushing concurrently races it and can leave the worktree mid-rebase. Just commit your work here. When the task is done, call `relay_task_deliver` with disposition `complete`, a summary, commits, and passing `typecheck` + `test` gates. Relay records the typed delivery on the Task, then rebases onto the latest {{baseRef}}, lands your work, and pushes for you.", "After `relay_task_deliver` accepts a complete delivery, the workspace status becomes `ready` \u2014 this is the NORMAL, healthy hand-off state, NOT an escalation or a stall. Relay auto-merges clean rebases roughly every 2 minutes; a steward agent is spawned (after a short delay) ONLY if it can't land deterministically, so seeing no steward means it's working, not stuck. Stop your turn after delivery and go idle; do NOT wait or poll the steward queue. Relay wakes you with `landed-success` when your branch lands and refreshes, or `landed-failure` if landing fatally fails. On success you'll be on a fresh rebased branch whose name gains a `--N` suffix \u2014 expected, keep working there. Never `cd` into the main checkout, and never merge/push/resolve conflicts yourself \u2014 Relay and the steward own all of that. `agent-relay workspace status` anytime shows your current state and the exact next step."),
60
+ defaultTemplate: lines("[agent-relay] Isolated workspace: you are in a git worktree on branch {{branch}}, based on {{baseRef}} \u2014 NOT the main checkout. Other agents may work in parallel and land to {{baseRef}}, so {{baseRef}} will move under you. That is expected; don't fight it.", "Do NOT push this branch yourself \u2014 not with `git push`, not with `tl push` or any other push wrapper, and do not manually rebase or merge. A steward may be auto-rebasing this branch in the background; pushing concurrently races it and can leave the worktree mid-rebase. Just commit your work here. When the task is done, call `relay_task_deliver` with disposition `complete`, a summary, commits, and passing `typecheck` + `test` gates. Relay records the typed delivery on the Task, then rebases onto the latest {{baseRef}}, lands your work, and pushes for you.", "After `relay_task_deliver` accepts a complete delivery, the workspace status becomes `ready` \u2014 this is the NORMAL, healthy hand-off state, NOT an escalation or a stall. Relay auto-merges clean rebases roughly every 2 minutes; a steward agent is spawned (after a short delay) ONLY if it can't land deterministically, so seeing no steward means it's working, not stuck. Stop your turn after delivery and go idle; do NOT wait or poll the steward queue. Relay wakes you with `landed-success` when your branch lands and refreshes, or `landed-failure` if landing fatally fails. On success you'll be on a fresh rebased branch whose name gains a `--N` suffix \u2014 expected, keep working there. Never `cd` into the main checkout, and never merge/push/resolve conflicts yourself \u2014 Relay and the steward own all of that. `agent-relay workspace status` anytime shows your current state and the exact next step."),
61
61
  variables: ["branch", "baseRef"]
62
62
  },
63
63
  {
@@ -43,7 +43,7 @@ const SSE_KEEPALIVE_MS = 25_000;
43
43
  const DEFAULT_BUFFERABLE_TOOLS = new Set<string>([
44
44
  "relay_send_message",
45
45
  "relay_reply",
46
- "relay_workspace_ready",
46
+ "relay_task_deliver",
47
47
  ]);
48
48
 
49
49
  // Tools surfaced ONLY to an agent that owns a live isolated worktree. For any other agent the
@@ -51,12 +51,10 @@ const DEFAULT_BUFFERABLE_TOOLS = new Set<string>([
51
51
  // would return them). This is the context the coarse token scope can't express (#214/#215).
52
52
  const WORKTREE_ONLY_TOOLS = new Set<string>([
53
53
  "relay_workspace_status",
54
- "relay_workspace_ready",
55
54
  "relay_workspace_deps",
56
55
  "relay_workspace_list",
57
56
  "relay_workspace_claim",
58
57
  "relay_workspace_release",
59
- "relay_workspace_land",
60
58
  ]);
61
59
 
62
60
  interface ProxyContext {
package/src/db/index.ts CHANGED
@@ -10,6 +10,7 @@ export * from "./agents.ts";
10
10
  export * from "./agent-search.ts";
11
11
  export * from "./tasks.ts";
12
12
  export * from "./task-entity.ts";
13
+ export * from "./task-deliverables.ts";
13
14
  export * from "./task-memory.ts";
14
15
  export * from "./task-comment-through.ts";
15
16
  export * from "./integrations.ts";
@@ -0,0 +1,206 @@
1
+ import {
2
+ TASK_DELIVERY_DISPOSITIONS,
3
+ TASK_GATE_STATUSES,
4
+ type TaskCommitRef,
5
+ type TaskBlocker,
6
+ type TaskDeliverableEventMetadata,
7
+ type TaskDeliveryDisposition,
8
+ type TaskDeliveryGateReports,
9
+ type TaskDeliveryResult,
10
+ type TaskDeliverySelfAcceptance,
11
+ type TaskEvent,
12
+ type TaskGateStatus,
13
+ } from "../types";
14
+ import { parseJson } from "../utils";
15
+ import { getDb, ValidationError } from "./connection.ts";
16
+ import { rowToTaskEvent } from "./mappers.ts";
17
+ import { DELIVERABLE_TASK_KIND, insertTaskEvent } from "./task-core.ts";
18
+
19
+ function requireText(value: unknown, field: string, max: number): string {
20
+ if (typeof value !== "string" || !value.trim()) throw new ValidationError(`${field} required`);
21
+ const cleaned = value.trim();
22
+ if (cleaned.length > max) throw new ValidationError(`${field} must be ${max} characters or fewer`);
23
+ return cleaned;
24
+ }
25
+
26
+ function optionalText(value: unknown, field: string, max: number): string | undefined {
27
+ if (value === undefined || value === null) return undefined;
28
+ if (typeof value !== "string") throw new ValidationError(`${field} must be a string`);
29
+ const cleaned = value.trim();
30
+ if (!cleaned) return undefined;
31
+ if (cleaned.length > max) throw new ValidationError(`${field} must be ${max} characters or fewer`);
32
+ return cleaned;
33
+ }
34
+
35
+ function isObject(value: unknown): value is Record<string, unknown> {
36
+ return typeof value === "object" && value !== null && !Array.isArray(value);
37
+ }
38
+
39
+ function taskCommitRows(id: number): TaskCommitRef[] {
40
+ const row = getDb().query("SELECT kind, commits FROM tasks WHERE id = ?").get(id) as { kind: string; commits: string | null } | undefined;
41
+ if (!row) throw new ValidationError(`task ${id} not found`);
42
+ if (row.kind !== DELIVERABLE_TASK_KIND) throw new ValidationError(`task ${id} is not a deliverable task`);
43
+ return parseJson<TaskCommitRef[]>(row.commits, []);
44
+ }
45
+
46
+ function appendCommitRow(id: number, commit: TaskCommitRef): void {
47
+ const sha = requireText(commit.sha, "commit.sha", 80);
48
+ const subject = optionalText(commit.subject, "commit.subject", 1000);
49
+ const commits = taskCommitRows(id);
50
+ if (commits.some((c) => c.sha === sha)) return;
51
+ commits.push({ sha, ...(subject ? { subject } : {}) });
52
+ getDb().query("UPDATE tasks SET commits = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(commits), Date.now(), id);
53
+ }
54
+
55
+ function cleanCommitRefs(value: unknown): TaskCommitRef[] | undefined {
56
+ if (value === undefined || value === null) return undefined;
57
+ if (!Array.isArray(value)) throw new ValidationError("commits must be an array");
58
+ const commits = value.map((entry, index) => {
59
+ if (!isObject(entry)) throw new ValidationError(`commits[${index}] must be an object`);
60
+ return {
61
+ sha: requireText(entry.sha, `commits[${index}].sha`, 80),
62
+ ...(optionalText(entry.subject, `commits[${index}].subject`, 1000) ? { subject: optionalText(entry.subject, `commits[${index}].subject`, 1000)! } : {}),
63
+ };
64
+ });
65
+ return commits.length > 0 ? commits : undefined;
66
+ }
67
+
68
+ function cleanGateReport(value: unknown, field: string): TaskDeliveryGateReports["typecheck"] {
69
+ if (value === undefined || value === null) return undefined;
70
+ if (!isObject(value)) throw new ValidationError(`${field} must be an object`);
71
+ if (!TASK_GATE_STATUSES.includes(value.status as TaskGateStatus)) throw new ValidationError(`${field}.status must be one of: ${TASK_GATE_STATUSES.join(", ")}`);
72
+ const count = value.count;
73
+ if (count !== undefined && (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0)) {
74
+ throw new ValidationError(`${field}.count must be a non-negative integer`);
75
+ }
76
+ return {
77
+ status: value.status as TaskGateStatus,
78
+ ...(optionalText(value.detail, `${field}.detail`, 4000) ? { detail: optionalText(value.detail, `${field}.detail`, 4000)! } : {}),
79
+ ...(typeof count === "number" ? { count } : {}),
80
+ ...(optionalText(value.runId, `${field}.runId`, 240) ? { runId: optionalText(value.runId, `${field}.runId`, 240)! } : {}),
81
+ };
82
+ }
83
+
84
+ function cleanGateReports(value: unknown): TaskDeliveryGateReports | undefined {
85
+ if (value === undefined || value === null) return undefined;
86
+ if (!isObject(value)) throw new ValidationError("gates must be an object");
87
+ const out: TaskDeliveryGateReports = {};
88
+ const typecheck = cleanGateReport(value.typecheck, "gates.typecheck");
89
+ const test = cleanGateReport(value.test, "gates.test");
90
+ if (typecheck) out.typecheck = typecheck;
91
+ if (test) out.test = test;
92
+ if (value.other !== undefined) {
93
+ if (!Array.isArray(value.other)) throw new ValidationError("gates.other must be an array");
94
+ const other = value.other.map((entry, index) => {
95
+ if (!isObject(entry)) throw new ValidationError(`gates.other[${index}] must be an object`);
96
+ const gate = cleanGateReport(entry, `gates.other[${index}]`)!;
97
+ return { name: requireText(entry.name, `gates.other[${index}].name`, 120), ...gate };
98
+ });
99
+ if (other.length > 0) out.other = other;
100
+ }
101
+ return out.typecheck || out.test || out.other?.length ? out : undefined;
102
+ }
103
+
104
+ function cleanRisks(value: unknown): string[] | undefined {
105
+ if (value === undefined || value === null) return undefined;
106
+ if (!Array.isArray(value)) throw new ValidationError("risks must be an array");
107
+ const risks = value.map((entry, index) => requireText(entry, `risks[${index}]`, 4000));
108
+ return risks.length > 0 ? risks : undefined;
109
+ }
110
+
111
+ function cleanBlockedBy(value: unknown): TaskBlocker[] | undefined {
112
+ if (value === undefined || value === null) return undefined;
113
+ if (!Array.isArray(value)) throw new ValidationError("blockedBy must be an array");
114
+ const entries = value.map((entry, index) => {
115
+ if (!isObject(entry)) throw new ValidationError(`blockedBy[${index}] must be an object`);
116
+ const type = entry.type as TaskBlocker["type"];
117
+ if (type !== "dep" && type !== "gate" && type !== "decision" && type !== "external") {
118
+ throw new ValidationError(`blockedBy[${index}].type must be one of: dep, gate, decision, external`);
119
+ }
120
+ return {
121
+ type,
122
+ ...(optionalText(entry.ref, `blockedBy[${index}].ref`, 240) ? { ref: optionalText(entry.ref, `blockedBy[${index}].ref`, 240)! } : {}),
123
+ ...(optionalText(entry.note, `blockedBy[${index}].note`, 2000) ? { note: optionalText(entry.note, `blockedBy[${index}].note`, 2000)! } : {}),
124
+ };
125
+ });
126
+ return entries.length > 0 ? entries : undefined;
127
+ }
128
+
129
+ function cleanSelfAcceptance(value: unknown): TaskDeliverySelfAcceptance[] | undefined {
130
+ if (value === undefined || value === null) return undefined;
131
+ if (!Array.isArray(value)) throw new ValidationError("selfAcceptance must be an array");
132
+ const entries = value.map((entry, index) => {
133
+ if (!isObject(entry)) throw new ValidationError(`selfAcceptance[${index}] must be an object`);
134
+ if (entry.status !== "met" && entry.status !== "unmet") throw new ValidationError(`selfAcceptance[${index}].status must be met or unmet`);
135
+ return {
136
+ criterion: requireText(entry.criterion, `selfAcceptance[${index}].criterion`, 1000),
137
+ status: entry.status as TaskDeliverySelfAcceptance["status"],
138
+ ...(optionalText(entry.note, `selfAcceptance[${index}].note`, 4000) ? { note: optionalText(entry.note, `selfAcceptance[${index}].note`, 4000)! } : {}),
139
+ };
140
+ });
141
+ return entries.length > 0 ? entries : undefined;
142
+ }
143
+
144
+ export interface RecordTaskDeliverableInput extends TaskDeliveryResult {
145
+ actor: string;
146
+ workspaceId?: string;
147
+ idempotencyKey?: string;
148
+ land?: TaskDeliverableEventMetadata["land"];
149
+ }
150
+
151
+ export function recordTaskDeliverable(id: number, input: RecordTaskDeliverableInput): { event: TaskEvent; created: boolean } {
152
+ taskCommitRows(id);
153
+ const actor = requireText(input.actor, "actor", 200);
154
+ const idempotencyKey = optionalText(input.idempotencyKey, "idempotencyKey", 240);
155
+ if (idempotencyKey) {
156
+ const existing = getDb()
157
+ .query("SELECT * FROM task_events WHERE task_id = ? AND type = 'deliverable' AND json_extract(metadata, '$.idempotencyKey') = ? LIMIT 1")
158
+ .get(id, idempotencyKey) as any;
159
+ if (existing) return { event: rowToTaskEvent(existing), created: false };
160
+ }
161
+ if (!TASK_DELIVERY_DISPOSITIONS.includes(input.disposition as TaskDeliveryDisposition)) {
162
+ throw new ValidationError(`disposition must be one of: ${TASK_DELIVERY_DISPOSITIONS.join(", ")}`);
163
+ }
164
+ const commits = cleanCommitRefs(input.commits);
165
+ const gates = cleanGateReports(input.gates);
166
+ const reason = optionalText(input.reason, "reason", 64_000);
167
+ const blockedBy = cleanBlockedBy(input.blockedBy);
168
+ const filesChanged = input.filesChanged;
169
+ if (filesChanged !== undefined && (typeof filesChanged !== "number" || !Number.isSafeInteger(filesChanged) || filesChanged < 0)) {
170
+ throw new ValidationError("filesChanged must be a non-negative integer");
171
+ }
172
+ const now = Date.now();
173
+ const metadata: TaskDeliverableEventMetadata = {
174
+ v: 1,
175
+ disposition: input.disposition,
176
+ summary: requireText(input.summary, "summary", 1_000_000),
177
+ actor,
178
+ ...(reason ? { reason } : {}),
179
+ ...(blockedBy ? { blockedBy } : {}),
180
+ ...(commits ? { commits } : {}),
181
+ ...(gates ? { gates } : {}),
182
+ ...(typeof filesChanged === "number" ? { filesChanged } : {}),
183
+ ...(optionalText(input.rca, "rca", 64_000) ? { rca: optionalText(input.rca, "rca", 64_000)! } : {}),
184
+ ...(optionalText(input.testCoverage, "testCoverage", 64_000) ? { testCoverage: optionalText(input.testCoverage, "testCoverage", 64_000)! } : {}),
185
+ ...(cleanRisks(input.risks) ? { risks: cleanRisks(input.risks)! } : {}),
186
+ ...(cleanSelfAcceptance(input.selfAcceptance) ? { selfAcceptance: cleanSelfAcceptance(input.selfAcceptance)! } : {}),
187
+ ...(optionalText(input.workspaceId, "workspaceId", 240) ? { workspaceId: optionalText(input.workspaceId, "workspaceId", 240)! } : {}),
188
+ ...(idempotencyKey ? { idempotencyKey } : {}),
189
+ ...(input.land ? { land: input.land } : {}),
190
+ };
191
+ const event = getDb().transaction(() => {
192
+ if (commits) {
193
+ for (const commit of commits) appendCommitRow(id, commit);
194
+ }
195
+ getDb().query("UPDATE tasks SET updated_at = ?, last_seen_at = ? WHERE id = ?").run(now, now, id);
196
+ return insertTaskEvent(id, {
197
+ source: actor,
198
+ type: "deliverable",
199
+ severity: input.disposition === "failed" || input.disposition === "blocked" ? "warning" : "info",
200
+ title: `Delivery result: ${input.disposition}`,
201
+ body: metadata.summary,
202
+ metadata: metadata as unknown as Record<string, unknown>,
203
+ }, now);
204
+ })();
205
+ return { event, created: true };
206
+ }
@@ -322,14 +322,7 @@ export function setTaskStage(id: number, input: SetTaskStageInput): TaskDetail {
322
322
  return getTaskDetail(id)!;
323
323
  }
324
324
 
325
- export interface RecordGateInput {
326
- gate: string;
327
- status: TaskGateStatus;
328
- detail?: string;
329
- commitSha?: string;
330
- runId?: string;
331
- actor?: string;
332
- }
325
+ export interface RecordGateInput { gate: string; status: TaskGateStatus; detail?: string; count?: number; commitSha?: string; runId?: string; actor?: string; }
333
326
 
334
327
  // Quality-gate ledger write (#834). The append-only `task_events` IS the ledger (no second
335
328
  // system of record). Idempotent by runId: the SAME run reported twice does not double-append,
@@ -341,6 +334,7 @@ export function recordGate(id: number, input: RecordGateInput): TaskGateState {
341
334
  if (!TASK_GATE_STATUSES.includes(input.status)) {
342
335
  throw new ValidationError(`gate status must be one of: ${TASK_GATE_STATUSES.join(", ")}`);
343
336
  }
337
+ if (input.count !== undefined && (!Number.isSafeInteger(input.count) || input.count < 0)) throw new ValidationError("count must be a non-negative integer");
344
338
  const type = `${GATE_EVENT_PREFIX}${gate}`;
345
339
  const runId = optionalText(input.runId, "runId", 240);
346
340
  if (runId) {
@@ -360,6 +354,7 @@ export function recordGate(id: number, input: RecordGateInput): TaskGateState {
360
354
  gate,
361
355
  status: input.status,
362
356
  ...(input.detail ? { detail: input.detail } : {}),
357
+ ...(typeof input.count === "number" ? { count: input.count } : {}),
363
358
  ...(input.commitSha ? { commitSha: input.commitSha } : {}),
364
359
  ...(runId ? { runId } : {}),
365
360
  },
@@ -373,6 +368,7 @@ function gateStateFromEvent(row: { type: string; created_at: number; metadata: s
373
368
  gate: typeof meta.gate === "string" ? meta.gate : row.type.slice(GATE_EVENT_PREFIX.length),
374
369
  status: (meta.status as TaskGateStatus) ?? "fail",
375
370
  ...(typeof meta.detail === "string" ? { detail: meta.detail } : {}),
371
+ ...(typeof meta.count === "number" ? { count: meta.count } : {}),
376
372
  ...(typeof meta.commitSha === "string" ? { commitSha: meta.commitSha } : {}),
377
373
  ...(typeof meta.runId === "string" ? { runId: meta.runId } : {}),
378
374
  ranAt: row.created_at,
package/src/mcp/index.ts CHANGED
@@ -6,6 +6,7 @@ import { McpAuthError, McpNotFoundError } from "../mcp-errors";
6
6
  import { createActivityEvent, ValidationError } from "../db";
7
7
  import { PLAN_TOOLS, relayPlanTool } from "../mcp-plan-tools";
8
8
  import { TASK_TOOLS, relayTaskTool } from "../mcp-task-tools";
9
+ import { TASK_DELIVER_TOOLS, relayTaskDeliverTool } from "../mcp-task-deliver-tools";
9
10
  import { TASK_MEMORY_TOOLS, relayTaskMemoryTool } from "../mcp-task-memory-tools";
10
11
  import { DISPATCH_TOOLS, relayDispatchTool } from "../mcp-dispatch-tools";
11
12
  import { TASK_WEAVE_TOOLS, relayTaskWeaveTool } from "../mcp-task-weave-tools";
@@ -37,7 +38,7 @@ const TOOLS: ToolDefinition[] = [
37
38
  AGENT_ACTION_TOOL,
38
39
  ...WORKSPACE_TOOLS,
39
40
  ...MERGE_TOOLS, ...TEAM_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,
41
+ ...PLAN_TOOLS, ...TASK_TOOLS, ...TASK_DELIVER_TOOLS, ...TASK_MEMORY_TOOLS, ...DISPATCH_TOOLS, ...TASK_WEAVE_TOOLS, ...RELEASE_TOOLS, ...ISSUE_REF_TOOLS, ...ISSUE_ASSOCIATION_TOOLS, ...ISSUE_ACTIVITY_TOOLS, ...ISSUE_LIFECYCLE_TOOLS,
41
42
  ...BLACKBOARD_TOOLS,
42
43
  ...PROJECT_TOOLS,
43
44
  ...MEMORY_TOOLS,
@@ -52,7 +53,7 @@ export const RELAY_TOOL_GROUPS: Record<string, readonly string[]> = {
52
53
  core: ["relay_whoami", "relay_send_message", "relay_reply", "relay_get_message", "relay_get_thread", "relay_find_agents"],
53
54
  memory: ["relay_recall", "relay_compact_and_resume", "relay_memory_write", "relay_memory_promote", "relay_memory_retract", "relay_memory_read", "relay_memory_search"],
54
55
  artifacts: ["relay_upload_artifact", "relay_attach_artifact"],
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"],
56
+ workspace: ["relay_workspace_status", "relay_workspace_deps", "relay_workspace_list", "relay_workspace_claim", "relay_workspace_release", "relay_task_deliver", "relay_merge_pause", "relay_merge_pauses", "relay_merge_renew", "relay_merge_resume"],
56
57
  "spawn-lifecycle": ["relay_spawn_agent", "relay_spawn_targets", "relay_my_agents", "relay_shutdown_agent", "relay_agent_action", "relay_agent_status", "relay_agent_crash_report"],
57
58
  schedule: ["relay_schedule_timer", "relay_list_timers", "relay_cancel_timer"],
58
59
  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_archive", "relay_plan_delete", "relay_plan_get"],
@@ -167,13 +168,12 @@ async function callTool(auth: McpAuthContext, params: unknown): Promise<Record<s
167
168
  else if (name === "relay_agent_action") result = relayAgentAction(auth, args);
168
169
  else if (name === "relay_workspace_status") result = await relayWorkspaceStatus(auth, args);
169
170
  else if (name === "relay_workspace_list") result = relayWorkspaceList(auth, args);
170
- else if (name === "relay_workspace_ready") result = await relayWorkspaceMutation(auth, "ready", args);
171
171
  else if (name === "relay_workspace_deps") result = await relayWorkspaceMutation(auth, "deps-refresh", args);
172
172
  else if (name === "relay_workspace_claim") result = await relayWorkspaceMutation(auth, "claim", args);
173
173
  else if (name === "relay_workspace_release") result = await relayWorkspaceMutation(auth, "release-claim", args);
174
- else if (name === "relay_workspace_land") result = await relayWorkspaceMutation(auth, "merge", args);
175
174
  else if (name.startsWith("relay_merge_")) result = relayMergeTool(auth, name, args); else if (name.startsWith("relay_team_")) result = await relayTeamTool(auth, name, args);
176
175
  else if (name.startsWith("relay_plan_")) result = relayPlanTool(auth, name, args);
176
+ else if (name === "relay_task_deliver") result = await relayTaskDeliverTool(auth, name, args);
177
177
  else if (name.startsWith("relay_task_memory_")) result = relayTaskMemoryTool(auth, name, args);
178
178
  else if (name === "relay_task_dispatch") result = relayDispatchTool(auth, name, args);
179
179
  else if (name === "relay_task_comment" || name === "relay_task_close_bridge") result = await relayTaskWeaveTool(auth, name, args);
@@ -15,33 +15,19 @@ import { optionalBoolean, optionalPositiveInt, optionalString } from "./validati
15
15
  export const WORKSPACE_TOOLS: ToolDefinition[] = [
16
16
  {
17
17
  name: "relay_workspace_status",
18
- description: "Get your isolated workspace's lifecycle status (active/ready/conflict/review_requested/merged/...). With wait:true this BLOCKS until the status changes; do not use wait:true after relay_workspace_ready unless explicitly debugging. Normal branch workers mark ready, stop their turn, and are woken later by landed-success or landed-failure notifications. Defaults to your own workspace; pass workspaceId to target another.",
18
+ description: "Get your isolated workspace's lifecycle status (active/ready/conflict/review_requested/merged/...). With wait:true this BLOCKS until the status changes; do not use wait:true after relay_task_deliver unless explicitly debugging. Normal branch workers deliver the Task, stop their turn, and are woken later by landed-success or landed-failure notifications. Defaults to your own workspace; pass workspaceId to target another.",
19
19
  requiredScopes: ["agents:read", "agent:read", "agent:write"],
20
20
  alwaysLoad: true,
21
21
  inputSchema: {
22
22
  type: "object",
23
23
  properties: {
24
24
  workspaceId: { type: "string", description: "Defaults to your own isolated workspace (resolved from your identity)." },
25
- wait: { type: "boolean", description: "Block until the workspace status changes (the actionable signal), or until the timeout. Do not use this after relay_workspace_ready in the normal worker flow; stop and wait for landed-success/landed-failure instead." },
25
+ wait: { type: "boolean", description: "Block until the workspace status changes (the actionable signal), or until the timeout. Do not use this after relay_task_deliver in the normal worker flow; stop and wait for landed-success/landed-failure instead." },
26
26
  timeoutSeconds: { type: "integer", minimum: 1, maximum: 600, description: "Max seconds to wait when wait:true (default 300, max 600). Returns early on any transition." },
27
27
  },
28
28
  additionalProperties: false,
29
29
  },
30
30
  },
31
- {
32
- name: "relay_workspace_ready",
33
- description: "Signal that your branch work is finished and ready to merge back to main. This kicks off the auto-merge-back: a clean merge lands immediately; on conflict a steward agent reconciles and lands it (escalating if it can't). After this tool returns, stop your turn and go idle; Relay wakes you later with landed-success when the merge lands and your branch refreshes, or landed-failure if landing fatally fails. Defaults to your own workspace.",
34
- requiredScopes: ["agent:write"],
35
- alwaysLoad: true,
36
- inputSchema: {
37
- type: "object",
38
- properties: {
39
- workspaceId: { type: "string", description: "Defaults to your own isolated workspace." },
40
- detail: { type: "string", description: "Optional note recorded on the activity feed." },
41
- },
42
- additionalProperties: false,
43
- },
44
- },
45
31
  {
46
32
  name: "relay_workspace_deps",
47
33
  description: "Re-provision your worktree's dependencies when the shared symlinked node_modules has gone stale (e.g. typecheck reports a missing module). checkOnly:true just reports staleness without changing anything. Self-service — acts only on your own worktree.",
@@ -98,25 +84,6 @@ export const WORKSPACE_TOOLS: ToolDefinition[] = [
98
84
  additionalProperties: false,
99
85
  },
100
86
  },
101
- {
102
- name: "relay_workspace_land",
103
- description: "Dispatch a base merge for your workspace directly (lease-serialized per repo, same path the auto-merge job uses). Most branch agents should use relay_workspace_ready instead and let the relay land it; this is the explicit/steward path and requires the command:write scope.",
104
- requiredScopes: ["command:write"],
105
- inputSchema: {
106
- type: "object",
107
- properties: {
108
- workspaceId: { type: "string", description: "Defaults to your own isolated workspace." },
109
- strategy: { type: "string", enum: ["pr", "rebase-ff", "auto"], description: "Merge strategy (default auto). Falls back to the instance landing.strategy config when unset." },
110
- deleteBranch: { type: "boolean", description: "Delete the branch after a successful merge (default true)." },
111
- prTitle: { type: "string" },
112
- prBody: { type: "string" },
113
- autoMerge: { type: "string", enum: AUTO_MERGE_POLICIES, description: "Auto-merge policy for PR-strategy lands. Defaults to 'on-green' when strategy resolves to 'pr'. Ignored for rebase-ff/auto strategies." },
114
- reviewer: { type: "string", description: "GitHub login or team slug to request as a reviewer on the opened PR (pr strategy only)." },
115
- detail: { type: "string" },
116
- },
117
- additionalProperties: false,
118
- },
119
- },
120
87
  ];
121
88
 
122
89
  function isWorkspaceAdmin(auth: McpAuthContext): boolean {
@@ -219,7 +186,7 @@ export async function relayWorkspaceMutation(auth: McpAuthContext, action: Works
219
186
  if (action === "ready") {
220
187
  payload.contract = readyContract(result.workspace);
221
188
  payload.guidance = describeWorkspacePhase(result.workspace);
222
- // #1084 relay_workspace_ready is an explicit, platform-owned completion signal: the caller
189
+ // Legacy workspace-ready route/CLI path is an explicit, platform-owned completion signal: the caller
223
190
  // is declaring its work window closed. Close it, forwarding only the held final group.
224
191
  markAgentWorkFinal(result.workspace.ownerAgentId ?? callerAgentId(auth), "workspace-ready");
225
192
  }