@jonit-dev/night-watch-cli 1.7.29 → 1.7.31

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 (384) hide show
  1. package/bin/night-watch.mjs +1 -1
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/{src/cli.js → cli.js} +1 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/{src/commands → commands}/audit.d.ts +2 -2
  7. package/dist/commands/audit.d.ts.map +1 -0
  8. package/dist/commands/audit.js +105 -0
  9. package/dist/commands/audit.js.map +1 -0
  10. package/dist/{src/commands → commands}/board.d.ts +1 -1
  11. package/dist/commands/board.d.ts.map +1 -0
  12. package/dist/commands/board.js +664 -0
  13. package/dist/commands/board.js.map +1 -0
  14. package/dist/{src/commands → commands}/cancel.d.ts +3 -3
  15. package/dist/commands/cancel.d.ts.map +1 -0
  16. package/dist/{src/commands → commands}/cancel.js +18 -20
  17. package/dist/commands/cancel.js.map +1 -0
  18. package/dist/commands/dashboard/tab-actions.d.ts.map +1 -0
  19. package/dist/commands/dashboard/tab-actions.js.map +1 -0
  20. package/dist/{src/commands → commands}/dashboard/tab-config.d.ts +3 -3
  21. package/dist/commands/dashboard/tab-config.d.ts.map +1 -0
  22. package/dist/{src/commands → commands}/dashboard/tab-config.js +250 -187
  23. package/dist/commands/dashboard/tab-config.js.map +1 -0
  24. package/dist/{src/commands → commands}/dashboard/tab-logs.d.ts +1 -1
  25. package/dist/commands/dashboard/tab-logs.d.ts.map +1 -0
  26. package/dist/{src/commands → commands}/dashboard/tab-logs.js +62 -38
  27. package/dist/commands/dashboard/tab-logs.js.map +1 -0
  28. package/dist/{src/commands → commands}/dashboard/tab-schedules.d.ts +1 -1
  29. package/dist/commands/dashboard/tab-schedules.d.ts.map +1 -0
  30. package/dist/{src/commands → commands}/dashboard/tab-schedules.js +85 -76
  31. package/dist/commands/dashboard/tab-schedules.js.map +1 -0
  32. package/dist/{src/commands → commands}/dashboard/tab-status.d.ts +7 -7
  33. package/dist/commands/dashboard/tab-status.d.ts.map +1 -0
  34. package/dist/{src/commands → commands}/dashboard/tab-status.js +98 -95
  35. package/dist/commands/dashboard/tab-status.js.map +1 -0
  36. package/dist/{src/commands → commands}/dashboard/types.d.ts +3 -4
  37. package/dist/commands/dashboard/types.d.ts.map +1 -0
  38. package/dist/commands/dashboard/types.js.map +1 -0
  39. package/dist/{src/commands → commands}/dashboard.d.ts +2 -2
  40. package/dist/commands/dashboard.d.ts.map +1 -0
  41. package/dist/{src/commands → commands}/dashboard.js +32 -33
  42. package/dist/commands/dashboard.js.map +1 -0
  43. package/dist/{src/commands → commands}/doctor.d.ts +2 -2
  44. package/dist/commands/doctor.d.ts.map +1 -0
  45. package/dist/{src/commands → commands}/doctor.js +40 -43
  46. package/dist/commands/doctor.js.map +1 -0
  47. package/dist/{src/commands → commands}/history.d.ts +1 -1
  48. package/dist/commands/history.d.ts.map +1 -0
  49. package/dist/{src/commands → commands}/history.js +11 -18
  50. package/dist/commands/history.js.map +1 -0
  51. package/dist/{src/commands → commands}/init.d.ts +1 -1
  52. package/dist/commands/init.d.ts.map +1 -0
  53. package/dist/{src/commands → commands}/init.js +62 -36
  54. package/dist/commands/init.js.map +1 -0
  55. package/dist/{src/commands → commands}/install.d.ts +2 -2
  56. package/dist/commands/install.d.ts.map +1 -0
  57. package/dist/{src/commands → commands}/install.js +48 -50
  58. package/dist/commands/install.js.map +1 -0
  59. package/dist/{src/commands → commands}/logs.d.ts +1 -1
  60. package/dist/commands/logs.d.ts.map +1 -0
  61. package/dist/{src/commands → commands}/logs.js +29 -30
  62. package/dist/commands/logs.js.map +1 -0
  63. package/dist/{src/commands → commands}/prd-state.d.ts +1 -1
  64. package/dist/commands/prd-state.d.ts.map +1 -0
  65. package/dist/{src/commands → commands}/prd-state.js +14 -14
  66. package/dist/commands/prd-state.js.map +1 -0
  67. package/dist/{src/commands → commands}/prd.d.ts +1 -1
  68. package/dist/commands/prd.d.ts.map +1 -0
  69. package/dist/{src/commands → commands}/prd.js +57 -66
  70. package/dist/commands/prd.js.map +1 -0
  71. package/dist/{src/commands → commands}/prds.d.ts +1 -1
  72. package/dist/commands/prds.d.ts.map +1 -0
  73. package/dist/{src/commands → commands}/prds.js +51 -53
  74. package/dist/commands/prds.js.map +1 -0
  75. package/dist/{src/commands → commands}/prs.d.ts +1 -1
  76. package/dist/commands/prs.d.ts.map +1 -0
  77. package/dist/{src/commands → commands}/prs.js +22 -24
  78. package/dist/commands/prs.js.map +1 -0
  79. package/dist/{src/commands → commands}/qa.d.ts +2 -2
  80. package/dist/commands/qa.d.ts.map +1 -0
  81. package/dist/{src/commands → commands}/qa.js +50 -51
  82. package/dist/commands/qa.js.map +1 -0
  83. package/dist/{src/commands → commands}/retry.d.ts +1 -1
  84. package/dist/commands/retry.d.ts.map +1 -0
  85. package/dist/{src/commands → commands}/retry.js +9 -10
  86. package/dist/commands/retry.js.map +1 -0
  87. package/dist/{src/commands → commands}/review.d.ts +2 -2
  88. package/dist/commands/review.d.ts.map +1 -0
  89. package/dist/{src/commands → commands}/review.js +68 -59
  90. package/dist/commands/review.js.map +1 -0
  91. package/dist/{src/commands → commands}/run.d.ts +2 -2
  92. package/dist/commands/run.d.ts.map +1 -0
  93. package/dist/{src/commands → commands}/run.js +87 -83
  94. package/dist/commands/run.js.map +1 -0
  95. package/dist/{src/commands → commands}/serve.d.ts +2 -2
  96. package/dist/commands/serve.d.ts.map +1 -0
  97. package/dist/{src/commands → commands}/serve.js +18 -18
  98. package/dist/commands/serve.js.map +1 -0
  99. package/dist/{src/commands → commands}/slice.d.ts +2 -2
  100. package/dist/commands/slice.d.ts.map +1 -0
  101. package/dist/{src/commands → commands}/slice.js +50 -46
  102. package/dist/commands/slice.js.map +1 -0
  103. package/dist/{src/commands → commands}/state.d.ts +1 -1
  104. package/dist/commands/state.d.ts.map +1 -0
  105. package/dist/{src/commands → commands}/state.js +20 -22
  106. package/dist/commands/state.js.map +1 -0
  107. package/dist/{src/commands → commands}/status.d.ts +1 -1
  108. package/dist/commands/status.d.ts.map +1 -0
  109. package/dist/{src/commands → commands}/status.js +75 -54
  110. package/dist/commands/status.js.map +1 -0
  111. package/dist/{src/commands → commands}/uninstall.d.ts +1 -1
  112. package/dist/commands/uninstall.d.ts.map +1 -0
  113. package/dist/{src/commands → commands}/uninstall.js +12 -14
  114. package/dist/commands/uninstall.js.map +1 -0
  115. package/dist/{src/commands → commands}/update.d.ts +1 -1
  116. package/dist/commands/update.d.ts.map +1 -0
  117. package/dist/{src/commands → commands}/update.js +23 -23
  118. package/dist/commands/update.js.map +1 -0
  119. package/package.json +18 -42
  120. package/LICENSE +0 -21
  121. package/README.md +0 -132
  122. package/dist/shared/types.d.ts +0 -226
  123. package/dist/shared/types.d.ts.map +0 -1
  124. package/dist/shared/types.js +0 -7
  125. package/dist/shared/types.js.map +0 -1
  126. package/dist/src/agents/soul-compiler.d.ts +0 -11
  127. package/dist/src/agents/soul-compiler.d.ts.map +0 -1
  128. package/dist/src/agents/soul-compiler.js +0 -157
  129. package/dist/src/agents/soul-compiler.js.map +0 -1
  130. package/dist/src/board/factory.d.ts +0 -3
  131. package/dist/src/board/factory.d.ts.map +0 -1
  132. package/dist/src/board/factory.js +0 -10
  133. package/dist/src/board/factory.js.map +0 -1
  134. package/dist/src/board/providers/github-graphql.d.ts +0 -16
  135. package/dist/src/board/providers/github-graphql.d.ts.map +0 -1
  136. package/dist/src/board/providers/github-graphql.js +0 -43
  137. package/dist/src/board/providers/github-graphql.js.map +0 -1
  138. package/dist/src/board/providers/github-projects.d.ts +0 -51
  139. package/dist/src/board/providers/github-projects.d.ts.map +0 -1
  140. package/dist/src/board/providers/github-projects.js +0 -672
  141. package/dist/src/board/providers/github-projects.js.map +0 -1
  142. package/dist/src/board/types.d.ts +0 -60
  143. package/dist/src/board/types.d.ts.map +0 -1
  144. package/dist/src/board/types.js +0 -4
  145. package/dist/src/board/types.js.map +0 -1
  146. package/dist/src/cli.d.ts +0 -3
  147. package/dist/src/cli.d.ts.map +0 -1
  148. package/dist/src/cli.js.map +0 -1
  149. package/dist/src/commands/audit.d.ts.map +0 -1
  150. package/dist/src/commands/audit.js +0 -98
  151. package/dist/src/commands/audit.js.map +0 -1
  152. package/dist/src/commands/board.d.ts.map +0 -1
  153. package/dist/src/commands/board.js +0 -294
  154. package/dist/src/commands/board.js.map +0 -1
  155. package/dist/src/commands/cancel.d.ts.map +0 -1
  156. package/dist/src/commands/cancel.js.map +0 -1
  157. package/dist/src/commands/dashboard/tab-actions.d.ts.map +0 -1
  158. package/dist/src/commands/dashboard/tab-actions.js.map +0 -1
  159. package/dist/src/commands/dashboard/tab-config.d.ts.map +0 -1
  160. package/dist/src/commands/dashboard/tab-config.js.map +0 -1
  161. package/dist/src/commands/dashboard/tab-logs.d.ts.map +0 -1
  162. package/dist/src/commands/dashboard/tab-logs.js.map +0 -1
  163. package/dist/src/commands/dashboard/tab-schedules.d.ts.map +0 -1
  164. package/dist/src/commands/dashboard/tab-schedules.js.map +0 -1
  165. package/dist/src/commands/dashboard/tab-status.d.ts.map +0 -1
  166. package/dist/src/commands/dashboard/tab-status.js.map +0 -1
  167. package/dist/src/commands/dashboard/types.d.ts.map +0 -1
  168. package/dist/src/commands/dashboard/types.js.map +0 -1
  169. package/dist/src/commands/dashboard.d.ts.map +0 -1
  170. package/dist/src/commands/dashboard.js.map +0 -1
  171. package/dist/src/commands/doctor.d.ts.map +0 -1
  172. package/dist/src/commands/doctor.js.map +0 -1
  173. package/dist/src/commands/history.d.ts.map +0 -1
  174. package/dist/src/commands/history.js.map +0 -1
  175. package/dist/src/commands/init.d.ts.map +0 -1
  176. package/dist/src/commands/init.js.map +0 -1
  177. package/dist/src/commands/install.d.ts.map +0 -1
  178. package/dist/src/commands/install.js.map +0 -1
  179. package/dist/src/commands/logs.d.ts.map +0 -1
  180. package/dist/src/commands/logs.js.map +0 -1
  181. package/dist/src/commands/prd-state.d.ts.map +0 -1
  182. package/dist/src/commands/prd-state.js.map +0 -1
  183. package/dist/src/commands/prd.d.ts.map +0 -1
  184. package/dist/src/commands/prd.js.map +0 -1
  185. package/dist/src/commands/prds.d.ts.map +0 -1
  186. package/dist/src/commands/prds.js.map +0 -1
  187. package/dist/src/commands/prs.d.ts.map +0 -1
  188. package/dist/src/commands/prs.js.map +0 -1
  189. package/dist/src/commands/qa.d.ts.map +0 -1
  190. package/dist/src/commands/qa.js.map +0 -1
  191. package/dist/src/commands/retry.d.ts.map +0 -1
  192. package/dist/src/commands/retry.js.map +0 -1
  193. package/dist/src/commands/review.d.ts.map +0 -1
  194. package/dist/src/commands/review.js.map +0 -1
  195. package/dist/src/commands/run.d.ts.map +0 -1
  196. package/dist/src/commands/run.js.map +0 -1
  197. package/dist/src/commands/serve.d.ts.map +0 -1
  198. package/dist/src/commands/serve.js.map +0 -1
  199. package/dist/src/commands/slice.d.ts.map +0 -1
  200. package/dist/src/commands/slice.js.map +0 -1
  201. package/dist/src/commands/state.d.ts.map +0 -1
  202. package/dist/src/commands/state.js.map +0 -1
  203. package/dist/src/commands/status.d.ts.map +0 -1
  204. package/dist/src/commands/status.js.map +0 -1
  205. package/dist/src/commands/uninstall.d.ts.map +0 -1
  206. package/dist/src/commands/uninstall.js.map +0 -1
  207. package/dist/src/commands/update.d.ts.map +0 -1
  208. package/dist/src/commands/update.js.map +0 -1
  209. package/dist/src/config.d.ts +0 -23
  210. package/dist/src/config.d.ts.map +0 -1
  211. package/dist/src/config.js +0 -671
  212. package/dist/src/config.js.map +0 -1
  213. package/dist/src/constants.d.ts +0 -67
  214. package/dist/src/constants.d.ts.map +0 -1
  215. package/dist/src/constants.js +0 -131
  216. package/dist/src/constants.js.map +0 -1
  217. package/dist/src/server/index.d.ts +0 -23
  218. package/dist/src/server/index.d.ts.map +0 -1
  219. package/dist/src/server/index.js +0 -1704
  220. package/dist/src/server/index.js.map +0 -1
  221. package/dist/src/slack/channel-manager.d.ts +0 -32
  222. package/dist/src/slack/channel-manager.d.ts.map +0 -1
  223. package/dist/src/slack/channel-manager.js +0 -128
  224. package/dist/src/slack/channel-manager.js.map +0 -1
  225. package/dist/src/slack/client.d.ts +0 -76
  226. package/dist/src/slack/client.d.ts.map +0 -1
  227. package/dist/src/slack/client.js +0 -193
  228. package/dist/src/slack/client.js.map +0 -1
  229. package/dist/src/slack/deliberation.d.ts +0 -87
  230. package/dist/src/slack/deliberation.d.ts.map +0 -1
  231. package/dist/src/slack/deliberation.js +0 -1354
  232. package/dist/src/slack/deliberation.js.map +0 -1
  233. package/dist/src/slack/index.d.ts +0 -6
  234. package/dist/src/slack/index.d.ts.map +0 -1
  235. package/dist/src/slack/index.js +0 -5
  236. package/dist/src/slack/index.js.map +0 -1
  237. package/dist/src/slack/interaction-listener.d.ts +0 -130
  238. package/dist/src/slack/interaction-listener.d.ts.map +0 -1
  239. package/dist/src/slack/interaction-listener.js +0 -1329
  240. package/dist/src/slack/interaction-listener.js.map +0 -1
  241. package/dist/src/storage/json-state-migrator.d.ts +0 -24
  242. package/dist/src/storage/json-state-migrator.d.ts.map +0 -1
  243. package/dist/src/storage/json-state-migrator.js +0 -197
  244. package/dist/src/storage/json-state-migrator.js.map +0 -1
  245. package/dist/src/storage/repositories/index.d.ts +0 -25
  246. package/dist/src/storage/repositories/index.d.ts.map +0 -1
  247. package/dist/src/storage/repositories/index.js +0 -45
  248. package/dist/src/storage/repositories/index.js.map +0 -1
  249. package/dist/src/storage/repositories/interfaces.d.ts +0 -60
  250. package/dist/src/storage/repositories/interfaces.d.ts.map +0 -1
  251. package/dist/src/storage/repositories/interfaces.js +0 -6
  252. package/dist/src/storage/repositories/interfaces.js.map +0 -1
  253. package/dist/src/storage/repositories/sqlite/agent-persona-repository.d.ts +0 -33
  254. package/dist/src/storage/repositories/sqlite/agent-persona-repository.d.ts.map +0 -1
  255. package/dist/src/storage/repositories/sqlite/agent-persona-repository.js +0 -715
  256. package/dist/src/storage/repositories/sqlite/agent-persona-repository.js.map +0 -1
  257. package/dist/src/storage/repositories/sqlite/execution-history-repository.d.ts +0 -21
  258. package/dist/src/storage/repositories/sqlite/execution-history-repository.d.ts.map +0 -1
  259. package/dist/src/storage/repositories/sqlite/execution-history-repository.js +0 -94
  260. package/dist/src/storage/repositories/sqlite/execution-history-repository.js.map +0 -1
  261. package/dist/src/storage/repositories/sqlite/prd-state-repository.d.ts +0 -17
  262. package/dist/src/storage/repositories/sqlite/prd-state-repository.d.ts.map +0 -1
  263. package/dist/src/storage/repositories/sqlite/prd-state-repository.js +0 -74
  264. package/dist/src/storage/repositories/sqlite/prd-state-repository.js.map +0 -1
  265. package/dist/src/storage/repositories/sqlite/project-registry-repository.d.ts +0 -17
  266. package/dist/src/storage/repositories/sqlite/project-registry-repository.d.ts.map +0 -1
  267. package/dist/src/storage/repositories/sqlite/project-registry-repository.js +0 -43
  268. package/dist/src/storage/repositories/sqlite/project-registry-repository.js.map +0 -1
  269. package/dist/src/storage/repositories/sqlite/roadmap-state-repository.d.ts +0 -14
  270. package/dist/src/storage/repositories/sqlite/roadmap-state-repository.d.ts.map +0 -1
  271. package/dist/src/storage/repositories/sqlite/roadmap-state-repository.js +0 -47
  272. package/dist/src/storage/repositories/sqlite/roadmap-state-repository.js.map +0 -1
  273. package/dist/src/storage/repositories/sqlite/slack-discussion-repository.d.ts +0 -20
  274. package/dist/src/storage/repositories/sqlite/slack-discussion-repository.d.ts.map +0 -1
  275. package/dist/src/storage/repositories/sqlite/slack-discussion-repository.js +0 -88
  276. package/dist/src/storage/repositories/sqlite/slack-discussion-repository.js.map +0 -1
  277. package/dist/src/storage/sqlite/client.d.ts +0 -23
  278. package/dist/src/storage/sqlite/client.d.ts.map +0 -1
  279. package/dist/src/storage/sqlite/client.js +0 -47
  280. package/dist/src/storage/sqlite/client.js.map +0 -1
  281. package/dist/src/storage/sqlite/migrations.d.ts +0 -11
  282. package/dist/src/storage/sqlite/migrations.d.ts.map +0 -1
  283. package/dist/src/storage/sqlite/migrations.js +0 -94
  284. package/dist/src/storage/sqlite/migrations.js.map +0 -1
  285. package/dist/src/templates/prd-template.d.ts +0 -11
  286. package/dist/src/templates/prd-template.d.ts.map +0 -1
  287. package/dist/src/templates/prd-template.js +0 -166
  288. package/dist/src/templates/prd-template.js.map +0 -1
  289. package/dist/src/templates/slicer-prompt.d.ts +0 -54
  290. package/dist/src/templates/slicer-prompt.d.ts.map +0 -1
  291. package/dist/src/templates/slicer-prompt.js +0 -163
  292. package/dist/src/templates/slicer-prompt.js.map +0 -1
  293. package/dist/src/types.d.ts +0 -140
  294. package/dist/src/types.d.ts.map +0 -1
  295. package/dist/src/types.js +0 -5
  296. package/dist/src/types.js.map +0 -1
  297. package/dist/src/utils/avatar-generator.d.ts +0 -6
  298. package/dist/src/utils/avatar-generator.d.ts.map +0 -1
  299. package/dist/src/utils/avatar-generator.js +0 -133
  300. package/dist/src/utils/avatar-generator.js.map +0 -1
  301. package/dist/src/utils/checks.d.ts +0 -55
  302. package/dist/src/utils/checks.d.ts.map +0 -1
  303. package/dist/src/utils/checks.js +0 -246
  304. package/dist/src/utils/checks.js.map +0 -1
  305. package/dist/src/utils/config-writer.d.ts +0 -16
  306. package/dist/src/utils/config-writer.d.ts.map +0 -1
  307. package/dist/src/utils/config-writer.js +0 -45
  308. package/dist/src/utils/config-writer.js.map +0 -1
  309. package/dist/src/utils/crontab.d.ts +0 -62
  310. package/dist/src/utils/crontab.d.ts.map +0 -1
  311. package/dist/src/utils/crontab.js +0 -168
  312. package/dist/src/utils/crontab.js.map +0 -1
  313. package/dist/src/utils/execution-history.d.ts +0 -54
  314. package/dist/src/utils/execution-history.d.ts.map +0 -1
  315. package/dist/src/utils/execution-history.js +0 -80
  316. package/dist/src/utils/execution-history.js.map +0 -1
  317. package/dist/src/utils/github.d.ts +0 -40
  318. package/dist/src/utils/github.d.ts.map +0 -1
  319. package/dist/src/utils/github.js +0 -126
  320. package/dist/src/utils/github.js.map +0 -1
  321. package/dist/src/utils/notify.d.ts +0 -64
  322. package/dist/src/utils/notify.d.ts.map +0 -1
  323. package/dist/src/utils/notify.js +0 -405
  324. package/dist/src/utils/notify.js.map +0 -1
  325. package/dist/src/utils/prd-states.d.ts +0 -16
  326. package/dist/src/utils/prd-states.d.ts.map +0 -1
  327. package/dist/src/utils/prd-states.js +0 -28
  328. package/dist/src/utils/prd-states.js.map +0 -1
  329. package/dist/src/utils/registry.d.ts +0 -45
  330. package/dist/src/utils/registry.d.ts.map +0 -1
  331. package/dist/src/utils/registry.js +0 -86
  332. package/dist/src/utils/registry.js.map +0 -1
  333. package/dist/src/utils/roadmap-parser.d.ts +0 -45
  334. package/dist/src/utils/roadmap-parser.d.ts.map +0 -1
  335. package/dist/src/utils/roadmap-parser.js +0 -136
  336. package/dist/src/utils/roadmap-parser.js.map +0 -1
  337. package/dist/src/utils/roadmap-scanner.d.ts +0 -92
  338. package/dist/src/utils/roadmap-scanner.d.ts.map +0 -1
  339. package/dist/src/utils/roadmap-scanner.js +0 -349
  340. package/dist/src/utils/roadmap-scanner.js.map +0 -1
  341. package/dist/src/utils/roadmap-state.d.ts +0 -90
  342. package/dist/src/utils/roadmap-state.d.ts.map +0 -1
  343. package/dist/src/utils/roadmap-state.js +0 -154
  344. package/dist/src/utils/roadmap-state.js.map +0 -1
  345. package/dist/src/utils/script-result.d.ts +0 -12
  346. package/dist/src/utils/script-result.d.ts.map +0 -1
  347. package/dist/src/utils/script-result.js +0 -46
  348. package/dist/src/utils/script-result.js.map +0 -1
  349. package/dist/src/utils/shell.d.ts +0 -27
  350. package/dist/src/utils/shell.d.ts.map +0 -1
  351. package/dist/src/utils/shell.js +0 -64
  352. package/dist/src/utils/shell.js.map +0 -1
  353. package/dist/src/utils/status-data.d.ts +0 -148
  354. package/dist/src/utils/status-data.d.ts.map +0 -1
  355. package/dist/src/utils/status-data.js +0 -548
  356. package/dist/src/utils/status-data.js.map +0 -1
  357. package/dist/src/utils/ui.d.ts +0 -55
  358. package/dist/src/utils/ui.d.ts.map +0 -1
  359. package/dist/src/utils/ui.js +0 -121
  360. package/dist/src/utils/ui.js.map +0 -1
  361. package/scripts/night-watch-audit-cron.sh +0 -149
  362. package/scripts/night-watch-cron.sh +0 -484
  363. package/scripts/night-watch-helpers.sh +0 -499
  364. package/scripts/night-watch-pr-reviewer-cron.sh +0 -528
  365. package/scripts/night-watch-qa-cron.sh +0 -281
  366. package/scripts/night-watch-slicer-cron.sh +0 -90
  367. package/scripts/test-helpers.bats +0 -77
  368. package/templates/night-watch-pr-reviewer.md +0 -174
  369. package/templates/night-watch-qa.md +0 -157
  370. package/templates/night-watch-slicer.md +0 -219
  371. package/templates/night-watch.config.json +0 -30
  372. package/templates/night-watch.md +0 -94
  373. package/templates/prd-executor.md +0 -235
  374. package/templates/prd.md +0 -26
  375. package/web/dist/assets/index-BiJf9LFT.js +0 -458
  376. package/web/dist/assets/index-OpSgvsYu.css +0 -1
  377. package/web/dist/avatars/carlos.webp +0 -0
  378. package/web/dist/avatars/dev.webp +0 -0
  379. package/web/dist/avatars/maya.webp +0 -0
  380. package/web/dist/avatars/priya.webp +0 -0
  381. package/web/dist/index.html +0 -82
  382. /package/dist/{src/commands → commands}/dashboard/tab-actions.d.ts +0 -0
  383. /package/dist/{src/commands → commands}/dashboard/tab-actions.js +0 -0
  384. /package/dist/{src/commands → commands}/dashboard/types.js +0 -0
@@ -1,1329 +0,0 @@
1
- /**
2
- * Slack interaction listener.
3
- * Listens to human messages (Socket Mode), routes @persona mentions,
4
- * and applies loop-protection safeguards.
5
- */
6
- import { SocketModeClient } from '@slack/socket-mode';
7
- import { execFileSync, spawn } from 'child_process';
8
- import * as fs from 'fs';
9
- import * as path from 'path';
10
- import { getDb } from '../storage/sqlite/client.js';
11
- import { getRepositories } from '../storage/repositories/index.js';
12
- import { parseScriptResult } from '../utils/script-result.js';
13
- import { getRoadmapStatus } from '../utils/roadmap-scanner.js';
14
- import { generatePersonaAvatar } from '../utils/avatar-generator.js';
15
- import { DeliberationEngine } from './deliberation.js';
16
- import { SlackClient } from './client.js';
17
- const MAX_PROCESSED_MESSAGE_KEYS = 2000;
18
- const PERSONA_REPLY_COOLDOWN_MS = 45_000;
19
- const AD_HOC_THREAD_MEMORY_MS = 60 * 60_000; // 1h
20
- const PROACTIVE_IDLE_MS = 20 * 60_000; // 20 min
21
- const PROACTIVE_MIN_INTERVAL_MS = 90 * 60_000; // per channel
22
- const PROACTIVE_SWEEP_INTERVAL_MS = 60_000;
23
- const PROACTIVE_CODEWATCH_MIN_INTERVAL_MS = 3 * 60 * 60_000; // per project
24
- const MAX_JOB_OUTPUT_CHARS = 12_000;
25
- const HUMAN_REACTION_PROBABILITY = 0.65;
26
- const REACTION_DELAY_MIN_MS = 180;
27
- const REACTION_DELAY_MAX_MS = 1200;
28
- const RESPONSE_DELAY_MIN_MS = 700;
29
- const RESPONSE_DELAY_MAX_MS = 3400;
30
- const SOCKET_DISCONNECT_TIMEOUT_MS = 5_000;
31
- const JOB_STOPWORDS = new Set([
32
- 'and',
33
- 'or',
34
- 'for',
35
- 'on',
36
- 'of',
37
- 'please',
38
- 'now',
39
- 'it',
40
- 'this',
41
- 'these',
42
- 'those',
43
- 'the',
44
- 'a',
45
- 'an',
46
- 'pr',
47
- 'pull',
48
- 'that',
49
- 'thanks',
50
- 'thank',
51
- 'again',
52
- 'job',
53
- 'pipeline',
54
- ]);
55
- function sleep(ms) {
56
- return new Promise((resolve) => setTimeout(resolve, ms));
57
- }
58
- function extractLastMeaningfulLines(output, maxLines = 4) {
59
- const lines = output
60
- .split(/\r?\n/)
61
- .map((line) => line.trim())
62
- .filter(Boolean);
63
- if (lines.length === 0)
64
- return '';
65
- return lines.slice(-maxLines).join(' | ');
66
- }
67
- function buildCurrentCliInvocation(args) {
68
- const cliEntry = process.argv[1];
69
- if (!cliEntry)
70
- return null;
71
- return [...process.execArgv, cliEntry, ...args];
72
- }
73
- function formatCommandForLog(bin, args) {
74
- return [bin, ...args].map((part) => JSON.stringify(part)).join(' ');
75
- }
76
- function extractInboundEvent(payload) {
77
- return payload.event ?? payload.body?.event ?? payload.payload?.event ?? null;
78
- }
79
- export function buildInboundMessageKey(channel, ts, type) {
80
- return `${channel}:${ts}:${type ?? 'message'}`;
81
- }
82
- function normalizeProjectRef(value) {
83
- return value.toLowerCase().replace(/[^a-z0-9]/g, '');
84
- }
85
- function stripSlackUserMentions(text) {
86
- return text.replace(/<@[A-Z0-9]+>/g, ' ');
87
- }
88
- function normalizeForParsing(text) {
89
- return text
90
- .toLowerCase()
91
- .replace(/[^\w\s./-]/g, ' ')
92
- .replace(/\s+/g, ' ')
93
- .trim();
94
- }
95
- export function isAmbientTeamMessage(text) {
96
- const normalized = normalizeForParsing(stripSlackUserMentions(text));
97
- if (!normalized)
98
- return false;
99
- if (/^(hey|hi|hello|yo|sup)\b/.test(normalized) && /\b(guys|team|everyone|folks)\b/.test(normalized)) {
100
- return true;
101
- }
102
- if (/^(hey|hi|hello|yo|sup)\b/.test(normalized) && normalized.split(' ').length <= 6) {
103
- return true;
104
- }
105
- return false;
106
- }
107
- export function parseSlackJobRequest(text) {
108
- const withoutMentions = stripSlackUserMentions(text);
109
- const normalized = normalizeForParsing(withoutMentions);
110
- if (!normalized)
111
- return null;
112
- // Be tolerant of wrapped/copied URLs where whitespace/newlines split segments.
113
- const compactForUrl = withoutMentions.replace(/\s+/g, '');
114
- const prUrlMatch = compactForUrl.match(/https?:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/pull\/(\d+)/i);
115
- const prPathMatch = compactForUrl.match(/\/pull\/(\d+)(?:[/?#]|$)/i);
116
- const prHashMatch = withoutMentions.match(/(?:^|\s)#(\d+)(?:\s|$)/);
117
- const conflictSignal = /\b(conflict|conflicts|merge conflict|merge issues?|rebase)\b/i.test(normalized);
118
- const requestSignal = /\b(can someone|someone|anyone|please|need|look at|take a look|fix|review|check)\b/i.test(normalized);
119
- const match = normalized.match(/\b(run|review|qa)\b(?:\s+(?:for|on)?\s*([a-z0-9./_-]+))?/i);
120
- if (!match && !prUrlMatch && !prHashMatch)
121
- return null;
122
- const explicitJob = match?.[1]?.toLowerCase();
123
- const hasPrReference = Boolean(prUrlMatch?.[3] ?? prPathMatch?.[1] ?? prHashMatch?.[1]);
124
- const inferredReviewJob = conflictSignal || (hasPrReference && requestSignal);
125
- const job = explicitJob ?? (inferredReviewJob ? 'review' : undefined);
126
- if (!job || !['run', 'review', 'qa'].includes(job))
127
- return null;
128
- const prNumber = prUrlMatch?.[3] ?? prPathMatch?.[1] ?? prHashMatch?.[1];
129
- const repoHintFromUrl = prUrlMatch?.[2]?.toLowerCase();
130
- const candidates = [match?.[2]?.toLowerCase(), repoHintFromUrl].filter((value) => Boolean(value && !JOB_STOPWORDS.has(value)));
131
- const projectHint = candidates[0];
132
- const request = { job };
133
- if (projectHint)
134
- request.projectHint = projectHint;
135
- if (prNumber)
136
- request.prNumber = prNumber;
137
- if (job === 'review' && conflictSignal)
138
- request.fixConflicts = true;
139
- return request;
140
- }
141
- export function parseSlackIssuePickupRequest(text) {
142
- const withoutMentions = stripSlackUserMentions(text);
143
- const normalized = normalizeForParsing(withoutMentions);
144
- if (!normalized)
145
- return null;
146
- // Extract GitHub issue URL — NOT pull requests (those handled by parseSlackJobRequest)
147
- const compactForUrl = withoutMentions.replace(/\s+/g, '');
148
- let issueUrl;
149
- let issueNumber;
150
- let repo;
151
- // Standard format: github.com/{owner}/{repo}/issues/{number}
152
- const directIssueMatch = compactForUrl.match(/https?:\/\/github\.com\/([^/\s<>]+)\/([^/\s<>]+)\/issues\/(\d+)/i);
153
- if (directIssueMatch) {
154
- [issueUrl, , repo, issueNumber] = directIssueMatch;
155
- repo = repo.toLowerCase();
156
- }
157
- else {
158
- // Project board format: github.com/...?...&issue={owner}%7C{repo}%7C{number}
159
- // e.g. github.com/users/jonit-dev/projects/41/views/2?pane=issue&issue=jonit-dev%7Cnight-watch-cli%7C12
160
- const boardMatch = compactForUrl.match(/https?:\/\/github\.com\/[^<>\s]*[?&]issue=([^<>\s&]+)/i);
161
- if (!boardMatch)
162
- return null;
163
- const rawParam = boardMatch[1].replace(/%7[Cc]/g, '|');
164
- const parts = rawParam.split('|');
165
- if (parts.length < 3 || !/^\d+$/.test(parts[parts.length - 1]))
166
- return null;
167
- issueNumber = parts[parts.length - 1];
168
- repo = parts[parts.length - 2].toLowerCase();
169
- issueUrl = boardMatch[0];
170
- }
171
- // Requires pickup-intent language or "this issue" + request language
172
- // "pickup" (one word) is also accepted alongside "pick up" (two words)
173
- const pickupSignal = /\b(pick\s+up|pickup|work\s+on|implement|tackle|start\s+on|grab|handle\s+this|ship\s+this)\b/i.test(normalized);
174
- const requestSignal = /\b(please|can\s+someone|anyone)\b/i.test(normalized) && /\bthis\s+issue\b/i.test(normalized);
175
- if (!pickupSignal && !requestSignal)
176
- return null;
177
- return {
178
- issueNumber,
179
- issueUrl,
180
- repoHint: repo,
181
- };
182
- }
183
- export function parseSlackProviderRequest(text) {
184
- const withoutMentions = stripSlackUserMentions(text);
185
- if (!withoutMentions.trim())
186
- return null;
187
- // Explicit direct-provider invocation from Slack, e.g.:
188
- // "claude fix the flaky tests", "run codex on repo-x: investigate CI failures"
189
- const prefixMatch = withoutMentions.match(/^\s*(?:can\s+(?:you|someone|anyone)\s+)?(?:please\s+)?(?:(?:run|use|invoke|trigger|ask)\s+)?(claude|codex)\b[\s:,-]*/i);
190
- if (!prefixMatch)
191
- return null;
192
- const provider = prefixMatch[1].toLowerCase();
193
- let remainder = withoutMentions.slice(prefixMatch[0].length).trim();
194
- if (!remainder)
195
- return null;
196
- let projectHint;
197
- const projectMatch = remainder.match(/^(?:for|on)\s+([a-z0-9./_-]+)\b[\s:,-]*/i);
198
- if (projectMatch) {
199
- const candidate = projectMatch[1].toLowerCase();
200
- if (!JOB_STOPWORDS.has(candidate)) {
201
- projectHint = candidate;
202
- }
203
- remainder = remainder.slice(projectMatch[0].length).trim();
204
- }
205
- if (!remainder)
206
- return null;
207
- return {
208
- provider,
209
- prompt: remainder,
210
- ...(projectHint ? { projectHint } : {}),
211
- };
212
- }
213
- function getPersonaDomain(persona) {
214
- const role = persona.role.toLowerCase();
215
- const expertise = (persona.soul?.expertise ?? []).join(' ').toLowerCase();
216
- const blob = `${role} ${expertise}`;
217
- if (/\bsecurity|auth|pentest|owasp|crypt|vuln\b/.test(blob))
218
- return 'security';
219
- if (/\bqa|quality|test|e2e\b/.test(blob))
220
- return 'qa';
221
- if (/\blead|architect|architecture|systems\b/.test(blob))
222
- return 'lead';
223
- if (/\bimplementer|developer|executor|engineer\b/.test(blob))
224
- return 'dev';
225
- return 'general';
226
- }
227
- export function scorePersonaForText(text, persona) {
228
- const normalized = normalizeForParsing(stripSlackUserMentions(text));
229
- if (!normalized)
230
- return 0;
231
- let score = 0;
232
- const domain = getPersonaDomain(persona);
233
- if (normalized.includes(persona.name.toLowerCase())) {
234
- score += 12;
235
- }
236
- const securitySignal = /\b(security|auth|vuln|owasp|xss|csrf|token|permission|exploit|threat)\b/.test(normalized);
237
- const qaSignal = /\b(qa|test|testing|bug|e2e|playwright|regression|flaky)\b/.test(normalized);
238
- const leadSignal = /\b(architecture|architect|design|scalability|performance|tech debt|tradeoff|strategy)\b/.test(normalized);
239
- const devSignal = /\b(implement|implementation|code|build|fix|patch|ship|pr)\b/.test(normalized);
240
- if (securitySignal && domain === 'security')
241
- score += 8;
242
- if (qaSignal && domain === 'qa')
243
- score += 8;
244
- if (leadSignal && domain === 'lead')
245
- score += 8;
246
- if (devSignal && domain === 'dev')
247
- score += 8;
248
- const personaTokens = new Set([
249
- ...persona.role.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3),
250
- ...(persona.soul?.expertise ?? [])
251
- .flatMap((s) => s.toLowerCase().split(/[^a-z0-9]+/))
252
- .filter((t) => t.length >= 3),
253
- ]);
254
- const textTokens = normalized.split(/\s+/).filter((t) => t.length >= 3);
255
- for (const token of textTokens) {
256
- if (personaTokens.has(token)) {
257
- score += 2;
258
- }
259
- }
260
- return score;
261
- }
262
- export function selectFollowUpPersona(preferred, personas, text) {
263
- if (personas.length === 0)
264
- return preferred;
265
- const preferredScore = scorePersonaForText(text, preferred);
266
- let best = preferred;
267
- let bestScore = preferredScore;
268
- for (const persona of personas) {
269
- const score = scorePersonaForText(text, persona);
270
- if (score > bestScore) {
271
- best = persona;
272
- bestScore = score;
273
- }
274
- }
275
- // Default to continuity unless another persona is clearly a better fit.
276
- if (best.id !== preferred.id && bestScore >= preferredScore + 4 && bestScore >= 8) {
277
- return best;
278
- }
279
- return preferred;
280
- }
281
- function normalizeHandle(value) {
282
- return value.toLowerCase().replace(/[^a-z0-9]/g, '');
283
- }
284
- /**
285
- * Extract @handle mentions from raw Slack text.
286
- * Example: "@maya please check this" -> ["maya"]
287
- */
288
- export function extractMentionHandles(text) {
289
- const matches = text.match(/@([a-z0-9._-]{2,32})/gi) ?? [];
290
- const seen = new Set();
291
- const handles = [];
292
- for (const match of matches) {
293
- const normalized = normalizeHandle(match.slice(1));
294
- if (!normalized || seen.has(normalized)) {
295
- continue;
296
- }
297
- seen.add(normalized);
298
- handles.push(normalized);
299
- }
300
- return handles;
301
- }
302
- /**
303
- * Resolve mention handles to active personas by display name.
304
- * Matches @-prefixed handles in text (e.g. "@maya").
305
- */
306
- export function resolveMentionedPersonas(text, personas) {
307
- const handles = extractMentionHandles(text);
308
- if (handles.length === 0)
309
- return [];
310
- const byHandle = new Map();
311
- for (const persona of personas) {
312
- byHandle.set(normalizeHandle(persona.name), persona);
313
- }
314
- const resolved = [];
315
- const seenPersonaIds = new Set();
316
- for (const handle of handles) {
317
- const persona = byHandle.get(handle);
318
- if (!persona || seenPersonaIds.has(persona.id)) {
319
- continue;
320
- }
321
- seenPersonaIds.add(persona.id);
322
- resolved.push(persona);
323
- }
324
- return resolved;
325
- }
326
- /**
327
- * Match personas whose name appears as a word in the text (case-insensitive, no @ needed).
328
- * Used for app_mention events where text looks like "<@BOTID> maya check this PR".
329
- */
330
- export function resolvePersonasByPlainName(text, personas) {
331
- // Strip Slack user ID mentions like <@U12345678> to avoid false positives
332
- const stripped = text.replace(/<@[A-Z0-9]+>/g, '').toLowerCase();
333
- const resolved = [];
334
- const seenPersonaIds = new Set();
335
- for (const persona of personas) {
336
- if (seenPersonaIds.has(persona.id))
337
- continue;
338
- const nameLower = persona.name.toLowerCase();
339
- // Word-boundary match: persona name as a whole word
340
- const re = new RegExp(`\\b${nameLower}\\b`);
341
- if (re.test(stripped)) {
342
- resolved.push(persona);
343
- seenPersonaIds.add(persona.id);
344
- }
345
- }
346
- return resolved;
347
- }
348
- export function shouldIgnoreInboundSlackEvent(event, botUserId) {
349
- if (!event.channel || !event.ts)
350
- return true;
351
- if (!event.user)
352
- return true;
353
- if (event.subtype)
354
- return true;
355
- if (event.bot_id)
356
- return true;
357
- if (botUserId && event.user === botUserId)
358
- return true;
359
- return false;
360
- }
361
- /**
362
- * Extract GitHub issue or PR URLs from a message string.
363
- */
364
- export function extractGitHubIssueUrls(text) {
365
- const matches = text.match(/https?:\/\/github\.com\/[^\s<>]+/g) ?? [];
366
- return matches.filter((u) => /\/(issues|pull)\/\d+/.test(u));
367
- }
368
- /**
369
- * Fetch GitHub issue/PR content via `gh api` for agent context.
370
- * Returns a formatted string, or '' on failure.
371
- */
372
- async function fetchGitHubIssueContext(urls) {
373
- if (urls.length === 0)
374
- return '';
375
- const parts = [];
376
- for (const url of urls.slice(0, 3)) {
377
- const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/(issues|pull)\/(\d+)/);
378
- if (!match)
379
- continue;
380
- const [, owner, repo, type, number] = match;
381
- const endpoint = type === 'pull'
382
- ? `/repos/${owner}/${repo}/pulls/${number}`
383
- : `/repos/${owner}/${repo}/issues/${number}`;
384
- try {
385
- const raw = execFileSync('gh', ['api', endpoint, '--jq', '{title: .title, state: .state, body: .body, labels: [.labels[].name]}'], {
386
- timeout: 10_000,
387
- encoding: 'utf-8',
388
- stdio: ['ignore', 'pipe', 'pipe'],
389
- });
390
- const data = JSON.parse(raw);
391
- const labelStr = data.labels.length > 0 ? ` [${data.labels.join(', ')}]` : '';
392
- const body = (data.body ?? '').trim().slice(0, 1200);
393
- parts.push(`GitHub ${type === 'pull' ? 'PR' : 'Issue'} #${number}${labelStr}: ${data.title} (${data.state})\n${body}`);
394
- }
395
- catch {
396
- // gh not available or not authenticated — skip
397
- }
398
- }
399
- return parts.join('\n\n---\n\n');
400
- }
401
- export class SlackInteractionListener {
402
- _config;
403
- _slackClient;
404
- _engine;
405
- _socketClient = null;
406
- _botUserId = null;
407
- _processedMessageKeys = new Set();
408
- _processedMessageOrder = [];
409
- _lastPersonaReplyAt = new Map();
410
- _adHocThreadState = new Map();
411
- _lastChannelActivityAt = new Map();
412
- _lastProactiveAt = new Map();
413
- _lastCodeWatchAt = new Map();
414
- _proactiveTimer = null;
415
- constructor(config) {
416
- this._config = config;
417
- const token = config.slack?.botToken ?? '';
418
- const serverBaseUrl = config.slack?.serverBaseUrl ?? 'http://localhost:7575';
419
- this._slackClient = new SlackClient(token, serverBaseUrl);
420
- this._engine = new DeliberationEngine(this._slackClient, config);
421
- }
422
- async start() {
423
- const slack = this._config.slack;
424
- if (!slack?.enabled ||
425
- !slack.discussionEnabled ||
426
- !slack.botToken ||
427
- !slack.appToken) {
428
- return;
429
- }
430
- if (this._socketClient) {
431
- return;
432
- }
433
- try {
434
- this._botUserId = await this._slackClient.getBotUserId();
435
- }
436
- catch (err) {
437
- const msg = err instanceof Error ? err.message : String(err);
438
- console.warn(`Slack interaction listener: failed to resolve bot user id (${msg})`);
439
- this._botUserId = null;
440
- }
441
- const socket = new SocketModeClient({
442
- appToken: slack.appToken,
443
- });
444
- const onInboundEvent = (payload) => {
445
- void this._handleEventsApi(payload);
446
- };
447
- // Socket Mode emits concrete event types (e.g. "app_mention", "message")
448
- // for Events API payloads in current SDK versions.
449
- socket.on('app_mention', onInboundEvent);
450
- socket.on('message', onInboundEvent);
451
- // Keep compatibility with alternate wrappers/older payload routing.
452
- socket.on('events_api', onInboundEvent);
453
- socket.on('error', (err) => {
454
- const msg = err instanceof Error ? err.message : String(err);
455
- console.warn(`Slack interaction listener error: ${msg}`);
456
- });
457
- await socket.start();
458
- this._socketClient = socket;
459
- console.log('Slack interaction listener started (Socket Mode)');
460
- this._startProactiveLoop();
461
- void this._postPersonaIntros();
462
- }
463
- async stop() {
464
- this._stopProactiveLoop();
465
- if (!this._socketClient) {
466
- return;
467
- }
468
- const socket = this._socketClient;
469
- this._socketClient = null;
470
- try {
471
- await Promise.race([
472
- socket.disconnect(),
473
- sleep(SOCKET_DISCONNECT_TIMEOUT_MS).then(() => {
474
- throw new Error(`timed out after ${SOCKET_DISCONNECT_TIMEOUT_MS}ms`);
475
- }),
476
- ]);
477
- console.log('Slack interaction listener stopped');
478
- }
479
- catch (err) {
480
- const msg = err instanceof Error ? err.message : String(err);
481
- console.warn(`Slack interaction listener shutdown failed: ${msg}`);
482
- }
483
- finally {
484
- socket.removeAllListeners();
485
- }
486
- }
487
- /**
488
- * Join all configured channels, generate avatars for personas that need them,
489
- * and post a one-time personality-driven intro for each new persona.
490
- */
491
- async _postPersonaIntros() {
492
- const slack = this._config.slack;
493
- if (!slack)
494
- return;
495
- // Join all configured channels so the bot receives messages in them
496
- const channelIds = Object.values(slack.channels ?? {}).filter(Boolean);
497
- const now = Date.now();
498
- for (const channelId of channelIds) {
499
- this._lastChannelActivityAt.set(channelId, now);
500
- }
501
- for (const channelId of channelIds) {
502
- try {
503
- await this._slackClient.joinChannel(channelId);
504
- console.log(`[slack] Joined channel ${channelId}`);
505
- }
506
- catch {
507
- // Ignore — channel may already be joined or private
508
- }
509
- }
510
- const engChannelId = slack.channels?.eng;
511
- if (!engChannelId)
512
- return;
513
- const db = getDb();
514
- const metaRow = db
515
- .prepare(`SELECT value FROM schema_meta WHERE key = 'slack_persona_intros_v4'`)
516
- .get();
517
- const introduced = new Set(metaRow ? JSON.parse(metaRow.value) : []);
518
- const repos = getRepositories();
519
- const personas = repos.agentPersona.getActive();
520
- const newPersonas = personas.filter((p) => !introduced.has(p.id));
521
- if (newPersonas.length === 0) {
522
- console.log('[slack] All personas already introduced — skipping intros');
523
- return;
524
- }
525
- console.log(`[slack] Introducing ${newPersonas.length} persona(s) to #eng`);
526
- for (const persona of newPersonas) {
527
- // Generate avatar if missing and Replicate token is configured
528
- let currentPersona = persona;
529
- if (!currentPersona.avatarUrl && slack.replicateApiToken) {
530
- try {
531
- console.log(`[slack] Generating avatar for ${persona.name}…`);
532
- const avatarUrl = await generatePersonaAvatar(persona.name, persona.role, slack.replicateApiToken);
533
- if (avatarUrl) {
534
- currentPersona = repos.agentPersona.update(persona.id, { avatarUrl });
535
- console.log(`[slack] Avatar set for ${persona.name}: ${avatarUrl}`);
536
- }
537
- }
538
- catch (err) {
539
- const msg = err instanceof Error ? err.message : String(err);
540
- console.warn(`[slack] Avatar generation failed for ${persona.name}: ${msg}`);
541
- }
542
- }
543
- // Personality-driven intro — persona's own voice, no canned boilerplate
544
- const whoIAm = currentPersona.soul?.whoIAm?.trim() ?? '';
545
- // Personas are not real Slack users (they share the Night Watch AI bot).
546
- // Users invoke them by @-mentioning the Night Watch AI bot and including the agent name.
547
- const howToTag = `To reach me: mention \`@Night Watch AI\` in any message and include my name — e.g. \`@Night Watch AI ${currentPersona.name}, what do you think about this PR?\``;
548
- const intro = whoIAm
549
- ? `${whoIAm}\n\n${howToTag}`
550
- : `*${currentPersona.name}* — ${currentPersona.role}.\n\n${howToTag}`;
551
- try {
552
- await this._slackClient.postAsAgent(engChannelId, intro, currentPersona);
553
- introduced.add(persona.id);
554
- db.prepare(`INSERT INTO schema_meta (key, value) VALUES ('slack_persona_intros_v4', ?)
555
- ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(JSON.stringify(Array.from(introduced)));
556
- console.log(`[slack] Intro posted for ${persona.name}`);
557
- }
558
- catch (err) {
559
- const msg = err instanceof Error ? err.message : String(err);
560
- console.warn(`[slack] Persona intro failed for ${persona.name}: ${msg}`);
561
- }
562
- }
563
- }
564
- async _handleEventsApi(payload) {
565
- if (payload.ack) {
566
- try {
567
- await payload.ack();
568
- }
569
- catch {
570
- // Ignore ack races/timeouts; processing can continue.
571
- }
572
- }
573
- const event = extractInboundEvent(payload);
574
- if (!event)
575
- return;
576
- if (event.type !== 'message' && event.type !== 'app_mention')
577
- return;
578
- const ignored = shouldIgnoreInboundSlackEvent(event, this._botUserId);
579
- if (ignored) {
580
- console.log(`[slack] ignored self/system event type=${event.type ?? '?'} subtype=${event.subtype ?? '-'} channel=${event.channel ?? '-'} user=${event.user ?? '-'} bot_id=${event.bot_id ?? '-'}`);
581
- return;
582
- }
583
- console.log(`[slack] inbound human event type=${event.type ?? '?'} channel=${event.channel ?? '-'} user=${event.user ?? '-'} text=${(event.text ?? '').slice(0, 80)}`);
584
- // Direct bot mentions arrive as app_mention; ignore the mirrored message event
585
- // to avoid duplicate or out-of-order handling on the same Slack message ts.
586
- if (event.type === 'message'
587
- && this._botUserId
588
- && (event.text ?? '').includes(`<@${this._botUserId}>`)) {
589
- console.log('[slack] ignoring mirrored message event for direct bot mention');
590
- return;
591
- }
592
- try {
593
- await this._handleInboundMessage(event);
594
- }
595
- catch (err) {
596
- const msg = err instanceof Error ? err.message : String(err);
597
- console.warn(`Slack interaction message handling failed: ${msg}`);
598
- }
599
- }
600
- _rememberMessageKey(key) {
601
- if (this._processedMessageKeys.has(key)) {
602
- return false;
603
- }
604
- this._processedMessageKeys.add(key);
605
- this._processedMessageOrder.push(key);
606
- while (this._processedMessageOrder.length > MAX_PROCESSED_MESSAGE_KEYS) {
607
- const oldest = this._processedMessageOrder.shift();
608
- if (oldest) {
609
- this._processedMessageKeys.delete(oldest);
610
- }
611
- }
612
- return true;
613
- }
614
- _isPersonaOnCooldown(channel, threadTs, personaId) {
615
- const key = `${channel}:${threadTs}:${personaId}`;
616
- const last = this._lastPersonaReplyAt.get(key);
617
- if (!last)
618
- return false;
619
- return Date.now() - last < PERSONA_REPLY_COOLDOWN_MS;
620
- }
621
- _markPersonaReply(channel, threadTs, personaId) {
622
- const key = `${channel}:${threadTs}:${personaId}`;
623
- this._lastPersonaReplyAt.set(key, Date.now());
624
- }
625
- _threadKey(channel, threadTs) {
626
- return `${channel}:${threadTs}`;
627
- }
628
- _markChannelActivity(channel) {
629
- this._lastChannelActivityAt.set(channel, Date.now());
630
- }
631
- _rememberAdHocThreadPersona(channel, threadTs, personaId) {
632
- this._adHocThreadState.set(this._threadKey(channel, threadTs), {
633
- personaId,
634
- expiresAt: Date.now() + AD_HOC_THREAD_MEMORY_MS,
635
- });
636
- }
637
- /**
638
- * After an agent posts a reply, check if the text mentions other personas by plain name.
639
- * If so, trigger those personas to respond once (no further cascading — depth 1 only).
640
- * This enables natural agent-to-agent handoffs like "Carlos, what's the priority here?"
641
- */
642
- async _followAgentMentions(postedText, channel, threadTs, personas, projectContext, skipPersonaId) {
643
- if (!postedText)
644
- return;
645
- const mentioned = resolvePersonasByPlainName(postedText, personas).filter((p) => p.id !== skipPersonaId && !this._isPersonaOnCooldown(channel, threadTs, p.id));
646
- if (mentioned.length === 0)
647
- return;
648
- console.log(`[slack] agent mention follow-up: ${mentioned.map((p) => p.name).join(', ')}`);
649
- for (const persona of mentioned) {
650
- // Small human-like delay before the tagged persona responds
651
- await sleep(this._randomInt(RESPONSE_DELAY_MIN_MS * 2, RESPONSE_DELAY_MAX_MS * 3));
652
- // replyAsAgent fetches thread history internally so Carlos sees Dev's message
653
- await this._engine.replyAsAgent(channel, threadTs, postedText, persona, projectContext);
654
- this._markPersonaReply(channel, threadTs, persona.id);
655
- this._rememberAdHocThreadPersona(channel, threadTs, persona.id);
656
- }
657
- }
658
- /**
659
- * Recover the persona that last replied in a thread by scanning its history.
660
- * Used as a fallback when in-memory state was lost (e.g. after a server restart).
661
- * Matches message `username` fields against known persona names.
662
- */
663
- async _recoverPersonaFromThreadHistory(channel, threadTs, personas) {
664
- try {
665
- const history = await this._slackClient.getChannelHistory(channel, threadTs, 50);
666
- // Walk backwards to find the most recent message sent by a persona
667
- for (const msg of [...history].reverse()) {
668
- if (!msg.username)
669
- continue;
670
- const matched = personas.find((p) => p.name.toLowerCase() === msg.username.toLowerCase());
671
- if (matched)
672
- return matched;
673
- }
674
- }
675
- catch {
676
- // Ignore — treat as no prior context
677
- }
678
- return null;
679
- }
680
- _getRememberedAdHocPersona(channel, threadTs, personas) {
681
- const key = this._threadKey(channel, threadTs);
682
- const remembered = this._adHocThreadState.get(key);
683
- if (!remembered)
684
- return null;
685
- if (Date.now() > remembered.expiresAt) {
686
- this._adHocThreadState.delete(key);
687
- return null;
688
- }
689
- return personas.find((p) => p.id === remembered.personaId) ?? null;
690
- }
691
- _pickRandomPersona(personas, channel, threadTs) {
692
- if (personas.length === 0)
693
- return null;
694
- const available = personas.filter((p) => !this._isPersonaOnCooldown(channel, threadTs, p.id));
695
- const pool = available.length > 0 ? available : personas;
696
- return pool[Math.floor(Math.random() * pool.length)] ?? null;
697
- }
698
- _findPersonaByName(personas, name) {
699
- const target = name.toLowerCase();
700
- return personas.find((p) => p.name.toLowerCase() === target) ?? null;
701
- }
702
- _buildProjectContext(channel, projects) {
703
- if (projects.length === 0)
704
- return '';
705
- const inChannel = projects.find((p) => p.slackChannelId === channel);
706
- const names = projects.map((p) => p.name).join(', ');
707
- if (inChannel) {
708
- return `Current channel project: ${inChannel.name}. Registered projects: ${names}.`;
709
- }
710
- return `Registered projects: ${names}.`;
711
- }
712
- _buildRoadmapContext(channel, projects) {
713
- if (projects.length === 0)
714
- return '';
715
- const parts = [];
716
- for (const project of projects) {
717
- try {
718
- const status = getRoadmapStatus(project.path, this._config);
719
- if (!status.found || status.items.length === 0)
720
- continue;
721
- const pending = status.items.filter((i) => !i.processed && !i.checked);
722
- const done = status.items.filter((i) => i.processed);
723
- const total = status.items.length;
724
- let summary = `${project.name}: ${done.length}/${total} roadmap items done`;
725
- if (pending.length > 0) {
726
- const nextItems = pending.slice(0, 3).map((i) => i.title);
727
- summary += `. Next up: ${nextItems.join(', ')}`;
728
- }
729
- if (done.length === total) {
730
- summary += ' (all complete)';
731
- }
732
- parts.push(summary);
733
- }
734
- catch {
735
- // Skip projects where roadmap can't be read
736
- }
737
- }
738
- return parts.join('\n');
739
- }
740
- _resolveProjectByHint(projects, hint) {
741
- const normalizedHint = normalizeProjectRef(hint);
742
- if (!normalizedHint)
743
- return null;
744
- const byNameExact = projects.find((p) => normalizeProjectRef(p.name) === normalizedHint);
745
- if (byNameExact)
746
- return byNameExact;
747
- const byPathExact = projects.find((p) => {
748
- const base = p.path.split('/').pop() ?? '';
749
- return normalizeProjectRef(base) === normalizedHint;
750
- });
751
- if (byPathExact)
752
- return byPathExact;
753
- const byNameContains = projects.find((p) => normalizeProjectRef(p.name).includes(normalizedHint));
754
- if (byNameContains)
755
- return byNameContains;
756
- return projects.find((p) => {
757
- const base = p.path.split('/').pop() ?? '';
758
- return normalizeProjectRef(base).includes(normalizedHint);
759
- }) ?? null;
760
- }
761
- _resolveTargetProject(channel, projects, projectHint) {
762
- if (projectHint) {
763
- return this._resolveProjectByHint(projects, projectHint);
764
- }
765
- const byChannel = projects.find((p) => p.slackChannelId === channel);
766
- if (byChannel)
767
- return byChannel;
768
- if (projects.length === 1)
769
- return projects[0];
770
- return null;
771
- }
772
- _isMessageAddressedToBot(event) {
773
- if (event.type === 'app_mention')
774
- return true;
775
- const text = normalizeForParsing(stripSlackUserMentions(event.text ?? ''));
776
- return /^night[-\s]?watch\b/.test(text) || /^nw\b/.test(text);
777
- }
778
- _randomInt(min, max) {
779
- if (max <= min)
780
- return min;
781
- return Math.floor(Math.random() * (max - min + 1)) + min;
782
- }
783
- _reactionCandidatesForPersona(persona) {
784
- const role = persona.role.toLowerCase();
785
- if (role.includes('security'))
786
- return ['eyes', 'thinking_face', 'shield', 'thumbsup'];
787
- if (role.includes('qa') || role.includes('quality'))
788
- return ['test_tube', 'mag', 'thinking_face', 'thumbsup'];
789
- if (role.includes('lead') || role.includes('architect'))
790
- return ['thinking_face', 'thumbsup', 'memo', 'eyes'];
791
- if (role.includes('implementer') || role.includes('developer'))
792
- return ['wrench', 'hammer_and_wrench', 'thumbsup', 'eyes'];
793
- return ['eyes', 'thinking_face', 'thumbsup', 'wave'];
794
- }
795
- async _maybeReactToHumanMessage(channel, messageTs, persona) {
796
- if (Math.random() > HUMAN_REACTION_PROBABILITY) {
797
- return;
798
- }
799
- const candidates = this._reactionCandidatesForPersona(persona);
800
- const reaction = candidates[this._randomInt(0, candidates.length - 1)];
801
- await sleep(this._randomInt(REACTION_DELAY_MIN_MS, REACTION_DELAY_MAX_MS));
802
- try {
803
- await this._slackClient.addReaction(channel, messageTs, reaction);
804
- }
805
- catch {
806
- // Ignore reaction failures (permissions, already reacted, etc.)
807
- }
808
- }
809
- async _applyHumanResponseTiming(channel, messageTs, persona) {
810
- await this._maybeReactToHumanMessage(channel, messageTs, persona);
811
- await sleep(this._randomInt(RESPONSE_DELAY_MIN_MS, RESPONSE_DELAY_MAX_MS));
812
- }
813
- async _spawnNightWatchJob(job, project, channel, threadTs, persona, opts) {
814
- const invocationArgs = buildCurrentCliInvocation([job]);
815
- const prRef = opts?.prNumber ? ` PR #${opts.prNumber}` : '';
816
- if (!invocationArgs) {
817
- console.warn(`[slack][job] ${persona.name} cannot start ${job} for ${project.name}${prRef ? ` (${prRef.trim()})` : ''}: CLI entry path unavailable`);
818
- await this._slackClient.postAsAgent(channel, `Can't start that ${job} right now — runtime issue. Checking it.`, persona, threadTs);
819
- this._markChannelActivity(channel);
820
- this._markPersonaReply(channel, threadTs, persona.id);
821
- return;
822
- }
823
- console.log(`[slack][job] persona=${persona.name} project=${project.name}${opts?.prNumber ? ` pr=${opts.prNumber}` : ''} spawn=${formatCommandForLog(process.execPath, invocationArgs)}`);
824
- const child = spawn(process.execPath, invocationArgs, {
825
- cwd: project.path,
826
- env: {
827
- ...process.env,
828
- NW_EXECUTION_CONTEXT: 'agent',
829
- ...(opts?.prNumber ? { NW_TARGET_PR: opts.prNumber } : {}),
830
- ...(opts?.issueNumber ? { NW_TARGET_ISSUE: opts.issueNumber } : {}),
831
- ...(opts?.fixConflicts
832
- ? {
833
- NW_SLACK_FEEDBACK: JSON.stringify({
834
- source: 'slack',
835
- kind: 'merge_conflict_resolution',
836
- prNumber: opts.prNumber ?? '',
837
- changes: 'Resolve merge conflicts and stabilize the PR for re-review.',
838
- }),
839
- }
840
- : {}),
841
- },
842
- stdio: ['ignore', 'pipe', 'pipe'],
843
- });
844
- console.log(`[slack][job] ${persona.name} spawned ${job} for ${project.name}${opts?.prNumber ? ` (PR #${opts.prNumber})` : ''} pid=${child.pid ?? 'unknown'}`);
845
- let output = '';
846
- let errored = false;
847
- const appendOutput = (chunk) => {
848
- output += chunk.toString();
849
- if (output.length > MAX_JOB_OUTPUT_CHARS) {
850
- output = output.slice(-MAX_JOB_OUTPUT_CHARS);
851
- }
852
- };
853
- child.stdout?.on('data', appendOutput);
854
- child.stderr?.on('data', appendOutput);
855
- child.on('error', async (err) => {
856
- errored = true;
857
- console.warn(`[slack][job] ${persona.name} ${job} spawn error for ${project.name}${opts?.prNumber ? ` (PR #${opts.prNumber})` : ''}: ${err.message}`);
858
- await this._slackClient.postAsAgent(channel, `Couldn't kick off that ${job}. Error logged — looking into it.`, persona, threadTs);
859
- this._markChannelActivity(channel);
860
- this._markPersonaReply(channel, threadTs, persona.id);
861
- });
862
- child.on('close', async (code) => {
863
- if (errored)
864
- return;
865
- console.log(`[slack][job] ${persona.name} ${job} finished for ${project.name}${opts?.prNumber ? ` (PR #${opts.prNumber})` : ''} exit=${code ?? 'unknown'}`);
866
- const parsed = parseScriptResult(output);
867
- const status = parsed?.status ? ` (${parsed.status})` : '';
868
- const detail = extractLastMeaningfulLines(output);
869
- if (code === 0) {
870
- const doneMessage = job === 'review'
871
- ? `Review done${prRef ? ` on${prRef}` : ''}.`
872
- : job === 'qa'
873
- ? `QA pass done${prRef ? ` on${prRef}` : ''}.`
874
- : `Run finished${prRef ? ` for${prRef}` : ''}.`;
875
- await this._slackClient.postAsAgent(channel, doneMessage, persona, threadTs);
876
- }
877
- else {
878
- if (detail) {
879
- console.warn(`[slack][job] ${persona.name} ${job} failure detail: ${detail}`);
880
- }
881
- await this._slackClient.postAsAgent(channel, `Hit a snag running ${job}${prRef ? ` on${prRef}` : ''}. Logged the details — looking into it.`, persona, threadTs);
882
- }
883
- if (code !== 0 && status) {
884
- console.warn(`[slack][job] ${persona.name} ${job} status=${status.replace(/[()]/g, '')}`);
885
- }
886
- this._markChannelActivity(channel);
887
- this._markPersonaReply(channel, threadTs, persona.id);
888
- });
889
- }
890
- async _spawnDirectProviderRequest(request, project, channel, threadTs, persona) {
891
- const providerLabel = request.provider === 'claude' ? 'Claude' : 'Codex';
892
- const args = request.provider === 'claude'
893
- ? ['-p', request.prompt, '--dangerously-skip-permissions']
894
- : ['--quiet', '--yolo', '--prompt', request.prompt];
895
- console.log(`[slack][provider] persona=${persona.name} provider=${request.provider} project=${project.name} spawn=${formatCommandForLog(request.provider, args)}`);
896
- const child = spawn(request.provider, args, {
897
- cwd: project.path,
898
- env: {
899
- ...process.env,
900
- ...(this._config.providerEnv ?? {}),
901
- NW_EXECUTION_CONTEXT: 'agent',
902
- },
903
- stdio: ['ignore', 'pipe', 'pipe'],
904
- });
905
- console.log(`[slack][provider] ${persona.name} spawned ${request.provider} for ${project.name} pid=${child.pid ?? 'unknown'}`);
906
- let output = '';
907
- let errored = false;
908
- const appendOutput = (chunk) => {
909
- output += chunk.toString();
910
- if (output.length > MAX_JOB_OUTPUT_CHARS) {
911
- output = output.slice(-MAX_JOB_OUTPUT_CHARS);
912
- }
913
- };
914
- child.stdout?.on('data', appendOutput);
915
- child.stderr?.on('data', appendOutput);
916
- child.on('error', async (err) => {
917
- errored = true;
918
- console.warn(`[slack][provider] ${persona.name} ${request.provider} spawn error for ${project.name}: ${err.message}`);
919
- await this._slackClient.postAsAgent(channel, `Couldn't start ${providerLabel}. Error logged — looking into it.`, persona, threadTs);
920
- this._markChannelActivity(channel);
921
- this._markPersonaReply(channel, threadTs, persona.id);
922
- });
923
- child.on('close', async (code) => {
924
- if (errored)
925
- return;
926
- console.log(`[slack][provider] ${persona.name} ${request.provider} finished for ${project.name} exit=${code ?? 'unknown'}`);
927
- const detail = extractLastMeaningfulLines(output);
928
- if (code === 0) {
929
- await this._slackClient.postAsAgent(channel, `${providerLabel} command finished.`, persona, threadTs);
930
- }
931
- else {
932
- if (detail) {
933
- console.warn(`[slack][provider] ${persona.name} ${request.provider} failure detail: ${detail}`);
934
- }
935
- await this._slackClient.postAsAgent(channel, `${providerLabel} hit a snag. Logged the details — looking into it.`, persona, threadTs);
936
- }
937
- this._markChannelActivity(channel);
938
- this._markPersonaReply(channel, threadTs, persona.id);
939
- });
940
- }
941
- async _triggerDirectProviderIfRequested(event, channel, threadTs, messageTs, personas) {
942
- const request = parseSlackProviderRequest(event.text ?? '');
943
- if (!request)
944
- return false;
945
- const addressedToBot = this._isMessageAddressedToBot(event);
946
- const normalized = normalizeForParsing(stripSlackUserMentions(event.text ?? ''));
947
- const startsWithProviderCommand = /^(?:can\s+(?:you|someone|anyone)\s+)?(?:please\s+)?(?:(?:run|use|invoke|trigger|ask)\s+)?(?:claude|codex)\b/i.test(normalized);
948
- if (!addressedToBot && !startsWithProviderCommand) {
949
- return false;
950
- }
951
- const repos = getRepositories();
952
- const projects = repos.projectRegistry.getAll();
953
- const persona = this._findPersonaByName(personas, 'Dev')
954
- ?? this._pickRandomPersona(personas, channel, threadTs)
955
- ?? personas[0];
956
- if (!persona)
957
- return false;
958
- const targetProject = this._resolveTargetProject(channel, projects, request.projectHint);
959
- if (!targetProject) {
960
- const projectNames = projects.map((p) => p.name).join(', ') || '(none registered)';
961
- await this._slackClient.postAsAgent(channel, `Which project? Registered: ${projectNames}.`, persona, threadTs);
962
- this._markChannelActivity(channel);
963
- this._markPersonaReply(channel, threadTs, persona.id);
964
- return true;
965
- }
966
- console.log(`[slack][provider] routing provider=${request.provider} to persona=${persona.name} project=${targetProject.name}`);
967
- const providerLabel = request.provider === 'claude' ? 'Claude' : 'Codex';
968
- const compactPrompt = request.prompt.replace(/\s+/g, ' ').trim();
969
- const promptPreview = compactPrompt.length > 120
970
- ? `${compactPrompt.slice(0, 117)}...`
971
- : compactPrompt;
972
- await this._applyHumanResponseTiming(channel, messageTs, persona);
973
- await this._slackClient.postAsAgent(channel, `Running ${providerLabel} directly${request.projectHint ? ` on ${targetProject.name}` : ''}: "${promptPreview}"`, persona, threadTs);
974
- this._markChannelActivity(channel);
975
- this._markPersonaReply(channel, threadTs, persona.id);
976
- this._rememberAdHocThreadPersona(channel, threadTs, persona.id);
977
- await this._spawnDirectProviderRequest(request, targetProject, channel, threadTs, persona);
978
- return true;
979
- }
980
- async _triggerSlackJobIfRequested(event, channel, threadTs, messageTs, personas) {
981
- const request = parseSlackJobRequest(event.text ?? '');
982
- if (!request)
983
- return false;
984
- const addressedToBot = this._isMessageAddressedToBot(event);
985
- const normalized = normalizeForParsing(stripSlackUserMentions(event.text ?? ''));
986
- const teamRequestLanguage = /\b(can someone|someone|anyone|please|need)\b/i.test(normalized);
987
- const startsWithCommand = /^(run|review|qa)\b/i.test(normalized);
988
- if (!addressedToBot
989
- && !request.prNumber
990
- && !request.fixConflicts
991
- && !teamRequestLanguage
992
- && !startsWithCommand) {
993
- return false;
994
- }
995
- const repos = getRepositories();
996
- const projects = repos.projectRegistry.getAll();
997
- const persona = (request.job === 'run' ? this._findPersonaByName(personas, 'Dev') : null)
998
- ?? (request.job === 'qa' ? this._findPersonaByName(personas, 'Priya') : null)
999
- ?? (request.job === 'review' ? this._findPersonaByName(personas, 'Carlos') : null)
1000
- ?? this._pickRandomPersona(personas, channel, threadTs)
1001
- ?? personas[0];
1002
- if (!persona)
1003
- return false;
1004
- const targetProject = this._resolveTargetProject(channel, projects, request.projectHint);
1005
- if (!targetProject) {
1006
- const projectNames = projects.map((p) => p.name).join(', ') || '(none registered)';
1007
- await this._slackClient.postAsAgent(channel, `Which project? Registered: ${projectNames}.`, persona, threadTs);
1008
- this._markChannelActivity(channel);
1009
- this._markPersonaReply(channel, threadTs, persona.id);
1010
- return true;
1011
- }
1012
- console.log(`[slack][job] routing job=${request.job} to persona=${persona.name} project=${targetProject.name}${request.prNumber ? ` pr=${request.prNumber}` : ''}${request.fixConflicts ? ' fix_conflicts=true' : ''}`);
1013
- const planLine = request.job === 'review'
1014
- ? `On it${request.prNumber ? ` — PR #${request.prNumber}` : ''}${request.fixConflicts ? ', including the conflicts' : ''}.`
1015
- : request.job === 'qa'
1016
- ? `Running QA${request.prNumber ? ` on #${request.prNumber}` : ''}.`
1017
- : `Starting the run${request.prNumber ? ` for #${request.prNumber}` : ''}.`;
1018
- await this._applyHumanResponseTiming(channel, messageTs, persona);
1019
- await this._slackClient.postAsAgent(channel, `${planLine}`, persona, threadTs);
1020
- console.log(`[slack][job] ${persona.name} accepted job=${request.job} project=${targetProject.name}${request.prNumber ? ` pr=${request.prNumber}` : ''}`);
1021
- this._markChannelActivity(channel);
1022
- this._markPersonaReply(channel, threadTs, persona.id);
1023
- this._rememberAdHocThreadPersona(channel, threadTs, persona.id);
1024
- await this._spawnNightWatchJob(request.job, targetProject, channel, threadTs, persona, { prNumber: request.prNumber, fixConflicts: request.fixConflicts });
1025
- return true;
1026
- }
1027
- async _triggerIssuePickupIfRequested(event, channel, threadTs, messageTs, personas) {
1028
- const request = parseSlackIssuePickupRequest(event.text ?? '');
1029
- if (!request)
1030
- return false;
1031
- const addressedToBot = this._isMessageAddressedToBot(event);
1032
- const normalized = normalizeForParsing(stripSlackUserMentions(event.text ?? ''));
1033
- const teamRequestLanguage = /\b(can someone|someone|anyone|please|need)\b/i.test(normalized);
1034
- if (!addressedToBot && !teamRequestLanguage)
1035
- return false;
1036
- const repos = getRepositories();
1037
- const projects = repos.projectRegistry.getAll();
1038
- const persona = this._findPersonaByName(personas, 'Dev')
1039
- ?? this._pickRandomPersona(personas, channel, threadTs)
1040
- ?? personas[0];
1041
- if (!persona)
1042
- return false;
1043
- const targetProject = this._resolveTargetProject(channel, projects, request.repoHint);
1044
- if (!targetProject) {
1045
- const projectNames = projects.map((p) => p.name).join(', ') || '(none registered)';
1046
- await this._slackClient.postAsAgent(channel, `Which project? Registered: ${projectNames}.`, persona, threadTs);
1047
- this._markChannelActivity(channel);
1048
- this._markPersonaReply(channel, threadTs, persona.id);
1049
- return true;
1050
- }
1051
- console.log(`[slack][issue-pickup] routing issue=#${request.issueNumber} to persona=${persona.name} project=${targetProject.name}`);
1052
- await this._applyHumanResponseTiming(channel, messageTs, persona);
1053
- await this._slackClient.postAsAgent(channel, `On it — picking up #${request.issueNumber}. Starting the run now.`, persona, threadTs);
1054
- this._markChannelActivity(channel);
1055
- this._markPersonaReply(channel, threadTs, persona.id);
1056
- this._rememberAdHocThreadPersona(channel, threadTs, persona.id);
1057
- // Move issue to In Progress on board (best-effort, spawn via CLI subprocess)
1058
- const boardArgs = buildCurrentCliInvocation([
1059
- 'board', 'move-issue', request.issueNumber, '--column', 'In Progress',
1060
- ]);
1061
- if (boardArgs) {
1062
- try {
1063
- execFileSync(process.execPath, boardArgs, {
1064
- cwd: targetProject.path,
1065
- timeout: 15_000,
1066
- stdio: ['ignore', 'pipe', 'pipe'],
1067
- });
1068
- console.log(`[slack][issue-pickup] moved #${request.issueNumber} to In Progress`);
1069
- }
1070
- catch {
1071
- console.warn(`[slack][issue-pickup] failed to move #${request.issueNumber} to In Progress`);
1072
- }
1073
- }
1074
- console.log(`[slack][issue-pickup] spawning run for #${request.issueNumber}`);
1075
- await this._spawnNightWatchJob('run', targetProject, channel, threadTs, persona, {
1076
- issueNumber: request.issueNumber,
1077
- });
1078
- return true;
1079
- }
1080
- _resolveProactiveChannelForProject(project) {
1081
- const slack = this._config.slack;
1082
- if (!slack)
1083
- return null;
1084
- return project.slackChannelId || slack.channels.eng || null;
1085
- }
1086
- _spawnCodeWatchAudit(project, channel) {
1087
- const invocationArgs = buildCurrentCliInvocation(['audit']);
1088
- if (!invocationArgs) {
1089
- console.warn(`[slack][codewatch] audit spawn failed for ${project.name}: CLI entry path unavailable`);
1090
- return;
1091
- }
1092
- console.log(`[slack][codewatch] spawning audit for ${project.name} → ${channel} cmd=${formatCommandForLog(process.execPath, invocationArgs)}`);
1093
- const child = spawn(process.execPath, invocationArgs, {
1094
- cwd: project.path,
1095
- env: { ...process.env, NW_EXECUTION_CONTEXT: 'agent' },
1096
- stdio: ['ignore', 'pipe', 'pipe'],
1097
- });
1098
- console.log(`[slack][codewatch] audit spawned for ${project.name} pid=${child.pid ?? 'unknown'}`);
1099
- child.stdout?.on('data', () => { });
1100
- child.stderr?.on('data', () => { });
1101
- child.on('error', (err) => {
1102
- console.warn(`[slack][codewatch] audit spawn error for ${project.name}: ${err.message}`);
1103
- });
1104
- child.on('close', async (code) => {
1105
- console.log(`[slack][codewatch] audit finished for ${project.name} exit=${code ?? 'unknown'}`);
1106
- const reportPath = path.join(project.path, 'logs', 'audit-report.md');
1107
- let report;
1108
- try {
1109
- report = fs.readFileSync(reportPath, 'utf-8').trim();
1110
- }
1111
- catch {
1112
- console.log(`[slack][codewatch] no audit report found at ${reportPath}`);
1113
- return;
1114
- }
1115
- try {
1116
- await this._engine.handleAuditReport(report, project.name, project.path, channel);
1117
- this._markChannelActivity(channel);
1118
- }
1119
- catch (err) {
1120
- const msg = err instanceof Error ? err.message : String(err);
1121
- console.warn(`[slack][codewatch] handleAuditReport failed for ${project.name}: ${msg}`);
1122
- }
1123
- });
1124
- }
1125
- async _runProactiveCodeWatch(projects, now) {
1126
- for (const project of projects) {
1127
- const channel = this._resolveProactiveChannelForProject(project);
1128
- if (!channel)
1129
- continue;
1130
- const lastScan = this._lastCodeWatchAt.get(project.path) ?? 0;
1131
- if (now - lastScan < PROACTIVE_CODEWATCH_MIN_INTERVAL_MS) {
1132
- continue;
1133
- }
1134
- this._lastCodeWatchAt.set(project.path, now);
1135
- this._spawnCodeWatchAudit(project, channel);
1136
- }
1137
- }
1138
- _startProactiveLoop() {
1139
- if (this._proactiveTimer)
1140
- return;
1141
- this._proactiveTimer = setInterval(() => {
1142
- void this._sendProactiveMessages();
1143
- }, PROACTIVE_SWEEP_INTERVAL_MS);
1144
- this._proactiveTimer.unref?.();
1145
- }
1146
- _stopProactiveLoop() {
1147
- if (!this._proactiveTimer)
1148
- return;
1149
- clearInterval(this._proactiveTimer);
1150
- this._proactiveTimer = null;
1151
- }
1152
- async _sendProactiveMessages() {
1153
- const slack = this._config.slack;
1154
- if (!slack?.enabled || !slack.discussionEnabled)
1155
- return;
1156
- const channelIds = Object.values(slack.channels ?? {}).filter(Boolean);
1157
- if (channelIds.length === 0)
1158
- return;
1159
- const repos = getRepositories();
1160
- const personas = repos.agentPersona.getActive();
1161
- if (personas.length === 0)
1162
- return;
1163
- const now = Date.now();
1164
- const projects = repos.projectRegistry.getAll();
1165
- await this._runProactiveCodeWatch(projects, now);
1166
- for (const channel of channelIds) {
1167
- const lastActivity = this._lastChannelActivityAt.get(channel) ?? now;
1168
- const lastProactive = this._lastProactiveAt.get(channel) ?? 0;
1169
- if (now - lastActivity < PROACTIVE_IDLE_MS)
1170
- continue;
1171
- if (now - lastProactive < PROACTIVE_MIN_INTERVAL_MS)
1172
- continue;
1173
- const persona = this._pickRandomPersona(personas, channel, `${now}`) ?? personas[0];
1174
- if (!persona)
1175
- continue;
1176
- const projectContext = this._buildProjectContext(channel, projects);
1177
- const roadmapContext = this._buildRoadmapContext(channel, projects);
1178
- try {
1179
- await this._engine.postProactiveMessage(channel, persona, projectContext, roadmapContext);
1180
- this._lastProactiveAt.set(channel, now);
1181
- this._markChannelActivity(channel);
1182
- console.log(`[slack] proactive message posted by ${persona.name} in ${channel}`);
1183
- }
1184
- catch (err) {
1185
- const msg = err instanceof Error ? err.message : String(err);
1186
- console.warn(`Slack proactive message failed: ${msg}`);
1187
- }
1188
- }
1189
- }
1190
- async _handleInboundMessage(event) {
1191
- if (shouldIgnoreInboundSlackEvent(event, this._botUserId)) {
1192
- console.log(`[slack] ignoring event — failed shouldIgnore check (user=${event.user}, bot_id=${event.bot_id ?? '-'}, subtype=${event.subtype ?? '-'})`);
1193
- return;
1194
- }
1195
- const channel = event.channel;
1196
- const ts = event.ts;
1197
- const threadTs = event.thread_ts ?? ts;
1198
- const text = event.text ?? '';
1199
- const messageKey = buildInboundMessageKey(channel, ts, event.type);
1200
- this._markChannelActivity(channel);
1201
- // Deduplicate retried/replayed events to prevent response loops.
1202
- if (!this._rememberMessageKey(messageKey)) {
1203
- console.log(`[slack] duplicate event ${messageKey} — skipping`);
1204
- return;
1205
- }
1206
- const repos = getRepositories();
1207
- const personas = repos.agentPersona.getActive();
1208
- const projects = repos.projectRegistry.getAll();
1209
- const projectContext = this._buildProjectContext(channel, projects);
1210
- // Fetch GitHub issue/PR content from URLs in the message so agents can inspect them.
1211
- const githubUrls = extractGitHubIssueUrls(text);
1212
- console.log(`[slack] processing message channel=${channel} thread=${threadTs} urls=${githubUrls.length}`);
1213
- const githubContext = githubUrls.length > 0 ? await fetchGitHubIssueContext(githubUrls) : '';
1214
- const fullContext = githubContext ? `${projectContext}\n\nReferenced GitHub content:\n${githubContext}` : projectContext;
1215
- if (await this._triggerDirectProviderIfRequested(event, channel, threadTs, ts, personas)) {
1216
- return;
1217
- }
1218
- if (await this._triggerSlackJobIfRequested(event, channel, threadTs, ts, personas)) {
1219
- return;
1220
- }
1221
- if (await this._triggerIssuePickupIfRequested(event, channel, threadTs, ts, personas)) {
1222
- return;
1223
- }
1224
- // @mention matching: "@maya ..."
1225
- let mentionedPersonas = resolveMentionedPersonas(text, personas);
1226
- // Also try plain-name matching (e.g. "Carlos, are you there?").
1227
- // For app_mention text like "<@UBOTID> maya check this", the @-regex won't find "maya".
1228
- if (mentionedPersonas.length === 0) {
1229
- mentionedPersonas = resolvePersonasByPlainName(text, personas);
1230
- if (mentionedPersonas.length > 0) {
1231
- console.log(`[slack] plain-name match: ${mentionedPersonas.map((p) => p.name).join(', ')}`);
1232
- }
1233
- }
1234
- // Persona mentioned → respond regardless of whether a formal discussion exists.
1235
- if (mentionedPersonas.length > 0) {
1236
- console.log(`[slack] routing to persona(s): ${mentionedPersonas.map((p) => p.name).join(', ')} in ${channel}`);
1237
- const discussion = repos
1238
- .slackDiscussion
1239
- .getActive('')
1240
- .find((d) => d.channelId === channel && d.threadTs === threadTs);
1241
- let lastPosted = '';
1242
- let lastPersonaId = '';
1243
- for (const persona of mentionedPersonas) {
1244
- if (this._isPersonaOnCooldown(channel, threadTs, persona.id)) {
1245
- console.log(`[slack] ${persona.name} is on cooldown — skipping`);
1246
- continue;
1247
- }
1248
- await this._applyHumanResponseTiming(channel, ts, persona);
1249
- if (discussion) {
1250
- await this._engine.contributeAsAgent(discussion.id, persona);
1251
- }
1252
- else {
1253
- console.log(`[slack] replying as ${persona.name} in ${channel}`);
1254
- lastPosted = await this._engine.replyAsAgent(channel, threadTs, text, persona, fullContext);
1255
- lastPersonaId = persona.id;
1256
- }
1257
- this._markPersonaReply(channel, threadTs, persona.id);
1258
- }
1259
- if (!discussion && mentionedPersonas[0]) {
1260
- this._rememberAdHocThreadPersona(channel, threadTs, mentionedPersonas[0].id);
1261
- }
1262
- // Follow up if the last agent reply mentions other teammates by name.
1263
- if (lastPosted && lastPersonaId) {
1264
- await this._followAgentMentions(lastPosted, channel, threadTs, personas, fullContext, lastPersonaId);
1265
- }
1266
- return;
1267
- }
1268
- console.log(`[slack] no persona match — checking for active discussion in ${channel}:${threadTs}`);
1269
- // No persona mention — only handle within an existing Night Watch discussion thread.
1270
- const discussion = repos
1271
- .slackDiscussion
1272
- .getActive('')
1273
- .find((d) => d.channelId === channel && d.threadTs === threadTs);
1274
- if (discussion) {
1275
- await this._engine.handleHumanMessage(channel, threadTs, text, event.user);
1276
- return;
1277
- }
1278
- // Continue ad-hoc threads even without a persisted discussion.
1279
- const rememberedPersona = this._getRememberedAdHocPersona(channel, threadTs, personas);
1280
- if (rememberedPersona) {
1281
- const followUpPersona = selectFollowUpPersona(rememberedPersona, personas, text);
1282
- if (followUpPersona.id !== rememberedPersona.id) {
1283
- console.log(`[slack] handing off ad-hoc thread from ${rememberedPersona.name} to ${followUpPersona.name} based on topic`);
1284
- }
1285
- else {
1286
- console.log(`[slack] continuing ad-hoc thread with ${rememberedPersona.name}`);
1287
- }
1288
- await this._applyHumanResponseTiming(channel, ts, followUpPersona);
1289
- console.log(`[slack] replying as ${followUpPersona.name} in ${channel}`);
1290
- const postedText = await this._engine.replyAsAgent(channel, threadTs, text, followUpPersona, fullContext);
1291
- this._markPersonaReply(channel, threadTs, followUpPersona.id);
1292
- this._rememberAdHocThreadPersona(channel, threadTs, followUpPersona.id);
1293
- await this._followAgentMentions(postedText, channel, threadTs, personas, fullContext, followUpPersona.id);
1294
- return;
1295
- }
1296
- // In-memory state was lost (e.g. server restart) — recover persona from thread history.
1297
- if (threadTs) {
1298
- const recoveredPersona = await this._recoverPersonaFromThreadHistory(channel, threadTs, personas);
1299
- if (recoveredPersona) {
1300
- const followUpPersona = selectFollowUpPersona(recoveredPersona, personas, text);
1301
- console.log(`[slack] recovered ad-hoc thread persona ${recoveredPersona.name} from history, replying as ${followUpPersona.name}`);
1302
- await this._applyHumanResponseTiming(channel, ts, followUpPersona);
1303
- console.log(`[slack] replying as ${followUpPersona.name} in ${channel}`);
1304
- const postedText = await this._engine.replyAsAgent(channel, threadTs, text, followUpPersona, fullContext);
1305
- this._markPersonaReply(channel, threadTs, followUpPersona.id);
1306
- this._rememberAdHocThreadPersona(channel, threadTs, followUpPersona.id);
1307
- await this._followAgentMentions(postedText, channel, threadTs, personas, fullContext, followUpPersona.id);
1308
- return;
1309
- }
1310
- }
1311
- // Keep the channel alive: direct mentions and ambient greetings get a random responder.
1312
- const shouldAutoEngage = event.type === 'app_mention' || isAmbientTeamMessage(text);
1313
- if (shouldAutoEngage) {
1314
- const randomPersona = this._pickRandomPersona(personas, channel, threadTs);
1315
- if (randomPersona) {
1316
- console.log(`[slack] auto-engaging via ${randomPersona.name}`);
1317
- await this._applyHumanResponseTiming(channel, ts, randomPersona);
1318
- console.log(`[slack] replying as ${randomPersona.name} in ${channel}`);
1319
- const postedText = await this._engine.replyAsAgent(channel, threadTs, text, randomPersona, fullContext);
1320
- this._markPersonaReply(channel, threadTs, randomPersona.id);
1321
- this._rememberAdHocThreadPersona(channel, threadTs, randomPersona.id);
1322
- await this._followAgentMentions(postedText, channel, threadTs, personas, fullContext, randomPersona.id);
1323
- return;
1324
- }
1325
- }
1326
- console.log(`[slack] no active discussion found — ignoring message`);
1327
- }
1328
- }
1329
- //# sourceMappingURL=interaction-listener.js.map