agent-relay-server 0.99.1 → 0.100.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 (89) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +5 -2
  3. package/public/assets/{MessageBubble-DiorZXyB.js → MessageBubble-DZ3cUoHO.js} +2 -2
  4. package/public/assets/{MessageBubble-DiorZXyB.js.map → MessageBubble-DZ3cUoHO.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-DY1p8v4p.js → PlanGraphPanel-B6G_RSZ3.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-DY1p8v4p.js.map → PlanGraphPanel-B6G_RSZ3.js.map} +1 -1
  7. package/public/assets/{activity-rIxqUtFC.js → activity-BefDMYiE.js} +2 -2
  8. package/public/assets/{activity-rIxqUtFC.js.map → activity-BefDMYiE.js.map} +1 -1
  9. package/public/assets/{agent-profiles-Cu-VkJm4.js → agent-profiles-D5VW943m.js} +2 -2
  10. package/public/assets/agent-profiles-D5VW943m.js.map +1 -0
  11. package/public/assets/agents-CtsNY05B.js +2 -0
  12. package/public/assets/agents-CtsNY05B.js.map +1 -0
  13. package/public/assets/{automation-B0cyCT5l.js → automation-B5TFhCGs.js} +2 -2
  14. package/public/assets/{automation-B0cyCT5l.js.map → automation-B5TFhCGs.js.map} +1 -1
  15. package/public/assets/branch-state-badge-BLF9LPtu.js +2 -0
  16. package/public/assets/branch-state-badge-BLF9LPtu.js.map +1 -0
  17. package/public/assets/chat-CoZcF6dA.js +2 -0
  18. package/public/assets/chat-CoZcF6dA.js.map +1 -0
  19. package/public/assets/{formatted-body-impl-ZwgTH3jh.js → formatted-body-impl-Zk5WjWS7.js} +2 -2
  20. package/public/assets/{formatted-body-impl-ZwgTH3jh.js.map → formatted-body-impl-Zk5WjWS7.js.map} +1 -1
  21. package/public/assets/{index-GoC6YrKn.js → index-B4cTW4A9.js} +5 -5
  22. package/public/assets/index-B4cTW4A9.js.map +1 -0
  23. package/public/assets/{insights-C3mlXZdd.js → insights-C8iC-IB1.js} +2 -2
  24. package/public/assets/{insights-C3mlXZdd.js.map → insights-C8iC-IB1.js.map} +1 -1
  25. package/public/assets/{maintenance-DL1lnqmG.js → maintenance-pl6U2a_y.js} +2 -2
  26. package/public/assets/{maintenance-DL1lnqmG.js.map → maintenance-pl6U2a_y.js.map} +1 -1
  27. package/public/assets/{managed-agents-7A6WH4Rn.js → managed-agents-1CynkXEy.js} +2 -2
  28. package/public/assets/{managed-agents-7A6WH4Rn.js.map → managed-agents-1CynkXEy.js.map} +1 -1
  29. package/public/assets/{markdown-preview-impl-THat8xv0.js → markdown-preview-impl-Dj6MExTR.js} +2 -2
  30. package/public/assets/{markdown-preview-impl-THat8xv0.js.map → markdown-preview-impl-Dj6MExTR.js.map} +1 -1
  31. package/public/assets/{memory-C349UJe9.js → memory-DyWT5mwx.js} +2 -2
  32. package/public/assets/{memory-C349UJe9.js.map → memory-DyWT5mwx.js.map} +1 -1
  33. package/public/assets/{messages-DXoLFj2P.js → messages-LqXFX2yc.js} +2 -2
  34. package/public/assets/{messages-DXoLFj2P.js.map → messages-LqXFX2yc.js.map} +1 -1
  35. package/public/assets/{orchestrators-CTXu4jAn.js → orchestrators-xdpBJOt1.js} +2 -2
  36. package/public/assets/{orchestrators-CTXu4jAn.js.map → orchestrators-xdpBJOt1.js.map} +1 -1
  37. package/public/assets/{overview-dWiN6TyN.js → overview-BDetOVf7.js} +2 -2
  38. package/public/assets/{overview-dWiN6TyN.js.map → overview-BDetOVf7.js.map} +1 -1
  39. package/public/assets/{pairs-KIXektDv.js → pairs-BxVd-YVM.js} +2 -2
  40. package/public/assets/{pairs-KIXektDv.js.map → pairs-BxVd-YVM.js.map} +1 -1
  41. package/public/assets/{registry-C-Y41yGB.js → registry-BeoPmyhz.js} +2 -2
  42. package/public/assets/{registry-C-Y41yGB.js.map → registry-BeoPmyhz.js.map} +1 -1
  43. package/public/assets/{security-BUymkwuj.js → security-DKDgxf96.js} +2 -2
  44. package/public/assets/{security-BUymkwuj.js.map → security-DKDgxf96.js.map} +1 -1
  45. package/public/assets/{select-DV25WRP5.js → select-DpRN9C_1.js} +2 -2
  46. package/public/assets/{select-DV25WRP5.js.map → select-DpRN9C_1.js.map} +1 -1
  47. package/public/assets/{settings-C1En58bx.js → settings-D_mLzA5O.js} +2 -2
  48. package/public/assets/{settings-C1En58bx.js.map → settings-D_mLzA5O.js.map} +1 -1
  49. package/public/assets/{tasks-BkpnKA63.js → tasks-D5OVfd-K.js} +2 -2
  50. package/public/assets/{tasks-BkpnKA63.js.map → tasks-D5OVfd-K.js.map} +1 -1
  51. package/public/assets/{teams-D-UYp7yH.js → teams-CFJM0XlG.js} +2 -2
  52. package/public/assets/{teams-D-UYp7yH.js.map → teams-CFJM0XlG.js.map} +1 -1
  53. package/public/assets/{terminal-viewer-impl-Bif-tkvD.js → terminal-viewer-impl-B-2tqoEB.js} +2 -2
  54. package/public/assets/{terminal-viewer-impl-Bif-tkvD.js.map → terminal-viewer-impl-B-2tqoEB.js.map} +1 -1
  55. package/public/assets/{user-avatar--czLv_AJ.js → user-avatar-BGXTBaME.js} +2 -2
  56. package/public/assets/{user-avatar--czLv_AJ.js.map → user-avatar-BGXTBaME.js.map} +1 -1
  57. package/public/assets/{work-queue-DN4oTe7w.js → work-queue-DMwtDbBg.js} +2 -2
  58. package/public/assets/{work-queue-DN4oTe7w.js.map → work-queue-DMwtDbBg.js.map} +1 -1
  59. package/public/index.html +1 -1
  60. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  61. package/runner/plugins/claude/hooks/kill-guard.sh +300 -0
  62. package/runner/plugins/claude/hooks/read-only-bash-guard.sh +38 -140
  63. package/runner/src/adapter.ts +2 -2
  64. package/src/agent-death-diagnosis.ts +12 -3
  65. package/src/bus.ts +6 -45
  66. package/src/config-store.ts +13 -9
  67. package/src/db/provider-quota-burn.ts +2 -1
  68. package/src/issue-tracker/adapter.ts +11 -1
  69. package/src/issue-tracker/github.ts +83 -2
  70. package/src/issue-tracker/index.ts +1 -0
  71. package/src/lifecycle-manager.ts +9 -22
  72. package/src/provider-state.ts +9 -18
  73. package/src/provider-timeline-event.ts +70 -0
  74. package/src/quota-advisory.ts +4 -7
  75. package/src/routes/agent-sessions.ts +4 -2
  76. package/src/services/issue-lifecycle.ts +54 -8
  77. package/src/services/provider-state-audit.ts +3 -3
  78. package/src/services/rate-limit-resume.ts +6 -4
  79. package/src/services/register-agent.ts +2 -1
  80. package/src/spawn-targets.ts +1 -0
  81. package/src/sse.ts +5 -4
  82. package/public/assets/agent-profiles-Cu-VkJm4.js.map +0 -1
  83. package/public/assets/agents-dRHOBDUe.js +0 -2
  84. package/public/assets/agents-dRHOBDUe.js.map +0 -1
  85. package/public/assets/branch-state-badge-CNko2_BL.js +0 -2
  86. package/public/assets/branch-state-badge-CNko2_BL.js.map +0 -1
  87. package/public/assets/chat-jit-e6S_.js +0 -2
  88. package/public/assets/chat-jit-e6S_.js.map +0 -1
  89. package/public/assets/index-GoC6YrKn.js.map +0 -1
@@ -1,2 +1,2 @@
1
- import{r as e}from"./chunk-CilyBKbf.js";import{Qn as t,Xn as n,dr as r,er as i}from"./lucide-react-BYZuAhQs.js";import{_ as a,rt as o}from"./display-DRiGi1zj.js";import{D as s}from"./index-GoC6YrKn.js";var c=e(r(),1),l=t(),u={id:o};function d({user:e,address:t,distinct:r=!1,className:o}){let d=e?.avatarUrl??(e?.avatarArtifactId?`/artifacts/${encodeURIComponent(e.avatarArtifactId)}/content`:null),[f,p]=(0,c.useState)(null);(0,c.useEffect)(()=>{if(!d){p(null);return}let e=!1,t=null;return(async()=>{try{let n=await i(d);if(e)return;t=URL.createObjectURL(n),p(t)}catch{e||p(null)}})(),()=>{e=!0,t&&URL.revokeObjectURL(t)}},[d]);let m=e?.displayName||e?.handle||`User`;if(f)return(0,l.jsx)(`img`,{src:f,alt:m,title:m,className:n(`w-7 h-7 rounded-md object-cover`,o)});if(r){let e=(m.trim()[0]||`U`).toUpperCase();return(0,l.jsx)(`span`,{title:m,className:n(`inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10`,a(t),o),children:e})}return(0,l.jsx)(s,{agent:u,className:o})}export{d as t};
2
- //# sourceMappingURL=user-avatar--czLv_AJ.js.map
1
+ import{r as e}from"./chunk-CilyBKbf.js";import{Qn as t,Xn as n,dr as r,er as i}from"./lucide-react-BYZuAhQs.js";import{_ as a,rt as o}from"./display-DRiGi1zj.js";import{D as s}from"./index-B4cTW4A9.js";var c=e(r(),1),l=t(),u={id:o};function d({user:e,address:t,distinct:r=!1,className:o}){let d=e?.avatarUrl??(e?.avatarArtifactId?`/artifacts/${encodeURIComponent(e.avatarArtifactId)}/content`:null),[f,p]=(0,c.useState)(null);(0,c.useEffect)(()=>{if(!d){p(null);return}let e=!1,t=null;return(async()=>{try{let n=await i(d);if(e)return;t=URL.createObjectURL(n),p(t)}catch{e||p(null)}})(),()=>{e=!0,t&&URL.revokeObjectURL(t)}},[d]);let m=e?.displayName||e?.handle||`User`;if(f)return(0,l.jsx)(`img`,{src:f,alt:m,title:m,className:n(`w-7 h-7 rounded-md object-cover`,o)});if(r){let e=(m.trim()[0]||`U`).toUpperCase();return(0,l.jsx)(`span`,{title:m,className:n(`inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10`,a(t),o),children:e})}return(0,l.jsx)(s,{agent:u,className:o})}export{d as t};
2
+ //# sourceMappingURL=user-avatar-BGXTBaME.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"user-avatar--czLv_AJ.js","names":[],"sources":["../../dashboard/src/components/shared/user-avatar.tsx"],"sourcesContent":["import { useEffect, useState } from 'react'\nimport { AgentTypeIcon } from '@/components/shared/agent-type-icon'\nimport { apiBlob } from '@/lib/api'\nimport { humanAccentClass } from '@/lib/display'\nimport { HUMAN_AGENT_ID } from '@/lib/constants'\nimport { cn } from '@/lib/utils'\nimport type { Agent, User } from '@/types'\n\n// Minimal stub so AgentTypeIcon resolves the generic 'user' face when no avatar exists.\nconst HUMAN_AGENT_STUB = { id: HUMAN_AGENT_ID } as unknown as Agent\n\n// A human's face (#390). Renders, in order of preference: the user's content-addressed avatar\n// image (the FK lands fully in #630 — until then `avatarArtifactId` is null and this path is\n// dormant), then a colored initial when we need to distinguish multiple operators, then the\n// generic operator icon. Degrades gracefully at every step so a single seeded user looks\n// exactly as before.\nexport function UserAvatar({\n user,\n address,\n distinct = false,\n className,\n}: {\n user: User | undefined\n /** The human address (`user` / `user:<id>`) — drives the deterministic accent color. */\n address?: string\n /** When true (multiple humans in view), fall back to a colored initial instead of the icon. */\n distinct?: boolean\n className?: string\n}) {\n const avatarUrl = user?.avatarUrl ?? (user?.avatarArtifactId ? `/artifacts/${encodeURIComponent(user.avatarArtifactId)}/content` : null)\n const [objectUrl, setObjectUrl] = useState<string | null>(null)\n\n useEffect(() => {\n if (!avatarUrl) { setObjectUrl(null); return }\n let cancelled = false\n let url: string | null = null\n void (async () => {\n try {\n const blob = await apiBlob(avatarUrl)\n if (cancelled) return\n url = URL.createObjectURL(blob)\n setObjectUrl(url)\n } catch {\n if (!cancelled) setObjectUrl(null)\n }\n })()\n return () => { cancelled = true; if (url) URL.revokeObjectURL(url) }\n }, [avatarUrl])\n\n const title = user?.displayName || user?.handle || 'User'\n\n if (objectUrl) {\n return <img src={objectUrl} alt={title} title={title} className={cn('w-7 h-7 rounded-md object-cover', className)} />\n }\n if (distinct) {\n const initial = (title.trim()[0] || 'U').toUpperCase()\n return (\n <span\n title={title}\n className={cn('inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10', humanAccentClass(address), className)}\n >\n {initial}\n </span>\n )\n }\n return <AgentTypeIcon agent={HUMAN_AGENT_STUB} className={className} />\n}\n"],"mappings":"+NASM,EAAmB,CAAE,GAAI,EAAgB,CAO/C,SAAgB,EAAW,CACzB,OACA,UACA,WAAW,GACX,aAQC,CACD,IAAM,EAAY,GAAM,YAAc,GAAM,iBAAmB,cAAc,mBAAmB,EAAK,iBAAiB,CAAC,UAAY,MAC7H,CAAC,EAAW,IAAA,EAAA,EAAA,UAAwC,KAAK,EAE/D,EAAA,EAAA,eAAgB,CACd,GAAI,CAAC,EAAW,CAAE,EAAa,KAAK,CAAE,OACtC,IAAI,EAAY,GACZ,EAAqB,KAWzB,OAVM,SAAY,CAChB,GAAI,CACF,IAAM,EAAO,MAAM,EAAQ,EAAU,CACrC,GAAI,EAAW,OACf,EAAM,IAAI,gBAAgB,EAAK,CAC/B,EAAa,EAAI,MACX,CACD,GAAW,EAAa,KAAK,KAElC,KACS,CAAE,EAAY,GAAU,GAAK,IAAI,gBAAgB,EAAI,GACjE,CAAC,EAAU,CAAC,CAEf,IAAM,EAAQ,GAAM,aAAe,GAAM,QAAU,OAEnD,GAAI,EACF,OAAO,EAAA,EAAA,KAAC,MAAD,CAAK,IAAK,EAAW,IAAK,EAAc,QAAO,UAAW,EAAG,kCAAmC,EAAU,CAAI,CAAA,CAEvH,GAAI,EAAU,CACZ,IAAM,GAAW,EAAM,MAAM,CAAC,IAAM,KAAK,aAAa,CACtD,OACE,EAAA,EAAA,KAAC,OAAD,CACS,QACP,UAAW,EAAG,sHAAuH,EAAiB,EAAQ,CAAE,EAAU,UAEzK,EACI,CAAA,CAGX,OAAO,EAAA,EAAA,KAAC,EAAD,CAAe,MAAO,EAA6B,YAAa,CAAA"}
1
+ {"version":3,"file":"user-avatar-BGXTBaME.js","names":[],"sources":["../../dashboard/src/components/shared/user-avatar.tsx"],"sourcesContent":["import { useEffect, useState } from 'react'\nimport { AgentTypeIcon } from '@/components/shared/agent-type-icon'\nimport { apiBlob } from '@/lib/api'\nimport { humanAccentClass } from '@/lib/display'\nimport { HUMAN_AGENT_ID } from '@/lib/constants'\nimport { cn } from '@/lib/utils'\nimport type { Agent, User } from '@/types'\n\n// Minimal stub so AgentTypeIcon resolves the generic 'user' face when no avatar exists.\nconst HUMAN_AGENT_STUB = { id: HUMAN_AGENT_ID } as unknown as Agent\n\n// A human's face (#390). Renders, in order of preference: the user's content-addressed avatar\n// image (the FK lands fully in #630 — until then `avatarArtifactId` is null and this path is\n// dormant), then a colored initial when we need to distinguish multiple operators, then the\n// generic operator icon. Degrades gracefully at every step so a single seeded user looks\n// exactly as before.\nexport function UserAvatar({\n user,\n address,\n distinct = false,\n className,\n}: {\n user: User | undefined\n /** The human address (`user` / `user:<id>`) — drives the deterministic accent color. */\n address?: string\n /** When true (multiple humans in view), fall back to a colored initial instead of the icon. */\n distinct?: boolean\n className?: string\n}) {\n const avatarUrl = user?.avatarUrl ?? (user?.avatarArtifactId ? `/artifacts/${encodeURIComponent(user.avatarArtifactId)}/content` : null)\n const [objectUrl, setObjectUrl] = useState<string | null>(null)\n\n useEffect(() => {\n if (!avatarUrl) { setObjectUrl(null); return }\n let cancelled = false\n let url: string | null = null\n void (async () => {\n try {\n const blob = await apiBlob(avatarUrl)\n if (cancelled) return\n url = URL.createObjectURL(blob)\n setObjectUrl(url)\n } catch {\n if (!cancelled) setObjectUrl(null)\n }\n })()\n return () => { cancelled = true; if (url) URL.revokeObjectURL(url) }\n }, [avatarUrl])\n\n const title = user?.displayName || user?.handle || 'User'\n\n if (objectUrl) {\n return <img src={objectUrl} alt={title} title={title} className={cn('w-7 h-7 rounded-md object-cover', className)} />\n }\n if (distinct) {\n const initial = (title.trim()[0] || 'U').toUpperCase()\n return (\n <span\n title={title}\n className={cn('inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10', humanAccentClass(address), className)}\n >\n {initial}\n </span>\n )\n }\n return <AgentTypeIcon agent={HUMAN_AGENT_STUB} className={className} />\n}\n"],"mappings":"+NASM,EAAmB,CAAE,GAAI,EAAgB,CAO/C,SAAgB,EAAW,CACzB,OACA,UACA,WAAW,GACX,aAQC,CACD,IAAM,EAAY,GAAM,YAAc,GAAM,iBAAmB,cAAc,mBAAmB,EAAK,iBAAiB,CAAC,UAAY,MAC7H,CAAC,EAAW,IAAA,EAAA,EAAA,UAAwC,KAAK,EAE/D,EAAA,EAAA,eAAgB,CACd,GAAI,CAAC,EAAW,CAAE,EAAa,KAAK,CAAE,OACtC,IAAI,EAAY,GACZ,EAAqB,KAWzB,OAVM,SAAY,CAChB,GAAI,CACF,IAAM,EAAO,MAAM,EAAQ,EAAU,CACrC,GAAI,EAAW,OACf,EAAM,IAAI,gBAAgB,EAAK,CAC/B,EAAa,EAAI,MACX,CACD,GAAW,EAAa,KAAK,KAElC,KACS,CAAE,EAAY,GAAU,GAAK,IAAI,gBAAgB,EAAI,GACjE,CAAC,EAAU,CAAC,CAEf,IAAM,EAAQ,GAAM,aAAe,GAAM,QAAU,OAEnD,GAAI,EACF,OAAO,EAAA,EAAA,KAAC,MAAD,CAAK,IAAK,EAAW,IAAK,EAAc,QAAO,UAAW,EAAG,kCAAmC,EAAU,CAAI,CAAA,CAEvH,GAAI,EAAU,CACZ,IAAM,GAAW,EAAM,MAAM,CAAC,IAAM,KAAK,aAAa,CACtD,OACE,EAAA,EAAA,KAAC,OAAD,CACS,QACP,UAAW,EAAG,sHAAuH,EAAiB,EAAQ,CAAE,EAAU,UAEzK,EACI,CAAA,CAGX,OAAO,EAAA,EAAA,KAAC,EAAD,CAAe,MAAO,EAA6B,YAAa,CAAA"}
@@ -1,2 +1,2 @@
1
- import{Dn as e,Qn as t,hn as n,i as r,pt as i}from"./lucide-react-BYZuAhQs.js";import{l as a,t as o}from"./store-pKbf_WYd.js";import{K as s,ct as c,d as l}from"./display-DRiGi1zj.js";import{t as u}from"./badge-BLar6FjP.js";import{t as d}from"./button-BrIB69qd.js";import{W as f,X as p,u as m}from"./index-GoC6YrKn.js";import{r as h,t as g}from"./card-lhnFnBoz.js";var _=t(),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),n=o(e=>e.compose),r=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=f(),S=p(),C=S.filter(e=>e.claimable).length;function w(e){if(!n.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`?`Task #${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)(i,{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:n.from,onChange:e=>r({compose:{...n,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:t,now:i,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)(n,{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)(e,{className:`w-3 h-3 inline mr-1`}),s(i,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)(n,{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)(r,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
- //# sourceMappingURL=work-queue-DN4oTe7w.js.map
1
+ import{Dn as e,Qn as t,hn as n,i as r,pt as i}from"./lucide-react-BYZuAhQs.js";import{l as a,t as o}from"./store-pKbf_WYd.js";import{K as s,ct as c,d as l}from"./display-DRiGi1zj.js";import{t as u}from"./badge-BLar6FjP.js";import{t as d}from"./button-BrIB69qd.js";import{W as f,X as p,u as m}from"./index-B4cTW4A9.js";import{r as h,t as g}from"./card-lhnFnBoz.js";var _=t(),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),n=o(e=>e.compose),r=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=f(),S=p(),C=S.filter(e=>e.claimable).length;function w(e){if(!n.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`?`Task #${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)(i,{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:n.from,onChange:e=>r({compose:{...n,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:t,now:i,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)(n,{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)(e,{className:`w-3 h-3 inline mr-1`}),s(i,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)(n,{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)(r,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
+ //# sourceMappingURL=work-queue-DMwtDbBg.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"work-queue-DN4oTe7w.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' ? `Task #${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-DMwtDbBg.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' ? `Task #${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-GoC6YrKn.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-B4cTW4A9.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-BYZuAhQs.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.99.1",
4
+ "version": "0.100.0",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -0,0 +1,300 @@
1
+ #!/usr/bin/env bash
2
+ # Shared "fleet fratricide" kill-pattern guard (issue #753).
3
+ #
4
+ # Every managed agent's runner runs as
5
+ # bun run .../agent-relay-runner/src/index.ts <provider> ...
6
+ # so a broad process kill such as `pkill -f "src/index.ts"` (or `pkill bun`,
7
+ # `killall node`, ...) matches EVERY sibling agent host-wide and silently SIGTERMs
8
+ # the whole fleet. This is the structural guard so that can't happen again.
9
+ #
10
+ # It blocks broad process-kill commands (pkill / killall, and `kill` with a
11
+ # pattern that leaks a fleet token) whose target would match sibling runners,
12
+ # while still allowing the safe shapes: PID-scoped kills (kill 12345,
13
+ # kill "$SERVER_PID") and pkill -f on a token that is unique to the caller's own
14
+ # throwaway server (its exact test port, or "$PWD/public").
15
+ #
16
+ # DUAL MODE:
17
+ # - sourced: defines fratricide_deny_reason() / fratricide_raw_reason() plus the
18
+ # shell-parsing helpers (trim_shell_text / split_segments / shell_words). The
19
+ # Claude PreToolUse Bash guard sources this and is wired to it.
20
+ # - executed: `kill-guard.sh "<command text>"` prints the guidance + exits 0 when
21
+ # the command is a dangerous broad kill, and exits 1 (silent) when it is safe.
22
+ # This lets non-bash callers reuse the SAME matcher as a subprocess.
23
+ #
24
+ # TODO(#753 follow-up): wire the Codex runner/exec layer to invoke this guard.
25
+ # Codex managed agents have no Claude PreToolUse hook, so the same broad pkill can
26
+ # still fratricide the fleet from a Codex session until that wiring lands. The
27
+ # matcher is intentionally factored here so the Codex side can call it directly
28
+ # (e.g. `bash kill-guard.sh "$command"`) without duplicating the pattern list.
29
+
30
+ shopt -s extglob
31
+
32
+ # Guidance returned on a block — tells the caller how to clean up safely instead.
33
+ FRATRICIDE_PATTERN_GUIDANCE='Blocked a broad process-kill (#753 fleet-fratricide guard): this pattern would match sibling agents'\'' runners, because every managed agent runs "bun run .../agent-relay-runner/src/index.ts". Kill only a PID you own — capture it when you spawn the server (SERVER_PID=$!; kill "$SERVER_PID") — or pkill -f a token that is unique to your own process (your exact test port, or "$PWD/public"). Never pkill/killall on bun, node, src/index.ts, agent-relay, or agent-relay-runner.'
34
+
35
+ trim_shell_text() {
36
+ local value="$1"
37
+ value="${value##+([[:space:]])}"
38
+ value="${value%%+([[:space:]])}"
39
+ printf '%s' "$value"
40
+ }
41
+
42
+ split_segments() {
43
+ local text="$1"
44
+ local current="" i len ch next quote="" escaped=0
45
+ SEGMENTS=()
46
+ len=${#text}
47
+ for ((i = 0; i < len; i++)); do
48
+ ch="${text:i:1}"
49
+ if (( escaped )); then
50
+ current+="$ch"
51
+ escaped=0
52
+ continue
53
+ fi
54
+ if [ -n "$quote" ]; then
55
+ current+="$ch"
56
+ if [ "$quote" = '"' ] && [ "$ch" = "\\" ]; then
57
+ escaped=1
58
+ continue
59
+ fi
60
+ if [ "$ch" = "$quote" ]; then
61
+ quote=""
62
+ fi
63
+ continue
64
+ fi
65
+
66
+ case "$ch" in
67
+ "'"|'"')
68
+ quote="$ch"
69
+ current+="$ch"
70
+ ;;
71
+ "\\")
72
+ escaped=1
73
+ current+="$ch"
74
+ ;;
75
+ $'\n'|";")
76
+ SEGMENTS+=("$current")
77
+ current=""
78
+ ;;
79
+ "&")
80
+ next="${text:i+1:1}"
81
+ if [ "$next" = "&" ]; then
82
+ ((i += 1))
83
+ fi
84
+ SEGMENTS+=("$current")
85
+ current=""
86
+ ;;
87
+ "|")
88
+ next="${text:i+1:1}"
89
+ if [ "$next" = "|" ]; then
90
+ ((i += 1))
91
+ fi
92
+ SEGMENTS+=("$current")
93
+ current=""
94
+ ;;
95
+ *)
96
+ current+="$ch"
97
+ ;;
98
+ esac
99
+ done
100
+
101
+ if [ -n "$quote" ] || (( escaped )); then
102
+ return 1
103
+ fi
104
+ SEGMENTS+=("$current")
105
+ return 0
106
+ }
107
+
108
+ shell_words() {
109
+ local text="$1"
110
+ local word="" i len ch quote="" escaped=0 in_word=0
111
+ WORDS=()
112
+ len=${#text}
113
+ for ((i = 0; i < len; i++)); do
114
+ ch="${text:i:1}"
115
+ if (( escaped )); then
116
+ word+="$ch"
117
+ in_word=1
118
+ escaped=0
119
+ continue
120
+ fi
121
+ if [ -n "$quote" ]; then
122
+ if [ "$quote" = '"' ] && [ "$ch" = "\\" ]; then
123
+ escaped=1
124
+ continue
125
+ fi
126
+ if [ "$ch" = "$quote" ]; then
127
+ quote=""
128
+ else
129
+ word+="$ch"
130
+ fi
131
+ in_word=1
132
+ continue
133
+ fi
134
+
135
+ case "$ch" in
136
+ [[:space:]])
137
+ if (( in_word )); then
138
+ WORDS+=("$word")
139
+ word=""
140
+ in_word=0
141
+ fi
142
+ ;;
143
+ "'"|'"')
144
+ quote="$ch"
145
+ in_word=1
146
+ ;;
147
+ "\\")
148
+ escaped=1
149
+ in_word=1
150
+ ;;
151
+ *)
152
+ word+="$ch"
153
+ in_word=1
154
+ ;;
155
+ esac
156
+ done
157
+
158
+ if [ -n "$quote" ] || (( escaped )); then
159
+ return 1
160
+ fi
161
+ if (( in_word )); then
162
+ WORDS+=("$word")
163
+ fi
164
+ return 0
165
+ }
166
+
167
+ # True (0) when text mentions a process-kill verb (pkill / killall / kill).
168
+ _looks_like_kill() {
169
+ if [[ "${1,,}" =~ (^|[^a-z])(pkill|killall|kill)([^a-z]|$) ]]; then
170
+ return 0
171
+ fi
172
+ return 1
173
+ }
174
+
175
+ # True (0) when a kill pattern/token would match sibling runners across the fleet.
176
+ # Matches the runner path tokens, and the bare interpreter process names bun/node
177
+ # even when wrapped in pgrep/regex metacharacters such as [b]un, ^node$, bun.* .
178
+ _pattern_hits_fleet() {
179
+ local lower="${1,,}"
180
+ case "$lower" in
181
+ *index.ts*|*agent-relay*)
182
+ return 0
183
+ ;;
184
+ esac
185
+ local cleaned="$lower" mc
186
+ for mc in '[' ']' '(' ')' '{' '}' '^' '$' '*' '+' '?' '\' '|'; do
187
+ cleaned="${cleaned//"$mc"/}"
188
+ done
189
+ if [[ "$cleaned" =~ (^|[^a-z0-9])(bun|node)([^a-z0-9]|$) ]]; then
190
+ return 0
191
+ fi
192
+ return 1
193
+ }
194
+
195
+ # Inspect one shell segment. Prints guidance + returns 0 when it is a dangerous
196
+ # broad kill; returns 1 when the segment is safe or not a kill.
197
+ _fratricide_segment_reason() {
198
+ local raw="$1" segment base i=0 j arg
199
+ segment="$(trim_shell_text "$raw")"
200
+ if [ -z "$segment" ]; then
201
+ return 1
202
+ fi
203
+ if ! shell_words "$segment"; then
204
+ # Unparseable — only worry if it both looks like a kill and leaks a fleet token.
205
+ if _looks_like_kill "$segment" && _pattern_hits_fleet "$segment"; then
206
+ printf '%s' "$FRATRICIDE_PATTERN_GUIDANCE"
207
+ return 0
208
+ fi
209
+ return 1
210
+ fi
211
+ if (( ${#WORDS[@]} == 0 )); then
212
+ return 1
213
+ fi
214
+
215
+ # Skip any leading VAR=value assignments (e.g. SERVER_PID=$!; kill ...).
216
+ while (( i < ${#WORDS[@]} )) && [[ "${WORDS[i]}" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; do
217
+ ((i += 1))
218
+ done
219
+ if (( i >= ${#WORDS[@]} )); then
220
+ return 1
221
+ fi
222
+
223
+ base="${WORDS[i]##*/}"
224
+ base="${base,,}"
225
+ case "$base" in
226
+ pkill|killall)
227
+ # Pattern-matching kills: block if ANY non-flag operand hits the fleet.
228
+ j=$((i + 1))
229
+ while (( j < ${#WORDS[@]} )); do
230
+ arg="${WORDS[j]}"
231
+ if [[ "$arg" == -* ]]; then
232
+ ((j += 1))
233
+ continue
234
+ fi
235
+ if _pattern_hits_fleet "$arg"; then
236
+ printf '%s' "$FRATRICIDE_PATTERN_GUIDANCE"
237
+ return 0
238
+ fi
239
+ ((j += 1))
240
+ done
241
+ return 1
242
+ ;;
243
+ kill)
244
+ # PID-scoped kills (kill 123, kill "$PID", kill -9 %1) are safe. Block only
245
+ # when an operand leaks a fleet token — e.g. kill $(pgrep -f src/index.ts).
246
+ j=$((i + 1))
247
+ while (( j < ${#WORDS[@]} )); do
248
+ arg="${WORDS[j]}"
249
+ if _pattern_hits_fleet "$arg"; then
250
+ printf '%s' "$FRATRICIDE_PATTERN_GUIDANCE"
251
+ return 0
252
+ fi
253
+ ((j += 1))
254
+ done
255
+ return 1
256
+ ;;
257
+ *)
258
+ return 1
259
+ ;;
260
+ esac
261
+ }
262
+
263
+ # Structured matcher: split a full command into segments and check each one.
264
+ # Prints guidance + returns 0 on the first dangerous broad kill; returns 1 if safe.
265
+ fratricide_deny_reason() {
266
+ local text="$1" segment reason
267
+ if ! split_segments "$text"; then
268
+ fratricide_raw_reason "$text"
269
+ return $?
270
+ fi
271
+ local -a segments=("${SEGMENTS[@]}")
272
+ for segment in "${segments[@]}"; do
273
+ if reason="$(_fratricide_segment_reason "$segment")"; then
274
+ printf '%s' "$reason"
275
+ return 0
276
+ fi
277
+ done
278
+ return 1
279
+ }
280
+
281
+ # Coarse raw-text fallback for when structured parsing is unavailable (no jq, or
282
+ # unparseable shell). Prints guidance + returns 0 only when the text both looks
283
+ # like a kill and leaks a fleet token.
284
+ fratricide_raw_reason() {
285
+ local text="$1"
286
+ if _looks_like_kill "$text" && _pattern_hits_fleet "$text"; then
287
+ printf '%s' "$FRATRICIDE_PATTERN_GUIDANCE"
288
+ return 0
289
+ fi
290
+ return 1
291
+ }
292
+
293
+ # Executed directly (not sourced): argv[1] is the command text to inspect.
294
+ if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
295
+ if reason="$(fratricide_deny_reason "${1:-}")"; then
296
+ printf '%s\n' "$reason"
297
+ exit 0
298
+ fi
299
+ exit 1
300
+ fi
@@ -4,8 +4,12 @@
4
4
  # Real boundaries are Relay token scope, unavailable write tools, and future
5
5
  # OS-level enforcement.
6
6
  set -euo pipefail
7
- source "${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/hooks/relay-status.sh"
7
+ HOOK_DIR="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/hooks"
8
+ source "${HOOK_DIR}/relay-status.sh"
8
9
  relay_install_hook_guard read-only-bash-guard
10
+ # Shared fleet-fratricide kill-pattern matcher (#753): trim_shell_text,
11
+ # split_segments, shell_words and fratricide_deny_reason live here.
12
+ source "${HOOK_DIR}/kill-guard.sh"
9
13
 
10
14
  payload="$(cat)"
11
15
 
@@ -15,6 +19,38 @@ emit_deny() {
15
19
  "$(relay_json_escape "$reason")"
16
20
  }
17
21
 
22
+ shopt -s extglob
23
+
24
+ # Extract the Bash command once. jq is strongly preferred; both the fleet-kill
25
+ # guard (all modes) and the read-only allow-list (read-only mode) consume it.
26
+ jq_available=0
27
+ tool_name=""
28
+ command_text=""
29
+ if command -v jq >/dev/null 2>&1; then
30
+ jq_available=1
31
+ tool_name="$(printf '%s' "$payload" | jq -r '.tool_name // empty' 2>/dev/null || true)"
32
+ command_text="$(printf '%s' "$payload" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
33
+ fi
34
+
35
+ # --- Fleet-fratricide guard: ACTIVE IN ALL APPROVAL MODES (#753) ---
36
+ # A broad pkill/killall/kill can SIGTERM every sibling agent's runner host-wide,
37
+ # so this block runs before (and independent of) the read-only allow-list.
38
+ if (( jq_available )); then
39
+ if [ "${tool_name,,}" = "bash" ] && [ -n "$command_text" ]; then
40
+ if fratricide_reason="$(fratricide_deny_reason "$command_text")"; then
41
+ emit_deny "$fratricide_reason"
42
+ exit 0
43
+ fi
44
+ fi
45
+ else
46
+ # No jq: best-effort raw scan so the fleet guard still bites the obvious cases.
47
+ if fratricide_reason="$(fratricide_raw_reason "$payload")"; then
48
+ emit_deny "$fratricide_reason"
49
+ exit 0
50
+ fi
51
+ fi
52
+
53
+ # --- Read-only allow-list: only the read-only approval mode below this point ---
18
54
  case "${AGENT_RELAY_APPROVAL:-}" in
19
55
  open|guarded)
20
56
  exit 0
@@ -27,40 +63,27 @@ case "${AGENT_RELAY_APPROVAL:-}" in
27
63
  ;;
28
64
  esac
29
65
 
30
- if ! command -v jq >/dev/null 2>&1; then
66
+ if (( ! jq_available )); then
31
67
  emit_deny "Read-only Bash guard could not inspect the command because jq is unavailable."
32
68
  exit 0
33
69
  fi
34
70
 
35
- tool_name="$(printf '%s' "$payload" | jq -r '.tool_name // empty' 2>/dev/null || true)"
36
71
  if [ "${tool_name,,}" != "bash" ]; then
37
72
  exit 0
38
73
  fi
39
74
 
40
- command_text="$(printf '%s' "$payload" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
41
75
  if [ -z "$command_text" ]; then
42
76
  emit_deny "Read-only Bash guard denied an empty Bash command."
43
77
  exit 0
44
78
  fi
45
79
 
46
- shopt -s extglob
47
-
48
80
  DENY_REASON=""
49
- SEGMENTS=()
50
- WORDS=()
51
81
 
52
82
  deny_segment() {
53
83
  DENY_REASON="$1"
54
84
  return 1
55
85
  }
56
86
 
57
- trim_shell_text() {
58
- local value="$1"
59
- value="${value##+([[:space:]])}"
60
- value="${value%%+([[:space:]])}"
61
- printf '%s' "$value"
62
- }
63
-
64
87
  hard_deny_reason() {
65
88
  local text="$1"
66
89
  case "$text" in
@@ -117,131 +140,6 @@ hard_deny_reason() {
117
140
  return 1
118
141
  }
119
142
 
120
- split_segments() {
121
- local text="$1"
122
- local current="" i len ch next quote="" escaped=0
123
- SEGMENTS=()
124
- len=${#text}
125
- for ((i = 0; i < len; i++)); do
126
- ch="${text:i:1}"
127
- if (( escaped )); then
128
- current+="$ch"
129
- escaped=0
130
- continue
131
- fi
132
- if [ -n "$quote" ]; then
133
- current+="$ch"
134
- if [ "$quote" = '"' ] && [ "$ch" = "\\" ]; then
135
- escaped=1
136
- continue
137
- fi
138
- if [ "$ch" = "$quote" ]; then
139
- quote=""
140
- fi
141
- continue
142
- fi
143
-
144
- case "$ch" in
145
- "'"|'"')
146
- quote="$ch"
147
- current+="$ch"
148
- ;;
149
- "\\")
150
- escaped=1
151
- current+="$ch"
152
- ;;
153
- $'\n'|";")
154
- SEGMENTS+=("$current")
155
- current=""
156
- ;;
157
- "&")
158
- next="${text:i+1:1}"
159
- if [ "$next" = "&" ]; then
160
- ((i += 1))
161
- fi
162
- SEGMENTS+=("$current")
163
- current=""
164
- ;;
165
- "|")
166
- next="${text:i+1:1}"
167
- if [ "$next" = "|" ]; then
168
- ((i += 1))
169
- fi
170
- SEGMENTS+=("$current")
171
- current=""
172
- ;;
173
- *)
174
- current+="$ch"
175
- ;;
176
- esac
177
- done
178
-
179
- if [ -n "$quote" ] || (( escaped )); then
180
- return 1
181
- fi
182
- SEGMENTS+=("$current")
183
- return 0
184
- }
185
-
186
- shell_words() {
187
- local text="$1"
188
- local word="" i len ch quote="" escaped=0 in_word=0
189
- WORDS=()
190
- len=${#text}
191
- for ((i = 0; i < len; i++)); do
192
- ch="${text:i:1}"
193
- if (( escaped )); then
194
- word+="$ch"
195
- in_word=1
196
- escaped=0
197
- continue
198
- fi
199
- if [ -n "$quote" ]; then
200
- if [ "$quote" = '"' ] && [ "$ch" = "\\" ]; then
201
- escaped=1
202
- continue
203
- fi
204
- if [ "$ch" = "$quote" ]; then
205
- quote=""
206
- else
207
- word+="$ch"
208
- fi
209
- in_word=1
210
- continue
211
- fi
212
-
213
- case "$ch" in
214
- [[:space:]])
215
- if (( in_word )); then
216
- WORDS+=("$word")
217
- word=""
218
- in_word=0
219
- fi
220
- ;;
221
- "'"|'"')
222
- quote="$ch"
223
- in_word=1
224
- ;;
225
- "\\")
226
- escaped=1
227
- in_word=1
228
- ;;
229
- *)
230
- word+="$ch"
231
- in_word=1
232
- ;;
233
- esac
234
- done
235
-
236
- if [ -n "$quote" ] || (( escaped )); then
237
- return 1
238
- fi
239
- if (( in_word )); then
240
- WORDS+=("$word")
241
- fi
242
- return 0
243
- }
244
-
245
143
  validate_git_branch() {
246
144
  local i="$1" arg
247
145
  while (( i < ${#WORDS[@]} )); do