crewly 1.20.361 → 1.20.363

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 (370) hide show
  1. package/config/hooks/destructive-git-guard/guard.mjs +96 -0
  2. package/config/roles/team-leader/prompt.md +1 -1
  3. package/config/roles/team-leader/tl-addon.md +1 -1
  4. package/config/skills/agent/core/ask-owner/SKILL.md +31 -0
  5. package/config/skills/agent/core/ask-owner/execute.sh +14 -3
  6. package/config/skills/agent/core/ask-owner/execute.test.sh +8 -0
  7. package/config/skills/agent/core/gmail-read/SKILL.md +8 -0
  8. package/config/skills/agent/core/gmail-send/SKILL.md +10 -3
  9. package/config/skills/agent/core/gmail-send/execute.sh +2 -1
  10. package/config/skills/agent/core/gmail-watch-thread/SKILL.md +69 -0
  11. package/config/skills/agent/core/gmail-watch-thread/execute.sh +51 -0
  12. package/config/skills/agent/core/project-tickets/SKILL.md +12 -0
  13. package/config/skills/agent/core/project-tickets/execute.sh +13 -3
  14. package/config/skills/agent/core/project-tickets/execute.test.sh +4 -0
  15. package/config/skills/agent/core/watch-for-event/SKILL.md +22 -0
  16. package/config/skills/orchestrator/harness-login/SKILL.md +1 -1
  17. package/config/skills/team-leader/delegate-task/SKILL.md +21 -6
  18. package/config/skills/team-leader/delegate-task/execute.sh +12 -1
  19. package/dist/backend/backend/src/constants.d.ts +216 -8
  20. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  21. package/dist/backend/backend/src/constants.js +217 -9
  22. package/dist/backend/backend/src/constants.js.map +1 -1
  23. package/dist/backend/backend/src/controllers/google/google.controller.d.ts +44 -0
  24. package/dist/backend/backend/src/controllers/google/google.controller.d.ts.map +1 -1
  25. package/dist/backend/backend/src/controllers/google/google.controller.js +121 -3
  26. package/dist/backend/backend/src/controllers/google/google.controller.js.map +1 -1
  27. package/dist/backend/backend/src/controllers/google/google.routes.d.ts.map +1 -1
  28. package/dist/backend/backend/src/controllers/google/google.routes.js +5 -1
  29. package/dist/backend/backend/src/controllers/google/google.routes.js.map +1 -1
  30. package/dist/backend/backend/src/controllers/harness/owner-login.controller.d.ts +1 -1
  31. package/dist/backend/backend/src/controllers/harness/owner-login.controller.js +1 -1
  32. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.d.ts +1 -1
  33. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.d.ts.map +1 -1
  34. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.js +2 -1
  35. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.js.map +1 -1
  36. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.d.ts.map +1 -1
  37. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js +7 -4
  38. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js.map +1 -1
  39. package/dist/backend/backend/src/index.d.ts.map +1 -1
  40. package/dist/backend/backend/src/index.js +114 -1
  41. package/dist/backend/backend/src/index.js.map +1 -1
  42. package/dist/backend/backend/src/services/agent/follow-through.wiring.d.ts.map +1 -1
  43. package/dist/backend/backend/src/services/agent/follow-through.wiring.js +2 -0
  44. package/dist/backend/backend/src/services/agent/follow-through.wiring.js.map +1 -1
  45. package/dist/backend/backend/src/services/agent/handover-interrupted.d.ts +76 -0
  46. package/dist/backend/backend/src/services/agent/handover-interrupted.d.ts.map +1 -0
  47. package/dist/backend/backend/src/services/agent/handover-interrupted.js +139 -0
  48. package/dist/backend/backend/src/services/agent/handover-interrupted.js.map +1 -0
  49. package/dist/backend/backend/src/services/agent/handover-pointers.d.ts +4 -0
  50. package/dist/backend/backend/src/services/agent/handover-pointers.d.ts.map +1 -1
  51. package/dist/backend/backend/src/services/agent/handover-pointers.js +11 -4
  52. package/dist/backend/backend/src/services/agent/handover-pointers.js.map +1 -1
  53. package/dist/backend/backend/src/services/agent/idle-work-guard.d.ts +3 -1
  54. package/dist/backend/backend/src/services/agent/idle-work-guard.d.ts.map +1 -1
  55. package/dist/backend/backend/src/services/agent/idle-work-guard.js +2 -0
  56. package/dist/backend/backend/src/services/agent/idle-work-guard.js.map +1 -1
  57. package/dist/backend/backend/src/services/agent/resource-mode-protection.d.ts +5 -1
  58. package/dist/backend/backend/src/services/agent/resource-mode-protection.d.ts.map +1 -1
  59. package/dist/backend/backend/src/services/agent/resource-mode-protection.js +6 -1
  60. package/dist/backend/backend/src/services/agent/resource-mode-protection.js.map +1 -1
  61. package/dist/backend/backend/src/services/agent/runtime-session-recovery.d.ts +7 -0
  62. package/dist/backend/backend/src/services/agent/runtime-session-recovery.d.ts.map +1 -1
  63. package/dist/backend/backend/src/services/agent/runtime-session-recovery.js +18 -1
  64. package/dist/backend/backend/src/services/agent/runtime-session-recovery.js.map +1 -1
  65. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  66. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +2 -1
  67. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  68. package/dist/backend/backend/src/services/chat-v2/chat-v2.singleton.d.ts +8 -0
  69. package/dist/backend/backend/src/services/chat-v2/chat-v2.singleton.d.ts.map +1 -1
  70. package/dist/backend/backend/src/services/chat-v2/chat-v2.singleton.js +10 -0
  71. package/dist/backend/backend/src/services/chat-v2/chat-v2.singleton.js.map +1 -1
  72. package/dist/backend/backend/src/services/decisions/decision-card.d.ts +46 -13
  73. package/dist/backend/backend/src/services/decisions/decision-card.d.ts.map +1 -1
  74. package/dist/backend/backend/src/services/decisions/decision-card.js +100 -50
  75. package/dist/backend/backend/src/services/decisions/decision-card.js.map +1 -1
  76. package/dist/backend/backend/src/services/decisions/decision-contract.d.ts +23 -1
  77. package/dist/backend/backend/src/services/decisions/decision-contract.d.ts.map +1 -1
  78. package/dist/backend/backend/src/services/decisions/decision-contract.js +76 -4
  79. package/dist/backend/backend/src/services/decisions/decision-contract.js.map +1 -1
  80. package/dist/backend/backend/src/services/decisions/decision-wording.d.ts +167 -0
  81. package/dist/backend/backend/src/services/decisions/decision-wording.d.ts.map +1 -0
  82. package/dist/backend/backend/src/services/decisions/decision-wording.js +406 -0
  83. package/dist/backend/backend/src/services/decisions/decision-wording.js.map +1 -0
  84. package/dist/backend/backend/src/services/decisions/decision.service.d.ts +30 -1
  85. package/dist/backend/backend/src/services/decisions/decision.service.d.ts.map +1 -1
  86. package/dist/backend/backend/src/services/decisions/decision.service.js +44 -8
  87. package/dist/backend/backend/src/services/decisions/decision.service.js.map +1 -1
  88. package/dist/backend/backend/src/services/decisions/decision.wiring.d.ts.map +1 -1
  89. package/dist/backend/backend/src/services/decisions/decision.wiring.js +17 -0
  90. package/dist/backend/backend/src/services/decisions/decision.wiring.js.map +1 -1
  91. package/dist/backend/backend/src/services/event-bus/event-bus.service.d.ts.map +1 -1
  92. package/dist/backend/backend/src/services/event-bus/event-bus.service.js +1 -0
  93. package/dist/backend/backend/src/services/event-bus/event-bus.service.js.map +1 -1
  94. package/dist/backend/backend/src/services/google/gmail-reply-watch.service.d.ts +142 -0
  95. package/dist/backend/backend/src/services/google/gmail-reply-watch.service.d.ts.map +1 -0
  96. package/dist/backend/backend/src/services/google/gmail-reply-watch.service.js +304 -0
  97. package/dist/backend/backend/src/services/google/gmail-reply-watch.service.js.map +1 -0
  98. package/dist/backend/backend/src/services/google/gmail-send-approval.service.d.ts +130 -0
  99. package/dist/backend/backend/src/services/google/gmail-send-approval.service.d.ts.map +1 -0
  100. package/dist/backend/backend/src/services/google/gmail-send-approval.service.js +216 -0
  101. package/dist/backend/backend/src/services/google/gmail-send-approval.service.js.map +1 -0
  102. package/dist/backend/backend/src/services/google/gmail-send-gate.d.ts +28 -13
  103. package/dist/backend/backend/src/services/google/gmail-send-gate.d.ts.map +1 -1
  104. package/dist/backend/backend/src/services/google/gmail-send-gate.js +82 -29
  105. package/dist/backend/backend/src/services/google/gmail-send-gate.js.map +1 -1
  106. package/dist/backend/backend/src/services/google/gmail.service.d.ts +90 -0
  107. package/dist/backend/backend/src/services/google/gmail.service.d.ts.map +1 -1
  108. package/dist/backend/backend/src/services/google/gmail.service.js +122 -0
  109. package/dist/backend/backend/src/services/google/gmail.service.js.map +1 -1
  110. package/dist/backend/backend/src/services/harness/claude-accounts.d.ts +24 -14
  111. package/dist/backend/backend/src/services/harness/claude-accounts.d.ts.map +1 -1
  112. package/dist/backend/backend/src/services/harness/claude-accounts.js +35 -21
  113. package/dist/backend/backend/src/services/harness/claude-accounts.js.map +1 -1
  114. package/dist/backend/backend/src/services/harness/claude-native-login.d.ts +106 -0
  115. package/dist/backend/backend/src/services/harness/claude-native-login.d.ts.map +1 -0
  116. package/dist/backend/backend/src/services/harness/claude-native-login.js +174 -0
  117. package/dist/backend/backend/src/services/harness/claude-native-login.js.map +1 -0
  118. package/dist/backend/backend/src/services/harness/claude-token-migration.service.d.ts +158 -0
  119. package/dist/backend/backend/src/services/harness/claude-token-migration.service.d.ts.map +1 -0
  120. package/dist/backend/backend/src/services/harness/claude-token-migration.service.js +283 -0
  121. package/dist/backend/backend/src/services/harness/claude-token-migration.service.js.map +1 -0
  122. package/dist/backend/backend/src/services/harness/harness-credentials.store.d.ts +44 -24
  123. package/dist/backend/backend/src/services/harness/harness-credentials.store.d.ts.map +1 -1
  124. package/dist/backend/backend/src/services/harness/harness-credentials.store.js +96 -39
  125. package/dist/backend/backend/src/services/harness/harness-credentials.store.js.map +1 -1
  126. package/dist/backend/backend/src/services/harness/harness-registry.js +1 -1
  127. package/dist/backend/backend/src/services/harness/harness-registry.js.map +1 -1
  128. package/dist/backend/backend/src/services/harness/harness-relogin.service.d.ts +2 -2
  129. package/dist/backend/backend/src/services/harness/harness-relogin.service.js +2 -2
  130. package/dist/backend/backend/src/services/harness/harness.service.d.ts.map +1 -1
  131. package/dist/backend/backend/src/services/harness/harness.service.js +10 -1
  132. package/dist/backend/backend/src/services/harness/harness.service.js.map +1 -1
  133. package/dist/backend/backend/src/services/harness/login-broker.service.d.ts +24 -21
  134. package/dist/backend/backend/src/services/harness/login-broker.service.d.ts.map +1 -1
  135. package/dist/backend/backend/src/services/harness/login-broker.service.js +50 -64
  136. package/dist/backend/backend/src/services/harness/login-broker.service.js.map +1 -1
  137. package/dist/backend/backend/src/services/harness/login-rules.d.ts +17 -11
  138. package/dist/backend/backend/src/services/harness/login-rules.d.ts.map +1 -1
  139. package/dist/backend/backend/src/services/harness/login-rules.js +22 -25
  140. package/dist/backend/backend/src/services/harness/login-rules.js.map +1 -1
  141. package/dist/backend/backend/src/services/open-items/open-item-extractor.d.ts +79 -2
  142. package/dist/backend/backend/src/services/open-items/open-item-extractor.d.ts.map +1 -1
  143. package/dist/backend/backend/src/services/open-items/open-item-extractor.js +194 -13
  144. package/dist/backend/backend/src/services/open-items/open-item-extractor.js.map +1 -1
  145. package/dist/backend/backend/src/services/open-items/open-items.service.d.ts +65 -1
  146. package/dist/backend/backend/src/services/open-items/open-items.service.d.ts.map +1 -1
  147. package/dist/backend/backend/src/services/open-items/open-items.service.js +140 -7
  148. package/dist/backend/backend/src/services/open-items/open-items.service.js.map +1 -1
  149. package/dist/backend/backend/src/services/open-items/open-items.wiring.d.ts +8 -0
  150. package/dist/backend/backend/src/services/open-items/open-items.wiring.d.ts.map +1 -1
  151. package/dist/backend/backend/src/services/open-items/open-items.wiring.js +19 -1
  152. package/dist/backend/backend/src/services/open-items/open-items.wiring.js.map +1 -1
  153. package/dist/backend/backend/src/services/orc/owner-completion-report.service.d.ts +35 -19
  154. package/dist/backend/backend/src/services/orc/owner-completion-report.service.d.ts.map +1 -1
  155. package/dist/backend/backend/src/services/orc/owner-completion-report.service.js +60 -63
  156. package/dist/backend/backend/src/services/orc/owner-completion-report.service.js.map +1 -1
  157. package/dist/backend/backend/src/services/orc/owner-completion-report.text.d.ts +86 -0
  158. package/dist/backend/backend/src/services/orc/owner-completion-report.text.d.ts.map +1 -0
  159. package/dist/backend/backend/src/services/orc/owner-completion-report.text.js +170 -0
  160. package/dist/backend/backend/src/services/orc/owner-completion-report.text.js.map +1 -0
  161. package/dist/backend/backend/src/services/orc/owner-completion-report.wiring.d.ts +24 -0
  162. package/dist/backend/backend/src/services/orc/owner-completion-report.wiring.d.ts.map +1 -1
  163. package/dist/backend/backend/src/services/orc/owner-completion-report.wiring.js +38 -1
  164. package/dist/backend/backend/src/services/orc/owner-completion-report.wiring.js.map +1 -1
  165. package/dist/backend/backend/src/services/orc/reply-destination.wiring.d.ts +2 -0
  166. package/dist/backend/backend/src/services/orc/reply-destination.wiring.d.ts.map +1 -1
  167. package/dist/backend/backend/src/services/orc/reply-destination.wiring.js +8 -3
  168. package/dist/backend/backend/src/services/orc/reply-destination.wiring.js.map +1 -1
  169. package/dist/backend/backend/src/services/orc/work-item-destination.d.ts +76 -0
  170. package/dist/backend/backend/src/services/orc/work-item-destination.d.ts.map +1 -1
  171. package/dist/backend/backend/src/services/orc/work-item-destination.js +83 -3
  172. package/dist/backend/backend/src/services/orc/work-item-destination.js.map +1 -1
  173. package/dist/backend/backend/src/services/orc/work-item-destination.wiring.d.ts +31 -2
  174. package/dist/backend/backend/src/services/orc/work-item-destination.wiring.d.ts.map +1 -1
  175. package/dist/backend/backend/src/services/orc/work-item-destination.wiring.js +38 -0
  176. package/dist/backend/backend/src/services/orc/work-item-destination.wiring.js.map +1 -1
  177. package/dist/backend/backend/src/services/project-tickets/project-ticket.service.d.ts +5 -0
  178. package/dist/backend/backend/src/services/project-tickets/project-ticket.service.d.ts.map +1 -1
  179. package/dist/backend/backend/src/services/project-tickets/project-ticket.service.js +30 -6
  180. package/dist/backend/backend/src/services/project-tickets/project-ticket.service.js.map +1 -1
  181. package/dist/backend/backend/src/services/project-tickets/ticket-file.d.ts.map +1 -1
  182. package/dist/backend/backend/src/services/project-tickets/ticket-file.js +6 -1
  183. package/dist/backend/backend/src/services/project-tickets/ticket-file.js.map +1 -1
  184. package/dist/backend/backend/src/services/project-tickets/ticket-hold-registry.d.ts +64 -0
  185. package/dist/backend/backend/src/services/project-tickets/ticket-hold-registry.d.ts.map +1 -0
  186. package/dist/backend/backend/src/services/project-tickets/ticket-hold-registry.js +93 -0
  187. package/dist/backend/backend/src/services/project-tickets/ticket-hold-registry.js.map +1 -0
  188. package/dist/backend/backend/src/services/project-tickets/ticket-hold.d.ts +176 -0
  189. package/dist/backend/backend/src/services/project-tickets/ticket-hold.d.ts.map +1 -0
  190. package/dist/backend/backend/src/services/project-tickets/ticket-hold.js +301 -0
  191. package/dist/backend/backend/src/services/project-tickets/ticket-hold.js.map +1 -0
  192. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +33 -3
  193. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
  194. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +50 -4
  195. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
  196. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts +8 -1
  197. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts.map +1 -1
  198. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js +26 -0
  199. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js.map +1 -1
  200. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +6 -1
  201. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
  202. package/dist/backend/backend/src/services/reconciler/reconciler.service.js +3 -2
  203. package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
  204. package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.wiring.d.ts.map +1 -1
  205. package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.wiring.js +4 -1
  206. package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.wiring.js.map +1 -1
  207. package/dist/backend/backend/src/services/stall/background-run-detector.d.ts +56 -0
  208. package/dist/backend/backend/src/services/stall/background-run-detector.d.ts.map +1 -0
  209. package/dist/backend/backend/src/services/stall/background-run-detector.js +93 -0
  210. package/dist/backend/backend/src/services/stall/background-run-detector.js.map +1 -0
  211. package/dist/backend/backend/src/services/stall/claim-check.d.ts +60 -0
  212. package/dist/backend/backend/src/services/stall/claim-check.d.ts.map +1 -0
  213. package/dist/backend/backend/src/services/stall/claim-check.js +98 -0
  214. package/dist/backend/backend/src/services/stall/claim-check.js.map +1 -0
  215. package/dist/backend/backend/src/services/stall/in-flight-runs.store.d.ts +110 -0
  216. package/dist/backend/backend/src/services/stall/in-flight-runs.store.d.ts.map +1 -0
  217. package/dist/backend/backend/src/services/stall/in-flight-runs.store.js +109 -0
  218. package/dist/backend/backend/src/services/stall/in-flight-runs.store.js.map +1 -0
  219. package/dist/backend/backend/src/services/stall/stall-watcher.service.d.ts +490 -0
  220. package/dist/backend/backend/src/services/stall/stall-watcher.service.d.ts.map +1 -0
  221. package/dist/backend/backend/src/services/stall/stall-watcher.service.js +922 -0
  222. package/dist/backend/backend/src/services/stall/stall-watcher.service.js.map +1 -0
  223. package/dist/backend/backend/src/services/stall/stall-watcher.wiring.d.ts +98 -0
  224. package/dist/backend/backend/src/services/stall/stall-watcher.wiring.d.ts.map +1 -0
  225. package/dist/backend/backend/src/services/stall/stall-watcher.wiring.js +217 -0
  226. package/dist/backend/backend/src/services/stall/stall-watcher.wiring.js.map +1 -0
  227. package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts +53 -0
  228. package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
  229. package/dist/backend/backend/src/services/task-pool/task-pool.service.js +162 -15
  230. package/dist/backend/backend/src/services/task-pool/task-pool.service.js.map +1 -1
  231. package/dist/backend/backend/src/services/tl-delegation/tl-delegation.service.js +1 -1
  232. package/dist/backend/backend/src/services/tl-delegation/tl-delegation.service.js.map +1 -1
  233. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.d.ts +3 -0
  234. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.d.ts.map +1 -1
  235. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.js +5 -2
  236. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.js.map +1 -1
  237. package/dist/backend/backend/src/services/worktree/worktree-unmerged-notice.d.ts +7 -2
  238. package/dist/backend/backend/src/services/worktree/worktree-unmerged-notice.d.ts.map +1 -1
  239. package/dist/backend/backend/src/services/worktree/worktree-unmerged-notice.js +11 -2
  240. package/dist/backend/backend/src/services/worktree/worktree-unmerged-notice.js.map +1 -1
  241. package/dist/backend/backend/src/types/decision.types.d.ts +53 -1
  242. package/dist/backend/backend/src/types/decision.types.d.ts.map +1 -1
  243. package/dist/backend/backend/src/types/event-bus.types.d.ts +4 -2
  244. package/dist/backend/backend/src/types/event-bus.types.d.ts.map +1 -1
  245. package/dist/backend/backend/src/types/event-bus.types.js +8 -0
  246. package/dist/backend/backend/src/types/event-bus.types.js.map +1 -1
  247. package/dist/backend/backend/src/types/project-ticket.types.d.ts +7 -0
  248. package/dist/backend/backend/src/types/project-ticket.types.d.ts.map +1 -1
  249. package/dist/backend/backend/src/types/project-ticket.types.js +1 -0
  250. package/dist/backend/backend/src/types/project-ticket.types.js.map +1 -1
  251. package/dist/backend/backend/src/types/v2/open-item.types.d.ts +10 -1
  252. package/dist/backend/backend/src/types/v2/open-item.types.d.ts.map +1 -1
  253. package/dist/backend/build-info.json +2 -2
  254. package/dist/cli/backend/src/constants.d.ts +216 -8
  255. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  256. package/dist/cli/backend/src/constants.js +217 -9
  257. package/dist/cli/backend/src/constants.js.map +1 -1
  258. package/dist/cli/backend/src/services/agent/handover-interrupted.d.ts +76 -0
  259. package/dist/cli/backend/src/services/agent/handover-interrupted.d.ts.map +1 -0
  260. package/dist/cli/backend/src/services/agent/handover-interrupted.js +139 -0
  261. package/dist/cli/backend/src/services/agent/handover-interrupted.js.map +1 -0
  262. package/dist/cli/backend/src/services/agent/handover-pointers.d.ts +4 -0
  263. package/dist/cli/backend/src/services/agent/handover-pointers.d.ts.map +1 -1
  264. package/dist/cli/backend/src/services/agent/handover-pointers.js +11 -4
  265. package/dist/cli/backend/src/services/agent/handover-pointers.js.map +1 -1
  266. package/dist/cli/backend/src/services/agent/runtime-session-recovery.d.ts +7 -0
  267. package/dist/cli/backend/src/services/agent/runtime-session-recovery.d.ts.map +1 -1
  268. package/dist/cli/backend/src/services/agent/runtime-session-recovery.js +18 -1
  269. package/dist/cli/backend/src/services/agent/runtime-session-recovery.js.map +1 -1
  270. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  271. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +2 -1
  272. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  273. package/dist/cli/backend/src/services/chat-v2/chat-v2.singleton.d.ts +8 -0
  274. package/dist/cli/backend/src/services/chat-v2/chat-v2.singleton.d.ts.map +1 -1
  275. package/dist/cli/backend/src/services/chat-v2/chat-v2.singleton.js +10 -0
  276. package/dist/cli/backend/src/services/chat-v2/chat-v2.singleton.js.map +1 -1
  277. package/dist/cli/backend/src/services/decisions/decision-card.d.ts +46 -13
  278. package/dist/cli/backend/src/services/decisions/decision-card.d.ts.map +1 -1
  279. package/dist/cli/backend/src/services/decisions/decision-card.js +100 -50
  280. package/dist/cli/backend/src/services/decisions/decision-card.js.map +1 -1
  281. package/dist/cli/backend/src/services/decisions/decision-contract.d.ts +23 -1
  282. package/dist/cli/backend/src/services/decisions/decision-contract.d.ts.map +1 -1
  283. package/dist/cli/backend/src/services/decisions/decision-contract.js +76 -4
  284. package/dist/cli/backend/src/services/decisions/decision-contract.js.map +1 -1
  285. package/dist/cli/backend/src/services/decisions/decision-wording.d.ts +167 -0
  286. package/dist/cli/backend/src/services/decisions/decision-wording.d.ts.map +1 -0
  287. package/dist/cli/backend/src/services/decisions/decision-wording.js +406 -0
  288. package/dist/cli/backend/src/services/decisions/decision-wording.js.map +1 -0
  289. package/dist/cli/backend/src/services/decisions/decision.service.d.ts +30 -1
  290. package/dist/cli/backend/src/services/decisions/decision.service.d.ts.map +1 -1
  291. package/dist/cli/backend/src/services/decisions/decision.service.js +44 -8
  292. package/dist/cli/backend/src/services/decisions/decision.service.js.map +1 -1
  293. package/dist/cli/backend/src/services/event-bus/event-bus.service.d.ts.map +1 -1
  294. package/dist/cli/backend/src/services/event-bus/event-bus.service.js +1 -0
  295. package/dist/cli/backend/src/services/event-bus/event-bus.service.js.map +1 -1
  296. package/dist/cli/backend/src/services/harness/claude-accounts.d.ts +24 -14
  297. package/dist/cli/backend/src/services/harness/claude-accounts.d.ts.map +1 -1
  298. package/dist/cli/backend/src/services/harness/claude-accounts.js +35 -21
  299. package/dist/cli/backend/src/services/harness/claude-accounts.js.map +1 -1
  300. package/dist/cli/backend/src/services/harness/claude-native-login.d.ts +106 -0
  301. package/dist/cli/backend/src/services/harness/claude-native-login.d.ts.map +1 -0
  302. package/dist/cli/backend/src/services/harness/claude-native-login.js +174 -0
  303. package/dist/cli/backend/src/services/harness/claude-native-login.js.map +1 -0
  304. package/dist/cli/backend/src/services/harness/claude-token-migration.service.d.ts +158 -0
  305. package/dist/cli/backend/src/services/harness/claude-token-migration.service.d.ts.map +1 -0
  306. package/dist/cli/backend/src/services/harness/claude-token-migration.service.js +283 -0
  307. package/dist/cli/backend/src/services/harness/claude-token-migration.service.js.map +1 -0
  308. package/dist/cli/backend/src/services/harness/harness-credentials.store.d.ts +44 -24
  309. package/dist/cli/backend/src/services/harness/harness-credentials.store.d.ts.map +1 -1
  310. package/dist/cli/backend/src/services/harness/harness-credentials.store.js +96 -39
  311. package/dist/cli/backend/src/services/harness/harness-credentials.store.js.map +1 -1
  312. package/dist/cli/backend/src/services/harness/harness-registry.js +1 -1
  313. package/dist/cli/backend/src/services/harness/harness-registry.js.map +1 -1
  314. package/dist/cli/backend/src/services/harness/harness-relogin.service.d.ts +2 -2
  315. package/dist/cli/backend/src/services/harness/harness-relogin.service.js +2 -2
  316. package/dist/cli/backend/src/services/harness/harness.service.d.ts.map +1 -1
  317. package/dist/cli/backend/src/services/harness/harness.service.js +10 -1
  318. package/dist/cli/backend/src/services/harness/harness.service.js.map +1 -1
  319. package/dist/cli/backend/src/services/harness/login-broker.service.d.ts +24 -21
  320. package/dist/cli/backend/src/services/harness/login-broker.service.d.ts.map +1 -1
  321. package/dist/cli/backend/src/services/harness/login-broker.service.js +50 -64
  322. package/dist/cli/backend/src/services/harness/login-broker.service.js.map +1 -1
  323. package/dist/cli/backend/src/services/harness/login-rules.d.ts +17 -11
  324. package/dist/cli/backend/src/services/harness/login-rules.d.ts.map +1 -1
  325. package/dist/cli/backend/src/services/harness/login-rules.js +22 -25
  326. package/dist/cli/backend/src/services/harness/login-rules.js.map +1 -1
  327. package/dist/cli/backend/src/services/open-items/open-item-extractor.d.ts +79 -2
  328. package/dist/cli/backend/src/services/open-items/open-item-extractor.d.ts.map +1 -1
  329. package/dist/cli/backend/src/services/open-items/open-item-extractor.js +194 -13
  330. package/dist/cli/backend/src/services/open-items/open-item-extractor.js.map +1 -1
  331. package/dist/cli/backend/src/services/open-items/open-items.service.d.ts +65 -1
  332. package/dist/cli/backend/src/services/open-items/open-items.service.d.ts.map +1 -1
  333. package/dist/cli/backend/src/services/open-items/open-items.service.js +140 -7
  334. package/dist/cli/backend/src/services/open-items/open-items.service.js.map +1 -1
  335. package/dist/cli/backend/src/services/orc/owner-completion-report.text.d.ts +86 -0
  336. package/dist/cli/backend/src/services/orc/owner-completion-report.text.d.ts.map +1 -0
  337. package/dist/cli/backend/src/services/orc/owner-completion-report.text.js +170 -0
  338. package/dist/cli/backend/src/services/orc/owner-completion-report.text.js.map +1 -0
  339. package/dist/cli/backend/src/services/orc/work-item-destination.d.ts +76 -0
  340. package/dist/cli/backend/src/services/orc/work-item-destination.d.ts.map +1 -1
  341. package/dist/cli/backend/src/services/orc/work-item-destination.js +83 -3
  342. package/dist/cli/backend/src/services/orc/work-item-destination.js.map +1 -1
  343. package/dist/cli/backend/src/services/project-tickets/project-ticket.service.d.ts +5 -0
  344. package/dist/cli/backend/src/services/project-tickets/project-ticket.service.d.ts.map +1 -1
  345. package/dist/cli/backend/src/services/project-tickets/project-ticket.service.js +30 -6
  346. package/dist/cli/backend/src/services/project-tickets/project-ticket.service.js.map +1 -1
  347. package/dist/cli/backend/src/services/project-tickets/ticket-file.d.ts.map +1 -1
  348. package/dist/cli/backend/src/services/project-tickets/ticket-file.js +6 -1
  349. package/dist/cli/backend/src/services/project-tickets/ticket-file.js.map +1 -1
  350. package/dist/cli/backend/src/services/project-tickets/ticket-hold.d.ts +176 -0
  351. package/dist/cli/backend/src/services/project-tickets/ticket-hold.d.ts.map +1 -0
  352. package/dist/cli/backend/src/services/project-tickets/ticket-hold.js +301 -0
  353. package/dist/cli/backend/src/services/project-tickets/ticket-hold.js.map +1 -0
  354. package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts +53 -0
  355. package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
  356. package/dist/cli/backend/src/services/task-pool/task-pool.service.js +162 -15
  357. package/dist/cli/backend/src/services/task-pool/task-pool.service.js.map +1 -1
  358. package/dist/cli/backend/src/types/decision.types.d.ts +53 -1
  359. package/dist/cli/backend/src/types/decision.types.d.ts.map +1 -1
  360. package/dist/cli/backend/src/types/event-bus.types.d.ts +4 -2
  361. package/dist/cli/backend/src/types/event-bus.types.d.ts.map +1 -1
  362. package/dist/cli/backend/src/types/event-bus.types.js +8 -0
  363. package/dist/cli/backend/src/types/event-bus.types.js.map +1 -1
  364. package/dist/cli/backend/src/types/project-ticket.types.d.ts +7 -0
  365. package/dist/cli/backend/src/types/project-ticket.types.d.ts.map +1 -1
  366. package/dist/cli/backend/src/types/project-ticket.types.js +1 -0
  367. package/dist/cli/backend/src/types/project-ticket.types.js.map +1 -1
  368. package/dist/cli/backend/src/types/v2/open-item.types.d.ts +10 -1
  369. package/dist/cli/backend/src/types/v2/open-item.types.d.ts.map +1 -1
  370. package/package.json +1 -1
@@ -0,0 +1,922 @@
1
+ /**
2
+ * Stall watcher (crewly#1363, crewly#1362, specs/2026-10-17-stall-watcher.md).
3
+ *
4
+ * Stalls used to be a metric only (`TRACE_CONSTANTS.STALL_MINUTES`). This
5
+ * service acts on three kinds of in-flight work, each keyed to the agent's
6
+ * own estimate when it gave one:
7
+ *
8
+ * - **Promises to the owner** (open-items commitments): at 1.5× the estimate
9
+ * past the promise with nothing posted, the agent is re-briefed once with
10
+ * the last message from someone else before the promise (the approval it
11
+ * may be re-waiting for, crewly#1361); at 3× the team lead is told once;
12
+ * when the lead has not answered in the thread within LEAD_GRACE_MS, the
13
+ * owner gets one plain line (at most one per item per day).
14
+ * - **Claimed WorkItems**: the run trace's ongoing stall (no tool calls,
15
+ * posts or worktree changes for CLAIM_STALL_MS) nudges the claimer once
16
+ * with the cause; a second window with no progress releases the claim
17
+ * (unassigned, so the route sweep hands it to the lead and then the
18
+ * orchestrator) and tells the lead. Tickets the ticket autopilot already
19
+ * watches are left to it.
20
+ * - **Background runs**: an agent saying it started something that takes a
21
+ * while ("已经开跑…预计 1 小时左右") is recorded in
22
+ * `CREWLY_HOME/in-flight-runs.json`; the agent is protected from idle stops
23
+ * and evictions until expectedBy + grace; the same ladder as promises
24
+ * applies when nothing (a file, a link, a finished / failed report) lands in
25
+ * the thread.
26
+ *
27
+ * Plus the claim check (crewly#1362): "X is doing it" told to the owner with
28
+ * no WorkItem or ticket for X within CLAIM_CHECK_WINDOW_MS gets the speaker
29
+ * one internal line; the owner hears nothing.
30
+ *
31
+ * Cost control: every line to an agent is one line and tagged; one nudge per
32
+ * agent per NUDGE_COOLDOWN_MS; the owner is never the retry loop. The first
33
+ * ARM_DELAY_MS after the first pass is a dry run (logs only).
34
+ *
35
+ * @module services/stall/stall-watcher.service
36
+ */
37
+ import { STALL_WATCHER_CONSTANTS } from '../../constants.js';
38
+ import { LoggerService } from '../core/logger.service.js';
39
+ import { ACTIVE_OPEN_ITEM_STATUSES } from '../../types/v2/open-item.types.js';
40
+ import { STALL_CAUSE_LABELS } from '../trace/trace-timeline.js';
41
+ import { isInterim } from '../slack/slack-typing-placeholder.service.js';
42
+ import { detectBackgroundRun, hasDeliverable, isRunFinishedReport } from './background-run-detector.js';
43
+ import { claimNudgeText, detectTeammateClaims } from './claim-check.js';
44
+ import { emptyState, pruneState } from './in-flight-runs.store.js';
45
+ const C = STALL_WATCHER_CONSTANTS;
46
+ // ---------------------------------------------------------------------------
47
+ // Pure helpers
48
+ // ---------------------------------------------------------------------------
49
+ /**
50
+ * Clip text to one line of at most `max` characters.
51
+ *
52
+ * @param text - Text
53
+ * @param max - Max characters
54
+ * @returns Clipped text
55
+ */
56
+ export function clip(text, max) {
57
+ const one = (text ?? '').replace(/\s+/g, ' ').trim();
58
+ return one.length > max ? `${one.slice(0, max - 1)}…` : one;
59
+ }
60
+ /**
61
+ * A duration in words, in the owner's language.
62
+ *
63
+ * @param ms - Milliseconds
64
+ * @param lang - Language
65
+ * @returns "40 分钟" / "1.5 小时" / "40 minutes" / "1.5 hours"
66
+ */
67
+ export function durationWords(ms, lang = 'en') {
68
+ const minutes = Math.max(1, Math.round(ms / 60_000));
69
+ if (minutes < 60)
70
+ return lang === 'zh' ? `${minutes} 分钟` : `${minutes} minute${minutes === 1 ? '' : 's'}`;
71
+ const hours = Math.round((minutes / 60) * 10) / 10;
72
+ if (hours < 48)
73
+ return lang === 'zh' ? `${hours} 小时` : `${hours} hour${hours === 1 ? '' : 's'}`;
74
+ const days = Math.round((hours / 24) * 10) / 10;
75
+ return lang === 'zh' ? `${days} 天` : `${days} day${days === 1 ? '' : 's'}`;
76
+ }
77
+ /**
78
+ * The owner's language in a thread: Chinese when the owner's own messages
79
+ * hold more CJK characters than Latin letters, else English (CLAUDE.md:
80
+ * harness text is English; a line to the owner follows the owner).
81
+ *
82
+ * @param messages - The thread
83
+ * @returns 'zh' or 'en'
84
+ */
85
+ export function ownerLanguage(messages) {
86
+ let cjk = 0;
87
+ let latin = 0;
88
+ for (const m of messages) {
89
+ if (m.senderType !== 'user')
90
+ continue;
91
+ cjk += (m.content.match(/[぀-ヿ㐀-䶿一-鿿]/g) ?? []).length;
92
+ latin += (m.content.match(/[a-z]/gi) ?? []).length;
93
+ }
94
+ return cjk > 0 && cjk >= latin ? 'zh' : 'en';
95
+ }
96
+ /**
97
+ * The last message by someone other than the agent before a time: the
98
+ * approval or instruction the promise answered (crewly#1361 expectation 2).
99
+ *
100
+ * @param messages - The thread
101
+ * @param agent - The promising agent
102
+ * @param before - Epoch ms of the promise
103
+ * @returns That message, or null
104
+ */
105
+ export function contextBefore(messages, agent, before) {
106
+ let found = null;
107
+ for (const m of messages) {
108
+ if (m.createdAt >= before || m.senderId === agent || m.senderType === 'system' || !m.content.trim())
109
+ continue;
110
+ if (!found || m.createdAt > found.createdAt)
111
+ found = m;
112
+ }
113
+ return found;
114
+ }
115
+ /**
116
+ * Whether a run's agent posted a deliverable or a finished / failed report in
117
+ * the thread after the run started.
118
+ *
119
+ * @param messages - The thread
120
+ * @param run - The run
121
+ * @returns True when the run is over as far as the thread shows
122
+ */
123
+ export function runDeliveredIn(messages, run) {
124
+ return messages.some((m) => m.senderType === 'agent' &&
125
+ m.senderId === run.agent &&
126
+ m.id !== run.id &&
127
+ m.createdAt > run.startedAt &&
128
+ !isInterim(m) &&
129
+ (hasDeliverable({ content: m.content, ...(m.attachmentCount !== undefined ? { attachmentCount: m.attachmentCount } : {}) }) || isRunFinishedReport(m.content)));
130
+ }
131
+ /**
132
+ * The time each ladder step is due.
133
+ *
134
+ * @param madeAt - Promise / run start (ms)
135
+ * @param estimateMs - The agent's estimate
136
+ * @returns Due times (ms)
137
+ */
138
+ export function ladderDue(madeAt, estimateMs) {
139
+ return {
140
+ agentDue: madeAt + Math.max(C.AGENT_STEP_MULTIPLIER * estimateMs, C.MIN_AGENT_STEP_MS),
141
+ leadDue: madeAt + Math.max(C.LEAD_STEP_MULTIPLIER * estimateMs, C.MIN_AGENT_STEP_MS + C.LEAD_GRACE_MS),
142
+ };
143
+ }
144
+ /**
145
+ * The idle-stop / eviction protection reason for a session with a run in
146
+ * flight, or null.
147
+ *
148
+ * @param runs - Recorded runs
149
+ * @param session - Agent session
150
+ * @param now - Epoch ms
151
+ * @returns English reason, or null
152
+ */
153
+ export function runProtectionReason(runs, session, now) {
154
+ const run = runs.find((r) => r.agent === session && now < r.expectedBy + C.RUN_PROTECTION_GRACE_MS);
155
+ if (!run)
156
+ return null;
157
+ return `a background run it announced ("${clip(run.head, C.QUOTE_MAX_CHARS)}") is expected by ${new Date(run.expectedBy).toISOString()}`;
158
+ }
159
+ /**
160
+ * Whether the watcher is switched on.
161
+ *
162
+ * @param env - Environment
163
+ * @returns False when CREWLY_STALL_WATCHER=false
164
+ */
165
+ export function stallWatcherEnabled(env = process.env) {
166
+ return (env[C.ENV_KILL_SWITCH] ?? '').trim().toLowerCase() !== 'false';
167
+ }
168
+ /**
169
+ * Whether a pass is a dry run: CREWLY_STALL_WATCHER_DRY_RUN=true/false wins;
170
+ * otherwise dry until ARM_DELAY_MS after the first pass.
171
+ *
172
+ * @param armedAt - When the first pass ran (ms), or undefined
173
+ * @param now - Epoch ms
174
+ * @param env - Environment
175
+ * @returns True to only log
176
+ */
177
+ export function stallWatcherDryRun(armedAt, now, env = process.env) {
178
+ const forced = (env[C.ENV_DRY_RUN] ?? '').trim().toLowerCase();
179
+ if (forced === 'true' || forced === '1')
180
+ return true;
181
+ if (forced === 'false' || forced === '0')
182
+ return false;
183
+ return armedAt === undefined || now - armedAt < C.ARM_DELAY_MS;
184
+ }
185
+ /**
186
+ * The agent-facing stall cause: the trace's label plus its detail.
187
+ *
188
+ * @param signal - Stall signal
189
+ * @returns One clause
190
+ */
191
+ export function causeText(signal) {
192
+ const label = STALL_CAUSE_LABELS[signal.cause] ?? signal.cause;
193
+ const detail = clip(signal.detail, C.QUOTE_MAX_CHARS);
194
+ return detail && detail.toLowerCase() !== label.toLowerCase() ? `${label} (${detail})` : label;
195
+ }
196
+ /**
197
+ * The one plain line the owner gets about a promise.
198
+ *
199
+ * @param lang - Owner's language
200
+ * @param input - Names and times
201
+ * @returns The line
202
+ */
203
+ export function promiseOwnerLine(lang, input) {
204
+ const est = durationWords(input.estimateMs, lang);
205
+ const elapsed = durationWords(input.elapsedMs, lang);
206
+ if (lang === 'zh') {
207
+ const asked = input.leadName ? `也问了 ${input.leadName}` : '团队里没有负责人可问';
208
+ return `${input.name} 说大约 ${est} 内给「${input.what}」,现在过了 ${elapsed} 还没有,已经提醒过 ${input.name},${asked}。`;
209
+ }
210
+ const asked = input.leadName ? `I asked ${input.leadName}` : 'there is no team lead to ask';
211
+ return `${input.name} said "${input.what}" would come in about ${est}; ${elapsed} later nothing has arrived. ${input.name} has been reminded and ${asked}.`;
212
+ }
213
+ /**
214
+ * Whether an open item is an owner decision asked in prose that has no card
215
+ * (crewly#1369): it is followed up on the promise ladder.
216
+ *
217
+ * @param item - Open item
218
+ * @returns True for an uncarded owner question with a due time
219
+ */
220
+ export function isUncardedOwnerQuestion(item) {
221
+ return item.type === 'question' && item.ownerQuestion === true && !item.decisionId && !!item.due;
222
+ }
223
+ /**
224
+ * The one plain line the owner gets about a question still waiting on him.
225
+ *
226
+ * @param lang - Owner's language
227
+ * @param input - Name, question, time since
228
+ * @returns The line
229
+ */
230
+ export function questionOwnerLine(lang, input) {
231
+ const elapsed = durationWords(input.elapsedMs, lang);
232
+ if (lang === 'zh')
233
+ return `${input.name} ${elapsed}前问你:「${input.what}」还没有你的回答。回一句就行,或者说「不用了」。`;
234
+ return `${input.name} asked you ${elapsed} ago: "${input.what}" — there is no answer from you yet. One line is enough, or say "skip".`;
235
+ }
236
+ /**
237
+ * The one plain line the owner gets about a background run.
238
+ *
239
+ * @param lang - Owner's language
240
+ * @param input - Names and times
241
+ * @returns The line
242
+ */
243
+ export function runOwnerLine(lang, input) {
244
+ const est = durationWords(input.estimateMs, lang);
245
+ const elapsed = durationWords(input.elapsedMs, lang);
246
+ if (lang === 'zh') {
247
+ const asked = input.leadName ? `也问了 ${input.leadName}` : '团队里没有负责人可问';
248
+ return `${input.name} 说「${input.head}」大约 ${est},现在过了 ${elapsed} 还没有结果,已经提醒过 ${input.name},${asked}。`;
249
+ }
250
+ const asked = input.leadName ? `I asked ${input.leadName}` : 'there is no team lead to ask';
251
+ return `${input.name} said "${input.head}" (about ${est}); ${elapsed} later there is no result in the thread. ${input.name} has been reminded and ${asked}.`;
252
+ }
253
+ /**
254
+ * ISO → epoch ms, NaN-safe.
255
+ *
256
+ * @param iso - ISO string
257
+ * @returns Epoch ms, or null
258
+ */
259
+ function ms(iso) {
260
+ if (!iso)
261
+ return null;
262
+ const t = Date.parse(iso);
263
+ return Number.isFinite(t) ? t : null;
264
+ }
265
+ // ---------------------------------------------------------------------------
266
+ // Service
267
+ // ---------------------------------------------------------------------------
268
+ /** Process-wide instance. */
269
+ let instance = null;
270
+ /**
271
+ * The running watcher, if any.
272
+ *
273
+ * @returns The service, or null
274
+ */
275
+ export function getStallWatcher() {
276
+ return instance;
277
+ }
278
+ /**
279
+ * Install (or clear) the process-wide watcher.
280
+ *
281
+ * @param service - The service, or null
282
+ */
283
+ export function setStallWatcher(service) {
284
+ instance = service;
285
+ }
286
+ /**
287
+ * Watches in-flight work and climbs the agent → lead → owner ladder.
288
+ */
289
+ export class StallWatcherService {
290
+ deps;
291
+ logger;
292
+ now;
293
+ env;
294
+ state = emptyState();
295
+ loaded = false;
296
+ timer = null;
297
+ bootTimer = null;
298
+ chain = Promise.resolve();
299
+ /**
300
+ * @param deps - Collaborators
301
+ */
302
+ constructor(deps) {
303
+ this.deps = deps;
304
+ this.logger = deps.logger ?? LoggerService.getInstance().createComponentLogger('StallWatcher');
305
+ this.now = deps.now ?? (() => Date.now());
306
+ this.env = deps.env ?? process.env;
307
+ }
308
+ /**
309
+ * Whether the kill switch is off.
310
+ *
311
+ * @returns True when the watcher may run
312
+ */
313
+ enabled() {
314
+ return stallWatcherEnabled(this.env);
315
+ }
316
+ /**
317
+ * Load the state file, then schedule the first pass (after the boot
318
+ * gate and BOOT_DELAY_MS) and the periodic passes. No-op when disabled.
319
+ */
320
+ start() {
321
+ if (!this.enabled()) {
322
+ this.logger.info('Stall watcher disabled by environment', { env: C.ENV_KILL_SWITCH });
323
+ return;
324
+ }
325
+ if (this.timer)
326
+ return;
327
+ void this.load();
328
+ this.bootTimer = setTimeout(() => {
329
+ this.bootTimer = null;
330
+ void this.bootTick();
331
+ }, C.BOOT_DELAY_MS);
332
+ this.bootTimer.unref?.();
333
+ this.timer = setInterval(() => void this.tick().catch((err) => this.logger.warn('Stall watcher pass failed', { error: errText(err) })), C.INTERVAL_MS);
334
+ this.timer.unref?.();
335
+ this.logger.info('Stall watcher started', { intervalMs: C.INTERVAL_MS, bootDelayMs: C.BOOT_DELAY_MS, stateFile: this.deps.store.path });
336
+ }
337
+ /** Stop the passes. Safe to call more than once. */
338
+ stop() {
339
+ if (this.bootTimer) {
340
+ clearTimeout(this.bootTimer);
341
+ this.bootTimer = null;
342
+ }
343
+ if (this.timer) {
344
+ clearInterval(this.timer);
345
+ this.timer = null;
346
+ }
347
+ }
348
+ /**
349
+ * Read the state file once (later passes keep the in-memory copy).
350
+ *
351
+ * @returns The state
352
+ */
353
+ async load() {
354
+ if (!this.loaded) {
355
+ this.state = await this.deps.store.load();
356
+ this.loaded = true;
357
+ }
358
+ return this.state;
359
+ }
360
+ /**
361
+ * The first pass: waits at the boot-pressure gate like the other
362
+ * maintenance passes, then ticks.
363
+ *
364
+ * @returns What the pass did
365
+ */
366
+ async bootTick() {
367
+ if (this.deps.bootGate) {
368
+ try {
369
+ const gate = await this.deps.bootGate();
370
+ if (gate.waitedMs > 0)
371
+ this.logger.info('Stall watcher boot pass waited for a quiet machine', { waitedMs: gate.waitedMs, reduced: gate.reduced, reason: gate.reason });
372
+ }
373
+ catch (err) {
374
+ this.logger.warn('Stall watcher boot gate failed (running anyway)', { error: errText(err) });
375
+ }
376
+ }
377
+ return this.tick();
378
+ }
379
+ /**
380
+ * Why an agent must not be idle-stopped or evicted right now: a run it
381
+ * announced is still expected. Synchronous, from the in-memory state.
382
+ *
383
+ * @param session - Agent session
384
+ * @returns English reason, or null
385
+ */
386
+ protectedRunReason(session) {
387
+ return runProtectionReason(this.state.runs, session, this.now());
388
+ }
389
+ /**
390
+ * The runs recorded for a session (tests and the status endpoint).
391
+ *
392
+ * @param session - Agent session
393
+ * @returns Runs, newest last
394
+ */
395
+ runsOf(session) {
396
+ return this.state.runs.filter((r) => r.agent === session);
397
+ }
398
+ /**
399
+ * A chat-v2 message arrived. Agent posts can start a background run,
400
+ * close one (a deliverable or a finished / failed report in its thread)
401
+ * or claim that a teammate is doing something. Other posts are ignored
402
+ * here; the lead's answer is read from the thread when the ladder needs it.
403
+ *
404
+ * @param message - The message
405
+ */
406
+ onMessage(message) {
407
+ if (!this.enabled() || message.senderType !== 'agent')
408
+ return Promise.resolve();
409
+ return this.serial(async () => {
410
+ await this.load();
411
+ const now = this.now();
412
+ const threadId = message.threadId ?? message.id;
413
+ let changed = false;
414
+ // Close runs this post delivers.
415
+ const done = this.state.runs.filter((r) => r.agent === message.senderId &&
416
+ r.channelId === message.channelId &&
417
+ r.threadId === threadId &&
418
+ r.id !== message.id &&
419
+ (hasDeliverable({ content: message.content, ...(message.attachmentCount !== undefined ? { attachmentCount: message.attachmentCount } : {}) }) || isRunFinishedReport(message.content)));
420
+ if (done.length > 0) {
421
+ this.state.runs = this.state.runs.filter((r) => !done.includes(r));
422
+ for (const r of done)
423
+ delete this.state.ladders[`run:${r.id}`];
424
+ this.logger.info('Background run closed by the agent\'s post', { agent: message.senderId, runs: done.map((r) => r.id), message: message.id });
425
+ changed = true;
426
+ }
427
+ // A new run.
428
+ if (!isInterim(message)) {
429
+ const run = detectBackgroundRun(message.content);
430
+ const already = this.state.runs.some((r) => r.agent === message.senderId && r.channelId === message.channelId && r.threadId === threadId);
431
+ if (run && !already) {
432
+ const estimateMs = run.estimateMs ?? C.RUN_DEFAULT_ESTIMATE_MS;
433
+ this.state.runs.push({
434
+ id: message.id,
435
+ agent: message.senderId,
436
+ channelId: message.channelId,
437
+ threadId,
438
+ head: run.head,
439
+ startedAt: message.createdAt,
440
+ estimateMs,
441
+ estimateSource: run.estimateMs !== null ? 'text' : 'default',
442
+ expectedBy: message.createdAt + estimateMs,
443
+ });
444
+ this.logger.info('Background run registered', { agent: message.senderId, message: message.id, estimateMs, expectedBy: new Date(message.createdAt + estimateMs).toISOString() });
445
+ changed = true;
446
+ }
447
+ }
448
+ // "X is doing it" to the owner.
449
+ if (!isInterim(message) && ((await this.deps.isOwnerThread?.(message).catch(() => false)) ?? true)) {
450
+ const teammates = await this.deps.teammatesOf(message.senderId).catch(() => []);
451
+ for (const claim of detectTeammateClaims(message.content, teammates, message.senderId)) {
452
+ const pending = this.state.claims.some((c) => c.speaker === message.senderId && c.member === claim.member && Math.abs(c.saidAt - message.createdAt) < C.CLAIM_CHECK_WINDOW_MS);
453
+ if (pending)
454
+ continue;
455
+ this.state.claims.push({ id: message.id, speaker: message.senderId, member: claim.member, memberName: claim.memberName, saidAt: message.createdAt, head: claim.sentence });
456
+ this.logger.info('Teammate claim noted for a check', { speaker: message.senderId, member: claim.member, message: message.id });
457
+ changed = true;
458
+ }
459
+ }
460
+ if (changed) {
461
+ pruneState(this.state, now);
462
+ await this.deps.store.save(this.state).catch((err) => this.logger.warn('Stall watcher state not saved', { error: errText(err) }));
463
+ }
464
+ });
465
+ }
466
+ /**
467
+ * One pass over every kind of in-flight work.
468
+ *
469
+ * @returns What the pass did
470
+ */
471
+ tick() {
472
+ return this.serial(() => this.pass());
473
+ }
474
+ /**
475
+ * Run `fn` after every earlier change to the state (one writer).
476
+ *
477
+ * @param fn - Work
478
+ * @returns Its result
479
+ */
480
+ serial(fn) {
481
+ const run = this.chain.then(fn, fn);
482
+ this.chain = run.catch(() => undefined);
483
+ return run;
484
+ }
485
+ /**
486
+ * The pass itself (under {@link serial}).
487
+ *
488
+ * @returns What it did
489
+ */
490
+ async pass() {
491
+ const result = {
492
+ dryRun: false,
493
+ promises: { agent: 0, lead: 0, owner: 0 },
494
+ claims: { nudged: 0, released: 0 },
495
+ runs: { agent: 0, lead: 0, owner: 0, cleared: 0 },
496
+ teammateClaims: { checked: 0, nudged: 0 },
497
+ };
498
+ if (!this.enabled())
499
+ return { ...result, skipped: 'disabled' };
500
+ await this.load();
501
+ const now = this.now();
502
+ if (this.state.armedAt === undefined) {
503
+ this.state.armedAt = now;
504
+ this.logger.info('Stall watcher armed: dry run (logs only) until the arm delay passes', { armedAt: new Date(now).toISOString(), armDelayMs: C.ARM_DELAY_MS });
505
+ }
506
+ const dry = stallWatcherDryRun(this.state.armedAt, now, this.env);
507
+ result.dryRun = dry;
508
+ pruneState(this.state, now);
509
+ await this.promisesPass(now, dry, result).catch((err) => this.logger.warn('Stall watcher promises pass failed', { error: errText(err) }));
510
+ await this.claimsPass(now, dry, result).catch((err) => this.logger.warn('Stall watcher claims pass failed', { error: errText(err) }));
511
+ await this.runsPass(now, dry, result).catch((err) => this.logger.warn('Stall watcher runs pass failed', { error: errText(err) }));
512
+ await this.teammateClaimsPass(now, dry, result).catch((err) => this.logger.warn('Stall watcher claim-check pass failed', { error: errText(err) }));
513
+ await this.deps.store.save(this.state).catch((err) => this.logger.warn('Stall watcher state not saved', { error: errText(err) }));
514
+ const acted = result.promises.agent + result.promises.lead + result.promises.owner + result.claims.nudged + result.claims.released + result.runs.agent + result.runs.lead + result.runs.owner + result.runs.cleared + result.teammateClaims.nudged;
515
+ if (acted > 0)
516
+ this.logger.info(dry ? 'Stall watcher pass (dry run, nothing sent)' : 'Stall watcher pass', { ...result });
517
+ return result;
518
+ }
519
+ // -------------------------------------------------------------------------
520
+ // Sending (every path honours the dry run and the per-agent cooldown)
521
+ // -------------------------------------------------------------------------
522
+ /**
523
+ * One line to an agent, at most one per agent per NUDGE_COOLDOWN_MS.
524
+ *
525
+ * @param agent - Session
526
+ * @param text - The line
527
+ * @param now - Epoch ms
528
+ * @param dry - Dry run: log only
529
+ * @param recentNudgeAt - A nudge from elsewhere that counts for the cooldown
530
+ * @returns What happened
531
+ */
532
+ async nudgeAgent(agent, text, now, dry, recentNudgeAt) {
533
+ const last = Math.max(this.state.lastNudgeByAgent[agent] ?? 0, recentNudgeAt ?? 0);
534
+ if (last && now - last < C.NUDGE_COOLDOWN_MS)
535
+ return 'cooldown';
536
+ if (dry) {
537
+ this.logger.info('Dry run: would nudge agent', { agent, text });
538
+ return 'dry';
539
+ }
540
+ const ok = await this.deps.deliverToAgent(agent, text).catch(() => false);
541
+ if (!ok)
542
+ return 'failed';
543
+ this.state.lastNudgeByAgent[agent] = now;
544
+ return 'sent';
545
+ }
546
+ /**
547
+ * One line to an agent's lead.
548
+ *
549
+ * @param agent - The agent whose lead is told
550
+ * @param text - The line (built with the lead's name)
551
+ * @param dry - Dry run
552
+ * @returns The lead session when told (or would be), null when there is no lead or it failed
553
+ */
554
+ async tellLead(agent, text, dry) {
555
+ const lead = await this.deps.leadOf(agent).catch(() => null);
556
+ if (!lead || lead === agent)
557
+ return null;
558
+ const line = await text(await this.name(lead));
559
+ if (dry) {
560
+ this.logger.info('Dry run: would tell the lead', { agent, lead, text: line });
561
+ return lead;
562
+ }
563
+ const ok = await this.deps.deliverToAgent(lead, line).catch(() => false);
564
+ return ok ? lead : null;
565
+ }
566
+ /**
567
+ * Display name of a session (falls back to the session).
568
+ *
569
+ * @param session - Session
570
+ * @returns Name
571
+ */
572
+ async name(session) {
573
+ return (await this.deps.displayName(session).catch(() => session)) || session;
574
+ }
575
+ // -------------------------------------------------------------------------
576
+ // The ladder
577
+ // -------------------------------------------------------------------------
578
+ /**
579
+ * Climb one step of the ladder for an item, when it is due.
580
+ *
581
+ * @param item - The item
582
+ * @param now - Epoch ms
583
+ * @param dry - Dry run
584
+ * @returns The step taken, or null
585
+ */
586
+ async climb(item, now, dry) {
587
+ const ladder = (this.state.ladders[item.key] ??= { seenAt: now });
588
+ ladder.seenAt = now;
589
+ const agentAt = ladder.agentAt ?? item.prior?.agentAt;
590
+ const leadAt = ladder.leadAt ?? item.prior?.leadAt;
591
+ const ownerAt = ladder.ownerAt ?? item.prior?.ownerAt;
592
+ const { agentDue, leadDue } = ladderDue(item.madeAt, item.estimateMs);
593
+ if (!agentAt) {
594
+ if (now < agentDue)
595
+ return null;
596
+ const outcome = await this.nudgeAgent(item.agent, await item.agentText(), now, dry, item.prior?.recentNudgeAt);
597
+ if (outcome === 'cooldown' || outcome === 'failed') {
598
+ this.logger.debug('Stall ladder: agent step deferred', { key: item.key, outcome });
599
+ return null;
600
+ }
601
+ ladder.agentAt = now;
602
+ this.deps.trace?.({ session: item.agent, summary: `Stall watcher: ${item.key} re-briefed at ${Math.round((now - item.madeAt) / 60_000)} min`, ...(item.workItemId ? { workItemId: item.workItemId } : {}), data: { reason: 'stall', step: 'agent', key: item.key, dryRun: dry } });
603
+ if (!dry)
604
+ await item.onStep?.('agent', new Date(now)).catch(() => undefined);
605
+ this.logger.info('Stall ladder: agent re-briefed', { key: item.key, agent: item.agent, dryRun: dry });
606
+ return 'agent';
607
+ }
608
+ if (!leadAt) {
609
+ if (now < leadDue)
610
+ return null;
611
+ const lead = await this.tellLead(item.agent, item.leadText, dry);
612
+ ladder.leadAt = now;
613
+ ladder.leadSession = lead ?? undefined;
614
+ this.deps.trace?.({ session: item.agent, summary: `Stall watcher: ${item.key} escalated to ${lead ?? 'no lead'}`, ...(item.workItemId ? { workItemId: item.workItemId } : {}), data: { reason: 'stall', step: 'lead', key: item.key, dryRun: dry } });
615
+ if (!dry)
616
+ await item.onStep?.('lead', new Date(now)).catch(() => undefined);
617
+ this.logger.info('Stall ladder: lead told', { key: item.key, agent: item.agent, lead, dryRun: dry });
618
+ return 'lead';
619
+ }
620
+ if (now < leadAt + C.LEAD_GRACE_MS)
621
+ return null;
622
+ if (ownerAt && now - ownerAt < C.OWNER_LINE_INTERVAL_MS)
623
+ return null;
624
+ const lead = ladder.leadSession ?? (await this.deps.leadOf(item.agent).catch(() => null));
625
+ const messages = item.thread ? await this.deps.threadMessages(item.thread).catch(() => []) : [];
626
+ if (lead && messages.some((m) => m.senderId === lead && m.createdAt > leadAt)) {
627
+ this.logger.info('Stall ladder: the lead answered in the thread; the owner is not told', { key: item.key, lead });
628
+ return null;
629
+ }
630
+ const line = await item.ownerLine(ownerLanguage(messages), lead ? await this.name(lead) : null);
631
+ if (dry) {
632
+ this.logger.info('Dry run: would tell the owner', { key: item.key, text: line });
633
+ }
634
+ else {
635
+ const ok = await this.deps.notifyOwner(line).catch(() => false);
636
+ if (!ok) {
637
+ this.logger.warn('Stall ladder: owner line not sent', { key: item.key });
638
+ return null;
639
+ }
640
+ await item.onStep?.('owner', new Date(now)).catch(() => undefined);
641
+ }
642
+ ladder.ownerAt = now;
643
+ this.deps.trace?.({ session: item.agent, summary: `Stall watcher: owner told about ${item.key}`, ...(item.workItemId ? { workItemId: item.workItemId } : {}), data: { reason: 'stall', step: 'owner', key: item.key, dryRun: dry } });
644
+ this.logger.info('Stall ladder: owner told', { key: item.key, agent: item.agent, dryRun: dry });
645
+ return 'owner';
646
+ }
647
+ // -------------------------------------------------------------------------
648
+ // (a) Promises to the owner
649
+ // -------------------------------------------------------------------------
650
+ /**
651
+ * Active commitments with a due time: the ladder keyed to the agent's
652
+ * estimate (due − made). The open-items sweep's own stamps count as steps.
653
+ *
654
+ * @param now - Epoch ms
655
+ * @param dry - Dry run
656
+ * @param result - Counters
657
+ */
658
+ async promisesPass(now, dry, result) {
659
+ const requests = await this.deps.listRequests();
660
+ for (const request of requests) {
661
+ if (request.status === 'cancelled' || request.status === 'done')
662
+ continue;
663
+ for (const item of request.openItems ?? []) {
664
+ if (!ACTIVE_OPEN_ITEM_STATUSES.has(item.status) || item.status === 'waiting_owner')
665
+ continue;
666
+ // An owner decision asked in prose with no card waits on an answer the same way (crewly#1369).
667
+ const question = isUncardedOwnerQuestion(item);
668
+ if (item.type !== 'commitment' && !question)
669
+ continue;
670
+ const madeAt = ms(item.createdAt);
671
+ const due = ms(item.due);
672
+ if (madeAt === null || due === null || due <= madeAt)
673
+ continue;
674
+ const ladderItem = question
675
+ ? this.questionLadderItem(request, item, madeAt, due - madeAt, now)
676
+ : this.promiseLadderItem(request, item, madeAt, due - madeAt, now);
677
+ const step = await this.climb(ladderItem, now, dry);
678
+ if (step)
679
+ result.promises[step] += 1;
680
+ }
681
+ }
682
+ }
683
+ /**
684
+ * The ladder item of a commitment.
685
+ *
686
+ * @param request - Its request
687
+ * @param item - The commitment
688
+ * @param madeAt - Promise time (ms)
689
+ * @param estimateMs - due − made
690
+ * @param now - Epoch ms
691
+ * @returns Ladder item
692
+ */
693
+ promiseLadderItem(request, item, madeAt, estimateMs, now) {
694
+ const thread = request.chatRef ? { channelId: request.chatRef.channelId, threadId: request.chatRef.threadRootId } : null;
695
+ const what = clip(item.text, C.QUOTE_MAX_CHARS);
696
+ const elapsedMs = now - madeAt;
697
+ return {
698
+ key: `promise:${request.id}/${item.id}`,
699
+ agent: item.agent,
700
+ madeAt,
701
+ estimateMs,
702
+ thread,
703
+ prior: {
704
+ ...(ms(item.stallNudgedAt) !== null ? { agentAt: ms(item.stallNudgedAt) } : {}),
705
+ ...(ms(item.leadNotifiedAt) !== null ? { leadAt: ms(item.leadNotifiedAt) } : {}),
706
+ ...(ms(item.ownerNotifiedAt) !== null ? { ownerAt: ms(item.ownerNotifiedAt) } : {}),
707
+ ...(ms(item.nudgedAt) !== null ? { recentNudgeAt: ms(item.nudgedAt) } : {}),
708
+ },
709
+ agentText: async () => {
710
+ const messages = thread ? await this.deps.threadMessages(thread).catch(() => []) : [];
711
+ const ctx = contextBefore(messages, item.agent, madeAt);
712
+ const context = ctx
713
+ ? ` Context — the last message before your promise (${ctx.senderType === 'user' ? 'the owner' : await this.name(ctx.senderId)}): "${clip(ctx.content, C.CONTEXT_MAX_CHARS)}" — if that was the go-ahead, do not wait for another.`
714
+ : '';
715
+ return (`${C.NUDGE_TAG} You told the owner "${what}" ${durationWords(elapsedMs)} ago and gave yourself about ${durationWords(estimateMs)}; nothing has been posted in that thread since. ` +
716
+ `Post the deliverable now, or tell the owner in one line what blocks it and when it will come.${context}`);
717
+ },
718
+ leadText: async () => {
719
+ const name = await this.name(item.agent);
720
+ return (`${C.NUDGE_TAG} ${name} promised the owner "${what}" ${durationWords(elapsedMs)} ago (estimate ${durationWords(estimateMs)}) and has posted nothing since being reminded. ` +
721
+ `Check on ${name}, reassign the work, or answer the owner in that thread.`);
722
+ },
723
+ ownerLine: async (lang, leadName) => promiseOwnerLine(lang, { name: await this.name(item.agent), what, estimateMs, elapsedMs, leadName }),
724
+ onStep: (step, at) => this.deps.recordPromiseStep(request.id, item.id, step, at),
725
+ ...(item.workItemId ? { workItemId: item.workItemId } : {}),
726
+ };
727
+ }
728
+ /**
729
+ * The ladder item of an uncarded owner question: the agent is asked to
730
+ * put it on a card (or record the answer), then the lead, then the owner
731
+ * gets the question itself once.
732
+ *
733
+ * @param request - Its request
734
+ * @param item - The owner question
735
+ * @param madeAt - When it was asked (ms)
736
+ * @param estimateMs - due − asked
737
+ * @param now - Epoch ms
738
+ * @returns Ladder item
739
+ */
740
+ questionLadderItem(request, item, madeAt, estimateMs, now) {
741
+ const thread = request.chatRef ? { channelId: request.chatRef.channelId, threadId: request.chatRef.threadRootId } : null;
742
+ const what = clip(item.text, C.QUOTE_MAX_CHARS);
743
+ const elapsedMs = now - madeAt;
744
+ return {
745
+ key: `question:${request.id}/${item.id}`,
746
+ agent: item.agent,
747
+ madeAt,
748
+ estimateMs,
749
+ thread,
750
+ prior: {
751
+ ...(ms(item.stallNudgedAt) !== null ? { agentAt: ms(item.stallNudgedAt) } : {}),
752
+ ...(ms(item.leadNotifiedAt) !== null ? { leadAt: ms(item.leadNotifiedAt) } : {}),
753
+ ...(ms(item.ownerNotifiedAt) !== null ? { ownerAt: ms(item.ownerNotifiedAt) } : {}),
754
+ },
755
+ agentText: async () => `${C.NUDGE_TAG} You asked the owner "${what}" ${durationWords(elapsedMs)} ago and no answer is on file. ` +
756
+ 'If he answered somewhere else, act on it and say so in that thread. If not, ask it once as a decision card (ask-owner) so he can tap an answer — or drop it if it no longer matters. Do not act on it without an answer.',
757
+ leadText: async () => {
758
+ const name = await this.name(item.agent);
759
+ return (`${C.NUDGE_TAG} ${name} asked the owner "${what}" ${durationWords(elapsedMs)} ago; there is still no answer on file and ${name} was reminded. ` +
760
+ `Make sure it becomes a decision card or is dropped; nobody acts on it without the owner's answer.`);
761
+ },
762
+ ownerLine: async (lang) => questionOwnerLine(lang, { name: await this.name(item.agent), what, elapsedMs }),
763
+ onStep: (step, at) => this.deps.recordPromiseStep(request.id, item.id, step, at),
764
+ };
765
+ }
766
+ // -------------------------------------------------------------------------
767
+ // (b) Claimed WorkItems
768
+ // -------------------------------------------------------------------------
769
+ /**
770
+ * Running items with an active claim: the trace's ongoing stall nudges the
771
+ * claimer once; a second window releases the claim and tells the lead.
772
+ *
773
+ * @param now - Epoch ms
774
+ * @param dry - Dry run
775
+ * @param result - Counters
776
+ */
777
+ async claimsPass(now, dry, result) {
778
+ const claims = (await this.deps.activeClaims()).filter((c) => c.status === 'active' || c.status === 'expiring');
779
+ if (claims.length === 0)
780
+ return;
781
+ const items = new Map((await this.deps.listWorkItems()).map((w) => [w.id, w]));
782
+ const linked = (await this.deps.ticketLinkedWorkItemIds?.().catch(() => new Set())) ?? new Set();
783
+ for (const claim of claims) {
784
+ const wi = items.get(claim.workItemId);
785
+ if (!wi || wi.status !== 'running')
786
+ continue;
787
+ const key = `claim:${wi.id}`;
788
+ if (linked.has(wi.id))
789
+ continue; // the ticket autopilot re-briefs and releases those
790
+ const signal = await this.deps.stallOf({ workItemId: wi.id, session: claim.agentId }).catch(() => null);
791
+ if (!signal || signal.ms < C.CLAIM_STALL_MS || signal.cause === 'waiting_on_owner') {
792
+ if (this.state.ladders[key])
793
+ delete this.state.ladders[key]; // moving again: a fresh cycle next time
794
+ continue;
795
+ }
796
+ const ladder = (this.state.ladders[key] ??= { seenAt: now });
797
+ ladder.seenAt = now;
798
+ const title = clip(wi.title ?? wi.id, C.QUOTE_MAX_CHARS);
799
+ const minutes = Math.round(signal.ms / 60_000);
800
+ const cause = causeText(signal);
801
+ if (!ladder.agentAt) {
802
+ const text = `${C.NUDGE_TAG} Work item ${wi.id} ("${title}") has shown no progress for ${minutes} min — ${cause}. Continue it now, or release it with a one-line reason.`;
803
+ const outcome = await this.nudgeAgent(claim.agentId, text, now, dry);
804
+ if (outcome === 'cooldown' || outcome === 'failed')
805
+ continue;
806
+ ladder.agentAt = now;
807
+ result.claims.nudged += 1;
808
+ this.deps.trace?.({ session: claim.agentId, summary: `Stall watcher: ${wi.id} stalled ${minutes} min (${signal.cause}) — claimer nudged`, workItemId: wi.id, data: { reason: 'stall', step: 'agent', cause: signal.cause, dryRun: dry } });
809
+ this.logger.info('Stalled claim: claimer nudged', { workItemId: wi.id, agent: claim.agentId, minutes, cause: signal.cause, dryRun: dry });
810
+ continue;
811
+ }
812
+ if (ladder.leadAt || now - ladder.agentAt < C.CLAIM_STALL_MS || signal.ms < 2 * C.CLAIM_STALL_MS)
813
+ continue;
814
+ const reason = `stall watcher: no progress for ${minutes} min after a reminder (${cause})`;
815
+ if (dry) {
816
+ this.logger.info('Dry run: would release the claim and tell the lead', { workItemId: wi.id, agent: claim.agentId, reason });
817
+ }
818
+ else {
819
+ await this.deps.releaseClaim(wi.id, reason).catch((err) => this.logger.warn('Stalled claim not released', { workItemId: wi.id, error: errText(err) }));
820
+ }
821
+ const name = await this.name(claim.agentId);
822
+ const lead = await this.tellLead(claim.agentId, async () => `${C.NUDGE_TAG} ${name}'s work item ${wi.id} ("${title}") showed no progress for ${minutes} min after a reminder (${cause}); its claim was released and it is queued again, unassigned ` +
823
+ `(the route sweep hands it to you, then to the orchestrator). Take it, reassign it, or cancel it with a reason.`, dry);
824
+ ladder.leadAt = now;
825
+ result.claims.released += 1;
826
+ this.deps.trace?.({ session: claim.agentId, summary: `Stall watcher: ${wi.id} released after ${minutes} min with no progress; lead ${lead ?? 'none'} told`, workItemId: wi.id, data: { reason: 'stall', step: 'lead', cause: signal.cause, dryRun: dry } });
827
+ this.logger.warn('Stalled claim released and escalated', { workItemId: wi.id, agent: claim.agentId, lead, minutes, cause: signal.cause, dryRun: dry });
828
+ }
829
+ }
830
+ // -------------------------------------------------------------------------
831
+ // (c) Background runs
832
+ // -------------------------------------------------------------------------
833
+ /**
834
+ * Runs agents announced: closed when the thread shows a deliverable or a
835
+ * report, else the same ladder as promises from the run's start.
836
+ *
837
+ * @param now - Epoch ms
838
+ * @param dry - Dry run
839
+ * @param result - Counters
840
+ */
841
+ async runsPass(now, dry, result) {
842
+ for (const run of [...this.state.runs]) {
843
+ const thread = { channelId: run.channelId, threadId: run.threadId };
844
+ const key = `run:${run.id}`;
845
+ const { agentDue } = ladderDue(run.startedAt, run.estimateMs);
846
+ if (now < agentDue)
847
+ continue;
848
+ const messages = await this.deps.threadMessages(thread).catch(() => []);
849
+ if (runDeliveredIn(messages, run)) {
850
+ this.state.runs = this.state.runs.filter((r) => r.id !== run.id);
851
+ delete this.state.ladders[key];
852
+ result.runs.cleared += 1;
853
+ this.logger.info('Background run closed: the thread shows its result', { agent: run.agent, run: run.id });
854
+ continue;
855
+ }
856
+ const head = clip(run.head, C.QUOTE_MAX_CHARS);
857
+ const elapsedMs = now - run.startedAt;
858
+ const step = await this.climb({
859
+ key,
860
+ agent: run.agent,
861
+ madeAt: run.startedAt,
862
+ estimateMs: run.estimateMs,
863
+ thread,
864
+ agentText: async () => `${C.NUDGE_TAG} You said "${head}" ${durationWords(elapsedMs)} ago (estimate ${durationWords(run.estimateMs)}) and nothing has been posted in that thread since. ` +
865
+ `Check the run: post the result, or say in one line that it failed and what you will do.`,
866
+ leadText: async () => {
867
+ const name = await this.name(run.agent);
868
+ return (`${C.NUDGE_TAG} ${name} said "${head}" ${durationWords(elapsedMs)} ago (estimate ${durationWords(run.estimateMs)}) and has posted no result since being reminded. ` +
869
+ `Check whether the run is still alive, restart it or reassign it, or answer the owner in that thread.`);
870
+ },
871
+ ownerLine: async (lang, leadName) => runOwnerLine(lang, { name: await this.name(run.agent), head, estimateMs: run.estimateMs, elapsedMs, leadName }),
872
+ }, now, dry);
873
+ if (step)
874
+ result.runs[step] += 1;
875
+ }
876
+ }
877
+ // -------------------------------------------------------------------------
878
+ // (#1362) "X is doing it"
879
+ // -------------------------------------------------------------------------
880
+ /**
881
+ * Claims past their window: a WorkItem or ticket for the teammate exists,
882
+ * or the speaker is told once. Never the owner.
883
+ *
884
+ * @param now - Epoch ms
885
+ * @param dry - Dry run
886
+ * @param result - Counters
887
+ */
888
+ async teammateClaimsPass(now, dry, result) {
889
+ const keep = [];
890
+ for (const claim of this.state.claims) {
891
+ if (now < claim.saidAt + C.CLAIM_CHECK_WINDOW_MS) {
892
+ keep.push(claim);
893
+ continue;
894
+ }
895
+ result.teammateClaims.checked += 1;
896
+ const exists = await this.deps.assignmentExists(claim.speaker, claim.member, claim.saidAt, C.CLAIM_CHECK_WINDOW_MS).catch(() => true);
897
+ if (exists) {
898
+ this.logger.debug('Teammate claim backed by an assignment', { speaker: claim.speaker, member: claim.member, message: claim.id });
899
+ continue;
900
+ }
901
+ const outcome = await this.nudgeAgent(claim.speaker, claimNudgeText(claim.memberName, claim.head), now, dry);
902
+ if (outcome === 'cooldown') {
903
+ keep.push(claim); // next pass (pruned if it never gets through)
904
+ continue;
905
+ }
906
+ result.teammateClaims.nudged += 1;
907
+ this.deps.trace?.({ session: claim.speaker, summary: `Claim check: told the owner ${claim.memberName} is doing it; nothing assigned to ${claim.member}`, data: { reason: 'claim_check', member: claim.member, dryRun: dry } });
908
+ this.logger.warn('Teammate claim with no assignment — speaker told', { speaker: claim.speaker, member: claim.member, message: claim.id, outcome });
909
+ }
910
+ this.state.claims = keep;
911
+ }
912
+ }
913
+ /**
914
+ * Error text.
915
+ *
916
+ * @param err - Anything thrown
917
+ * @returns Message
918
+ */
919
+ function errText(err) {
920
+ return err instanceof Error ? err.message : String(err);
921
+ }
922
+ //# sourceMappingURL=stall-watcher.service.js.map