@hotmeshio/long-tail 0.5.12 → 0.6.2

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 (337) hide show
  1. package/README.md +1 -0
  2. package/build/api/escalations/bulk.js +9 -1
  3. package/build/api/escalations/cancel.js +5 -7
  4. package/build/api/escalations/claim.js +18 -15
  5. package/build/api/escalations/create.d.ts +6 -0
  6. package/build/api/escalations/create.js +11 -8
  7. package/build/api/escalations/facets.d.ts +18 -0
  8. package/build/api/escalations/facets.js +115 -0
  9. package/build/api/escalations/helpers.d.ts +70 -1
  10. package/build/api/escalations/helpers.js +101 -7
  11. package/build/api/escalations/index.d.ts +3 -2
  12. package/build/api/escalations/index.js +7 -1
  13. package/build/api/escalations/list.d.ts +21 -2
  14. package/build/api/escalations/list.js +109 -10
  15. package/build/api/escalations/metadata.js +22 -27
  16. package/build/api/escalations/resolve.d.ts +19 -1
  17. package/build/api/escalations/resolve.js +111 -26
  18. package/build/api/escalations/single.js +8 -8
  19. package/build/api/users.d.ts +6 -0
  20. package/build/api/users.js +29 -1
  21. package/build/bin/ltc.js +40 -0
  22. package/build/lib/cli/commands/escalations.d.ts +36 -0
  23. package/build/lib/cli/commands/escalations.js +98 -0
  24. package/build/lib/db/schemas/001_schema.sql +10 -5
  25. package/build/lib/db/schemas/012_lt_tasks_workflow_id_unique.sql +20 -0
  26. package/build/lib/db/schemas/013_role_scope.sql +37 -0
  27. package/build/modules/version.d.ts +10 -5
  28. package/build/modules/version.js +27 -11
  29. package/build/routes/escalations/facets.d.ts +7 -0
  30. package/build/routes/escalations/facets.js +81 -0
  31. package/build/routes/escalations/index.js +3 -0
  32. package/build/routes/escalations/list.js +46 -0
  33. package/build/routes/escalations/resolve.js +22 -4
  34. package/build/routes/users.js +9 -5
  35. package/build/sdk/index.d.ts +33 -2
  36. package/build/sdk/index.js +7 -0
  37. package/build/services/escalation/crud.d.ts +56 -6
  38. package/build/services/escalation/crud.js +102 -31
  39. package/build/services/escalation/facet-sql.d.ts +18 -0
  40. package/build/services/escalation/facet-sql.js +118 -0
  41. package/build/services/escalation/facets.d.ts +45 -0
  42. package/build/services/escalation/facets.js +218 -0
  43. package/build/services/escalation/index.d.ts +1 -0
  44. package/build/services/escalation/index.js +8 -1
  45. package/build/services/escalation/queries.d.ts +45 -0
  46. package/build/services/escalation/queries.js +107 -6
  47. package/build/services/escalation/sql.d.ts +10 -4
  48. package/build/services/escalation/sql.js +36 -8
  49. package/build/services/interceptor/activities/escalation.d.ts +2 -0
  50. package/build/services/interceptor/activities/escalation.js +1 -1
  51. package/build/services/interceptor/lifecycle.js +4 -0
  52. package/build/services/mcp/server-tools.js +10 -4
  53. package/build/services/orchestrator/condition.js +6 -2
  54. package/build/services/role/index.js +2 -3
  55. package/build/services/role/sql.d.ts +8 -0
  56. package/build/services/role/sql.js +17 -1
  57. package/build/services/task/crud.js +22 -14
  58. package/build/services/task/process.js +3 -21
  59. package/build/services/task/sql.d.ts +24 -1
  60. package/build/services/task/sql.js +65 -1
  61. package/build/services/user/crud.d.ts +1 -0
  62. package/build/services/user/crud.js +29 -11
  63. package/build/services/user/index.d.ts +2 -1
  64. package/build/services/user/index.js +12 -1
  65. package/build/services/user/rbac.d.ts +18 -0
  66. package/build/services/user/rbac.js +27 -0
  67. package/build/services/user/roles.d.ts +7 -2
  68. package/build/services/user/roles.js +21 -5
  69. package/build/services/user/scope.d.ts +22 -0
  70. package/build/services/user/scope.js +50 -0
  71. package/build/services/user/seed-admin.js +34 -17
  72. package/build/services/user/sql.d.ts +18 -7
  73. package/build/services/user/sql.js +38 -13
  74. package/build/services/user/sso-provision.js +41 -24
  75. package/build/services/user/types.d.ts +9 -5
  76. package/build/system/mcp-servers/admin/escalations.js +65 -0
  77. package/build/system/mcp-servers/admin/schemas.d.ts +325 -0
  78. package/build/system/mcp-servers/admin/schemas.js +63 -2
  79. package/build/system/mcp-servers/admin/users.js +28 -2
  80. package/build/tsconfig.tsbuildinfo +1 -1
  81. package/build/types/envelope.d.ts +7 -0
  82. package/build/types/facets.d.ts +60 -0
  83. package/build/types/facets.js +10 -0
  84. package/build/types/index.d.ts +2 -1
  85. package/build/types/user.d.ts +11 -0
  86. package/dashboard/dist/assets/AdminDashboard-C5gfVEeL.js +2 -0
  87. package/dashboard/dist/assets/{AdminDashboard-LTiYLuzq.js.map → AdminDashboard-C5gfVEeL.js.map} +1 -1
  88. package/dashboard/dist/assets/{AgentConfigPage-nlOudfpZ.js → AgentConfigPage-CWEnuiGN.js} +6 -6
  89. package/dashboard/dist/assets/{AgentConfigPage-nlOudfpZ.js.map → AgentConfigPage-CWEnuiGN.js.map} +1 -1
  90. package/dashboard/dist/assets/{AgentDetailPage-BSUnDvGp.js → AgentDetailPage-B6IE27iK.js} +3 -3
  91. package/dashboard/dist/assets/AgentDetailPage-B6IE27iK.js.map +1 -0
  92. package/dashboard/dist/assets/AgentsPage-Blhh2RsK.js +2 -0
  93. package/dashboard/dist/assets/{AgentsPage-DpS0cCYe.js.map → AgentsPage-Blhh2RsK.js.map} +1 -1
  94. package/dashboard/dist/assets/AvailableEscalationsPage-ClwAle76.js +2 -0
  95. package/dashboard/dist/assets/AvailableEscalationsPage-ClwAle76.js.map +1 -0
  96. package/dashboard/dist/assets/{BotPicker-BhKmq7M7.js → BotPicker-DSozotGk.js} +2 -2
  97. package/dashboard/dist/assets/{BotPicker-BhKmq7M7.js.map → BotPicker-DSozotGk.js.map} +1 -1
  98. package/dashboard/dist/assets/CapabilitiesPage-CrS_DTA1.js +2 -0
  99. package/dashboard/dist/assets/CapabilitiesPage-CrS_DTA1.js.map +1 -0
  100. package/dashboard/dist/assets/{CollapsibleSection-D7SVwNVk.js → CollapsibleSection-1i6bOQ1L.js} +2 -2
  101. package/dashboard/dist/assets/{CollapsibleSection-D7SVwNVk.js.map → CollapsibleSection-1i6bOQ1L.js.map} +1 -1
  102. package/dashboard/dist/assets/CopyableId-blg-gZvT.js +2 -0
  103. package/dashboard/dist/assets/CopyableId-blg-gZvT.js.map +1 -0
  104. package/dashboard/dist/assets/{CredentialsPage-fat5Z2U9.js → CredentialsPage-BxJio06E.js} +2 -2
  105. package/dashboard/dist/assets/{CredentialsPage-fat5Z2U9.js.map → CredentialsPage-BxJio06E.js.map} +1 -1
  106. package/dashboard/dist/assets/{CronLabel-OOP4P2mJ.js → CronLabel-BAfIHHz4.js} +2 -2
  107. package/dashboard/dist/assets/{CronLabel-OOP4P2mJ.js.map → CronLabel-BAfIHHz4.js.map} +1 -1
  108. package/dashboard/dist/assets/{CustomDurationPicker-COacSAXO.js → CustomDurationPicker-BLF0NFJK.js} +2 -2
  109. package/dashboard/dist/assets/{CustomDurationPicker-COacSAXO.js.map → CustomDurationPicker-BLF0NFJK.js.map} +1 -1
  110. package/dashboard/dist/assets/{DataTable-D9yuBv0w.js → DataTable-ZWgVQg7F.js} +2 -2
  111. package/dashboard/dist/assets/DataTable-ZWgVQg7F.js.map +1 -0
  112. package/dashboard/dist/assets/{DropZone-Bfw2YLVS.js → DropZone-CEx_wk-g.js} +2 -2
  113. package/dashboard/dist/assets/{DropZone-Bfw2YLVS.js.map → DropZone-CEx_wk-g.js.map} +1 -1
  114. package/dashboard/dist/assets/{ElapsedCell-CkfCD-m7.js → ElapsedCell-nKGJvznH.js} +2 -2
  115. package/dashboard/dist/assets/{ElapsedCell-CkfCD-m7.js.map → ElapsedCell-nKGJvznH.js.map} +1 -1
  116. package/dashboard/dist/assets/{EscalationsOverview-BmI8jxlh.js → EscalationsOverview-bNNVA6R2.js} +2 -2
  117. package/dashboard/dist/assets/{EscalationsOverview-BmI8jxlh.js.map → EscalationsOverview-bNNVA6R2.js.map} +1 -1
  118. package/dashboard/dist/assets/{EventTable-CeKfFk3m.js → EventTable-BCuDdqlU.js} +2 -2
  119. package/dashboard/dist/assets/{EventTable-CeKfFk3m.js.map → EventTable-BCuDdqlU.js.map} +1 -1
  120. package/dashboard/dist/assets/FilterBar-BQ2JGg8U.js +2 -0
  121. package/dashboard/dist/assets/FilterBar-BQ2JGg8U.js.map +1 -0
  122. package/dashboard/dist/assets/GraphInvokePage-BPAvqHls.js +2 -0
  123. package/dashboard/dist/assets/GraphInvokePage-BPAvqHls.js.map +1 -0
  124. package/dashboard/dist/assets/HomePage-BZ1YTrjR.js +2 -0
  125. package/dashboard/dist/assets/HomePage-BZ1YTrjR.js.map +1 -0
  126. package/dashboard/dist/assets/{ListToolbar-BJaCzAXS.js → ListToolbar-DIJZfEgu.js} +2 -2
  127. package/dashboard/dist/assets/{ListToolbar-BJaCzAXS.js.map → ListToolbar-DIJZfEgu.js.map} +1 -1
  128. package/dashboard/dist/assets/{McpOverview-CvnDG5OB.js → McpOverview-DCfSCfPg.js} +2 -2
  129. package/dashboard/dist/assets/{McpOverview-CvnDG5OB.js.map → McpOverview-DCfSCfPg.js.map} +1 -1
  130. package/dashboard/dist/assets/{McpQueryDetailPage-DwmSIcIL.js → McpQueryDetailPage-mb7rmtNe.js} +2 -2
  131. package/dashboard/dist/assets/{McpQueryDetailPage-DwmSIcIL.js.map → McpQueryDetailPage-mb7rmtNe.js.map} +1 -1
  132. package/dashboard/dist/assets/{McpQueryPage-CU6FMEEk.js → McpQueryPage-D9b1T0I3.js} +2 -2
  133. package/dashboard/dist/assets/{McpQueryPage-CU6FMEEk.js.map → McpQueryPage-D9b1T0I3.js.map} +1 -1
  134. package/dashboard/dist/assets/{McpRunDetailPage-8a6lsehv.js → McpRunDetailPage-Bz2UyuNm.js} +2 -2
  135. package/dashboard/dist/assets/{McpRunDetailPage-8a6lsehv.js.map → McpRunDetailPage-Bz2UyuNm.js.map} +1 -1
  136. package/dashboard/dist/assets/McpRunsPage-Dw7w_ZO-.js +2 -0
  137. package/dashboard/dist/assets/McpRunsPage-Dw7w_ZO-.js.map +1 -0
  138. package/dashboard/dist/assets/{NamespacePill-D6btr6e8.js → NamespacePill-D27vqDXy.js} +2 -2
  139. package/dashboard/dist/assets/{NamespacePill-D6btr6e8.js.map → NamespacePill-D27vqDXy.js.map} +1 -1
  140. package/dashboard/dist/assets/OperatorDashboard-CTzSDW3N.js +2 -0
  141. package/dashboard/dist/assets/{OperatorDashboard-g9o7I925.js.map → OperatorDashboard-CTzSDW3N.js.map} +1 -1
  142. package/dashboard/dist/assets/PageHeader-B_1zTx2q.js +2 -0
  143. package/dashboard/dist/assets/PageHeader-B_1zTx2q.js.map +1 -0
  144. package/dashboard/dist/assets/{PageHeaderWithStats-DJQCZ0FR.js → PageHeaderWithStats-D-cPg7UD.js} +2 -2
  145. package/dashboard/dist/assets/{PageHeaderWithStats-DJQCZ0FR.js.map → PageHeaderWithStats-D-cPg7UD.js.map} +1 -1
  146. package/dashboard/dist/assets/PriorityBadge-B4ykBH7f.js +2 -0
  147. package/dashboard/dist/assets/PriorityBadge-B4ykBH7f.js.map +1 -0
  148. package/dashboard/dist/assets/{ProcessDetailPage-ByyWCs2A.js → ProcessDetailPage-olG2CS-h.js} +2 -2
  149. package/dashboard/dist/assets/{ProcessDetailPage-ByyWCs2A.js.map → ProcessDetailPage-olG2CS-h.js.map} +1 -1
  150. package/dashboard/dist/assets/{ProcessesListPage-BTsXds4l.js → ProcessesListPage-CUZSL06Y.js} +2 -2
  151. package/dashboard/dist/assets/{ProcessesListPage-BTsXds4l.js.map → ProcessesListPage-CUZSL06Y.js.map} +1 -1
  152. package/dashboard/dist/assets/RolePill-B-V-31lC.js +2 -0
  153. package/dashboard/dist/assets/RolePill-B-V-31lC.js.map +1 -0
  154. package/dashboard/dist/assets/RolesPage-DLnaW6iD.js +2 -0
  155. package/dashboard/dist/assets/RolesPage-DLnaW6iD.js.map +1 -0
  156. package/dashboard/dist/assets/RowActions-BA9L9djf.js +2 -0
  157. package/dashboard/dist/assets/RowActions-BA9L9djf.js.map +1 -0
  158. package/dashboard/dist/assets/{RunAsSelector-BzkZXpSX.js → RunAsSelector-BoLHF4fy.js} +2 -2
  159. package/dashboard/dist/assets/{RunAsSelector-BzkZXpSX.js.map → RunAsSelector-BoLHF4fy.js.map} +1 -1
  160. package/dashboard/dist/assets/StickyPagination-BEMY1L-H.js +2 -0
  161. package/dashboard/dist/assets/StickyPagination-BEMY1L-H.js.map +1 -0
  162. package/dashboard/dist/assets/{StreamMessageDetail-B6ch-6SY.js → StreamMessageDetail-CVRVWXcU.js} +2 -2
  163. package/dashboard/dist/assets/{StreamMessageDetail-B6ch-6SY.js.map → StreamMessageDetail-CVRVWXcU.js.map} +1 -1
  164. package/dashboard/dist/assets/{SwimlaneTimeline-CjvfUkls.js → SwimlaneTimeline-DDpU0-Jn.js} +2 -2
  165. package/dashboard/dist/assets/{SwimlaneTimeline-CjvfUkls.js.map → SwimlaneTimeline-DDpU0-Jn.js.map} +1 -1
  166. package/dashboard/dist/assets/{TagInput-CZSeyesi.js → TagInput-BHVfG307.js} +2 -2
  167. package/dashboard/dist/assets/{TagInput-CZSeyesi.js.map → TagInput-BHVfG307.js.map} +1 -1
  168. package/dashboard/dist/assets/{TaskDetailPage-BjHU6opi.js → TaskDetailPage-B_xQtJMg.js} +2 -2
  169. package/dashboard/dist/assets/{TaskDetailPage-BjHU6opi.js.map → TaskDetailPage-B_xQtJMg.js.map} +1 -1
  170. package/dashboard/dist/assets/{TaskQueuePill-CQrXrSxk.js → TaskQueuePill-CGlxXqWt.js} +2 -2
  171. package/dashboard/dist/assets/{TaskQueuePill-CQrXrSxk.js.map → TaskQueuePill-CGlxXqWt.js.map} +1 -1
  172. package/dashboard/dist/assets/{TasksListPage-Bem44zd0.js → TasksListPage-CsrdIOKk.js} +2 -2
  173. package/dashboard/dist/assets/{TasksListPage-Bem44zd0.js.map → TasksListPage-CsrdIOKk.js.map} +1 -1
  174. package/dashboard/dist/assets/TimeAgo-DmicaxjT.js +2 -0
  175. package/dashboard/dist/assets/{TimeAgo-CUIYqSt_.js.map → TimeAgo-DmicaxjT.js.map} +1 -1
  176. package/dashboard/dist/assets/{TimestampCell--tnreI1i.js → TimestampCell-DeK89eRV.js} +2 -2
  177. package/dashboard/dist/assets/{TimestampCell--tnreI1i.js.map → TimestampCell-DeK89eRV.js.map} +1 -1
  178. package/dashboard/dist/assets/{ToolPill-O09jsTQM.js → ToolPill-NV7PxSpA.js} +2 -2
  179. package/dashboard/dist/assets/{ToolPill-O09jsTQM.js.map → ToolPill-NV7PxSpA.js.map} +1 -1
  180. package/dashboard/dist/assets/{ToolTestPanel-OD99H7Pk.js → ToolTestPanel-D3s2n9-9.js} +2 -2
  181. package/dashboard/dist/assets/{ToolTestPanel-OD99H7Pk.js.map → ToolTestPanel-D3s2n9-9.js.map} +1 -1
  182. package/dashboard/dist/assets/{TopicDetailPage-rh_dsTJI.js → TopicDetailPage-qyv7UWT_.js} +3 -3
  183. package/dashboard/dist/assets/TopicDetailPage-qyv7UWT_.js.map +1 -0
  184. package/dashboard/dist/assets/TopicsPage-CEUfMoed.js +2 -0
  185. package/dashboard/dist/assets/TopicsPage-CEUfMoed.js.map +1 -0
  186. package/dashboard/dist/assets/{UserName-C__c8BN0.js → UserName-Bn_BxI8K.js} +2 -2
  187. package/dashboard/dist/assets/{UserName-C__c8BN0.js.map → UserName-Bn_BxI8K.js.map} +1 -1
  188. package/dashboard/dist/assets/{WorkflowExecutionPage-Bw6QdtUO.js → WorkflowExecutionPage-DAk2RqpB.js} +2 -2
  189. package/dashboard/dist/assets/{WorkflowExecutionPage-Bw6QdtUO.js.map → WorkflowExecutionPage-DAk2RqpB.js.map} +1 -1
  190. package/dashboard/dist/assets/{WorkflowPill-CS-IsR0U.js → WorkflowPill-DDR0B6DN.js} +2 -2
  191. package/dashboard/dist/assets/{WorkflowPill-CS-IsR0U.js.map → WorkflowPill-DDR0B6DN.js.map} +1 -1
  192. package/dashboard/dist/assets/WorkflowsDashboard-DfBgM80t.js +2 -0
  193. package/dashboard/dist/assets/WorkflowsDashboard-DfBgM80t.js.map +1 -0
  194. package/dashboard/dist/assets/{WorkflowsOverview-BRc0QbA7.js → WorkflowsOverview--Dkoo9Jq.js} +2 -2
  195. package/dashboard/dist/assets/{WorkflowsOverview-BRc0QbA7.js.map → WorkflowsOverview--Dkoo9Jq.js.map} +1 -1
  196. package/dashboard/dist/assets/{YamlWorkflowDetailPage-DdzspNQR.js → YamlWorkflowDetailPage-B6HYbfFj.js} +25 -25
  197. package/dashboard/dist/assets/{YamlWorkflowDetailPage-DdzspNQR.js.map → YamlWorkflowDetailPage-B6HYbfFj.js.map} +1 -1
  198. package/dashboard/dist/assets/{YamlWorkflowsPage-Djw5ZbgQ.js → YamlWorkflowsPage-CJE-PxC0.js} +2 -2
  199. package/dashboard/dist/assets/{YamlWorkflowsPage-Djw5ZbgQ.js.map → YamlWorkflowsPage-CJE-PxC0.js.map} +1 -1
  200. package/dashboard/dist/assets/{agents-xstNkF6c.js → agents-DUciUZpt.js} +2 -2
  201. package/dashboard/dist/assets/{agents-xstNkF6c.js.map → agents-DUciUZpt.js.map} +1 -1
  202. package/dashboard/dist/assets/{bots-BztAI0s_.js → bots-BnyqCfuC.js} +2 -2
  203. package/dashboard/dist/assets/{bots-BztAI0s_.js.map → bots-BnyqCfuC.js.map} +1 -1
  204. package/dashboard/dist/assets/{capabilities-CEDwtxNL.js → capabilities-CkTWWT0q.js} +2 -2
  205. package/dashboard/dist/assets/{capabilities-CEDwtxNL.js.map → capabilities-CkTWWT0q.js.map} +1 -1
  206. package/dashboard/dist/assets/{controlplane-BlGEsRKe.js → controlplane-D9HaFZsC.js} +2 -2
  207. package/dashboard/dist/assets/{controlplane-BlGEsRKe.js.map → controlplane-D9HaFZsC.js.map} +1 -1
  208. package/dashboard/dist/assets/{escalation-iI4QKlP-.js → escalation-De9JnTKI.js} +2 -2
  209. package/dashboard/dist/assets/{escalation-iI4QKlP-.js.map → escalation-De9JnTKI.js.map} +1 -1
  210. package/dashboard/dist/assets/escalation-columns-Df7Pluzg.js +2 -0
  211. package/dashboard/dist/assets/escalation-columns-Df7Pluzg.js.map +1 -0
  212. package/dashboard/dist/assets/index-7hqtQI43.js +2 -0
  213. package/dashboard/dist/assets/index-7hqtQI43.js.map +1 -0
  214. package/dashboard/dist/assets/{index-CAqEy2Db.js → index-B07xLlyz.js} +2 -2
  215. package/dashboard/dist/assets/{index-CAqEy2Db.js.map → index-B07xLlyz.js.map} +1 -1
  216. package/dashboard/dist/assets/{index-D1xVY4JR.js → index-B1kc6itM.js} +24 -24
  217. package/dashboard/dist/assets/index-B1kc6itM.js.map +1 -0
  218. package/dashboard/dist/assets/index-BAhkWcOq.js +9 -0
  219. package/dashboard/dist/assets/index-BAhkWcOq.js.map +1 -0
  220. package/dashboard/dist/assets/{index-Dsi5o7dx.js → index-BsIaUbKD.js} +2 -2
  221. package/dashboard/dist/assets/{index-Dsi5o7dx.js.map → index-BsIaUbKD.js.map} +1 -1
  222. package/dashboard/dist/assets/index-BxdiKgnH.js +6 -0
  223. package/dashboard/dist/assets/index-BxdiKgnH.js.map +1 -0
  224. package/dashboard/dist/assets/{index-B7VC0vjf.js → index-BzMsh6Ui.js} +2 -2
  225. package/dashboard/dist/assets/{index-B7VC0vjf.js.map → index-BzMsh6Ui.js.map} +1 -1
  226. package/dashboard/dist/assets/index-CN0efCE9.js +6 -0
  227. package/dashboard/dist/assets/index-CN0efCE9.js.map +1 -0
  228. package/dashboard/dist/assets/{index-B5qsp8lb.js → index-CgD9fA9P.js} +2 -2
  229. package/dashboard/dist/assets/{index-B5qsp8lb.js.map → index-CgD9fA9P.js.map} +1 -1
  230. package/dashboard/dist/assets/{index-CUfgt3mV.js → index-Cqjf-yVP.js} +2 -2
  231. package/dashboard/dist/assets/{index-CUfgt3mV.js.map → index-Cqjf-yVP.js.map} +1 -1
  232. package/dashboard/dist/assets/{index-DjJ6VCuc.js → index-DNugmBs7.js} +2 -2
  233. package/dashboard/dist/assets/{index-DjJ6VCuc.js.map → index-DNugmBs7.js.map} +1 -1
  234. package/dashboard/dist/assets/{index-BUmnJ4eT.js → index-DjxTV3cn.js} +2 -2
  235. package/dashboard/dist/assets/{index-BUmnJ4eT.js.map → index-DjxTV3cn.js.map} +1 -1
  236. package/dashboard/dist/assets/index-DpAS4-DA.js +2 -0
  237. package/dashboard/dist/assets/index-DpAS4-DA.js.map +1 -0
  238. package/dashboard/dist/assets/index-nO0cim3_.css +1 -0
  239. package/dashboard/dist/assets/{knowledge-BKg0cr39.js → knowledge-D4SvdNv5.js} +2 -2
  240. package/dashboard/dist/assets/{knowledge-BKg0cr39.js.map → knowledge-D4SvdNv5.js.map} +1 -1
  241. package/dashboard/dist/assets/{mcp-CVdHpKlz.js → mcp-DVxt4T9J.js} +2 -2
  242. package/dashboard/dist/assets/{mcp-CVdHpKlz.js.map → mcp-DVxt4T9J.js.map} +1 -1
  243. package/dashboard/dist/assets/{mcp-query-CYxZcbwQ.js → mcp-query-CLP9EnnU.js} +2 -2
  244. package/dashboard/dist/assets/{mcp-query-CYxZcbwQ.js.map → mcp-query-CLP9EnnU.js.map} +1 -1
  245. package/dashboard/dist/assets/{pipelines-BOEv7P6O.js → pipelines-C_x0LTBN.js} +2 -2
  246. package/dashboard/dist/assets/{pipelines-BOEv7P6O.js.map → pipelines-C_x0LTBN.js.map} +1 -1
  247. package/dashboard/dist/assets/{roles-c5cV2QYQ.js → roles-QaGFZQ58.js} +2 -2
  248. package/dashboard/dist/assets/{roles-c5cV2QYQ.js.map → roles-QaGFZQ58.js.map} +1 -1
  249. package/dashboard/dist/assets/{tasks-JV3BjeEF.js → tasks-1eTuP02d.js} +2 -2
  250. package/dashboard/dist/assets/{tasks-JV3BjeEF.js.map → tasks-1eTuP02d.js.map} +1 -1
  251. package/dashboard/dist/assets/{topics-BDaPnhnQ.js → topics-B3p8K0PW.js} +2 -2
  252. package/dashboard/dist/assets/{topics-BDaPnhnQ.js.map → topics-B3p8K0PW.js.map} +1 -1
  253. package/dashboard/dist/assets/{useEventHooks-a7Dnh8G4.js → useEventHooks-BNZky4xM.js} +2 -2
  254. package/dashboard/dist/assets/{useEventHooks-a7Dnh8G4.js.map → useEventHooks-BNZky4xM.js.map} +1 -1
  255. package/dashboard/dist/assets/{useNamespace-BSwEV0e9.js → useNamespace-Dx81_JM0.js} +2 -2
  256. package/dashboard/dist/assets/{useNamespace-BSwEV0e9.js.map → useNamespace-Dx81_JM0.js.map} +1 -1
  257. package/dashboard/dist/assets/{useYamlActivityEvents-BuqvL-cn.js → useYamlActivityEvents-D5T3TbE7.js} +2 -2
  258. package/dashboard/dist/assets/{useYamlActivityEvents-BuqvL-cn.js.map → useYamlActivityEvents-D5T3TbE7.js.map} +1 -1
  259. package/dashboard/dist/assets/users-BEMysNdR.js +2 -0
  260. package/dashboard/dist/assets/users-BEMysNdR.js.map +1 -0
  261. package/dashboard/dist/assets/{vendor-icons-D5ycKPA2.js → vendor-icons--Ivmp-RX.js} +132 -112
  262. package/dashboard/dist/assets/vendor-icons--Ivmp-RX.js.map +1 -0
  263. package/dashboard/dist/assets/{workflows-DF-9Voyi.js → workflows-C4rjvQbI.js} +2 -2
  264. package/dashboard/dist/assets/{workflows-DF-9Voyi.js.map → workflows-C4rjvQbI.js.map} +1 -1
  265. package/dashboard/dist/assets/{yaml-workflows-2roKaSSK.js → yaml-workflows-4rJ5v5rJ.js} +2 -2
  266. package/dashboard/dist/assets/{yaml-workflows-2roKaSSK.js.map → yaml-workflows-4rJ5v5rJ.js.map} +1 -1
  267. package/dashboard/dist/index.html +3 -3
  268. package/docs/api/http/escalations.md +131 -10
  269. package/docs/api/http/roles.md +44 -9
  270. package/docs/api/http/users.md +15 -6
  271. package/docs/api/mcp/admin.md +8 -2
  272. package/docs/api/mcp/human-queue.md +34 -2
  273. package/docs/api/sdk/escalations.md +155 -2
  274. package/docs/api/sdk/roles.md +11 -0
  275. package/docs/api/sdk/users.md +26 -1
  276. package/docs/auth.md +1 -1
  277. package/docs/dashboard.md +5 -4
  278. package/docs/data.md +9 -2
  279. package/docs/faceted-routing.md +221 -0
  280. package/docs/hitl-guide.md +50 -0
  281. package/docs/iam.md +31 -0
  282. package/docs/mcp.md +8 -0
  283. package/package.json +2 -2
  284. package/dashboard/dist/assets/AdminDashboard-LTiYLuzq.js +0 -2
  285. package/dashboard/dist/assets/AgentDetailPage-BSUnDvGp.js.map +0 -1
  286. package/dashboard/dist/assets/AgentsPage-DpS0cCYe.js +0 -2
  287. package/dashboard/dist/assets/AvailableEscalationsPage-BEylRqjP.js +0 -2
  288. package/dashboard/dist/assets/AvailableEscalationsPage-BEylRqjP.js.map +0 -1
  289. package/dashboard/dist/assets/CapabilitiesPage-DLIleX2L.js +0 -2
  290. package/dashboard/dist/assets/CapabilitiesPage-DLIleX2L.js.map +0 -1
  291. package/dashboard/dist/assets/CopyableId-DaT0ZRHg.js +0 -2
  292. package/dashboard/dist/assets/CopyableId-DaT0ZRHg.js.map +0 -1
  293. package/dashboard/dist/assets/DataTable-D9yuBv0w.js.map +0 -1
  294. package/dashboard/dist/assets/FilterBar-Ck4K4rzu.js +0 -2
  295. package/dashboard/dist/assets/FilterBar-Ck4K4rzu.js.map +0 -1
  296. package/dashboard/dist/assets/GraphInvokePage-BZBPBCSq.js +0 -2
  297. package/dashboard/dist/assets/GraphInvokePage-BZBPBCSq.js.map +0 -1
  298. package/dashboard/dist/assets/HomePage-dM7U2tl6.js +0 -2
  299. package/dashboard/dist/assets/HomePage-dM7U2tl6.js.map +0 -1
  300. package/dashboard/dist/assets/McpRunsPage-CPMYWgm_.js +0 -2
  301. package/dashboard/dist/assets/McpRunsPage-CPMYWgm_.js.map +0 -1
  302. package/dashboard/dist/assets/OperatorDashboard-g9o7I925.js +0 -2
  303. package/dashboard/dist/assets/PageHeader-Dk2Pe44Q.js +0 -2
  304. package/dashboard/dist/assets/PageHeader-Dk2Pe44Q.js.map +0 -1
  305. package/dashboard/dist/assets/PriorityBadge-DfQY9St9.js +0 -2
  306. package/dashboard/dist/assets/PriorityBadge-DfQY9St9.js.map +0 -1
  307. package/dashboard/dist/assets/RolePill-Du-aNnjR.js +0 -2
  308. package/dashboard/dist/assets/RolePill-Du-aNnjR.js.map +0 -1
  309. package/dashboard/dist/assets/RolesPage-B2Kdf-L9.js +0 -2
  310. package/dashboard/dist/assets/RolesPage-B2Kdf-L9.js.map +0 -1
  311. package/dashboard/dist/assets/RowActions-Dg-Fsm5O.js +0 -2
  312. package/dashboard/dist/assets/RowActions-Dg-Fsm5O.js.map +0 -1
  313. package/dashboard/dist/assets/StickyPagination-BWhFSr2d.js +0 -2
  314. package/dashboard/dist/assets/StickyPagination-BWhFSr2d.js.map +0 -1
  315. package/dashboard/dist/assets/TimeAgo-CUIYqSt_.js +0 -2
  316. package/dashboard/dist/assets/TopicDetailPage-rh_dsTJI.js.map +0 -1
  317. package/dashboard/dist/assets/TopicsPage-CD_40_v-.js +0 -2
  318. package/dashboard/dist/assets/TopicsPage-CD_40_v-.js.map +0 -1
  319. package/dashboard/dist/assets/WorkflowsDashboard-BgI-iOP6.js +0 -2
  320. package/dashboard/dist/assets/WorkflowsDashboard-BgI-iOP6.js.map +0 -1
  321. package/dashboard/dist/assets/escalation-columns-nVYVt7Gl.js +0 -2
  322. package/dashboard/dist/assets/escalation-columns-nVYVt7Gl.js.map +0 -1
  323. package/dashboard/dist/assets/index-CMviPjXQ.js +0 -9
  324. package/dashboard/dist/assets/index-CMviPjXQ.js.map +0 -1
  325. package/dashboard/dist/assets/index-CP0JEQhZ.js +0 -6
  326. package/dashboard/dist/assets/index-CP0JEQhZ.js.map +0 -1
  327. package/dashboard/dist/assets/index-ClvYSNZd.css +0 -1
  328. package/dashboard/dist/assets/index-D1xVY4JR.js.map +0 -1
  329. package/dashboard/dist/assets/index-DPLy-jwO.js +0 -2
  330. package/dashboard/dist/assets/index-DPLy-jwO.js.map +0 -1
  331. package/dashboard/dist/assets/index-DzVYjoex.js +0 -6
  332. package/dashboard/dist/assets/index-DzVYjoex.js.map +0 -1
  333. package/dashboard/dist/assets/index-FuhiJuHL.js +0 -2
  334. package/dashboard/dist/assets/index-FuhiJuHL.js.map +0 -1
  335. package/dashboard/dist/assets/users-Pn85K_3C.js +0 -2
  336. package/dashboard/dist/assets/users-Pn85K_3C.js.map +0 -1
  337. package/dashboard/dist/assets/vendor-icons-D5ycKPA2.js.map +0 -1
