@hotmeshio/long-tail 0.19.1 → 0.21.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 (393) hide show
  1. package/README.md +4 -4
  2. package/build/api/escalations/accumulate.d.ts +96 -0
  3. package/build/api/escalations/accumulate.js +516 -0
  4. package/build/api/escalations/index.d.ts +1 -0
  5. package/build/api/escalations/index.js +10 -1
  6. package/build/api/roles.d.ts +4 -5
  7. package/build/api/roles.js +53 -4
  8. package/build/api/scan-codes/execute.js +2 -0
  9. package/build/api/scan-codes/verbs.d.ts +15 -0
  10. package/build/api/scan-codes/verbs.js +65 -0
  11. package/build/api/workflows/config.d.ts +3 -0
  12. package/build/api/workflows/config.js +15 -0
  13. package/build/api/workflows/index.d.ts +1 -0
  14. package/build/api/workflows/index.js +3 -1
  15. package/build/api/workflows/input-lookups.d.ts +16 -0
  16. package/build/api/workflows/input-lookups.js +69 -0
  17. package/build/api/workflows/input-validation.d.ts +4 -3
  18. package/build/api/workflows/input-validation.js +7 -4
  19. package/build/api/workflows/invocable.js +1 -0
  20. package/build/api/workflows/invocation.js +1 -1
  21. package/build/index.js +60 -0
  22. package/build/lib/db/schemas/035_workflow_input_lookups.sql +9 -0
  23. package/build/lib/db/schemas/036_role_portals.sql +12 -0
  24. package/build/routes/escalations/accumulate.d.ts +7 -0
  25. package/build/routes/escalations/accumulate.js +150 -0
  26. package/build/routes/escalations/index.js +5 -0
  27. package/build/routes/workflows/config.js +10 -0
  28. package/build/sdk/index.d.ts +25 -0
  29. package/build/sdk/index.js +8 -0
  30. package/build/services/config/cache.js +1 -0
  31. package/build/services/config/read.js +2 -0
  32. package/build/services/config/sql.d.ts +2 -2
  33. package/build/services/config/sql.js +6 -5
  34. package/build/services/config/write.js +10 -1
  35. package/build/services/escalation/accumulate.d.ts +81 -0
  36. package/build/services/escalation/accumulate.js +154 -0
  37. package/build/services/escalation/index.d.ts +1 -0
  38. package/build/services/escalation/index.js +1 -0
  39. package/build/services/knowledge/index.d.ts +1 -0
  40. package/build/services/knowledge/index.js +1 -0
  41. package/build/services/knowledge/lookup-refs.d.ts +7 -0
  42. package/build/services/knowledge/lookup-refs.js +26 -0
  43. package/build/services/mcp/db-server/schemas.d.ts +2 -2
  44. package/build/services/mcp/playwright-server/schemas.d.ts +2 -2
  45. package/build/services/orchestrator/condition.d.ts +40 -3
  46. package/build/services/orchestrator/condition.js +42 -2
  47. package/build/services/role/index.js +1 -0
  48. package/build/services/role/seed.js +6 -1
  49. package/build/services/role/sql.d.ts +6 -3
  50. package/build/services/role/sql.js +7 -2
  51. package/build/services/role/types.d.ts +38 -10
  52. package/build/services/role/types.js +10 -1
  53. package/build/services/scan-code/validate.js +16 -0
  54. package/build/shared/form-validation/display-only-widgets.d.ts +16 -0
  55. package/build/shared/form-validation/display-only-widgets.js +25 -0
  56. package/build/shared/form-validation/field-validator.d.ts +1 -0
  57. package/build/shared/form-validation/field-validator.js +94 -6
  58. package/build/shared/form-validation/index.d.ts +2 -0
  59. package/build/shared/form-validation/index.js +2 -0
  60. package/build/shared/form-validation/invoke-context.d.ts +4 -3
  61. package/build/shared/form-validation/invoke-context.js +5 -4
  62. package/build/shared/form-validation/x-lt-bind.d.ts +2 -14
  63. package/build/shared/form-validation/x-lt-bind.js +12 -8
  64. package/build/shared/form-validation/x-lt-invoke.d.ts +38 -0
  65. package/build/shared/form-validation/x-lt-invoke.js +103 -0
  66. package/build/shared/form-validation/x-lt-options.d.ts +3 -3
  67. package/build/shared/form-validation/x-lt-options.js +19 -5
  68. package/build/shared/form-validation/x-lt-show-if.d.ts +5 -0
  69. package/build/shared/form-validation/x-lt-show-if.js +10 -0
  70. package/build/start/workers.js +10 -0
  71. package/build/system/mcp-servers/admin/escalations.js +39 -0
  72. package/build/system/mcp-servers/admin/schemas.d.ts +256 -33
  73. package/build/system/mcp-servers/admin/schemas.js +55 -3
  74. package/build/system/mcp-servers/admin/users.js +1 -0
  75. package/build/system/mcp-servers/admin/workflow-config.js +54 -9
  76. package/build/system/mcp-servers/admin/workflows.js +9 -7
  77. package/build/system/mcp-servers/db-query/schemas.d.ts +2 -2
  78. package/build/system/mcp-servers/docs.js +4 -0
  79. package/build/system/mcp-servers/human-queue-schemas.d.ts +85 -0
  80. package/build/system/mcp-servers/human-queue-schemas.js +19 -1
  81. package/build/system/mcp-servers/human-queue.d.ts +2 -0
  82. package/build/system/mcp-servers/human-queue.js +77 -0
  83. package/build/system/seed/tool-manifests-admin.d.ts +1509 -227
  84. package/build/system/seed/tool-manifests-admin.js +8 -4
  85. package/build/system/seed/tool-manifests-escalation.d.ts +174 -0
  86. package/build/system/seed/tool-manifests-escalation.js +56 -0
  87. package/build/tsconfig.tsbuildinfo +1 -1
  88. package/build/types/config.d.ts +4 -0
  89. package/build/types/escalation.d.ts +52 -0
  90. package/build/types/escalation.js +29 -1
  91. package/build/types/index.d.ts +2 -2
  92. package/build/types/index.js +3 -1
  93. package/build/types/scan-code.d.ts +24 -0
  94. package/build/types/scan-code.js +9 -0
  95. package/build/types/startup.d.ts +26 -0
  96. package/dashboard/dist/assets/{AdminDashboard-Drs-9TRM.js → AdminDashboard-uarpHB88.js} +2 -2
  97. package/dashboard/dist/assets/{AdminDashboard-Drs-9TRM.js.map → AdminDashboard-uarpHB88.js.map} +1 -1
  98. package/dashboard/dist/assets/{AgentConfigPage-BDPDZ7-v.js → AgentConfigPage-BcUaDD5E.js} +6 -6
  99. package/dashboard/dist/assets/{AgentConfigPage-BDPDZ7-v.js.map → AgentConfigPage-BcUaDD5E.js.map} +1 -1
  100. package/dashboard/dist/assets/{AgentDetailPage-CGBulM4_.js → AgentDetailPage-BaZBhK6i.js} +3 -3
  101. package/dashboard/dist/assets/{AgentDetailPage-CGBulM4_.js.map → AgentDetailPage-BaZBhK6i.js.map} +1 -1
  102. package/dashboard/dist/assets/AgentsPage-CuVFSDc4.js +2 -0
  103. package/dashboard/dist/assets/{AgentsPage-D77JeViQ.js.map → AgentsPage-CuVFSDc4.js.map} +1 -1
  104. package/dashboard/dist/assets/AvailableEscalationsPage-iO5NyyIK.js +2 -0
  105. package/dashboard/dist/assets/AvailableEscalationsPage-iO5NyyIK.js.map +1 -0
  106. package/dashboard/dist/assets/{BotPicker-CC5vI_Sv.js → BotPicker-CClMby6B.js} +2 -2
  107. package/dashboard/dist/assets/{BotPicker-CC5vI_Sv.js.map → BotPicker-CClMby6B.js.map} +1 -1
  108. package/dashboard/dist/assets/BulkAssignModal-BL8FRgkN.js +2 -0
  109. package/dashboard/dist/assets/{BulkAssignModal-BXiJnlqt.js.map → BulkAssignModal-BL8FRgkN.js.map} +1 -1
  110. package/dashboard/dist/assets/CapabilitiesPage-D53ogPor.js +2 -0
  111. package/dashboard/dist/assets/{CapabilitiesPage-D9TC2iKy.js.map → CapabilitiesPage-D53ogPor.js.map} +1 -1
  112. package/dashboard/dist/assets/{CollapsibleSection-D10lGw_u.js → CollapsibleSection-ow8jpbtD.js} +2 -2
  113. package/dashboard/dist/assets/{CollapsibleSection-D10lGw_u.js.map → CollapsibleSection-ow8jpbtD.js.map} +1 -1
  114. package/dashboard/dist/assets/{ConfirmDeleteModal-BJdLN6No.js → ConfirmDeleteModal-CeEDt9iJ.js} +2 -2
  115. package/dashboard/dist/assets/{ConfirmDeleteModal-BJdLN6No.js.map → ConfirmDeleteModal-CeEDt9iJ.js.map} +1 -1
  116. package/dashboard/dist/assets/{CountdownTimer-D9B-BjQ7.js → CountdownTimer-CSqyZMCG.js} +2 -2
  117. package/dashboard/dist/assets/{CountdownTimer-D9B-BjQ7.js.map → CountdownTimer-CSqyZMCG.js.map} +1 -1
  118. package/dashboard/dist/assets/{CredentialsPage-hniUAFBK.js → CredentialsPage-Dx7SxA23.js} +2 -2
  119. package/dashboard/dist/assets/{CredentialsPage-hniUAFBK.js.map → CredentialsPage-Dx7SxA23.js.map} +1 -1
  120. package/dashboard/dist/assets/{CronLabel-v9DCD2FX.js → CronLabel-LYPxpvNN.js} +2 -2
  121. package/dashboard/dist/assets/{CronLabel-v9DCD2FX.js.map → CronLabel-LYPxpvNN.js.map} +1 -1
  122. package/dashboard/dist/assets/DataTable-spX1dFyb.js +2 -0
  123. package/dashboard/dist/assets/DataTable-spX1dFyb.js.map +1 -0
  124. package/dashboard/dist/assets/{DropZone-XlcY8hEH.js → DropZone-f1o-GLM8.js} +2 -2
  125. package/dashboard/dist/assets/{DropZone-XlcY8hEH.js.map → DropZone-f1o-GLM8.js.map} +1 -1
  126. package/dashboard/dist/assets/{ElapsedCell-_xvVTK8N.js → ElapsedCell-BZJTSm9N.js} +2 -2
  127. package/dashboard/dist/assets/{ElapsedCell-_xvVTK8N.js.map → ElapsedCell-BZJTSm9N.js.map} +1 -1
  128. package/dashboard/dist/assets/EntityTimelinePanel-0wIIW2en.js +2 -0
  129. package/dashboard/dist/assets/{EntityTimelinePanel-1lT3jkCM.js.map → EntityTimelinePanel-0wIIW2en.js.map} +1 -1
  130. package/dashboard/dist/assets/{EscalationListSchemaPage-BeQUTUEO.js → EscalationListSchemaPage-CoYvtMzj.js} +3 -3
  131. package/dashboard/dist/assets/{EscalationListSchemaPage-BeQUTUEO.js.map → EscalationListSchemaPage-CoYvtMzj.js.map} +1 -1
  132. package/dashboard/dist/assets/EscalationListView-DWFHPufv.js +2 -0
  133. package/dashboard/dist/assets/EscalationListView-DWFHPufv.js.map +1 -0
  134. package/dashboard/dist/assets/{EscalationSchemaPage-D4VlEwyS.js → EscalationSchemaPage-C9Xu_A25.js} +3 -3
  135. package/dashboard/dist/assets/{EscalationSchemaPage-D4VlEwyS.js.map → EscalationSchemaPage-C9Xu_A25.js.map} +1 -1
  136. package/dashboard/dist/assets/EscalationTimeline-D8W2LNNk.js +2 -0
  137. package/dashboard/dist/assets/EscalationTimeline-D8W2LNNk.js.map +1 -0
  138. package/dashboard/dist/assets/EscalationTitleSelect-DLK8NWMp.js +2 -0
  139. package/dashboard/dist/assets/EscalationTitleSelect-DLK8NWMp.js.map +1 -0
  140. package/dashboard/dist/assets/{EscalationsOverview-Bx6Zrzm-.js → EscalationsOverview-CW57FaP9.js} +2 -2
  141. package/dashboard/dist/assets/{EscalationsOverview-Bx6Zrzm-.js.map → EscalationsOverview-CW57FaP9.js.map} +1 -1
  142. package/dashboard/dist/assets/{EventTable-KEC3YWBf.js → EventTable-D1ctWhKf.js} +2 -2
  143. package/dashboard/dist/assets/{EventTable-KEC3YWBf.js.map → EventTable-D1ctWhKf.js.map} +1 -1
  144. package/dashboard/dist/assets/{FilterBar-xwN_b5fj.js → FilterBar-C-iur-Nj.js} +2 -2
  145. package/dashboard/dist/assets/{FilterBar-xwN_b5fj.js.map → FilterBar-C-iur-Nj.js.map} +1 -1
  146. package/dashboard/dist/assets/GraphInvokePage-CcA_44M4.js +2 -0
  147. package/dashboard/dist/assets/{GraphInvokePage-DV7Y0fe2.js.map → GraphInvokePage-CcA_44M4.js.map} +1 -1
  148. package/dashboard/dist/assets/HomePage-Dc085ghJ.js +2 -0
  149. package/dashboard/dist/assets/{HomePage-G785SWcW.js.map → HomePage-Dc085ghJ.js.map} +1 -1
  150. package/dashboard/dist/assets/ListToolbar-h_ziBt4O.js +2 -0
  151. package/dashboard/dist/assets/{ListToolbar-zPVYPg3W.js.map → ListToolbar-h_ziBt4O.js.map} +1 -1
  152. package/dashboard/dist/assets/{McpOverview-CMGUq8hx.js → McpOverview-qXXhbNWV.js} +2 -2
  153. package/dashboard/dist/assets/{McpOverview-CMGUq8hx.js.map → McpOverview-qXXhbNWV.js.map} +1 -1
  154. package/dashboard/dist/assets/{McpQueryDetailPage-CdhLhaKn.js → McpQueryDetailPage-SEzSbZrd.js} +2 -2
  155. package/dashboard/dist/assets/{McpQueryDetailPage-CdhLhaKn.js.map → McpQueryDetailPage-SEzSbZrd.js.map} +1 -1
  156. package/dashboard/dist/assets/McpQueryPage-Cy0V0kv9.js +2 -0
  157. package/dashboard/dist/assets/{McpQueryPage-CNSNMagY.js.map → McpQueryPage-Cy0V0kv9.js.map} +1 -1
  158. package/dashboard/dist/assets/{McpRunDetailPage-DBm8hUaW.js → McpRunDetailPage-DjJO5vSb.js} +2 -2
  159. package/dashboard/dist/assets/{McpRunDetailPage-DBm8hUaW.js.map → McpRunDetailPage-DjJO5vSb.js.map} +1 -1
  160. package/dashboard/dist/assets/McpRunsPage-BfjiJ8mM.js +2 -0
  161. package/dashboard/dist/assets/{McpRunsPage-B6rb5QBf.js.map → McpRunsPage-BfjiJ8mM.js.map} +1 -1
  162. package/dashboard/dist/assets/{NamespacePill-Bb3revqD.js → NamespacePill-DADRJOUl.js} +2 -2
  163. package/dashboard/dist/assets/{NamespacePill-Bb3revqD.js.map → NamespacePill-DADRJOUl.js.map} +1 -1
  164. package/dashboard/dist/assets/OperationsPage-dJcER0T_.js +3 -0
  165. package/dashboard/dist/assets/OperationsPage-dJcER0T_.js.map +1 -0
  166. package/dashboard/dist/assets/OperatorDashboard-zCkW4eqk.js +2 -0
  167. package/dashboard/dist/assets/{OperatorDashboard-DBOscDnf.js.map → OperatorDashboard-zCkW4eqk.js.map} +1 -1
  168. package/dashboard/dist/assets/{PageHeader-Di5Sz3an.js → PageHeader-Csq434-v.js} +2 -2
  169. package/dashboard/dist/assets/{PageHeader-Di5Sz3an.js.map → PageHeader-Csq434-v.js.map} +1 -1
  170. package/dashboard/dist/assets/{PageHeaderWithStats-DutKJ-h2.js → PageHeaderWithStats-CSnk1p5A.js} +2 -2
  171. package/dashboard/dist/assets/{PageHeaderWithStats-DutKJ-h2.js.map → PageHeaderWithStats-CSnk1p5A.js.map} +1 -1
  172. package/dashboard/dist/assets/{PersonaDetailPage-BGRdGpeR.js → PersonaDetailPage-Cx6L3Jvu.js} +2 -2
  173. package/dashboard/dist/assets/{PersonaDetailPage-BGRdGpeR.js.map → PersonaDetailPage-Cx6L3Jvu.js.map} +1 -1
  174. package/dashboard/dist/assets/{PersonasPage-DRzOZcVa.js → PersonasPage-DOgGUlJl.js} +2 -2
  175. package/dashboard/dist/assets/{PersonasPage-DRzOZcVa.js.map → PersonasPage-DOgGUlJl.js.map} +1 -1
  176. package/dashboard/dist/assets/PortalPage-BKuL1BeJ.js +2 -0
  177. package/dashboard/dist/assets/PortalPage-BKuL1BeJ.js.map +1 -0
  178. package/dashboard/dist/assets/{ProcessDetailPage-BLGjZXpM.js → ProcessDetailPage-BFcFSDUv.js} +2 -2
  179. package/dashboard/dist/assets/{ProcessDetailPage-BLGjZXpM.js.map → ProcessDetailPage-BFcFSDUv.js.map} +1 -1
  180. package/dashboard/dist/assets/ProcessesListPage-BaHV2cXp.js +2 -0
  181. package/dashboard/dist/assets/{ProcessesListPage-5NnysKFC.js.map → ProcessesListPage-BaHV2cXp.js.map} +1 -1
  182. package/dashboard/dist/assets/ResolverForm-Bgc0Zrya.js +7 -0
  183. package/dashboard/dist/assets/ResolverForm-Bgc0Zrya.js.map +1 -0
  184. package/dashboard/dist/assets/RoleDetailPage-BPY6i3CU.js +8 -0
  185. package/dashboard/dist/assets/RoleDetailPage-BPY6i3CU.js.map +1 -0
  186. package/dashboard/dist/assets/{RolePill-DhnVPK4q.js → RolePill-BafrASDX.js} +2 -2
  187. package/dashboard/dist/assets/{RolePill-DhnVPK4q.js.map → RolePill-BafrASDX.js.map} +1 -1
  188. package/dashboard/dist/assets/{RolesPage-BmW1LzkU.js → RolesPage-DSqqKPbv.js} +2 -2
  189. package/dashboard/dist/assets/{RolesPage-BmW1LzkU.js.map → RolesPage-DSqqKPbv.js.map} +1 -1
  190. package/dashboard/dist/assets/RunAsSelector-B29iWwel.js +2 -0
  191. package/dashboard/dist/assets/{RunAsSelector-BwZTJ4Yj.js.map → RunAsSelector-B29iWwel.js.map} +1 -1
  192. package/dashboard/dist/assets/ScanCodesPage-S8aAJfnC.js +2 -0
  193. package/dashboard/dist/assets/{ScanCodesPage-FiYIYB-m.js.map → ScanCodesPage-S8aAJfnC.js.map} +1 -1
  194. package/dashboard/dist/assets/{ScanSchemeDetailPage-BiAqtD-a.js → ScanSchemeDetailPage-CUBRAtmJ.js} +2 -2
  195. package/dashboard/dist/assets/{ScanSchemeDetailPage-BiAqtD-a.js.map → ScanSchemeDetailPage-CUBRAtmJ.js.map} +1 -1
  196. package/dashboard/dist/assets/ScanStationPage-R9t1-CeQ.js +2 -0
  197. package/dashboard/dist/assets/{ScanStationPage-8CdOtbCD.js.map → ScanStationPage-R9t1-CeQ.js.map} +1 -1
  198. package/dashboard/dist/assets/{ScopeBadge-CXUg0FHo.js → ScopeBadge-b6MIBiYD.js} +2 -2
  199. package/dashboard/dist/assets/{ScopeBadge-CXUg0FHo.js.map → ScopeBadge-b6MIBiYD.js.map} +1 -1
  200. package/dashboard/dist/assets/{StickyPagination-DRuqKhZt.js → StickyPagination-CkW_A507.js} +2 -2
  201. package/dashboard/dist/assets/{StickyPagination-DRuqKhZt.js.map → StickyPagination-CkW_A507.js.map} +1 -1
  202. package/dashboard/dist/assets/{StreamMessageDetail-JwlbRiMn.js → StreamMessageDetail-yzWX0wac.js} +2 -2
  203. package/dashboard/dist/assets/{StreamMessageDetail-JwlbRiMn.js.map → StreamMessageDetail-yzWX0wac.js.map} +1 -1
  204. package/dashboard/dist/assets/{SwimlaneTimeline-hu3c4bCJ.js → SwimlaneTimeline-Cf4DXFxj.js} +2 -2
  205. package/dashboard/dist/assets/{SwimlaneTimeline-hu3c4bCJ.js.map → SwimlaneTimeline-Cf4DXFxj.js.map} +1 -1
  206. package/dashboard/dist/assets/{TagInput-BZ8vb2oW.js → TagInput-ZrIIrEUP.js} +2 -2
  207. package/dashboard/dist/assets/{TagInput-BZ8vb2oW.js.map → TagInput-ZrIIrEUP.js.map} +1 -1
  208. package/dashboard/dist/assets/{TaskDetailPage-NqVZF3jW.js → TaskDetailPage-DCDcEps9.js} +2 -2
  209. package/dashboard/dist/assets/{TaskDetailPage-NqVZF3jW.js.map → TaskDetailPage-DCDcEps9.js.map} +1 -1
  210. package/dashboard/dist/assets/{TaskQueuePill-Bxir8WRl.js → TaskQueuePill-DkQ1uc6y.js} +2 -2
  211. package/dashboard/dist/assets/{TaskQueuePill-Bxir8WRl.js.map → TaskQueuePill-DkQ1uc6y.js.map} +1 -1
  212. package/dashboard/dist/assets/TasksListPage-BBATgpf7.js +2 -0
  213. package/dashboard/dist/assets/{TasksListPage-BQNaSzCn.js.map → TasksListPage-BBATgpf7.js.map} +1 -1
  214. package/dashboard/dist/assets/{TimeAgo-Cv35DOiI.js → TimeAgo-CSNVEjch.js} +2 -2
  215. package/dashboard/dist/assets/{TimeAgo-Cv35DOiI.js.map → TimeAgo-CSNVEjch.js.map} +1 -1
  216. package/dashboard/dist/assets/{TimestampCell-DHUXey9s.js → TimestampCell-Du1nBrPn.js} +2 -2
  217. package/dashboard/dist/assets/{TimestampCell-DHUXey9s.js.map → TimestampCell-Du1nBrPn.js.map} +1 -1
  218. package/dashboard/dist/assets/{ToolPill-FkgTXIhq.js → ToolPill-Baxm02L6.js} +2 -2
  219. package/dashboard/dist/assets/{ToolPill-FkgTXIhq.js.map → ToolPill-Baxm02L6.js.map} +1 -1
  220. package/dashboard/dist/assets/ToolTestPanel-CF-h_5Rg.js +2 -0
  221. package/dashboard/dist/assets/{ToolTestPanel-DvuLFWqQ.js.map → ToolTestPanel-CF-h_5Rg.js.map} +1 -1
  222. package/dashboard/dist/assets/{TopicDetailPage-TXX3QNSo.js → TopicDetailPage-Dr1UmWcp.js} +3 -3
  223. package/dashboard/dist/assets/{TopicDetailPage-TXX3QNSo.js.map → TopicDetailPage-Dr1UmWcp.js.map} +1 -1
  224. package/dashboard/dist/assets/TopicsPage-CUUo-Ylf.js +2 -0
  225. package/dashboard/dist/assets/{TopicsPage-h-oyTeEJ.js.map → TopicsPage-CUUo-Ylf.js.map} +1 -1
  226. package/dashboard/dist/assets/{UserName-BceRCeDF.js → UserName-kBh1dc39.js} +2 -2
  227. package/dashboard/dist/assets/{UserName-BceRCeDF.js.map → UserName-kBh1dc39.js.map} +1 -1
  228. package/dashboard/dist/assets/{WorkflowExecutionPage-CmHGcaN9.js → WorkflowExecutionPage-CCRSbnDx.js} +2 -2
  229. package/dashboard/dist/assets/{WorkflowExecutionPage-CmHGcaN9.js.map → WorkflowExecutionPage-CCRSbnDx.js.map} +1 -1
  230. package/dashboard/dist/assets/{WorkflowPill--AYMW7u_.js → WorkflowPill-ZepbO_ny.js} +2 -2
  231. package/dashboard/dist/assets/{WorkflowPill--AYMW7u_.js.map → WorkflowPill-ZepbO_ny.js.map} +1 -1
  232. package/dashboard/dist/assets/WorkflowsDashboard-BNQVvyek.js +2 -0
  233. package/dashboard/dist/assets/{WorkflowsDashboard-B0r2lQXk.js.map → WorkflowsDashboard-BNQVvyek.js.map} +1 -1
  234. package/dashboard/dist/assets/{WorkflowsOverview-BTMc2PJ_.js → WorkflowsOverview-DswubNJN.js} +2 -2
  235. package/dashboard/dist/assets/{WorkflowsOverview-BTMc2PJ_.js.map → WorkflowsOverview-DswubNJN.js.map} +1 -1
  236. package/dashboard/dist/assets/{YamlWorkflowDetailPage-DUiCVnZp.js → YamlWorkflowDetailPage-Cw9Dlmup.js} +11 -11
  237. package/dashboard/dist/assets/{YamlWorkflowDetailPage-DUiCVnZp.js.map → YamlWorkflowDetailPage-Cw9Dlmup.js.map} +1 -1
  238. package/dashboard/dist/assets/{YamlWorkflowsPage-BAuXfwi0.js → YamlWorkflowsPage-DB8xnIXy.js} +2 -2
  239. package/dashboard/dist/assets/{YamlWorkflowsPage-BAuXfwi0.js.map → YamlWorkflowsPage-DB8xnIXy.js.map} +1 -1
  240. package/dashboard/dist/assets/{agents-BvmLCk4h.js → agents-BwEnf6N9.js} +2 -2
  241. package/dashboard/dist/assets/{agents-BvmLCk4h.js.map → agents-BwEnf6N9.js.map} +1 -1
  242. package/dashboard/dist/assets/{bots-f_tyhl4I.js → bots-CCmJTT2u.js} +2 -2
  243. package/dashboard/dist/assets/{bots-f_tyhl4I.js.map → bots-CCmJTT2u.js.map} +1 -1
  244. package/dashboard/dist/assets/{capabilities-DNo5bqLx.js → capabilities-BZHgp_nh.js} +2 -2
  245. package/dashboard/dist/assets/{capabilities-DNo5bqLx.js.map → capabilities-BZHgp_nh.js.map} +1 -1
  246. package/dashboard/dist/assets/{constants-BHkpVaqx.js → constants-CeBBTzf5.js} +2 -2
  247. package/dashboard/dist/assets/{constants-BHkpVaqx.js.map → constants-CeBBTzf5.js.map} +1 -1
  248. package/dashboard/dist/assets/{controlplane-Dyyj2Irb.js → controlplane-CyfwUlFA.js} +2 -2
  249. package/dashboard/dist/assets/{controlplane-Dyyj2Irb.js.map → controlplane-CyfwUlFA.js.map} +1 -1
  250. package/dashboard/dist/assets/escalation-columns-BJPTNmLW.js +2 -0
  251. package/dashboard/dist/assets/escalation-columns-BJPTNmLW.js.map +1 -0
  252. package/dashboard/dist/assets/index-3gbd5neL.js +67 -0
  253. package/dashboard/dist/assets/index-3gbd5neL.js.map +1 -0
  254. package/dashboard/dist/assets/index-BAAVTyZ-.js +2 -0
  255. package/dashboard/dist/assets/{index-DL83W7Gu.js.map → index-BAAVTyZ-.js.map} +1 -1
  256. package/dashboard/dist/assets/{index-CsKiRyhy.js → index-BC6Jenlo.js} +2 -2
  257. package/dashboard/dist/assets/{index-CsKiRyhy.js.map → index-BC6Jenlo.js.map} +1 -1
  258. package/dashboard/dist/assets/index-C7KHqOTQ.js +2 -0
  259. package/dashboard/dist/assets/{index-D7ESqn22.js.map → index-C7KHqOTQ.js.map} +1 -1
  260. package/dashboard/dist/assets/index-CDrqqbSR.js +3 -0
  261. package/dashboard/dist/assets/index-CDrqqbSR.js.map +1 -0
  262. package/dashboard/dist/assets/index-CVwzCZ43.js +2 -0
  263. package/dashboard/dist/assets/{index-DvxxrFBo.js.map → index-CVwzCZ43.js.map} +1 -1
  264. package/dashboard/dist/assets/index-CaGF6TCC.js +2 -0
  265. package/dashboard/dist/assets/index-CaGF6TCC.js.map +1 -0
  266. package/dashboard/dist/assets/index-ClVbfe4z.js +13 -0
  267. package/dashboard/dist/assets/index-ClVbfe4z.js.map +1 -0
  268. package/dashboard/dist/assets/{index-DWse7ERF.js → index-CuVkSmj5.js} +2 -2
  269. package/dashboard/dist/assets/{index-DWse7ERF.js.map → index-CuVkSmj5.js.map} +1 -1
  270. package/dashboard/dist/assets/{index-PcyGjmBm.js → index-DAuX_PlL.js} +2 -2
  271. package/dashboard/dist/assets/{index-PcyGjmBm.js.map → index-DAuX_PlL.js.map} +1 -1
  272. package/dashboard/dist/assets/{index-e2X-s0EQ.js → index-DBWS5QL8.js} +2 -2
  273. package/dashboard/dist/assets/{index-e2X-s0EQ.js.map → index-DBWS5QL8.js.map} +1 -1
  274. package/dashboard/dist/assets/index-DIWCI6HC.js +2 -0
  275. package/dashboard/dist/assets/{index-BNZknxFY.js.map → index-DIWCI6HC.js.map} +1 -1
  276. package/dashboard/dist/assets/{index-C-Xr2V3F.js → index-DZ8X_Gp4.js} +2 -2
  277. package/dashboard/dist/assets/{index-C-Xr2V3F.js.map → index-DZ8X_Gp4.js.map} +1 -1
  278. package/dashboard/dist/assets/index-Dh8MJl3L.css +1 -0
  279. package/dashboard/dist/assets/{knowledge-Coo6CKzo.js → knowledge-CqyLhUMg.js} +2 -2
  280. package/dashboard/dist/assets/{knowledge-Coo6CKzo.js.map → knowledge-CqyLhUMg.js.map} +1 -1
  281. package/dashboard/dist/assets/{mcp-Cz5eFnP_.js → mcp-Cvu6bFYH.js} +2 -2
  282. package/dashboard/dist/assets/{mcp-Cz5eFnP_.js.map → mcp-Cvu6bFYH.js.map} +1 -1
  283. package/dashboard/dist/assets/{mcp-query-BtnuO4LF.js → mcp-query-qC6A3e9l.js} +2 -2
  284. package/dashboard/dist/assets/{mcp-query-BtnuO4LF.js.map → mcp-query-qC6A3e9l.js.map} +1 -1
  285. package/dashboard/dist/assets/metadata-display-COo3mMCr.js +2 -0
  286. package/dashboard/dist/assets/metadata-display-COo3mMCr.js.map +1 -0
  287. package/dashboard/dist/assets/{personas-C2-LgpHQ.js → personas-BKN_lJuV.js} +2 -2
  288. package/dashboard/dist/assets/{personas-C2-LgpHQ.js.map → personas-BKN_lJuV.js.map} +1 -1
  289. package/dashboard/dist/assets/{pipelines-TuXQvUA-.js → pipelines-BldIfv85.js} +2 -2
  290. package/dashboard/dist/assets/{pipelines-TuXQvUA-.js.map → pipelines-BldIfv85.js.map} +1 -1
  291. package/dashboard/dist/assets/{tasks-OUzpu__z.js → tasks-B-Uay8uL.js} +2 -2
  292. package/dashboard/dist/assets/{tasks-OUzpu__z.js.map → tasks-B-Uay8uL.js.map} +1 -1
  293. package/dashboard/dist/assets/{topics-B8bdrBaW.js → topics-438EQogt.js} +2 -2
  294. package/dashboard/dist/assets/{topics-B8bdrBaW.js.map → topics-438EQogt.js.map} +1 -1
  295. package/dashboard/dist/assets/useContainerWidth-DSLp1gEm.js +2 -0
  296. package/dashboard/dist/assets/useContainerWidth-DSLp1gEm.js.map +1 -0
  297. package/dashboard/dist/assets/{useDebouncedValue-Cp5ZOtVE.js → useDebouncedValue-Cw8hYZ8y.js} +2 -2
  298. package/dashboard/dist/assets/{useDebouncedValue-Cp5ZOtVE.js.map → useDebouncedValue-Cw8hYZ8y.js.map} +1 -1
  299. package/dashboard/dist/assets/{useNamespace-DtQe3wZ5.js → useNamespace-D1218GQ8.js} +2 -2
  300. package/dashboard/dist/assets/{useNamespace-DtQe3wZ5.js.map → useNamespace-D1218GQ8.js.map} +1 -1
  301. package/dashboard/dist/assets/{useYamlActivityEvents-CS22c15X.js → useYamlActivityEvents-BAQf_y_U.js} +2 -2
  302. package/dashboard/dist/assets/{useYamlActivityEvents-CS22c15X.js.map → useYamlActivityEvents-BAQf_y_U.js.map} +1 -1
  303. package/dashboard/dist/assets/{vendor-icons-DeBdvyKJ.js → vendor-icons-B7OcUM6b.js} +2 -2
  304. package/dashboard/dist/assets/{vendor-icons-DeBdvyKJ.js.map → vendor-icons-B7OcUM6b.js.map} +1 -1
  305. package/dashboard/dist/assets/{yaml-workflows-HQIki3hI.js → yaml-workflows-CN01gEB0.js} +2 -2
  306. package/dashboard/dist/assets/{yaml-workflows-HQIki3hI.js.map → yaml-workflows-CN01gEB0.js.map} +1 -1
  307. package/dashboard/dist/index.html +3 -3
  308. package/docs/accumulator.md +113 -0
  309. package/docs/api/http/escalations.md +89 -0
  310. package/docs/api/http/roles.md +1 -0
  311. package/docs/api/http/workflows.md +28 -1
  312. package/docs/api/mcp/admin.md +40 -4
  313. package/docs/api/sdk/escalations.md +91 -0
  314. package/docs/api/sdk/roles.md +2 -0
  315. package/docs/architecture.md +1 -1
  316. package/docs/code-owned-configuration.md +3 -2
  317. package/docs/dashboard.md +11 -5
  318. package/docs/hitl/design.md +1 -1
  319. package/docs/hitl/escalation.md +37 -1
  320. package/docs/hitl/form.md +12 -7
  321. package/docs/hitl/invoke-form.md +10 -3
  322. package/docs/hitl/lookups.md +21 -0
  323. package/docs/hitl/pinned-views.md +4 -0
  324. package/docs/hitl/portal.md +97 -0
  325. package/docs/hitl/resolution.md +41 -0
  326. package/docs/hitl/x-lt-embed.md +1 -1
  327. package/docs/hitl/x-lt-invoke.md +95 -0
  328. package/docs/hitl/x-lt-list-schema.md +3 -1
  329. package/docs/hitl/x-lt-validation.md +50 -3
  330. package/docs/hitl/x-lt-widget.md +22 -0
  331. package/docs/hitl-guide.md +12 -2
  332. package/docs/scan-codes.md +15 -2
  333. package/docs/sdk.md +1 -1
  334. package/docs/workflows.md +2 -2
  335. package/package.json +4 -2
  336. package/dashboard/dist/assets/AgentsPage-D77JeViQ.js +0 -2
  337. package/dashboard/dist/assets/AvailableEscalationsPage-CqM_MOMy.js +0 -2
  338. package/dashboard/dist/assets/AvailableEscalationsPage-CqM_MOMy.js.map +0 -1
  339. package/dashboard/dist/assets/BulkAssignModal-BXiJnlqt.js +0 -2
  340. package/dashboard/dist/assets/CapabilitiesPage-D9TC2iKy.js +0 -2
  341. package/dashboard/dist/assets/DataTable-7C7riavf.js +0 -2
  342. package/dashboard/dist/assets/DataTable-7C7riavf.js.map +0 -1
  343. package/dashboard/dist/assets/EntityTimelinePanel-1lT3jkCM.js +0 -2
  344. package/dashboard/dist/assets/EscalationListView-uuZRMSLB.js +0 -2
  345. package/dashboard/dist/assets/EscalationListView-uuZRMSLB.js.map +0 -1
  346. package/dashboard/dist/assets/GraphInvokePage-DV7Y0fe2.js +0 -2
  347. package/dashboard/dist/assets/HomePage-G785SWcW.js +0 -2
  348. package/dashboard/dist/assets/ListToolbar-zPVYPg3W.js +0 -2
  349. package/dashboard/dist/assets/McpQueryPage-CNSNMagY.js +0 -2
  350. package/dashboard/dist/assets/McpRunsPage-B6rb5QBf.js +0 -2
  351. package/dashboard/dist/assets/OperationsPage-Dxk1fAMg.js +0 -3
  352. package/dashboard/dist/assets/OperationsPage-Dxk1fAMg.js.map +0 -1
  353. package/dashboard/dist/assets/OperatorDashboard-DBOscDnf.js +0 -2
  354. package/dashboard/dist/assets/ProcessesListPage-5NnysKFC.js +0 -2
  355. package/dashboard/dist/assets/ResolverForm-Dy0yirtP.js +0 -2
  356. package/dashboard/dist/assets/ResolverForm-Dy0yirtP.js.map +0 -1
  357. package/dashboard/dist/assets/RoleDetailPage-C_LIoe1j.js +0 -8
  358. package/dashboard/dist/assets/RoleDetailPage-C_LIoe1j.js.map +0 -1
  359. package/dashboard/dist/assets/RunAsSelector-BwZTJ4Yj.js +0 -2
  360. package/dashboard/dist/assets/ScanCodesPage-FiYIYB-m.js +0 -2
  361. package/dashboard/dist/assets/ScanStationPage-8CdOtbCD.js +0 -2
  362. package/dashboard/dist/assets/TasksListPage-BQNaSzCn.js +0 -2
  363. package/dashboard/dist/assets/ToolTestPanel-DvuLFWqQ.js +0 -2
  364. package/dashboard/dist/assets/TopicsPage-h-oyTeEJ.js +0 -2
  365. package/dashboard/dist/assets/WorkflowsDashboard-B0r2lQXk.js +0 -2
  366. package/dashboard/dist/assets/escalation-columns-DIigeTOK.js +0 -2
  367. package/dashboard/dist/assets/escalation-columns-DIigeTOK.js.map +0 -1
  368. package/dashboard/dist/assets/index-BMlQYOE1.js +0 -67
  369. package/dashboard/dist/assets/index-BMlQYOE1.js.map +0 -1
  370. package/dashboard/dist/assets/index-BNZknxFY.js +0 -2
  371. package/dashboard/dist/assets/index-Bpvfr4OW.css +0 -1
  372. package/dashboard/dist/assets/index-CegPUKpc.js +0 -3
  373. package/dashboard/dist/assets/index-CegPUKpc.js.map +0 -1
  374. package/dashboard/dist/assets/index-D7ESqn22.js +0 -2
  375. package/dashboard/dist/assets/index-DL83W7Gu.js +0 -2
  376. package/dashboard/dist/assets/index-DPLssnMW.js +0 -6
  377. package/dashboard/dist/assets/index-DPLssnMW.js.map +0 -1
  378. package/dashboard/dist/assets/index-DoETPlJP.js +0 -11
  379. package/dashboard/dist/assets/index-DoETPlJP.js.map +0 -1
  380. package/dashboard/dist/assets/index-DvxxrFBo.js +0 -2
  381. package/dashboard/dist/assets/metadata-display--TzK0REC.js +0 -2
  382. package/dashboard/dist/assets/metadata-display--TzK0REC.js.map +0 -1
  383. package/dashboard/dist/assets/resolver-payload-C1dXB3n6.js +0 -2
  384. package/dashboard/dist/assets/resolver-payload-C1dXB3n6.js.map +0 -1
  385. package/dashboard/dist/assets/seed-form-json-DvPsISxD.js +0 -2
  386. package/dashboard/dist/assets/seed-form-json-DvPsISxD.js.map +0 -1
  387. package/docs/compilation.md +0 -152
  388. package/docs/compiler.md +0 -311
  389. package/docs/contributing.md +0 -56
  390. package/docs/design-principles.md +0 -244
  391. package/docs/schema-exchange.md +0 -173
  392. package/docs/self-test.md +0 -106
  393. package/docs/story.md +0 -184
