reflectt-node 0.1.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 (486) hide show
  1. package/LICENSE +178 -0
  2. package/README.md +188 -0
  3. package/dist/activationEvents.d.ts +110 -0
  4. package/dist/activationEvents.d.ts.map +1 -0
  5. package/dist/activationEvents.js +378 -0
  6. package/dist/activationEvents.js.map +1 -0
  7. package/dist/activity-signal.d.ts +30 -0
  8. package/dist/activity-signal.d.ts.map +1 -0
  9. package/dist/activity-signal.js +93 -0
  10. package/dist/activity-signal.js.map +1 -0
  11. package/dist/alert-integrity.d.ts +100 -0
  12. package/dist/alert-integrity.d.ts.map +1 -0
  13. package/dist/alert-integrity.js +333 -0
  14. package/dist/alert-integrity.js.map +1 -0
  15. package/dist/alert-preflight.d.ts +40 -0
  16. package/dist/alert-preflight.d.ts.map +1 -0
  17. package/dist/alert-preflight.js +235 -0
  18. package/dist/alert-preflight.js.map +1 -0
  19. package/dist/analytics.d.ts +131 -0
  20. package/dist/analytics.d.ts.map +1 -0
  21. package/dist/analytics.js +371 -0
  22. package/dist/analytics.js.map +1 -0
  23. package/dist/artifact-mirror.d.ts +26 -0
  24. package/dist/artifact-mirror.d.ts.map +1 -0
  25. package/dist/artifact-mirror.js +170 -0
  26. package/dist/artifact-mirror.js.map +1 -0
  27. package/dist/artifact-resolver.d.ts +48 -0
  28. package/dist/artifact-resolver.d.ts.map +1 -0
  29. package/dist/artifact-resolver.js +164 -0
  30. package/dist/artifact-resolver.js.map +1 -0
  31. package/dist/assignment.d.ts +116 -0
  32. package/dist/assignment.d.ts.map +1 -0
  33. package/dist/assignment.js +475 -0
  34. package/dist/assignment.js.map +1 -0
  35. package/dist/auditLedger.d.ts +50 -0
  36. package/dist/auditLedger.d.ts.map +1 -0
  37. package/dist/auditLedger.js +136 -0
  38. package/dist/auditLedger.js.map +1 -0
  39. package/dist/boardHealthWorker.d.ts +134 -0
  40. package/dist/boardHealthWorker.d.ts.map +1 -0
  41. package/dist/boardHealthWorker.js +882 -0
  42. package/dist/boardHealthWorker.js.map +1 -0
  43. package/dist/bootstrap-team.d.ts +42 -0
  44. package/dist/bootstrap-team.d.ts.map +1 -0
  45. package/dist/bootstrap-team.js +111 -0
  46. package/dist/bootstrap-team.js.map +1 -0
  47. package/dist/buildInfo.d.ts +17 -0
  48. package/dist/buildInfo.d.ts.map +1 -0
  49. package/dist/buildInfo.js +56 -0
  50. package/dist/buildInfo.js.map +1 -0
  51. package/dist/calendar-events.d.ts +133 -0
  52. package/dist/calendar-events.d.ts.map +1 -0
  53. package/dist/calendar-events.js +615 -0
  54. package/dist/calendar-events.js.map +1 -0
  55. package/dist/calendar-ical.d.ts +41 -0
  56. package/dist/calendar-ical.d.ts.map +1 -0
  57. package/dist/calendar-ical.js +413 -0
  58. package/dist/calendar-ical.js.map +1 -0
  59. package/dist/calendar-reminder-engine.d.ts +10 -0
  60. package/dist/calendar-reminder-engine.d.ts.map +1 -0
  61. package/dist/calendar-reminder-engine.js +143 -0
  62. package/dist/calendar-reminder-engine.js.map +1 -0
  63. package/dist/calendar.d.ts +75 -0
  64. package/dist/calendar.d.ts.map +1 -0
  65. package/dist/calendar.js +391 -0
  66. package/dist/calendar.js.map +1 -0
  67. package/dist/canvas-multiplexer.d.ts +44 -0
  68. package/dist/canvas-multiplexer.d.ts.map +1 -0
  69. package/dist/canvas-multiplexer.js +150 -0
  70. package/dist/canvas-multiplexer.js.map +1 -0
  71. package/dist/canvas-slots.d.ts +83 -0
  72. package/dist/canvas-slots.d.ts.map +1 -0
  73. package/dist/canvas-slots.js +144 -0
  74. package/dist/canvas-slots.js.map +1 -0
  75. package/dist/canvas-types.d.ts +56 -0
  76. package/dist/canvas-types.d.ts.map +1 -0
  77. package/dist/canvas-types.js +54 -0
  78. package/dist/canvas-types.js.map +1 -0
  79. package/dist/cf-keepalive.d.ts +40 -0
  80. package/dist/cf-keepalive.d.ts.map +1 -0
  81. package/dist/cf-keepalive.js +153 -0
  82. package/dist/cf-keepalive.js.map +1 -0
  83. package/dist/changeFeed.d.ts +38 -0
  84. package/dist/changeFeed.d.ts.map +1 -0
  85. package/dist/changeFeed.js +324 -0
  86. package/dist/changeFeed.js.map +1 -0
  87. package/dist/channels.d.ts +28 -0
  88. package/dist/channels.d.ts.map +1 -0
  89. package/dist/channels.js +23 -0
  90. package/dist/channels.js.map +1 -0
  91. package/dist/chat-approval-detector.d.ts +47 -0
  92. package/dist/chat-approval-detector.d.ts.map +1 -0
  93. package/dist/chat-approval-detector.js +224 -0
  94. package/dist/chat-approval-detector.js.map +1 -0
  95. package/dist/chat.d.ts +119 -0
  96. package/dist/chat.d.ts.map +1 -0
  97. package/dist/chat.js +666 -0
  98. package/dist/chat.js.map +1 -0
  99. package/dist/cli.d.ts +3 -0
  100. package/dist/cli.d.ts.map +1 -0
  101. package/dist/cli.js +1142 -0
  102. package/dist/cli.js.map +1 -0
  103. package/dist/cloud.d.ts +45 -0
  104. package/dist/cloud.d.ts.map +1 -0
  105. package/dist/cloud.js +962 -0
  106. package/dist/cloud.js.map +1 -0
  107. package/dist/config.d.ts +17 -0
  108. package/dist/config.d.ts.map +1 -0
  109. package/dist/config.js +33 -0
  110. package/dist/config.js.map +1 -0
  111. package/dist/connectivity.d.ts +59 -0
  112. package/dist/connectivity.d.ts.map +1 -0
  113. package/dist/connectivity.js +173 -0
  114. package/dist/connectivity.js.map +1 -0
  115. package/dist/contacts.d.ts +59 -0
  116. package/dist/contacts.d.ts.map +1 -0
  117. package/dist/contacts.js +183 -0
  118. package/dist/contacts.js.map +1 -0
  119. package/dist/content.d.ts +130 -0
  120. package/dist/content.d.ts.map +1 -0
  121. package/dist/content.js +186 -0
  122. package/dist/content.js.map +1 -0
  123. package/dist/context-budget.d.ts +87 -0
  124. package/dist/context-budget.d.ts.map +1 -0
  125. package/dist/context-budget.js +459 -0
  126. package/dist/context-budget.js.map +1 -0
  127. package/dist/continuity-loop.d.ts +55 -0
  128. package/dist/continuity-loop.d.ts.map +1 -0
  129. package/dist/continuity-loop.js +267 -0
  130. package/dist/continuity-loop.js.map +1 -0
  131. package/dist/dashboard.d.ts +6 -0
  132. package/dist/dashboard.d.ts.map +1 -0
  133. package/dist/dashboard.js +2348 -0
  134. package/dist/dashboard.js.map +1 -0
  135. package/dist/db.d.ts +44 -0
  136. package/dist/db.d.ts.map +1 -0
  137. package/dist/db.js +648 -0
  138. package/dist/db.js.map +1 -0
  139. package/dist/doctor.d.ts +30 -0
  140. package/dist/doctor.d.ts.map +1 -0
  141. package/dist/doctor.js +159 -0
  142. package/dist/doctor.js.map +1 -0
  143. package/dist/duplicateClosureGuard.d.ts +31 -0
  144. package/dist/duplicateClosureGuard.d.ts.map +1 -0
  145. package/dist/duplicateClosureGuard.js +83 -0
  146. package/dist/duplicateClosureGuard.js.map +1 -0
  147. package/dist/embeddings.d.ts +13 -0
  148. package/dist/embeddings.d.ts.map +1 -0
  149. package/dist/embeddings.js +78 -0
  150. package/dist/embeddings.js.map +1 -0
  151. package/dist/escalation.d.ts +80 -0
  152. package/dist/escalation.d.ts.map +1 -0
  153. package/dist/escalation.js +213 -0
  154. package/dist/escalation.js.map +1 -0
  155. package/dist/events.d.ts +130 -0
  156. package/dist/events.d.ts.map +1 -0
  157. package/dist/events.js +382 -0
  158. package/dist/events.js.map +1 -0
  159. package/dist/executionSweeper.d.ts +97 -0
  160. package/dist/executionSweeper.d.ts.map +1 -0
  161. package/dist/executionSweeper.js +875 -0
  162. package/dist/executionSweeper.js.map +1 -0
  163. package/dist/experiments.d.ts +47 -0
  164. package/dist/experiments.d.ts.map +1 -0
  165. package/dist/experiments.js +133 -0
  166. package/dist/experiments.js.map +1 -0
  167. package/dist/feedback.d.ts +179 -0
  168. package/dist/feedback.d.ts.map +1 -0
  169. package/dist/feedback.js +397 -0
  170. package/dist/feedback.js.map +1 -0
  171. package/dist/files.d.ts +52 -0
  172. package/dist/files.d.ts.map +1 -0
  173. package/dist/files.js +172 -0
  174. package/dist/files.js.map +1 -0
  175. package/dist/format-duration.d.ts +19 -0
  176. package/dist/format-duration.d.ts.map +1 -0
  177. package/dist/format-duration.js +33 -0
  178. package/dist/format-duration.js.map +1 -0
  179. package/dist/github-actor-auth.d.ts +20 -0
  180. package/dist/github-actor-auth.d.ts.map +1 -0
  181. package/dist/github-actor-auth.js +54 -0
  182. package/dist/github-actor-auth.js.map +1 -0
  183. package/dist/github-ci.d.ts +16 -0
  184. package/dist/github-ci.d.ts.map +1 -0
  185. package/dist/github-ci.js +37 -0
  186. package/dist/github-ci.js.map +1 -0
  187. package/dist/github-identity.d.ts +30 -0
  188. package/dist/github-identity.d.ts.map +1 -0
  189. package/dist/github-identity.js +96 -0
  190. package/dist/github-identity.js.map +1 -0
  191. package/dist/github-reviews.d.ts +24 -0
  192. package/dist/github-reviews.d.ts.map +1 -0
  193. package/dist/github-reviews.js +56 -0
  194. package/dist/github-reviews.js.map +1 -0
  195. package/dist/health.d.ts +391 -0
  196. package/dist/health.d.ts.map +1 -0
  197. package/dist/health.js +1841 -0
  198. package/dist/health.js.map +1 -0
  199. package/dist/host-keepalive.d.ts +22 -0
  200. package/dist/host-keepalive.d.ts.map +1 -0
  201. package/dist/host-keepalive.js +126 -0
  202. package/dist/host-keepalive.js.map +1 -0
  203. package/dist/host-registry.d.ts +43 -0
  204. package/dist/host-registry.d.ts.map +1 -0
  205. package/dist/host-registry.js +93 -0
  206. package/dist/host-registry.js.map +1 -0
  207. package/dist/inbox.d.ts +87 -0
  208. package/dist/inbox.d.ts.map +1 -0
  209. package/dist/inbox.js +410 -0
  210. package/dist/inbox.js.map +1 -0
  211. package/dist/index.d.ts +2 -0
  212. package/dist/index.d.ts.map +1 -0
  213. package/dist/index.js +306 -0
  214. package/dist/index.js.map +1 -0
  215. package/dist/insight-mutation.d.ts +32 -0
  216. package/dist/insight-mutation.d.ts.map +1 -0
  217. package/dist/insight-mutation.js +160 -0
  218. package/dist/insight-mutation.js.map +1 -0
  219. package/dist/insight-promotion.d.ts +89 -0
  220. package/dist/insight-promotion.d.ts.map +1 -0
  221. package/dist/insight-promotion.js +278 -0
  222. package/dist/insight-promotion.js.map +1 -0
  223. package/dist/insight-task-bridge.d.ts +77 -0
  224. package/dist/insight-task-bridge.d.ts.map +1 -0
  225. package/dist/insight-task-bridge.js +556 -0
  226. package/dist/insight-task-bridge.js.map +1 -0
  227. package/dist/insights.d.ts +222 -0
  228. package/dist/insights.d.ts.map +1 -0
  229. package/dist/insights.js +871 -0
  230. package/dist/insights.js.map +1 -0
  231. package/dist/intake-pipeline.d.ts +74 -0
  232. package/dist/intake-pipeline.d.ts.map +1 -0
  233. package/dist/intake-pipeline.js +199 -0
  234. package/dist/intake-pipeline.js.map +1 -0
  235. package/dist/intensity.d.ts +31 -0
  236. package/dist/intensity.d.ts.map +1 -0
  237. package/dist/intensity.js +94 -0
  238. package/dist/intensity.js.map +1 -0
  239. package/dist/knowledge-auto-index.d.ts +37 -0
  240. package/dist/knowledge-auto-index.d.ts.map +1 -0
  241. package/dist/knowledge-auto-index.js +149 -0
  242. package/dist/knowledge-auto-index.js.map +1 -0
  243. package/dist/knowledge-docs.d.ts +45 -0
  244. package/dist/knowledge-docs.d.ts.map +1 -0
  245. package/dist/knowledge-docs.js +188 -0
  246. package/dist/knowledge-docs.js.map +1 -0
  247. package/dist/lane-config.d.ts +25 -0
  248. package/dist/lane-config.d.ts.map +1 -0
  249. package/dist/lane-config.js +105 -0
  250. package/dist/lane-config.js.map +1 -0
  251. package/dist/lineage.d.ts +86 -0
  252. package/dist/lineage.d.ts.map +1 -0
  253. package/dist/lineage.js +303 -0
  254. package/dist/lineage.js.map +1 -0
  255. package/dist/logStore.d.ts +25 -0
  256. package/dist/logStore.d.ts.map +1 -0
  257. package/dist/logStore.js +83 -0
  258. package/dist/logStore.js.map +1 -0
  259. package/dist/manage.d.ts +12 -0
  260. package/dist/manage.d.ts.map +1 -0
  261. package/dist/manage.js +253 -0
  262. package/dist/manage.js.map +1 -0
  263. package/dist/mcp.d.ts +5 -0
  264. package/dist/mcp.d.ts.map +1 -0
  265. package/dist/mcp.js +604 -0
  266. package/dist/mcp.js.map +1 -0
  267. package/dist/memory.d.ts +47 -0
  268. package/dist/memory.d.ts.map +1 -0
  269. package/dist/memory.js +149 -0
  270. package/dist/memory.js.map +1 -0
  271. package/dist/mention-ack.d.ts +80 -0
  272. package/dist/mention-ack.d.ts.map +1 -0
  273. package/dist/mention-ack.js +175 -0
  274. package/dist/mention-ack.js.map +1 -0
  275. package/dist/messageRouter.d.ts +60 -0
  276. package/dist/messageRouter.d.ts.map +1 -0
  277. package/dist/messageRouter.js +309 -0
  278. package/dist/messageRouter.js.map +1 -0
  279. package/dist/mutationAlert.d.ts +44 -0
  280. package/dist/mutationAlert.d.ts.map +1 -0
  281. package/dist/mutationAlert.js +174 -0
  282. package/dist/mutationAlert.js.map +1 -0
  283. package/dist/noise-budget.d.ts +136 -0
  284. package/dist/noise-budget.d.ts.map +1 -0
  285. package/dist/noise-budget.js +340 -0
  286. package/dist/noise-budget.js.map +1 -0
  287. package/dist/notifications.d.ts +67 -0
  288. package/dist/notifications.d.ts.map +1 -0
  289. package/dist/notifications.js +253 -0
  290. package/dist/notifications.js.map +1 -0
  291. package/dist/openclaw.d.ts +34 -0
  292. package/dist/openclaw.d.ts.map +1 -0
  293. package/dist/openclaw.js +208 -0
  294. package/dist/openclaw.js.map +1 -0
  295. package/dist/pause-controls.d.ts +31 -0
  296. package/dist/pause-controls.d.ts.map +1 -0
  297. package/dist/pause-controls.js +130 -0
  298. package/dist/pause-controls.js.map +1 -0
  299. package/dist/pidlock.d.ts +25 -0
  300. package/dist/pidlock.d.ts.map +1 -0
  301. package/dist/pidlock.js +179 -0
  302. package/dist/pidlock.js.map +1 -0
  303. package/dist/policy.d.ts +139 -0
  304. package/dist/policy.d.ts.map +1 -0
  305. package/dist/policy.js +264 -0
  306. package/dist/policy.js.map +1 -0
  307. package/dist/polls.d.ts +47 -0
  308. package/dist/polls.d.ts.map +1 -0
  309. package/dist/polls.js +162 -0
  310. package/dist/polls.js.map +1 -0
  311. package/dist/portability.d.ts +55 -0
  312. package/dist/portability.d.ts.map +1 -0
  313. package/dist/portability.js +292 -0
  314. package/dist/portability.js.map +1 -0
  315. package/dist/pr-integrity.d.ts +45 -0
  316. package/dist/pr-integrity.d.ts.map +1 -0
  317. package/dist/pr-integrity.js +124 -0
  318. package/dist/pr-integrity.js.map +1 -0
  319. package/dist/prAutoMerge.d.ts +62 -0
  320. package/dist/prAutoMerge.d.ts.map +1 -0
  321. package/dist/prAutoMerge.js +493 -0
  322. package/dist/prAutoMerge.js.map +1 -0
  323. package/dist/preflight.d.ts +66 -0
  324. package/dist/preflight.d.ts.map +1 -0
  325. package/dist/preflight.js +864 -0
  326. package/dist/preflight.js.map +1 -0
  327. package/dist/presence.d.ts +98 -0
  328. package/dist/presence.d.ts.map +1 -0
  329. package/dist/presence.js +347 -0
  330. package/dist/presence.js.map +1 -0
  331. package/dist/provisioning.d.ts +101 -0
  332. package/dist/provisioning.d.ts.map +1 -0
  333. package/dist/provisioning.js +430 -0
  334. package/dist/provisioning.js.map +1 -0
  335. package/dist/reflection-automation.d.ts +59 -0
  336. package/dist/reflection-automation.d.ts.map +1 -0
  337. package/dist/reflection-automation.js +350 -0
  338. package/dist/reflection-automation.js.map +1 -0
  339. package/dist/reflections.d.ts +65 -0
  340. package/dist/reflections.d.ts.map +1 -0
  341. package/dist/reflections.js +306 -0
  342. package/dist/reflections.js.map +1 -0
  343. package/dist/release.d.ts +67 -0
  344. package/dist/release.d.ts.map +1 -0
  345. package/dist/release.js +275 -0
  346. package/dist/release.js.map +1 -0
  347. package/dist/request-tracker.d.ts +36 -0
  348. package/dist/request-tracker.d.ts.map +1 -0
  349. package/dist/request-tracker.js +109 -0
  350. package/dist/request-tracker.js.map +1 -0
  351. package/dist/research.d.ts +75 -0
  352. package/dist/research.d.ts.map +1 -0
  353. package/dist/research.js +171 -0
  354. package/dist/research.js.map +1 -0
  355. package/dist/routing-approvals.d.ts +73 -0
  356. package/dist/routing-approvals.d.ts.map +1 -0
  357. package/dist/routing-approvals.js +88 -0
  358. package/dist/routing-approvals.js.map +1 -0
  359. package/dist/routing-override.d.ts +94 -0
  360. package/dist/routing-override.d.ts.map +1 -0
  361. package/dist/routing-override.js +290 -0
  362. package/dist/routing-override.js.map +1 -0
  363. package/dist/scope-routing.d.ts +18 -0
  364. package/dist/scope-routing.d.ts.map +1 -0
  365. package/dist/scope-routing.js +29 -0
  366. package/dist/scope-routing.js.map +1 -0
  367. package/dist/secrets.d.ts +77 -0
  368. package/dist/secrets.d.ts.map +1 -0
  369. package/dist/secrets.js +287 -0
  370. package/dist/secrets.js.map +1 -0
  371. package/dist/server.d.ts +3 -0
  372. package/dist/server.d.ts.map +1 -0
  373. package/dist/server.js +10887 -0
  374. package/dist/server.js.map +1 -0
  375. package/dist/service-probe.d.ts +53 -0
  376. package/dist/service-probe.d.ts.map +1 -0
  377. package/dist/service-probe.js +225 -0
  378. package/dist/service-probe.js.map +1 -0
  379. package/dist/shared-workspace-api.d.ts +73 -0
  380. package/dist/shared-workspace-api.d.ts.map +1 -0
  381. package/dist/shared-workspace-api.js +281 -0
  382. package/dist/shared-workspace-api.js.map +1 -0
  383. package/dist/shipped-heartbeat.d.ts +91 -0
  384. package/dist/shipped-heartbeat.d.ts.map +1 -0
  385. package/dist/shipped-heartbeat.js +272 -0
  386. package/dist/shipped-heartbeat.js.map +1 -0
  387. package/dist/starter-team.d.ts +23 -0
  388. package/dist/starter-team.d.ts.map +1 -0
  389. package/dist/starter-team.js +88 -0
  390. package/dist/starter-team.js.map +1 -0
  391. package/dist/suppression-ledger.d.ts +73 -0
  392. package/dist/suppression-ledger.d.ts.map +1 -0
  393. package/dist/suppression-ledger.js +125 -0
  394. package/dist/suppression-ledger.js.map +1 -0
  395. package/dist/system-loop-state.d.ts +4 -0
  396. package/dist/system-loop-state.d.ts.map +1 -0
  397. package/dist/system-loop-state.js +40 -0
  398. package/dist/system-loop-state.js.map +1 -0
  399. package/dist/taskCommentIngest.d.ts +43 -0
  400. package/dist/taskCommentIngest.d.ts.map +1 -0
  401. package/dist/taskCommentIngest.js +59 -0
  402. package/dist/taskCommentIngest.js.map +1 -0
  403. package/dist/taskPrecheck.d.ts +20 -0
  404. package/dist/taskPrecheck.d.ts.map +1 -0
  405. package/dist/taskPrecheck.js +329 -0
  406. package/dist/taskPrecheck.js.map +1 -0
  407. package/dist/taskStateSync.d.ts +8 -0
  408. package/dist/taskStateSync.d.ts.map +1 -0
  409. package/dist/taskStateSync.js +79 -0
  410. package/dist/taskStateSync.js.map +1 -0
  411. package/dist/tasks.d.ts +140 -0
  412. package/dist/tasks.d.ts.map +1 -0
  413. package/dist/tasks.js +1281 -0
  414. package/dist/tasks.js.map +1 -0
  415. package/dist/team-config.d.ts +24 -0
  416. package/dist/team-config.d.ts.map +1 -0
  417. package/dist/team-config.js +221 -0
  418. package/dist/team-config.js.map +1 -0
  419. package/dist/team-doctor.d.ts +22 -0
  420. package/dist/team-doctor.d.ts.map +1 -0
  421. package/dist/team-doctor.js +270 -0
  422. package/dist/team-doctor.js.map +1 -0
  423. package/dist/team-pulse.d.ts +52 -0
  424. package/dist/team-pulse.d.ts.map +1 -0
  425. package/dist/team-pulse.js +176 -0
  426. package/dist/team-pulse.js.map +1 -0
  427. package/dist/telemetry.d.ts +74 -0
  428. package/dist/telemetry.d.ts.map +1 -0
  429. package/dist/telemetry.js +256 -0
  430. package/dist/telemetry.js.map +1 -0
  431. package/dist/test-task-filter.d.ts +21 -0
  432. package/dist/test-task-filter.d.ts.map +1 -0
  433. package/dist/test-task-filter.js +48 -0
  434. package/dist/test-task-filter.js.map +1 -0
  435. package/dist/types.d.ts +126 -0
  436. package/dist/types.d.ts.map +1 -0
  437. package/dist/types.js +4 -0
  438. package/dist/types.js.map +1 -0
  439. package/dist/usage-tracking.d.ts +101 -0
  440. package/dist/usage-tracking.d.ts.map +1 -0
  441. package/dist/usage-tracking.js +325 -0
  442. package/dist/usage-tracking.js.map +1 -0
  443. package/dist/vector-store.d.ts +87 -0
  444. package/dist/vector-store.d.ts.map +1 -0
  445. package/dist/vector-store.js +247 -0
  446. package/dist/vector-store.js.map +1 -0
  447. package/dist/watchdog/idleNudgeLane.d.ts +22 -0
  448. package/dist/watchdog/idleNudgeLane.d.ts.map +1 -0
  449. package/dist/watchdog/idleNudgeLane.js +98 -0
  450. package/dist/watchdog/idleNudgeLane.js.map +1 -0
  451. package/dist/webhooks.d.ts +103 -0
  452. package/dist/webhooks.d.ts.map +1 -0
  453. package/dist/webhooks.js +398 -0
  454. package/dist/webhooks.js.map +1 -0
  455. package/dist/working-contract.d.ts +42 -0
  456. package/dist/working-contract.d.ts.map +1 -0
  457. package/dist/working-contract.js +228 -0
  458. package/dist/working-contract.js.map +1 -0
  459. package/dist/ws-heartbeat.d.ts +66 -0
  460. package/dist/ws-heartbeat.d.ts.map +1 -0
  461. package/dist/ws-heartbeat.js +174 -0
  462. package/dist/ws-heartbeat.js.map +1 -0
  463. package/package.json +87 -0
  464. package/plugins/reflectt-channel/README.md +96 -0
  465. package/plugins/reflectt-channel/index.ts +789 -0
  466. package/plugins/reflectt-channel/openclaw.plugin.json +23 -0
  467. package/plugins/reflectt-channel/package.json +23 -0
  468. package/plugins/reflectt-channel/src/channel.ts +433 -0
  469. package/plugins/reflectt-channel/src/types.ts +29 -0
  470. package/public/avatars/echo.png +0 -0
  471. package/public/avatars/harmony.png +0 -0
  472. package/public/avatars/kai.png +0 -0
  473. package/public/avatars/link.png +0 -0
  474. package/public/avatars/pixel.png +0 -0
  475. package/public/avatars/rhythm.png +0 -0
  476. package/public/avatars/ryan.png +0 -0
  477. package/public/avatars/sage.png +0 -0
  478. package/public/avatars/scout.png +0 -0
  479. package/public/avatars/spark.png +0 -0
  480. package/public/dashboard-animations.css +381 -0
  481. package/public/dashboard.js +3479 -0
  482. package/public/docs.md +1062 -0
  483. package/public/file-upload-mock.html +1097 -0
  484. package/public/og-card.png +0 -0
  485. package/public/ui-kit.html +318 -0
  486. package/public/widget/feedback.js +194 -0
package/dist/tasks.js ADDED
@@ -0,0 +1,1281 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) Reflectt AI
3
+ import { promises as fs } from 'fs';
4
+ import { join } from 'path';
5
+ import { eventBus } from './events.js';
6
+ import { DATA_DIR, LEGACY_DATA_DIR } from './config.js';
7
+ import { createTaskStateAdapterFromEnv } from './taskStateSync.js';
8
+ import { getDb, importJsonlIfNeeded, safeJsonStringify, safeJsonParse } from './db.js';
9
+ import { isTestHarnessTask, TEST_TASK_EXCLUDE_SQL } from './test-task-filter.js';
10
+ import { assertDuplicateClosureHasCanonicalRefs } from './duplicateClosureGuard.js';
11
+ import { getAgentAliases } from './assignment.js';
12
+ const TASKS_FILE = join(DATA_DIR, 'tasks.jsonl');
13
+ const LEGACY_TASKS_FILE = join(LEGACY_DATA_DIR, 'tasks.jsonl');
14
+ const RECURRING_TASKS_FILE = join(DATA_DIR, 'tasks.recurring.jsonl');
15
+ const TASK_HISTORY_FILE = join(DATA_DIR, 'tasks.history.jsonl');
16
+ const TASK_COMMENTS_FILE = join(DATA_DIR, 'tasks.comments.jsonl');
17
+ /**
18
+ * Import functions for one-time JSONL → SQLite migration
19
+ */
20
+ function importTasks(db, records) {
21
+ const insert = db.prepare(`
22
+ INSERT OR REPLACE INTO tasks (
23
+ id, title, description, status, assignee, reviewer, done_criteria,
24
+ created_by, created_at, updated_at, priority, blocked_by, epic_id,
25
+ tags, metadata, team_id, comment_count
26
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
27
+ `);
28
+ const insertMany = db.transaction((tasks) => {
29
+ for (const record of tasks) {
30
+ const task = record;
31
+ insert.run(task.id, task.title, task.description ?? null, task.status, task.assignee ?? null, task.reviewer ?? null, safeJsonStringify(task.done_criteria), task.createdBy, task.createdAt, task.updatedAt, task.priority ?? null, safeJsonStringify(task.blocked_by), task.epic_id ?? null, safeJsonStringify(task.tags), safeJsonStringify(task.metadata), task.teamId ?? null, 0 // comment_count will be recalculated when comments are imported
32
+ );
33
+ }
34
+ });
35
+ insertMany(records);
36
+ return records.length;
37
+ }
38
+ function importRecurringTasks(db, records) {
39
+ const insert = db.prepare(`
40
+ INSERT OR REPLACE INTO recurring_tasks (
41
+ id, title, description, assignee, reviewer, done_criteria, created_by,
42
+ priority, blocked_by, epic_id, tags, metadata, schedule, enabled,
43
+ status, last_run_at, last_skip_at, last_skip_reason, next_run_at,
44
+ created_at, updated_at
45
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
46
+ `);
47
+ const insertMany = db.transaction((tasks) => {
48
+ for (const record of tasks) {
49
+ const rt = record;
50
+ insert.run(rt.id, rt.title, rt.description ?? null, rt.assignee ?? null, rt.reviewer ?? null, safeJsonStringify(rt.done_criteria), rt.createdBy, rt.priority ?? null, safeJsonStringify(rt.blocked_by), rt.epic_id ?? null, safeJsonStringify(rt.tags), safeJsonStringify(rt.metadata), safeJsonStringify(rt.schedule), rt.enabled ? 1 : 0, rt.status ?? 'todo', rt.lastRunAt ?? null, rt.lastSkipAt ?? null, rt.lastSkipReason ?? null, rt.nextRunAt, rt.createdAt, rt.updatedAt);
51
+ }
52
+ });
53
+ insertMany(records);
54
+ return records.length;
55
+ }
56
+ function importTaskHistory(db, records) {
57
+ const insert = db.prepare(`
58
+ INSERT OR REPLACE INTO task_history (
59
+ id, task_id, type, actor, timestamp, data
60
+ ) VALUES (?, ?, ?, ?, ?, ?)
61
+ `);
62
+ const insertMany = db.transaction((events) => {
63
+ for (const record of events) {
64
+ const event = record;
65
+ insert.run(event.id, event.taskId, event.type, event.actor, event.timestamp, safeJsonStringify(event.data));
66
+ }
67
+ });
68
+ insertMany(records);
69
+ return records.length;
70
+ }
71
+ function importTaskComments(db, records) {
72
+ const insert = db.prepare(`
73
+ INSERT OR REPLACE INTO task_comments (
74
+ id, task_id, author, content, timestamp,
75
+ category, suppressed, suppressed_reason, suppressed_rule
76
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
77
+ `);
78
+ const updateCommentCount = db.prepare(`
79
+ UPDATE tasks
80
+ SET comment_count = (SELECT COUNT(*) FROM task_comments WHERE task_id = ?)
81
+ WHERE id = ?
82
+ `);
83
+ const insertMany = db.transaction((comments) => {
84
+ const taskIds = new Set();
85
+ for (const record of comments) {
86
+ const comment = record;
87
+ insert.run(comment.id, comment.taskId, comment.author, comment.content, comment.timestamp, comment.category ?? null, comment.suppressed ? 1 : 0, comment.suppressedReason ?? null, comment.suppressedRule ?? null);
88
+ taskIds.add(comment.taskId);
89
+ }
90
+ // Update comment counts for all affected tasks
91
+ for (const taskId of taskIds) {
92
+ updateCommentCount.run(taskId, taskId);
93
+ }
94
+ });
95
+ insertMany(records);
96
+ return records.length;
97
+ }
98
+ function rowToTask(row) {
99
+ return {
100
+ id: row.id,
101
+ title: row.title,
102
+ description: row.description ?? undefined,
103
+ status: row.status,
104
+ assignee: row.assignee ?? undefined,
105
+ reviewer: row.reviewer ?? undefined,
106
+ done_criteria: safeJsonParse(row.done_criteria),
107
+ createdBy: row.created_by,
108
+ createdAt: row.created_at,
109
+ updatedAt: row.updated_at,
110
+ priority: row.priority ?? undefined,
111
+ blocked_by: safeJsonParse(row.blocked_by),
112
+ epic_id: row.epic_id ?? undefined,
113
+ tags: safeJsonParse(row.tags),
114
+ metadata: safeJsonParse(row.metadata),
115
+ teamId: row.team_id ?? undefined,
116
+ };
117
+ }
118
+ /** Query all tasks from SQLite */
119
+ function queryAllTasks() {
120
+ const db = getDb();
121
+ const rows = db.prepare('SELECT * FROM tasks').all();
122
+ return rows.map(rowToTask);
123
+ }
124
+ /** Query single task by ID from SQLite */
125
+ function queryTask(id) {
126
+ const db = getDb();
127
+ const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
128
+ return row ? rowToTask(row) : undefined;
129
+ }
130
+ /** Check if task exists in SQLite */
131
+ function taskExists(id) {
132
+ const db = getDb();
133
+ const row = db.prepare('SELECT 1 FROM tasks WHERE id = ? LIMIT 1').get(id);
134
+ return !!row;
135
+ }
136
+ /** Get count of tasks */
137
+ function taskCount() {
138
+ const db = getDb();
139
+ const row = db.prepare('SELECT COUNT(*) as count FROM tasks').get();
140
+ return row.count;
141
+ }
142
+ class TaskManager {
143
+ // No in-memory tasks Map — all reads go to SQLite
144
+ subscribers = new Set();
145
+ recurringTasks = new Map();
146
+ // taskHistory and taskComments are queried from SQLite on demand — no in-memory cache
147
+ initialized = false;
148
+ recurringInitialized = false;
149
+ recurringTicker;
150
+ taskStateAdapter = createTaskStateAdapterFromEnv();
151
+ isCanonicalArtifactPath(path) {
152
+ const normalized = path.trim();
153
+ if (normalized.length === 0)
154
+ return false;
155
+ if (normalized.startsWith('/') || normalized.startsWith('~'))
156
+ return false;
157
+ if (normalized.includes('\\'))
158
+ return false;
159
+ if (normalized.includes('..'))
160
+ return false;
161
+ return normalized.startsWith('process/');
162
+ }
163
+ validateLifecycleGates(task) {
164
+ if (task.status === 'todo')
165
+ return;
166
+ const hasReviewer = Boolean(task.reviewer && task.reviewer.trim().length > 0);
167
+ const hasDoneCriteria = Boolean(task.done_criteria && task.done_criteria.length > 0);
168
+ const eta = task.metadata?.eta;
169
+ const hasEta = typeof eta === 'string' && eta.trim().length > 0;
170
+ const artifactPath = task.metadata?.artifact_path;
171
+ const hasArtifactPath = typeof artifactPath === 'string' && artifactPath.trim().length > 0;
172
+ // Duplicate-closure contract (server-side): never allow a task to be closed as a
173
+ // "duplicate" without canonical refs (duplicate_of + canonical_pr + canonical_commit).
174
+ // Auto-close writers can bypass interactive precheck flows, so this belongs here.
175
+ if (task.status === 'done') {
176
+ assertDuplicateClosureHasCanonicalRefs(task.metadata);
177
+ }
178
+ if (!hasDoneCriteria) {
179
+ throw new Error('Lifecycle gate: done_criteria is required before starting task work');
180
+ }
181
+ if (!hasReviewer) {
182
+ throw new Error('Lifecycle gate: reviewer is required before starting task work');
183
+ }
184
+ if (task.status === 'doing' && !hasEta) {
185
+ throw new Error('Status contract: doing requires metadata.eta');
186
+ }
187
+ if (task.status === 'validating' && !hasArtifactPath) {
188
+ throw new Error('Status contract: validating requires metadata.artifact_path');
189
+ }
190
+ if (task.status === 'validating' && hasArtifactPath && !this.isCanonicalArtifactPath(artifactPath)) {
191
+ throw new Error('Status contract: validating requires metadata.artifact_path under process/ (repo-relative, workspace-agnostic)');
192
+ }
193
+ }
194
+ constructor() {
195
+ this.loadTasks()
196
+ .then(() => this.importLegacyHistory())
197
+ .then(() => this.importLegacyComments())
198
+ .then(() => this.loadRecurringTasks())
199
+ .then(() => this.materializeDueRecurringTasks())
200
+ .catch(err => {
201
+ console.error('[Tasks] Failed to load tasks:', err);
202
+ });
203
+ this.recurringTicker = setInterval(() => {
204
+ this.materializeDueRecurringTasks().catch(err => {
205
+ console.error('[Tasks] Recurring materialization failed:', err);
206
+ });
207
+ }, 60_000);
208
+ this.recurringTicker.unref();
209
+ }
210
+ async loadTasks() {
211
+ try {
212
+ // Ensure data directory exists
213
+ await fs.mkdir(DATA_DIR, { recursive: true });
214
+ const db = getDb();
215
+ // Import JSONL → SQLite if needed (one-time migration)
216
+ importJsonlIfNeeded(db, TASKS_FILE, 'tasks', importTasks);
217
+ // Also check legacy location for migration
218
+ importJsonlIfNeeded(db, LEGACY_TASKS_FILE, 'tasks', importTasks);
219
+ // All tasks queried from SQLite on demand — no in-memory Map
220
+ const count = taskCount();
221
+ console.log(`[Tasks] SQLite has ${count} tasks (all queries go to DB)`);
222
+ // Cloud hydration if empty
223
+ if (count === 0 && this.taskStateAdapter) {
224
+ try {
225
+ const remoteTasks = await this.taskStateAdapter.pullTasks();
226
+ for (const task of remoteTasks) {
227
+ this.writeTaskToDb(task);
228
+ }
229
+ if (remoteTasks.length > 0) {
230
+ console.log(`[Tasks] Hydrated ${remoteTasks.length} tasks from cloud state`);
231
+ }
232
+ }
233
+ catch (err) {
234
+ console.error('[Tasks] Failed to hydrate tasks from cloud state:', err);
235
+ }
236
+ }
237
+ }
238
+ finally {
239
+ this.initialized = true;
240
+ }
241
+ }
242
+ normalizeRecurringTask(recurring) {
243
+ return {
244
+ ...recurring,
245
+ enabled: typeof recurring.enabled === 'boolean' ? recurring.enabled : true,
246
+ };
247
+ }
248
+ async loadRecurringTasks() {
249
+ try {
250
+ await fs.mkdir(DATA_DIR, { recursive: true });
251
+ const db = getDb();
252
+ // Import JSONL → SQLite if needed
253
+ importJsonlIfNeeded(db, RECURRING_TASKS_FILE, 'recurring_tasks', importRecurringTasks);
254
+ // Load from SQLite into in-memory Map
255
+ const rows = db.prepare('SELECT * FROM recurring_tasks').all();
256
+ for (const row of rows) {
257
+ const recurring = {
258
+ id: row.id,
259
+ title: row.title,
260
+ description: row.description ?? undefined,
261
+ assignee: row.assignee ?? undefined,
262
+ reviewer: row.reviewer ?? undefined,
263
+ done_criteria: safeJsonParse(row.done_criteria),
264
+ createdBy: row.created_by,
265
+ priority: row.priority ?? undefined,
266
+ blocked_by: safeJsonParse(row.blocked_by),
267
+ epic_id: row.epic_id ?? undefined,
268
+ tags: safeJsonParse(row.tags),
269
+ metadata: safeJsonParse(row.metadata),
270
+ schedule: safeJsonParse(row.schedule),
271
+ enabled: Boolean(row.enabled),
272
+ status: row.status ?? undefined,
273
+ lastRunAt: row.last_run_at ?? undefined,
274
+ lastSkipAt: row.last_skip_at ?? undefined,
275
+ lastSkipReason: row.last_skip_reason ?? undefined,
276
+ nextRunAt: row.next_run_at,
277
+ createdAt: row.created_at,
278
+ updatedAt: row.updated_at,
279
+ };
280
+ this.recurringTasks.set(recurring.id, this.normalizeRecurringTask(recurring));
281
+ }
282
+ console.log(`[Tasks] Loaded ${this.recurringTasks.size} recurring task definitions from SQLite`);
283
+ }
284
+ finally {
285
+ this.recurringInitialized = true;
286
+ }
287
+ }
288
+ /** One-time JSONL → SQLite import for task history (no in-memory loading) */
289
+ async importLegacyHistory() {
290
+ try {
291
+ await fs.mkdir(DATA_DIR, { recursive: true });
292
+ const db = getDb();
293
+ importJsonlIfNeeded(db, TASK_HISTORY_FILE, 'task_history', importTaskHistory);
294
+ const countRow = db.prepare('SELECT COUNT(*) as count FROM task_history').get();
295
+ console.log(`[Tasks] SQLite has ${countRow.count} task history events (queried on demand)`);
296
+ }
297
+ catch (err) {
298
+ console.error('[Tasks] Failed to import task history:', err);
299
+ }
300
+ }
301
+ /** One-time JSONL → SQLite import for task comments (no in-memory loading) */
302
+ async importLegacyComments() {
303
+ try {
304
+ await fs.mkdir(DATA_DIR, { recursive: true });
305
+ const db = getDb();
306
+ importJsonlIfNeeded(db, TASK_COMMENTS_FILE, 'task_comments', importTaskComments);
307
+ const countRow = db.prepare('SELECT COUNT(*) as count FROM task_comments').get();
308
+ console.log(`[Tasks] SQLite has ${countRow.count} task comments (queried on demand)`);
309
+ }
310
+ catch (err) {
311
+ console.error('[Tasks] Failed to import task comments:', err);
312
+ }
313
+ }
314
+ async appendTaskHistory(event) {
315
+ try {
316
+ const db = getDb();
317
+ // Write to SQLite (primary)
318
+ const insert = db.prepare(`
319
+ INSERT INTO task_history (id, task_id, type, actor, timestamp, data)
320
+ VALUES (?, ?, ?, ?, ?, ?)
321
+ `);
322
+ insert.run(event.id, event.taskId, event.type, event.actor, event.timestamp, safeJsonStringify(event.data));
323
+ // Append to JSONL (audit log)
324
+ await fs.mkdir(DATA_DIR, { recursive: true });
325
+ await fs.appendFile(TASK_HISTORY_FILE, `${JSON.stringify(event)}\n`, 'utf-8');
326
+ }
327
+ catch (err) {
328
+ console.error('[Tasks] Failed to append task history:', err);
329
+ }
330
+ }
331
+ async recordTaskHistoryEvent(taskId, type, actor, data) {
332
+ const event = {
333
+ id: `thevt-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
334
+ taskId,
335
+ type,
336
+ actor,
337
+ timestamp: Date.now(),
338
+ data,
339
+ };
340
+ await this.appendTaskHistory(event);
341
+ }
342
+ async appendTaskComment(comment) {
343
+ try {
344
+ const db = getDb();
345
+ // Write to SQLite (primary)
346
+ const insert = db.prepare(`
347
+ INSERT INTO task_comments (
348
+ id, task_id, author, content, timestamp,
349
+ category, suppressed, suppressed_reason, suppressed_rule
350
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
351
+ `);
352
+ insert.run(comment.id, comment.taskId, comment.author, comment.content, comment.timestamp, comment.category ?? null, comment.suppressed ? 1 : 0, comment.suppressedReason ?? null, comment.suppressedRule ?? null);
353
+ // Update comment count + updated_at for the task.
354
+ // Comments are material activity and should advance updated_at to avoid autonomy/heartbeat false positives.
355
+ const updateTask = db.prepare(`
356
+ UPDATE tasks
357
+ SET
358
+ comment_count = (SELECT COUNT(*) FROM task_comments WHERE task_id = ?),
359
+ updated_at = ?
360
+ WHERE id = ?
361
+ `);
362
+ updateTask.run(comment.taskId, comment.timestamp, comment.taskId);
363
+ // Touch task updated_at so comment activity counts as task activity (autonomy, SLA, sorting)
364
+ const touchUpdatedAt = db.prepare(`
365
+ UPDATE tasks
366
+ SET updated_at = MAX(updated_at, ?)
367
+ WHERE id = ?
368
+ `);
369
+ touchUpdatedAt.run(comment.timestamp, comment.taskId);
370
+ // Append to JSONL (audit log)
371
+ await fs.mkdir(DATA_DIR, { recursive: true });
372
+ await fs.appendFile(TASK_COMMENTS_FILE, `${JSON.stringify(comment)}\n`, 'utf-8');
373
+ }
374
+ catch (err) {
375
+ console.error('[Tasks] Failed to append task comment:', err);
376
+ }
377
+ }
378
+ async persistRecurringTasks() {
379
+ try {
380
+ const db = getDb();
381
+ const upsert = db.prepare(`
382
+ INSERT OR REPLACE INTO recurring_tasks (
383
+ id, title, description, assignee, reviewer, done_criteria, created_by,
384
+ priority, blocked_by, epic_id, tags, metadata, schedule, enabled,
385
+ status, last_run_at, last_skip_at, last_skip_reason, next_run_at,
386
+ created_at, updated_at
387
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
388
+ `);
389
+ const upsertAll = db.transaction(() => {
390
+ for (const rt of this.recurringTasks.values()) {
391
+ upsert.run(rt.id, rt.title, rt.description ?? null, rt.assignee ?? null, rt.reviewer ?? null, safeJsonStringify(rt.done_criteria), rt.createdBy, rt.priority ?? null, safeJsonStringify(rt.blocked_by), rt.epic_id ?? null, safeJsonStringify(rt.tags), safeJsonStringify(rt.metadata), safeJsonStringify(rt.schedule), rt.enabled ? 1 : 0, rt.status ?? 'todo', rt.lastRunAt ?? null, rt.lastSkipAt ?? null, rt.lastSkipReason ?? null, rt.nextRunAt, rt.createdAt, rt.updatedAt);
392
+ }
393
+ });
394
+ upsertAll();
395
+ }
396
+ catch (err) {
397
+ console.error('[Tasks] Failed to persist recurring tasks to SQLite:', err);
398
+ }
399
+ // JSONL audit log
400
+ try {
401
+ await fs.mkdir(DATA_DIR, { recursive: true });
402
+ const lines = Array.from(this.recurringTasks.values()).map(task => JSON.stringify({
403
+ ...task,
404
+ enabled: Boolean(task.enabled),
405
+ }));
406
+ await fs.writeFile(RECURRING_TASKS_FILE, lines.join('\n') + '\n', 'utf-8');
407
+ }
408
+ catch (err) {
409
+ console.error('[Tasks] Failed to write recurring tasks JSONL audit log:', err);
410
+ }
411
+ }
412
+ computeNextRunAt(schedule, fromMs, createdAt) {
413
+ if (schedule.kind === 'interval') {
414
+ const everyMs = Math.max(60_000, schedule.everyMs);
415
+ const anchor = schedule.anchorAt ?? createdAt;
416
+ if (fromMs < anchor)
417
+ return anchor;
418
+ const periods = Math.floor((fromMs - anchor) / everyMs) + 1;
419
+ return anchor + periods * everyMs;
420
+ }
421
+ const hour = schedule.hour ?? 9;
422
+ const minute = schedule.minute ?? 0;
423
+ const candidate = new Date(fromMs);
424
+ candidate.setSeconds(0, 0);
425
+ candidate.setHours(hour, minute, 0, 0);
426
+ let dayDelta = schedule.dayOfWeek - candidate.getDay();
427
+ if (dayDelta < 0 || (dayDelta === 0 && candidate.getTime() <= fromMs)) {
428
+ dayDelta += 7;
429
+ }
430
+ candidate.setDate(candidate.getDate() + dayDelta);
431
+ if (candidate.getTime() <= fromMs) {
432
+ candidate.setDate(candidate.getDate() + 7);
433
+ }
434
+ return candidate.getTime();
435
+ }
436
+ hasMaterializedRun(recurringId, scheduledFor) {
437
+ const db = getDb();
438
+ // Use JSON extract to check recurring metadata without loading all tasks
439
+ const row = db.prepare(`
440
+ SELECT 1 FROM tasks
441
+ WHERE json_extract(metadata, '$.recurring.id') = ?
442
+ AND json_extract(metadata, '$.recurring.scheduledFor') = ?
443
+ LIMIT 1
444
+ `).get(recurringId, scheduledFor);
445
+ return !!row;
446
+ }
447
+ getLatestRecurringInstance(recurringId) {
448
+ const db = getDb();
449
+ const row = db.prepare(`
450
+ SELECT * FROM tasks
451
+ WHERE json_extract(metadata, '$.recurring.id') = ?
452
+ ORDER BY created_at DESC
453
+ LIMIT 1
454
+ `).get(recurringId);
455
+ return row ? rowToTask(row) : undefined;
456
+ }
457
+ async materializeDueRecurringTasks(now = Date.now(), options) {
458
+ let created = 0;
459
+ let skipped = 0;
460
+ let recurringChanged = false;
461
+ for (const recurring of this.recurringTasks.values()) {
462
+ if (!recurring.enabled)
463
+ continue;
464
+ let safetyCounter = 0;
465
+ while (recurring.nextRunAt <= now && safetyCounter < 16) {
466
+ const scheduledFor = recurring.nextRunAt;
467
+ const previousInstance = this.getLatestRecurringInstance(recurring.id);
468
+ const shouldSkipForOpenPredecessor = !options?.force &&
469
+ previousInstance !== undefined &&
470
+ previousInstance.status !== 'done';
471
+ if (shouldSkipForOpenPredecessor) {
472
+ const reason = `skip: previous recurring instance still open (${previousInstance.id}, status=${previousInstance.status})`;
473
+ recurring.lastSkipAt = Date.now();
474
+ recurring.lastSkipReason = reason;
475
+ console.log(`[Tasks] Recurring materialization skipped for ${recurring.id}: ${reason}`);
476
+ skipped += 1;
477
+ }
478
+ else if (!this.hasMaterializedRun(recurring.id, scheduledFor)) {
479
+ await this.createTask({
480
+ title: recurring.title,
481
+ description: recurring.description,
482
+ status: recurring.status ?? 'todo',
483
+ assignee: recurring.assignee,
484
+ reviewer: recurring.reviewer,
485
+ done_criteria: recurring.done_criteria,
486
+ createdBy: recurring.createdBy,
487
+ priority: recurring.priority,
488
+ blocked_by: recurring.blocked_by,
489
+ epic_id: recurring.epic_id,
490
+ tags: recurring.tags,
491
+ metadata: {
492
+ ...(recurring.metadata || {}),
493
+ recurring: {
494
+ id: recurring.id,
495
+ scheduledFor,
496
+ },
497
+ },
498
+ });
499
+ created += 1;
500
+ }
501
+ recurring.lastRunAt = scheduledFor;
502
+ recurring.nextRunAt = this.computeNextRunAt(recurring.schedule, scheduledFor, recurring.createdAt);
503
+ recurring.updatedAt = Date.now();
504
+ recurringChanged = true;
505
+ safetyCounter += 1;
506
+ }
507
+ }
508
+ if (recurringChanged) {
509
+ await this.persistRecurringTasks();
510
+ }
511
+ return { created, skipped };
512
+ }
513
+ /** Write a single task to SQLite + JSONL audit */
514
+ writeTaskToDb(task) {
515
+ try {
516
+ const db = getDb();
517
+ const commentCount = this.getTaskCommentCount(task.id);
518
+ db.prepare(`
519
+ INSERT OR REPLACE INTO tasks (
520
+ id, title, description, status, assignee, reviewer, done_criteria,
521
+ created_by, created_at, updated_at, priority, blocked_by, epic_id,
522
+ tags, metadata, team_id, comment_count
523
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
524
+ `).run(task.id, task.title, task.description ?? null, task.status, task.assignee ?? null, task.reviewer ?? null, safeJsonStringify(task.done_criteria), task.createdBy, task.createdAt, task.updatedAt, task.priority ?? null, safeJsonStringify(task.blocked_by), task.epic_id ?? null, safeJsonStringify(task.tags), safeJsonStringify(task.metadata), task.teamId ?? null, commentCount);
525
+ }
526
+ catch (err) {
527
+ console.error(`[Tasks] Failed to write task ${task.id} to SQLite:`, err);
528
+ }
529
+ // JSONL audit (best-effort, async)
530
+ fs.appendFile(TASKS_FILE, JSON.stringify(task) + '\n', 'utf-8').catch(() => { });
531
+ }
532
+ /** Legacy bulk persist — now just writes all tasks from DB to JSONL */
533
+ async persistTasks() {
534
+ try {
535
+ await fs.mkdir(DATA_DIR, { recursive: true });
536
+ const tasks = queryAllTasks();
537
+ const lines = tasks.map(task => JSON.stringify(task));
538
+ await fs.writeFile(TASKS_FILE, lines.join('\n') + '\n', 'utf-8');
539
+ }
540
+ catch (err) {
541
+ console.error('[Tasks] Failed to write JSONL audit log:', err);
542
+ }
543
+ }
544
+ async syncTaskToCloud(task) {
545
+ if (!this.taskStateAdapter)
546
+ return;
547
+ try {
548
+ await this.taskStateAdapter.upsertTask(task);
549
+ }
550
+ catch (err) {
551
+ console.error('[Tasks] Cloud sync upsert failed, continuing with local JSON fallback:', err);
552
+ }
553
+ }
554
+ async syncTaskDeleteToCloud(taskId) {
555
+ if (!this.taskStateAdapter)
556
+ return;
557
+ try {
558
+ await this.taskStateAdapter.deleteTask(taskId);
559
+ }
560
+ catch (err) {
561
+ console.error('[Tasks] Cloud sync delete failed, continuing with local JSON fallback:', err);
562
+ }
563
+ }
564
+ async createTask(data) {
565
+ let metadata = data.metadata ? { ...data.metadata } : undefined;
566
+ const metadataTeamId = typeof metadata?.teamId === 'string' && metadata.teamId.trim().length > 0
567
+ ? metadata.teamId.trim()
568
+ : undefined;
569
+ const explicitTeamId = typeof data.teamId === 'string' && data.teamId.trim().length > 0
570
+ ? data.teamId.trim()
571
+ : undefined;
572
+ const normalizedTeamId = explicitTeamId ?? metadataTeamId;
573
+ if (normalizedTeamId) {
574
+ metadata = {
575
+ ...(metadata || {}),
576
+ teamId: normalizedTeamId,
577
+ };
578
+ }
579
+ const normalizedData = {
580
+ ...data,
581
+ ...(normalizedTeamId ? { teamId: normalizedTeamId } : {}),
582
+ metadata,
583
+ };
584
+ this.validateLifecycleGates(normalizedData);
585
+ // Validate blocked_by references
586
+ if (normalizedData.blocked_by && normalizedData.blocked_by.length > 0) {
587
+ for (const blockerId of normalizedData.blocked_by) {
588
+ if (!taskExists(blockerId)) {
589
+ throw new Error(`Invalid blocked_by reference: task ${blockerId} does not exist`);
590
+ }
591
+ }
592
+ }
593
+ const task = {
594
+ ...normalizedData,
595
+ id: `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
596
+ createdAt: Date.now(),
597
+ updatedAt: Date.now(),
598
+ };
599
+ this.writeTaskToDb(task);
600
+ await this.syncTaskToCloud(task);
601
+ await this.recordTaskHistoryEvent(task.id, 'created', task.createdBy, {
602
+ status: task.status,
603
+ assignee: task.assignee ?? null,
604
+ });
605
+ if (task.assignee) {
606
+ await this.recordTaskHistoryEvent(task.id, 'assigned', task.createdBy, {
607
+ from: null,
608
+ to: task.assignee,
609
+ });
610
+ }
611
+ this.notifySubscribers(task, 'created');
612
+ // Emit events to event bus
613
+ eventBus.emitTaskCreated(task);
614
+ if (task.assignee) {
615
+ eventBus.emitTaskAssigned(task);
616
+ }
617
+ return task;
618
+ }
619
+ async createRecurringTask(data) {
620
+ if (data.blocked_by && data.blocked_by.length > 0) {
621
+ for (const blockerId of data.blocked_by) {
622
+ if (!taskExists(blockerId)) {
623
+ throw new Error(`Invalid blocked_by reference: task ${blockerId} does not exist`);
624
+ }
625
+ }
626
+ }
627
+ const now = Date.now();
628
+ const recurring = {
629
+ id: `rtask-${now}-${Math.random().toString(36).substr(2, 9)}`,
630
+ title: data.title,
631
+ description: data.description,
632
+ assignee: data.assignee,
633
+ reviewer: data.reviewer,
634
+ done_criteria: data.done_criteria,
635
+ createdBy: data.createdBy,
636
+ priority: data.priority,
637
+ blocked_by: data.blocked_by,
638
+ epic_id: data.epic_id,
639
+ tags: data.tags,
640
+ metadata: data.metadata,
641
+ schedule: data.schedule,
642
+ enabled: data.enabled ?? true,
643
+ status: data.status ?? 'todo',
644
+ nextRunAt: this.computeNextRunAt(data.schedule, now, now),
645
+ createdAt: now,
646
+ updatedAt: now,
647
+ };
648
+ this.recurringTasks.set(recurring.id, recurring);
649
+ await this.persistRecurringTasks();
650
+ await this.materializeDueRecurringTasks();
651
+ return recurring;
652
+ }
653
+ listRecurringTasks(options) {
654
+ let tasks = Array.from(this.recurringTasks.values());
655
+ if (typeof options?.enabled === 'boolean') {
656
+ tasks = tasks.filter(task => task.enabled === options.enabled);
657
+ }
658
+ return tasks.sort((a, b) => a.nextRunAt - b.nextRunAt);
659
+ }
660
+ async updateRecurringTask(id, updates) {
661
+ const recurring = this.recurringTasks.get(id);
662
+ if (!recurring)
663
+ return undefined;
664
+ const next = {
665
+ ...recurring,
666
+ ...updates,
667
+ updatedAt: Date.now(),
668
+ };
669
+ if (typeof updates.enabled === 'boolean') {
670
+ next.enabled = updates.enabled;
671
+ }
672
+ if (updates.schedule) {
673
+ next.nextRunAt = this.computeNextRunAt(updates.schedule, Date.now(), recurring.createdAt);
674
+ }
675
+ this.recurringTasks.set(id, next);
676
+ await this.persistRecurringTasks();
677
+ return next;
678
+ }
679
+ async deleteRecurringTask(id) {
680
+ const existed = this.recurringTasks.delete(id);
681
+ if (!existed)
682
+ return false;
683
+ await this.persistRecurringTasks();
684
+ return true;
685
+ }
686
+ getTask(id) {
687
+ return queryTask(id);
688
+ }
689
+ resolveTaskId(inputId) {
690
+ const raw = String(inputId || '').trim();
691
+ if (!raw) {
692
+ return { matchType: 'not_found', suggestions: [] };
693
+ }
694
+ const exact = queryTask(raw);
695
+ if (exact) {
696
+ return { task: exact, resolvedId: raw, matchType: 'exact', suggestions: [] };
697
+ }
698
+ const lowerRaw = raw.toLowerCase();
699
+ const db = getDb();
700
+ const allIds = db.prepare('SELECT id FROM tasks').all().map(r => r.id);
701
+ const prefixMatches = allIds.filter(id => id.toLowerCase().startsWith(lowerRaw));
702
+ if (prefixMatches.length === 1) {
703
+ const resolvedId = prefixMatches[0];
704
+ return {
705
+ task: queryTask(resolvedId),
706
+ resolvedId,
707
+ matchType: 'prefix',
708
+ suggestions: [],
709
+ };
710
+ }
711
+ if (prefixMatches.length > 1) {
712
+ return {
713
+ matchType: 'ambiguous',
714
+ suggestions: prefixMatches.slice(0, 8),
715
+ };
716
+ }
717
+ const containsMatches = allIds.filter((id) => id.toLowerCase().includes(lowerRaw)).slice(0, 8);
718
+ return {
719
+ matchType: 'not_found',
720
+ suggestions: containsMatches,
721
+ };
722
+ }
723
+ /**
724
+ * Lightweight metadata-only patch that bypasses lifecycle gates.
725
+ * Used by internal subsystems (sweeper, auto-close) to persist bookkeeping
726
+ * fields without triggering full validation or history events.
727
+ */
728
+ patchTaskMetadata(id, metadataUpdates) {
729
+ const task = queryTask(id);
730
+ if (!task)
731
+ return false;
732
+ const merged = { ...(task.metadata || {}), ...metadataUpdates };
733
+ const updated = { ...task, metadata: merged, updatedAt: Date.now() };
734
+ this.writeTaskToDb(updated);
735
+ return true;
736
+ }
737
+ getTaskHistory(id) {
738
+ const db = getDb();
739
+ const rows = db.prepare('SELECT * FROM task_history WHERE task_id = ? ORDER BY timestamp ASC').all(id);
740
+ return rows.map(row => ({
741
+ id: row.id,
742
+ taskId: row.task_id,
743
+ type: row.type,
744
+ actor: row.actor,
745
+ timestamp: row.timestamp,
746
+ data: safeJsonParse(row.data),
747
+ }));
748
+ }
749
+ getTaskComments(id, options) {
750
+ const db = getDb();
751
+ const includeSuppressed = Boolean(options?.includeSuppressed);
752
+ const sql = includeSuppressed
753
+ ? 'SELECT * FROM task_comments WHERE task_id = ? ORDER BY timestamp ASC'
754
+ : 'SELECT * FROM task_comments WHERE task_id = ? AND (suppressed IS NULL OR suppressed = 0) ORDER BY timestamp ASC';
755
+ const rows = db.prepare(sql).all(id);
756
+ return rows.map(row => ({
757
+ id: row.id,
758
+ taskId: row.task_id,
759
+ author: row.author,
760
+ content: row.content,
761
+ timestamp: row.timestamp,
762
+ category: row.category ?? null,
763
+ suppressed: Boolean(row.suppressed),
764
+ suppressedReason: row.suppressed_reason ?? null,
765
+ suppressedRule: row.suppressed_rule ?? null,
766
+ }));
767
+ }
768
+ getTaskCommentCount(id) {
769
+ const db = getDb();
770
+ const row = db.prepare('SELECT COUNT(*) as count FROM task_comments WHERE task_id = ?').get(id);
771
+ return row.count;
772
+ }
773
+ async addTaskComment(taskId, author, content, options) {
774
+ const task = queryTask(taskId);
775
+ if (!task) {
776
+ throw new Error('Task not found');
777
+ }
778
+ const now = Date.now();
779
+ // ── Comms policy enforcement (store always; suppress from default feeds) ──
780
+ const meta = (task.metadata || {});
781
+ const rule = meta?.comms_policy?.rule;
782
+ const extractCategoryFromContent = (raw) => {
783
+ const s = String(raw || '').trim();
784
+ // [restart] ...
785
+ const bracket = s.match(/^\[(restart|rollback_trigger|promote_due_verdict)\]\s*/i);
786
+ if (bracket?.[1])
787
+ return bracket[1].toLowerCase();
788
+ // restart: ...
789
+ const colon = s.match(/^(restart|rollback_trigger|promote_due_verdict)\s*:\s+/i);
790
+ if (colon?.[1])
791
+ return colon[1].toLowerCase();
792
+ // category=restart / category: restart
793
+ const kv = s.match(/^(?:category|cat)\s*[:=]\s*(restart|rollback_trigger|promote_due_verdict)\b/i);
794
+ if (kv?.[1])
795
+ return kv[1].toLowerCase();
796
+ return null;
797
+ };
798
+ let category = (options?.category ? String(options.category).trim() : '') || null;
799
+ if (!category)
800
+ category = extractCategoryFromContent(content);
801
+ let suppressed = false;
802
+ let suppressedReason = null;
803
+ let suppressedRule = null;
804
+ if (rule === 'silent_until_restart_or_promote_due') {
805
+ suppressedRule = rule;
806
+ const allowed = new Set(['restart', 'rollback_trigger', 'promote_due_verdict']);
807
+ const normalized = category ? category.toLowerCase() : null;
808
+ if (!normalized) {
809
+ suppressed = true;
810
+ suppressedReason = 'missing_category';
811
+ }
812
+ else if (!allowed.has(normalized)) {
813
+ suppressed = true;
814
+ suppressedReason = `non_whitelisted_category:${normalized}`;
815
+ }
816
+ category = normalized;
817
+ }
818
+ const comment = {
819
+ id: `tcomment-${now}-${Math.random().toString(36).substr(2, 9)}`,
820
+ taskId,
821
+ author,
822
+ content,
823
+ timestamp: now,
824
+ category,
825
+ suppressed,
826
+ suppressedReason,
827
+ suppressedRule,
828
+ };
829
+ await this.appendTaskComment(comment);
830
+ await this.recordTaskHistoryEvent(taskId, 'commented', author, {
831
+ commentId: comment.id,
832
+ content,
833
+ category,
834
+ suppressed,
835
+ suppressedReason,
836
+ suppressedRule,
837
+ provenance: options?.provenance ?? null,
838
+ });
839
+ return comment;
840
+ }
841
+ /** Tracks last materialization time to debounce recurring task checks */
842
+ lastRecurringMaterializeAt = 0;
843
+ static RECURRING_DEBOUNCE_MS = 60_000; // 1 minute
844
+ listTasks(options) {
845
+ // Debounce recurring task materialization: at most once per minute
846
+ // This was previously called on every listTasks() invocation (~50+ callers),
847
+ // causing significant event loop blocking under load.
848
+ const now = Date.now();
849
+ if (now - this.lastRecurringMaterializeAt >= TaskManager.RECURRING_DEBOUNCE_MS) {
850
+ this.lastRecurringMaterializeAt = now;
851
+ void this.materializeDueRecurringTasks().catch(() => { });
852
+ }
853
+ // Helper: check if a task is blocked by incomplete dependencies
854
+ const isBlocked = (task) => {
855
+ if (!task.blocked_by || task.blocked_by.length === 0)
856
+ return false;
857
+ return task.blocked_by.some(blockerId => {
858
+ const blocker = queryTask(blockerId);
859
+ return blocker && blocker.status !== 'done';
860
+ });
861
+ };
862
+ const db = getDb();
863
+ const conditions = [];
864
+ const params = [];
865
+ if (options?.status) {
866
+ conditions.push('status = ?');
867
+ params.push(options.status);
868
+ }
869
+ const assigneeFilter = options?.assignee || options?.assignedTo;
870
+ const assigneeIn = options?.assigneeIn;
871
+ if (assigneeIn && assigneeIn.length > 0) {
872
+ const normalized = assigneeIn.map(a => String(a || '').trim().toLowerCase()).filter(Boolean);
873
+ if (normalized.length > 0) {
874
+ conditions.push(`LOWER(assignee) IN (${normalized.map(() => '?').join(', ')})`);
875
+ params.push(...normalized);
876
+ }
877
+ }
878
+ else if (assigneeFilter) {
879
+ conditions.push('assignee = ?');
880
+ params.push(assigneeFilter);
881
+ }
882
+ if (options?.createdBy) {
883
+ conditions.push('created_by = ?');
884
+ params.push(options.createdBy);
885
+ }
886
+ if (options?.teamId) {
887
+ conditions.push("(team_id = ? OR json_extract(metadata, '$.teamId') = ?)");
888
+ params.push(options.teamId, options.teamId);
889
+ }
890
+ if (options?.priority) {
891
+ conditions.push('priority = ?');
892
+ params.push(options.priority);
893
+ }
894
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
895
+ const sql = `SELECT * FROM tasks ${where} ORDER BY updated_at DESC`;
896
+ let tasks = db.prepare(sql).all(...params).map(rowToTask);
897
+ // Tag filtering requires JSON parsing (can't easily do in SQL)
898
+ if (options?.tags && options.tags.length > 0) {
899
+ tasks = tasks.filter(t => t.tags && options.tags.some(tag => t.tags.includes(tag)));
900
+ }
901
+ // Filter blocked tasks if requested
902
+ if (options?.includeBlocked === false) {
903
+ tasks = tasks.filter(t => !isBlocked(t));
904
+ }
905
+ // Filter out test-harness-generated tasks by default (shared classifier in test-task-filter.ts)
906
+ if (!options?.includeTest) {
907
+ tasks = tasks.filter(t => !isTestHarnessTask(t));
908
+ }
909
+ return tasks;
910
+ }
911
+ searchTasks(query) {
912
+ const normalized = query.trim().toLowerCase();
913
+ if (!normalized)
914
+ return [];
915
+ const db = getDb();
916
+ const pattern = `%${normalized}%`;
917
+ const rows = db.prepare(`
918
+ SELECT * FROM tasks
919
+ WHERE title LIKE ? COLLATE NOCASE OR description LIKE ? COLLATE NOCASE
920
+ ORDER BY updated_at DESC
921
+ `).all(pattern, pattern);
922
+ return rows.map(rowToTask);
923
+ }
924
+ resolveHistoryActor(task, updates) {
925
+ const metadataActor = updates.metadata?.actor;
926
+ if (typeof metadataActor === 'string' && metadataActor.trim().length > 0) {
927
+ return metadataActor.trim();
928
+ }
929
+ if (typeof updates.assignee === 'string' && updates.assignee.trim().length > 0) {
930
+ return updates.assignee.trim();
931
+ }
932
+ if (task.assignee && task.assignee.trim().length > 0) {
933
+ return task.assignee.trim();
934
+ }
935
+ return task.createdBy;
936
+ }
937
+ parseLaneTransition(updates) {
938
+ const transition = updates.metadata?.transition;
939
+ if (!transition || typeof transition !== 'object' || Array.isArray(transition))
940
+ return undefined;
941
+ return transition;
942
+ }
943
+ applyLaneStateLock(task, updates, actor) {
944
+ const transition = this.parseLaneTransition(updates);
945
+ const nextStatus = updates.status ?? task.status;
946
+ const nextAssignee = updates.assignee ?? task.assignee;
947
+ const statusChanged = nextStatus !== task.status;
948
+ const assigneeChanged = updates.assignee !== undefined && updates.assignee !== task.assignee;
949
+ const requireTransition = (expectedType, requiredFields, contextLabel) => {
950
+ if (!transition) {
951
+ throw new Error(`Lane-state lock: ${contextLabel} requires metadata.transition`);
952
+ }
953
+ const type = transition.type;
954
+ if (type !== expectedType) {
955
+ throw new Error(`Lane-state lock: ${contextLabel} requires metadata.transition.type="${expectedType}"`);
956
+ }
957
+ for (const field of requiredFields) {
958
+ const value = transition[field];
959
+ if (typeof value !== 'string' || value.trim().length === 0) {
960
+ throw new Error(`Lane-state lock: ${contextLabel} requires metadata.transition.${field}`);
961
+ }
962
+ }
963
+ return transition;
964
+ };
965
+ let transitionEvent;
966
+ if (task.status === 'doing' && nextStatus === 'blocked') {
967
+ const parsed = requireTransition('pause', ['reason'], 'doing->blocked transition');
968
+ transitionEvent = {
969
+ type: 'pause',
970
+ reason: parsed.reason,
971
+ };
972
+ }
973
+ else if (task.status === 'blocked' && nextStatus === 'doing') {
974
+ const parsed = requireTransition('resume', ['reason'], 'blocked->doing transition');
975
+ transitionEvent = {
976
+ type: 'resume',
977
+ reason: parsed.reason,
978
+ };
979
+ }
980
+ else if (task.status === 'doing' && nextStatus === 'doing' && assigneeChanged) {
981
+ const parsed = requireTransition('handoff', ['handoff_to', 'reason'], 'doing handoff transition');
982
+ if (typeof nextAssignee !== 'string' || nextAssignee.trim().length === 0) {
983
+ throw new Error('Lane-state lock: handoff requires assignee to be set');
984
+ }
985
+ if (String(parsed.handoff_to).trim() !== nextAssignee.trim()) {
986
+ throw new Error('Lane-state lock: metadata.transition.handoff_to must match new assignee');
987
+ }
988
+ transitionEvent = {
989
+ type: 'handoff',
990
+ reason: parsed.reason,
991
+ handoff_to: parsed.handoff_to,
992
+ };
993
+ }
994
+ if (!transitionEvent) {
995
+ return {};
996
+ }
997
+ const timestamp = Date.now();
998
+ const metadata = {
999
+ ...(updates.metadata || {}),
1000
+ lane_state: nextStatus === 'blocked' ? 'paused' : 'active',
1001
+ last_transition: {
1002
+ type: transitionEvent.type,
1003
+ actor,
1004
+ timestamp,
1005
+ from_status: task.status,
1006
+ to_status: nextStatus,
1007
+ from_assignee: task.assignee ?? null,
1008
+ to_assignee: nextAssignee ?? null,
1009
+ reason: transitionEvent.reason ?? null,
1010
+ handoff_to: transitionEvent.handoff_to ?? null,
1011
+ },
1012
+ };
1013
+ updates.metadata = metadata;
1014
+ return {
1015
+ transitionEvent: {
1016
+ ...transitionEvent,
1017
+ actor,
1018
+ timestamp,
1019
+ from_status: task.status,
1020
+ to_status: nextStatus,
1021
+ from_assignee: task.assignee ?? null,
1022
+ to_assignee: nextAssignee ?? null,
1023
+ },
1024
+ };
1025
+ }
1026
+ async updateTask(id, updates) {
1027
+ const task = queryTask(id);
1028
+ if (!task)
1029
+ return undefined;
1030
+ // Validate blocked_by references if being updated
1031
+ if (updates.blocked_by && updates.blocked_by.length > 0) {
1032
+ for (const blockerId of updates.blocked_by) {
1033
+ if (blockerId === id) {
1034
+ throw new Error('Task cannot be blocked by itself');
1035
+ }
1036
+ if (!taskExists(blockerId)) {
1037
+ throw new Error(`Invalid blocked_by reference: task ${blockerId} does not exist`);
1038
+ }
1039
+ }
1040
+ // Check for circular dependencies
1041
+ // We need to verify that none of the new blockers (or their dependencies) point back to this task
1042
+ const checkCircular = (taskId, visited = new Set()) => {
1043
+ // If we've reached the original task, there's a cycle
1044
+ if (taskId === id)
1045
+ return true;
1046
+ // If we've already visited this node in this path, no cycle (but avoid infinite loops)
1047
+ if (visited.has(taskId))
1048
+ return false;
1049
+ visited.add(taskId);
1050
+ // Get the task and check its dependencies
1051
+ const t = queryTask(taskId);
1052
+ if (!t || !t.blocked_by)
1053
+ return false;
1054
+ // Recursively check each dependency
1055
+ for (const bid of t.blocked_by) {
1056
+ if (checkCircular(bid, new Set(visited)))
1057
+ return true;
1058
+ }
1059
+ return false;
1060
+ };
1061
+ for (const blockerId of updates.blocked_by) {
1062
+ if (checkCircular(blockerId)) {
1063
+ throw new Error('Circular dependency detected in blocked_by chain');
1064
+ }
1065
+ }
1066
+ }
1067
+ const actor = this.resolveHistoryActor(task, updates);
1068
+ const { transitionEvent } = this.applyLaneStateLock(task, updates, actor);
1069
+ const updated = {
1070
+ ...task,
1071
+ ...updates,
1072
+ updatedAt: Date.now(),
1073
+ };
1074
+ this.validateLifecycleGates(updated);
1075
+ this.writeTaskToDb(updated);
1076
+ await this.syncTaskToCloud(updated);
1077
+ if (updates.assignee !== undefined && updates.assignee !== task.assignee) {
1078
+ await this.recordTaskHistoryEvent(id, 'assigned', actor, {
1079
+ from: task.assignee ?? null,
1080
+ to: updates.assignee ?? null,
1081
+ });
1082
+ }
1083
+ if (updates.status !== undefined && updates.status !== task.status) {
1084
+ await this.recordTaskHistoryEvent(id, 'status_changed', actor, {
1085
+ from: task.status,
1086
+ to: updates.status,
1087
+ });
1088
+ }
1089
+ if (transitionEvent) {
1090
+ await this.recordTaskHistoryEvent(id, 'lane_transition', actor, transitionEvent);
1091
+ }
1092
+ this.notifySubscribers(updated, 'updated');
1093
+ // Emit events to event bus
1094
+ eventBus.emitTaskUpdated(updated, updates);
1095
+ // If assignee changed, emit task_assigned
1096
+ if (updates.assignee && updates.assignee !== task.assignee) {
1097
+ eventBus.emitTaskAssigned(updated);
1098
+ }
1099
+ // If task completed, check for unblocked tasks
1100
+ if (updates.status === 'done' && task.status !== 'done') {
1101
+ this.checkUnblockedTasks(id);
1102
+ }
1103
+ return updated;
1104
+ }
1105
+ async deleteTask(id) {
1106
+ const task = queryTask(id);
1107
+ if (!task)
1108
+ return false;
1109
+ // Delete from SQLite
1110
+ try {
1111
+ const db = getDb();
1112
+ db.prepare('DELETE FROM tasks WHERE id = ?').run(id);
1113
+ db.prepare('DELETE FROM task_comments WHERE task_id = ?').run(id);
1114
+ db.prepare('DELETE FROM task_history WHERE task_id = ?').run(id);
1115
+ }
1116
+ catch (err) {
1117
+ console.error(`[Tasks] SQLite delete failed for ${id}:`, err);
1118
+ }
1119
+ await this.syncTaskDeleteToCloud(id);
1120
+ this.notifySubscribers(task, 'deleted');
1121
+ return true;
1122
+ }
1123
+ subscribe(callback) {
1124
+ this.subscribers.add(callback);
1125
+ return () => this.subscribers.delete(callback);
1126
+ }
1127
+ notifySubscribers(task, action) {
1128
+ this.subscribers.forEach(callback => {
1129
+ try {
1130
+ callback(task, action);
1131
+ }
1132
+ catch (err) {
1133
+ console.error('[Tasks] Subscriber error:', err);
1134
+ }
1135
+ });
1136
+ }
1137
+ checkUnblockedTasks(completedTaskId) {
1138
+ // Find all tasks that were blocked by this completed task
1139
+ const unblockedTasks = [];
1140
+ const db = getDb();
1141
+ const blockedRows = db.prepare(`SELECT * FROM tasks WHERE blocked_by LIKE ?`).all(`%${completedTaskId}%`);
1142
+ for (const task of blockedRows.map(rowToTask)) {
1143
+ if (task.blocked_by && task.blocked_by.includes(completedTaskId)) {
1144
+ const stillBlocked = task.blocked_by.some(blockerId => {
1145
+ const blocker = queryTask(blockerId);
1146
+ return blocker && blocker.status !== 'done';
1147
+ });
1148
+ if (!stillBlocked) {
1149
+ unblockedTasks.push(task);
1150
+ }
1151
+ }
1152
+ }
1153
+ if (unblockedTasks.length > 0) {
1154
+ console.log(`[Tasks] Task ${completedTaskId} completion unblocked ${unblockedTasks.length} task(s):`, unblockedTasks.map(t => t.id).join(', '));
1155
+ // Emit event for each unblocked task
1156
+ for (const task of unblockedTasks) {
1157
+ eventBus.emit({
1158
+ id: `evt-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
1159
+ type: 'task_updated',
1160
+ timestamp: Date.now(),
1161
+ data: {
1162
+ ...task,
1163
+ unblocked: true,
1164
+ unblockedBy: completedTaskId
1165
+ }
1166
+ });
1167
+ }
1168
+ }
1169
+ }
1170
+ getNextTask(agent, opts) {
1171
+ void this.materializeDueRecurringTasks().catch(() => { });
1172
+ // Filter out test-harness-generated tasks (shared classifier in test-task-filter.ts)
1173
+ const shouldExcludeTest = !opts?.includeTest;
1174
+ const filterTestTask = (task) => shouldExcludeTest ? !isTestHarnessTask(task) : true;
1175
+ // Priority order: P0 > P1 > P2 > P3
1176
+ const priorityOrder = {
1177
+ 'P0': 0,
1178
+ 'P1': 1,
1179
+ 'P2': 2,
1180
+ 'P3': 3,
1181
+ };
1182
+ // Helper: check if a task is blocked by incomplete dependencies
1183
+ const isBlocked = (task) => {
1184
+ if (!task.blocked_by || task.blocked_by.length === 0)
1185
+ return false;
1186
+ return task.blocked_by.some(blockerId => {
1187
+ const blocker = queryTask(blockerId);
1188
+ return blocker && blocker.status !== 'done';
1189
+ });
1190
+ };
1191
+ const sortByPriority = (a, b) => {
1192
+ const aPriority = priorityOrder[a.priority || 'P3'] ?? 999;
1193
+ const bPriority = priorityOrder[b.priority || 'P3'] ?? 999;
1194
+ if (aPriority !== bPriority)
1195
+ return aPriority - bPriority;
1196
+ return a.createdAt - b.createdAt;
1197
+ };
1198
+ // If agent specified, first return their highest-priority doing task
1199
+ // This ensures agents resume in-progress work before picking up new tasks
1200
+ const db = getDb();
1201
+ if (agent) {
1202
+ const agentNames = getAgentAliases(agent);
1203
+ if (agentNames.length > 0) {
1204
+ const inClause = agentNames.map(() => '?').join(', ');
1205
+ const doingRows = db.prepare(`SELECT * FROM tasks WHERE status = ? AND LOWER(assignee) IN (${inClause})`).all('doing', ...agentNames);
1206
+ const doingTasks = doingRows.map(rowToTask).filter(t => !isBlocked(t) && filterTestTask(t)).sort(sortByPriority);
1207
+ if (doingTasks.length > 0) {
1208
+ return doingTasks[0];
1209
+ }
1210
+ }
1211
+ }
1212
+ // Then check todo tasks: unassigned or assigned to this agent
1213
+ const todoUnassignedRows = db.prepare('SELECT * FROM tasks WHERE status = ? AND assignee IS NULL').all('todo');
1214
+ let tasks = todoUnassignedRows.map(rowToTask).filter(t => !isBlocked(t) && filterTestTask(t));
1215
+ if (agent) {
1216
+ const agentNames = getAgentAliases(agent);
1217
+ if (agentNames.length > 0) {
1218
+ const inClause = agentNames.map(() => '?').join(', ');
1219
+ const agentTodoRows = db.prepare(`SELECT * FROM tasks WHERE status = ? AND LOWER(assignee) IN (${inClause})`).all('todo', ...agentNames);
1220
+ tasks = [...tasks, ...agentTodoRows.map(rowToTask).filter(t => !isBlocked(t) && filterTestTask(t))];
1221
+ }
1222
+ }
1223
+ if (tasks.length === 0)
1224
+ return undefined;
1225
+ tasks.sort(sortByPriority);
1226
+ return tasks[0];
1227
+ }
1228
+ getLifecycleInstrumentation() {
1229
+ const db = getDb();
1230
+ const activeRows = db.prepare(`SELECT * FROM tasks WHERE status NOT IN ('todo', 'done')`).all();
1231
+ const active = activeRows.map(rowToTask);
1232
+ const missingReviewer = active.filter(t => !t.reviewer || t.reviewer.trim().length === 0);
1233
+ const missingDoneCriteria = active.filter(t => !t.done_criteria || t.done_criteria.length === 0);
1234
+ const missingEtaOnDoing = active.filter(t => {
1235
+ if (t.status !== 'doing')
1236
+ return false;
1237
+ const eta = t.metadata?.eta;
1238
+ return typeof eta !== 'string' || eta.trim().length === 0;
1239
+ });
1240
+ const missingArtifactPathOnValidating = active.filter(t => {
1241
+ if (t.status !== 'validating')
1242
+ return false;
1243
+ const artifactPath = t.metadata?.artifact_path;
1244
+ return typeof artifactPath !== 'string' || artifactPath.trim().length === 0;
1245
+ });
1246
+ return {
1247
+ activeCount: active.length,
1248
+ gateViolations: {
1249
+ missingReviewer: missingReviewer.length,
1250
+ missingDoneCriteria: missingDoneCriteria.length,
1251
+ },
1252
+ statusContractViolations: {
1253
+ missingEtaOnDoing: missingEtaOnDoing.length,
1254
+ missingArtifactPathOnValidating: missingArtifactPathOnValidating.length,
1255
+ },
1256
+ violatingTaskIds: {
1257
+ missingReviewer: missingReviewer.map(t => t.id),
1258
+ missingDoneCriteria: missingDoneCriteria.map(t => t.id),
1259
+ missingEtaOnDoing: missingEtaOnDoing.map(t => t.id),
1260
+ missingArtifactPathOnValidating: missingArtifactPathOnValidating.map(t => t.id),
1261
+ },
1262
+ };
1263
+ }
1264
+ getStats(options) {
1265
+ const db = getDb();
1266
+ const excludeTest = !options?.includeTest;
1267
+ const whereClause = excludeTest ? `WHERE ${TEST_TASK_EXCLUDE_SQL}` : '';
1268
+ const total = db.prepare(`SELECT COUNT(*) as count FROM tasks ${whereClause}`).get().count;
1269
+ const byStatusRows = db.prepare(`SELECT status, COUNT(*) as count FROM tasks ${whereClause} GROUP BY status`).all();
1270
+ const byStatus = {
1271
+ todo: 0, doing: 0, blocked: 0, validating: 0, done: 0, 'in-progress': 0,
1272
+ };
1273
+ for (const row of byStatusRows) {
1274
+ byStatus[row.status] = row.count;
1275
+ }
1276
+ byStatus['in-progress'] = byStatus.doing; // Backward compatibility
1277
+ return { total, byStatus };
1278
+ }
1279
+ }
1280
+ export const taskManager = new TaskManager();
1281
+ //# sourceMappingURL=tasks.js.map