@@ -2,6 +2,15 @@
2
2
 
3
3
  Manage human-in-the-loop escalations -- list, claim, resolve, and bulk-operate on workflow escalations.
4
4
 
5
+ ## Work-Surface Scope
6
+
7
+ Escalation methods enforce the caller's role work-surface scope server-side; the SDK method signatures are unchanged. For a `member`:
8
+
9
+ - `read_scope` governs **search** — `list`, `listAvailable`, `findByMetadata`, `getStats`, and a single `get` return only the escalations the member is allowed to see. `read_scope=self` limits this to items assigned to the member; `read_scope=all` exposes the whole role queue.
10
+ - `write_scope` governs **claim / ack (resolve) / delete (cancel)** — a member with `write_scope=self` may only `claim`, `resolve`, and `cancel` items already assigned to them. `release`, `escalate`, and `create` (standalone) are queue-management verbs and require `write_scope=all`.
11
+
12
+ `admin` and `superadmin` ignore scope and act on the whole queue. Scope is set when a role is assigned — see [`lt.users.addRole`](users.md) and [Roles API — Work-Surface Scope](../http/roles.md).
13
+
5
14
  ## create
6
15
 
7
16
  Create an escalation. The caller must hold the target role or be a superadmin.
@@ -52,6 +61,18 @@ const result = await lt.escalations.list({
52
61
  role: 'reviewer',
53
62
  limit: 25,
54
63
  });
64
+
65
+ // Faceted query — a "facet" is a key/value INSIDE the row's metadata JSONB. The
66
+ // filter AND the count run in SQL, role-scoped; nothing is filtered client-side.
67
+ const faceted = await lt.escalations.list({
68
+ status: 'pending',
69
+ facets: { flags: 'too_short' }, // metadata @> { flags: 'too_short' }
70
+ range: [{ facet: 'confidence', op: '<=', value: 0.7 }],
71
+ block: [{ outcome: 'success' }], // exclude completed
72
+ exists: ['needsReview'],
73
+ orderBy: [{ field: 'metadata.confidence', numeric: true, direction: 'asc' }],
74
+ limit: 50,
75
+ });
55
76
  ```
56
77
 
57
78
  **Parameters:**
@@ -60,14 +81,25 @@ const result = await lt.escalations.list({
60
81
  |-------|------|----------|-------------|
61
82
  | `status` | `string` | No | Filter by `pending`, `resolved`, or `cancelled` |
62
83
  | `role` | `string` | No | Filter by assigned role |
84
+ | `roles` | `string[]` | No | Restrict to these roles (`role = ANY`) — narrows within the caller's scope, never widens past it |
63
85
  | `type` | `string` | No | Filter by workflow type |
64
86
  | `subtype` | `string` | No | Filter by subtype |
65
87
  | `assigned_to` | `string` | No | Filter by assigned user ID |
66
88
  | `priority` | `number` | No | Filter by priority (1--4) |
89
+ | `facets` | `Record<string, any>` | No | Required metadata facets — `metadata @> facets` (AND, GIN-served). `{ k: v }` means `metadata.k == v` for a top-level scalar; for nested/arrays it is JSONB **containment** |
90
+ | `block` | `Record<string, any>[]` | No | Exclude rows whose metadata contains ANY of these facet sets — `NOT (metadata @> ANY(block))` |
91
+ | `range` | `{ facet, op, value }[]` | No | Numeric range over a metadata facet, e.g. `{ facet: 'confidence', op: '<=', value: 0.7 }` |
92
+ | `exists` | `string[]` | No | Metadata keys that must be present — `metadata ? key` |
93
+ | `available` | `boolean` | No | `true` = unclaimed/expired only; `false` = held now |
67
94
  | `limit` | `number` | No | Max results (default: 50) |
68
95
  | `offset` | `number` | No | Pagination offset |
69
96
  | `sort_by` | `string` | No | Column to sort by (e.g. `created_at`, `priority`) |
70
97
  | `order` | `string` | No | `asc` or `desc` |
98
+ | `orderBy` | `{ field, direction?, numeric? }[]` | No | Multi-key sort over columns or a metadata path written `metadata.<key>` (set `numeric` for numeric sort) |
99
+
100
+ When any faceted element (`facets`/`block`/`range`/`exists`/`roles`/`available`/`orderBy`) is
101
+ present the request runs through the scoped faceted query; otherwise the simple list path is used.
102
+ See [Faceted Routing — the human / operations query](../../faceted-routing.md#the-human--operations-query).
71
103
 
72
104
  **Returns:** `LTApiResult<{ escalations, total }>`
73
105
 
@@ -99,6 +131,9 @@ const result = await lt.escalations.listAvailable({
99
131
  | `sort_by` | `string` | No | Column to sort by |
100
132
  | `order` | `string` | No | `asc` or `desc` |
101
133
 
134
+ Also accepts the same faceted parameters as [`list`](#list) (`facets`, `block`, `range`,
135
+ `exists`, `roles`, `orderBy`), pinned to the available pool.
136
+
102
137
  **Returns:** `LTApiResult<{ escalations, total }>`
103
138
 
104
139
  **Auth:** Required
@@ -259,6 +294,7 @@ Supports two resolution paths: signal-routed (sends payload to a paused workflow
259
294
  const result = await lt.escalations.resolve({
260
295
  id: 'esc_123',
261
296
  resolverPayload: { approved: true, comment: 'Looks good' },
297
+ metadata: { outcome: 'approved', reviewedBy: 'alice', durationMs: 1_240 },
262
298
  });
263
299
  ```
264
300
 
@@ -267,7 +303,8 @@ const result = await lt.escalations.resolve({
267
303
  | Field | Type | Required | Description |
268
304
  |-------|------|----------|-------------|
269
305
  | `id` | `string` | Yes | Escalation UUID |
270
- | `resolverPayload` | `Record<string, any>` | Yes | Human decision data |
306
+ | `resolverPayload` | `Record<string, any>` | Yes | Human decision data — resumes the paused workflow; not indexed |
307
+ | `metadata` | `Record<string, any>` | No | Outcome facets merged into the row's GIN-indexed metadata. Records *what happened* (disposition, timing) next to *what was asked*; `@>`-queryable. See [Recording the outcome](#recording-the-outcome-on-resolve) |
271
308
 
272
309
  **Returns:** `LTApiResult<{ signaled, escalationId, workflowId }>` (signal path) or `LTApiResult<{ started, escalationId, workflowId }>` (re-run path) -- returns 404 if not found, 409 if not pending.
273
310
 
@@ -275,6 +312,35 @@ const result = await lt.escalations.resolve({
275
312
 
276
313
  ---
277
314
 
315
+ ## Recording the outcome on resolve
316
+
317
+ Every resolve path — `resolve`, `resolveBySignalKey`, the HTTP routes, the MCP tools, and the
318
+ in-process `EscalationService.resolveEscalation` — takes an optional `metadata` patch. It is
319
+ merged, not replaced, into the row's GIN-indexed metadata, recording the **outcome** on the
320
+ same row that carried the **intent**.
321
+
322
+ ```typescript
323
+ await lt.escalations.resolve({
324
+ id: 'esc_123',
325
+ resolverPayload: { approved: true }, // resumes the workflow; not indexed
326
+ metadata: { outcome: 'approved', durationMs: 1_240 }, // recorded on the row; @>-queryable
327
+ });
328
+ ```
329
+
330
+ The patch is distinct from `resolverPayload`: the payload is delivered to the waiting workflow
331
+ as `condition()`'s return value; the patch is the durable, queryable record. Intent and
332
+ outcome live on one row — no side table:
333
+
334
+ ```typescript
335
+ await lt.escalations.findByMetadata({ key: 'outcome', value: 'approved' });
336
+ // every resolved row that was approved — with its disposition and duration
337
+ ```
338
+
339
+ The in-process library takes the same patch as a third argument:
340
+ `resolveEscalation(id, payload, metadata)` and `resolveEscalationBySignalKey(signalKey, payload, metadata)`.
341
+
342
+ ---
343
+
278
344
  ## conditionLT (workflow helper)
279
345
 
280
346
  Wait for a signal and automatically resolve the associated escalation. This is the counterpart to `executeLT` — where `executeLT` wraps `startChild` + `condition`, `conditionLT` wraps `condition` + escalation resolution.
@@ -283,6 +349,16 @@ Wait for a signal and automatically resolve the associated escalation. This is t
283
349
  conditionLT<T>(signalId: string, escalation?: ConditionQueueConfig): Promise<T | false | null>
284
350
  ```
285
351
 
352
+ ### Two ways to pause on an escalation
353
+
354
+ There are two ways to make a workflow pause as a claimable escalation, and they are not equivalent in cost:
355
+
356
+ - **Native `condition(signalId, escalationConfig)` — the efficient primitive.** HotMesh's `condition` takes an optional escalation config as its second argument. The row is written inside the workflow's Leg1 checkpoint, with `signal_key = signalId`. Resolving it (`resolve` / `resolveBySignalKey`) marks the row resolved **and** delivers the signal in one guarded transaction, resuming the job in place. No create activity, no enrich step, and **no proxy-activity round-trip on the resume** — the resolve is the whole transaction. This is the path to prefer.
357
+
358
+ - **`conditionLT(signalId, config?)` — long-tail sugar.** With a config it delegates to the native efficient `condition` above (same atomic behavior — use it freely). Without a config it also supports the older **two-step** pattern: an escalation created separately, where the resume injects `$escalation_id` and `conditionLT` resolves it through a durable `proxyActivity` (`ltResolveEscalation`). That extra activity round-trip is the cost of the two-step form; the efficient form (and native `condition`) avoid it.
359
+
360
+ Reach for native `condition(signalId, config)` when you want the leanest path; reach for `conditionLT` for the ergonomic wrapper or to support the legacy two-step flow. Both resume the same row, and both accept the resolve-time `metadata` patch (the efficient path merges it in the single guarded UPDATE; the two-step path forwards it through `ltResolveEscalation` into that same atomic resolve).
361
+
286
362
  ### Atomic form (recommended)
287
363
 
288
364
  Pass an escalation config as the second argument. The escalation row is written inside the workflow's Leg1 checkpoint — one commit, crash-safe: no separate `ltCreateEscalation` activity, no enrich step. `signal_key` is set to `signalId`, so the dashboard resolve endpoint (resolve-by-id → Path 0) and `POST /escalations/resolve-by-signal-key` resume *this* job in place, and `system.escalation.{id}.created` fires automatically.
@@ -662,7 +738,7 @@ const result = await lt.escalations.claimByMetadata({
662
738
  | `metadata` | `object` | No | Merge into escalation metadata (single atomic SQL call with the claim) |
663
739
  | `provisionIfAbsent` | `object` | No | JIT-provision the assignee if they don't exist or lack the required role (superadmin only) |
664
740
 
665
- `provisionIfAbsent` accepts `{ displayName?, email?, roles?: [{ role, type? }] }`. Only callers with global escalation access can use this flag. The happy path (user exists, has role) adds zero extra queries.
741
+ `provisionIfAbsent` accepts `{ displayName?, email?, roles?: [{ role, type?, read_scope?, write_scope? }] }`. Each role entry forwards the optional work-surface scope fields `read_scope` (`self` or `all`, default `all`) and `write_scope` (`none`, `self`, or `all`, default `all`), subject to the **write ⊆ read** constraint; scope is ignored for `admin`/`superadmin`. To JIT-provision a one-time user who sees and acts on exactly the item being claimed, provision them `read_scope: 'self'` + `write_scope: 'self'`. Only callers with global escalation access can use this flag. The happy path (user exists, has role) adds zero extra queries.
666
742
 
667
743
  **Returns:** `LTApiResult<{ escalation, isExtension }>` -- 404 if no match, 409 if already claimed.
668
744
 
@@ -707,3 +783,80 @@ const result = await lt.escalations.resolveByMetadata({
707
783
  **Returns:** `LTApiResult<{ escalation }>` for non-signal, `LTApiResult<{ signaled, escalationId, workflowId }>` for signal-backed. 404 if no match.
708
784
 
709
785
  **Auth:** Required
786
+
787
+ ## resolveByIds
788
+
789
+ Resolve a set of escalations by id in one guarded statement (the set-based sibling of `resolve`). For bookkeeping rows woken collectively — it does not deliver a per-row signal. RBAC: a scoped caller may only resolve rows whose role they hold.
790
+
791
+ ```typescript
792
+ const result = await lt.escalations.resolveByIds({
793
+ ids: ['esc_1', 'esc_2', 'esc_3'],
794
+ resolverPayload: { printerId: 'p-7' },
795
+ metadata: { outcome: 'settled' },
796
+ });
797
+ ```
798
+
799
+ | Field | Type | Required | Description |
800
+ |-------|------|----------|-------------|
801
+ | `ids` | `string[]` | Yes | Escalation ids to resolve as one set |
802
+ | `resolverPayload` | `Record<string, any>` | Yes | Payload applied to every row |
803
+ | `metadata` | `Record<string, any>` | No | Outcome patch merged into each row |
804
+
805
+ **Returns:** `LTApiResult<{ resolved: number; escalationIds: string[] }>` — only still-`pending` rows are resolved.
806
+
807
+ ## resolveBySignalKey
808
+
809
+ Resolve an efficient (atomic) escalation directly by its `signal_key` and resume the waiting workflow in place. For callers that know the deterministic signal id and want to skip the id lookup. RBAC-scoped to the escalation's role.
810
+
811
+ ```typescript
812
+ const result = await lt.escalations.resolveBySignalKey({
813
+ signalKey: 'signal-scan-ar-order-42',
814
+ resolverPayload: { approved: true },
815
+ });
816
+ ```
817
+
818
+ **Returns:** `LTApiResult<{ signaled: true; escalationId; workflowId }>`.
819
+
820
+ ## searchByFacets
821
+
822
+ Item-level faceted search over a single pond `role`, scoped to the caller's role. The faceted-routing read primitive.
823
+
824
+ ```typescript
825
+ const result = await lt.escalations.searchByFacets({
826
+ role: 'printer-pool-diabetic',
827
+ status: 'pending',
828
+ available: true,
829
+ facets: { state: 'ready' },
830
+ limit: 50,
831
+ });
832
+ ```
833
+
834
+ **Returns:** `LTApiResult<{ escalations; total }>`.
835
+
836
+ ## claimGroups
837
+
838
+ Batch-claim complete origin groups (e.g. all units of an order) in priority order over a pond, assigned to the caller. RBAC-scoped to the pond role.
839
+
840
+ ```typescript
841
+ const result = await lt.escalations.claimGroups({
842
+ query: { role: 'print-farm-diabetic', available: true, facets: { filament: 'pla', size_class: 'standard' } },
843
+ limit: 4,
844
+ durationMinutes: 30,
845
+ sizeFacet: 'order_size',
846
+ });
847
+ ```
848
+
849
+ **Returns:** `LTApiResult<{ groups }>`.
850
+
851
+ ## claimByFacets
852
+
853
+ Batch-claim individual rows matching a facet query (`FOR UPDATE SKIP LOCKED`), assigned to the caller. With `allOrNone`, commits only when the full `limit` is acquired. RBAC-scoped to the pond role.
854
+
855
+ ```typescript
856
+ const result = await lt.escalations.claimByFacets({
857
+ query: { role: 'printer-pool-diabetic', facets: { state: 'ready', filament: 'pla' } },
858
+ limit: 3,
859
+ });
860
+ ```
861
+
862
+ **Returns:** `LTApiResult<{ claimed }>`.
@@ -2,6 +2,17 @@
2
2
 
3
3
  Manage roles and escalation chain routing between roles.
4
4
 
5
+ ## Work-Surface Scope
6
+
7
+ A role is a task queue worked by its members. Each `member` assignment carries two work-surface scope axes that set how much of the queue that member touches:
8
+
9
+ - `read_scope` (`self` or `all`, default `all`) governs **search** — which escalations the member sees.
10
+ - `write_scope` (`none`, `self`, or `all`, default `all`) governs **claim / ack (resolve) / delete (cancel)** — which escalations the member may act on.
11
+
12
+ `self` means items assigned to the member; `all` means the whole role queue. The constraint is **write ⊆ read** — `write_scope=all` requires `read_scope=all`. `admin` and `superadmin` ignore scope and always act on the whole queue. The default `all`/`all` is the full-queue worker.
13
+
14
+ Scope is set when a role is assigned to a user, via [`lt.users.addRole`](users.md) and [`lt.users.create`](users.md). See [Roles API — Work-Surface Scope](../http/roles.md) for the five member profiles.
15
+
5
16
  ## list
6
17
 
7
18
  List all distinct role names in the system.
@@ -70,9 +70,19 @@ const result = await lt.users.create({
70
70
  | `external_id` | `string` | Yes | External system identifier |
71
71
  | `email` | `string` | No | Email address |
72
72
  | `display_name` | `string` | No | Display name |
73
- | `roles` | `{ role: string; type: string }[]` | No | Initial role assignments (type: `superadmin`, `admin`, or `member`) |
73
+ | `roles` | `{ role: string; type: string; read_scope?: string; write_scope?: string }[]` | No | Initial role assignments (type: `superadmin`, `admin`, or `member`) |
74
74
  | `metadata` | `Record<string, any>` | No | Arbitrary key-value metadata |
75
75
 
76
+ Each role entry forwards the optional work-surface scope fields `read_scope` (`self` or `all`, default `all`) and `write_scope` (`none`, `self`, or `all`, default `all`) to the API. Scope refines a `member` grant and is ignored for `admin`/`superadmin`. The constraint is **write ⊆ read** — `write_scope=all` requires `read_scope=all`. The default `all`/`all` is the full-queue worker. See [Roles API — Work-Surface Scope](../http/roles.md) for the five member profiles.
77
+
78
+ ```typescript
79
+ // A one-time user who sees and acts only on their own pre-assigned item
80
+ const result = await lt.users.create({
81
+ external_id: 'new-user',
82
+ roles: [{ role: 'customer-triage', type: 'member', read_scope: 'self', write_scope: 'self' }],
83
+ });
84
+ ```
85
+
76
86
  **Returns:** `LTApiResult<User>` (status 201) -- returns 409 if `external_id` already exists.
77
87
 
78
88
  **Auth:** Not required
@@ -166,6 +176,21 @@ const result = await lt.users.addRole({
166
176
  | `id` | `string` | Yes | User UUID |
167
177
  | `role` | `string` | Yes | Role name to assign |
168
178
  | `type` | `string` | Yes | Role type (`superadmin`, `admin`, or `member`) |
179
+ | `read_scope` | `string` | No | `self` or `all` (default `all`). Search breadth for a `member`; ignored for admin/superadmin |
180
+ | `write_scope` | `string` | No | `none`, `self`, or `all` (default `all`). Claim/ack/delete breadth for a `member` |
181
+
182
+ `read_scope` and `write_scope` are the work-surface scope axes for a `member` grant: `read_scope` governs which escalations the member sees in search; `write_scope` governs which they may claim, ack (resolve), or delete (cancel). `self` means items assigned to the member; `all` means the whole role queue. The constraint is **write ⊆ read** — `write_scope=all` requires `read_scope=all`. Both default to `all` (full-queue worker), and both are ignored for `admin`/`superadmin`, which always act on the whole queue. The returned role object includes `read_scope` and `write_scope`. See [Roles API — Work-Surface Scope](../http/roles.md) for the five member profiles.
183
+
184
+ ```typescript
185
+ // See the whole queue, act only on own items (e.g. a chat-style room)
186
+ const result = await lt.users.addRole({
187
+ id: 'user_123',
188
+ role: 'reviewer',
189
+ type: 'member',
190
+ read_scope: 'all',
191
+ write_scope: 'self',
192
+ });
193
+ ```
169
194
 
170
195
  **Returns:** `LTApiResult<UserRole>` (status 201)
171
196
 
package/docs/auth.md CHANGED
@@ -239,7 +239,7 @@ When SSO is configured and a request arrives without a Bearer token, `requireAut
239
239
 
240
240
  Roles are resolved once during the token exchange (not per-request) and baked into the JWT — the same pattern as the built-in login. When the host system changes a user's roles, the new roles take effect on the next token refresh.
241
241
 
242
- If `roleMap` is provided, only mapped roles are assigned. If omitted, host role names are passed through directly as LT role names. Roles named `superadmin` or `admin` are assigned the corresponding role type; all others default to `member`.
242
+ If `roleMap` is provided, only mapped roles are assigned. If omitted, host role names are passed through directly as LT role names. Roles named `superadmin` or `admin` are assigned the corresponding role type; all others default to `member`. Provisioned `member` grants take the default work-surface scope of `read_all`/`write_all` — the full-queue worker. Narrower scopes (for one-time or read-only users) are set through the [Roles API](api/http/roles.md#work-surface-scope) or the dashboard Scope picker.
243
243
 
244
244
  ### Standalone Deployments
245
245
 
package/docs/dashboard.md CHANGED
@@ -187,18 +187,19 @@ Lists all durable workflow runs across the system.
187
187
 
188
188
  User Accounts and Service Accounts live on the same page, separated by a tab toggle.
189
189
 
190
- - **User Accounts** — human operators. Create users, assign display names, and grant roles. Roles determine which escalations a user can see and claim, and which workflows they can invoke from the dashboard.
190
+ - **User Accounts** — human operators. Create users, assign display names, and grant roles. Roles determine which escalations a user can see and claim, and which workflows they can invoke from the dashboard. A `member` grant carries a work-surface scope (read/write breadth) chosen from the Scope picker; see [Roles](#roles).
191
191
  - **Service Accounts** — programmatic callers (bots, CI pipelines, external systems). Each service account has an API key for authentication. Assign roles to control access just like human users. Service accounts with the `reviewer` role can claim and resolve escalations programmatically.
192
192
  - **Role assignment** — both account types participate in the same role system. Click any account to edit roles, change display name, or manage credentials.
193
193
 
194
194
  **API:** `GET /api/users` lists accounts. `POST /api/users` creates. `PUT /api/users/:id/roles` assigns roles.
195
195
 
196
- ### Roles and Permissions
196
+ ### Roles
197
197
 
198
198
  Define roles and configure escalation chains that control how work flows between teams.
199
199
 
200
200
  - **Role list** — all roles in the system with their type (admin, operator, custom). Click to view assigned users.
201
201
  - **Create Role** — add a new role. Roles referenced in workflow configs are auto-created, but you can also create them here for organizational clarity.
202
+ - **Scope picker** — when granting a role at `member` type, a Scope picker offers the five named work-surface profiles: full worker (`all`/`all`, default), see-all-act-own (`all`/`self`), own-items-only (`self`/`self`), read-only auditor (`all`/`none`), and read-only own (`self`/`none`). `admin` and `superadmin` grants show no Scope picker — they always work the whole queue. The picker enforces **write ⊆ read**, so a write breadth wider than the read breadth cannot be selected.
202
203
  - **Escalation chains** — define source → target role mappings. When a reviewer escalates, the chain determines which roles receive the escalation next. Chains are directional (reviewer → engineer → admin) and support multiple targets per source.
203
204
 
204
205
  **API:** `GET /api/roles` lists roles. `POST /api/roles` creates. `GET /api/roles/escalation-chains` lists chains. `POST /api/roles/escalation-chains` adds a chain.
@@ -245,8 +246,8 @@ The central queue for all escalation activity across every workflow.
245
246
 
246
247
  - **Filter bar** — filter by status (pending/claimed/resolved), role, workflow type, priority, and time window.
247
248
  - **Columns:** Escalation ID, workflow type, role, status, priority, created time, and claimed-by user.
248
- - **Claim** — click the claim action to lock an escalation to your user. Only users with matching roles see pending escalations.
249
- - **Resolve** — after claiming, submit a resolver payload (pre-filled from the workflow's `resolver_schema` if configured). Resolution triggers a workflow re-run with the resolver data injected.
249
+ - **Claim** — click the claim action to lock an escalation to your user. Only users with matching roles see pending escalations. The queue list and aggregate stats reflect `read_all` memberships — a member scoped to `read_self` lands directly on their own assigned item in user mode rather than browsing the full queue.
250
+ - **Resolve** — after claiming, submit a resolver payload (pre-filled from the workflow's `resolver_schema` if configured). Resolution triggers a workflow re-run with the resolver data injected. A `member` whose `write_scope` is `self` can resolve only items already assigned to them; `write_scope=none` is read-only.
250
251
  - **Escalate** — forward a claimed escalation to a higher-tier role via the escalation chain.
251
252
 
252
253
  **API:** `GET /api/escalations` lists with filters. `POST /api/escalations/:id/claim` claims. `POST /api/escalations/:id/resolve` resolves.
package/docs/data.md CHANGED
@@ -74,6 +74,11 @@ backward-compatible **view** over it — `SELECT *` plus a computed `available`
74
74
  column — so existing read queries and the public API are unchanged. Indexes are
75
75
  managed by the SDK on `hmsh_escalations` (see below).
76
76
 
77
+ Role read/write scope does **not** add columns here. An escalation carries a `role`
78
+ and an optional `assigned_to`; work-surface scope lives on the membership table
79
+ (`lt_user_roles`) and is applied at read time. `condition()` / `conditionLT()` and
80
+ the escalation engine are unaffected.
81
+
77
82
  The columns below are the `hmsh_escalations` table. The public API record
78
83
  (`LTEscalationRecord`) is mapped from these: the JSONB `envelope` /
79
84
  `escalation_payload` / `resolver_payload` are serialized to JSON **strings**, and
@@ -179,12 +184,14 @@ Maps users to roles. Each user can hold multiple roles with different permission
179
184
  |--------|------|---------|-------------|
180
185
  | `user_id` | `UUID NOT NULL` | — | FK to `lt_users(id)`, CASCADE on delete |
181
186
  | `role` | `TEXT NOT NULL` | — | Role name (e.g., `reviewer`, `senior-reviewer`) |
182
- | `type` | `TEXT NOT NULL` | `'member'` | `superadmin`, `admin`, or `member` |
187
+ | `type` | `TEXT NOT NULL` | `'member'` | `superadmin`, `admin`, or `member` — the management tier |
188
+ | `read_scope` | `TEXT NOT NULL` | `'all'` | `self` or `all` — search breadth for a `member`: which escalations in the role queue the member sees. `self` = items where `assigned_to = user`; `all` = the whole queue. Ignored for `admin`/`superadmin`, which always see the whole queue. |
189
+ | `write_scope` | `TEXT NOT NULL` | `'all'` | `none`, `self`, or `all` — claim/ack/delete breadth for a `member`. `none` = read-only; `self` = items assigned to the member; `all` = the whole queue. Ignored for `admin`/`superadmin`. |
183
190
  | `created_at` | `TIMESTAMPTZ NOT NULL` | `NOW()` | When the role was assigned |
184
191
 
185
192
  Primary key: `(user_id, role)` — a user can hold each role at most once.
186
193
 
187
- Type is enforced by a CHECK constraint: `type IN ('superadmin', 'admin', 'member')`.
194
+ Type is enforced by a CHECK constraint: `type IN ('superadmin', 'admin', 'member')`. Scope is enforced by CHECK constraints: `read_scope IN ('self', 'all')`, `write_scope IN ('none', 'self', 'all')`, and **write ⊆ read** — `write_scope = 'all'` requires `read_scope = 'all'` (a member cannot act on what it cannot see). Both scopes default to `all`, the full-queue worker.
188
195
 
189
196
  ### lt_bot_api_keys
190
197
 
@@ -0,0 +1,221 @@
1
+ # Faceted Routing
2
+
3
+ The escalation queue is more than a HITL inbox — it is a durable, queryable pool of work
4
+ that any number of consumers can search and atomically claim by its **facets**. Faceted
5
+ routing is the platform surface for that: a composable query language over the queue and a
6
+ set of atomic claim primitives. The platform supplies the surface; the *policy* — what a
7
+ facet means, how capability and priority map onto it, the matching loop — is the consuming
8
+ app's. The [print-routing example](../examples/workflows/print-routing/README.md) is a full
9
+ worked policy built entirely on these primitives.
10
+
11
+ ## Contents
12
+
13
+ - [The model](#the-model)
14
+ - [The query — `FacetQuery`](#the-query--facetquery)
15
+ - [Reads](#reads)
16
+ - [Atomic claims](#atomic-claims)
17
+ - [The dispatcher pattern](#the-dispatcher-pattern)
18
+ - [Recording the outcome on resolve](#recording-the-outcome-on-resolve)
19
+ - [Safety](#safety)
20
+
21
+ ## The model
22
+
23
+ Every escalation row carries a `role` (the hard isolation boundary) and a `metadata` JSONB
24
+ object (the soft facets a late-binding consumer queries, sorts, and claims by). Routing
25
+ work is a funnel over those columns:
26
+
27
+ - **Capability** — the hard wall — is the `role`. A consumer bound to one role can never
28
+ see another role's work. Soft capability is `metadata @>` containment.
29
+ - **Preference / priority** — ordering — is `orderBy` over top-level columns and facets.
30
+ - **Capacity** — how much to take — is the page `limit` of a claim.
31
+
32
+ An **order** (or any multi-unit job) is a set of rows sharing an `origin_id`; its unit
33
+ count lives in a metadata facet so a group can be claimed all-or-nothing only when complete.
34
+
35
+ ## The query — `FacetQuery`
36
+
37
+ ```typescript
38
+ interface FacetQuery {
39
+ role?: string; // hard isolation (exact)
40
+ roles?: string[]; // role = ANY(...)
41
+ facets?: Record<string, any>; // metadata @> facets — required (AND), GIN-served
42
+ block?: Record<string, any>[]; // NOT (metadata @> ANY(block)) — exclusion list
43
+ range?: { facet: string; op: '<' | '<=' | '>' | '>=' | '='; value: number }[];
44
+ exists?: string[]; // metadata ? key — facet must be present
45
+ status?: string; // e.g. 'pending'
46
+ available?: boolean; // true = unclaimed/expired only; false = held now
47
+ orderBy?: { field: string; direction?: 'asc' | 'desc'; numeric?: boolean }[];
48
+ limit?: number;
49
+ offset?: number;
50
+ }
51
+ ```
52
+
53
+ `orderBy.field` is either a whitelisted top-level column (`priority`, `created_at`,
54
+ `updated_at`, `status`, `role`) or a metadata path written as `metadata.<key>` (extracted as
55
+ text, or numeric when `numeric` is set). Every value is parameterized; only validated column
56
+ names / metadata keys and a fixed operator set are ever interpolated.
57
+
58
+ ```typescript
59
+ // Pending, available diabetic-certified work needing PLA, soonest-deadline first.
60
+ const query: FacetQuery = {
61
+ role: 'print-farm-diabetic',
62
+ status: 'pending',
63
+ available: true,
64
+ facets: { filament: 'pla' },
65
+ orderBy: [{ field: 'metadata.mustCompleteBy', numeric: true, direction: 'asc' }],
66
+ };
67
+ ```
68
+
69
+ ## Reads
70
+
71
+ ```typescript
72
+ import { searchByFacets, searchGroups, countByFacets } from '@hotmeshio/long-tail';
73
+
74
+ // Item-level: filter/sort over columns and metadata facets.
75
+ const { escalations, total } = await searchByFacets(query);
76
+
77
+ // Order-level: each row is an origin group with its unit count, availability and
78
+ // completeness — what a batched dispatcher reads to page by capacity.
79
+ const groups = await searchGroups(query, { sizeFacet: 'orderSize', limit: 10 });
80
+
81
+ // Aggregate: capacity / in-flight soft-limit checks.
82
+ const inFlight = await countByFacets({ role: 'print-farm-diabetic', available: false });
83
+ ```
84
+
85
+ ## Atomic claims
86
+
87
+ Two claim primitives, both `FOR UPDATE SKIP LOCKED` so many consumers run without
88
+ contention. Each takes the `FacetQuery`, a `consumer` id, and options.
89
+
90
+ ```typescript
91
+ import { claimGroups, claimByFacets } from '@hotmeshio/long-tail';
92
+
93
+ // Claim whole orders (origin groups), all-or-nothing, in rank order. The page `limit`
94
+ // is how many consumers/printers are free, so a dependent grabs exactly what it can
95
+ // distribute. A group is only eligible when complete (member count = declared size).
96
+ const orders = await claimGroups(query, 'broker-1', { limit: 4, sizeFacet: 'orderSize' });
97
+
98
+ // Claim individual rows by facet — the single-row sibling. Locks up to `limit` available
99
+ // rows in rank order. With { allOrNone: true } the claim commits only when the full limit
100
+ // is acquired, otherwise it rolls back and returns [] — the all-or-none lock a dispatcher
101
+ // wants over a counted set it anticipated.
102
+ const printers = await claimByFacets(
103
+ { role: 'printer-pool-diabetic', facets: { state: 'ready' } },
104
+ 'broker-1',
105
+ { limit: 4 },
106
+ );
107
+ ```
108
+
109
+ A claim sets `assigned_to` + `assigned_until` (default 30 minutes). Availability is a
110
+ query, not a flag: a row is held iff `assigned_to` is set and `assigned_until > NOW()`.
111
+ Resolve, release, or let the claim expire to return it to the pool.
112
+
113
+ ## The dispatcher pattern
114
+
115
+ The primitives compose into a market: pull a *page* of complete orders sized to how many
116
+ consumers are free, then distribute one per consumer. Reads go through the
117
+ `public.lt_escalations` view (it adds the computed `available` flag); atomic claims run on
118
+ the shared `public.hmsh_escalations` table — the same raw-SQL-on-the-shared-table pattern as
119
+ the rest of the escalation service. Metadata facets use the GIN index (`@>`).
120
+
121
+ ```typescript
122
+ // 1. Anticipate supply.
123
+ const free = await searchByFacets({ role: 'printer-pool', available: true, facets: { state: 'ready' } });
124
+ // 2. Claim that much demand by priority.
125
+ const orders = await claimGroups(demandQuery, consumer, { limit: free.escalations.length });
126
+ // 3. Lock the printers for the claimed orders, all-or-none.
127
+ const printers = await claimByFacets(supplyQuery, consumer, { limit: orders.length, allOrNone: true });
128
+ // 4. Pair, do the work, resolve both.
129
+ ```
130
+
131
+ See the [print-routing example](../examples/workflows/print-routing/README.md) for the full
132
+ two-sided market, carry-forward under contention, and the inspection/convergence loop.
133
+
134
+ ## Recording the outcome on resolve
135
+
136
+ A row is created carrying **intent** — the facets that route and claim it. Resolving it can
137
+ record the **outcome** onto the same row: pass a `metadata` patch and it is merged (not
138
+ replaced) into the GIN-indexed metadata in the same atomic UPDATE, on the winning resolve
139
+ only. "What actually happened" becomes `@>`-queryable next to "what was asked".
140
+
141
+ ```typescript
142
+ import { resolveEscalation } from '@hotmeshio/long-tail';
143
+
144
+ // The work is done; resolve the row AND stamp the outcome onto it — one atomic call.
145
+ await resolveEscalation(rowId, { result: 'success' }, {
146
+ outcome: 'success',
147
+ unitsPrinted: 6,
148
+ });
149
+ // Duration needs no facet: the row's own created_at → resolved_at is the elapsed time.
150
+ ```
151
+
152
+ The third argument is distinct from the second: `resolverPayload` (arg 2) is delivered to a
153
+ waiting workflow as `condition()`'s return value and is **not** indexed; the `metadata` patch
154
+ (arg 3) is the durable, queryable record on the row. With both, the row alone answers what was
155
+ asked, what happened, and how long it took:
156
+
157
+ ```typescript
158
+ searchByFacets({ role: 'printer-pool-diabetic', facets: { state: 'printing', outcome: 'success' } });
159
+ // → every completed job: which consumer, which origin, how long — no side table to reconcile
160
+ ```
161
+
162
+ The print farm leans on this: a printer resolves its in-flight `printing` row with the run's
163
+ result and units in one atomic call, so the supply pond is also the production log — and the
164
+ row's `created_at` → `resolved_at` is the print duration, no stored field required.
165
+
166
+ ## The human / operations query
167
+
168
+ The same query language is also the operations team's read surface. Where the dispatcher
169
+ sections above target a **single pond** for a workflow consumer, a **person** — a superadmin, or
170
+ a member of several roles — queries "what work needs doing / was done" across **their** roles by
171
+ the same facets, through the scoped list API.
172
+
173
+ ```typescript
174
+ import { createClient } from '@hotmeshio/long-tail/sdk';
175
+ const lt = createClient({ auth }); // auth carries the caller's userId
176
+
177
+ // Faceted, role-scoped, paginated — the filter AND the count run in SQL.
178
+ const res = await lt.escalations.list({
179
+ status: 'pending',
180
+ facets: { flags: 'too_short' }, // metadata @> (GIN-served)
181
+ range: [{ facet: 'confidence', op: '<=', value: 0.7 }],
182
+ exists: ['needsReview'],
183
+ block: [{ outcome: 'success' }], // exclude completed
184
+ orderBy: [{ field: 'metadata.confidence', numeric: true, direction: 'asc' }],
185
+ limit: 50, offset: 0,
186
+ });
187
+ // res.data → { escalations, total }
188
+ ```
189
+
190
+ `list` and `listAvailable` accept the FacetQuery elements (`facets`, `block`, `range`, `exists`,
191
+ `roles`, `available`, `orderBy`) on top of the plain filters. When any are present the request
192
+ runs through the scoped faceted query; otherwise the simple list path is used.
193
+
194
+ **Role scope is enforced in SQL, not in the controller.** The result is the read-scope predicate
195
+ ANDed with the facets:
196
+
197
+ - **superadmin / admin-of-admin** (global) — every role's queue, no role filter.
198
+ - **`read_all` on a role** — the whole pond for that role.
199
+ - **`read_self` on a role** — only items assigned to the caller (`assigned_to = me`).
200
+
201
+ A caller may **narrow** within their scope (pass `role`/`roles`) but can never **widen** past it —
202
+ a `role` they don't hold simply matches nothing. The page total is computed by the same WHERE, so
203
+ pagination stays correct; the controller never filters a fetched page client-side.
204
+
205
+ Over HTTP the faceted elements are JSON-encoded query params, so the whole query rides on a GET
206
+ URL and the dashboard's copy-URL / copy-curl reproduces the exact request:
207
+
208
+ ```
209
+ GET /api/escalations?status=pending&facets=%7B%22flags%22%3A%22too_short%22%7D&range=%5B%7B%22facet%22%3A%22confidence%22%2C%22op%22%3A%22%3C%3D%22%2C%22value%22%3A0.7%7D%5D
210
+ ```
211
+
212
+ ## Safety
213
+
214
+ - **Parameterized** — every value is a bound parameter; only audited column names, metadata
215
+ keys (strict `^[a-zA-Z0-9_]+$`), and a fixed operator set are interpolated.
216
+ - **Atomic** — claims are single transactions with `FOR UPDATE SKIP LOCKED`; group claims
217
+ re-check completeness *inside* the lock, so concurrent dispatchers never split a group or
218
+ double-claim a row.
219
+ - **Durable consumers** — because the surface is a query, a consumer that holds a claim it
220
+ cannot yet place can carry it across `continueAsNew` and converge later rather than
221
+ releasing — the durable workflow is the coordinator, not a distributed transaction.
@@ -158,6 +158,29 @@ export async function reviewWorkflow(envelope: LTEnvelope) {
158
158
 
159
159
  ---
160
160
 
161
+ ## Recording the Outcome on Resolution
162
+
163
+ An escalation row is created carrying **intent** — what was asked, who it routed to. Resolving it can stamp the **outcome** onto the same row: every resolve surface takes an optional `metadata` patch, merged into the row's GIN-indexed metadata.
164
+
165
+ | Surface | How to pass it |
166
+ |---------|----------------|
167
+ | HTTP | `metadata` in the resolve body (`POST /api/escalations/:id/resolve`, `/resolve-by-signal-key`) |
168
+ | SDK facade | `lt.escalations.resolve({ id, resolverPayload, metadata })` |
169
+ | MCP | `metadata` arg on `claim_and_resolve` / `resolve_escalation` |
170
+ | In-process library | `resolveEscalation(id, payload, metadata)` / `resolveEscalationBySignalKey(signalKey, payload, metadata)` |
171
+
172
+ ```typescript
173
+ await lt.escalations.resolve({
174
+ id,
175
+ resolverPayload: { approved: true }, // resumes the workflow; not indexed
176
+ metadata: { outcome: 'approved', reviewedBy: 'alice', durationMs: elapsed },
177
+ });
178
+ ```
179
+
180
+ The patch is distinct from the resolver payload: the payload resumes the paused workflow and is not indexed; the metadata patch is the durable, queryable record on the row. Use it for the audit trail and analytics — disposition, reviewer, time-to-resolve — so the escalation table answers *what was asked, what was decided, and how long it took* without a parallel log.
181
+
182
+ ---
183
+
161
184
  ## JSON Schema Form Authoring
162
185
 
163
186
  The dashboard renders forms automatically from JSON Schema. No frontend code needed.
@@ -456,6 +479,33 @@ await ltCreateEscalation({
456
479
  });
457
480
  ```
458
481
 
482
+ ### Work-Surface Scope
483
+
484
+ A `member` of a role carries a work-surface scope that narrows what they see and act on within that queue: `read_scope` (`self` | `all`) governs which escalations they see, and `write_scope` (`none` | `self` | `all`) governs which they can claim, resolve, or cancel. `self` means escalations assigned to that member (`assigned_to = user`); `all` means the whole role queue. `admin` and `superadmin` always work the whole queue. See the [Roles API](api/http/roles.md#work-surface-scope) for the five member profiles and the assignment contract.
485
+
486
+ Scope is a property of the **membership** (`lt_user_roles`), not of the escalation. The escalation engine is unchanged: `condition()` / `conditionLT()`, `ltCreateEscalation`, and the interceptor write escalation rows (`hmsh_escalations`) exactly as before — an escalation carries a `role` and an optional `assigned_to`, with no scope column. Scope is resolved at read time, when a *user* lists or acts on the queue. Escalating to a role that does not exist yet still registers the role name only — roles are typeless; `type` and scope are set per user when the role is granted. The one place scope affects creation is the standalone `POST /api/escalations` HTTP endpoint, which requires `write_scope=all`; workflow-emitted escalations (`conditionLT`, interceptor returns, `ltCreateEscalation`) do not pass through that check.
487
+
488
+ ### One-Time and Pre-Assigned Users
489
+
490
+ To route a single item to a named person, assign the escalation to them and provision them with `read_scope=self` + `write_scope=self`. The workflow sets `assigned_to` to the person's user ID (a pre-claim) when it creates the escalation, then provisions or updates that user as a `member` with self/self scope on the target role. They land directly on that one item — a just-in-time form scoped by RBAC, with no access to the rest of the queue and no direct table access. An update or follow-up is simply another workflow firing another escalation to that same person.
491
+
492
+ ```typescript
493
+ // Pre-assign the escalation to a specific person and route a one-time form to them
494
+ await ltCreateEscalation({
495
+ role: 'customer-triage',
496
+ assigned_to: userId, // pre-claim — durable, keyed off the user, not the soft-lock TTL
497
+ description: 'Confirm your shipping address',
498
+ metadata: {
499
+ form_schema: {
500
+ title: 'Confirm Address',
501
+ properties: { address: { type: 'string' }, confirmed: { type: 'boolean' } },
502
+ required: ['confirmed'],
503
+ },
504
+ },
505
+ });
506
+ // The person is provisioned as a member of `customer-triage` with read_scope=self, write_scope=self.
507
+ ```
508
+
459
509
  ### Escalation Chains
460
510
 
461
511
  Users can escalate to other roles via the "Escalate" tab: