crewly 1.6.5 → 1.8.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 (338) hide show
  1. package/config/constants.ts +2 -0
  2. package/config/roles/auditor/prompt.md +24 -0
  3. package/config/roles/developer/prompt.md +2 -1
  4. package/config/roles/orchestrator/prompt.md +118 -2
  5. package/config/roles/team-leader/prompt.md +6 -0
  6. package/config/skills/agent/core/create-request/SKILL.md +1 -1
  7. package/config/skills/agent/core/create-request/execute.sh +29 -2
  8. package/config/skills/agent/core/create-request/execute.test.sh +168 -0
  9. package/config/skills/agent/core/report-status/SKILL.md +8 -1
  10. package/config/skills/agent/core/report-status/execute.sh +23 -1
  11. package/config/skills/orchestrator/heartbeat/execute.sh +48 -6
  12. package/config/sops/common/mid-flight-milestone-surface.md +128 -0
  13. package/config/sops/common/owner-facing-communication.md +46 -2
  14. package/config/sops/developer/git-workflow.md +33 -0
  15. package/dist/backend/backend/src/constants.d.ts +13 -0
  16. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  17. package/dist/backend/backend/src/constants.js +12 -0
  18. package/dist/backend/backend/src/constants.js.map +1 -1
  19. package/dist/backend/backend/src/controllers/browser/browser.controller.js +2 -2
  20. package/dist/backend/backend/src/controllers/browser/browser.controller.js.map +1 -1
  21. package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
  22. package/dist/backend/backend/src/controllers/chat/chat.controller.js +6 -0
  23. package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
  24. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.d.ts +73 -0
  25. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.d.ts.map +1 -1
  26. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.js +128 -0
  27. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.js.map +1 -1
  28. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.routes.d.ts +3 -0
  29. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.routes.d.ts.map +1 -1
  30. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.routes.js +8 -0
  31. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.routes.js.map +1 -1
  32. package/dist/backend/backend/src/controllers/session/session.controller.d.ts.map +1 -1
  33. package/dist/backend/backend/src/controllers/session/session.controller.js +50 -8
  34. package/dist/backend/backend/src/controllers/session/session.controller.js.map +1 -1
  35. package/dist/backend/backend/src/controllers/slack/slack.controller.d.ts.map +1 -1
  36. package/dist/backend/backend/src/controllers/slack/slack.controller.js +215 -94
  37. package/dist/backend/backend/src/controllers/slack/slack.controller.js.map +1 -1
  38. package/dist/backend/backend/src/controllers/team/team.controller.d.ts.map +1 -1
  39. package/dist/backend/backend/src/controllers/team/team.controller.js +27 -0
  40. package/dist/backend/backend/src/controllers/team/team.controller.js.map +1 -1
  41. package/dist/backend/backend/src/index.d.ts +1 -0
  42. package/dist/backend/backend/src/index.d.ts.map +1 -1
  43. package/dist/backend/backend/src/index.js +201 -37
  44. package/dist/backend/backend/src/index.js.map +1 -1
  45. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  46. package/dist/backend/backend/src/routes/api.routes.js +11 -1
  47. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  48. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +45 -3
  49. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  50. package/dist/backend/backend/src/services/agent/agent-registration.service.js +219 -7
  51. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  52. package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.d.ts +61 -1
  53. package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.d.ts.map +1 -1
  54. package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.js +117 -9
  55. package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.js.map +1 -1
  56. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +44 -0
  57. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
  58. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +179 -10
  59. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
  60. package/dist/backend/backend/src/services/agent/crewly-agent/in-process-runtime-registry.d.ts +6 -6
  61. package/dist/backend/backend/src/services/agent/crewly-agent/in-process-runtime-registry.d.ts.map +1 -1
  62. package/dist/backend/backend/src/services/agent/crewly-agent/in-process-runtime-registry.js +3 -3
  63. package/dist/backend/backend/src/services/agent/crewly-agent/in-process-runtime-registry.js.map +1 -1
  64. package/dist/backend/backend/src/services/agent/crewly-agent/types.d.ts +7 -1
  65. package/dist/backend/backend/src/services/agent/crewly-agent/types.d.ts.map +1 -1
  66. package/dist/backend/backend/src/services/agent/crewly-agent/types.js.map +1 -1
  67. package/dist/backend/backend/src/services/agent/idle-detection.service.d.ts +33 -0
  68. package/dist/backend/backend/src/services/agent/idle-detection.service.d.ts.map +1 -1
  69. package/dist/backend/backend/src/services/agent/idle-detection.service.js +108 -4
  70. package/dist/backend/backend/src/services/agent/idle-detection.service.js.map +1 -1
  71. package/dist/backend/backend/src/services/agent/runtime-service.factory.js +4 -4
  72. package/dist/backend/backend/src/services/agent/runtime-service.factory.js.map +1 -1
  73. package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts +1 -1
  74. package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts.map +1 -1
  75. package/dist/backend/backend/src/services/browser/browser-proxy.service.js +40 -2
  76. package/dist/backend/backend/src/services/browser/browser-proxy.service.js.map +1 -1
  77. package/dist/backend/backend/src/services/chat/chat.service.d.ts +48 -331
  78. package/dist/backend/backend/src/services/chat/chat.service.d.ts.map +1 -1
  79. package/dist/backend/backend/src/services/chat/chat.service.js +261 -712
  80. package/dist/backend/backend/src/services/chat/chat.service.js.map +1 -1
  81. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +82 -1
  82. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
  83. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +120 -2
  84. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
  85. package/dist/backend/backend/src/services/chat-v2/chat-v2.providers.d.ts +114 -0
  86. package/dist/backend/backend/src/services/chat-v2/chat-v2.providers.d.ts.map +1 -0
  87. package/dist/backend/backend/src/services/chat-v2/chat-v2.providers.js +182 -0
  88. package/dist/backend/backend/src/services/chat-v2/chat-v2.providers.js.map +1 -0
  89. package/dist/backend/backend/src/services/chat-v2/chat-v2.relay-adapter.service.d.ts +188 -0
  90. package/dist/backend/backend/src/services/chat-v2/chat-v2.relay-adapter.service.d.ts.map +1 -0
  91. package/dist/backend/backend/src/services/chat-v2/chat-v2.relay-adapter.service.js +434 -0
  92. package/dist/backend/backend/src/services/chat-v2/chat-v2.relay-adapter.service.js.map +1 -0
  93. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts +401 -5
  94. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
  95. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js +619 -3
  96. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
  97. package/dist/backend/backend/src/services/chat-v2/legacy-dto.utils.d.ts +93 -0
  98. package/dist/backend/backend/src/services/chat-v2/legacy-dto.utils.d.ts.map +1 -0
  99. package/dist/backend/backend/src/services/chat-v2/legacy-dto.utils.js +138 -0
  100. package/dist/backend/backend/src/services/chat-v2/legacy-dto.utils.js.map +1 -0
  101. package/dist/backend/backend/src/services/chat-v2/sqlite/channel.store.d.ts +46 -0
  102. package/dist/backend/backend/src/services/chat-v2/sqlite/channel.store.d.ts.map +1 -1
  103. package/dist/backend/backend/src/services/chat-v2/sqlite/channel.store.js +75 -0
  104. package/dist/backend/backend/src/services/chat-v2/sqlite/channel.store.js.map +1 -1
  105. package/dist/backend/backend/src/services/chat-v2/sqlite/chat-db.d.ts +10 -2
  106. package/dist/backend/backend/src/services/chat-v2/sqlite/chat-db.d.ts.map +1 -1
  107. package/dist/backend/backend/src/services/chat-v2/sqlite/chat-db.js +178 -10
  108. package/dist/backend/backend/src/services/chat-v2/sqlite/chat-db.js.map +1 -1
  109. package/dist/backend/backend/src/services/chat-v2/sqlite/message.store.d.ts +37 -0
  110. package/dist/backend/backend/src/services/chat-v2/sqlite/message.store.d.ts.map +1 -1
  111. package/dist/backend/backend/src/services/chat-v2/sqlite/message.store.js +71 -0
  112. package/dist/backend/backend/src/services/chat-v2/sqlite/message.store.js.map +1 -1
  113. package/dist/backend/backend/src/services/chat-v2/types.d.ts +33 -1
  114. package/dist/backend/backend/src/services/chat-v2/types.d.ts.map +1 -1
  115. package/dist/backend/backend/src/services/chat-v2/types.js +1 -1
  116. package/dist/backend/backend/src/services/chat-v2/types.js.map +1 -1
  117. package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts +22 -0
  118. package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -1
  119. package/dist/backend/backend/src/services/cloud/cloud-sync.service.js +71 -1
  120. package/dist/backend/backend/src/services/cloud/cloud-sync.service.js.map +1 -1
  121. package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts +102 -1
  122. package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -1
  123. package/dist/backend/backend/src/services/cloud/cloud-sync.types.js +61 -0
  124. package/dist/backend/backend/src/services/cloud/cloud-sync.types.js.map +1 -1
  125. package/dist/backend/backend/src/services/cloud/device-auto-discovery.service.d.ts +21 -3
  126. package/dist/backend/backend/src/services/cloud/device-auto-discovery.service.d.ts.map +1 -1
  127. package/dist/backend/backend/src/services/cloud/device-auto-discovery.service.js +47 -13
  128. package/dist/backend/backend/src/services/cloud/device-auto-discovery.service.js.map +1 -1
  129. package/dist/backend/backend/src/services/core/system-health.util.d.ts +25 -4
  130. package/dist/backend/backend/src/services/core/system-health.util.d.ts.map +1 -1
  131. package/dist/backend/backend/src/services/core/system-health.util.js +30 -5
  132. package/dist/backend/backend/src/services/core/system-health.util.js.map +1 -1
  133. package/dist/backend/backend/src/services/event-bus/event-bus.service.d.ts.map +1 -1
  134. package/dist/backend/backend/src/services/event-bus/event-bus.service.js +22 -11
  135. package/dist/backend/backend/src/services/event-bus/event-bus.service.js.map +1 -1
  136. package/dist/backend/backend/src/services/mcp-server.d.ts.map +1 -1
  137. package/dist/backend/backend/src/services/mcp-server.js +5 -0
  138. package/dist/backend/backend/src/services/mcp-server.js.map +1 -1
  139. package/dist/backend/backend/src/services/mcp-tool-definitions.d.ts +4 -0
  140. package/dist/backend/backend/src/services/mcp-tool-definitions.d.ts.map +1 -1
  141. package/dist/backend/backend/src/services/mcp-tool-definitions.js +12 -1
  142. package/dist/backend/backend/src/services/mcp-tool-definitions.js.map +1 -1
  143. package/dist/backend/backend/src/services/memory/capability-inference.d.ts +73 -0
  144. package/dist/backend/backend/src/services/memory/capability-inference.d.ts.map +1 -0
  145. package/dist/backend/backend/src/services/memory/capability-inference.js +115 -0
  146. package/dist/backend/backend/src/services/memory/capability-inference.js.map +1 -0
  147. package/dist/backend/backend/src/services/memory/memory.service.d.ts +22 -1
  148. package/dist/backend/backend/src/services/memory/memory.service.d.ts.map +1 -1
  149. package/dist/backend/backend/src/services/memory/memory.service.js +81 -3
  150. package/dist/backend/backend/src/services/memory/memory.service.js.map +1 -1
  151. package/dist/backend/backend/src/services/memory/project-memory.service.d.ts +25 -1
  152. package/dist/backend/backend/src/services/memory/project-memory.service.d.ts.map +1 -1
  153. package/dist/backend/backend/src/services/memory/project-memory.service.js +43 -0
  154. package/dist/backend/backend/src/services/memory/project-memory.service.js.map +1 -1
  155. package/dist/backend/backend/src/services/memory/task-history-seeder.d.ts +47 -0
  156. package/dist/backend/backend/src/services/memory/task-history-seeder.d.ts.map +1 -0
  157. package/dist/backend/backend/src/services/memory/task-history-seeder.js +89 -0
  158. package/dist/backend/backend/src/services/memory/task-history-seeder.js.map +1 -0
  159. package/dist/backend/backend/src/services/memory/task-history.subscriber.d.ts +76 -0
  160. package/dist/backend/backend/src/services/memory/task-history.subscriber.d.ts.map +1 -0
  161. package/dist/backend/backend/src/services/memory/task-history.subscriber.js +199 -0
  162. package/dist/backend/backend/src/services/memory/task-history.subscriber.js.map +1 -0
  163. package/dist/backend/backend/src/services/messaging/message-replay.service.d.ts +2 -4
  164. package/dist/backend/backend/src/services/messaging/message-replay.service.d.ts.map +1 -1
  165. package/dist/backend/backend/src/services/messaging/message-replay.service.js +22 -12
  166. package/dist/backend/backend/src/services/messaging/message-replay.service.js.map +1 -1
  167. package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts.map +1 -1
  168. package/dist/backend/backend/src/services/messaging/queue-processor.service.js +25 -5
  169. package/dist/backend/backend/src/services/messaging/queue-processor.service.js.map +1 -1
  170. package/dist/backend/backend/src/services/monitoring/system-resource-alert.service.d.ts.map +1 -1
  171. package/dist/backend/backend/src/services/monitoring/system-resource-alert.service.js +13 -3
  172. package/dist/backend/backend/src/services/monitoring/system-resource-alert.service.js.map +1 -1
  173. package/dist/backend/backend/src/services/notification/milestone-notification.subscriber.d.ts +99 -0
  174. package/dist/backend/backend/src/services/notification/milestone-notification.subscriber.d.ts.map +1 -0
  175. package/dist/backend/backend/src/services/notification/milestone-notification.subscriber.js +225 -0
  176. package/dist/backend/backend/src/services/notification/milestone-notification.subscriber.js.map +1 -0
  177. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +39 -18
  178. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
  179. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +60 -32
  180. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
  181. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts +134 -0
  182. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts.map +1 -1
  183. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js +416 -13
  184. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js.map +1 -1
  185. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
  186. package/dist/backend/backend/src/services/reconciler/reconciler.service.js +73 -7
  187. package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
  188. package/dist/backend/backend/src/services/session/session-handoff.service.d.ts.map +1 -1
  189. package/dist/backend/backend/src/services/session/session-handoff.service.js +30 -4
  190. package/dist/backend/backend/src/services/session/session-handoff.service.js.map +1 -1
  191. package/dist/backend/backend/src/services/skill/skill-executor.service.d.ts.map +1 -1
  192. package/dist/backend/backend/src/services/skill/skill-executor.service.js +13 -1
  193. package/dist/backend/backend/src/services/skill/skill-executor.service.js.map +1 -1
  194. package/dist/backend/backend/src/services/slack/notify-reconciliation.service.d.ts.map +1 -1
  195. package/dist/backend/backend/src/services/slack/notify-reconciliation.service.js +9 -6
  196. package/dist/backend/backend/src/services/slack/notify-reconciliation.service.js.map +1 -1
  197. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts +21 -2
  198. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  199. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +120 -46
  200. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  201. package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
  202. package/dist/backend/backend/src/services/slack/slack.service.js +49 -0
  203. package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
  204. package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts +33 -2
  205. package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
  206. package/dist/backend/backend/src/services/task-pool/task-pool.service.js +160 -8
  207. package/dist/backend/backend/src/services/task-pool/task-pool.service.js.map +1 -1
  208. package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts.map +1 -1
  209. package/dist/backend/backend/src/services/v3/cascade-request-status.js +55 -2
  210. package/dist/backend/backend/src/services/v3/cascade-request-status.js.map +1 -1
  211. package/dist/backend/backend/src/services/v3/mission-executor.service.d.ts.map +1 -1
  212. package/dist/backend/backend/src/services/v3/mission-executor.service.js +9 -1
  213. package/dist/backend/backend/src/services/v3/mission-executor.service.js.map +1 -1
  214. package/dist/backend/backend/src/services/v3/request-decompose.subscriber.d.ts.map +1 -1
  215. package/dist/backend/backend/src/services/v3/request-decompose.subscriber.js +28 -3
  216. package/dist/backend/backend/src/services/v3/request-decompose.subscriber.js.map +1 -1
  217. package/dist/backend/backend/src/services/v3/request-sla.subscriber.d.ts.map +1 -1
  218. package/dist/backend/backend/src/services/v3/request-sla.subscriber.js +5 -2
  219. package/dist/backend/backend/src/services/v3/request-sla.subscriber.js.map +1 -1
  220. package/dist/backend/backend/src/services/v3/request-status-update.subscriber.d.ts.map +1 -1
  221. package/dist/backend/backend/src/services/v3/request-status-update.subscriber.js +57 -15
  222. package/dist/backend/backend/src/services/v3/request-status-update.subscriber.js.map +1 -1
  223. package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts +39 -0
  224. package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts.map +1 -1
  225. package/dist/backend/backend/src/services/v3/trigger-engine.service.js +81 -0
  226. package/dist/backend/backend/src/services/v3/trigger-engine.service.js.map +1 -1
  227. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.d.ts +17 -1
  228. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.d.ts.map +1 -1
  229. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js +22 -3
  230. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js.map +1 -1
  231. package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.d.ts +1 -1
  232. package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.d.ts.map +1 -1
  233. package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.js +26 -10
  234. package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.js.map +1 -1
  235. package/dist/backend/backend/src/services/workflow/cron-task.service.d.ts.map +1 -1
  236. package/dist/backend/backend/src/services/workflow/cron-task.service.js +68 -5
  237. package/dist/backend/backend/src/services/workflow/cron-task.service.js.map +1 -1
  238. package/dist/backend/backend/src/services/workflow/team-identifier-resolver.d.ts +44 -0
  239. package/dist/backend/backend/src/services/workflow/team-identifier-resolver.d.ts.map +1 -0
  240. package/dist/backend/backend/src/services/workflow/team-identifier-resolver.js +57 -0
  241. package/dist/backend/backend/src/services/workflow/team-identifier-resolver.js.map +1 -0
  242. package/dist/backend/backend/src/types/credential.types.d.ts +17 -1
  243. package/dist/backend/backend/src/types/credential.types.d.ts.map +1 -1
  244. package/dist/backend/backend/src/types/credential.types.js +15 -5
  245. package/dist/backend/backend/src/types/credential.types.js.map +1 -1
  246. package/dist/backend/backend/src/types/cron-task.types.d.ts +17 -0
  247. package/dist/backend/backend/src/types/cron-task.types.d.ts.map +1 -1
  248. package/dist/backend/backend/src/types/event-bus.types.d.ts +1 -1
  249. package/dist/backend/backend/src/types/event-bus.types.d.ts.map +1 -1
  250. package/dist/backend/backend/src/types/event-bus.types.js +12 -0
  251. package/dist/backend/backend/src/types/event-bus.types.js.map +1 -1
  252. package/dist/backend/backend/src/types/intent-task.types.d.ts +10 -13
  253. package/dist/backend/backend/src/types/intent-task.types.d.ts.map +1 -1
  254. package/dist/backend/backend/src/types/intent-task.types.js +4 -1
  255. package/dist/backend/backend/src/types/intent-task.types.js.map +1 -1
  256. package/dist/backend/backend/src/types/memory.types.d.ts +94 -1
  257. package/dist/backend/backend/src/types/memory.types.d.ts.map +1 -1
  258. package/dist/backend/backend/src/types/memory.types.js.map +1 -1
  259. package/dist/backend/backend/src/types/v2/work-item.types.d.ts +23 -0
  260. package/dist/backend/backend/src/types/v2/work-item.types.d.ts.map +1 -1
  261. package/dist/backend/backend/src/types/v2/work-item.types.js.map +1 -1
  262. package/dist/backend/backend/src/utils/team.utils.d.ts +3 -1
  263. package/dist/backend/backend/src/utils/team.utils.d.ts.map +1 -1
  264. package/dist/backend/backend/src/utils/team.utils.js +26 -5
  265. package/dist/backend/backend/src/utils/team.utils.js.map +1 -1
  266. package/dist/backend/backend/src/websocket/chat-v2.gateway.d.ts +23 -0
  267. package/dist/backend/backend/src/websocket/chat-v2.gateway.d.ts.map +1 -1
  268. package/dist/backend/backend/src/websocket/chat-v2.gateway.js +56 -7
  269. package/dist/backend/backend/src/websocket/chat-v2.gateway.js.map +1 -1
  270. package/dist/backend/backend/src/websocket/chat.gateway.d.ts +19 -4
  271. package/dist/backend/backend/src/websocket/chat.gateway.d.ts.map +1 -1
  272. package/dist/backend/backend/src/websocket/chat.gateway.js +78 -63
  273. package/dist/backend/backend/src/websocket/chat.gateway.js.map +1 -1
  274. package/dist/backend/backend/src/websocket/terminal.gateway.d.ts.map +1 -1
  275. package/dist/backend/backend/src/websocket/terminal.gateway.js +10 -2
  276. package/dist/backend/backend/src/websocket/terminal.gateway.js.map +1 -1
  277. package/dist/backend/config/constants.d.ts +2 -0
  278. package/dist/backend/config/constants.d.ts.map +1 -1
  279. package/dist/backend/config/constants.js +2 -0
  280. package/dist/backend/config/constants.js.map +1 -1
  281. package/dist/backend/config/index.d.ts +1 -0
  282. package/dist/backend/config/index.d.ts.map +1 -1
  283. package/dist/cli/backend/src/constants.d.ts +13 -0
  284. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  285. package/dist/cli/backend/src/constants.js +12 -0
  286. package/dist/cli/backend/src/constants.js.map +1 -1
  287. package/dist/cli/backend/src/services/event-bus/event-bus.service.d.ts.map +1 -1
  288. package/dist/cli/backend/src/services/event-bus/event-bus.service.js +22 -11
  289. package/dist/cli/backend/src/services/event-bus/event-bus.service.js.map +1 -1
  290. package/dist/cli/backend/src/services/mcp-server.d.ts.map +1 -1
  291. package/dist/cli/backend/src/services/mcp-server.js +5 -0
  292. package/dist/cli/backend/src/services/mcp-server.js.map +1 -1
  293. package/dist/cli/backend/src/services/mcp-tool-definitions.d.ts +4 -0
  294. package/dist/cli/backend/src/services/mcp-tool-definitions.d.ts.map +1 -1
  295. package/dist/cli/backend/src/services/mcp-tool-definitions.js +12 -1
  296. package/dist/cli/backend/src/services/mcp-tool-definitions.js.map +1 -1
  297. package/dist/cli/backend/src/services/memory/memory.service.d.ts +22 -1
  298. package/dist/cli/backend/src/services/memory/memory.service.d.ts.map +1 -1
  299. package/dist/cli/backend/src/services/memory/memory.service.js +81 -3
  300. package/dist/cli/backend/src/services/memory/memory.service.js.map +1 -1
  301. package/dist/cli/backend/src/services/memory/project-memory.service.d.ts +25 -1
  302. package/dist/cli/backend/src/services/memory/project-memory.service.d.ts.map +1 -1
  303. package/dist/cli/backend/src/services/memory/project-memory.service.js +43 -0
  304. package/dist/cli/backend/src/services/memory/project-memory.service.js.map +1 -1
  305. package/dist/cli/backend/src/services/skill/skill-executor.service.d.ts.map +1 -1
  306. package/dist/cli/backend/src/services/skill/skill-executor.service.js +13 -1
  307. package/dist/cli/backend/src/services/skill/skill-executor.service.js.map +1 -1
  308. package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts +33 -2
  309. package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
  310. package/dist/cli/backend/src/services/task-pool/task-pool.service.js +160 -8
  311. package/dist/cli/backend/src/services/task-pool/task-pool.service.js.map +1 -1
  312. package/dist/cli/backend/src/types/credential.types.d.ts +17 -1
  313. package/dist/cli/backend/src/types/credential.types.d.ts.map +1 -1
  314. package/dist/cli/backend/src/types/credential.types.js +15 -5
  315. package/dist/cli/backend/src/types/credential.types.js.map +1 -1
  316. package/dist/cli/backend/src/types/event-bus.types.d.ts +1 -1
  317. package/dist/cli/backend/src/types/event-bus.types.d.ts.map +1 -1
  318. package/dist/cli/backend/src/types/event-bus.types.js +12 -0
  319. package/dist/cli/backend/src/types/event-bus.types.js.map +1 -1
  320. package/dist/cli/backend/src/types/memory.types.d.ts +94 -1
  321. package/dist/cli/backend/src/types/memory.types.d.ts.map +1 -1
  322. package/dist/cli/backend/src/types/memory.types.js.map +1 -1
  323. package/dist/cli/backend/src/types/v2/work-item.types.d.ts +23 -0
  324. package/dist/cli/backend/src/types/v2/work-item.types.d.ts.map +1 -1
  325. package/dist/cli/backend/src/types/v2/work-item.types.js.map +1 -1
  326. package/dist/cli/cli/src/commands/start.js +73 -12
  327. package/dist/cli/cli/src/commands/start.js.map +1 -1
  328. package/dist/cli/config/constants.d.ts +2 -0
  329. package/dist/cli/config/constants.d.ts.map +1 -1
  330. package/dist/cli/config/constants.js +2 -0
  331. package/dist/cli/config/constants.js.map +1 -1
  332. package/dist/cli/config/index.d.ts +1 -0
  333. package/dist/cli/config/index.d.ts.map +1 -1
  334. package/frontend/dist/assets/index-b279da34.js +4926 -0
  335. package/frontend/dist/assets/{index-b7e59b2b.css → index-c07e04c0.css} +2 -2
  336. package/frontend/dist/index.html +2 -2
  337. package/package.json +1 -1
  338. package/frontend/dist/assets/index-698305f3.js +0 -5228
@@ -10,9 +10,27 @@
10
10
  * @module services/chat-v2/chat-v2.service
11
11
  */
12
12
  import { ChannelStore } from './sqlite/channel.store.js';
13
+ import { EventEmitter } from 'events';
13
14
  import { MessageStore } from './sqlite/message.store.js';
14
15
  import { openChatDatabase } from './sqlite/chat-db.js';
15
- import { CHAT_CHANNEL_TYPES, CHAT_CONTENT_TYPES, CHAT_ERROR_CODES, ChatError, } from './types.js';
16
+ import { CHAT_CHANNEL_TYPES, CHAT_CONTENT_TYPES, CHAT_ERROR_CODES, CHAT_SENDER_TYPES, ChatError, } from './types.js';
17
+ /**
18
+ * Allowed values for `RecordTurnInput.metadata.source` — the audit-trail
19
+ * discriminator that identifies which subsystem produced the message.
20
+ *
21
+ * Per spec `2026-05-14-unified-chat-message-store.md`, every {@link
22
+ * ChatV2Service.recordTurn} caller MUST set `metadata.source` to one of
23
+ * these values. The set is intentionally closed so future audits can
24
+ * `GROUP BY metadata->>'$.source'` without surprise values.
25
+ */
26
+ export const RECORD_TURN_SOURCES = [
27
+ 'web',
28
+ 'slack',
29
+ 'pty-runtime',
30
+ 'in-process-runtime',
31
+ 'reply-tool',
32
+ 'system',
33
+ ];
16
34
  // ---------------------------------------------------------------------------
17
35
  // Service
18
36
  // ---------------------------------------------------------------------------
@@ -30,7 +48,7 @@ const DEFAULT_PRESENCE = () => ({
30
48
  * - Maps rows → DTOs.
31
49
  * - Fans out to WebSocket / adapters in later phases.
32
50
  */
33
- export class ChatV2Service {
51
+ export class ChatV2Service extends EventEmitter {
34
52
  /** Phase A spec §3.2: max mention count per message. */
35
53
  static MAX_MENTIONS_PER_MESSAGE = 50;
36
54
  /** Phase A spec §3.2: max JSON-encoded byte size of the mentions array. */
@@ -43,6 +61,7 @@ export class ChatV2Service {
43
61
  validateTeamMembership;
44
62
  now;
45
63
  constructor(options) {
64
+ super();
46
65
  this.config = options.config;
47
66
  this.db = options.db ?? openChatDatabase({ dbPath: options.config.storage.dbPath });
48
67
  this.channels = new ChannelStore(this.db);
@@ -75,6 +94,63 @@ export class ChatV2Service {
75
94
  countAllMessages() {
76
95
  return this.messages.countAll();
77
96
  }
97
+ /**
98
+ * Phase 6.0 of unified-chat-message-store spec — replacement for the
99
+ * legacy `ChatService.updateMessageMetadata`. Merges a partial
100
+ * metadata object into the stored row's `metadata` JSON column using
101
+ * SQLite's `json_patch` (atomic, server-side).
102
+ *
103
+ * No principal check — this is a server-internal mutation path used
104
+ * by reconciliation jobs (Slack delivery status updates) and never
105
+ * exposed directly to user HTTP traffic. Phase 6c will retire the
106
+ * legacy method that called this; until then it is the only callable
107
+ * write-through for the existing reconciliation code.
108
+ *
109
+ * @param messageId - Message id to update
110
+ * @param patch - Shallow metadata patch to merge
111
+ * @returns The updated message DTO, or null if no such message exists
112
+ */
113
+ updateMessageMetadata(messageId, patch) {
114
+ const row = this.messages.updateMetadata(messageId, patch);
115
+ if (!row)
116
+ return null;
117
+ return this.toMessageDTO(row, []);
118
+ }
119
+ /**
120
+ * Phase 6.0 — replacement for the legacy
121
+ * `ChatService.getMessagesWithPendingSlackDelivery`. Returns the
122
+ * messages still marked `slackDeliveryStatus='pending'` within the
123
+ * caller-supplied lookback window, used by NotifyReconciliationService
124
+ * to retry stuck Slack deliveries.
125
+ *
126
+ * @param maxAgeMs - Lookback window in milliseconds
127
+ * @returns Pending-delivery messages, newest first, capped at MAX_LIMIT
128
+ */
129
+ findMessagesWithPendingSlackDelivery(maxAgeMs) {
130
+ const rows = this.messages.findPendingSlackDelivery(maxAgeMs);
131
+ return rows.map((r) => this.toMessageDTO(r, []));
132
+ }
133
+ /**
134
+ * Phase 6.0 — replacement for the legacy `ChatService.getStatistics`.
135
+ * Aggregate counts used by the boot-time telemetry and the
136
+ * admin/audit dashboards.
137
+ *
138
+ * @returns Active/archived channel counts plus total message count
139
+ */
140
+ getStatistics() {
141
+ const activeChannels = this.db
142
+ .prepare(`SELECT COUNT(*) AS n FROM chat_channels WHERE archived_at IS NULL`)
143
+ .get().n;
144
+ const archivedChannels = this.db
145
+ .prepare(`SELECT COUNT(*) AS n FROM chat_channels WHERE archived_at IS NOT NULL`)
146
+ .get().n;
147
+ return {
148
+ totalChannels: activeChannels + archivedChannels,
149
+ activeChannels,
150
+ archivedChannels,
151
+ totalMessages: this.messages.countAll(),
152
+ };
153
+ }
78
154
  // -------------------------------------------------------------------------
79
155
  // Channel operations
80
156
  // -------------------------------------------------------------------------
@@ -164,6 +240,354 @@ export class ChatV2Service {
164
240
  });
165
241
  return this.toChannelDTO(row);
166
242
  }
243
+ /**
244
+ * Phase B-2 (2026-05-17) — create a huddle (ad-hoc multi-agent group
245
+ * channel). Creates a `type='huddle'` channel row with no team
246
+ * binding, then inserts one row per member into
247
+ * `chat_channel_members`. The dispatcher uses that roster to fan out
248
+ * subsequent user messages to every member; agents whose session is
249
+ * in the outgoing message's `mentions[]` get a "must respond"
250
+ * prompt, others get an "optional" one.
251
+ *
252
+ * @param args - name, optional purpose, member roster, owning principal
253
+ * @returns The created huddle channel as a DTO, with `members` populated
254
+ * @throws {ChatError} `validation_error` (400) when name is empty/too long,
255
+ * purpose too long, or memberSessions is empty / has too many entries.
256
+ */
257
+ createHuddle(args) {
258
+ const name = (args.name ?? '').trim();
259
+ if (name.length === 0) {
260
+ throw new ChatError(CHAT_ERROR_CODES.VALIDATION, 400, 'name is required');
261
+ }
262
+ if (name.length > this.config.maxChannelNameChars) {
263
+ throw new ChatError(CHAT_ERROR_CODES.VALIDATION, 400, `name exceeds ${this.config.maxChannelNameChars} characters`);
264
+ }
265
+ const purpose = args.purpose?.trim();
266
+ if (purpose && purpose.length > this.config.maxPurposeChars) {
267
+ throw new ChatError(CHAT_ERROR_CODES.VALIDATION, 400, `purpose exceeds ${this.config.maxPurposeChars} characters`);
268
+ }
269
+ // Dedupe + trim member sessions. We accept any non-empty trimmed
270
+ // string here — actual agent existence is validated by the
271
+ // dispatcher when it tries to resolve the session at fan-out time.
272
+ const seen = new Set();
273
+ const members = [];
274
+ for (const raw of args.memberSessions ?? []) {
275
+ const s = (raw ?? '').trim();
276
+ if (!s)
277
+ continue;
278
+ if (seen.has(s))
279
+ continue;
280
+ seen.add(s);
281
+ members.push(s);
282
+ }
283
+ if (members.length === 0) {
284
+ throw new ChatError(CHAT_ERROR_CODES.VALIDATION, 400, 'memberSessions must include at least one agent session');
285
+ }
286
+ // Defensive upper bound — a 200-member huddle would tax both the
287
+ // dispatcher fan-out and downstream rate limits. The cap can be
288
+ // raised when we have a real use case.
289
+ const MAX_HUDDLE_MEMBERS = 50;
290
+ if (members.length > MAX_HUDDLE_MEMBERS) {
291
+ throw new ChatError(CHAT_ERROR_CODES.VALIDATION, 400, `huddle exceeds the ${MAX_HUDDLE_MEMBERS}-member cap`);
292
+ }
293
+ const nowMs = this.now();
294
+ const row = this.channels.create({
295
+ // Huddle isn't 1:1-bound — keep agent_session empty (same convention
296
+ // as type='channel'). No team_id either: huddles are ad-hoc groups,
297
+ // not team-scoped surfaces.
298
+ agentSession: '',
299
+ ownerUserId: args.principal.userId,
300
+ name,
301
+ purpose: purpose || null,
302
+ type: 'huddle',
303
+ teamId: null,
304
+ projectId: null,
305
+ targetMemberId: null,
306
+ nowMs,
307
+ });
308
+ // Insert membership rows. INSERT OR IGNORE is defensive against the
309
+ // dedupe above getting bypassed (e.g., when callers reuse this
310
+ // method via the relay adapter with raw input).
311
+ const memberStmt = this.db.prepare(`INSERT OR IGNORE INTO chat_channel_members
312
+ (channel_id, member_session, joined_at)
313
+ VALUES (?, ?, ?)`);
314
+ const insertMany = this.db.transaction((rows) => {
315
+ for (const s of rows)
316
+ memberStmt.run(row.id, s, nowMs);
317
+ });
318
+ insertMany(members);
319
+ return this.toChannelDTO(row);
320
+ }
321
+ /**
322
+ * Phase B-2 — list the members of a huddle channel. Returns an empty
323
+ * array (not an error) for non-huddle channels so consumers can call
324
+ * this unconditionally during channel rendering.
325
+ *
326
+ * @param channelId - The channel to enumerate
327
+ * @param principal - The caller; used to verify ownership
328
+ * @returns Array of `{ sessionName, joinedAt }` rows
329
+ * @throws {ChatError} `not_found` (404) when the channel doesn't exist or
330
+ * isn't owned by `principal`.
331
+ */
332
+ listHuddleMembers(channelId, principal) {
333
+ const row = this.requireOwnedChannel(channelId, principal);
334
+ if (row.type !== 'huddle')
335
+ return [];
336
+ return this.queryHuddleMembers(channelId);
337
+ }
338
+ /**
339
+ * Phase B-2 — dispatcher-facing roster lookup. Returns the session
340
+ * names of every member in a huddle, in `joined_at ASC` order.
341
+ * Unlike {@link listHuddleMembers} this is NOT principal-scoped:
342
+ * the dispatcher runs server-side and already holds the channel
343
+ * (it just persisted a message into it). Returns an empty array for
344
+ * non-huddle channels or unknown ids so the dispatcher's
345
+ * `members.length === 0` skip path stays clean.
346
+ *
347
+ * @param channelId - The huddle channel id
348
+ * @returns Array of agent session names
349
+ */
350
+ queryHuddleMembersForDispatch(channelId) {
351
+ return this.queryHuddleMembers(channelId).map((m) => m.sessionName);
352
+ }
353
+ /** Internal: read members straight from the DB (no ownership check). */
354
+ queryHuddleMembers(channelId) {
355
+ const rows = this.db
356
+ .prepare(`SELECT member_session AS sessionName, joined_at AS joinedAt
357
+ FROM chat_channel_members
358
+ WHERE channel_id = ?
359
+ ORDER BY joined_at ASC`)
360
+ .all(channelId);
361
+ return rows.map((r) => ({ sessionName: r.sessionName, joinedAt: r.joinedAt }));
362
+ }
363
+ /**
364
+ * Idempotent DM channel lookup-or-create for the /agents page.
365
+ *
366
+ * Returns the most-recent active DM channel owned by `principal.userId`
367
+ * and bound to `agentSession`; creates a new one when none exists. The
368
+ * caller is the human owner (auth principal), unlike
369
+ * {@link ensureChannelForLegacyConversation} which runs as `'system'`
370
+ * for server-internal bridge paths.
371
+ *
372
+ * Used by `POST /api/chat/channels/dm/ensure` so the /agents page can
373
+ * map "user clicked an agent in the directory" → "send/receive messages
374
+ * on this channel" without leaking duplicate DMs every time the page
375
+ * is reloaded.
376
+ *
377
+ * @param args - Lookup-or-create args
378
+ * @returns The channel DTO plus a `created` flag (true when a new row
379
+ * was inserted; false when an existing row was reused).
380
+ * @throws {ChatError} `validation_error` (400) on empty `agentSession`
381
+ * or oversize `name` / `purpose`.
382
+ */
383
+ ensureDmChannel(args) {
384
+ const agentSession = (args.agentSession ?? '').trim();
385
+ if (agentSession.length === 0) {
386
+ throw new ChatError(CHAT_ERROR_CODES.VALIDATION, 400, 'agentSession is required');
387
+ }
388
+ const existing = this.channels.findActiveDmByOwnerAndAgent(args.principal.userId, agentSession);
389
+ if (existing) {
390
+ return { channel: this.toChannelDTO(existing), created: false };
391
+ }
392
+ // Fall through to createChannel so the full validation + tenant
393
+ // checks (purpose length, name length, etc.) run consistently with
394
+ // the public POST /channels endpoint.
395
+ const channel = this.createChannel({
396
+ agentSession,
397
+ name: (args.name ?? agentSession).trim(),
398
+ purpose: args.purpose,
399
+ principal: args.principal,
400
+ type: 'dm',
401
+ });
402
+ return { channel, created: true };
403
+ }
404
+ /**
405
+ * Server-internal idempotent helper used by migration / bridge code to
406
+ * map a legacy conversationId (e.g. `slack-D0AC7-1234`, `web-conv-abc`)
407
+ * onto a chat-v2 channel row with the conversationId as the primary key.
408
+ *
409
+ * Unlike {@link createChannel}, this method:
410
+ * - Returns the existing channel when one already lives at the given
411
+ * id — idempotent for runtimes that call it before every recordTurn.
412
+ * - Accepts a synthetic owner (`'system'`) for paths where no human
413
+ * principal is on the call stack (PTY finish hooks, Slack inbound
414
+ * bridge, in-process runtime auto-route).
415
+ * - Sets `type='dm'` so the channel matches the conversation-per-thread
416
+ * legacy model the user approved (spec Option B).
417
+ *
418
+ * The `agent_already_bound` failure mode that existed in chat-v2 Phase A
419
+ * does not apply — the `uq_channel_agent_dm_active` index was dropped
420
+ * per the unified-chat-message-store spec exactly so this helper can
421
+ * lazy-create N concurrent DM channels for a single agent.
422
+ *
423
+ * @param args - Legacy bridge args
424
+ * @returns The existing or freshly created channel DTO
425
+ * @throws {ChatError} `validation_error` on missing id / agentSession
426
+ *
427
+ * @example
428
+ * ```typescript
429
+ * // Called from `routeInProcessResponseToChat` before `recordTurn`:
430
+ * const channel = chatV2.ensureChannelForLegacyConversation({
431
+ * conversationId: 'slack-D0AC7-1700000000.000111',
432
+ * agentSession: 'crewly-orc',
433
+ * });
434
+ * chatV2.recordTurn({ channelId: channel.id, ... });
435
+ * ```
436
+ */
437
+ ensureChannelForLegacyConversation(args) {
438
+ const conversationId = (args.conversationId ?? '').trim();
439
+ if (conversationId.length === 0) {
440
+ throw new ChatError(CHAT_ERROR_CODES.VALIDATION, 400, 'conversationId is required');
441
+ }
442
+ const agentSession = (args.agentSession ?? '').trim();
443
+ if (agentSession.length === 0) {
444
+ throw new ChatError(CHAT_ERROR_CODES.VALIDATION, 400, 'agentSession is required');
445
+ }
446
+ const existing = this.channels.getById(conversationId);
447
+ if (existing) {
448
+ return this.toChannelDTO(existing);
449
+ }
450
+ const name = (args.name ?? conversationId).trim();
451
+ const ownerUserId = (args.ownerUserId ?? 'system').trim();
452
+ const row = this.channels.create({
453
+ id: conversationId,
454
+ agentSession,
455
+ ownerUserId,
456
+ name,
457
+ purpose: null,
458
+ type: 'dm',
459
+ teamId: null,
460
+ projectId: null,
461
+ targetMemberId: null,
462
+ nowMs: this.now(),
463
+ });
464
+ return this.toChannelDTO(row);
465
+ }
466
+ /**
467
+ * Phase 5 of unified-chat-message-store spec — idempotent import of one
468
+ * legacy conversation file (`~/.crewly/chat/<conversationId>.json`)
469
+ * into the chat-v2 SQLite store. Each legacy message becomes one
470
+ * `chat_messages` row keyed by a deterministic `clientMessageId` so
471
+ * re-running the import is safe — the underlying `MessageStore`
472
+ * dedups on `(channel_id, clientMessageId)`.
473
+ *
474
+ * Designed as a service method (not a free function) so the CLI
475
+ * migration script can call it per file and so callers can unit-test
476
+ * the mapping in isolation.
477
+ *
478
+ * The mapping:
479
+ * - `conversation.id` → `chat_channels.id` (via
480
+ * {@link ensureChannelForLegacyConversation}). `agentSession`
481
+ * defaults to `'crewly-orc'` because every legacy conversation was
482
+ * a DM between the user and the orchestrator.
483
+ * - `messages[].from.type === 'user'` → `senderType: 'user'`
484
+ * - `messages[].from.type === 'orchestrator'` (or 'agent') →
485
+ * `senderType: 'agent'`, `senderId: 'crewly-orc'`
486
+ * - Anything else → `senderType: 'system'`, `senderId: 'system'`
487
+ * - `messages[].id` → `clientMessageId = 'legacy-' + msg.id` for
488
+ * stable idempotency across re-runs.
489
+ * - `messages[].metadata.source` (legacy slack/web) → carried
490
+ * through; `recordTurn`'s required outer `metadata.source` is set
491
+ * to `'system'` to identify the migration as the writer.
492
+ *
493
+ * @param input - Parsed legacy JSON (the entire file body)
494
+ * @returns Per-message outcome (imported vs deduped) + the channel id
495
+ *
496
+ * @example
497
+ * ```typescript
498
+ * const json = JSON.parse(await readFile(filePath, 'utf-8'));
499
+ * const result = chatV2.importLegacyConversation(json);
500
+ * console.log(`Imported ${result.imported} new, ${result.deduped} dedup`);
501
+ * ```
502
+ */
503
+ importLegacyConversation(input) {
504
+ if (!input?.conversation?.id) {
505
+ throw new ChatError(CHAT_ERROR_CODES.VALIDATION, 400, 'legacy conversation.id is required');
506
+ }
507
+ if (!Array.isArray(input.messages)) {
508
+ throw new ChatError(CHAT_ERROR_CODES.VALIDATION, 400, 'legacy messages must be an array');
509
+ }
510
+ const channel = this.ensureChannelForLegacyConversation({
511
+ conversationId: input.conversation.id,
512
+ agentSession: 'crewly-orc',
513
+ name: input.conversation.id,
514
+ });
515
+ let imported = 0;
516
+ let deduped = 0;
517
+ let skipped = 0;
518
+ const skippedReasons = [];
519
+ for (let i = 0; i < input.messages.length; i++) {
520
+ const msg = input.messages[i];
521
+ if (!msg?.id) {
522
+ skipped++;
523
+ skippedReasons.push({ index: i, reason: 'missing-id', id: msg?.id });
524
+ continue;
525
+ }
526
+ if (typeof msg.content !== 'string' || msg.content.length === 0) {
527
+ skipped++;
528
+ skippedReasons.push({ index: i, reason: 'empty-content', id: msg.id });
529
+ continue;
530
+ }
531
+ const fromType = (msg.from?.type ?? '').toLowerCase();
532
+ let senderType;
533
+ let senderId;
534
+ if (fromType === 'user') {
535
+ senderType = 'user';
536
+ senderId =
537
+ (typeof msg.metadata?.userId === 'string' && msg.metadata.userId) ||
538
+ msg.from?.name ||
539
+ 'legacy-user';
540
+ }
541
+ else if (fromType === 'agent' || fromType === 'orchestrator') {
542
+ senderType = 'agent';
543
+ senderId = 'crewly-orc';
544
+ }
545
+ else {
546
+ senderType = 'system';
547
+ senderId = 'system';
548
+ }
549
+ const result = this.recordTurn({
550
+ channelId: channel.id,
551
+ senderType,
552
+ senderId,
553
+ content: msg.content,
554
+ clientMessageId: `legacy-${msg.id}`,
555
+ metadata: {
556
+ source: 'system',
557
+ // Carry through legacy metadata for forensic completeness —
558
+ // future audits can still see "this row was originally a slack
559
+ // inbound" via metadata.legacySource etc.
560
+ legacySource: typeof msg.metadata?.source === 'string' ? msg.metadata.source : undefined,
561
+ legacyMessageId: msg.id,
562
+ legacyTimestamp: msg.timestamp,
563
+ },
564
+ });
565
+ if (result.deduped) {
566
+ deduped++;
567
+ }
568
+ else {
569
+ imported++;
570
+ }
571
+ }
572
+ if (skipped > 0) {
573
+ // Surface malformed legacy rows so the migration operator can
574
+ // decide whether to repair the source JSON before re-running, or
575
+ // accept that some history is unrecoverable. Without this log the
576
+ // earlier silent-skip behavior turned data-loss into a counter
577
+ // mismatch that nobody noticed. ChatV2Service has no injected
578
+ // logger; the migration runs as a CLI script so console output
579
+ // is the right sink.
580
+ // eslint-disable-next-line no-console
581
+ console.warn(`[chat-v2] importLegacyConversation: skipped ${skipped}/${input.messages.length} malformed row(s) in ${input.conversation.id}`, {
582
+ channelId: channel.id,
583
+ skipped,
584
+ totalRows: input.messages.length,
585
+ reasons: skippedReasons.slice(0, 10),
586
+ truncated: skippedReasons.length > 10,
587
+ });
588
+ }
589
+ return { channelId: channel.id, imported, deduped, skipped };
590
+ }
167
591
  /**
168
592
  * List channels owned by the caller.
169
593
  *
@@ -220,6 +644,87 @@ export class ChatV2Service {
220
644
  this.requireOwnedChannel(channelId, principal);
221
645
  return this.channels.archive(channelId, this.now());
222
646
  }
647
+ /**
648
+ * Phase 6.0b — clear the `archived_at` flag on a channel. Inverse of
649
+ * {@link archiveChannel}; required to retire the legacy
650
+ * `unarchiveConversation` route.
651
+ *
652
+ * @param channelId - The channel to unarchive
653
+ * @param principal - Auth principal (must own the channel)
654
+ * @returns True if newly unarchived, false if already active
655
+ * @throws {ChatError} `channel_not_found` (404)
656
+ */
657
+ unarchiveChannel(channelId, principal) {
658
+ this.requireOwnedChannel(channelId, principal);
659
+ return this.channels.unarchive(channelId);
660
+ }
661
+ /**
662
+ * Phase 6.0b — rename a channel. Replaces the legacy
663
+ * `updateConversationTitle` route. Server validates the same name
664
+ * constraints as `createChannel`.
665
+ *
666
+ * @param channelId - The channel to rename
667
+ * @param name - New name (trimmed, ≤ maxChannelNameChars)
668
+ * @param principal - Auth principal (must own the channel)
669
+ * @returns The renamed channel DTO
670
+ * @throws {ChatError} `validation_error` (400) on empty / oversize name,
671
+ * `channel_not_found` (404)
672
+ */
673
+ renameChannel(channelId, name, principal) {
674
+ const trimmed = (name ?? '').trim();
675
+ if (trimmed.length === 0) {
676
+ throw new ChatError(CHAT_ERROR_CODES.VALIDATION, 400, 'name is required');
677
+ }
678
+ if (trimmed.length > this.config.maxChannelNameChars) {
679
+ throw new ChatError(CHAT_ERROR_CODES.VALIDATION, 400, `name exceeds ${this.config.maxChannelNameChars} characters`);
680
+ }
681
+ const row = this.requireOwnedChannel(channelId, principal);
682
+ this.channels.rename(channelId, trimmed);
683
+ return this.toChannelDTO({ ...row, name: trimmed });
684
+ }
685
+ /**
686
+ * Phase 6.0b — hard-delete a channel and all its messages. Distinct
687
+ * from {@link archiveChannel} (soft delete). Replaces the legacy
688
+ * `deleteConversation` route. Uses SQLite FK cascade so the row
689
+ * deletion atomically removes child messages.
690
+ *
691
+ * @param channelId - The channel to delete
692
+ * @param principal - Auth principal (must own the channel)
693
+ * @returns True if removed, false if the channel didn't exist
694
+ * @throws {ChatError} `channel_not_found` (404)
695
+ */
696
+ deleteChannel(channelId, principal) {
697
+ this.requireOwnedChannel(channelId, principal);
698
+ return this.channels.hardDelete(channelId);
699
+ }
700
+ /**
701
+ * Phase 6.0b — delete all messages in a channel while keeping the
702
+ * channel row. Replaces the legacy `clearConversation` route. Useful
703
+ * when the user wants a "fresh start" without losing the channel
704
+ * itself (and its bookkeeping like `agent_session` binding).
705
+ *
706
+ * @param channelId - The channel to clear
707
+ * @param principal - Auth principal (must own the channel)
708
+ * @returns Number of messages deleted
709
+ * @throws {ChatError} `channel_not_found` (404)
710
+ */
711
+ clearChannel(channelId, principal) {
712
+ this.requireOwnedChannel(channelId, principal);
713
+ return this.messages.deleteAllByChannel(channelId);
714
+ }
715
+ /**
716
+ * Phase 6.0b — count messages in a single channel. Replaces the
717
+ * legacy `getMessageCount` filtered to one conversation.
718
+ *
719
+ * @param channelId - The channel to count
720
+ * @param principal - Auth principal (must own the channel)
721
+ * @returns Message count (0 for empty channels)
722
+ * @throws {ChatError} `channel_not_found` (404)
723
+ */
724
+ countChannelMessages(channelId, principal) {
725
+ this.requireOwnedChannel(channelId, principal);
726
+ return this.messages.count(channelId);
727
+ }
223
728
  // -------------------------------------------------------------------------
224
729
  // Message operations
225
730
  // -------------------------------------------------------------------------
@@ -268,7 +773,112 @@ export class ChatV2Service {
268
773
  threadId,
269
774
  nowMs: this.now(),
270
775
  });
271
- return this.toMessageDTO(persisted, args.attachments ?? []);
776
+ const dto = this.toMessageDTO(persisted, args.attachments ?? []);
777
+ // Phase 6c: broadcast so the WebSocket gateway (and any other
778
+ // in-process subscribers) can fan the new message out to connected
779
+ // clients. The legacy ChatService.EventEmitter contract is now
780
+ // owned by chat-v2 directly.
781
+ this.emit('chat_message', dto);
782
+ return dto;
783
+ }
784
+ /**
785
+ * Canonical server-internal write entry for chat messages.
786
+ *
787
+ * Unlike {@link sendMessage}, which derives `sender_type` / `sender_id`
788
+ * from an authenticated request principal, `recordTurn` is the path
789
+ * used by runtimes, controllers, and bridges that have already
790
+ * resolved exactly who the sender is — e.g.:
791
+ *
792
+ * - In-process agent runtime finishing a turn
793
+ * - PTY runtime emitting a complete reply
794
+ * - Slack inbound bridge persisting a user DM
795
+ * - `/slack/send` controller persisting the agent's outbound reply
796
+ *
797
+ * Per spec `2026-05-14-unified-chat-message-store.md`, every chat
798
+ * write in the system funnels through this method. No caller should
799
+ * write to {@link MessageStore} directly; no caller should reach
800
+ * into legacy {@link ChatService} (Phase 6 retires it).
801
+ *
802
+ * Idempotent via `clientMessageId` — the underlying store dedups by
803
+ * `(channel_id, clientMessageId)` and returns the existing row with
804
+ * `deduped=true` instead of inserting a duplicate.
805
+ *
806
+ * @param input - Turn payload (channel, sender, content, metadata)
807
+ * @returns The persisted message DTO + dedupe flag
808
+ * @throws {ChatError} `channel_not_found` (404) if the channel is missing
809
+ * @throws {ChatError} `validation_error` (400) on empty content or invalid contentType
810
+ * @throws {ChatError} `payload_too_large` (413) if content exceeds maxMessageBytes
811
+ *
812
+ * @example
813
+ * ```typescript
814
+ * const { message, deduped } = chatV2.recordTurn({
815
+ * channelId: 'slack-D0AC7-1234',
816
+ * senderType: 'agent',
817
+ * senderId: 'crewly-orc',
818
+ * content: 'Hello!',
819
+ * clientMessageId: 'agent-finish-2026-05-14T22:30:00Z',
820
+ * metadata: {
821
+ * source: 'in-process-runtime',
822
+ * runtime: 'crewly-agent',
823
+ * slackChannelId: 'D0AC7',
824
+ * slackThreadTs: '1234',
825
+ * },
826
+ * });
827
+ * ```
828
+ */
829
+ recordTurn(input) {
830
+ const content = input.content ?? '';
831
+ if (content.length === 0) {
832
+ throw new ChatError(CHAT_ERROR_CODES.VALIDATION, 400, 'content is required');
833
+ }
834
+ const byteLen = Buffer.byteLength(content, 'utf-8');
835
+ if (byteLen > this.config.maxMessageBytes) {
836
+ throw new ChatError(CHAT_ERROR_CODES.PAYLOAD_TOO_LARGE, 413, `content exceeds max bytes (${this.config.maxMessageBytes})`, { maxBytes: this.config.maxMessageBytes, yourBytes: byteLen });
837
+ }
838
+ const contentType = input.contentType ?? 'markdown';
839
+ if (!CHAT_CONTENT_TYPES.includes(contentType)) {
840
+ throw new ChatError(CHAT_ERROR_CODES.VALIDATION, 400, `unknown contentType: ${contentType}`);
841
+ }
842
+ if (!CHAT_SENDER_TYPES.includes(input.senderType)) {
843
+ throw new ChatError(CHAT_ERROR_CODES.VALIDATION, 400, `unknown senderType: ${input.senderType}`);
844
+ }
845
+ if (!input.senderId || input.senderId.length === 0) {
846
+ throw new ChatError(CHAT_ERROR_CODES.VALIDATION, 400, 'senderId is required');
847
+ }
848
+ const mentions = this.validateMentions(input.mentions);
849
+ const threadId = this.validateThreadId(input.threadId, input.channelId);
850
+ // `metadata.source` is the audit-trail discriminator that lets
851
+ // future tooling tell "this message came from PTY" vs "from
852
+ // in-process runtime" vs "from /slack/send" without parsing
853
+ // content. Spec success criterion #4 depends on this tag being
854
+ // present for every recordTurn caller.
855
+ const metadata = { ...(input.metadata ?? {}) };
856
+ if (!metadata.source) {
857
+ throw new ChatError(CHAT_ERROR_CODES.VALIDATION, 400, 'metadata.source is required for recordTurn (audit trail)');
858
+ }
859
+ if (!RECORD_TURN_SOURCES.includes(metadata.source)) {
860
+ throw new ChatError(CHAT_ERROR_CODES.VALIDATION, 400, `unknown metadata.source: ${String(metadata.source)}`, { allowed: RECORD_TURN_SOURCES });
861
+ }
862
+ const { row: persisted, deduped } = this.messages.insert({
863
+ channelId: input.channelId,
864
+ senderType: input.senderType,
865
+ senderId: input.senderId,
866
+ content,
867
+ contentType,
868
+ clientMessageId: input.clientMessageId,
869
+ mentions,
870
+ threadId,
871
+ metadata,
872
+ nowMs: this.now(),
873
+ });
874
+ const dto = this.toMessageDTO(persisted, []);
875
+ // Phase 6c: emit only for freshly inserted rows. Skipping dedup hits
876
+ // prevents replay-loop subscribers from seeing the same message twice
877
+ // on idempotent retries.
878
+ if (!deduped) {
879
+ this.emit('chat_message', dto);
880
+ }
881
+ return { message: dto, deduped };
272
882
  }
273
883
  /**
274
884
  * Phase A — validate the mentions array passed to sendMessage.
@@ -433,6 +1043,12 @@ export class ChatV2Service {
433
1043
  teamId: row.team_id ?? undefined,
434
1044
  projectId: row.project_id ?? undefined,
435
1045
  targetMemberId: row.target_member_id ?? undefined,
1046
+ // Phase B-2: huddle channels surface their roster inline so the
1047
+ // Portal can render member avatars without a second round-trip.
1048
+ // Non-huddle rows leave this undefined.
1049
+ ...(channelType === 'huddle'
1050
+ ? { members: this.queryHuddleMembers(row.id) }
1051
+ : {}),
436
1052
  };
437
1053
  }
438
1054
  /** Map a message row to the wire DTO. Attachments passed in by the caller. */