package/docs/dashboard.md CHANGED
@@ -139,7 +139,7 @@ Every invokable workflow is presented as a tool. The list of tools takes the lef
139
139
  - **Heading and description** — the icon and title, with the identifier, tier, and queue as metadata, then the config's one-line description. Keep reference material in `x-lt-help`; it appears in the side panel on demand.
140
140
  - **Identity summary** — who will execute: the current user, the workflow's configured `execute_as` bot ("configured default"), or, for admins and superadmins, an override chosen from the bot picker ("admin override").
141
141
  - **Certification checkbox** — for a certified workflow, stamps `metadata.certified` on this one run.
142
- - **The form** — a workflow that declares `inputSchema` renders the x-lt-* form: sections, two columns, conditional fields and instruction blocks, and a side panel with **Instructions** (the interpolated `x-lt-help`) and **Issues** (violations, click to focus). Every other workflow renders the envelope template form from `envelopeSchema`, with its Form and JSON views. See [Invoke forms](hitl/invoke-form.md).
142
+ - **The form** — a workflow that declares `inputSchema` renders the x-lt-* form: sections, two columns, conditional fields and instruction blocks, selects with inline or lookup-backed labeled options, Yes/No decisions over booleans, multi-select lists, validated JSON maps, and a side panel with **Instructions** (the interpolated `x-lt-help`) and **Issues** (violations, click to focus). The `fleetTools` example exercises every shape. Every other workflow renders the envelope template form from `envelopeSchema`, with its Form and JSON views. See [Invoke forms](hitl/invoke-form.md).
143
143
  - **Submit** — posts `{ data, metadata }` to the invoke endpoint. The page stays put, reports the started id, and subscribes to that run's `system.workflow.{id}.completed` and `.failed` events, so the outcome and the workflow's returned `data` appear beside Submit without leaving the page. One click disarms the button until the person chooses **Submit again**. While live events are off, a warning beside the button offers a reconnect, since the result could not arrive otherwise. Builders also get a **View workflow** link to its execution. A `422` from the input schema gate lands in the Issues view.
144
144
 
145
145
  Below 1280px the list folds into a select and the form takes the full width.
@@ -166,7 +166,7 @@ The MCP Tool Designer page lists previous tool design sessions and provides entr
166
166
 
167
167
  **Composer mode** — manual tool design for building tools from scratch using the visual DAG editor.
168
168
 
169
- Steps unlock sequentially in each wizard. Compiled tools appear in **Graph Workflows** and **Graph Executions**. See the [Compilation Pipeline](compilation.md) guide for details.
169
+ Steps unlock sequentially in each wizard. Compiled tools appear in **Graph Workflows** and **Graph Executions**. See the [Compilation Pipeline](https://github.com/hotmeshio/long-tail/blob/main/docs/donotpublish/compilation.md) guide for details.
170
170
 
171
171
  ### MCP Server Tools
172
172
 
@@ -278,7 +278,9 @@ Accessible at `/admin/roles/:role`. One page per role — a quiet header carryin
278
278
 
279
279
  **Pins** — two live-save groups. **Link Variables** leads: the facet names members bind per device (`properties.link_variables` — name, optional label, optional default). A pin below may reference a variable as a facet value, `facets={"facility":"{lt:facility}"}`; each member device opens it scoped to its own binding (set from the user menu → Link variables), falling back to the declared default or, unbound, applying no filter. A caption under each templated pin previews the binding on the viewing device (`facility = 'soleful'` / `facility = <empty>`). **Default Pins** follows — the pinned-view seeds this role hands its members (`default_pins`): label, dashboard-relative URL, optional badge. Members see them in their Pinned nav section from first login (marked role-provided) and may promote, hide, or reorder them via preferences; promoting a templated pin copies the template, so it keeps following the device's bindings. See [Faceted Routing — Link variables](faceted-routing.md#link-variables).
280
280
 
281
- **API:** `PATCH /api/roles/:role` updates identity, Pace Board dials, placement, schemas, and pins.
281
+ **Portal** the role's named portals, each its pinned views laid out as one page. The editor mirrors the page: sheets, title bands, cell panels, dashed slots for adding. Name a portal and start it from a pin; then rows of cells, each cell a pin picked from the role's default pins or the admin's own pins, or typed as a label and URL, with editable panel titles (`portals`, up to 12 portals of 4 rows by 6). Edits join the page draft and **Save** writes them. A sketch shows each shape; **Open** opens `/portal/<role>/<key>`. See [Portal](hitl/portal.md).
282
+
283
+ **API:** `PATCH /api/roles/:role` updates identity, Pace Board dials, placement, schemas, pins, and the portal.
282
284
 
283
285
  ### Escalation Schema
284
286
 
@@ -377,7 +379,7 @@ Accessible at `/escalations/available`. The central queue for escalation activit
377
379
  - **Columns:** a **Summary** cell (status dot + description or type), **Assignee** (claimant, or `—`), **Role** pill, **Priority**, and **Created**. The status dot distinguishes pending, claimed, resolved, cancelled, and expired; notification escalations show a bell.
378
380
  - **Faceted query** — beyond the filter bar, the queue takes a structured metadata query, all URL-backed so a query copy-pastes: `?facets={"orderId":"..."}` (equality, AND-ed), `?block=` (exclude), `?range=` (numeric bounds), `?exists=` (key present), `?roles=` (union of role queues), and `?orderBy=` (JSON sort). `?jeopardy=1` narrows to rows past the role's age threshold. List rows omit the envelope by default; `?include=envelope` pulls the full envelope and payload columns for rich views.
379
381
  - **Metadata value affordances** — each metadata key/value row carries a three-icon triad on hover: **filter** (scope the current role's queue to `key = value`), **search** (the same facet match across all roles), and **history** (open the entity's cross-queue interval timeline in the right panel — every role this value moved through, with durations and gaps). Filter-present, search-present, history-past. History renders for string values, since the timeline's GIN containment match serves JSON-string facets.
380
- - **View modes** (`?view=`) — `table` (the columns above), `timeline` (100 rows per page of cross-queue movement), and `rich`. When a role owns a [list schema](#escalations-list-schema), the list defaults to the role-authored `rich` view (the live item as a card, plus a load-on-demand history) with a **Table view** toggle back.
382
+ - **View modes** (`?view=`) — `table` (the columns above), `timeline` (100 rows per page of cross-queue movement), and `rich`. A `?layout=` hint shapes the table: `compact` stays a tight table however narrow, holding non-identity columns until the container clears the split width; `cards` folds into console cards on purpose; absent, the table folds on its own below the card threshold (the same hint a portal pin carries, see [Portal](hitl/portal.md)). When a role owns a [list schema](#escalations-list-schema), the list defaults to the role-authored `rich` view (the live item as a card, plus a load-on-demand history) with a **Table view** toggle back.
381
383
  - **Claim** — the claim action locks an item to your user for a claim window (default 30 min). Only members of the role see its pending items; a member scoped `read_self` lands directly on their own assigned item rather than browsing the queue. Re-claiming your own expired item extends it rather than failing.
382
384
  - **Bulk actions (admin)** — row checkboxes surface a selection bar: **Claim**, **Assign**, **Unassign**, **Escalate** (move to another role), and **Cancel**. Assign opens a modal for the target user and claim duration; rows under a live claim are skipped unless the modal's takeover checkbox (shown when the selection includes live claims) reassigns them — admin/superadmin only. Unassign returns claimed rows to the pool. Each response reports how many rows were skipped and why. Scoped to a single role, the Cancel verb and its confirmation speak the role form's `x-lt-labels.cancel` vocabulary (`false` hides it). Assign also accepts a faceted query instead of an id list, so "claim everything matching this facet" is one call.
383
385
 
@@ -387,6 +389,10 @@ Accessible at `/escalations/available`. The central queue for escalation activit
387
389
 
388
390
  Accessible at `/escalations/queue`. The personal inbox — the items currently assigned to you across your roles. It carries the same title-as-role-selector, the same rich/table views, and the same metadata affordances as All Escalations, minus the bulk selection bar (every row is already yours). A **Claim expiry** column counts down each item's remaining window. The shell also auto-navigates here on hand-off: when a workflow assigns a follow-on escalation to you (`assigned_to = you`), the dashboard opens that item's detail directly.
389
391
 
392
+ ### Portal
393
+
394
+ Accessible at `/portal/<role>/<key>` to any signed-in user, from the **Portals** section of the user menu, and from the rows that lead the role's group in the nav. Each of the role's `portals` renders as a grid: one row per declared row, each dividing its width among its cells, every cell a bounded panel of a pinned list view drawn with the same rich, table, and timeline components as All Escalations; the left nav steps aside on portal pages as it does in kiosk mode. Panels show the pin label, the live count, a view toggle and sort direction for the visit, a refresh, and a link to the full view; rows open the item detail. A kiosk role lands on its first portal as home. See [Portal](hitl/portal.md).
395
+
390
396
  ### Escalation Detail & Resolve Form
391
397
 
392
398
  Accessible at `/escalations/detail/:id`. Where a person completes the work the workflow is waiting on. The main column is the resolve form; a switchable side panel carries context.
@@ -399,7 +405,7 @@ Accessible at `/escalations/detail/:id`. Where a person completes the work the w
399
405
  - **Submit guard** — `x-lt-submit-guard` gates a parent on a child query (e.g. "every item in this batch must resolve first"): the dashboard disables Submit with a live count while rows remain, and the server re-checks the guard atomically inside the resolving `UPDATE` so no race slips through. `autoResolveWhenEmpty` submits the parent the moment the query empties.
400
406
  - **Transition hand-off** — `x-lt-transition` replaces the return-to-previous-page jump with a short wait screen: the workflow assigns a follow-on escalation back to the submitter, the screen detects it and navigates onward (or falls back to `x-lt-transition-done` after a bounded wait). This is the submit → side-effect → next-step chain (submit, a label prints, the harvest task opens).
401
407
  - **Batch resolution** — a `conditional({ batch })` escalation accumulates N items on one row; `POST /api/escalations/:id/resolve-batch-item` submits each (claim-agnostic by default, `assertClaim` to require your own live claim), and the last item wakes the workflow with the full collection.
402
- - **Side panel** — switchable views selected by the icon strip: **Help** (the form's `x-lt-help` markdown, `{{domain.path}}`-interpolated against the live record — escalation, metadata, envelope, payload, resolver, and `lookup.*` — or a state-aware hint like "Claim this escalation to enable the form"), **Details** (status, role, priority, claim provenance, timestamps, and for builders the identifier links), **AI Analysis** (what triage diagnosed and corrected, when AI is on), **Metadata** (the row's facets, with the same triad), **Context** (input envelope, escalation context, resolver payload), **Record** (the raw JSON, builders only), and **Errors** (the last submit's validation failures). The panel and form share the width as a flex set; it opens expanded on Help when the form carries `x-lt-help`.
408
+ - **Side panel** — switchable views selected by the icon strip: **Help** (the form's `x-lt-help` markdown, `{{domain.path}}`-interpolated against the live record — escalation, metadata, envelope, payload, resolver, and `lookup.*` — or a state-aware hint like "Claim this escalation to enable the form"), **Details** (status, role, priority, claim provenance, timestamps, and for builders the identifier links), **AI Analysis** (what triage diagnosed and corrected, when AI is on), **Metadata** (the row's facets, with the same triad), **Items** (on an accumulator or batch row: the held items in arrival order with the count against max, each with its payload summary, arrival time, actor, and a link to its reciprocal row; on a pending accumulator the actor adds an item by key with an optional JSON payload or removes one, each through the same guarded statement the API runs), **Context** (input envelope, escalation context, resolver payload), **Record** (the raw JSON, builders only), and **Errors** (the last submit's validation failures). The panel and form share the width as a flex set; it opens expanded on Help when the form carries `x-lt-help`.
403
409
  - **Admin claim override** — when an item is held by someone else, admins and superadmins see **Reassign…** (hand the claim to another user, takeover implied) and **Return to queue** (unassign) in the action bar.
404
410
 
405
411
  **API:** `POST /api/escalations/:id/claim` claims, `/release` releases, `/resolve` resolves; `/resolve-batch-item` submits one batch item; `/escalate` moves; `/cancel` cancels. The role's form and its versions come from `GET /api/roles/:role/schema`.
@@ -8,7 +8,7 @@ author schemas and copy, the rules the renderer applies to errors, and how a
8
8
  deployment plugs its own stylesheet in through static config.
9
9
 
10
10
  The full internal doctrine lives in
11
- [`docs/design-principles.md`](../design-principles.md); the reference form
11
+ [`docs/donotpublish/design-principles.md`](https://github.com/hotmeshio/long-tail/blob/main/docs/donotpublish/design-principles.md); the reference form
12
12
  implementation is `examples/workflows/acme-stations`.
13
13
 
14
14
  ---
@@ -103,7 +103,43 @@ The declaration folds into the row at creation, inside the same Leg1 commit:
103
103
 
104
104
  Items are submitted via `POST /api/escalations/:id/resolve-batch-item`, `POST /api/escalations/resolve-batch-item-by-signal-key` (the deterministic home signal id — no UUID lookup, no facet duplication), or `POST /api/escalations/resolve-batch-item-by-metadata` (see [Resolution — Batch items](resolution.md)). Each item validates against the same versioned role form a single-item resolver gets, `schemaVersion` pins included. Each fill is one guarded statement: the payload lands only while its key is still pending (duplicates return `duplicate-item` untouched), and the LAST fill resolves the row, stores the assembled collection as `resolver_payload`, and wakes this wait — atomically. The completing submission's `$resolution` rides the delivered collection.
105
105
 
106
- `timeout` and cancellation keep their standard semantics (`false` / `null`); a plain resolve on a batch row remains an admin override that resolves the whole row with the payload given.
106
+ `timeout` and cancellation keep their standard semantics (`false` / `null`); a plain resolve on a batch row remains an admin override that resolves the whole row with the payload given. Add `partialOnTimeout: true` (hotmesh 0.29.0+) and the timer instead resumes the wait with the items filled so far plus `$trigger: 'timeout'`, the same value the expired row stores as `resolver_payload`.
107
+
108
+ ### Open accumulation — items arrive over time
109
+
110
+ Declare `accumulate` and the escalation becomes an open container: items join it one at a time through `accumulateItem` while the row stays pending, and the wait resumes with the ordered collection when the container is full, when its window closes, or when someone resolves it by hand. A timeout is a delivery, never a failure, so the wait has no `false` branch; `conditionalAccumulator` types that contract.
111
+
112
+ ```typescript
113
+ import { conditionalAccumulator } from '@hotmeshio/long-tail';
114
+
115
+ const bin = await conditionalAccumulator<BagV1, { shippedBy?: string }>(signalId, {
116
+ role: 'bin',
117
+ description: 'Scan each bag into the bin',
118
+ metadata: { binKey },
119
+ accumulate: { max: 12 }, // count trigger; omit for an unbounded window
120
+ timeout: '4h', // the window
121
+ });
122
+ if (bin === null) { /* cancelled */ }
123
+ bin.$trigger; // 'count' | 'timeout' | 'resolve'
124
+ bin.$accumulated; // [{ itemKey, payload?, at, actor?, reciprocalId? }, ...] in arrival order
125
+ bin.shippedBy; // present only when $trigger is 'resolve' (the resolver's payload merges in)
126
+ ```
127
+
128
+ `accumulate` takes three dials. `max` is the count trigger; absent, only the timer or a manual resolve ends the wait. `resolveAtMax: false` makes `max` a cap: the last slot answers `accepted`, further adds answer `full`, and the wait still ends by timer or resolve. `unique: false` lets a repeated item key replace its entry in place; the default answers `duplicate-item` and leaves the row untouched.
129
+
130
+ The declaration folds into the row at creation, inside the same Leg1 commit:
131
+
132
+ - `metadata.accumulate_count` — items held right now, recomputed in every add and remove
133
+ - `metadata.accumulate_max` — the count trigger, or `null` when unbounded
134
+ - `metadata.accumulate_keys` — the held item keys (`@>`-queryable: `{"accumulate_keys":["ORD-9"]}` finds the container holding that order)
135
+ - `envelope.accumulate_items` — `Record<itemKey, { payload?, at, actor?, reciprocalId? }>`, with `at` stamped by the database clock in the add statement
136
+ - `envelope.accumulate_config` — the folded `{ unique, resolveAtMax }`
137
+
138
+ Items are added via `POST /api/escalations/:id/accumulate`, `POST /api/escalations/accumulate-by-signal-key`, or `POST /api/escalations/accumulate-by-metadata`, removed via the matching `remove-item` forms, and read in arrival order via `GET /api/escalations/:id/items` (see [Resolution — Accumulator items](resolution.md#accumulator-items)). Each add is one guarded statement: the entry lands only while the row is pending and the key is not already held, the facets recompute, and the add that reaches `max` resolves the row, stores `{ $accumulated, $trigger: 'count' }` as `resolver_payload`, and wakes this wait, atomically. When the timer fires first the expiry statement stores and delivers `{ $accumulated, $trigger: 'timeout' }` and the row is `expired`; a manual `resolve` stores and delivers the collection merged with the resolver's payload under `$trigger: 'resolve'`. Cancellation still yields `null` with the held items preserved on the row.
139
+
140
+ A **reciprocal** add writes two accumulator rows in that same statement, both or neither: the container gains the item, and a second row (the item's own escalation, declared `accumulate: { max: 1 }`) gains the container's id as its item key. Each entry carries the other row's id as `reciprocalId`, so "which bin holds this bag" reads from the bag's row and "which bags are in this bin" from the bin's, both as row truth. A blocked reciprocal (already resolved, full, already holding this container) answers `reciprocal-*` and the container is untouched. The `rollupBin` and `rollupMember` examples show both sides.
141
+
142
+ `accumulate` and `batch` are mutually exclusive on one wait. The full contract, guarantees, and what is deferred are collected in [Open accumulator](../accumulator.md).
107
143
 
108
144
  ### Row completeness guarantee
109
145
 
package/docs/hitl/form.md CHANGED
@@ -26,16 +26,21 @@ Declare `title` on every field whose key isn't already a readable label — it i
26
26
 
27
27
  ## Field Types
28
28
 
29
- | JSON Type | Renders As |
29
+ The control follows the field's declared `type` and its tokens; the stored value only fills it. A field with no value yet renders the empty state of its control: a number is an empty input, a boolean is an unchecked box, a select opens on **Choose…**, a multi-select has nothing picked, a JSON editor is empty. Every property starts at its `default`, or `{}` for an object, or empty text.
30
+
31
+ | Declared type | Renders As |
30
32
  |-----------|-----------|
31
33
  | `boolean` | Checkbox toggle |
32
- | `number` | Number input |
33
- | `string` | Text input (default) |
34
- | `string` + `enum` | Dropdown select |
35
- | `string`/`number` + `x-lt-options` | Dropdown select — options resolved from the escalation context (see [x-lt-validation.md](x-lt-validation.md#dynamic-select-options-x-lt-options)) |
34
+ | `boolean` + `enum` / `x-lt-options` | Select emitting `true` or `false` (a Yes/No decision) |
35
+ | `number` / `integer` | Number input |
36
+ | `string` | Text input; `format: "textarea"` for multi-line |
37
+ | `string`/`number` + `enum` | Select |
38
+ | any scalar + `x-lt-options` | Select with labeled options: inline, from the context, or from a lookup (see [x-lt-validation.md](x-lt-validation.md#select-options-x-lt-options)) |
39
+ | `array` + `x-lt-options` | Multi-select chips emitting the picked values in list order |
40
+ | `array` / `object` + `x-lt-widget: "json"` | Validated JSON editor (see [x-lt-widget.md](x-lt-widget.md#json)) |
41
+ | `array` without options | Tag display (read-only) |
42
+ | `object` | Nested fields from `properties`, or a widget (see [x-lt-widget.md](x-lt-widget.md)) |
36
43
  | `null` | Read-only "null" display |
37
- | `array` | Tag display (read-only) |
38
- | `object` | Nested section with recursive fields, or a widget (see [x-lt-widget.md](x-lt-widget.md)) |
39
44
 
40
45
  ---
41
46
 
@@ -22,7 +22,8 @@ Workflow names read as titles on the page: `fleetTools` shows as **Fleet Tools**
22
22
  | Field | Role in the invoke |
23
23
  |---|---|
24
24
  | `inputSchema` | The form. JSON Schema `properties` plus x-lt-* tokens. Rendered by the Invoke Tool page; enforced by the invoke API. |
25
- | `envelopeSchema.metadata` | The `metadata` stamped on every run from this form. `envelopeSchema.data` is ignored when `inputSchema` is present. |
25
+ | `envelopeSchema.metadata` | The `metadata` the Invoke Tool page stamps on every run from this form. `envelopeSchema.data` is ignored when `inputSchema` is present. |
26
+ | `inputLookups` | Versioned knowledge refs (`{ domain, key, version, as? }`) the form reads as `lookup.<as ?? key>`. See [Lookups](lookups.md#pinning-on-a-workflow-config). |
26
27
  | `invocationRoles` | Who sees the workflow on the Invoke Tool page and who may start it. Empty means every authenticated user. |
27
28
  | `description` | One line at the top of the form column, markdown allowed. Keep the reference material in `x-lt-help`, which appears in the side panel on demand. |
28
29
  | `icon` | A curated icon from `WORKFLOW_ICONS` (`icon: WORKFLOW_ICONS.WRENCH`). Leads the workflow's row and heading in place of the tier glyph so operators tell tools apart at a glance. The registry offers the same set as a picker. |
@@ -44,7 +45,9 @@ The form edits flat values keyed by property name. On submit, `x-lt-bind` maps t
44
45
  "metadata": { "source": "dashboard" } }
45
46
  ```
46
47
 
47
- Conditions and help tokens read the live form under the `input` domain: `x-lt-showIf: "input.action=retire"`, `{{input.serialNumber}}`. `resolver` names the same values, so a schema written for an escalation form works unchanged. The only other domain an invoke form can read is `metadata`, the envelope metadata declared above.
48
+ Conditions and help tokens read the live form under the `input` domain: `x-lt-showIf: "input.action=retire"`, `{{input.serialNumber}}`. `resolver` names the same values, so a schema written for an escalation form works unchanged. An invoke form reads four domains: `input`, `resolver`, `metadata` (the run's metadata), and `lookup` (the editions pinned by `inputLookups`, keyed `<as ?? key>`).
49
+
50
+ The gate validates against the `metadata` the caller sends. The Invoke Tool page sends the declared `envelopeSchema.metadata` plus the per-run `certified` flag; an API or MCP caller that wants a condition such as `x-lt-showIf: "metadata.mode=strict"` to hold sends that metadata itself.
48
51
 
49
52
  Hidden conditional fields still submit their defaults, as on every x-lt-* form. Treat empty as absent in the workflow.
50
53
 
@@ -67,6 +70,10 @@ An array of conditions requires every one, so the prompt belongs to its tool and
67
70
 
68
71
  `x-lt-help` on the schema root is the longer reference for the side panel: markdown, tables, and `{{input.*}}` tokens that re-interpolate as the operator types.
69
72
 
73
+ ## From an escalation form
74
+
75
+ A role's `form_schema` can offer the same invoke from inside the work item: an `x-lt-invoke` field names the workflow and maps the escalation's values into its payload, then either posts directly or opens this form in a dialog with those values in place. Visibility follows the same per-caller list, the payload meets the same gate, and the run reports over the same events. See [x-lt-invoke.md](x-lt-invoke.md).
76
+
70
77
  ## The Invoke Tool page
71
78
 
72
79
  The page is open to anyone the server lists an invokable workflow for. Builders reach **Invoke Tool** under Orchestrate; every other persona gets a **Tools** section in the nav that appears only when there is something to invoke. Each invokable workflow is a tool; the list sits on the left, grouped by task queue, with the first workflow preselected; the form takes the rest of the row. Below 1280px the list folds into a select and the form takes the full width.
@@ -93,6 +100,6 @@ Once a run starts, the page subscribes to `system.workflow.{workflowId}.complete
93
100
  }
94
101
  ```
95
102
 
96
- The MCP tools `invoke_workflow` and `invoke_workflow_read_safe` run the same gate and return the body as an error result. The gate reads bound paths from the nested `data`, skips fields a `x-lt-showIf` hides, and applies every validation keyword the form applies.
103
+ The MCP tools `invoke_workflow` and `invoke_workflow_read_safe` run the same gate and return the body as an error result; `get_workflow_config` returns the `input_schema`, the declared metadata, and the resolved lookup data a caller needs to compose a valid payload. The gate reads bound paths from the nested `data`, skips fields a `x-lt-showIf` hides, resolves `x-lt-options` over pinned lookups, and applies every validation keyword the form applies.
97
104
 
98
105
  `GET /api/workflows/invocable` returns the caller's invokable workflows with their tier. The list and the gate share one predicate, so the page never offers a workflow the API would refuse.
@@ -37,6 +37,27 @@ const decision = await conditional<CascadeResolverV1>(signalId, {
37
37
 
38
38
  The refs fold into `envelope.lookups` as a pure transform — a pinned wait costs exactly what an unpinned one does. A malformed ref (missing field, non-integer version) throws before the row is written; over the HTTP create surface the same validation answers with a 400.
39
39
 
40
+ ## Pinning on a Workflow Config
41
+
42
+ An invoke form pins editions the same way. Declare `inputLookups` beside `inputSchema` on the worker config and the form reads them under `lookup.<as ?? key>`:
43
+
44
+ ```typescript
45
+ const fleetToolsConfig: LTWorkerConfig = {
46
+ invocable: true,
47
+ inputSchema: FLEET_TOOLS_INPUT_SCHEMA,
48
+ inputLookups: [{ domain: 'fleet', key: 'serial-numbers', version: 1, as: 'serials' }],
49
+ };
50
+ ```
51
+
52
+ ```jsonc
53
+ // inputSchema.properties
54
+ "serialNumber": { "type": "string", "title": "Serial", "x-lt-options": "lookup.serials.items" }
55
+ ```
56
+
57
+ The Invoke Tool page fetches `GET /api/workflows/:type/input-lookups` once per workflow; the grant is the invoke predicate, so whoever may start the workflow reads exactly the pinned editions. The invoke API resolves the same refs into the gate, so a value outside the edition is refused with the canonical 422. The registry detail page edits the refs under **Lookups** in its Invocation column; the preview renders against the resolved editions.
58
+
59
+ The refs are checked twice on save. Shape first: a missing `domain` or `key`, a `version` that is not a positive integer, or an empty `as` answers `400` with the field named. Existence second: a ref whose edition does not exist answers `400` naming the editions that do (`Lookup ref fleet/serial-numbers v3 names no edition (editions: v1, v2)`), from `PUT /api/workflows/:type/config` and from the MCP `upsert_workflow_config` tool alike. On the worker boot path a malformed ref fails the boot; a ref whose edition is not yet written logs a warning, since seeds commonly run after registration. At run time the resolver reads each edition once and holds it in memory (editions are immutable), a missing edition answers `missing: true` for that ref without failing the batch, the Invoke Tool page shows which pinned editions are unavailable, and the fields that read them follow the `x-lt-options` rules: a plain path falls back to the typed input, an interpolated path stays a disabled select and fails closed.
60
+
40
61
  ## Versioning
41
62
 
42
63
  Every knowledge entry carries a `current_version`, and every write that changes its data mints an immutable snapshot automatically — no publish step:
@@ -40,6 +40,10 @@ A role may declare `default_pins: [{ label, url, badge? }]` (edited on Role Deta
40
40
 
41
41
  Membership *is* the persona's bookmark set — no per-user setup. Duplicate labels across a user's roles collapse to the first role's pin.
42
42
 
43
+ ## Portal
44
+
45
+ A role can lay its pins out as pages: `portals` is a list of named matrices, each an array of rows of pins, rendered at `/portal/<role>/<key>` as a grid of live list panels. Members choose one from the global menu, each also leads the role's group in the nav, and a kiosk role lands on its first portal as home. See [portal.md](portal.md).
46
+
43
47
  ## Reference Example
44
48
 
45
49
  The `fleet-servicer` seed (`examples/seed-fleet-sim.ts`) ships a facet-board list schema, jeopardy dials, and three default pins — the board, a badged "Needs harvesting" facet query, and a badged jeopardy view — so the whole persona story is exercisable from the dashboard. The seeded `reviewer` user is a member.
@@ -0,0 +1,97 @@
1
+ # Portals: a role's pinned views as pages
2
+
3
+ A role can declare **portals**: named matrices of pinned views, each rendered as a single page of live list panels at `/portal/<role>/<key>`. A matrix is an array of rows, each row an array of cells, each cell a pin. One row with one cell fills the page with that view. Two rows, the first holding one cell and the second three, give a full-width band over three columns. A 2x2 splits one queue by region, each cell the same role scoped to a different facet. Cells carry nothing but the pin, so everything a pin already says composes for free: the view (`view=rich`, `table`, `timeline`), facet scopes, jeopardy, link variables, and the live count.
4
+
5
+ A role carries several portals because screens serve different purposes. One shows urgent items by facility on the floor, four facilities in a 2x2. Another is the day's focus for the team, one panel per person, each named for them and scoped to their facets, so everyone sees their own list under their own name. Members choose a portal from the global menu, where the kiosk queue picker already lives, and each portal also appears as a row under the role's group in the left nav. A kiosk role lands on its first portal as home.
6
+
7
+ ---
8
+
9
+ ## Declaring portals
10
+
11
+ `portals` on the role: a list of `{ key, label, rows }`. `key` is a short url-safe slug, unique within the role, and the portal's address segment. `label` is the name shown in the menu and the nav. `rows` is the matrix: up to 4 rows of up to 6 cells, every row holding at least one cell, every cell `{ label, url, badge? }` with a dashboard-relative `url`, the `default_pins` shape.
12
+
13
+ ```typescript
14
+ roles: [
15
+ {
16
+ role: 'printer-fleet',
17
+ portals: [
18
+ {
19
+ key: 'floor',
20
+ label: 'Floor screen',
21
+ rows: [
22
+ [{ label: 'Fleet board', url: '/escalations/available?role=printer-fleet&view=rich' }],
23
+ [
24
+ { label: 'North', url: '/escalations/available?role=printer-fleet&facets=%7B%22facility%22%3A%22north%22%7D&view=table', badge: true },
25
+ { label: 'South', url: '/escalations/available?role=printer-fleet&facets=%7B%22facility%22%3A%22south%22%7D&view=table', badge: true },
26
+ { label: 'Harvest queue', url: '/escalations/available?role=printer-harvest&view=table', badge: true },
27
+ ],
28
+ ],
29
+ },
30
+ {
31
+ key: 'focus',
32
+ label: 'Focus of the day',
33
+ rows: [[
34
+ { label: 'Jim', url: '/escalations/available?role=printer-fleet&facets=%7B%22owner%22%3A%22jim%22%7D' },
35
+ { label: 'Sally', url: '/escalations/available?role=printer-fleet&facets=%7B%22owner%22%3A%22sally%22%7D' },
36
+ { label: 'Anu', url: '/escalations/available?role=printer-fleet&facets=%7B%22owner%22%3A%22anu%22%7D' },
37
+ ]],
38
+ },
39
+ ],
40
+ },
41
+ ],
42
+ ```
43
+
44
+ ### Counts above the panels
45
+
46
+ A portal may lead with **counts**: `counts` is a list of `{ label, url, blurb? }` (up to 8). Each becomes a tile above the panels showing the live total of its list URL, the same number a pin badge carries, under the label and over the blurb, and the tile opens the list. A queue's counts read at a glance before its lists do:
47
+
48
+ ```typescript
49
+ {
50
+ key: 'review-desk',
51
+ label: 'Review desk',
52
+ counts: [
53
+ { label: 'Waiting', url: '/escalations/available?role=reviewer', blurb: 'Unclaimed items in the queue' },
54
+ { label: 'In progress', url: '/escalations/available?role=reviewer&status=claimed', blurb: 'Claimed and being worked' },
55
+ { label: 'Resolved', url: '/escalations/available?role=reviewer&status=resolved', blurb: 'Closed out by a reviewer' },
56
+ { label: 'Expired', url: '/escalations/available?role=reviewer&status=expired', blurb: 'Timed out before anyone acted' },
57
+ { label: 'Cancelled', url: '/escalations/available?role=reviewer&status=cancelled', blurb: 'Withdrawn by the workflow' },
58
+ ],
59
+ rows: [[
60
+ { label: 'Waiting', url: '/escalations/available?role=reviewer&view=table&layout=compact', badge: true },
61
+ { label: 'In progress', url: '/escalations/available?role=reviewer&status=claimed&view=table&layout=compact', badge: true },
62
+ ]],
63
+ }
64
+ ```
65
+
66
+ The `reviewer` role in the examples' start config (`index.ts`) declares exactly this portal, in code, with `reset: true` so it applies on every boot.
67
+
68
+ The same field rides `PATCH /api/roles/:role` and the `update_role` MCP tool. `null` clears every portal. Under code-owned configuration the declaration is compared in order and applied on every boot, so reordering portals or rows is a change.
69
+
70
+ ## Composing them in the dashboard
71
+
72
+ Role Detail carries a **Portal** section listing the role's portals. The editor draws what it produces: each portal a bounded sheet with a title band, each cell its own small panel, and dashed placeholders wherever a view can be added. Edits build the page's draft; **Save** writes them with the rest of the role, one `PATCH` into the role's `portals` column:
73
+
74
+ - **New portal**: type a name, then **Start it with a view** over the role's default pins and the admin's own pins. The first pick creates the portal; its key derives from the name. **Label and URL** adds a view that is not yet pinned.
75
+ - Each portal shows a sketch of its shape, its editable name, its key, **Open**, and a remove control. Each row lists its cells with an editable panel title (name a cell for the person or place it serves), the view its URL asks for (`rich`, `table`, `timeline`, or `default` when the role's list schema decides), the URL, and the device binding of any link variable it references.
76
+ - Each row ends with **Add a view to this row** (up to six); below the matrix, **Start a new row with a view** (up to four rows). Removing the last cell of a row removes the row; removing the last cell of a portal removes the portal.
77
+ - A **counts** strip leads each portal's body: **Add a count** picks a pin whose total becomes a tile; each tile's title and blurb edit in place.
78
+
79
+ ## The page
80
+
81
+ A portal page fills the content area with one grid row per declared row, each dividing its width evenly among its cells. Each panel is a bounded sheet with a tinted title band and its own scroll, drawn with the same components as the escalations list: the rich view when the role owns a non-table list schema and the pin does not ask for another, the table when it asks for one or the schema is plain, the timeline when asked. Count tiles, when declared, sit in one row above the panels: label, large number, blurb, each a link to its list. The band of every panel shows the pin's label, the live total, a view toggle (table or rich, when the role offers both), the sort direction, a refresh, and a link to the full view. The toggle and sort hold for the visit; the pin stays as authored. Rows open the item detail page, which returns to the portal. Up to 25 rows render per panel, then a quiet "N more in the full view" link. A narrow window stacks the cells in declaration order.
82
+
83
+ **Fitting a screen.** A panel on a wall screen is read, not scrolled, so a pin can ask the table to spend its width tightly: `?view=table&layout=compact` keeps the table a table however narrow the panel, tightens every cell, and holds the non-identity columns (assignee, role, priority) back until the panel is wide enough, so a narrow cell reads as a tight list of item and age and twenty rows fit where the folded layout showed four. `layout=cards` asks for the folded console-card layout on purpose. Without `layout`, the table folds on its own below the card threshold. The hint rides the URL like `view`, so it works on the full list page too, and it is the seam for a compact timeline later: a series of tight offset lines that fit the whole story in the panel.
84
+
85
+ A portal page shows without the left nav, as kiosk does: it is a screen read from across a room, and the panels are the page. The header stays, so the user menu still switches portals. Any signed-in user may open a portal. Panels read through the same role-scoped endpoints as the list page, so RBAC is unchanged: each viewer sees exactly what their read scope returns, and a queue outside it renders empty. Link variables resolve per device at render time, exactly as pins in the nav do. One event subscription per queue named by the cells keeps every panel current, on the LIST refresh tier.
86
+
87
+ ## Choosing a portal
88
+
89
+ The global menu (the user menu in the header) lists **Portals**: every portal of the roles the viewer belongs to, grouped by role when more than one role declares any; global viewers see every role's. A device dedicated to a portal, a screen on the floor, opens the menu once and lands there, the same gesture as choosing a kiosk queue. The left nav repeats each portal as a row leading the role's pinned group.
90
+
91
+ ## Kiosk
92
+
93
+ Kiosk home is the role's first portal when it declares any, and the role's escalation list otherwise. The station queue picker follows the same rule. `/portal` is on the kiosk allow-list beside the list, detail, and scan screens. See [Kiosk mode](../scan-codes.md#kiosk-mode--the-locked-station-viewport).
94
+
95
+ ## Reference example
96
+
97
+ The `printer-fleet` seed (`examples/seed-fleet-sim.ts`) declares two portals: **Floor screen**, the fleet board over the two facility tables and the harvest queue, and **By model**, one panel per printer model. Open Admin → Roles → printer-fleet → Portal to see them composed, and the user menu's Portals section to open either.
@@ -109,6 +109,47 @@ Each item validates against the same versioned role form a single-item resolve u
109
109
 
110
110
  Item keys are non-empty strings up to 128 characters; prefer URL-friendly names (`u1-L`). Payload keys inside each item are caller-owned — the platform reserves no names inside `batch_items` values. Every fill stamps `envelope.batch_filled_at[itemKey]` with the database clock in the same statement, so the row carries the collection timeline as row truth. After an SLA expiry (`conditional` returns `false`) the terminal row retains the partial `batch_items` and their timestamps — read them back with `GET /api/escalations/:id` or `getEscalationBySignalKey` from an activity.
111
111
 
112
+ ### Accumulator items
113
+
114
+ An accumulator escalation (a `conditionalAccumulator` wait declared with `accumulate: {...}`, see [Creating Escalations](escalation.md#open-accumulation--items-arrive-over-time)) holds items added over time. Add by id, by signal key, or by metadata facet:
115
+
116
+ ```typescript
117
+ const first = await lt.escalations.accumulate({
118
+ id: binId,
119
+ itemKey: orderId,
120
+ payload: { weight: 2 }, // optional; validates against the bin role's form
121
+ reciprocal: { id: bagEscalationId }, // optional; the bag's own row is written in the same statement
122
+ });
123
+ // → { outcome: 'accepted', count: 1, remaining: 11, escalationId,
124
+ // reciprocal: { outcome: 'completed', count: 1, escalationId, signaled: true } }
125
+
126
+ const second = await lt.escalations.accumulateBySignalKey({
127
+ signalKey: homeSignalId,
128
+ itemKey: 'order-2',
129
+ });
130
+
131
+ const last = await lt.escalations.accumulateByMetadata({
132
+ key: 'binKey',
133
+ value: binKey,
134
+ itemKey: 'order-12',
135
+ });
136
+ // → { outcome: 'completed', count: 12, remaining: 0, signaled: true, workflowId }
137
+
138
+ await lt.escalations.removeItem({ id: binId, itemKey: 'order-2' });
139
+ // → { outcome: 'removed', count: 11, escalationId }
140
+
141
+ const held = await lt.escalations.getItems({ id: binId });
142
+ // → { kind: 'accumulate', count, max, items: [{ itemKey, payload?, at, actor?, reciprocalId? }, ...] }
143
+ ```
144
+
145
+ Interim adds return `accepted` with the count held and the slots remaining (`null` when unbounded) and publish `escalation.updated` carrying `item_key`, `count`, `actor`, and `reciprocal_id`; the add that reaches `max` returns `completed`, the row resolved with `{ $accumulated, $trigger: 'count' }` as `resolver_payload` and the workflow woke with it, in the same statement. A key already held returns 409 (`duplicate-item`, safe under scanner double-reads); a full cap returns 409 (`full`); a row that is not an accumulator returns 400. Removals return the new count, publish `escalation.updated` with `removed: true`, and never wake the waiter.
146
+
147
+ Adds are claim-agnostic, the same rationale as batch fills: a container collects contributions from many hands. `assertClaim: true` (by-id form) opts into the caller's own live-claim assertion inside the guarded statement. Every add records the actor on the entry and merges `resolved_by` into the row's metadata, so the completing add's stamp is the resolution provenance and `$resolution` rides the completing wake.
148
+
149
+ A reciprocal names a second row by `id`, `signalKey`, or `key`/`value`; it is RBAC-gated before the statement (404 when the caller cannot see it, 403 when they see it but cannot act on it) and written in the same statement as the container, both or neither. The reciprocal row holds the container's id as its item key; a member declared `accumulate: { max: 1 }` completes and wakes on that add. When the reciprocal blocks the add, the response names why (`reciprocal-full`, `reciprocal-duplicate`, `reciprocal-terminal`, `reciprocal-not-accumulator`) and the container is untouched.
150
+
151
+ Item keys are non-empty strings up to 128 characters. Reserved accumulate keys (`accumulate_count`, `accumulate_max`, `accumulate_keys`) cannot appear in the `metadata` patch. A plain `resolve` on an accumulator row is the hand-close: the workflow receives the collection merged with the resolver payload under `$trigger: 'resolve'`, and the row stores the same. After the window closes the row is `expired` with the collection stored as `resolver_payload`, exactly what the workflow received.
152
+
112
153
  ### Resolving a set atomically
113
154
 
114
155
  When one decision settles a set of waits — each with its own payload — use `resolveAllOrNone`:
@@ -1,6 +1,6 @@
1
1
  # Embed and Navigation Widgets
2
2
 
3
- Three display-only widgets that surface related information inside an escalation's resolver form without leaving the page. All three produce **no resolver payload** they must never appear in the schema's `required` array and are always declared `readOnly: true`.
3
+ Three display-only widgets that surface related information inside an escalation's resolver form without leaving the page. All three produce **no resolver payload**: their keys are dropped from the submitted payload, they must never appear in the schema's `required` array, and they are always declared `readOnly: true`. The [`invoke`](x-lt-invoke.md) widget, which starts a workflow from the form, follows the same rules.
4
4
 
5
5
  | Widget | Token | What it shows |
6
6
  |--------|-------|---------------|
@@ -0,0 +1,95 @@
1
+ # x-lt-invoke — start a workflow from the form
2
+
3
+ A role's `form_schema` can place a control that starts another workflow: a link, an icon-link, or a button that either fires the workflow at once with a payload mapped from the escalation, or opens the workflow's own invoke form in a dialog, prefilled from the same mapping. The control reuses the Invoke Tool page end to end: the same per-caller list decides who sees it, the same `input_schema` gate validates the payload, the same registered icon leads the label, and the run's outcome arrives over the same live events.
4
+
5
+ The field is display-only. It carries no answer, is dropped from the resolver payload, and must never appear in `required`.
6
+
7
+ ```jsonc
8
+ "print_pamphlet": {
9
+ "type": "string",
10
+ "readOnly": true,
11
+ "x-lt-widget": "invoke",
12
+ "x-lt-invoke": {
13
+ "workflow": "printPamphlet",
14
+ "data": { "order": { "id": "{{metadata.orderId}}" }, "copies": 1 },
15
+ "metadata": { "escalationId": "{{escalation.id}}" },
16
+ "modal": true
17
+ },
18
+ "title": "Print pamphlet",
19
+ "description": "Prints the patient pamphlet for this order",
20
+ "x-lt-section": "Tools",
21
+ "x-lt-span": 2,
22
+ "x-lt-showIf": "metadata.orderId"
23
+ }
24
+ ```
25
+
26
+ ## The declaration
27
+
28
+ | Key | Type | Meaning |
29
+ |-----|------|---------|
30
+ | `workflow` | string | The `workflow_type` to start. Required. |
31
+ | `data` | object | The nested `data` the invoke API receives. Leaves interpolate `{{domain.path}}` tokens against the escalation context. Defaults to `{}`. |
32
+ | `metadata` | object | Extra run metadata, merged over the workflow's declared `envelopeSchema.metadata`. Same interpolation. |
33
+ | `modal` | boolean | `true` opens the workflow's invoke form prefilled; `false` (default) posts `data` at once. |
34
+ | `confirm` | string | Direct mode only: a prompt shown before the post. Tokens interpolate. |
35
+ | `variant` | `"link"` \| `"button"` | The control's shape. Defaults to `link`. |
36
+ | `icon` | boolean | Lead the label with the workflow's registered icon, the one the registry picker sets and the Invoke Tool page shows. Defaults to `true`. |
37
+
38
+ The field's `title` is the label; without one the workflow type reads as a title (`printPamphlet` → **Print Pamphlet**). `description` renders as the helper line beneath it. Every layout token composes: `x-lt-section`, `x-lt-span`, `x-lt-showIf`, `x-lt-order`.
39
+
40
+ ## Mapping values
41
+
42
+ `data` is written once, in the shape the workflow receives. In direct mode it is posted as resolved. In modal mode the same object prefills the workflow's `input_schema` by inverting each field's `x-lt-bind`, so a field bound to `printer.serialNumber` opens holding `data.printer.serialNumber`. A workflow without an `input_schema` gets its envelope template form with `data` merged over the template.
43
+
44
+ Interpolation keeps types:
45
+
46
+ | Leaf | Resolves to |
47
+ |------|-------------|
48
+ | `"{{metadata.copies}}"` | The raw context value: a number stays a number, a boolean a boolean, an array an array |
49
+ | `"ORD-{{metadata.orderId}}"` | A string |
50
+ | A token whose value is missing or null | The key is omitted, so the workflow's own defaults apply |
51
+ | A mixed string with any missing segment | The key is omitted |
52
+ | `1`, `true`, `"plain text"` | Passed through unchanged |
53
+
54
+ Objects and arrays recurse. The domains are the escalation form's: `escalation`, `metadata`, `envelope`, `payload`, `lookup`, and the live `resolver` values, so a mapping can carry what the operator has typed so far.
55
+
56
+ ## Who sees it
57
+
58
+ The control renders only for workflows the signed-in user may invoke, read from the same per-caller list that drives the Invoke Tool page and its nav entry. A workflow outside that list leaves no trace in the form. The server runs the same predicate on the post. The control follows the form's claim state: it is live once the operator has claimed the escalation, and it leaves the form once the escalation is resolved, cancelled, or expired.
59
+
60
+ One request serves every invoke field on the page, and the list is shared with the rest of the dashboard's cache.
61
+
62
+ ## Direct mode
63
+
64
+ Clicking the control posts `data` with the workflow's declared metadata (plus the field's `metadata` mapping) and the `certified` flag the workflow is registered with. The control gives way to **Working…** while the run is in flight, then reads **Completed** with the returned `data` as an outline beneath, or **Failed** with the reason. The outcome arrives over the same `system.workflow.{id}.completed` and `.failed` events the Invoke Tool page follows. Once the run has settled the control returns beneath the result, ready for another run. A warning offers to reconnect live events when they are off.
65
+
66
+ When the target declares an `input_schema`, the payload is checked against it before the post with the shared validation pass. Violations, from that check or from a server 422, list beneath the control with an **Open form** action that opens the workflow's form prefilled, so the operator completes what the mapping could not supply. A mapping meant for direct mode should satisfy the schema on its own; declare `confirm` when the action deserves a second look.
67
+
68
+ ## Modal mode
69
+
70
+ `"modal": true` opens the workflow's invoke form in a dialog headed by its icon and title: the full x-lt-* form when the workflow declares an `input_schema`, with pinned lookups resolved, conditional sections, and the shared validation pass; the envelope template form otherwise. Prefilled values sit in their fields ready to be changed. Issues and the form's `x-lt-help` render inside the dialog.
71
+
72
+ Submit posts exactly as the Invoke Tool page does, and the dialog becomes a receipt with one action. While the run works it shows the workflow's icon and **Working…** with a Close control; closing leaves the run going. On completion it reads **Completed** with the returned `data` as an outline and a **Done** button. On failure it reads **Failed** with the reason, **Try again** returns to the form holding what was typed, and Close dismisses. The backdrop does not dismiss the dialog; Escape and the close control do. Each opening starts clean.
73
+
74
+ ## Example
75
+
76
+ The `printer-fleet` role's work form offers the fleet tools for the machine on screen. Its escalations carry `metadata.serialNumber`, the same facet the fleet-tools serial lookup reads, so the dialog opens with the serial already chosen:
77
+
78
+ ```typescript
79
+ fleet_tools: {
80
+ type: 'string',
81
+ readOnly: true,
82
+ 'x-lt-widget': 'invoke',
83
+ 'x-lt-invoke': {
84
+ workflow: 'fleetTools',
85
+ modal: true,
86
+ data: { printer: { serialNumber: '{{metadata.serialNumber}}' } },
87
+ },
88
+ 'x-lt-section': 'Tools',
89
+ 'x-lt-span': 2,
90
+ title: 'Fleet tools',
91
+ description: 'Reprint a label, change filament, or report this machine offline',
92
+ }
93
+ ```
94
+
95
+ See [`examples/seed-fleet-sim.ts`](../../examples/seed-fleet-sim.ts) for the role and [invoke-form.md](invoke-form.md) for the `fleetTools` form it opens.
@@ -21,7 +21,9 @@ Every string is a markdown/text template run through the same `{{domain.path}}`
21
21
  | `x-lt-card` | schema | `facet-board`: the per-entity card — `{ title, state?, fields?: [{ label, value, format? }] }` |
22
22
  | `x-lt-row-action` | schema | The per-row action button: `{ action?, label?, durationMinutes? }` — see below |
23
23
 
24
- `format: "age"` on a `facet-table` column or `facet-board` field renders a timestamp as a compact age (`12m`, `3h`, `2d`) with the absolute time as its tooltip, repainted each minute — aging interim states are scannable at a glance.
24
+ `format: "age"` (or its spoken twin `"ago"`) on a `facet-table` column or `facet-board` field renders a timestamp as a compact age (`12m`, `3h`, `2d`) with the absolute time as its tooltip, repainted each minute — aging interim states are scannable at a glance. `format: "date"` renders the full date the dashboard uses everywhere, hoverable for the exact time; a bare ISO timestamp with no format renders the same way. Both readings of a date column are useful: age for pace, date for the record.
25
+
26
+ The `facet-table` renders at the table's dense setting: small type and tight side padding, so a row of six values fits without truncating. When no column declares a `width`, the first column holds 200px and the rest share the remainder; the row action sizes to its label.
25
27
 
26
28
  `format: "user"` resolves a user-id token — `{{escalation.assigned_to}}` — to that person's display name. Resolution is batched: every id on a page is coalesced into one `POST /api/users/names` request returning display fields only (no secrets, scopes, or metadata), cached by id for the session and not refetched on window focus; a user edit invalidates the cache. It truncates with an ellipsis and carries the full name on hover; an unassigned row shows a quiet em dash. `assigned_to` is the claimant while the item is worked and stays as the resolver once it is resolved, so one column reads correctly across the row's whole life. The built-in table and timeline views carry this user column by default; authored templates opt in per column.
27
29
 
@@ -6,7 +6,7 @@ The form runs a full validation pass before any submit is accepted. Every visibl
6
6
 
7
7
  ## Required
8
8
 
9
- Fields listed in the schema's `required` array block submission when empty. A red asterisk marks required fields. Required validation is skipped for fields hidden by `x-lt-showIf` at submission time.
9
+ Fields listed in the schema's `required` array block submission when empty. A red asterisk marks required fields. Empty means `null`, `undefined`, blank text, an unchecked checkbox, an object with no truthy value, an empty multi-select, or an empty `json` list. A boolean with an option list is answered by either value, and a `json` map is answered by any key. Required validation is skipped for fields hidden by `x-lt-showIf` at submission time.
10
10
 
11
11
  ```json
12
12
  {
@@ -93,9 +93,30 @@ When `envelope.min_score` is `60`, submitting a score of `45` blocks with "Minim
93
93
 
94
94
  ---
95
95
 
96
- ## Dynamic Select Options (`x-lt-options`)
96
+ ## Select Options (`x-lt-options`)
97
97
 
98
- A select's option list can ride the escalation instead of the schema. The field-level token names a `"domain.path"`; the array of strings or numbers at that path becomes the field's options one static role form, per-row legal values:
98
+ `x-lt-options` takes one of three forms. A `"domain.path"` string, or an array of path strings, resolves the list from the context (first path that resolves wins). An array with **no string entries** is the list itself, written inline: `{ value, label }` objects, numbers, or booleans. A literal string value is written as an object, `{ "value": "rn", "label": "Registered Nurse" }`, since a bare string is always read as a path. A static `enum` wins over both.
99
+
100
+ ```json
101
+ {
102
+ "properties": {
103
+ "role": {
104
+ "type": "string",
105
+ "x-lt-options": [{ "value": "rn", "label": "Registered Nurse" }, { "value": "lpn", "label": "Practical Nurse" }]
106
+ },
107
+ "powerCycled": {
108
+ "type": "boolean",
109
+ "x-lt-options": [{ "value": true, "label": "Yes" }, { "value": false, "label": "No" }]
110
+ }
111
+ }
112
+ }
113
+ ```
114
+
115
+ A boolean with an option list renders as a select, not a checkbox: use it for decisions, and keep the checkbox for confirmations.
116
+
117
+ **Clearable selects.** A select opens on a disabled **Choose…** placeholder and, once a value is picked, offers no way back. With `"x-lt-nullable": true` the placeholder stays enabled and picking it submits `null`, for a choice that may lawfully stay unmade.
118
+
119
+ The list can also ride the escalation instead of the schema. The token names a `"domain.path"`; the array of strings or numbers at that path becomes the field's options, one static role form, per-row legal values:
99
120
 
100
121
  ```json
101
122
  {
@@ -126,6 +147,32 @@ When the envelope carries `left_quantity_options: [0, 1, 2, 3]`, the field rende
126
147
 
127
148
  ---
128
149
 
150
+ ## Lists and Maps
151
+
152
+ A `type: "array"` field with its own `x-lt-options` (path or literal) renders as a multi-select of chips; the submitted value is the array of picked values in list order. A list is validated only when it is edited this way or through the `json` widget; a list shown as read-only tags is left alone.
153
+
154
+ ```json
155
+ {
156
+ "required": ["roles"],
157
+ "properties": {
158
+ "roles": { "type": "array", "x-lt-options": [{ "value": "gluer", "label": "Gluer" }, { "value": "qa", "label": "QA" }], "minItems": 1, "maxItems": 2 },
159
+ "weights": {
160
+ "type": "object",
161
+ "x-lt-widget": "json",
162
+ "propertyNames": { "enum": ["bag", "plate", "spool"] },
163
+ "additionalProperties": { "type": "number", "minimum": 0, "maximum": 1 }
164
+ }
165
+ }
166
+ }
167
+ ```
168
+
169
+ - A required multi-select or `json` list needs at least one item. An empty optional list passes its constraints.
170
+ - Every item must belong to the option list (`Must be one of: gluer, qa`); `minItems`/`maxItems` bound the length (`At least 1 item`, `At most 2 items`); a `json` list checks each item against `items`, including `items.enum` (`Item 2: Expected a number`).
171
+ - A map's keys must belong to `propertyNames.enum` (`Unknown key "tray". Allowed: bag, plate, spool`) and every value must satisfy an object-shaped `additionalProperties` (`"bag": Minimum value is 0`). A required map needs at least one key; a zero weight is an answer.
172
+ - The same rules run on the wire: an API or MCP caller sending the list or map under the bound key receives the same violations as the form shows.
173
+
174
+ ---
175
+
129
176
  ## Checklist Completion (`x-lt-require-all`)
130
177
 
131
178
  A checklist-widget field can require every item to be checked before submission: