agent-relay-server 0.117.0 → 0.118.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 (74) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +3 -3
  3. package/public/assets/{MessageBubble-CLp0Pn3c.js → MessageBubble-BdnzeQjY.js} +2 -2
  4. package/public/assets/{MessageBubble-CLp0Pn3c.js.map → MessageBubble-BdnzeQjY.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-CePSeYcB.js → PlanGraphPanel-sG8nqUHw.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-CePSeYcB.js.map → PlanGraphPanel-sG8nqUHw.js.map} +1 -1
  7. package/public/assets/{activity-BNgbdFfb.js → activity-CZpvbkAW.js} +2 -2
  8. package/public/assets/{activity-BNgbdFfb.js.map → activity-CZpvbkAW.js.map} +1 -1
  9. package/public/assets/{agent-profiles-BW3m6weI.js → agent-profiles-BfZgLPvN.js} +2 -2
  10. package/public/assets/{agent-profiles-BW3m6weI.js.map → agent-profiles-BfZgLPvN.js.map} +1 -1
  11. package/public/assets/{agents-BVNA2qZ2.js → agents-DSOCIAV6.js} +2 -2
  12. package/public/assets/{agents-BVNA2qZ2.js.map → agents-DSOCIAV6.js.map} +1 -1
  13. package/public/assets/{automation-CKID4eKu.js → automation-RkZtUqxK.js} +2 -2
  14. package/public/assets/{automation-CKID4eKu.js.map → automation-RkZtUqxK.js.map} +1 -1
  15. package/public/assets/{chat-BIACikuU.js → chat-E5MI0WxW.js} +2 -2
  16. package/public/assets/{chat-BIACikuU.js.map → chat-E5MI0WxW.js.map} +1 -1
  17. package/public/assets/{formatted-body-DbbTkUOa.js → formatted-body-BnlRQjBi.js} +3 -3
  18. package/public/assets/{formatted-body-DbbTkUOa.js.map → formatted-body-BnlRQjBi.js.map} +1 -1
  19. package/public/assets/{formatted-body-impl-C2Nj-8SA.js → formatted-body-impl-9iNv975q.js} +2 -2
  20. package/public/assets/{formatted-body-impl-C2Nj-8SA.js.map → formatted-body-impl-9iNv975q.js.map} +1 -1
  21. package/public/assets/{index-DaOyfyYS.js → index-CUugubHc.js} +7 -7
  22. package/public/assets/index-CUugubHc.js.map +1 -0
  23. package/public/assets/{insights-BOcni-pg.js → insights-D0bcKaNd.js} +2 -2
  24. package/public/assets/{insights-BOcni-pg.js.map → insights-D0bcKaNd.js.map} +1 -1
  25. package/public/assets/{maintenance-B0K_rc4C.js → maintenance-C0vzzP7r.js} +2 -2
  26. package/public/assets/{maintenance-B0K_rc4C.js.map → maintenance-C0vzzP7r.js.map} +1 -1
  27. package/public/assets/{managed-agents-7dvO-g-h.js → managed-agents-DUkT3MHx.js} +2 -2
  28. package/public/assets/{managed-agents-7dvO-g-h.js.map → managed-agents-DUkT3MHx.js.map} +1 -1
  29. package/public/assets/{markdown-preview-impl-CpDfO0_U.js → markdown-preview-impl-CDHHjZr3.js} +2 -2
  30. package/public/assets/{markdown-preview-impl-CpDfO0_U.js.map → markdown-preview-impl-CDHHjZr3.js.map} +1 -1
  31. package/public/assets/{memory-gid6VSaO.js → memory-hrJcJKdD.js} +2 -2
  32. package/public/assets/{memory-gid6VSaO.js.map → memory-hrJcJKdD.js.map} +1 -1
  33. package/public/assets/{messages-z7-i9EW8.js → messages-C3YD6xJ3.js} +2 -2
  34. package/public/assets/{messages-z7-i9EW8.js.map → messages-C3YD6xJ3.js.map} +1 -1
  35. package/public/assets/{orchestrators-BVKoUWW8.js → orchestrators-CshVnsUu.js} +2 -2
  36. package/public/assets/{orchestrators-BVKoUWW8.js.map → orchestrators-CshVnsUu.js.map} +1 -1
  37. package/public/assets/{overview-CWB2dBss.js → overview-DEtzai33.js} +2 -2
  38. package/public/assets/{overview-CWB2dBss.js.map → overview-DEtzai33.js.map} +1 -1
  39. package/public/assets/{pairs-CApyChnb.js → pairs-DFpYOOlU.js} +2 -2
  40. package/public/assets/{pairs-CApyChnb.js.map → pairs-DFpYOOlU.js.map} +1 -1
  41. package/public/assets/{prompt-settings-BTbpy4vy.js → prompt-settings-jpRG8tAv.js} +2 -2
  42. package/public/assets/{prompt-settings-BTbpy4vy.js.map → prompt-settings-jpRG8tAv.js.map} +1 -1
  43. package/public/assets/{registry-_WV9RkFk.js → registry-DFiSS4hB.js} +2 -2
  44. package/public/assets/{registry-_WV9RkFk.js.map → registry-DFiSS4hB.js.map} +1 -1
  45. package/public/assets/{security-Bn9fkaGV.js → security-D5tkecYh.js} +2 -2
  46. package/public/assets/{security-Bn9fkaGV.js.map → security-D5tkecYh.js.map} +1 -1
  47. package/public/assets/{select-BqsCdzNS.js → select-DctZnR2Q.js} +2 -2
  48. package/public/assets/{select-BqsCdzNS.js.map → select-DctZnR2Q.js.map} +1 -1
  49. package/public/assets/{settings-CnGW0mOD.js → settings-NlRG3Wvf.js} +2 -2
  50. package/public/assets/{settings-CnGW0mOD.js.map → settings-NlRG3Wvf.js.map} +1 -1
  51. package/public/assets/{tasks-BcRzqM-W.js → tasks-Q9e41WzI.js} +2 -2
  52. package/public/assets/{tasks-BcRzqM-W.js.map → tasks-Q9e41WzI.js.map} +1 -1
  53. package/public/assets/{teams-D_3xp2mK.js → teams-pyH9kHhS.js} +2 -2
  54. package/public/assets/{teams-D_3xp2mK.js.map → teams-pyH9kHhS.js.map} +1 -1
  55. package/public/assets/{terminal-viewer-impl-DOzxltAd.js → terminal-viewer-impl-RPC_OOk0.js} +2 -2
  56. package/public/assets/{terminal-viewer-impl-DOzxltAd.js.map → terminal-viewer-impl-RPC_OOk0.js.map} +1 -1
  57. package/public/assets/types-DSwhOFcJ.js.map +1 -1
  58. package/public/assets/{work-queue-D68TrWRr.js → work-queue-CWLzjgBV.js} +2 -2
  59. package/public/assets/{work-queue-D68TrWRr.js.map → work-queue-CWLzjgBV.js.map} +1 -1
  60. package/public/index.html +2 -2
  61. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  62. package/src/artifact-text.ts +36 -0
  63. package/src/auto-spawn-dispatch.ts +136 -0
  64. package/src/branch-landed.ts +127 -1
  65. package/src/db/connection.ts +5 -1
  66. package/src/land-gates.ts +8 -80
  67. package/src/notification-types.ts +9 -0
  68. package/src/routes/agents-spawn.ts +35 -6
  69. package/src/routes/commands.ts +91 -7
  70. package/src/steward.ts +38 -6
  71. package/src/task-dispatcher.ts +94 -7
  72. package/src/utils.ts +7 -38
  73. package/src/validation.ts +6 -25
  74. package/public/assets/index-DaOyfyYS.js.map +0 -1
@@ -1,2 +1,2 @@
1
- import{An as e,a as t,ht as n,nr as r,vn as i}from"./lucide-react-Dc3ZwCBi.js";import{f as a,t as o}from"./store-D2BjyanL.js";import{K as s,ct as c,d as l}from"./display-_5kFrHJh.js";import{t as u}from"./badge-BhUZ1cAb.js";import{t as d}from"./button-C-V4oij1.js";import{Ot as f,St as p,U as m}from"./index-DaOyfyYS.js";import{r as h,t as g}from"./card-Br1JLjTH.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(),t=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=p(),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)(n,{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)(m,{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(n=>(0,_.jsx)(b,{item:n,now:e,agentName:e=>t[e]?l(t[e]):e.slice(-10),onClaim:()=>w(n),onCancel:()=>T(n),onEvents:n.task?()=>E(n):void 0},n.id))]})})]})}function b({item:n,now:r,agentName:a,onClaim:o,onCancel:l,onEvents:f}){let p=c[n.severity]||c.info,m=v[n.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:n.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:n.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:n.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:n.status}),n.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`})]}),n.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:n.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[n.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(i,{className:`w-3 h-3 inline mr-1`}),a(n.owner)]}),n.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:n.source}),(0,_.jsxs)(`span`,{title:String(n.updatedAt),children:[(0,_.jsx)(e,{className:`w-3 h-3 inline mr-1`}),s(r,n.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[n.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)(t,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
- //# sourceMappingURL=work-queue-D68TrWRr.js.map
1
+ import{An as e,a as t,ht as n,nr as r,vn as i}from"./lucide-react-Dc3ZwCBi.js";import{f as a,t as o}from"./store-D2BjyanL.js";import{K as s,ct as c,d as l}from"./display-_5kFrHJh.js";import{t as u}from"./badge-BhUZ1cAb.js";import{t as d}from"./button-C-V4oij1.js";import{Ot as f,St as p,U as m}from"./index-CUugubHc.js";import{r as h,t as g}from"./card-Br1JLjTH.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(),t=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=p(),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)(n,{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)(m,{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(n=>(0,_.jsx)(b,{item:n,now:e,agentName:e=>t[e]?l(t[e]):e.slice(-10),onClaim:()=>w(n),onCancel:()=>T(n),onEvents:n.task?()=>E(n):void 0},n.id))]})})]})}function b({item:n,now:r,agentName:a,onClaim:o,onCancel:l,onEvents:f}){let p=c[n.severity]||c.info,m=v[n.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:n.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:n.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:n.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:n.status}),n.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`})]}),n.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:n.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[n.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(i,{className:`w-3 h-3 inline mr-1`}),a(n.owner)]}),n.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:n.source}),(0,_.jsxs)(`span`,{title:String(n.updatedAt),children:[(0,_.jsx)(e,{className:`w-3 h-3 inline mr-1`}),s(r,n.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[n.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)(t,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
+ //# sourceMappingURL=work-queue-CWLzjgBV.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"work-queue-D68TrWRr.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-CWLzjgBV.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-DaOyfyYS.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-CUugubHc.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-Dc3ZwCBi.js">
@@ -36,7 +36,7 @@
36
36
  <link rel="modulepreload" crossorigin href="./assets/agent-type-icon-CG2v7tF0.js">
37
37
  <link rel="modulepreload" crossorigin href="./assets/button-C-V4oij1.js">
38
38
  <link rel="modulepreload" crossorigin href="./assets/copy-button-BStzXgtd.js">
39
- <link rel="modulepreload" crossorigin href="./assets/formatted-body-DbbTkUOa.js">
39
+ <link rel="modulepreload" crossorigin href="./assets/formatted-body-BnlRQjBi.js">
40
40
  <link rel="modulepreload" crossorigin href="./assets/task-chip-kxk4eNm1.js">
41
41
  <link rel="modulepreload" crossorigin href="./assets/badge-BhUZ1cAb.js">
42
42
  <link rel="modulepreload" crossorigin href="./assets/input-CtLT7WXi.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.117.0",
4
+ "version": "0.118.0",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -0,0 +1,36 @@
1
+ import { getArtifactStorage } from "./artifact-storage";
2
+ import { bytesToStream } from "./http-body";
3
+ import { createArtifact, upsertArtifactBlob } from "./db/artifacts";
4
+ import type { ArtifactKind } from "./types";
5
+
6
+ /**
7
+ * Persist a UTF-8 text blob as a relay artifact and return its id, composing the
8
+ * same store→blob→record pipeline the MCP upload tool uses. Used by the land-gate
9
+ * pipeline (#902) to stream a failing gate's FULL output to an artifact so the
10
+ * worker/coordinator notification carries only the tail + this ref — never the
11
+ * flood of full output.
12
+ */
13
+ export async function storeTextArtifact(input: {
14
+ text: string;
15
+ filename: string;
16
+ createdBy: string;
17
+ kind?: ArtifactKind;
18
+ mediaType?: string;
19
+ metadata?: Record<string, unknown>;
20
+ }): Promise<{ id: string; size: number }> {
21
+ const bytes = new TextEncoder().encode(input.text);
22
+ const mediaType = input.mediaType ?? "text/plain; charset=utf-8";
23
+ const storage = getArtifactStorage();
24
+ const stored = await storage.store(bytesToStream(bytes), { mediaType, size: bytes.byteLength });
25
+ upsertArtifactBlob({ digest: stored.digest, storageUri: stored.storageUri, mediaType, size: stored.size });
26
+ const artifact = createArtifact({
27
+ blobDigest: stored.digest,
28
+ mediaType,
29
+ kind: input.kind ?? "document",
30
+ filename: input.filename,
31
+ size: stored.size,
32
+ createdBy: input.createdBy,
33
+ metadata: input.metadata,
34
+ });
35
+ return { id: artifact.id, size: stored.size };
36
+ }
@@ -0,0 +1,136 @@
1
+ // #901 — resolver shim + configurable per-type work-shape config for internal auto-spawns.
2
+ // PURE ADDITIONS — zero behavior change at existing call sites. The dispatch brain
3
+ // (dispatchTask) is reused verbatim; this module only drives it with the configured
4
+ // work-shape for each InternalSpawnType and the #901 degrade-never-refuse flag.
5
+ import { getConfig, setConfig } from "./config-store";
6
+ import { dispatchTask, type DispatchDecision, type DispatchPin } from "./task-dispatcher";
7
+ import {
8
+ INTERNAL_SPAWN_TYPES,
9
+ TASK_DISPATCH_COMPLEXITIES,
10
+ TASK_DISPATCH_CRITICALITIES,
11
+ TASK_DISPATCH_FOOTPRINTS,
12
+ TASK_DISPATCH_TYPES,
13
+ isRecord,
14
+ } from "agent-relay-sdk";
15
+ import type {
16
+ AutoSpawnDispatchConfig,
17
+ InternalSpawnType,
18
+ TaskDispatch,
19
+ TaskDispatchCriticality,
20
+ WorkShapeProfile,
21
+ } from "agent-relay-sdk";
22
+
23
+ export const AUTO_SPAWN_DISPATCH_NAMESPACE = "auto-spawn-dispatch";
24
+ export const AUTO_SPAWN_DISPATCH_KEY = "default";
25
+
26
+ // Re-export so callers only need this module.
27
+ export type { InternalSpawnType } from "agent-relay-sdk";
28
+
29
+ const ALL_AUTO_PROFILE: WorkShapeProfile = {
30
+ complexity: "auto",
31
+ type: "auto",
32
+ contextFootprint: "auto",
33
+ criticality: "auto",
34
+ };
35
+
36
+ export const DEFAULT_AUTO_SPAWN_DISPATCH_CONFIG: AutoSpawnDispatchConfig = {
37
+ schemaVersion: 1,
38
+ profiles: {
39
+ "landing-steward": { ...ALL_AUTO_PROFILE },
40
+ "file-size-ratchet-heal": { ...ALL_AUTO_PROFILE },
41
+ "spawn-smoke": { ...ALL_AUTO_PROFILE },
42
+ },
43
+ };
44
+
45
+ // Built-in criticality for each spawn type when the profile's criticality dimension is "auto".
46
+ // landing-steward is high because it is the critical-infra auto-land path; the degrade-never-refuse
47
+ // safety (mustResolveAvailable) is therefore always on for it unless the operator overrides.
48
+ const BUILT_IN_CRITICALITY: Record<InternalSpawnType, TaskDispatchCriticality> = {
49
+ "landing-steward": "high",
50
+ "file-size-ratchet-heal": "normal",
51
+ "spawn-smoke": "low",
52
+ };
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Config store
56
+ // ---------------------------------------------------------------------------
57
+
58
+ export function getAutoSpawnDispatchConfig(): AutoSpawnDispatchConfig {
59
+ const entry = getConfig<AutoSpawnDispatchConfig>(AUTO_SPAWN_DISPATCH_NAMESPACE, AUTO_SPAWN_DISPATCH_KEY);
60
+ return entry ? normalizeAutoSpawnDispatchConfig(entry.value) : { ...DEFAULT_AUTO_SPAWN_DISPATCH_CONFIG };
61
+ }
62
+
63
+ export function setAutoSpawnDispatchConfig(value: unknown, opts: { updatedBy?: string } = {}): AutoSpawnDispatchConfig {
64
+ const normalized = normalizeAutoSpawnDispatchConfig(value);
65
+ const current = getAutoSpawnDispatchConfig();
66
+ if (JSON.stringify(normalized) === JSON.stringify(current)) return normalized;
67
+ setConfig(AUTO_SPAWN_DISPATCH_NAMESPACE, AUTO_SPAWN_DISPATCH_KEY, normalized, opts.updatedBy);
68
+ return normalized;
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Normalizer — coerces arbitrary input into a valid AutoSpawnDispatchConfig.
73
+ // "auto" is the safe default for every dimension, so a partial config is always valid.
74
+ // ---------------------------------------------------------------------------
75
+
76
+ export function normalizeAutoSpawnDispatchConfig(value: unknown): AutoSpawnDispatchConfig {
77
+ const input = isRecord(value) ? value : {};
78
+ const rawProfiles = isRecord(input.profiles) ? input.profiles : {};
79
+ const profiles = {} as AutoSpawnDispatchConfig["profiles"];
80
+ for (const type of INTERNAL_SPAWN_TYPES) {
81
+ const raw = isRecord(rawProfiles[type]) ? rawProfiles[type] : {};
82
+ profiles[type] = normalizeWorkShapeProfile(raw);
83
+ }
84
+ return { schemaVersion: 1, profiles };
85
+ }
86
+
87
+ export function validateAutoSpawnDispatchConfig(value: unknown): AutoSpawnDispatchConfig {
88
+ return normalizeAutoSpawnDispatchConfig(value);
89
+ }
90
+
91
+ function normalizeWorkShapeProfile(raw: Record<string, unknown>): WorkShapeProfile {
92
+ return {
93
+ complexity: normalizeAutoDim(raw.complexity, TASK_DISPATCH_COMPLEXITIES),
94
+ type: normalizeAutoDim(raw.type, TASK_DISPATCH_TYPES),
95
+ contextFootprint: normalizeAutoDim(raw.contextFootprint, TASK_DISPATCH_FOOTPRINTS),
96
+ criticality: normalizeAutoDim(raw.criticality, TASK_DISPATCH_CRITICALITIES),
97
+ };
98
+ }
99
+
100
+ function normalizeAutoDim<T extends string>(value: unknown, allowed: readonly T[]): "auto" | T {
101
+ if (value === "auto") return "auto";
102
+ if (typeof value === "string" && (allowed as readonly string[]).includes(value)) return value as T;
103
+ return "auto";
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Resolver shim
108
+ // ---------------------------------------------------------------------------
109
+
110
+ function effectiveCriticality(type: InternalSpawnType, profile: WorkShapeProfile): TaskDispatchCriticality {
111
+ return profile.criticality === "auto" ? BUILT_IN_CRITICALITY[type] : profile.criticality;
112
+ }
113
+
114
+ /**
115
+ * Resolve a concrete spawn target for an internal auto-spawn. Reads the configured work-shape
116
+ * for `type`, maps each "auto" dimension → undefined (wildcard), and calls dispatchTask.
117
+ * Returns the full DispatchDecision so callers can log WHY and surface the rationale.
118
+ *
119
+ * For high-criticality types (landing-steward by default) the degrade-never-refuse flag is set,
120
+ * guaranteeing a provider is returned even when all quota pools are blocked.
121
+ */
122
+ export function resolveInternalSpawnTarget(type: InternalSpawnType, pin?: DispatchPin): DispatchDecision {
123
+ const config = getAutoSpawnDispatchConfig();
124
+ const profile = config.profiles[type];
125
+
126
+ // Map "auto" → undefined; a concrete value is passed through to constrain the dispatch match.
127
+ const shape: TaskDispatch = {
128
+ ...(profile.complexity !== "auto" ? { complexity: profile.complexity } : {}),
129
+ ...(profile.type !== "auto" ? { type: profile.type } : {}),
130
+ ...(profile.contextFootprint !== "auto" ? { contextFootprint: profile.contextFootprint } : {}),
131
+ ...(profile.criticality !== "auto" ? { criticality: profile.criticality } : {}),
132
+ };
133
+
134
+ const mustResolveAvailable = effectiveCriticality(type, profile) === "high";
135
+ return dispatchTask(shape, pin ?? {}, { mustResolveAvailable });
136
+ }
@@ -1,4 +1,4 @@
1
- import { errMessage, type BaseWorktreeSyncResult } from "agent-relay-sdk";
1
+ import { errMessage, type BaseWorktreeSyncResult, type LandGateRunResult } from "agent-relay-sdk";
2
2
  import { emitRelayEvent } from "./events";
3
3
  import { routeNotification, INFORMATIONAL_TTL_MS } from "./notification-router";
4
4
  import { registerDefaultAudienceRules, audiencePredicates } from "./notification-rules";
@@ -357,6 +357,132 @@ export function notifyBranchLandFailed(input: {
357
357
  });
358
358
  }
359
359
 
360
+ function gateOutcomeLabel(gate: LandGateRunResult): string {
361
+ if (gate.timedOut) return "timed out";
362
+ if (gate.exitCode === null) return "could not run";
363
+ return `exited ${gate.exitCode}`;
364
+ }
365
+
366
+ /**
367
+ * #902 — a REQUIRED land gate failed against the candidate merged tree, so the land was
368
+ * aborted WITHOUT advancing `main` (ref unchanged). This is a CORRECTNESS gate, orthogonal
369
+ * to merge conflicts: it NEVER routes through the steward/conflict path. Wake the worker
370
+ * (owner) AND its coordinator with the failing gate name + a bounded output TAIL + the relay
371
+ * artifact ref holding the full output, so the worker fixes the gate and re-lands cleanly.
372
+ * Durable (ttl null): the owner is parked idle after `ready` and must still learn why its
373
+ * land bounced. The full gate output lives in the artifact — the notice carries only the tail,
374
+ * so a noisy gate never floods relay.
375
+ */
376
+ export function notifyLandGateFailed(input: {
377
+ workspace: Pick<WorkspaceRecord, "id" | "repoRoot" | "branch" | "baseRef" | "ownerAgentId">;
378
+ gate: LandGateRunResult;
379
+ /** Relay artifact id holding the full gate output, when the upload succeeded. */
380
+ artifactId?: string;
381
+ }): void {
382
+ const { workspace, gate } = input;
383
+ const base = workspace.baseRef ?? "base";
384
+ const owner = workspace.ownerAgentId;
385
+ const coordinator = resolveCoordinatorAgentId(owner ?? undefined);
386
+ const fallback = stewardFallbackTarget();
387
+ const branchLabel = workspace.branch ? `\`${workspace.branch}\`` : "Your branch";
388
+ const reasonLabel = gateOutcomeLabel(gate);
389
+ const artifactLabel = input.artifactId ? ` Full output: artifact \`${input.artifactId}\`.` : "";
390
+ const tail = gate.outputTail?.trim() ? `\n\n\`\`\`\n${gate.outputTail.trim()}\n\`\`\`` : "";
391
+ const body = `🚦 ${branchLabel} did NOT land: required land gate \`${gate.name}\` ${reasonLabel} against the candidate merge into \`${base}\`. \`${base}\` was NOT advanced — fix the gate and re-land (\`agent-relay workspace ready\`).${artifactLabel}${tail}`;
392
+
393
+ const payload = {
394
+ kind: "workspace.land-gate-failed",
395
+ workspaceId: workspace.id,
396
+ repoRoot: workspace.repoRoot,
397
+ branch: workspace.branch,
398
+ base,
399
+ author: owner,
400
+ gateName: gate.name,
401
+ gateCommand: gate.command,
402
+ exitCode: gate.exitCode,
403
+ timedOut: gate.timedOut,
404
+ durationMs: gate.durationMs,
405
+ artifactId: input.artifactId,
406
+ };
407
+
408
+ emitRelayEvent({ type: "workspace.land_gate_failed", source: "server", subject: workspace.id, data: payload });
409
+
410
+ createActivityEvent({
411
+ clientId: `workspace-land-gate-failed-${workspace.id}-${Date.now()}`,
412
+ kind: "state",
413
+ title: "Land blocked by gate failure",
414
+ body: `${workspace.branch ?? workspace.id} → ${base}: required gate "${gate.name}" ${reasonLabel} — ${base} not advanced`,
415
+ meta: workspace.branch ?? workspace.id,
416
+ icon: "ti-alert-octagon",
417
+ view: "orchestrators",
418
+ metadata: { source: "server", maintenanceJobId: "workspace-land-gate", workspaceId: workspace.id, repoRoot: workspace.repoRoot, gateName: gate.name, ...(input.artifactId ? { artifactId: input.artifactId } : {}) },
419
+ });
420
+
421
+ // Worker + coordinator (spec). Fall back to the configured steward target only when no
422
+ // coordinator resolves (a teamless land) so the bounce always reaches a human/agent.
423
+ const recipients = [...new Set([owner, coordinator ?? fallback].filter((r): r is string => Boolean(r)))];
424
+ if (!recipients.length) return;
425
+ routeNotification({
426
+ type: "workspace.land_gate_failed",
427
+ scope: { author: owner, parent: coordinator ?? undefined, repoRoot: workspace.repoRoot, workspaceId: workspace.id },
428
+ recipients,
429
+ ttlMs: null,
430
+ message: {
431
+ subject: `Land blocked: gate "${gate.name}" failed`,
432
+ body,
433
+ payload: { ...payload, notification: "land-gate-failed" },
434
+ replyExpected: false,
435
+ },
436
+ });
437
+ }
438
+
439
+ /**
440
+ * #902 — OPTIONAL land gates that failed but did NOT block the land (the land succeeded and
441
+ * `main` advanced). Surface a single low-priority warning to the worker with the gate names
442
+ * + the artifact ref holding the combined output, so the warn signal isn't lost but doesn't
443
+ * bounce a successful land. Informational TTL: a stale warning shouldn't wake a late joiner.
444
+ */
445
+ export function notifyLandGateWarnings(input: {
446
+ workspace: Pick<WorkspaceRecord, "id" | "repoRoot" | "branch" | "baseRef" | "ownerAgentId">;
447
+ warnings: LandGateRunResult[];
448
+ artifactId?: string;
449
+ }): void {
450
+ const { workspace, warnings } = input;
451
+ if (warnings.length === 0) return;
452
+ const base = workspace.baseRef ?? "base";
453
+ const owner = workspace.ownerAgentId;
454
+ const branchLabel = workspace.branch ? `\`${workspace.branch}\`` : "Your branch";
455
+ const names = warnings.map((w) => `\`${w.name}\` (${gateOutcomeLabel(w)})`).join(", ");
456
+ const artifactLabel = input.artifactId ? ` Full output: artifact \`${input.artifactId}\`.` : "";
457
+
458
+ const payload = {
459
+ kind: "workspace.land-gate-warning",
460
+ workspaceId: workspace.id,
461
+ repoRoot: workspace.repoRoot,
462
+ branch: workspace.branch,
463
+ base,
464
+ author: owner,
465
+ gateNames: warnings.map((w) => w.name),
466
+ artifactId: input.artifactId,
467
+ };
468
+
469
+ emitRelayEvent({ type: "workspace.land_gate_warning", source: "server", subject: workspace.id, data: payload });
470
+
471
+ if (!owner) return;
472
+ routeNotification({
473
+ type: "workspace.land_gate_warning",
474
+ scope: { author: owner, repoRoot: workspace.repoRoot, workspaceId: workspace.id },
475
+ recipients: [owner],
476
+ ttlMs: INFORMATIONAL_TTL_MS,
477
+ message: {
478
+ subject: `Land gate warning${warnings.length > 1 ? "s" : ""} (landed anyway)`,
479
+ body: `⚠️ ${branchLabel} landed to \`${base}\`, but optional land gate${warnings.length > 1 ? "s" : ""} failed: ${names}. Not blocking — review when convenient.${artifactLabel}`,
480
+ payload: { ...payload, notification: "land-gate-warning" },
481
+ replyExpected: false,
482
+ },
483
+ });
484
+ }
485
+
360
486
  /**
361
487
  * Steward-handoff awareness wake (#641/#642). When the relay can't auto-merge a branch and
362
488
  * hands the workspace to a steward, the two ends used to be mutually blind: the branch agent
@@ -68,5 +68,9 @@ export function runDbMaintenance(options: { vacuum?: boolean } = {}): {
68
68
  }
69
69
 
70
70
  // Shared error types thrown across query domains and unwrapped at call sites.
71
- export class ValidationError extends Error {}
71
+ // `ValidationError` now lives in the SDK (agent-relay-sdk) as the single canonical
72
+ // home shared with the orchestrator package; re-exported here so the ~120 server
73
+ // call sites (and #892's `toThrow(ValidationError)` tests) stay byte-unchanged and
74
+ // keep the same class identity.
75
+ export { ValidationError } from "agent-relay-sdk";
72
76
  export class ClaimError extends Error {}
package/src/land-gates.ts CHANGED
@@ -1,80 +1,8 @@
1
- import { isAbsolute, relative, resolve } from "node:path";
2
- import { readFileSync } from "node:fs";
3
- import { isRecord } from "agent-relay-sdk";
4
- import type { LandGate } from "agent-relay-sdk";
5
- import { ValidationError } from "./db/connection.ts";
6
- import { cleanString } from "./validation.ts";
7
- import { parseJson, isPathWithinBase } from "./utils.ts";
8
-
9
- const LAND_GATES_FILE = ".agent-relay/land-gates.json";
10
-
11
- export function cleanLandGate(value: unknown, index: number, worktreePath?: string): LandGate {
12
- if (!isRecord(value)) throw new ValidationError(`gates[${index}] must be an object`);
13
- const name = cleanString(value.name, `gates[${index}].name`, { required: true, max: 120 });
14
- if (!name) throw new ValidationError(`gates[${index}].name required`);
15
- const command = cleanString(value.command, `gates[${index}].command`, { required: true, max: 4096 });
16
- if (!command) throw new ValidationError(`gates[${index}].command required`);
17
-
18
- let cwd: string | undefined;
19
- if (value.cwd !== undefined && value.cwd !== null) {
20
- const rawCwd = cleanString(value.cwd, `gates[${index}].cwd`, { max: 1000 });
21
- if (rawCwd) {
22
- if (worktreePath) {
23
- const resolved = resolve(worktreePath, rawCwd);
24
- if (!isPathWithinBase(resolved, worktreePath)) {
25
- throw new ValidationError(`gates[${index}].cwd must be within the worktree`);
26
- }
27
- cwd = relative(worktreePath, resolved) || ".";
28
- } else {
29
- if (isAbsolute(rawCwd)) throw new ValidationError(`gates[${index}].cwd must be a relative path when no worktreePath is provided`);
30
- cwd = rawCwd;
31
- }
32
- }
33
- }
34
-
35
- if (value.timeoutMs !== undefined && value.timeoutMs !== null) {
36
- if (typeof value.timeoutMs !== "number" || !Number.isSafeInteger(value.timeoutMs) || value.timeoutMs <= 0) {
37
- throw new ValidationError(`gates[${index}].timeoutMs must be a positive integer`);
38
- }
39
- }
40
- if (value.optional !== undefined && value.optional !== null) {
41
- if (typeof value.optional !== "boolean") {
42
- throw new ValidationError(`gates[${index}].optional must be a boolean`);
43
- }
44
- }
45
-
46
- return {
47
- name,
48
- command,
49
- ...(cwd !== undefined ? { cwd } : {}),
50
- ...(typeof value.timeoutMs === "number" ? { timeoutMs: value.timeoutMs as number } : {}),
51
- ...(typeof value.optional === "boolean" ? { optional: value.optional as boolean } : {}),
52
- };
53
- }
54
-
55
- /** Parse + validate a raw `.agent-relay/land-gates.json` object. */
56
- export function cleanLandGatesConfig(value: unknown, worktreePath?: string): LandGate[] {
57
- if (!isRecord(value)) throw new ValidationError("land-gates config must be an object");
58
- const { gates } = value;
59
- if (!Array.isArray(gates)) throw new ValidationError("gates must be an array");
60
- if (gates.length > 50) throw new ValidationError("gates: at most 50 gates allowed");
61
- return gates.map((g, i) => cleanLandGate(g, i, worktreePath));
62
- }
63
-
64
- /**
65
- * Read + validate `.agent-relay/land-gates.json` from a worktree path.
66
- * Missing file → empty list (opt-in, zero regression).
67
- */
68
- export function loadRepoLandGates(worktreePath: string): LandGate[] {
69
- const filePath = resolve(worktreePath, LAND_GATES_FILE);
70
- let raw: string;
71
- try {
72
- raw = readFileSync(filePath, "utf8");
73
- } catch (err: unknown) {
74
- if ((err as NodeJS.ErrnoException).code === "ENOENT") return [];
75
- throw err;
76
- }
77
- const parsed = parseJson<unknown>(raw, null);
78
- if (parsed === null) throw new ValidationError(`${LAND_GATES_FILE}: invalid JSON`);
79
- return cleanLandGatesConfig(parsed, worktreePath);
80
- }
1
+ // The land-gates config loader (#892) now lives in the SDK so the orchestrator
2
+ // package which advances `refs/heads/main` and therefore runs the gates — can
3
+ // import the SAME implementation across the package boundary (it can only depend
4
+ // on agent-relay-sdk, not this server package). Re-exported here so #892's
5
+ // importers and co-located tests stay byte-unchanged. The canonical home is
6
+ // `agent-relay-sdk/land-gates` (a non-barrel subpath: it reads from disk, so it
7
+ // stays out of browser bundles).
8
+ export { cleanLandGate, cleanLandGatesConfig, loadRepoLandGates } from "agent-relay-sdk/land-gates";
@@ -40,6 +40,8 @@ export type NotificationType =
40
40
  | "workspace.steward_reassigned"
41
41
  | "workspace.stranded_escalation"
42
42
  | "workspace.base_checkout_strand"
43
+ | "workspace.land_gate_failed"
44
+ | "workspace.land_gate_warning"
43
45
  | "workspace.auto_abandoned"
44
46
  | "workspace.review_request"
45
47
  | "workspace.review_changes_requested"
@@ -136,6 +138,13 @@ export const DEFAULT_TTL_MS: Record<NotificationType, number | null> = {
136
138
  "workspace.steward_reassigned": null,
137
139
  "workspace.stranded_escalation": null,
138
140
  "workspace.base_checkout_strand": null,
141
+ // #902 — a required land gate blocked a land; the worker is parked idle after `ready` and
142
+ // must learn why its land bounced, so this is durable (about the recipient's own work).
143
+ "workspace.land_gate_failed": null,
144
+ // Optional-gate warning on an already-successful land — only useful while fresh, so it
145
+ // expires like other bystander/informational notices (= INFORMATIONAL_TTL_MS, inlined to
146
+ // avoid referencing that const before its declaration below).
147
+ "workspace.land_gate_warning": 30 * MINUTES,
139
148
  "workspace.auto_abandoned": null,
140
149
  "workspace.review_request": null,
141
150
  "workspace.review_changes_requested": null,
@@ -1,5 +1,5 @@
1
1
  // Auto-split from routes.ts (#299). Domain: agents-spawn.
2
- import { APPROVAL_MODES, AUTO_MERGE_POLICIES, SPAWN_PROVIDERS, VALID_AGENT_LIFECYCLES, VALID_WORKSPACE_MODES, isRecord } from "agent-relay-sdk";
2
+ import { APPROVAL_MODES, AUTO_MERGE_POLICIES, INTERNAL_SPAWN_TYPES, SPAWN_PROVIDERS, VALID_AGENT_LIFECYCLES, VALID_WORKSPACE_MODES, isRecord } from "agent-relay-sdk";
3
3
  import { McpAuthError, McpNotFoundError } from "../mcp-errors";
4
4
  import { VALID_AGENT_STATUSES, error, json, parseBody, type Handler } from "./_shared";
5
5
  import { ValidationError, listAgents } from "../db";
@@ -15,6 +15,7 @@ import { authContextFromRequest } from "../services/auth-context";
15
15
  import { listHostDirectories } from "../agent-spawn";
16
16
  import { rankRouteCandidates, type RouteAdvisorInput } from "../context-router";
17
17
  import { quotaStateFromUnknown } from "../quota";
18
+ import { resolveInternalSpawnTarget, type InternalSpawnType } from "../auto-spawn-dispatch";
18
19
  import { type AgentKind, type AgentLifecycle, type RegisterAgentInput, type SpawnApprovalMode, type SpawnProvider, type WorkspaceAutoMergePolicy, type WorkspaceLandingPolicy, type WorkspaceMode } from "../types";
19
20
  import { type ProviderEffort } from "agent-relay-sdk/provider-catalog";
20
21
 
@@ -118,8 +119,36 @@ export const postAgentSpawn: Handler = async (req) => {
118
119
  if (!parsed.ok) return error(parsed.error, parsed.status);
119
120
  try {
120
121
  if (!isRecord(parsed.body)) return error("provider required");
121
- const provider = optionalEnum(parsed.body.provider, "provider", SPAWN_PROVIDERS) as SpawnProvider | undefined;
122
- if (!provider) return error("provider required");
122
+
123
+ // #901 server-side internalSpawnType: when present, the resolver picks provider/model/effort
124
+ // from routing policy + quota bands so no internal caller needs to hard-code a raw model alias.
125
+ // Explicit caller model/effort are forwarded as pins so they still win over the resolver.
126
+ // Spawns that omit internalSpawnType are byte-identical to before (normal user/coordinator spawns).
127
+ const internalSpawnType = cleanString(parsed.body.internalSpawnType, "internalSpawnType", { max: 80 });
128
+ let resolvedProvider = optionalEnum(parsed.body.provider, "provider", SPAWN_PROVIDERS) as SpawnProvider | undefined;
129
+ let resolvedModel = cleanString(parsed.body.model, "model", { max: 120 });
130
+ let resolvedEffort = cleanString(parsed.body.effort, "effort", { max: 120 }) as ProviderEffort | undefined;
131
+
132
+ if (internalSpawnType) {
133
+ if (!(INTERNAL_SPAWN_TYPES as readonly string[]).includes(internalSpawnType)) {
134
+ return error(`internalSpawnType must be one of: ${INTERNAL_SPAWN_TYPES.join(", ")}`);
135
+ }
136
+ // Fold explicit caller pins into the resolver so "explicit pin wins" applies here too —
137
+ // a provider:"codex" pin with internalSpawnType (no model) must not assemble codex +
138
+ // the resolver's claude-specific model.
139
+ const callerPin = {
140
+ ...(resolvedProvider ? { provider: resolvedProvider } : {}),
141
+ ...(resolvedModel ? { model: resolvedModel } : {}),
142
+ ...(resolvedEffort ? { effort: resolvedEffort } : {}),
143
+ };
144
+ const decision = resolveInternalSpawnTarget(internalSpawnType as InternalSpawnType, callerPin);
145
+ resolvedProvider = decision.target.provider;
146
+ resolvedModel = decision.target.model;
147
+ resolvedEffort = decision.target.effort as ProviderEffort | undefined;
148
+ }
149
+
150
+ if (!resolvedProvider) return error("provider required");
151
+
123
152
  // Thin transport: parse wire → AuthContext → spawnAgent service → serialize. The gate
124
153
  // (command:spawn scope, #221 no-grandchild + quota, #323 resource), caller-context
125
154
  // inheritance (#328/#331/#324), profile validation, lineage stamping, host selection, and
@@ -127,11 +156,11 @@ export const postAgentSpawn: Handler = async (req) => {
127
156
  // (epic #342, #349). The dashboard authenticates with an admin `*` token, so the tightened
128
157
  // command:spawn requirement is transparent to it.
129
158
  const input: SpawnAgentInput = {
130
- provider,
159
+ provider: resolvedProvider,
131
160
  orchestratorId: cleanString(parsed.body.orchestratorId, "orchestratorId", { max: 200 }),
132
161
  cwd: cleanString(parsed.body.cwd, "cwd", { max: 500 }),
133
- model: cleanString(parsed.body.model, "model", { max: 120 }),
134
- effort: cleanString(parsed.body.effort, "effort", { max: 120 }) as ProviderEffort | undefined,
162
+ model: resolvedModel,
163
+ effort: resolvedEffort,
135
164
  approvalMode: optionalEnum(parsed.body.approvalMode, "approvalMode", APPROVAL_MODES) as SpawnApprovalMode | undefined,
136
165
  workspaceMode: optionalEnum(parsed.body.workspaceMode, "workspaceMode", VALID_WORKSPACE_MODES) as WorkspaceMode | undefined,
137
166
  lifecycle: optionalEnum(parsed.body.lifecycle, "lifecycle", VALID_AGENT_LIFECYCLES) as AgentLifecycle | undefined,