crewly 1.13.0 → 1.14.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 (985) hide show
  1. package/config/constants.ts +8 -0
  2. package/config/roles/orchestrator/prompt.md +23 -0
  3. package/config/skills/_common/complete-body-shape.test.sh +113 -0
  4. package/config/skills/_common/no-dangling-symlinks.test.sh +63 -0
  5. package/config/skills/_common/task-pool-body-shape.test.sh +471 -0
  6. package/config/skills/agent/core/accept-task/execute.sh +30 -5
  7. package/config/skills/agent/core/block-task/execute.sh +59 -6
  8. package/config/skills/agent/core/complete-task/SKILL.md +37 -5
  9. package/config/skills/agent/core/complete-task/execute.sh +40 -9
  10. package/config/skills/agent/core/complete-task/execute.test.sh +213 -0
  11. package/config/skills/agent/core/create-task/SKILL.md +33 -0
  12. package/config/skills/agent/core/create-task/execute.sh +82 -23
  13. package/config/skills/agent/core/delegate-task/execute.sh +46 -0
  14. package/config/skills/agent/core/get-team-norms/SKILL.md +1 -1
  15. package/config/skills/agent/core/get-team-norms/execute.sh +73 -2
  16. package/config/skills/agent/core/handoff-task/execute.sh +8 -1
  17. package/config/skills/agent/core/reply-channel/SKILL.md +6 -0
  18. package/config/skills/agent/core/reply-channel/execute.sh +14 -2
  19. package/config/skills/agent/core/report-status/SKILL.md +26 -0
  20. package/config/skills/agent/core/report-status/execute.sh +40 -5
  21. package/config/skills/agent/core/slack-post/SKILL.md +126 -0
  22. package/config/skills/agent/core/slack-post/execute.sh +127 -0
  23. package/config/skills/agent/remote-browser/SKILL.md +178 -0
  24. package/config/skills/agent/xiaoyuzhoufm-transcript/SKILL.md +46 -22
  25. package/config/skills/agent/xiaoyuzhoufm-transcript/execute.sh +82 -214
  26. package/config/skills/agent/xiaoyuzhoufm-transcript/skill.json +3 -3
  27. package/config/skills/orchestrator/delegate-task/SKILL.md +1 -1
  28. package/config/skills/orchestrator/delegate-task/execute.sh +45 -11
  29. package/config/skills/orchestrator/delegate-task/execute.test.sh +153 -1
  30. package/config/skills/orchestrator/get-team-status/SKILL.md +5 -1
  31. package/config/skills/orchestrator/get-team-status/execute.sh +28 -2
  32. package/config/skills/team-leader/aggregate-results/SKILL.md +1 -1
  33. package/config/skills/team-leader/decompose-goal/execute.sh +11 -16
  34. package/config/skills/team-leader/delegate-task/SKILL.md +14 -1
  35. package/config/skills/team-leader/delegate-task/execute.sh +132 -43
  36. package/config/skills/team-leader/delegate-task/execute.test.sh +180 -27
  37. package/config/slack-app-manifest.json +7 -1
  38. package/config/vitest-exclude.constants.ts +56 -0
  39. package/dist/backend/backend/src/config/oauth.config.d.ts +93 -19
  40. package/dist/backend/backend/src/config/oauth.config.d.ts.map +1 -1
  41. package/dist/backend/backend/src/config/oauth.config.js +121 -19
  42. package/dist/backend/backend/src/config/oauth.config.js.map +1 -1
  43. package/dist/backend/backend/src/constants.d.ts +60 -0
  44. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  45. package/dist/backend/backend/src/constants.js +74 -0
  46. package/dist/backend/backend/src/constants.js.map +1 -1
  47. package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
  48. package/dist/backend/backend/src/controllers/chat/chat.controller.js +33 -4
  49. package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
  50. package/dist/backend/backend/src/controllers/credentials/google-oauth.controller.d.ts.map +1 -1
  51. package/dist/backend/backend/src/controllers/credentials/google-oauth.controller.js +27 -9
  52. package/dist/backend/backend/src/controllers/credentials/google-oauth.controller.js.map +1 -1
  53. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.d.ts.map +1 -1
  54. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js +18 -3
  55. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js.map +1 -1
  56. package/dist/backend/backend/src/controllers/slack/slack.controller.d.ts.map +1 -1
  57. package/dist/backend/backend/src/controllers/slack/slack.controller.js +360 -15
  58. package/dist/backend/backend/src/controllers/slack/slack.controller.js.map +1 -1
  59. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.d.ts +6 -1
  60. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.d.ts.map +1 -1
  61. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js +19 -10
  62. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js.map +1 -1
  63. package/dist/backend/backend/src/index.d.ts.map +1 -1
  64. package/dist/backend/backend/src/index.js +43 -7
  65. package/dist/backend/backend/src/index.js.map +1 -1
  66. package/dist/backend/backend/src/scripts/stamp-build.d.ts +25 -0
  67. package/dist/backend/backend/src/scripts/stamp-build.d.ts.map +1 -0
  68. package/dist/backend/backend/src/scripts/stamp-build.js +48 -0
  69. package/dist/backend/backend/src/scripts/stamp-build.js.map +1 -0
  70. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  71. package/dist/backend/backend/src/services/agent/agent-registration.service.js +25 -0
  72. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  73. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
  74. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +9 -1
  75. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
  76. package/dist/backend/backend/src/services/agent/crewly-agent/types.d.ts +42 -3
  77. package/dist/backend/backend/src/services/agent/crewly-agent/types.d.ts.map +1 -1
  78. package/dist/backend/backend/src/services/agent/crewly-agent/types.js +22 -1
  79. package/dist/backend/backend/src/services/agent/crewly-agent/types.js.map +1 -1
  80. package/dist/backend/backend/src/services/agent/idle-detection.service.d.ts +10 -0
  81. package/dist/backend/backend/src/services/agent/idle-detection.service.d.ts.map +1 -1
  82. package/dist/backend/backend/src/services/agent/idle-detection.service.js +37 -0
  83. package/dist/backend/backend/src/services/agent/idle-detection.service.js.map +1 -1
  84. package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts.map +1 -1
  85. package/dist/backend/backend/src/services/browser/browser-bridge.service.js +13 -11
  86. package/dist/backend/backend/src/services/browser/browser-bridge.service.js.map +1 -1
  87. package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts +14 -0
  88. package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts.map +1 -1
  89. package/dist/backend/backend/src/services/browser/browser-proxy.service.js +56 -0
  90. package/dist/backend/backend/src/services/browser/browser-proxy.service.js.map +1 -1
  91. package/dist/backend/backend/src/services/browser/browser-services.registry.d.ts +63 -0
  92. package/dist/backend/backend/src/services/browser/browser-services.registry.d.ts.map +1 -0
  93. package/dist/backend/backend/src/services/browser/browser-services.registry.js +74 -0
  94. package/dist/backend/backend/src/services/browser/browser-services.registry.js.map +1 -0
  95. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +25 -1
  96. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
  97. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +19 -7
  98. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
  99. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts +57 -0
  100. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
  101. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js +110 -1
  102. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
  103. package/dist/backend/backend/src/services/chat-v2/sqlite/message.store.d.ts +21 -0
  104. package/dist/backend/backend/src/services/chat-v2/sqlite/message.store.d.ts.map +1 -1
  105. package/dist/backend/backend/src/services/chat-v2/sqlite/message.store.js +43 -0
  106. package/dist/backend/backend/src/services/chat-v2/sqlite/message.store.js.map +1 -1
  107. package/dist/backend/backend/src/services/core/storage.service.d.ts +6 -0
  108. package/dist/backend/backend/src/services/core/storage.service.d.ts.map +1 -1
  109. package/dist/backend/backend/src/services/core/storage.service.js +1 -1
  110. package/dist/backend/backend/src/services/core/storage.service.js.map +1 -1
  111. package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts.map +1 -1
  112. package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js +6 -2
  113. package/dist/backend/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js.map +1 -1
  114. package/dist/backend/backend/src/services/event-bus/event-to-workitem-bridge.service.d.ts +15 -0
  115. package/dist/backend/backend/src/services/event-bus/event-to-workitem-bridge.service.d.ts.map +1 -1
  116. package/dist/backend/backend/src/services/event-bus/event-to-workitem-bridge.service.js +54 -0
  117. package/dist/backend/backend/src/services/event-bus/event-to-workitem-bridge.service.js.map +1 -1
  118. package/dist/backend/backend/src/services/orc/orc-delivery-enforcer.service.d.ts +8 -0
  119. package/dist/backend/backend/src/services/orc/orc-delivery-enforcer.service.d.ts.map +1 -1
  120. package/dist/backend/backend/src/services/orc/orc-delivery-enforcer.service.js +31 -3
  121. package/dist/backend/backend/src/services/orc/orc-delivery-enforcer.service.js.map +1 -1
  122. package/dist/backend/backend/src/services/orchestrator/orchestrator-status.service.d.ts +28 -3
  123. package/dist/backend/backend/src/services/orchestrator/orchestrator-status.service.d.ts.map +1 -1
  124. package/dist/backend/backend/src/services/orchestrator/orchestrator-status.service.js +65 -8
  125. package/dist/backend/backend/src/services/orchestrator/orchestrator-status.service.js.map +1 -1
  126. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +214 -4
  127. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
  128. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +330 -48
  129. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
  130. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts +47 -1
  131. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts.map +1 -1
  132. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js +183 -15
  133. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js.map +1 -1
  134. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +14 -0
  135. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
  136. package/dist/backend/backend/src/services/reconciler/reconciler.service.js +77 -3
  137. package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
  138. package/dist/backend/backend/src/services/slack/slack-agent-identity.service.d.ts +209 -0
  139. package/dist/backend/backend/src/services/slack/slack-agent-identity.service.d.ts.map +1 -0
  140. package/dist/backend/backend/src/services/slack/slack-agent-identity.service.js +416 -0
  141. package/dist/backend/backend/src/services/slack/slack-agent-identity.service.js.map +1 -0
  142. package/dist/backend/backend/src/services/slack/slack-agent-post.service.d.ts +124 -0
  143. package/dist/backend/backend/src/services/slack/slack-agent-post.service.d.ts.map +1 -0
  144. package/dist/backend/backend/src/services/slack/slack-agent-post.service.js +243 -0
  145. package/dist/backend/backend/src/services/slack/slack-agent-post.service.js.map +1 -0
  146. package/dist/backend/backend/src/services/slack/slack-initializer.d.ts +11 -0
  147. package/dist/backend/backend/src/services/slack/slack-initializer.d.ts.map +1 -1
  148. package/dist/backend/backend/src/services/slack/slack-initializer.js +56 -0
  149. package/dist/backend/backend/src/services/slack/slack-initializer.js.map +1 -1
  150. package/dist/backend/backend/src/services/slack/slack-mention-resolver.d.ts +103 -0
  151. package/dist/backend/backend/src/services/slack/slack-mention-resolver.d.ts.map +1 -0
  152. package/dist/backend/backend/src/services/slack/slack-mention-resolver.js +197 -0
  153. package/dist/backend/backend/src/services/slack/slack-mention-resolver.js.map +1 -0
  154. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  155. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +18 -0
  156. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  157. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +329 -0
  158. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -0
  159. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +821 -0
  160. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -0
  161. package/dist/backend/backend/src/services/slack/slack.service.d.ts +116 -1
  162. package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
  163. package/dist/backend/backend/src/services/slack/slack.service.js +297 -3
  164. package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
  165. package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts +152 -7
  166. package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
  167. package/dist/backend/backend/src/services/task-pool/task-pool.service.js +268 -36
  168. package/dist/backend/backend/src/services/task-pool/task-pool.service.js.map +1 -1
  169. package/dist/backend/backend/src/services/v3/escalation-router.service.js +2 -2
  170. package/dist/backend/backend/src/services/v3/escalation-router.service.js.map +1 -1
  171. package/dist/backend/backend/src/services/v3/request-sla.subscriber.d.ts.map +1 -1
  172. package/dist/backend/backend/src/services/v3/request-sla.subscriber.js +39 -2
  173. package/dist/backend/backend/src/services/v3/request-sla.subscriber.js.map +1 -1
  174. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.d.ts +22 -0
  175. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.d.ts.map +1 -1
  176. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js +71 -0
  177. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js.map +1 -1
  178. package/dist/backend/backend/src/services/wiki/wiki-bookkeep-trigger.service.d.ts +20 -2
  179. package/dist/backend/backend/src/services/wiki/wiki-bookkeep-trigger.service.d.ts.map +1 -1
  180. package/dist/backend/backend/src/services/wiki/wiki-bookkeep-trigger.service.js +27 -2
  181. package/dist/backend/backend/src/services/wiki/wiki-bookkeep-trigger.service.js.map +1 -1
  182. package/dist/backend/backend/src/services/wiki/wiki-reflect-trigger.service.d.ts +15 -2
  183. package/dist/backend/backend/src/services/wiki/wiki-reflect-trigger.service.d.ts.map +1 -1
  184. package/dist/backend/backend/src/services/wiki/wiki-reflect-trigger.service.js +33 -7
  185. package/dist/backend/backend/src/services/wiki/wiki-reflect-trigger.service.js.map +1 -1
  186. package/dist/backend/backend/src/services/wiki/wiki-workitem-bridge.service.d.ts +37 -0
  187. package/dist/backend/backend/src/services/wiki/wiki-workitem-bridge.service.d.ts.map +1 -1
  188. package/dist/backend/backend/src/services/wiki/wiki-workitem-bridge.service.js +74 -8
  189. package/dist/backend/backend/src/services/wiki/wiki-workitem-bridge.service.js.map +1 -1
  190. package/dist/backend/backend/src/types/slack.types.d.ts +87 -0
  191. package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
  192. package/dist/backend/backend/src/types/slack.types.js.map +1 -1
  193. package/dist/backend/backend/src/types/v2/index.d.ts +1 -1
  194. package/dist/backend/backend/src/types/v2/index.d.ts.map +1 -1
  195. package/dist/backend/backend/src/types/v2/index.js +1 -1
  196. package/dist/backend/backend/src/types/v2/index.js.map +1 -1
  197. package/dist/backend/backend/src/types/v2/work-item.types.d.ts +181 -1
  198. package/dist/backend/backend/src/types/v2/work-item.types.d.ts.map +1 -1
  199. package/dist/backend/backend/src/types/v2/work-item.types.js +104 -17
  200. package/dist/backend/backend/src/types/v2/work-item.types.js.map +1 -1
  201. package/dist/backend/backend/src/utils/build-provenance.d.ts +150 -0
  202. package/dist/backend/backend/src/utils/build-provenance.d.ts.map +1 -0
  203. package/dist/backend/backend/src/utils/build-provenance.js +211 -0
  204. package/dist/backend/backend/src/utils/build-provenance.js.map +1 -0
  205. package/dist/backend/build-info.json +4 -0
  206. package/dist/backend/config/constants.d.ts +8 -0
  207. package/dist/backend/config/constants.d.ts.map +1 -1
  208. package/dist/backend/config/constants.js +8 -0
  209. package/dist/backend/config/constants.js.map +1 -1
  210. package/dist/backend/config/vitest-exclude.constants.d.ts +53 -0
  211. package/dist/backend/config/vitest-exclude.constants.d.ts.map +1 -0
  212. package/dist/backend/config/vitest-exclude.constants.js +55 -0
  213. package/dist/backend/config/vitest-exclude.constants.js.map +1 -0
  214. package/dist/cli/backend/src/config/oauth.config.d.ts +93 -19
  215. package/dist/cli/backend/src/config/oauth.config.d.ts.map +1 -1
  216. package/dist/cli/backend/src/config/oauth.config.js +121 -19
  217. package/dist/cli/backend/src/config/oauth.config.js.map +1 -1
  218. package/dist/cli/backend/src/constants.d.ts +60 -0
  219. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  220. package/dist/cli/backend/src/constants.js +74 -0
  221. package/dist/cli/backend/src/constants.js.map +1 -1
  222. package/dist/cli/backend/src/services/agent/active-work-briefing.service.d.ts +538 -0
  223. package/dist/cli/backend/src/services/agent/active-work-briefing.service.d.ts.map +1 -0
  224. package/dist/cli/backend/src/services/agent/active-work-briefing.service.js +802 -0
  225. package/dist/cli/backend/src/services/agent/active-work-briefing.service.js.map +1 -0
  226. package/dist/cli/backend/src/services/agent/agent-heartbeat.service.d.ts +240 -0
  227. package/dist/cli/backend/src/services/agent/agent-heartbeat.service.d.ts.map +1 -0
  228. package/dist/cli/backend/src/services/agent/agent-heartbeat.service.js +524 -0
  229. package/dist/cli/backend/src/services/agent/agent-heartbeat.service.js.map +1 -0
  230. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts +629 -0
  231. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -0
  232. package/dist/cli/backend/src/services/agent/agent-registration.service.js +4548 -0
  233. package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -0
  234. package/dist/cli/backend/src/services/agent/agent-suspend.service.d.ts +118 -0
  235. package/dist/cli/backend/src/services/agent/agent-suspend.service.d.ts.map +1 -0
  236. package/dist/cli/backend/src/services/agent/agent-suspend.service.js +304 -0
  237. package/dist/cli/backend/src/services/agent/agent-suspend.service.js.map +1 -0
  238. package/dist/cli/backend/src/services/agent/auditor-scheduler.service.d.ts +196 -0
  239. package/dist/cli/backend/src/services/agent/auditor-scheduler.service.d.ts.map +1 -0
  240. package/dist/cli/backend/src/services/agent/auditor-scheduler.service.js +504 -0
  241. package/dist/cli/backend/src/services/agent/auditor-scheduler.service.js.map +1 -0
  242. package/dist/cli/backend/src/services/agent/claude-runtime.service.d.ts +97 -0
  243. package/dist/cli/backend/src/services/agent/claude-runtime.service.d.ts.map +1 -0
  244. package/dist/cli/backend/src/services/agent/claude-runtime.service.js +334 -0
  245. package/dist/cli/backend/src/services/agent/claude-runtime.service.js.map +1 -0
  246. package/dist/cli/backend/src/services/agent/codex-runtime.service.d.ts +47 -0
  247. package/dist/cli/backend/src/services/agent/codex-runtime.service.d.ts.map +1 -0
  248. package/dist/cli/backend/src/services/agent/codex-runtime.service.js +112 -0
  249. package/dist/cli/backend/src/services/agent/codex-runtime.service.js.map +1 -0
  250. package/dist/cli/backend/src/services/agent/context-window-monitor.service.d.ts +323 -0
  251. package/dist/cli/backend/src/services/agent/context-window-monitor.service.d.ts.map +1 -0
  252. package/dist/cli/backend/src/services/agent/context-window-monitor.service.js +1056 -0
  253. package/dist/cli/backend/src/services/agent/context-window-monitor.service.js.map +1 -0
  254. package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +198 -0
  255. package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -0
  256. package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +816 -0
  257. package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -0
  258. package/dist/cli/backend/src/services/agent/crewly-agent/in-process-log-buffer.d.ts +155 -0
  259. package/dist/cli/backend/src/services/agent/crewly-agent/in-process-log-buffer.d.ts.map +1 -0
  260. package/dist/cli/backend/src/services/agent/crewly-agent/in-process-log-buffer.js +272 -0
  261. package/dist/cli/backend/src/services/agent/crewly-agent/in-process-log-buffer.js.map +1 -0
  262. package/dist/cli/backend/src/services/agent/crewly-agent/in-process-runtime-registry.d.ts +91 -0
  263. package/dist/cli/backend/src/services/agent/crewly-agent/in-process-runtime-registry.d.ts.map +1 -0
  264. package/dist/cli/backend/src/services/agent/crewly-agent/in-process-runtime-registry.js +120 -0
  265. package/dist/cli/backend/src/services/agent/crewly-agent/in-process-runtime-registry.js.map +1 -0
  266. package/dist/cli/backend/src/services/agent/crewly-agent/types.d.ts +536 -0
  267. package/dist/cli/backend/src/services/agent/crewly-agent/types.d.ts.map +1 -0
  268. package/dist/cli/backend/src/services/agent/crewly-agent/types.js +282 -0
  269. package/dist/cli/backend/src/services/agent/crewly-agent/types.js.map +1 -0
  270. package/dist/cli/backend/src/services/agent/disk-cleanup.service.d.ts +100 -0
  271. package/dist/cli/backend/src/services/agent/disk-cleanup.service.d.ts.map +1 -0
  272. package/dist/cli/backend/src/services/agent/disk-cleanup.service.js +149 -0
  273. package/dist/cli/backend/src/services/agent/disk-cleanup.service.js.map +1 -0
  274. package/dist/cli/backend/src/services/agent/gemini-runtime.service.d.ts +130 -0
  275. package/dist/cli/backend/src/services/agent/gemini-runtime.service.d.ts.map +1 -0
  276. package/dist/cli/backend/src/services/agent/gemini-runtime.service.js +758 -0
  277. package/dist/cli/backend/src/services/agent/gemini-runtime.service.js.map +1 -0
  278. package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.d.ts +216 -0
  279. package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.d.ts.map +1 -0
  280. package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.js +508 -0
  281. package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.js.map +1 -0
  282. package/dist/cli/backend/src/services/agent/pty-activity-tracker.service.d.ts +113 -0
  283. package/dist/cli/backend/src/services/agent/pty-activity-tracker.service.d.ts.map +1 -0
  284. package/dist/cli/backend/src/services/agent/pty-activity-tracker.service.js +161 -0
  285. package/dist/cli/backend/src/services/agent/pty-activity-tracker.service.js.map +1 -0
  286. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.d.ts +156 -0
  287. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.d.ts.map +1 -0
  288. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js +641 -0
  289. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -0
  290. package/dist/cli/backend/src/services/agent/runtime-exit-monitor.service.d.ts +295 -0
  291. package/dist/cli/backend/src/services/agent/runtime-exit-monitor.service.d.ts.map +1 -0
  292. package/dist/cli/backend/src/services/agent/runtime-exit-monitor.service.js +1166 -0
  293. package/dist/cli/backend/src/services/agent/runtime-exit-monitor.service.js.map +1 -0
  294. package/dist/cli/backend/src/services/agent/runtime-service.factory.d.ts +71 -0
  295. package/dist/cli/backend/src/services/agent/runtime-service.factory.d.ts.map +1 -0
  296. package/dist/cli/backend/src/services/agent/runtime-service.factory.js +174 -0
  297. package/dist/cli/backend/src/services/agent/runtime-service.factory.js.map +1 -0
  298. package/dist/cli/backend/src/services/ai/prompt-builder.service.d.ts +435 -0
  299. package/dist/cli/backend/src/services/ai/prompt-builder.service.d.ts.map +1 -0
  300. package/dist/cli/backend/src/services/ai/prompt-builder.service.js +1331 -0
  301. package/dist/cli/backend/src/services/ai/prompt-builder.service.js.map +1 -0
  302. package/dist/cli/backend/src/services/ai/prompt-modules/capability-overlay.module.d.ts +33 -0
  303. package/dist/cli/backend/src/services/ai/prompt-modules/capability-overlay.module.d.ts.map +1 -0
  304. package/dist/cli/backend/src/services/ai/prompt-modules/capability-overlay.module.js +70 -0
  305. package/dist/cli/backend/src/services/ai/prompt-modules/capability-overlay.module.js.map +1 -0
  306. package/dist/cli/backend/src/services/ai/prompt-modules/communication.module.d.ts +100 -0
  307. package/dist/cli/backend/src/services/ai/prompt-modules/communication.module.d.ts.map +1 -0
  308. package/dist/cli/backend/src/services/ai/prompt-modules/communication.module.js +296 -0
  309. package/dist/cli/backend/src/services/ai/prompt-modules/communication.module.js.map +1 -0
  310. package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.d.ts +59 -0
  311. package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.d.ts.map +1 -0
  312. package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.js +87 -0
  313. package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.js.map +1 -0
  314. package/dist/cli/backend/src/services/ai/prompt-modules/default-execution-loop.module.d.ts +67 -0
  315. package/dist/cli/backend/src/services/ai/prompt-modules/default-execution-loop.module.d.ts.map +1 -0
  316. package/dist/cli/backend/src/services/ai/prompt-modules/default-execution-loop.module.js +84 -0
  317. package/dist/cli/backend/src/services/ai/prompt-modules/default-execution-loop.module.js.map +1 -0
  318. package/dist/cli/backend/src/services/ai/prompt-modules/domain-sop.module.d.ts +35 -0
  319. package/dist/cli/backend/src/services/ai/prompt-modules/domain-sop.module.d.ts.map +1 -0
  320. package/dist/cli/backend/src/services/ai/prompt-modules/domain-sop.module.js +50 -0
  321. package/dist/cli/backend/src/services/ai/prompt-modules/domain-sop.module.js.map +1 -0
  322. package/dist/cli/backend/src/services/ai/prompt-modules/expert-profile.module.d.ts +41 -0
  323. package/dist/cli/backend/src/services/ai/prompt-modules/expert-profile.module.d.ts.map +1 -0
  324. package/dist/cli/backend/src/services/ai/prompt-modules/expert-profile.module.js +95 -0
  325. package/dist/cli/backend/src/services/ai/prompt-modules/expert-profile.module.js.map +1 -0
  326. package/dist/cli/backend/src/services/ai/prompt-modules/identity.module.d.ts +28 -0
  327. package/dist/cli/backend/src/services/ai/prompt-modules/identity.module.d.ts.map +1 -0
  328. package/dist/cli/backend/src/services/ai/prompt-modules/identity.module.js +47 -0
  329. package/dist/cli/backend/src/services/ai/prompt-modules/identity.module.js.map +1 -0
  330. package/dist/cli/backend/src/services/ai/prompt-modules/lazy-anti-patterns.module.d.ts +65 -0
  331. package/dist/cli/backend/src/services/ai/prompt-modules/lazy-anti-patterns.module.d.ts.map +1 -0
  332. package/dist/cli/backend/src/services/ai/prompt-modules/lazy-anti-patterns.module.js +79 -0
  333. package/dist/cli/backend/src/services/ai/prompt-modules/lazy-anti-patterns.module.js.map +1 -0
  334. package/dist/cli/backend/src/services/ai/prompt-modules/learning-reference.module.d.ts +28 -0
  335. package/dist/cli/backend/src/services/ai/prompt-modules/learning-reference.module.d.ts.map +1 -0
  336. package/dist/cli/backend/src/services/ai/prompt-modules/learning-reference.module.js +55 -0
  337. package/dist/cli/backend/src/services/ai/prompt-modules/learning-reference.module.js.map +1 -0
  338. package/dist/cli/backend/src/services/ai/prompt-modules/lifecycle.module.d.ts +44 -0
  339. package/dist/cli/backend/src/services/ai/prompt-modules/lifecycle.module.d.ts.map +1 -0
  340. package/dist/cli/backend/src/services/ai/prompt-modules/lifecycle.module.js +120 -0
  341. package/dist/cli/backend/src/services/ai/prompt-modules/lifecycle.module.js.map +1 -0
  342. package/dist/cli/backend/src/services/ai/prompt-modules/markdown-file-module.d.ts +46 -0
  343. package/dist/cli/backend/src/services/ai/prompt-modules/markdown-file-module.d.ts.map +1 -0
  344. package/dist/cli/backend/src/services/ai/prompt-modules/markdown-file-module.js +44 -0
  345. package/dist/cli/backend/src/services/ai/prompt-modules/markdown-file-module.js.map +1 -0
  346. package/dist/cli/backend/src/services/ai/prompt-modules/memory-reference.module.d.ts +28 -0
  347. package/dist/cli/backend/src/services/ai/prompt-modules/memory-reference.module.d.ts.map +1 -0
  348. package/dist/cli/backend/src/services/ai/prompt-modules/memory-reference.module.js +46 -0
  349. package/dist/cli/backend/src/services/ai/prompt-modules/memory-reference.module.js.map +1 -0
  350. package/dist/cli/backend/src/services/ai/prompt-modules/mission-context.module.d.ts +60 -0
  351. package/dist/cli/backend/src/services/ai/prompt-modules/mission-context.module.d.ts.map +1 -0
  352. package/dist/cli/backend/src/services/ai/prompt-modules/mission-context.module.js +104 -0
  353. package/dist/cli/backend/src/services/ai/prompt-modules/mission-context.module.js.map +1 -0
  354. package/dist/cli/backend/src/services/ai/prompt-modules/project-reference.module.d.ts +29 -0
  355. package/dist/cli/backend/src/services/ai/prompt-modules/project-reference.module.d.ts.map +1 -0
  356. package/dist/cli/backend/src/services/ai/prompt-modules/project-reference.module.js +50 -0
  357. package/dist/cli/backend/src/services/ai/prompt-modules/project-reference.module.js.map +1 -0
  358. package/dist/cli/backend/src/services/ai/prompt-modules/prompt-assembly.service.d.ts +103 -0
  359. package/dist/cli/backend/src/services/ai/prompt-modules/prompt-assembly.service.d.ts.map +1 -0
  360. package/dist/cli/backend/src/services/ai/prompt-modules/prompt-assembly.service.js +383 -0
  361. package/dist/cli/backend/src/services/ai/prompt-modules/prompt-assembly.service.js.map +1 -0
  362. package/dist/cli/backend/src/services/ai/prompt-modules/recovery.module.d.ts +34 -0
  363. package/dist/cli/backend/src/services/ai/prompt-modules/recovery.module.d.ts.map +1 -0
  364. package/dist/cli/backend/src/services/ai/prompt-modules/recovery.module.js +96 -0
  365. package/dist/cli/backend/src/services/ai/prompt-modules/recovery.module.js.map +1 -0
  366. package/dist/cli/backend/src/services/ai/prompt-modules/request-contract.module.d.ts +106 -0
  367. package/dist/cli/backend/src/services/ai/prompt-modules/request-contract.module.d.ts.map +1 -0
  368. package/dist/cli/backend/src/services/ai/prompt-modules/request-contract.module.js +167 -0
  369. package/dist/cli/backend/src/services/ai/prompt-modules/request-contract.module.js.map +1 -0
  370. package/dist/cli/backend/src/services/ai/prompt-modules/risk-policy.module.d.ts +35 -0
  371. package/dist/cli/backend/src/services/ai/prompt-modules/risk-policy.module.d.ts.map +1 -0
  372. package/dist/cli/backend/src/services/ai/prompt-modules/risk-policy.module.js +50 -0
  373. package/dist/cli/backend/src/services/ai/prompt-modules/risk-policy.module.js.map +1 -0
  374. package/dist/cli/backend/src/services/ai/prompt-modules/role-boundary.module.d.ts +91 -0
  375. package/dist/cli/backend/src/services/ai/prompt-modules/role-boundary.module.d.ts.map +1 -0
  376. package/dist/cli/backend/src/services/ai/prompt-modules/role-boundary.module.js +411 -0
  377. package/dist/cli/backend/src/services/ai/prompt-modules/role-boundary.module.js.map +1 -0
  378. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts +62 -0
  379. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -0
  380. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +199 -0
  381. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -0
  382. package/dist/cli/backend/src/services/ai/prompt-modules/sop-norm-distinction.module.d.ts +79 -0
  383. package/dist/cli/backend/src/services/ai/prompt-modules/sop-norm-distinction.module.d.ts.map +1 -0
  384. package/dist/cli/backend/src/services/ai/prompt-modules/sop-norm-distinction.module.js +118 -0
  385. package/dist/cli/backend/src/services/ai/prompt-modules/sop-norm-distinction.module.js.map +1 -0
  386. package/dist/cli/backend/src/services/ai/prompt-modules/soul.module.d.ts +102 -0
  387. package/dist/cli/backend/src/services/ai/prompt-modules/soul.module.d.ts.map +1 -0
  388. package/dist/cli/backend/src/services/ai/prompt-modules/soul.module.js +229 -0
  389. package/dist/cli/backend/src/services/ai/prompt-modules/soul.module.js.map +1 -0
  390. package/dist/cli/backend/src/services/ai/prompt-modules/team-norms.module.d.ts +61 -0
  391. package/dist/cli/backend/src/services/ai/prompt-modules/team-norms.module.d.ts.map +1 -0
  392. package/dist/cli/backend/src/services/ai/prompt-modules/team-norms.module.js +197 -0
  393. package/dist/cli/backend/src/services/ai/prompt-modules/team-norms.module.js.map +1 -0
  394. package/dist/cli/backend/src/services/ai/prompt-modules/team-reference.module.d.ts +54 -0
  395. package/dist/cli/backend/src/services/ai/prompt-modules/team-reference.module.d.ts.map +1 -0
  396. package/dist/cli/backend/src/services/ai/prompt-modules/team-reference.module.js +149 -0
  397. package/dist/cli/backend/src/services/ai/prompt-modules/team-reference.module.js.map +1 -0
  398. package/dist/cli/backend/src/services/ai/prompt-modules/user-profile-reference.module.d.ts +32 -0
  399. package/dist/cli/backend/src/services/ai/prompt-modules/user-profile-reference.module.d.ts.map +1 -0
  400. package/dist/cli/backend/src/services/ai/prompt-modules/user-profile-reference.module.js +69 -0
  401. package/dist/cli/backend/src/services/ai/prompt-modules/user-profile-reference.module.js.map +1 -0
  402. package/dist/cli/backend/src/services/ai/prompt-modules/working-memory.module.d.ts +91 -0
  403. package/dist/cli/backend/src/services/ai/prompt-modules/working-memory.module.d.ts.map +1 -0
  404. package/dist/cli/backend/src/services/ai/prompt-modules/working-memory.module.js +136 -0
  405. package/dist/cli/backend/src/services/ai/prompt-modules/working-memory.module.js.map +1 -0
  406. package/dist/cli/backend/src/services/ai/self-improvement/growth-areas.service.d.ts +81 -0
  407. package/dist/cli/backend/src/services/ai/self-improvement/growth-areas.service.d.ts.map +1 -0
  408. package/dist/cli/backend/src/services/ai/self-improvement/growth-areas.service.js +135 -0
  409. package/dist/cli/backend/src/services/ai/self-improvement/growth-areas.service.js.map +1 -0
  410. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +292 -0
  411. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -0
  412. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +420 -0
  413. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -0
  414. package/dist/cli/backend/src/services/chat-v2/chat-v2.mention-resolver.d.ts +134 -0
  415. package/dist/cli/backend/src/services/chat-v2/chat-v2.mention-resolver.d.ts.map +1 -0
  416. package/dist/cli/backend/src/services/chat-v2/chat-v2.mention-resolver.js +232 -0
  417. package/dist/cli/backend/src/services/chat-v2/chat-v2.mention-resolver.js.map +1 -0
  418. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts +794 -0
  419. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -0
  420. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js +1322 -0
  421. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js.map +1 -0
  422. package/dist/cli/backend/src/services/chat-v2/chat-v2.singleton.d.ts +27 -0
  423. package/dist/cli/backend/src/services/chat-v2/chat-v2.singleton.d.ts.map +1 -0
  424. package/dist/cli/backend/src/services/chat-v2/chat-v2.singleton.js +57 -0
  425. package/dist/cli/backend/src/services/chat-v2/chat-v2.singleton.js.map +1 -0
  426. package/dist/cli/backend/src/services/chat-v2/config.d.ts +100 -0
  427. package/dist/cli/backend/src/services/chat-v2/config.d.ts.map +1 -0
  428. package/dist/cli/backend/src/services/chat-v2/config.js +174 -0
  429. package/dist/cli/backend/src/services/chat-v2/config.js.map +1 -0
  430. package/dist/cli/backend/src/services/chat-v2/legacy-dto.utils.d.ts +118 -0
  431. package/dist/cli/backend/src/services/chat-v2/legacy-dto.utils.d.ts.map +1 -0
  432. package/dist/cli/backend/src/services/chat-v2/legacy-dto.utils.js +171 -0
  433. package/dist/cli/backend/src/services/chat-v2/legacy-dto.utils.js.map +1 -0
  434. package/dist/cli/backend/src/services/chat-v2/sqlite/channel.store.d.ts +188 -0
  435. package/dist/cli/backend/src/services/chat-v2/sqlite/channel.store.d.ts.map +1 -0
  436. package/dist/cli/backend/src/services/chat-v2/sqlite/channel.store.js +317 -0
  437. package/dist/cli/backend/src/services/chat-v2/sqlite/channel.store.js.map +1 -0
  438. package/dist/cli/backend/src/services/chat-v2/sqlite/chat-db.d.ts +119 -0
  439. package/dist/cli/backend/src/services/chat-v2/sqlite/chat-db.d.ts.map +1 -0
  440. package/dist/cli/backend/src/services/chat-v2/sqlite/chat-db.js +551 -0
  441. package/dist/cli/backend/src/services/chat-v2/sqlite/chat-db.js.map +1 -0
  442. package/dist/cli/backend/src/services/chat-v2/sqlite/message.store.d.ts +229 -0
  443. package/dist/cli/backend/src/services/chat-v2/sqlite/message.store.d.ts.map +1 -0
  444. package/dist/cli/backend/src/services/chat-v2/sqlite/message.store.js +448 -0
  445. package/dist/cli/backend/src/services/chat-v2/sqlite/message.store.js.map +1 -0
  446. package/dist/cli/backend/src/services/chat-v2/types.d.ts +341 -0
  447. package/dist/cli/backend/src/services/chat-v2/types.d.ts.map +1 -0
  448. package/dist/cli/backend/src/services/chat-v2/types.js +61 -0
  449. package/dist/cli/backend/src/services/chat-v2/types.js.map +1 -0
  450. package/dist/cli/backend/src/services/continuation/patterns/idle-patterns.d.ts +37 -0
  451. package/dist/cli/backend/src/services/continuation/patterns/idle-patterns.d.ts.map +1 -0
  452. package/dist/cli/backend/src/services/continuation/patterns/idle-patterns.js +63 -0
  453. package/dist/cli/backend/src/services/continuation/patterns/idle-patterns.js.map +1 -0
  454. package/dist/cli/backend/src/services/core/storage.service.d.ts +6 -0
  455. package/dist/cli/backend/src/services/core/storage.service.d.ts.map +1 -1
  456. package/dist/cli/backend/src/services/core/storage.service.js +1 -1
  457. package/dist/cli/backend/src/services/core/storage.service.js.map +1 -1
  458. package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.d.ts.map +1 -1
  459. package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js +6 -2
  460. package/dist/cli/backend/src/services/credential/helpers/gemini-cli-workspace.helper.js.map +1 -1
  461. package/dist/cli/backend/src/services/intent-task/intent-classifier.rules.d.ts +335 -0
  462. package/dist/cli/backend/src/services/intent-task/intent-classifier.rules.d.ts.map +1 -0
  463. package/dist/cli/backend/src/services/intent-task/intent-classifier.rules.js +421 -0
  464. package/dist/cli/backend/src/services/intent-task/intent-classifier.rules.js.map +1 -0
  465. package/dist/cli/backend/src/services/mcp-client.d.ts +233 -0
  466. package/dist/cli/backend/src/services/mcp-client.d.ts.map +1 -0
  467. package/dist/cli/backend/src/services/mcp-client.js +302 -0
  468. package/dist/cli/backend/src/services/mcp-client.js.map +1 -0
  469. package/dist/cli/backend/src/services/memory/mission-context.service.d.ts +168 -0
  470. package/dist/cli/backend/src/services/memory/mission-context.service.d.ts.map +1 -0
  471. package/dist/cli/backend/src/services/memory/mission-context.service.js +365 -0
  472. package/dist/cli/backend/src/services/memory/mission-context.service.js.map +1 -0
  473. package/dist/cli/backend/src/services/memory/session-memory.service.d.ts +197 -0
  474. package/dist/cli/backend/src/services/memory/session-memory.service.d.ts.map +1 -0
  475. package/dist/cli/backend/src/services/memory/session-memory.service.js +369 -0
  476. package/dist/cli/backend/src/services/memory/session-memory.service.js.map +1 -0
  477. package/dist/cli/backend/src/services/memory/user-profile.service.d.ts +77 -0
  478. package/dist/cli/backend/src/services/memory/user-profile.service.d.ts.map +1 -0
  479. package/dist/cli/backend/src/services/memory/user-profile.service.js +171 -0
  480. package/dist/cli/backend/src/services/memory/user-profile.service.js.map +1 -0
  481. package/dist/cli/backend/src/services/memory/working-memory.service.d.ts +232 -0
  482. package/dist/cli/backend/src/services/memory/working-memory.service.d.ts.map +1 -0
  483. package/dist/cli/backend/src/services/memory/working-memory.service.js +417 -0
  484. package/dist/cli/backend/src/services/memory/working-memory.service.js.map +1 -0
  485. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +84 -0
  486. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -0
  487. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js +136 -0
  488. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -0
  489. package/dist/cli/backend/src/services/messaging/thread-status-queue.service.d.ts +358 -0
  490. package/dist/cli/backend/src/services/messaging/thread-status-queue.service.d.ts.map +1 -0
  491. package/dist/cli/backend/src/services/messaging/thread-status-queue.service.js +755 -0
  492. package/dist/cli/backend/src/services/messaging/thread-status-queue.service.js.map +1 -0
  493. package/dist/cli/backend/src/services/monitoring/token-usage.service.d.ts +269 -0
  494. package/dist/cli/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -0
  495. package/dist/cli/backend/src/services/monitoring/token-usage.service.js +391 -0
  496. package/dist/cli/backend/src/services/monitoring/token-usage.service.js.map +1 -0
  497. package/dist/cli/backend/src/services/observability/agent-behavior-log.service.d.ts +132 -0
  498. package/dist/cli/backend/src/services/observability/agent-behavior-log.service.d.ts.map +1 -0
  499. package/dist/cli/backend/src/services/observability/agent-behavior-log.service.js +284 -0
  500. package/dist/cli/backend/src/services/observability/agent-behavior-log.service.js.map +1 -0
  501. package/dist/cli/backend/src/services/observability/agent-behavior-log.singleton.d.ts +70 -0
  502. package/dist/cli/backend/src/services/observability/agent-behavior-log.singleton.d.ts.map +1 -0
  503. package/dist/cli/backend/src/services/observability/agent-behavior-log.singleton.js +121 -0
  504. package/dist/cli/backend/src/services/observability/agent-behavior-log.singleton.js.map +1 -0
  505. package/dist/cli/backend/src/services/observability/agent-behavior-log.types.d.ts +130 -0
  506. package/dist/cli/backend/src/services/observability/agent-behavior-log.types.d.ts.map +1 -0
  507. package/dist/cli/backend/src/services/observability/agent-behavior-log.types.js +48 -0
  508. package/dist/cli/backend/src/services/observability/agent-behavior-log.types.js.map +1 -0
  509. package/dist/cli/backend/src/services/observability/observability-db.d.ts +84 -0
  510. package/dist/cli/backend/src/services/observability/observability-db.d.ts.map +1 -0
  511. package/dist/cli/backend/src/services/observability/observability-db.js +165 -0
  512. package/dist/cli/backend/src/services/observability/observability-db.js.map +1 -0
  513. package/dist/cli/backend/src/services/onboarding/content-approval.service.d.ts +124 -0
  514. package/dist/cli/backend/src/services/onboarding/content-approval.service.d.ts.map +1 -0
  515. package/dist/cli/backend/src/services/onboarding/content-approval.service.js +256 -0
  516. package/dist/cli/backend/src/services/onboarding/content-approval.service.js.map +1 -0
  517. package/dist/cli/backend/src/services/onboarding/content-approval.types.d.ts +80 -0
  518. package/dist/cli/backend/src/services/onboarding/content-approval.types.d.ts.map +1 -0
  519. package/dist/cli/backend/src/services/onboarding/content-approval.types.js +16 -0
  520. package/dist/cli/backend/src/services/onboarding/content-approval.types.js.map +1 -0
  521. package/dist/cli/backend/src/services/orc/orc-delivery-enforcer.service.d.ts +150 -0
  522. package/dist/cli/backend/src/services/orc/orc-delivery-enforcer.service.d.ts.map +1 -0
  523. package/dist/cli/backend/src/services/orc/orc-delivery-enforcer.service.js +293 -0
  524. package/dist/cli/backend/src/services/orc/orc-delivery-enforcer.service.js.map +1 -0
  525. package/dist/cli/backend/src/services/orchestrator/improvement-marker.service.d.ts +231 -0
  526. package/dist/cli/backend/src/services/orchestrator/improvement-marker.service.d.ts.map +1 -0
  527. package/dist/cli/backend/src/services/orchestrator/improvement-marker.service.js +353 -0
  528. package/dist/cli/backend/src/services/orchestrator/improvement-marker.service.js.map +1 -0
  529. package/dist/cli/backend/src/services/orchestrator/improvement-startup.service.d.ts +157 -0
  530. package/dist/cli/backend/src/services/orchestrator/improvement-startup.service.d.ts.map +1 -0
  531. package/dist/cli/backend/src/services/orchestrator/improvement-startup.service.js +483 -0
  532. package/dist/cli/backend/src/services/orchestrator/improvement-startup.service.js.map +1 -0
  533. package/dist/cli/backend/src/services/orchestrator/index.d.ts +16 -0
  534. package/dist/cli/backend/src/services/orchestrator/index.d.ts.map +1 -0
  535. package/dist/cli/backend/src/services/orchestrator/index.js +16 -0
  536. package/dist/cli/backend/src/services/orchestrator/index.js.map +1 -0
  537. package/dist/cli/backend/src/services/orchestrator/orchestrator-restart.service.d.ts +117 -0
  538. package/dist/cli/backend/src/services/orchestrator/orchestrator-restart.service.d.ts.map +1 -0
  539. package/dist/cli/backend/src/services/orchestrator/orchestrator-restart.service.js +300 -0
  540. package/dist/cli/backend/src/services/orchestrator/orchestrator-restart.service.js.map +1 -0
  541. package/dist/cli/backend/src/services/orchestrator/orchestrator-setup.service.d.ts +114 -0
  542. package/dist/cli/backend/src/services/orchestrator/orchestrator-setup.service.d.ts.map +1 -0
  543. package/dist/cli/backend/src/services/orchestrator/orchestrator-setup.service.js +195 -0
  544. package/dist/cli/backend/src/services/orchestrator/orchestrator-setup.service.js.map +1 -0
  545. package/dist/cli/backend/src/services/orchestrator/orchestrator-status.service.d.ts +94 -0
  546. package/dist/cli/backend/src/services/orchestrator/orchestrator-status.service.d.ts.map +1 -0
  547. package/dist/cli/backend/src/services/orchestrator/orchestrator-status.service.js +279 -0
  548. package/dist/cli/backend/src/services/orchestrator/orchestrator-status.service.js.map +1 -0
  549. package/dist/cli/backend/src/services/orchestrator/safe-restart.service.d.ts +192 -0
  550. package/dist/cli/backend/src/services/orchestrator/safe-restart.service.d.ts.map +1 -0
  551. package/dist/cli/backend/src/services/orchestrator/safe-restart.service.js +395 -0
  552. package/dist/cli/backend/src/services/orchestrator/safe-restart.service.js.map +1 -0
  553. package/dist/cli/backend/src/services/orchestrator/self-improvement.service.d.ts +201 -0
  554. package/dist/cli/backend/src/services/orchestrator/self-improvement.service.d.ts.map +1 -0
  555. package/dist/cli/backend/src/services/orchestrator/self-improvement.service.js +396 -0
  556. package/dist/cli/backend/src/services/orchestrator/self-improvement.service.js.map +1 -0
  557. package/dist/cli/backend/src/services/orchestrator/slack-bridge-lazy.d.ts +17 -0
  558. package/dist/cli/backend/src/services/orchestrator/slack-bridge-lazy.d.ts.map +1 -0
  559. package/dist/cli/backend/src/services/orchestrator/slack-bridge-lazy.js +30 -0
  560. package/dist/cli/backend/src/services/orchestrator/slack-bridge-lazy.js.map +1 -0
  561. package/dist/cli/backend/src/services/orchestrator/state-persistence.service.d.ts +249 -0
  562. package/dist/cli/backend/src/services/orchestrator/state-persistence.service.d.ts.map +1 -0
  563. package/dist/cli/backend/src/services/orchestrator/state-persistence.service.js +569 -0
  564. package/dist/cli/backend/src/services/orchestrator/state-persistence.service.js.map +1 -0
  565. package/dist/cli/backend/src/services/session/index.d.ts +47 -0
  566. package/dist/cli/backend/src/services/session/index.d.ts.map +1 -0
  567. package/dist/cli/backend/src/services/session/index.js +50 -0
  568. package/dist/cli/backend/src/services/session/index.js.map +1 -0
  569. package/dist/cli/backend/src/services/session/log-rotation.service.d.ts +163 -0
  570. package/dist/cli/backend/src/services/session/log-rotation.service.d.ts.map +1 -0
  571. package/dist/cli/backend/src/services/session/log-rotation.service.js +362 -0
  572. package/dist/cli/backend/src/services/session/log-rotation.service.js.map +1 -0
  573. package/dist/cli/backend/src/services/session/pty/index.d.ts +11 -0
  574. package/dist/cli/backend/src/services/session/pty/index.d.ts.map +1 -0
  575. package/dist/cli/backend/src/services/session/pty/index.js +11 -0
  576. package/dist/cli/backend/src/services/session/pty/index.js.map +1 -0
  577. package/dist/cli/backend/src/services/session/pty/pty-session-backend.d.ts +445 -0
  578. package/dist/cli/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -0
  579. package/dist/cli/backend/src/services/session/pty/pty-session-backend.js +804 -0
  580. package/dist/cli/backend/src/services/session/pty/pty-session-backend.js.map +1 -0
  581. package/dist/cli/backend/src/services/session/pty/pty-session.d.ts +252 -0
  582. package/dist/cli/backend/src/services/session/pty/pty-session.d.ts.map +1 -0
  583. package/dist/cli/backend/src/services/session/pty/pty-session.js +611 -0
  584. package/dist/cli/backend/src/services/session/pty/pty-session.js.map +1 -0
  585. package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.d.ts +275 -0
  586. package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.d.ts.map +1 -0
  587. package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.js +403 -0
  588. package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.js.map +1 -0
  589. package/dist/cli/backend/src/services/session/runtime-pid-registry.service.d.ts +86 -0
  590. package/dist/cli/backend/src/services/session/runtime-pid-registry.service.d.ts.map +1 -0
  591. package/dist/cli/backend/src/services/session/runtime-pid-registry.service.js +214 -0
  592. package/dist/cli/backend/src/services/session/runtime-pid-registry.service.js.map +1 -0
  593. package/dist/cli/backend/src/services/session/session-backend.factory.d.ts +127 -0
  594. package/dist/cli/backend/src/services/session/session-backend.factory.d.ts.map +1 -0
  595. package/dist/cli/backend/src/services/session/session-backend.factory.js +226 -0
  596. package/dist/cli/backend/src/services/session/session-backend.factory.js.map +1 -0
  597. package/dist/cli/backend/src/services/session/session-backend.interface.d.ts +390 -0
  598. package/dist/cli/backend/src/services/session/session-backend.interface.d.ts.map +1 -0
  599. package/dist/cli/backend/src/services/session/session-backend.interface.js +19 -0
  600. package/dist/cli/backend/src/services/session/session-backend.interface.js.map +1 -0
  601. package/dist/cli/backend/src/services/session/session-command-helper.d.ts +350 -0
  602. package/dist/cli/backend/src/services/session/session-command-helper.d.ts.map +1 -0
  603. package/dist/cli/backend/src/services/session/session-command-helper.js +810 -0
  604. package/dist/cli/backend/src/services/session/session-command-helper.js.map +1 -0
  605. package/dist/cli/backend/src/services/session/session-handoff.service.d.ts +307 -0
  606. package/dist/cli/backend/src/services/session/session-handoff.service.d.ts.map +1 -0
  607. package/dist/cli/backend/src/services/session/session-handoff.service.js +742 -0
  608. package/dist/cli/backend/src/services/session/session-handoff.service.js.map +1 -0
  609. package/dist/cli/backend/src/services/session/session-state-persistence.d.ts +231 -0
  610. package/dist/cli/backend/src/services/session/session-state-persistence.d.ts.map +1 -0
  611. package/dist/cli/backend/src/services/session/session-state-persistence.js +407 -0
  612. package/dist/cli/backend/src/services/session/session-state-persistence.js.map +1 -0
  613. package/dist/cli/backend/src/services/settings/role.service.d.ts +277 -0
  614. package/dist/cli/backend/src/services/settings/role.service.d.ts.map +1 -0
  615. package/dist/cli/backend/src/services/settings/role.service.js +728 -0
  616. package/dist/cli/backend/src/services/settings/role.service.js.map +1 -0
  617. package/dist/cli/backend/src/services/slack/cross-machine-message.service.d.ts +152 -0
  618. package/dist/cli/backend/src/services/slack/cross-machine-message.service.d.ts.map +1 -0
  619. package/dist/cli/backend/src/services/slack/cross-machine-message.service.js +354 -0
  620. package/dist/cli/backend/src/services/slack/cross-machine-message.service.js.map +1 -0
  621. package/dist/cli/backend/src/services/slack/slack-agent-identity.service.d.ts +209 -0
  622. package/dist/cli/backend/src/services/slack/slack-agent-identity.service.d.ts.map +1 -0
  623. package/dist/cli/backend/src/services/slack/slack-agent-identity.service.js +416 -0
  624. package/dist/cli/backend/src/services/slack/slack-agent-identity.service.js.map +1 -0
  625. package/dist/cli/backend/src/services/slack/slack-image.service.d.ts +114 -0
  626. package/dist/cli/backend/src/services/slack/slack-image.service.d.ts.map +1 -0
  627. package/dist/cli/backend/src/services/slack/slack-image.service.js +331 -0
  628. package/dist/cli/backend/src/services/slack/slack-image.service.js.map +1 -0
  629. package/dist/cli/backend/src/services/slack/slack-mention-resolver.d.ts +103 -0
  630. package/dist/cli/backend/src/services/slack/slack-mention-resolver.d.ts.map +1 -0
  631. package/dist/cli/backend/src/services/slack/slack-mention-resolver.js +197 -0
  632. package/dist/cli/backend/src/services/slack/slack-mention-resolver.js.map +1 -0
  633. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts +499 -0
  634. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -0
  635. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +1946 -0
  636. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -0
  637. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +329 -0
  638. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -0
  639. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +821 -0
  640. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -0
  641. package/dist/cli/backend/src/services/slack/slack.service.d.ts +430 -0
  642. package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -0
  643. package/dist/cli/backend/src/services/slack/slack.service.js +1388 -0
  644. package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -0
  645. package/dist/cli/backend/src/services/sop/sop.service.d.ts +364 -0
  646. package/dist/cli/backend/src/services/sop/sop.service.d.ts.map +1 -0
  647. package/dist/cli/backend/src/services/sop/sop.service.js +734 -0
  648. package/dist/cli/backend/src/services/sop/sop.service.js.map +1 -0
  649. package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts +152 -7
  650. package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
  651. package/dist/cli/backend/src/services/task-pool/task-pool.service.js +268 -36
  652. package/dist/cli/backend/src/services/task-pool/task-pool.service.js.map +1 -1
  653. package/dist/cli/backend/src/services/v3/escalation-router.service.d.ts +178 -0
  654. package/dist/cli/backend/src/services/v3/escalation-router.service.d.ts.map +1 -0
  655. package/dist/cli/backend/src/services/v3/escalation-router.service.js +692 -0
  656. package/dist/cli/backend/src/services/v3/escalation-router.service.js.map +1 -0
  657. package/dist/cli/backend/src/services/v3/kr-tracking.service.d.ts +200 -0
  658. package/dist/cli/backend/src/services/v3/kr-tracking.service.d.ts.map +1 -0
  659. package/dist/cli/backend/src/services/v3/kr-tracking.service.js +482 -0
  660. package/dist/cli/backend/src/services/v3/kr-tracking.service.js.map +1 -0
  661. package/dist/cli/backend/src/services/v3/mission-executor.service.d.ts +117 -0
  662. package/dist/cli/backend/src/services/v3/mission-executor.service.d.ts.map +1 -0
  663. package/dist/cli/backend/src/services/v3/mission-executor.service.js +321 -0
  664. package/dist/cli/backend/src/services/v3/mission-executor.service.js.map +1 -0
  665. package/dist/cli/backend/src/services/v3/request-sla.subscriber.d.ts +610 -0
  666. package/dist/cli/backend/src/services/v3/request-sla.subscriber.d.ts.map +1 -0
  667. package/dist/cli/backend/src/services/v3/request-sla.subscriber.js +1529 -0
  668. package/dist/cli/backend/src/services/v3/request-sla.subscriber.js.map +1 -0
  669. package/dist/cli/backend/src/services/v3/request-tracker.service.d.ts +78 -0
  670. package/dist/cli/backend/src/services/v3/request-tracker.service.d.ts.map +1 -0
  671. package/dist/cli/backend/src/services/v3/request-tracker.service.js +108 -0
  672. package/dist/cli/backend/src/services/v3/request-tracker.service.js.map +1 -0
  673. package/dist/cli/backend/src/services/v3/request.service.d.ts +295 -0
  674. package/dist/cli/backend/src/services/v3/request.service.d.ts.map +1 -0
  675. package/dist/cli/backend/src/services/v3/request.service.js +534 -0
  676. package/dist/cli/backend/src/services/v3/request.service.js.map +1 -0
  677. package/dist/cli/backend/src/services/v3/v3-data.service.d.ts +340 -0
  678. package/dist/cli/backend/src/services/v3/v3-data.service.d.ts.map +1 -0
  679. package/dist/cli/backend/src/services/v3/v3-data.service.js +1113 -0
  680. package/dist/cli/backend/src/services/v3/v3-data.service.js.map +1 -0
  681. package/dist/cli/backend/src/types/cross-machine.types.d.ts +103 -0
  682. package/dist/cli/backend/src/types/cross-machine.types.d.ts.map +1 -0
  683. package/dist/cli/backend/src/types/cross-machine.types.js +47 -0
  684. package/dist/cli/backend/src/types/cross-machine.types.js.map +1 -0
  685. package/dist/cli/backend/src/types/intent-task.types.d.ts +402 -0
  686. package/dist/cli/backend/src/types/intent-task.types.d.ts.map +1 -0
  687. package/dist/cli/backend/src/types/intent-task.types.js +403 -0
  688. package/dist/cli/backend/src/types/intent-task.types.js.map +1 -0
  689. package/dist/cli/backend/src/types/orchestrator-state.types.d.ts +531 -0
  690. package/dist/cli/backend/src/types/orchestrator-state.types.d.ts.map +1 -0
  691. package/dist/cli/backend/src/types/orchestrator-state.types.js +110 -0
  692. package/dist/cli/backend/src/types/orchestrator-state.types.js.map +1 -0
  693. package/dist/cli/backend/src/types/slack.types.d.ts +87 -0
  694. package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
  695. package/dist/cli/backend/src/types/slack.types.js.map +1 -1
  696. package/dist/cli/backend/src/types/v2/index.d.ts +30 -0
  697. package/dist/cli/backend/src/types/v2/index.d.ts.map +1 -0
  698. package/dist/cli/backend/src/types/v2/index.js +22 -0
  699. package/dist/cli/backend/src/types/v2/index.js.map +1 -0
  700. package/dist/cli/backend/src/types/v2/key-result.types.d.ts +279 -0
  701. package/dist/cli/backend/src/types/v2/key-result.types.d.ts.map +1 -0
  702. package/dist/cli/backend/src/types/v2/key-result.types.js +264 -0
  703. package/dist/cli/backend/src/types/v2/key-result.types.js.map +1 -0
  704. package/dist/cli/backend/src/types/v2/mission.types.d.ts +661 -0
  705. package/dist/cli/backend/src/types/v2/mission.types.d.ts.map +1 -0
  706. package/dist/cli/backend/src/types/v2/mission.types.js +713 -0
  707. package/dist/cli/backend/src/types/v2/mission.types.js.map +1 -0
  708. package/dist/cli/backend/src/types/v2/reconcile.types.d.ts +204 -0
  709. package/dist/cli/backend/src/types/v2/reconcile.types.d.ts.map +1 -0
  710. package/dist/cli/backend/src/types/v2/reconcile.types.js +121 -0
  711. package/dist/cli/backend/src/types/v2/reconcile.types.js.map +1 -0
  712. package/dist/cli/backend/src/types/v2/request.types.d.ts +192 -0
  713. package/dist/cli/backend/src/types/v2/request.types.d.ts.map +1 -0
  714. package/dist/cli/backend/src/types/v2/request.types.js +179 -0
  715. package/dist/cli/backend/src/types/v2/request.types.js.map +1 -0
  716. package/dist/cli/backend/src/types/v2/work-item.types.d.ts +181 -1
  717. package/dist/cli/backend/src/types/v2/work-item.types.d.ts.map +1 -1
  718. package/dist/cli/backend/src/types/v2/work-item.types.js +104 -17
  719. package/dist/cli/backend/src/types/v2/work-item.types.js.map +1 -1
  720. package/dist/cli/backend/src/types/v2/workspace.types.d.ts +157 -0
  721. package/dist/cli/backend/src/types/v2/workspace.types.d.ts.map +1 -0
  722. package/dist/cli/backend/src/types/v2/workspace.types.js +173 -0
  723. package/dist/cli/backend/src/types/v2/workspace.types.js.map +1 -0
  724. package/dist/cli/backend/src/utils/async.utils.d.ts +68 -0
  725. package/dist/cli/backend/src/utils/async.utils.d.ts.map +1 -0
  726. package/dist/cli/backend/src/utils/async.utils.js +80 -0
  727. package/dist/cli/backend/src/utils/async.utils.js.map +1 -0
  728. package/dist/cli/backend/src/utils/native-binding.utils.d.ts +128 -0
  729. package/dist/cli/backend/src/utils/native-binding.utils.d.ts.map +1 -0
  730. package/dist/cli/backend/src/utils/native-binding.utils.js +206 -0
  731. package/dist/cli/backend/src/utils/native-binding.utils.js.map +1 -0
  732. package/dist/cli/backend/src/utils/team.utils.d.ts +40 -0
  733. package/dist/cli/backend/src/utils/team.utils.d.ts.map +1 -0
  734. package/dist/cli/backend/src/utils/team.utils.js +66 -0
  735. package/dist/cli/backend/src/utils/team.utils.js.map +1 -0
  736. package/dist/cli/backend/src/websocket/chat.gateway.d.ts +142 -0
  737. package/dist/cli/backend/src/websocket/chat.gateway.d.ts.map +1 -0
  738. package/dist/cli/backend/src/websocket/chat.gateway.js +327 -0
  739. package/dist/cli/backend/src/websocket/chat.gateway.js.map +1 -0
  740. package/dist/cli/backend/src/websocket/terminal.gateway.d.ts +341 -0
  741. package/dist/cli/backend/src/websocket/terminal.gateway.d.ts.map +1 -0
  742. package/dist/cli/backend/src/websocket/terminal.gateway.js +933 -0
  743. package/dist/cli/backend/src/websocket/terminal.gateway.js.map +1 -0
  744. package/dist/cli/cli/src/index.js +0 -0
  745. package/dist/cli/config/constants.d.ts +8 -0
  746. package/dist/cli/config/constants.d.ts.map +1 -1
  747. package/dist/cli/config/constants.js +8 -0
  748. package/dist/cli/config/constants.js.map +1 -1
  749. package/dist/cli/config/vitest-exclude.constants.d.ts +53 -0
  750. package/dist/cli/config/vitest-exclude.constants.d.ts.map +1 -0
  751. package/dist/cli/config/vitest-exclude.constants.js +55 -0
  752. package/dist/cli/config/vitest-exclude.constants.js.map +1 -0
  753. package/frontend/dist/assets/index-190a957f.js +4961 -0
  754. package/frontend/dist/assets/{index-8205ea5e.css → index-88feb412.css} +1 -1
  755. package/frontend/dist/index.html +2 -2
  756. package/package.json +2 -2
  757. package/packages/crewly-agent/src/runtime/agent-runner.service.test.ts +174 -30
  758. package/packages/crewly-agent/src/runtime/agent-runner.service.ts +121 -6
  759. package/packages/crewly-agent/src/runtime/types.ts +56 -5
  760. package/dist/backend/backend/src/controllers/knowledge/index.d.ts +0 -7
  761. package/dist/backend/backend/src/controllers/knowledge/index.d.ts.map +0 -1
  762. package/dist/backend/backend/src/controllers/knowledge/index.js +0 -7
  763. package/dist/backend/backend/src/controllers/knowledge/index.js.map +0 -1
  764. package/dist/backend/backend/src/controllers/knowledge/knowledge.controller.d.ts +0 -71
  765. package/dist/backend/backend/src/controllers/knowledge/knowledge.controller.d.ts.map +0 -1
  766. package/dist/backend/backend/src/controllers/knowledge/knowledge.controller.js +0 -237
  767. package/dist/backend/backend/src/controllers/knowledge/knowledge.controller.js.map +0 -1
  768. package/dist/backend/backend/src/controllers/knowledge/knowledge.routes.d.ts +0 -24
  769. package/dist/backend/backend/src/controllers/knowledge/knowledge.routes.d.ts.map +0 -1
  770. package/dist/backend/backend/src/controllers/knowledge/knowledge.routes.js +0 -34
  771. package/dist/backend/backend/src/controllers/knowledge/knowledge.routes.js.map +0 -1
  772. package/dist/backend/backend/src/controllers/scheduler/unified-scheduler.controller.d.ts +0 -91
  773. package/dist/backend/backend/src/controllers/scheduler/unified-scheduler.controller.d.ts.map +0 -1
  774. package/dist/backend/backend/src/controllers/scheduler/unified-scheduler.controller.js +0 -290
  775. package/dist/backend/backend/src/controllers/scheduler/unified-scheduler.controller.js.map +0 -1
  776. package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts +0 -169
  777. package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts.map +0 -1
  778. package/dist/backend/backend/src/controllers/task-management/task-management.controller.js +0 -1779
  779. package/dist/backend/backend/src/controllers/task-management/task-management.controller.js.map +0 -1
  780. package/dist/backend/backend/src/index.js.orc-bak-20260529 +0 -3130
  781. package/dist/backend/backend/src/routes/modules/unified-scheduler.routes.d.ts +0 -27
  782. package/dist/backend/backend/src/routes/modules/unified-scheduler.routes.d.ts.map +0 -1
  783. package/dist/backend/backend/src/routes/modules/unified-scheduler.routes.js +0 -40
  784. package/dist/backend/backend/src/routes/modules/unified-scheduler.routes.js.map +0 -1
  785. package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.d.ts +0 -513
  786. package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.d.ts.map +0 -1
  787. package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.js +0 -1568
  788. package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.js.map +0 -1
  789. package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.d.ts +0 -86
  790. package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.d.ts.map +0 -1
  791. package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.js +0 -147
  792. package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.js.map +0 -1
  793. package/dist/backend/backend/src/services/agent/crewly-agent/api-client.d.ts +0 -68
  794. package/dist/backend/backend/src/services/agent/crewly-agent/api-client.d.ts.map +0 -1
  795. package/dist/backend/backend/src/services/agent/crewly-agent/api-client.js +0 -131
  796. package/dist/backend/backend/src/services/agent/crewly-agent/api-client.js.map +0 -1
  797. package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.d.ts +0 -130
  798. package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.d.ts.map +0 -1
  799. package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.js +0 -263
  800. package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.js.map +0 -1
  801. package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.d.ts +0 -74
  802. package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.d.ts.map +0 -1
  803. package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.js +0 -140
  804. package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.js.map +0 -1
  805. package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.d.ts +0 -29
  806. package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.d.ts.map +0 -1
  807. package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.js +0 -279
  808. package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.js.map +0 -1
  809. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.d.ts +0 -340
  810. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.d.ts.map +0 -1
  811. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.js +0 -1176
  812. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.js.map +0 -1
  813. package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.d.ts +0 -79
  814. package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.d.ts.map +0 -1
  815. package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.js +0 -145
  816. package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.js.map +0 -1
  817. package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.d.ts +0 -79
  818. package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.d.ts.map +0 -1
  819. package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.js +0 -218
  820. package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.js.map +0 -1
  821. package/dist/backend/backend/src/services/agent/crewly-agent/index.d.ts +0 -16
  822. package/dist/backend/backend/src/services/agent/crewly-agent/index.d.ts.map +0 -1
  823. package/dist/backend/backend/src/services/agent/crewly-agent/index.js +0 -16
  824. package/dist/backend/backend/src/services/agent/crewly-agent/index.js.map +0 -1
  825. package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.d.ts +0 -135
  826. package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.d.ts.map +0 -1
  827. package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.js +0 -185
  828. package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.js.map +0 -1
  829. package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.d.ts +0 -141
  830. package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.d.ts.map +0 -1
  831. package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.js +0 -310
  832. package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.js.map +0 -1
  833. package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.d.ts +0 -91
  834. package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.d.ts.map +0 -1
  835. package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.js +0 -143
  836. package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.js.map +0 -1
  837. package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.d.ts +0 -103
  838. package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.d.ts.map +0 -1
  839. package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.js +0 -256
  840. package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.js.map +0 -1
  841. package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.d.ts +0 -143
  842. package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.d.ts.map +0 -1
  843. package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.js +0 -264
  844. package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.js.map +0 -1
  845. package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.d.ts +0 -13
  846. package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.d.ts.map +0 -1
  847. package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.js +0 -91
  848. package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.js.map +0 -1
  849. package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.d.ts +0 -135
  850. package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.d.ts.map +0 -1
  851. package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.js +0 -1937
  852. package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.js.map +0 -1
  853. package/dist/backend/backend/src/services/autonomous/auto-assign.service.d.ts +0 -429
  854. package/dist/backend/backend/src/services/autonomous/auto-assign.service.d.ts.map +0 -1
  855. package/dist/backend/backend/src/services/autonomous/auto-assign.service.js +0 -852
  856. package/dist/backend/backend/src/services/autonomous/auto-assign.service.js.map +0 -1
  857. package/dist/backend/backend/src/services/knowledge/embedding-provider.d.ts +0 -78
  858. package/dist/backend/backend/src/services/knowledge/embedding-provider.d.ts.map +0 -1
  859. package/dist/backend/backend/src/services/knowledge/embedding-provider.js +0 -179
  860. package/dist/backend/backend/src/services/knowledge/embedding-provider.js.map +0 -1
  861. package/dist/backend/backend/src/services/knowledge/fts5-index.service.d.ts +0 -118
  862. package/dist/backend/backend/src/services/knowledge/fts5-index.service.d.ts.map +0 -1
  863. package/dist/backend/backend/src/services/knowledge/fts5-index.service.js +0 -241
  864. package/dist/backend/backend/src/services/knowledge/fts5-index.service.js.map +0 -1
  865. package/dist/backend/backend/src/services/knowledge/fts5-query-sanitizer.d.ts +0 -102
  866. package/dist/backend/backend/src/services/knowledge/fts5-query-sanitizer.d.ts.map +0 -1
  867. package/dist/backend/backend/src/services/knowledge/fts5-query-sanitizer.js +0 -118
  868. package/dist/backend/backend/src/services/knowledge/fts5-query-sanitizer.js.map +0 -1
  869. package/dist/backend/backend/src/services/knowledge/index.d.ts +0 -8
  870. package/dist/backend/backend/src/services/knowledge/index.d.ts.map +0 -1
  871. package/dist/backend/backend/src/services/knowledge/index.js +0 -8
  872. package/dist/backend/backend/src/services/knowledge/index.js.map +0 -1
  873. package/dist/backend/backend/src/services/knowledge/knowledge-search.service.d.ts +0 -241
  874. package/dist/backend/backend/src/services/knowledge/knowledge-search.service.d.ts.map +0 -1
  875. package/dist/backend/backend/src/services/knowledge/knowledge-search.service.js +0 -590
  876. package/dist/backend/backend/src/services/knowledge/knowledge-search.service.js.map +0 -1
  877. package/dist/backend/backend/src/services/knowledge/knowledge.service.d.ts +0 -153
  878. package/dist/backend/backend/src/services/knowledge/knowledge.service.d.ts.map +0 -1
  879. package/dist/backend/backend/src/services/knowledge/knowledge.service.js +0 -409
  880. package/dist/backend/backend/src/services/knowledge/knowledge.service.js.map +0 -1
  881. package/dist/backend/backend/src/services/knowledge/vector-store.service.d.ts +0 -331
  882. package/dist/backend/backend/src/services/knowledge/vector-store.service.d.ts.map +0 -1
  883. package/dist/backend/backend/src/services/knowledge/vector-store.service.js +0 -830
  884. package/dist/backend/backend/src/services/knowledge/vector-store.service.js.map +0 -1
  885. package/dist/backend/backend/src/services/knowledge/wiki-compiler.service.d.ts +0 -115
  886. package/dist/backend/backend/src/services/knowledge/wiki-compiler.service.d.ts.map +0 -1
  887. package/dist/backend/backend/src/services/knowledge/wiki-compiler.service.js +0 -215
  888. package/dist/backend/backend/src/services/knowledge/wiki-compiler.service.js.map +0 -1
  889. package/dist/backend/backend/src/services/memory/embedding-provider.d.ts +0 -78
  890. package/dist/backend/backend/src/services/memory/embedding-provider.d.ts.map +0 -1
  891. package/dist/backend/backend/src/services/memory/embedding-provider.js +0 -185
  892. package/dist/backend/backend/src/services/memory/embedding-provider.js.map +0 -1
  893. package/dist/backend/backend/src/services/memory/vector-store.service.d.ts +0 -331
  894. package/dist/backend/backend/src/services/memory/vector-store.service.d.ts.map +0 -1
  895. package/dist/backend/backend/src/services/memory/vector-store.service.js +0 -825
  896. package/dist/backend/backend/src/services/memory/vector-store.service.js.map +0 -1
  897. package/dist/backend/backend/src/services/project/task-tracking.service.d.ts +0 -171
  898. package/dist/backend/backend/src/services/project/task-tracking.service.d.ts.map +0 -1
  899. package/dist/backend/backend/src/services/project/task-tracking.service.js +0 -725
  900. package/dist/backend/backend/src/services/project/task-tracking.service.js.map +0 -1
  901. package/dist/backend/backend/src/services/v3/project-task-watcher.service.d.ts +0 -118
  902. package/dist/backend/backend/src/services/v3/project-task-watcher.service.d.ts.map +0 -1
  903. package/dist/backend/backend/src/services/v3/project-task-watcher.service.js +0 -326
  904. package/dist/backend/backend/src/services/v3/project-task-watcher.service.js.map +0 -1
  905. package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.d.ts +0 -74
  906. package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.d.ts.map +0 -1
  907. package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.js +0 -154
  908. package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.js.map +0 -1
  909. package/dist/backend/backend/src/services/workflow/unified-scheduler.service.d.ts +0 -234
  910. package/dist/backend/backend/src/services/workflow/unified-scheduler.service.d.ts.map +0 -1
  911. package/dist/backend/backend/src/services/workflow/unified-scheduler.service.js +0 -636
  912. package/dist/backend/backend/src/services/workflow/unified-scheduler.service.js.map +0 -1
  913. package/dist/backend/backend/src/services/workflow/unified-scheduler.types.d.ts +0 -156
  914. package/dist/backend/backend/src/services/workflow/unified-scheduler.types.d.ts.map +0 -1
  915. package/dist/backend/backend/src/services/workflow/unified-scheduler.types.js +0 -81
  916. package/dist/backend/backend/src/services/workflow/unified-scheduler.types.js.map +0 -1
  917. package/dist/backend/backend/src/types/auto-assign.types.d.ts +0 -271
  918. package/dist/backend/backend/src/types/auto-assign.types.d.ts.map +0 -1
  919. package/dist/backend/backend/src/types/auto-assign.types.js +0 -136
  920. package/dist/backend/backend/src/types/auto-assign.types.js.map +0 -1
  921. package/dist/backend/backend/src/types/knowledge.types.d.ts +0 -195
  922. package/dist/backend/backend/src/types/knowledge.types.d.ts.map +0 -1
  923. package/dist/backend/backend/src/types/knowledge.types.js +0 -38
  924. package/dist/backend/backend/src/types/knowledge.types.js.map +0 -1
  925. package/dist/backend/backend/src/utils/esm-require.utils.d.ts +0 -111
  926. package/dist/backend/backend/src/utils/esm-require.utils.d.ts.map +0 -1
  927. package/dist/backend/backend/src/utils/esm-require.utils.js +0 -124
  928. package/dist/backend/backend/src/utils/esm-require.utils.js.map +0 -1
  929. package/dist/cli/backend/src/services/knowledge/embedding-provider.d.ts +0 -78
  930. package/dist/cli/backend/src/services/knowledge/embedding-provider.d.ts.map +0 -1
  931. package/dist/cli/backend/src/services/knowledge/embedding-provider.js +0 -179
  932. package/dist/cli/backend/src/services/knowledge/embedding-provider.js.map +0 -1
  933. package/dist/cli/backend/src/services/knowledge/fts5-index.service.d.ts +0 -118
  934. package/dist/cli/backend/src/services/knowledge/fts5-index.service.d.ts.map +0 -1
  935. package/dist/cli/backend/src/services/knowledge/fts5-index.service.js +0 -241
  936. package/dist/cli/backend/src/services/knowledge/fts5-index.service.js.map +0 -1
  937. package/dist/cli/backend/src/services/knowledge/fts5-query-sanitizer.d.ts +0 -102
  938. package/dist/cli/backend/src/services/knowledge/fts5-query-sanitizer.d.ts.map +0 -1
  939. package/dist/cli/backend/src/services/knowledge/fts5-query-sanitizer.js +0 -118
  940. package/dist/cli/backend/src/services/knowledge/fts5-query-sanitizer.js.map +0 -1
  941. package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.d.ts +0 -56
  942. package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.d.ts.map +0 -1
  943. package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.js +0 -91
  944. package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.js.map +0 -1
  945. package/dist/cli/backend/src/services/knowledge/knowledge-search.service.d.ts +0 -241
  946. package/dist/cli/backend/src/services/knowledge/knowledge-search.service.d.ts.map +0 -1
  947. package/dist/cli/backend/src/services/knowledge/knowledge-search.service.js +0 -590
  948. package/dist/cli/backend/src/services/knowledge/knowledge-search.service.js.map +0 -1
  949. package/dist/cli/backend/src/services/knowledge/knowledge.service.d.ts +0 -153
  950. package/dist/cli/backend/src/services/knowledge/knowledge.service.d.ts.map +0 -1
  951. package/dist/cli/backend/src/services/knowledge/knowledge.service.js +0 -409
  952. package/dist/cli/backend/src/services/knowledge/knowledge.service.js.map +0 -1
  953. package/dist/cli/backend/src/services/knowledge/learnings-index.service.d.ts +0 -159
  954. package/dist/cli/backend/src/services/knowledge/learnings-index.service.d.ts.map +0 -1
  955. package/dist/cli/backend/src/services/knowledge/learnings-index.service.js +0 -304
  956. package/dist/cli/backend/src/services/knowledge/learnings-index.service.js.map +0 -1
  957. package/dist/cli/backend/src/services/knowledge/vector-store.service.d.ts +0 -331
  958. package/dist/cli/backend/src/services/knowledge/vector-store.service.d.ts.map +0 -1
  959. package/dist/cli/backend/src/services/knowledge/vector-store.service.js +0 -830
  960. package/dist/cli/backend/src/services/knowledge/vector-store.service.js.map +0 -1
  961. package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.d.ts +0 -115
  962. package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.d.ts.map +0 -1
  963. package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.js +0 -215
  964. package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.js.map +0 -1
  965. package/dist/cli/backend/src/services/memory/embedding-provider.d.ts +0 -78
  966. package/dist/cli/backend/src/services/memory/embedding-provider.d.ts.map +0 -1
  967. package/dist/cli/backend/src/services/memory/embedding-provider.js +0 -179
  968. package/dist/cli/backend/src/services/memory/embedding-provider.js.map +0 -1
  969. package/dist/cli/backend/src/services/memory/vector-store.service.d.ts +0 -331
  970. package/dist/cli/backend/src/services/memory/vector-store.service.d.ts.map +0 -1
  971. package/dist/cli/backend/src/services/memory/vector-store.service.js +0 -814
  972. package/dist/cli/backend/src/services/memory/vector-store.service.js.map +0 -1
  973. package/dist/cli/backend/src/services/project/task-tracking.service.d.ts +0 -171
  974. package/dist/cli/backend/src/services/project/task-tracking.service.d.ts.map +0 -1
  975. package/dist/cli/backend/src/services/project/task-tracking.service.js +0 -725
  976. package/dist/cli/backend/src/services/project/task-tracking.service.js.map +0 -1
  977. package/dist/cli/backend/src/types/auto-assign.types.d.ts +0 -271
  978. package/dist/cli/backend/src/types/auto-assign.types.d.ts.map +0 -1
  979. package/dist/cli/backend/src/types/auto-assign.types.js +0 -136
  980. package/dist/cli/backend/src/types/auto-assign.types.js.map +0 -1
  981. package/dist/cli/backend/src/types/knowledge.types.d.ts +0 -195
  982. package/dist/cli/backend/src/types/knowledge.types.d.ts.map +0 -1
  983. package/dist/cli/backend/src/types/knowledge.types.js +0 -38
  984. package/dist/cli/backend/src/types/knowledge.types.js.map +0 -1
  985. package/frontend/dist/assets/index-aa737fd3.js +0 -5254
@@ -0,0 +1,4548 @@
1
+ import * as path from 'path';
2
+ import * as os from 'os';
3
+ import { readFile, readdir, stat, mkdir, writeFile, access } from 'fs/promises';
4
+ import { existsSync } from 'fs';
5
+ import { LoggerService } from '../core/logger.service.js';
6
+ import { createSessionCommandHelper, getSessionBackendSync, createSessionBackend, getSessionStatePersistence, } from '../session/index.js';
7
+ import { RuntimeServiceFactory } from './runtime-service.factory.js';
8
+ import { registerInProcessRuntime, unregisterInProcessRuntime, } from './crewly-agent/in-process-runtime-registry.js';
9
+ import { getCrewlyHomePath } from '../core/crewly-home.utils.js';
10
+ import { CREWLY_CONSTANTS, ENV_CONSTANTS, AGENT_TIMEOUTS, ORCHESTRATOR_SESSION_NAME, ORCHESTRATOR_ROLE, RUNTIME_TYPES, SESSION_COMMAND_DELAYS, EVENT_DELIVERY_CONSTANTS, TERMINAL_PATTERNS, GEMINI_SHELL_MODE_CONSTANTS, GEMINI_STUCK_CONNECTIVITY_PATTERN, GEMINI_ERROR_STATE_CONSTANTS, } from '../../constants.js';
11
+ import { WEB_CONSTANTS } from '../../../../config/constants.js';
12
+ import { delay } from '../../utils/async.utils.js';
13
+ import { getSettingsService } from '../settings/settings.service.js';
14
+ import { SessionMemoryService } from '../memory/session-memory.service.js';
15
+ import { ActiveWorkBriefingService } from './active-work-briefing.service.js';
16
+ import { RuntimeExitMonitorService } from './runtime-exit-monitor.service.js';
17
+ import { ContextWindowMonitorService } from './context-window-monitor.service.js';
18
+ import { OAuthReloginMonitorService } from './oauth-relogin-monitor.service.js';
19
+ import { SubAgentMessageQueue } from '../messaging/sub-agent-message-queue.service.js';
20
+ import { AgentSuspendService } from './agent-suspend.service.js';
21
+ import { PromptBuilderService, buildModuleConfigFromTeamMember, } from '../ai/prompt-builder.service.js';
22
+ import { PromptAssemblyService } from '../ai/prompt-modules/prompt-assembly.service.js';
23
+ import { stripAnsiCodes } from '../../utils/terminal-output.utils.js';
24
+ import { isPromptLine, containsSpinnerOrWorkingIndicator, containsProcessingIndicator, containsBusyStatusBar, containsRewindMode, extractChatPrefix, stripTuiLineBorders, matchTuiPromptLine, } from '../../utils/terminal-string-ops.js';
25
+ import { PtyActivityTrackerService } from './pty-activity-tracker.service.js';
26
+ import { synthesizeSlackConversationId } from '../chat-v2/legacy-dto.utils.js';
27
+ /**
28
+ * Error thrown when an agent role cannot be resolved to a valid value.
29
+ *
30
+ * Indicates a member with no `role` field in storage, or a caller passing
31
+ * `undefined`/empty/literal-"undefined". Surfaces the failure loudly so it
32
+ * cannot manifest as the silent "empty Claude prompt" symptom (Bug #1).
33
+ */
34
+ export class InvalidAgentRoleError extends Error {
35
+ received;
36
+ constructor(received, context) {
37
+ const ctx = context ? ` (${context})` : '';
38
+ const display = received === undefined ? 'undefined'
39
+ : received === null ? 'null'
40
+ : typeof received === 'string' ? `"${received}"`
41
+ : String(received);
42
+ super(`Invalid agent role${ctx}: received ${display}. Role must be a non-empty string ` +
43
+ `matching a directory under config/roles/. ` +
44
+ `Common cause: team member.role field missing or unset in storage.`);
45
+ this.received = received;
46
+ this.name = 'InvalidAgentRoleError';
47
+ }
48
+ }
49
+ /**
50
+ * Validate that an agent role is a usable string before it flows into session
51
+ * creation, env-var injection, or prompt-template path resolution.
52
+ *
53
+ * Rejects: undefined, null, non-strings, empty/whitespace strings, and the
54
+ * literal string "undefined" (which results from JS coercion when an undefined
55
+ * is interpolated into a template literal — the exact failure mode behind
56
+ * `CREWLY_ROLE="undefined"` in tmux env).
57
+ *
58
+ * @param role - The candidate role value
59
+ * @param context - Optional caller context for the error message (e.g. "startTeamMember")
60
+ * @throws {InvalidAgentRoleError} when role is unusable
61
+ */
62
+ export function validateAgentRole(role, context) {
63
+ if (typeof role !== 'string') {
64
+ throw new InvalidAgentRoleError(role, context);
65
+ }
66
+ const trimmed = role.trim();
67
+ if (trimmed.length === 0 || trimmed.toLowerCase() === 'undefined' || trimmed.toLowerCase() === 'null') {
68
+ throw new InvalidAgentRoleError(role, context);
69
+ }
70
+ }
71
+ /**
72
+ * Service responsible for the complex, multi-step process of agent initialization and registration.
73
+ * Isolates the complex state management of agent startup with progressive escalation.
74
+ *
75
+ * Key capabilities:
76
+ * - Agent session creation and management
77
+ * - Runtime initialization and registration
78
+ * - Reliable message delivery to Claude Code with retry logic
79
+ * - Health checking and status management
80
+ */
81
+ export class AgentRegistrationService {
82
+ logger;
83
+ _sessionHelper = null;
84
+ storageService;
85
+ projectRoot;
86
+ // Prompt file caching to eliminate file I/O contention during concurrent session creation
87
+ promptCache = new Map();
88
+ // Session creation locks to prevent concurrent createAgentSession calls for the same session
89
+ sessionCreationLocks = new Map();
90
+ // AbortControllers for pending registration prompts (keyed by session name)
91
+ registrationAbortControllers = new Map();
92
+ // Background stuck-message detector timer
93
+ stuckMessageDetectorTimer = null;
94
+ // Track TUI sessions for the stuck-message detector (sessionName → runtimeType)
95
+ tuiSessionRegistry = new Map();
96
+ // Track recently-sent messages for background stuck-detection (all runtimes).
97
+ // Key: sessionName, Value: array of tracked message entries
98
+ sentMessageTracker = new Map();
99
+ // Per-session delivery mutex to serialize message delivery.
100
+ // Prevents concurrent sendMessageWithRetry calls to the same session,
101
+ // which causes multiple Ctrl+C presses that can crash the runtime.
102
+ sessionDeliveryMutex = new Map();
103
+ // Per-session hash of last sent message to prevent duplicate writes (#128).
104
+ // Key: sessionName, Value: { hash, sentAt }
105
+ lastSentMessageHash = new Map();
106
+ // In-process Crewly Agent runtimes (sessionName → runtime instance).
107
+ // Used for crewly-agent runtimeType agents that run without PTY sessions.
108
+ inProcessRuntimes = new Map();
109
+ /** #167: Optional scheduler service for DLQ drain on agent activation. */
110
+ schedulerService = null;
111
+ /** Architecture Upgrade Phase 6: EventBusService for standing task subscriptions. */
112
+ eventBusService = null;
113
+ // Terminal patterns are now centralized in TERMINAL_PATTERNS constant
114
+ // Keeping prompt chars as static getter for backwards compatibility within the class
115
+ static get CLAUDE_PROMPT_INDICATORS() {
116
+ return TERMINAL_PATTERNS.PROMPT_CHARS;
117
+ }
118
+ constructor(_legacyTmuxService, // Legacy parameter for backwards compatibility
119
+ projectRoot, storageService) {
120
+ this.logger = LoggerService.getInstance().createComponentLogger('AgentRegistrationService');
121
+ this.storageService = storageService;
122
+ this.projectRoot = projectRoot || this.findProjectRoot();
123
+ // Wire up the exit monitor to cancel pending registrations on runtime exit
124
+ RuntimeExitMonitorService.getInstance().setOnExitDetectedCallback((sessionName) => {
125
+ this.cancelPendingRegistration(sessionName);
126
+ this.unregisterTuiSession(sessionName);
127
+ // Preserve queued messages for suspended agents (they'll be delivered on rehydrate)
128
+ if (!AgentSuspendService.getInstance().isSuspended(sessionName)) {
129
+ SubAgentMessageQueue.getInstance().clear(sessionName);
130
+ }
131
+ });
132
+ }
133
+ /**
134
+ * #167: Inject scheduler service for dead-letter queue drain on agent activation.
135
+ *
136
+ * @param scheduler - Scheduler service with drainDeadLetterQueue method
137
+ */
138
+ setSchedulerService(scheduler) {
139
+ this.schedulerService = scheduler;
140
+ }
141
+ /**
142
+ * Inject EventBusService for standing task workflow subscriptions at agent registration.
143
+ *
144
+ * @param eventBus - EventBusService instance with subscribe method
145
+ */
146
+ setEventBusService(eventBus) {
147
+ this.eventBusService = eventBus;
148
+ }
149
+ /**
150
+ * Cancel a pending registration prompt for a session.
151
+ * Called by RuntimeExitMonitorService when a runtime exit is detected.
152
+ *
153
+ * @param sessionName - The session whose registration to cancel
154
+ */
155
+ cancelPendingRegistration(sessionName) {
156
+ const controller = this.registrationAbortControllers.get(sessionName);
157
+ if (controller) {
158
+ controller.abort();
159
+ this.registrationAbortControllers.delete(sessionName);
160
+ this.logger.info('Cancelled pending registration', { sessionName });
161
+ }
162
+ }
163
+ /**
164
+ * Get or create the session helper with lazy initialization
165
+ */
166
+ async getSessionHelper() {
167
+ this.logger.debug('Getting session helper', {
168
+ hasExistingHelper: !!this._sessionHelper,
169
+ });
170
+ if (this._sessionHelper) {
171
+ return this._sessionHelper;
172
+ }
173
+ let backend = getSessionBackendSync();
174
+ this.logger.debug('Backend sync check', {
175
+ hasBackend: !!backend,
176
+ });
177
+ if (!backend) {
178
+ this.logger.info('Creating new PTY session backend');
179
+ backend = await createSessionBackend('pty');
180
+ this.logger.info('PTY session backend created', {
181
+ hasBackend: !!backend,
182
+ });
183
+ }
184
+ this._sessionHelper = createSessionCommandHelper(backend);
185
+ this.logger.debug('Session helper created');
186
+ return this._sessionHelper;
187
+ }
188
+ /**
189
+ * Get session helper synchronously (may throw if not initialized)
190
+ */
191
+ getSessionHelperSync() {
192
+ if (!this._sessionHelper) {
193
+ const backend = getSessionBackendSync();
194
+ if (!backend) {
195
+ throw new Error('Session backend not initialized');
196
+ }
197
+ this._sessionHelper = createSessionCommandHelper(backend);
198
+ }
199
+ return this._sessionHelper;
200
+ }
201
+ /**
202
+ * Resolve the runtime type for a session from storage.
203
+ * Checks orchestrator status first, then team member data.
204
+ * Falls back to CLAUDE_CODE if nothing is found.
205
+ *
206
+ * IMPORTANT: Callers should prefer passing runtimeType explicitly.
207
+ * This method exists as a safety net so that Ctrl+C (Claude Code
208
+ * cleanup) is never sent to a Gemini CLI session, which would
209
+ * trigger /quit and kill the agent.
210
+ */
211
+ async resolveSessionRuntimeType(sessionName) {
212
+ try {
213
+ // Check if this is the orchestrator session
214
+ if (sessionName === ORCHESTRATOR_SESSION_NAME) {
215
+ const orchestratorStatus = await this.storageService.getOrchestratorStatus();
216
+ if (orchestratorStatus?.runtimeType) {
217
+ return orchestratorStatus.runtimeType;
218
+ }
219
+ }
220
+ // Check team member data
221
+ const memberInfo = await this.storageService.findMemberBySessionName(sessionName);
222
+ if (memberInfo?.member?.runtimeType) {
223
+ return memberInfo.member.runtimeType;
224
+ }
225
+ }
226
+ catch (error) {
227
+ this.logger.debug('Could not resolve runtime type from storage, using default', {
228
+ sessionName,
229
+ error: error instanceof Error ? error.message : String(error),
230
+ });
231
+ }
232
+ // Check in-process runtimes before defaulting to CLAUDE_CODE.
233
+ // crewly-agent runtimes may not have a team config entry yet.
234
+ if (this.inProcessRuntimes.has(sessionName)) {
235
+ return RUNTIME_TYPES.CREWLY_AGENT;
236
+ }
237
+ return RUNTIME_TYPES.CLAUDE_CODE;
238
+ }
239
+ /**
240
+ * Route a crewly-agent in-process response to the originating chat conversation.
241
+ *
242
+ * PTY-based runtimes emit [NOTIFY] markers that the terminal gateway parses.
243
+ * In-process crewly-agents have no terminal output, so this method provides
244
+ * the equivalent routing for their text responses.
245
+ *
246
+ * @param sessionName - Agent session that produced the response
247
+ * @param text - Response text from the agent
248
+ * @param conversationId - Chat conversation to route the response to
249
+ */
250
+ routeInProcessResponseToChat(sessionName, text, conversationId) {
251
+ // LEGACY PATH — chatGateway.processNotifyMessage → ChatService
252
+ // (JSON files in ~/.crewly/chat/). Kept until Phase 5 data
253
+ // migration retires the JSON store; until then the legacy file
254
+ // is what the frontend chat-v1 readers consume.
255
+ import('../../websocket/chat.gateway.js')
256
+ .then(({ getChatGateway }) => {
257
+ const chatGateway = getChatGateway();
258
+ if (!chatGateway) {
259
+ this.logger.debug('ChatGateway not available, skipping response routing', {
260
+ sessionName, conversationId,
261
+ });
262
+ return;
263
+ }
264
+ // No metadata override here: processNotifyMessage already tags the
265
+ // turn `source: 'in-process-runtime'` — the canonical member of the
266
+ // closed RECORD_TURN_SOURCES enum for exactly this path. The old
267
+ // `'crewly-agent'` value was spread OVER that correct default and
268
+ // then failed enum validation, so every in-process reply to a web
269
+ // conversation was rejected with "unknown metadata.source" and
270
+ // silently dropped after the agent had already done the work.
271
+ return chatGateway.processNotifyMessage(sessionName, text, conversationId);
272
+ })
273
+ .then((chatMessage) => {
274
+ if (chatMessage) {
275
+ this.logger.debug('Routed in-process agent response to chat (legacy)', {
276
+ sessionName, conversationId,
277
+ messageId: typeof chatMessage === 'object' ? chatMessage.id : undefined,
278
+ });
279
+ }
280
+ })
281
+ .catch((err) => {
282
+ this.logger.warn('Failed to route in-process agent response to chat', {
283
+ sessionName, conversationId,
284
+ error: err instanceof Error ? err.message : String(err),
285
+ });
286
+ });
287
+ // Phase 6β of unified-chat-message-store spec: the explicit
288
+ // chat-v2 recordTurn call that lived here during Phases 2-3
289
+ // was removed because `ChatService` is now a thin façade over
290
+ // `ChatV2Service` — the `processNotifyMessage` call above
291
+ // already writes through to chat-v2 internally. Keeping both
292
+ // paths would produce duplicate rows.
293
+ }
294
+ /**
295
+ * Auto-route an in-process agent's text response back to Slack.
296
+ *
297
+ * Mirrors the post-send side effects of POST /slack/send so that the
298
+ * Slack reply, thread-status bookkeeping, and V3 SLA cascade all stay in
299
+ * sync. Used as a finish-event hook for the in-process Crewly Agent
300
+ * runtime when:
301
+ * 1. The inbound message carried a `[SLACK:channelId:threadTs]` marker
302
+ * 2. The agent produced text output (`result.text` non-empty)
303
+ * 3. The agent did NOT explicitly call the `reply_slack` tool
304
+ *
305
+ * Without this hook, AI SDK agents that finish with `toolCalls=0,
306
+ * finishReason=stop` would drop their reply on the floor — unlike the
307
+ * claude-code PTY path, the in-process runtime has no stdout stream the
308
+ * SlackBridge can tail.
309
+ *
310
+ * Best-effort: a Slack-not-connected condition or a downstream
311
+ * bookkeeping failure is logged and swallowed; never throws. The chat
312
+ * route (`routeInProcessResponseToChat`) handles persistence to the chat
313
+ * service separately.
314
+ *
315
+ * @param sessionName - Agent session that produced the response
316
+ * @param text - Response text from the agent
317
+ * @param slackContext - Slack channel + thread metadata captured from the
318
+ * inbound `[SLACK:channelId:threadTs]` marker
319
+ */
320
+ routeInProcessResponseToSlack(sessionName, text, slackContext) {
321
+ // Lazy import to avoid module-load circular dependency between
322
+ // agent-registration ↔ slack ↔ messaging ↔ v3.
323
+ import('../slack/slack.service.js')
324
+ .then(async ({ getSlackService }) => {
325
+ const slack = getSlackService();
326
+ if (!slack.isConnected()) {
327
+ this.logger.warn('Slack not connected — cannot auto-route in-process agent response', {
328
+ sessionName,
329
+ channelId: slackContext.channelId,
330
+ threadTs: slackContext.threadTs,
331
+ textLength: text.length,
332
+ });
333
+ return;
334
+ }
335
+ // Auto-prefix with the agent display name (mirrors reply_slack tool
336
+ // behavior) so the message is attributable in the Slack thread.
337
+ // Skip if the text already starts with `[` — the agent supplied its
338
+ // own prefix.
339
+ let cleanText = text;
340
+ if (sessionName && !cleanText.startsWith('[')) {
341
+ const parts = sessionName.split('-');
342
+ const namePart = parts.length >= 3 ? parts[2] : parts[parts.length - 1];
343
+ if (namePart) {
344
+ const capitalized = namePart.charAt(0).toUpperCase() + namePart.slice(1);
345
+ cleanText = `[${capitalized}] ${cleanText}`;
346
+ }
347
+ }
348
+ await slack.sendMessage({
349
+ channelId: slackContext.channelId,
350
+ text: cleanText,
351
+ threadTs: slackContext.threadTs,
352
+ });
353
+ this.logger.info('Auto-routed in-process agent response to Slack', {
354
+ sessionName,
355
+ channelId: slackContext.channelId,
356
+ threadTs: slackContext.threadTs,
357
+ textLength: cleanText.length,
358
+ });
359
+ // Mirror /slack/send bookkeeping #4 — clear the pending-delivery
360
+ // reminder OrcDeliveryEnforcer holds for this thread. This is the one
361
+ // side-effect the auto-route never mirrored, and the gap is
362
+ // user-visible: when the agent answers with plain text instead of
363
+ // calling the reply_slack tool, the reply reaches Slack but the
364
+ // ledger entry survives, so the enforcer nudges again at 10 min and
365
+ // again at 30 min — and the agent posts the same answer each time.
366
+ // SlackService's dedup window is 30s, far too short to absorb
367
+ // reminders spaced minutes apart, so every nudge lands in the thread
368
+ // as a visible duplicate.
369
+ if (slackContext.threadTs) {
370
+ try {
371
+ const { OrcDeliveryEnforcerService } = await import('../orc/orc-delivery-enforcer.service.js');
372
+ OrcDeliveryEnforcerService.getInstance()?.markDelivered({
373
+ channelId: slackContext.channelId,
374
+ threadTs: slackContext.threadTs,
375
+ });
376
+ }
377
+ catch (err) {
378
+ this.logger.debug('Failed to clear pending delivery after auto-route (non-fatal)', {
379
+ sessionName,
380
+ error: err instanceof Error ? err.message : String(err),
381
+ });
382
+ }
383
+ }
384
+ // Mirror /slack/send bookkeeping #3 — record `agent.action`
385
+ // with actionType='send_slack' so the auditor view and any
386
+ // V3 downstream analysis see the auto-routed reply. Without
387
+ // this entry the in-process auto-route is invisible in the
388
+ // behavior log (only `/slack/send`-originated replies show
389
+ // up). Follow-up #9 from PR #543 review.
390
+ try {
391
+ const { getAgentBehaviorLogService } = await import('../observability/agent-behavior-log.singleton.js');
392
+ getAgentBehaviorLogService()?.record({
393
+ type: 'agent.action',
394
+ agent: sessionName,
395
+ actionType: 'send_slack',
396
+ details: {
397
+ channelId: slackContext.channelId,
398
+ threadTs: slackContext.threadTs ?? null,
399
+ textLength: cleanText.length,
400
+ source: 'in_process_auto_route',
401
+ },
402
+ });
403
+ }
404
+ catch (err) {
405
+ this.logger.debug('Failed to record send_slack agent.action (non-fatal)', {
406
+ sessionName,
407
+ error: err instanceof Error ? err.message : String(err),
408
+ });
409
+ }
410
+ // Mirror /slack/send bookkeeping #1 — mark thread as
411
+ // replied_completed so the recovery loop on the next restart
412
+ // does not re-enqueue this inbound message. Without this, every
413
+ // backend restart would cause orc to re-reply.
414
+ //
415
+ // Race-protection: build the thread-status entry on the fly if
416
+ // it isn't tracked yet. The Slack listener's `trackInbound` is
417
+ // what normally populates the entry, but in a startup-race
418
+ // scenario the agent finish event can land before the listener
419
+ // has persisted the inbound. Without this fallback,
420
+ // `markReplied` short-circuits and the recovery loop on the
421
+ // next boot still re-fires the inbound (the exact bug
422
+ // /slack/send already guards against — keep parity).
423
+ if (slackContext.threadTs) {
424
+ try {
425
+ const { ThreadStatusQueueService } = await import('../messaging/thread-status-queue.service.js');
426
+ const tsq = ThreadStatusQueueService.getInstance();
427
+ const threadKey = `${slackContext.channelId}:${slackContext.threadTs}`;
428
+ if (!tsq.get(threadKey)) {
429
+ tsq.trackInbound({
430
+ threadKey,
431
+ conversationId: synthesizeSlackConversationId(slackContext.channelId, slackContext.threadTs),
432
+ source: 'slack',
433
+ messagePreview: '[in-process auto-route — no inbound recorded]',
434
+ });
435
+ }
436
+ tsq.markReplied(threadKey, 'replied_completed');
437
+ }
438
+ catch (err) {
439
+ this.logger.debug('Failed to update thread-status after auto-route (non-fatal)', {
440
+ sessionName,
441
+ error: err instanceof Error ? err.message : String(err),
442
+ });
443
+ }
444
+ }
445
+ // Mirror /slack/send bookkeeping #2 — fire the V3 SLA cascade so
446
+ // the matching Request transitions out of `queued`. Without
447
+ // this, requests whose acknowledgement came via this auto-route
448
+ // would never reach a terminal state.
449
+ if (slackContext.threadTs) {
450
+ try {
451
+ const { getRequestSlaSubscriber } = await import('../v3/request-sla.subscriber.js');
452
+ const sub = getRequestSlaSubscriber();
453
+ if (sub) {
454
+ await sub.markResolvedByThread(slackContext.threadTs);
455
+ }
456
+ }
457
+ catch (err) {
458
+ this.logger.debug('Failed to fire SLA cascade after auto-route (non-fatal)', {
459
+ sessionName,
460
+ error: err instanceof Error ? err.message : String(err),
461
+ });
462
+ }
463
+ }
464
+ })
465
+ .catch((err) => {
466
+ this.logger.warn('Failed to auto-route in-process agent response to Slack', {
467
+ sessionName,
468
+ channelId: slackContext.channelId,
469
+ threadTs: slackContext.threadTs,
470
+ error: err instanceof Error ? err.message : String(err),
471
+ });
472
+ });
473
+ }
474
+ /**
475
+ * Find the project root by looking for package.json
476
+ */
477
+ findProjectRoot() {
478
+ // Resolve from the compiled dist/ location to the package root (#209)
479
+ // dist/backend/src/services/agent/ → root (5 levels up)
480
+ // This works for both local dev and npm global installs
481
+ try {
482
+ // Use __dirname equivalent: path.dirname(fileURLToPath(import.meta.url))
483
+ // But since Jest doesn't support import.meta, we use the module path directly
484
+ const candidates = [
485
+ // From source: backend/src/services/agent/ → root
486
+ path.resolve(__dirname, '..', '..', '..', '..'),
487
+ // From compiled dist: dist/backend/src/services/agent/ → root
488
+ path.resolve(__dirname, '..', '..', '..', '..', '..'),
489
+ ];
490
+ for (const candidate of candidates) {
491
+ if (existsSync(path.join(candidate, 'config', 'skills'))) {
492
+ return candidate;
493
+ }
494
+ }
495
+ }
496
+ catch {
497
+ // Path resolution failed — fall back
498
+ }
499
+ return process.cwd();
500
+ }
501
+ /**
502
+ * Resolve runtime flags from the agent's effective skills.
503
+ * Reads role config and skill configs to collect flags compatible with the runtime.
504
+ *
505
+ * @param role - The agent's role name
506
+ * @param runtimeType - The agent's runtime type (e.g. 'claude-code')
507
+ * @param skillOverrides - Additional skills assigned to the member
508
+ * @param excludedRoleSkills - Skills excluded from the role's default set
509
+ * @returns Array of CLI flags to inject (e.g. ['--chrome'])
510
+ */
511
+ async resolveRuntimeFlags(role, runtimeType, skillOverrides, excludedRoleSkills) {
512
+ const flags = new Set();
513
+ try {
514
+ // 1. Get role's assigned skills from config/roles/{role}/role.json
515
+ const roleName = role.toLowerCase().replace(/\s+/g, '-');
516
+ const rolePath = path.join(this.projectRoot, 'config', 'roles', roleName, 'role.json');
517
+ const roleContent = await readFile(rolePath, 'utf8');
518
+ const roleConfig = JSON.parse(roleContent);
519
+ const roleSkills = roleConfig.assignedSkills || [];
520
+ // 2. Apply skill overrides and exclusions
521
+ const effectiveSkills = new Set([...roleSkills, ...(skillOverrides || [])]);
522
+ for (const excluded of (excludedRoleSkills || [])) {
523
+ effectiveSkills.delete(excluded);
524
+ }
525
+ // 3. For each skill, read skill.json and collect flags matching runtime.
526
+ // Skills live under agent/core/ or agent/marketplace/ subdirectories,
527
+ // so search both paths when looking up skill.json.
528
+ for (const skillId of effectiveSkills) {
529
+ try {
530
+ const skillJsonPath = await this.findSkillJsonPath(skillId);
531
+ if (!skillJsonPath) {
532
+ this.logger.debug('Skill config not found in any known directory (no runtime flags)', { skillId });
533
+ continue;
534
+ }
535
+ const skillContent = await readFile(skillJsonPath, 'utf8');
536
+ const skillConfig = JSON.parse(skillContent);
537
+ if (skillConfig.runtime?.runtime === runtimeType && Array.isArray(skillConfig.runtime?.flags)) {
538
+ for (const flag of skillConfig.runtime.flags) {
539
+ flags.add(flag);
540
+ }
541
+ }
542
+ }
543
+ catch (skillErr) {
544
+ this.logger.warn('Failed to read skill config', {
545
+ skillId,
546
+ error: skillErr instanceof Error ? skillErr.message : String(skillErr),
547
+ });
548
+ }
549
+ }
550
+ if (flags.size > 0) {
551
+ this.logger.info('Resolved runtime flags from skills', {
552
+ role, runtimeType, flags: Array.from(flags),
553
+ });
554
+ }
555
+ }
556
+ catch (error) {
557
+ this.logger.debug('Could not resolve runtime flags (no role config or read error)', {
558
+ role, runtimeType, error: error instanceof Error ? error.message : String(error),
559
+ });
560
+ }
561
+ return Array.from(flags);
562
+ }
563
+ /**
564
+ * Search known skill directories for a skill's skill.json file.
565
+ * Skills live under `config/skills/agent/core/` or `config/skills/agent/marketplace/`.
566
+ *
567
+ * @param skillId - The skill identifier (directory name)
568
+ * @returns Absolute path to skill.json, or null if not found
569
+ */
570
+ async findSkillJsonPath(skillId) {
571
+ const searchDirs = [
572
+ path.join(this.projectRoot, 'config', 'skills', 'agent', 'core', skillId, 'skill.json'),
573
+ path.join(this.projectRoot, 'config', 'skills', 'agent', 'marketplace', skillId, 'skill.json'),
574
+ ];
575
+ for (const candidate of searchDirs) {
576
+ try {
577
+ await access(candidate);
578
+ return candidate;
579
+ }
580
+ catch {
581
+ // Not found in this directory, try next
582
+ }
583
+ }
584
+ return null;
585
+ }
586
+ /**
587
+ * Create a runtime service instance for the given runtime type.
588
+ * Centralizes RuntimeServiceFactory creation to reduce code duplication.
589
+ */
590
+ createRuntimeService(runtimeType) {
591
+ return RuntimeServiceFactory.create(runtimeType, null, this.projectRoot);
592
+ }
593
+ /**
594
+ * Get an in-process Crewly Agent runtime by session name.
595
+ * Used by the terminal controller for force-delivery to in-process agents.
596
+ *
597
+ * @param sessionName - Session name to look up
598
+ * @returns The CrewlyAgentExternalRuntimeService if it exists, undefined otherwise
599
+ */
600
+ getInProcessRuntime(sessionName) {
601
+ return this.inProcessRuntimes.get(sessionName);
602
+ }
603
+ /**
604
+ * Check if an in-process Crewly Agent runtime is active and ready for a given session.
605
+ *
606
+ * Used by team status resolution to correctly report active status for
607
+ * crewly-agent runtimes that have no PTY session.
608
+ *
609
+ * @param sessionName - The session name to check
610
+ * @returns True if an in-process runtime exists and is ready
611
+ */
612
+ isInProcessRuntimeActive(sessionName) {
613
+ const runtime = this.inProcessRuntimes.get(sessionName);
614
+ return runtime !== undefined && runtime.isReady();
615
+ }
616
+ /**
617
+ * Get the check interval based on environment.
618
+ * Uses shorter intervals in test environment for faster tests.
619
+ */
620
+ getCheckInterval() {
621
+ return process.env.NODE_ENV === 'test' ? 1000 : 2000;
622
+ }
623
+ /**
624
+ * Update agent status with safe error handling (non-blocking).
625
+ * Returns true if successful, false if failed.
626
+ */
627
+ async updateAgentStatusSafe(sessionName, status, context) {
628
+ try {
629
+ await this.storageService.updateAgentStatus(sessionName, status);
630
+ this.logger.info(`Agent status updated to ${status}`, { sessionName, ...context });
631
+ return true;
632
+ }
633
+ catch (error) {
634
+ this.logger.warn('Failed to update agent status (non-critical)', {
635
+ sessionName,
636
+ error: error instanceof Error ? error.message : String(error),
637
+ });
638
+ return false;
639
+ }
640
+ }
641
+ /**
642
+ * Role aliases — common synonyms users naturally type in team configs
643
+ * mapped to the canonical directory name under `config/roles/`. Without
644
+ * these, a team-config role like `"tech-lead"` would fall through to a
645
+ * non-existent directory and trigger the ENOENT fallback path on every
646
+ * agent registration (Steve 2026-05-15 dogfood: CE team config used
647
+ * `role: "tech-lead"` while the canonical directory is `team-leader`,
648
+ * producing a steady drip of "Could not load prompt from config" warns
649
+ * for every member-start with no actionable resolution short of editing
650
+ * user data).
651
+ *
652
+ * Keep keys lowercased + dash-normalized (matches the post-normalization
653
+ * shape used by `getPromptFileForRole`).
654
+ */
655
+ static ROLE_DIR_ALIASES = new Map([
656
+ ['tech-lead', 'team-leader'],
657
+ ['techlead', 'team-leader'],
658
+ ['tl', 'team-leader'],
659
+ ['team-lead', 'team-leader'],
660
+ ]);
661
+ /**
662
+ * Get prompt file path for a specific role
663
+ * Uses the unified config/roles/{role}/prompt.md structure
664
+ */
665
+ async getPromptFileForRole(role) {
666
+ // Defensive: reject empty/undefined/literal-"undefined" before path concat,
667
+ // which would otherwise resolve to a non-existent `config/roles/undefined/prompt.md`
668
+ // and trigger the silent fallback path that left agents with empty prompts (Bug #1).
669
+ validateAgentRole(role, 'getPromptFileForRole');
670
+ // Normalize role name to directory name format
671
+ const normalized = role.toLowerCase().replace(/\s+/g, '-');
672
+ // Resolve aliases (e.g. tech-lead → team-leader). Falls through for
673
+ // any role that already matches its directory name.
674
+ const roleName = AgentRegistrationService.ROLE_DIR_ALIASES.get(normalized) ?? normalized;
675
+ return path.join(process.cwd(), 'config', 'roles', roleName, 'prompt.md');
676
+ }
677
+ /**
678
+ * Initialize agent with optimized 2-step escalation process
679
+ * Reduced from 4-step to 2-step with shorter timeouts for better concurrency
680
+ */
681
+ async initializeAgentWithRegistration(sessionName, role, projectPath, timeout = AGENT_TIMEOUTS.REGULAR_AGENT_INITIALIZATION, memberId, runtimeType = RUNTIME_TYPES.CLAUDE_CODE, runtimeFlags, additionalAllowlistPaths) {
682
+ const startTime = Date.now();
683
+ this.logger.info('Starting optimized agent initialization with registration', {
684
+ sessionName,
685
+ role,
686
+ timeout,
687
+ runtimeType,
688
+ });
689
+ // PHASE 4: agentStatus is now set immediately in API endpoints (startTeam/startTeamMember)
690
+ // No longer need to set it here - focus only on session creation
691
+ this.logger.info('Starting agent session initialization', { sessionName, role });
692
+ // Clear detection cache to ensure fresh runtime detection
693
+ const runtimeService = this.createRuntimeService(runtimeType);
694
+ runtimeService.clearDetectionCache(sessionName);
695
+ // Skip Step 1 (direct registration) as it often fails in concurrent scenarios
696
+ // Go directly to Step 2: Cleanup + reinit (more reliable)
697
+ try {
698
+ this.logger.info('Step 1: Attempting cleanup and reinitialization', {
699
+ sessionName,
700
+ });
701
+ const step1Success = await this.tryCleanupAndReinit(sessionName, role, 70000, // 70 seconds for cleanup and reinit (allows 60s for runtime ready)
702
+ projectPath, memberId, runtimeType, runtimeFlags, additionalAllowlistPaths);
703
+ if (step1Success) {
704
+ return {
705
+ success: true,
706
+ message: 'Agent registered successfully after cleanup and reinit',
707
+ };
708
+ }
709
+ }
710
+ catch (error) {
711
+ this.logger.warn('Step 1 (cleanup + reinit) failed', {
712
+ sessionName,
713
+ error: error instanceof Error ? error.message : String(error),
714
+ });
715
+ }
716
+ // Step 2: Full session recreation (30 seconds) - only if time remaining
717
+ if (Date.now() - startTime < timeout - 35000) {
718
+ try {
719
+ this.logger.info('Step 2: Attempting full session recreation', { sessionName });
720
+ const step2Success = await this.tryFullRecreation(sessionName, role, 30000, // Reduced from 45000 to 30000
721
+ projectPath, memberId, runtimeType, runtimeFlags);
722
+ if (step2Success) {
723
+ return {
724
+ success: true,
725
+ message: 'Agent registered successfully after full recreation',
726
+ };
727
+ }
728
+ }
729
+ catch (error) {
730
+ this.logger.warn('Step 2 (full recreation) failed', {
731
+ sessionName,
732
+ error: error instanceof Error ? error.message : String(error),
733
+ });
734
+ }
735
+ }
736
+ // Give up after 2 steps
737
+ const errorMsg = `Failed to initialize agent after optimized escalation attempts (${Math.round((Date.now() - startTime) / 1000)}s)`;
738
+ this.logger.error(errorMsg, { sessionName, role });
739
+ return { success: false, error: errorMsg };
740
+ }
741
+ /**
742
+ * Step 1: Try direct registration prompt
743
+ * Assumes Claude is already running and just needs a registration prompt
744
+ */
745
+ async tryDirectRegistration(sessionName, role, timeout, memberId, runtimeType = RUNTIME_TYPES.CLAUDE_CODE) {
746
+ // Clear any existing input. Claude already performs Ctrl+C cleanup during
747
+ // runtime detection, so avoid sending additional Ctrl+C sequences.
748
+ // Gemini CLI: Skip cleanup — Ctrl+C at an empty prompt triggers /quit.
749
+ // Escape defocuses the TUI. Ctrl+U is ignored by the TUI.
750
+ const helper = await this.getSessionHelper();
751
+ // First check if runtime is running before sending the prompt
752
+ // runtimeService2: Separate instance for pre-registration runtime detection
753
+ // This instance is isolated from main runtimeService to avoid cache interference
754
+ const runtimeService2 = this.createRuntimeService(runtimeType);
755
+ const runtimeRunning = await runtimeService2.detectRuntimeWithCommand(sessionName);
756
+ if (!runtimeRunning) {
757
+ this.logger.debug('Runtime not detected in Step 1, skipping direct registration', {
758
+ sessionName,
759
+ runtimeType,
760
+ });
761
+ return false;
762
+ }
763
+ this.logger.debug('Runtime detected, sending registration prompt', {
764
+ sessionName,
765
+ runtimeType,
766
+ });
767
+ // Clear any pending slash command from detection. Claude detection already
768
+ // exits slash mode. Gemini CLI: skip — Ctrl+C at empty prompt exits CLI,
769
+ // Escape defocuses TUI, Ctrl+U is ignored. The prompt should be clean.
770
+ const prompt = await this.loadRegistrationPrompt(role, sessionName, memberId, runtimeType);
771
+ const promptDelivered = await this.sendPromptRobustly(sessionName, prompt, runtimeType);
772
+ if (!promptDelivered) {
773
+ this.logger.warn('Failed to deliver registration prompt', { sessionName, role });
774
+ return false;
775
+ }
776
+ return await this.waitForRegistration(sessionName, role, timeout);
777
+ }
778
+ /**
779
+ * Step 2: Cleanup with Ctrl+C and reinitialize
780
+ * Tries to reset the runtime session and start fresh
781
+ */
782
+ async tryCleanupAndReinit(sessionName, role, timeout, projectPath, memberId, runtimeType = RUNTIME_TYPES.CLAUDE_CODE, runtimeFlags, additionalAllowlistPaths) {
783
+ // Clear Commandline
784
+ await (await this.getSessionHelper()).clearCurrentCommandLine(sessionName);
785
+ // Inject --resume flag if this was a previously running Claude Code session
786
+ const effectiveFlags = runtimeFlags ? [...runtimeFlags] : [];
787
+ if (runtimeType === RUNTIME_TYPES.CLAUDE_CODE) {
788
+ try {
789
+ const settings = await getSettingsService().getSettings();
790
+ if (settings.general.autoResumeOnRestart) {
791
+ const persistence = getSessionStatePersistence();
792
+ if (persistence.isRestoredSession(sessionName)) {
793
+ const storedSessionId = persistence.getSessionId(sessionName);
794
+ if (storedSessionId) {
795
+ effectiveFlags.push('--resume', storedSessionId);
796
+ this.logger.info('Injecting --resume flag for session restore', {
797
+ sessionName, sessionId: storedSessionId,
798
+ });
799
+ }
800
+ }
801
+ }
802
+ else {
803
+ this.logger.info('Auto-resume disabled by settings, skipping session resume', { sessionName });
804
+ }
805
+ }
806
+ catch (resumeError) {
807
+ this.logger.debug('Could not resolve resume flag (non-fatal)', {
808
+ sessionName, error: resumeError instanceof Error ? resumeError.message : String(resumeError),
809
+ });
810
+ }
811
+ }
812
+ // Write prompt file before launching runtime so --agent (Claude Code) or --append-system-prompt-file works
813
+ let promptFilePath;
814
+ let agentName;
815
+ if (runtimeType === RUNTIME_TYPES.CLAUDE_CODE) {
816
+ try {
817
+ const prompt = await this.loadRegistrationPrompt(role, sessionName, memberId, runtimeType);
818
+ promptFilePath = await this.writePromptFile(sessionName, prompt, {
819
+ runtimeType,
820
+ projectPath,
821
+ role,
822
+ });
823
+ agentName = sessionName;
824
+ }
825
+ catch (promptError) {
826
+ this.logger.warn('Failed to pre-write prompt file (non-fatal, will fall back to direct delivery)', {
827
+ sessionName,
828
+ error: promptError instanceof Error ? promptError.message : String(promptError),
829
+ });
830
+ }
831
+ }
832
+ // Reinitialize runtime using the appropriate initialization script (always fresh start)
833
+ const runtimeService2 = this.createRuntimeService(runtimeType);
834
+ await runtimeService2.executeRuntimeInitScript(sessionName, projectPath, effectiveFlags, promptFilePath, agentName);
835
+ // Wait for runtime to be ready (simplified detection)
836
+ // Use shorter check interval in test environment, and reasonable interval in production
837
+ const checkInterval = this.getCheckInterval();
838
+ const isReady = await runtimeService2.waitForRuntimeReady(sessionName, 60000, checkInterval); // 60s timeout — Claude Code cold starts can take 30-60s
839
+ if (!isReady) {
840
+ throw new Error(`Failed to reinitialize ${runtimeType} within timeout`);
841
+ }
842
+ // Start runtime exit monitoring IMMEDIATELY after runtime is ready.
843
+ // Must be before postInitialize and sendRegistrationPromptAsync so exits
844
+ // during those phases are detected and the abort signal fires in time.
845
+ RuntimeExitMonitorService.getInstance().startMonitoring(sessionName, runtimeType, role);
846
+ // Start OAuth relogin monitoring for automatic re-authentication
847
+ OAuthReloginMonitorService.getInstance().startMonitoring(sessionName, runtimeType);
848
+ // Look up per-agent browser automation override from member config
849
+ let browserAutomationOverride;
850
+ if (memberId) {
851
+ try {
852
+ const teams = await this.storageService.getTeams();
853
+ for (const team of teams) {
854
+ const member = team.members?.find((m) => m.id === memberId || m.sessionName === sessionName);
855
+ if (member?.enableBrowserAutomation !== undefined) {
856
+ browserAutomationOverride = member.enableBrowserAutomation;
857
+ break;
858
+ }
859
+ }
860
+ }
861
+ catch {
862
+ // Non-fatal — will use global setting
863
+ }
864
+ }
865
+ // Run post-initialization hook (e.g., Gemini CLI directory allowlist)
866
+ try {
867
+ await runtimeService2.postInitialize(sessionName, projectPath, additionalAllowlistPaths, browserAutomationOverride);
868
+ // Drain stale terminal escape sequences (e.g. DA1 [?1;2c) that may have
869
+ // arrived during postInitialize commands, so they don't leak into the prompt input
870
+ await delay(500);
871
+ // Clear any pending input after post-initialization.
872
+ // Claude Code: Ctrl+C + Ctrl+U (clearCurrentCommandLine) — standard cleanup.
873
+ // Gemini CLI: Skip cleanup entirely — the TUI just started with a clean
874
+ // prompt. Ctrl+C at an empty Gemini CLI prompt triggers /quit and exits
875
+ // the CLI. Escape defocuses the TUI. Ctrl+U is ignored. The delay(500)
876
+ // above is sufficient to drain stale escape sequences.
877
+ if (runtimeType === RUNTIME_TYPES.CLAUDE_CODE) {
878
+ await (await this.getSessionHelper()).clearCurrentCommandLine(sessionName);
879
+ }
880
+ }
881
+ catch (postInitError) {
882
+ this.logger.warn('Post-initialization hook failed (non-fatal)', {
883
+ sessionName,
884
+ runtimeType,
885
+ error: postInitError instanceof Error ? postInitError.message : String(postInitError),
886
+ });
887
+ }
888
+ // For PTY sessions, once runtime is detected as ready, consider initialization successful
889
+ // MCP registration will happen async when the agent processes its first prompt
890
+ this.logger.info('Runtime detected as ready, session initialization successful', {
891
+ sessionName,
892
+ role,
893
+ runtimeType,
894
+ });
895
+ // Background: detect and store session ID for resume-on-restart
896
+ if (runtimeType === RUNTIME_TYPES.CLAUDE_CODE) {
897
+ const detectPath = projectPath || process.cwd();
898
+ setTimeout(() => {
899
+ this.detectAndStoreSessionId(sessionName, detectPath).catch(() => { });
900
+ }, 10000);
901
+ }
902
+ // Send the registration prompt in background (don't block on it)
903
+ this.sendRegistrationPromptAsync(sessionName, role, memberId, runtimeType).catch((err) => {
904
+ this.logger.warn('Background registration prompt failed (non-blocking)', {
905
+ sessionName,
906
+ error: err instanceof Error ? err.message : String(err),
907
+ });
908
+ });
909
+ // Update agent status to 'started' since the runtime is running
910
+ // The agent will become 'active' only after it registers via the API endpoint
911
+ try {
912
+ await this.storageService.updateAgentStatus(sessionName, CREWLY_CONSTANTS.AGENT_STATUSES.STARTED);
913
+ this.logger.info('Agent status updated to started (runtime running, awaiting registration)', { sessionName, role });
914
+ }
915
+ catch (statusError) {
916
+ this.logger.warn('Failed to update agent status (non-critical)', {
917
+ sessionName,
918
+ error: statusError instanceof Error ? statusError.message : String(statusError),
919
+ });
920
+ }
921
+ return true;
922
+ }
923
+ /**
924
+ * Detect and store the Claude Code session ID by scanning the filesystem.
925
+ * Claude Code stores conversations as <UUID>.jsonl files in
926
+ * ~/.claude/projects/<path-slug>/ where path-slug is the absolute
927
+ * project path with / replaced by -.
928
+ *
929
+ * Runs in background after agent init to capture the session ID
930
+ * for resume-on-restart support.
931
+ *
932
+ * @param sessionName - PTY session name
933
+ * @param projectPath - Absolute path to the agent's project directory
934
+ */
935
+ async detectAndStoreSessionId(sessionName, projectPath) {
936
+ try {
937
+ const slug = projectPath.replace(/\//g, '-');
938
+ const claudeProjectDir = path.join(os.homedir(), '.claude', 'projects', slug);
939
+ const files = await readdir(claudeProjectDir);
940
+ const jsonlFiles = files.filter(f => f.endsWith('.jsonl'));
941
+ if (jsonlFiles.length === 0)
942
+ return;
943
+ // Find most recent .jsonl by mtime
944
+ let latestFile = '';
945
+ let latestMtime = 0;
946
+ for (const file of jsonlFiles) {
947
+ const fileStat = await stat(path.join(claudeProjectDir, file));
948
+ if (fileStat.mtimeMs > latestMtime) {
949
+ latestMtime = fileStat.mtimeMs;
950
+ latestFile = file;
951
+ }
952
+ }
953
+ if (!latestFile)
954
+ return;
955
+ // Extract UUID from filename (remove .jsonl extension)
956
+ const sessionId = latestFile.replace('.jsonl', '');
957
+ const persistence = getSessionStatePersistence();
958
+ persistence.updateSessionId(sessionName, sessionId);
959
+ this.logger.info('Detected and stored Claude session ID from filesystem', {
960
+ sessionName,
961
+ sessionId,
962
+ claudeProjectDir,
963
+ });
964
+ }
965
+ catch (error) {
966
+ this.logger.debug('Could not detect Claude session ID from filesystem (non-fatal)', {
967
+ sessionName,
968
+ error: error instanceof Error ? error.message : String(error),
969
+ });
970
+ }
971
+ }
972
+ /**
973
+ * Provision the runtime-specific project config file.
974
+ *
975
+ * Each agent CLI reads a different file for project configuration:
976
+ * - Claude Code → .crewly/CLAUDE.md
977
+ * - Gemini CLI → GEMINI.md (project root)
978
+ * - Codex → AGENTS.md (project root)
979
+ *
980
+ * Uses 'wx' flag to avoid overwriting existing files.
981
+ *
982
+ * @param projectPath - Project directory path
983
+ * @param runtimeType - Agent runtime type
984
+ */
985
+ async provisionRuntimeConfigFile(projectPath, runtimeType) {
986
+ // Map runtime type to template file and output path
987
+ const configMap = {
988
+ [RUNTIME_TYPES.CLAUDE_CODE]: {
989
+ template: 'agent-claude-md.md',
990
+ outputPath: path.join(projectPath, '.crewly', 'CLAUDE.md'),
991
+ },
992
+ [RUNTIME_TYPES.GEMINI_CLI]: {
993
+ template: 'agent-gemini-md.md',
994
+ outputPath: path.join(projectPath, 'GEMINI.md'),
995
+ },
996
+ [RUNTIME_TYPES.CODEX_CLI]: {
997
+ template: 'agent-agents-md.md',
998
+ outputPath: path.join(projectPath, 'AGENTS.md'),
999
+ },
1000
+ };
1001
+ const config = configMap[runtimeType];
1002
+ if (!config)
1003
+ return; // Unknown runtime — skip
1004
+ try {
1005
+ // Ensure parent directory exists
1006
+ const dir = path.dirname(config.outputPath);
1007
+ await mkdir(dir, { recursive: true });
1008
+ const templatePath = path.join(this.projectRoot, 'config', 'templates', config.template);
1009
+ let content = this.promptCache.get(templatePath);
1010
+ if (!content) {
1011
+ content = await readFile(templatePath, 'utf8');
1012
+ this.promptCache.set(templatePath, content);
1013
+ }
1014
+ // Substitute path placeholders before writing (#209). The cache holds
1015
+ // the RAW template; resolve into a per-project copy so skill-script and
1016
+ // project paths in the provisioned config file point at real locations
1017
+ // (mirrors the placeholder substitution done for the registration prompt).
1018
+ const agentSkillsPath = path.join(this.projectRoot, 'config', 'skills', 'agent');
1019
+ const orchestratorSkillsPath = path.join(this.projectRoot, 'config', 'skills', 'orchestrator');
1020
+ const marketplaceSkillsPath = path.join(os.homedir(), '.crewly', 'marketplace', 'skills');
1021
+ const resolved = content
1022
+ .replace(/\{\{PROJECT_PATH\}\}/g, projectPath)
1023
+ .replace(/\{\{AGENT_SKILLS_PATH\}\}/g, agentSkillsPath)
1024
+ .replace(/\{\{ORCHESTRATOR_SKILLS_PATH\}\}/g, orchestratorSkillsPath)
1025
+ .replace(/\{\{MARKETPLACE_SKILLS_PATH\}\}/g, marketplaceSkillsPath);
1026
+ await writeFile(config.outputPath, resolved, { flag: 'wx' }).catch(() => {
1027
+ // File already exists — no action needed
1028
+ });
1029
+ }
1030
+ catch (err) {
1031
+ this.logger.warn('Could not provision runtime config file (non-critical)', {
1032
+ runtimeType,
1033
+ outputPath: config.outputPath,
1034
+ error: err instanceof Error ? err.message : String(err),
1035
+ });
1036
+ }
1037
+ }
1038
+ /**
1039
+ * Send registration prompt asynchronously (non-blocking).
1040
+ * Uses an AbortController so the operation can be cancelled if the
1041
+ * runtime exits before registration completes.
1042
+ */
1043
+ async sendRegistrationPromptAsync(sessionName, role, memberId, runtimeType = RUNTIME_TYPES.CLAUDE_CODE) {
1044
+ // Create AbortController for this registration
1045
+ const controller = new AbortController();
1046
+ this.registrationAbortControllers.set(sessionName, controller);
1047
+ try {
1048
+ this.logger.info('Loading registration prompt', { sessionName, role, runtimeType });
1049
+ if (controller.signal.aborted)
1050
+ return;
1051
+ const prompt = await this.loadRegistrationPrompt(role, sessionName, memberId, runtimeType);
1052
+ this.logger.info('Registration prompt loaded, sending to agent', {
1053
+ sessionName, role, runtimeType, promptLength: prompt.length,
1054
+ });
1055
+ if (controller.signal.aborted)
1056
+ return;
1057
+ const sent = await this.sendPromptRobustly(sessionName, prompt, runtimeType, controller.signal);
1058
+ if (sent) {
1059
+ this.logger.info('Registration prompt sent successfully', { sessionName, role });
1060
+ }
1061
+ else {
1062
+ this.logger.warn('Registration prompt delivery returned false', { sessionName, role, runtimeType });
1063
+ }
1064
+ }
1065
+ catch (error) {
1066
+ if (controller.signal.aborted) {
1067
+ this.logger.info('Registration prompt cancelled (runtime exited)', { sessionName });
1068
+ return;
1069
+ }
1070
+ this.logger.warn('Failed to send registration prompt asynchronously', {
1071
+ sessionName,
1072
+ error: error instanceof Error ? error.message : String(error),
1073
+ stack: error instanceof Error ? error.stack : undefined,
1074
+ });
1075
+ }
1076
+ finally {
1077
+ this.registrationAbortControllers.delete(sessionName);
1078
+ }
1079
+ }
1080
+ /**
1081
+ * Step 3: Kill session and recreate completely
1082
+ * Most aggressive approach - completely recreates the session from scratch
1083
+ */
1084
+ async tryFullRecreation(sessionName, role, timeout, projectPath, memberId, runtimeType = RUNTIME_TYPES.CLAUDE_CODE, runtimeFlags) {
1085
+ // Kill existing session
1086
+ await (await this.getSessionHelper()).killSession(sessionName);
1087
+ // Wait for cleanup
1088
+ await delay(1000);
1089
+ // Inject --resume flag if this was a previously running Claude Code session
1090
+ const effectiveFlags = runtimeFlags ? [...runtimeFlags] : [];
1091
+ if (runtimeType === RUNTIME_TYPES.CLAUDE_CODE) {
1092
+ try {
1093
+ const settings = await getSettingsService().getSettings();
1094
+ if (settings.general.autoResumeOnRestart) {
1095
+ const persistence = getSessionStatePersistence();
1096
+ if (persistence.isRestoredSession(sessionName)) {
1097
+ const storedSessionId = persistence.getSessionId(sessionName);
1098
+ if (storedSessionId) {
1099
+ effectiveFlags.push('--resume', storedSessionId);
1100
+ this.logger.info('Injecting --resume flag for session restore (full recreation)', {
1101
+ sessionName, sessionId: storedSessionId,
1102
+ });
1103
+ }
1104
+ }
1105
+ }
1106
+ else {
1107
+ this.logger.info('Auto-resume disabled by settings, skipping session resume after recreation', { sessionName });
1108
+ }
1109
+ }
1110
+ catch (resumeError) {
1111
+ this.logger.debug('Could not resolve resume flag (non-fatal)', {
1112
+ sessionName, error: resumeError instanceof Error ? resumeError.message : String(resumeError),
1113
+ });
1114
+ }
1115
+ }
1116
+ // Write prompt file before launching runtime so --agent (Claude Code) or --append-system-prompt-file works
1117
+ let promptFilePath;
1118
+ let agentName;
1119
+ if (runtimeType === RUNTIME_TYPES.CLAUDE_CODE) {
1120
+ try {
1121
+ const prompt = await this.loadRegistrationPrompt(role, sessionName, memberId, runtimeType);
1122
+ promptFilePath = await this.writePromptFile(sessionName, prompt, {
1123
+ runtimeType,
1124
+ projectPath,
1125
+ role,
1126
+ });
1127
+ agentName = sessionName;
1128
+ }
1129
+ catch (promptError) {
1130
+ this.logger.warn('Failed to pre-write prompt file in full recreation (non-fatal)', {
1131
+ sessionName,
1132
+ error: promptError instanceof Error ? promptError.message : String(promptError),
1133
+ });
1134
+ }
1135
+ }
1136
+ // Recreate session based on role
1137
+ if (role === ORCHESTRATOR_ROLE) {
1138
+ await this.createOrchestratorSession({
1139
+ sessionName,
1140
+ projectPath: projectPath || process.cwd(),
1141
+ });
1142
+ // Initialize runtime for orchestrator using script (always fresh start)
1143
+ const runtimeService = this.createRuntimeService(runtimeType);
1144
+ await runtimeService.executeRuntimeInitScript(sessionName, process.cwd(), effectiveFlags, promptFilePath, agentName);
1145
+ // Wait for runtime to be ready
1146
+ const checkInterval = this.getCheckInterval();
1147
+ // runtimeService3: Separate instance for orchestrator ready-state detection
1148
+ // Isolated from runtimeService to prevent interference between init and ready checks
1149
+ const runtimeService3 = this.createRuntimeService(runtimeType);
1150
+ const isReady = await runtimeService3.waitForRuntimeReady(sessionName, 45000, checkInterval);
1151
+ if (!isReady) {
1152
+ throw new Error(`Failed to initialize ${runtimeType} in recreated orchestrator session within timeout`);
1153
+ }
1154
+ // Start runtime exit monitoring immediately after runtime is ready
1155
+ RuntimeExitMonitorService.getInstance().startMonitoring(sessionName, runtimeType, role);
1156
+ // Start OAuth relogin monitoring for automatic re-authentication
1157
+ OAuthReloginMonitorService.getInstance().startMonitoring(sessionName, runtimeType);
1158
+ // Additional verification: Use runtime detection to confirm runtime is responding
1159
+ // Wait a bit longer for runtime to fully load after showing welcome message
1160
+ this.logger.debug('Runtime ready detected for orchestrator, waiting for full startup before verification', { sessionName, runtimeType });
1161
+ await delay(5000);
1162
+ // Codex CLI is sensitive to active key-probe verification (it may drop to
1163
+ // the shell when probe keys are interpreted outside the TUI). We already
1164
+ // passed waitForRuntimeReady above, so skip the extra command probe here.
1165
+ if (runtimeType !== RUNTIME_TYPES.CODEX_CLI) {
1166
+ this.logger.debug('Verifying orchestrator runtime responsiveness', {
1167
+ sessionName,
1168
+ runtimeType,
1169
+ });
1170
+ // runtimeService4: Final verification instance for orchestrator responsiveness
1171
+ // Clean instance for post-initialization responsiveness testing
1172
+ const runtimeService4 = this.createRuntimeService(runtimeType);
1173
+ const runtimeResponding = await runtimeService4.detectRuntimeWithCommand(sessionName);
1174
+ if (!runtimeResponding) {
1175
+ throw new Error(`${runtimeType} not responding to commands after orchestrator recreation`);
1176
+ }
1177
+ }
1178
+ else {
1179
+ this.logger.debug('Skipping active runtime probe for Codex orchestrator recreation', {
1180
+ sessionName,
1181
+ runtimeType,
1182
+ });
1183
+ }
1184
+ this.logger.debug('Runtime confirmed ready for orchestrator in Step 3', { sessionName, runtimeType });
1185
+ }
1186
+ else {
1187
+ // For other roles, create basic session and initialize Claude (always fresh start)
1188
+ await (await this.getSessionHelper()).createSession(sessionName, projectPath || process.cwd());
1189
+ const runtimeService = this.createRuntimeService(runtimeType);
1190
+ await runtimeService.executeRuntimeInitScript(sessionName, projectPath, effectiveFlags, promptFilePath, agentName);
1191
+ // Wait for runtime to be ready (simplified detection)
1192
+ const checkInterval = this.getCheckInterval();
1193
+ const isReady = await this.createRuntimeService(runtimeType)
1194
+ .waitForRuntimeReady(sessionName, 25000, checkInterval); // 25s timeout
1195
+ if (!isReady) {
1196
+ throw new Error(`Failed to initialize ${runtimeType} in recreated session within timeout`);
1197
+ }
1198
+ // Start runtime exit monitoring immediately after runtime is ready
1199
+ RuntimeExitMonitorService.getInstance().startMonitoring(sessionName, runtimeType, role);
1200
+ // Start OAuth relogin monitoring for automatic re-authentication
1201
+ OAuthReloginMonitorService.getInstance().startMonitoring(sessionName, runtimeType);
1202
+ }
1203
+ // Look up per-agent browser automation override from member config
1204
+ let browserOverrideForRecreation;
1205
+ if (memberId) {
1206
+ try {
1207
+ const teams = await this.storageService.getTeams();
1208
+ for (const team of teams) {
1209
+ const member = team.members?.find((m) => m.id === memberId || m.sessionName === sessionName);
1210
+ if (member?.enableBrowserAutomation !== undefined) {
1211
+ browserOverrideForRecreation = member.enableBrowserAutomation;
1212
+ break;
1213
+ }
1214
+ }
1215
+ }
1216
+ catch {
1217
+ // Non-fatal — will use global setting
1218
+ }
1219
+ }
1220
+ // Run post-initialization hook (e.g., Gemini CLI directory allowlist)
1221
+ try {
1222
+ const postInitService = this.createRuntimeService(runtimeType);
1223
+ await postInitService.postInitialize(sessionName, projectPath, undefined, browserOverrideForRecreation);
1224
+ // Drain stale terminal escape sequences (e.g. DA1 [?1;2c) that may have
1225
+ // arrived during postInitialize commands, so they don't leak into the prompt input
1226
+ await delay(500);
1227
+ // Claude Code: Ctrl+C + Ctrl+U to clear any stale input.
1228
+ // Gemini CLI (Ink TUI): Do NOT send any cleanup keystrokes.
1229
+ // Escape defocuses the Ink TUI input permanently. Ctrl+C at empty
1230
+ // prompt triggers /quit. Ctrl+U is ignored. The delay above is
1231
+ // sufficient to drain stale escape sequences.
1232
+ if (runtimeType === RUNTIME_TYPES.CLAUDE_CODE) {
1233
+ await (await this.getSessionHelper()).clearCurrentCommandLine(sessionName);
1234
+ }
1235
+ }
1236
+ catch (postInitError) {
1237
+ this.logger.warn('Post-initialization hook failed after recreation (non-fatal)', {
1238
+ sessionName,
1239
+ runtimeType,
1240
+ error: postInitError instanceof Error ? postInitError.message : String(postInitError),
1241
+ });
1242
+ }
1243
+ // For PTY sessions, once runtime is detected as ready, consider initialization successful
1244
+ this.logger.info('Runtime detected as ready after full recreation, session initialization successful', {
1245
+ sessionName,
1246
+ role,
1247
+ runtimeType,
1248
+ });
1249
+ // Background: detect and store session ID for resume-on-restart
1250
+ if (runtimeType === RUNTIME_TYPES.CLAUDE_CODE) {
1251
+ const detectPath = projectPath || process.cwd();
1252
+ setTimeout(() => {
1253
+ this.detectAndStoreSessionId(sessionName, detectPath).catch(() => { });
1254
+ }, 10000);
1255
+ }
1256
+ // Send the registration prompt in background (don't block on it)
1257
+ this.sendRegistrationPromptAsync(sessionName, role, memberId, runtimeType).catch((err) => {
1258
+ this.logger.warn('Background registration prompt failed after recreation (non-blocking)', {
1259
+ sessionName,
1260
+ error: err instanceof Error ? err.message : String(err),
1261
+ });
1262
+ });
1263
+ // Update agent status to 'started' since the runtime is running
1264
+ // The agent will become 'active' only after it registers via the API endpoint
1265
+ try {
1266
+ await this.storageService.updateAgentStatus(sessionName, CREWLY_CONSTANTS.AGENT_STATUSES.STARTED);
1267
+ this.logger.info('Agent status updated to started after recreation (awaiting registration)', { sessionName, role });
1268
+ }
1269
+ catch (statusError) {
1270
+ this.logger.warn('Failed to update agent status after recreation (non-critical)', {
1271
+ sessionName,
1272
+ error: statusError instanceof Error ? statusError.message : String(statusError),
1273
+ });
1274
+ }
1275
+ return true;
1276
+ }
1277
+ /**
1278
+ * Register a crewly-agent member as active by updating its team config directly.
1279
+ * In-process agents cannot run bash scripts, so this performs the registration
1280
+ * that the register-self skill would normally do for PTY-based runtimes.
1281
+ *
1282
+ * @param sessionName - Agent session name
1283
+ * @param role - Agent role
1284
+ * @param memberId - Optional member UUID
1285
+ * @returns True if registration succeeded
1286
+ */
1287
+ async registerMemberActive(sessionName, role, memberId) {
1288
+ try {
1289
+ // Handle orchestrator separately — its config is stored outside getTeams()
1290
+ if (sessionName === ORCHESTRATOR_SESSION_NAME) {
1291
+ await this.storageService.updateOrchestratorStatus(CREWLY_CONSTANTS.AGENT_STATUSES.ACTIVE);
1292
+ this.logger.info('Orchestrator registered as active via config', { sessionName });
1293
+ return true;
1294
+ }
1295
+ const teams = await this.storageService.getTeams();
1296
+ for (const team of teams) {
1297
+ const member = team.members.find((m) => (m.sessionName === sessionName) ||
1298
+ (memberId !== undefined && m.id === memberId));
1299
+ if (member) {
1300
+ member.agentStatus = 'active';
1301
+ member.workingStatus = 'idle';
1302
+ member.updatedAt = new Date().toISOString();
1303
+ // Pre-populate sessionName so register_self tool can find this member
1304
+ if (!member.sessionName && sessionName) {
1305
+ member.sessionName = sessionName;
1306
+ }
1307
+ await this.storageService.saveTeam(team);
1308
+ this.logger.info('Member registered as active', { sessionName, teamId: team.id });
1309
+ // #167: Drain dead-letter queue for newly active agent
1310
+ if (this.schedulerService) {
1311
+ this.schedulerService.drainDeadLetterQueue(sessionName).catch((err) => {
1312
+ this.logger.warn('Failed to drain DLQ on activation (non-critical)', {
1313
+ sessionName,
1314
+ error: err.message,
1315
+ });
1316
+ });
1317
+ }
1318
+ // Architecture Upgrade Phase 6: set up standing task event subscriptions
1319
+ this.setupStandingSubscriptions(sessionName, member.canDelegate ? 'team-lead' : 'executor', team.id);
1320
+ return true;
1321
+ }
1322
+ }
1323
+ this.logger.warn('Could not find member to register as active', { sessionName, memberId });
1324
+ return false;
1325
+ }
1326
+ catch (error) {
1327
+ this.logger.error('Failed to register member as active', {
1328
+ sessionName,
1329
+ error: error instanceof Error ? error.message : String(error),
1330
+ });
1331
+ return false;
1332
+ }
1333
+ }
1334
+ /**
1335
+ * Set up standing event subscriptions for task workflow events based on org role.
1336
+ * Called once at agent registration. Subscriptions are long-lived (24h TTL)
1337
+ * and NOT oneShot — they persist for the agent's session lifetime.
1338
+ *
1339
+ * Orchestrator: task:verified, task:blocked, task:failed (all teams)
1340
+ * Team Lead: task:done, task:blocked, task:failed, task:needs_clarification (own team)
1341
+ * Executor: task:assigned (self scope)
1342
+ *
1343
+ * @param sessionName - Agent session name
1344
+ * @param orgRole - Organizational role (orchestrator, team-lead, executor)
1345
+ * @param teamId - Team ID for scoping (optional for orchestrator)
1346
+ */
1347
+ setupStandingSubscriptions(sessionName, orgRole, teamId) {
1348
+ if (!this.eventBusService) {
1349
+ this.logger.debug('EventBusService not available, skipping standing subscriptions', { sessionName });
1350
+ return;
1351
+ }
1352
+ const TTL_MINUTES = 60 * 24; // 24 hours
1353
+ try {
1354
+ if (orgRole === 'orchestrator') {
1355
+ // Orchestrator: awareness-only subscriptions for all teams
1356
+ for (const eventType of ['task:verified', 'task:blocked', 'task:failed', 'team:all_tasks_done']) {
1357
+ this.eventBusService.subscribe({
1358
+ eventType,
1359
+ filter: {},
1360
+ subscriberSession: sessionName,
1361
+ oneShot: false,
1362
+ ttlMinutes: TTL_MINUTES,
1363
+ });
1364
+ }
1365
+ this.logger.info('Standing subscriptions set up for orchestrator', { sessionName });
1366
+ }
1367
+ else if (orgRole === 'team-lead') {
1368
+ // TL: own team scope
1369
+ for (const eventType of ['task:done', 'task:blocked', 'task:failed', 'task:needs_clarification']) {
1370
+ this.eventBusService.subscribe({
1371
+ eventType,
1372
+ filter: teamId ? { teamId } : {},
1373
+ subscriberSession: sessionName,
1374
+ oneShot: false,
1375
+ ttlMinutes: TTL_MINUTES,
1376
+ });
1377
+ }
1378
+ this.logger.info('Standing subscriptions set up for team-lead', { sessionName, teamId });
1379
+ }
1380
+ else {
1381
+ // Executor: self scope only
1382
+ this.eventBusService.subscribe({
1383
+ eventType: 'task:assigned',
1384
+ filter: { sessionName },
1385
+ subscriberSession: sessionName,
1386
+ oneShot: false,
1387
+ ttlMinutes: TTL_MINUTES,
1388
+ });
1389
+ this.logger.info('Standing subscription set up for executor', { sessionName });
1390
+ }
1391
+ }
1392
+ catch (err) {
1393
+ this.logger.warn('Failed to set up standing subscriptions (non-fatal)', {
1394
+ sessionName,
1395
+ orgRole,
1396
+ error: err instanceof Error ? err.message : String(err),
1397
+ });
1398
+ }
1399
+ }
1400
+ /**
1401
+ * Load registration prompt from config files (with caching to prevent file I/O contention)
1402
+ */
1403
+ async loadRegistrationPrompt(role, sessionName, memberId, runtimeType = RUNTIME_TYPES.CLAUDE_CODE) {
1404
+ try {
1405
+ // Cache key is role only - the template file is the same regardless of memberId
1406
+ const cacheKey = role;
1407
+ // Check cache first to avoid file I/O during concurrent operations
1408
+ if (!this.promptCache.has(cacheKey)) {
1409
+ this.logger.debug('Loading prompt template from file', { role, cacheKey });
1410
+ // Get the prompt file path from team roles configuration
1411
+ const promptPath = await this.getPromptFileForRole(role);
1412
+ const promptTemplate = await readFile(promptPath, 'utf8');
1413
+ this.promptCache.set(cacheKey, promptTemplate);
1414
+ this.logger.debug('Prompt template cached', { role, cacheKey, promptPath });
1415
+ }
1416
+ else {
1417
+ this.logger.debug('Using cached prompt template', { role, cacheKey });
1418
+ }
1419
+ // Get cached template and apply variable replacements
1420
+ let prompt = this.promptCache.get(cacheKey);
1421
+ // Replace all template placeholders
1422
+ prompt = prompt.replace(/\{\{SESSION_ID\}\}/g, sessionName);
1423
+ prompt = prompt.replace(/\{\{SESSION_NAME\}\}/g, sessionName);
1424
+ prompt = prompt.replace(/\{\{ROLE\}\}/g, role);
1425
+ if (memberId) {
1426
+ prompt = prompt.replace(/\{\{MEMBER_ID\}\}/g, memberId);
1427
+ }
1428
+ else {
1429
+ // For orchestrator or cases without member ID, remove the memberId parameter
1430
+ prompt = prompt.replace(/,\s*"memberId":\s*"\{\{MEMBER_ID\}\}"/g, '');
1431
+ }
1432
+ // Look up project path and TL hierarchy for team members
1433
+ let projectPath = process.cwd();
1434
+ let foundTeam = null;
1435
+ let foundMember = null;
1436
+ try {
1437
+ const teams = await this.storageService.getTeams();
1438
+ for (const team of teams) {
1439
+ const member = team.members?.find((m) => m.sessionName === sessionName);
1440
+ if (member) {
1441
+ foundTeam = team;
1442
+ foundMember = member;
1443
+ if (team.projectIds[0]) {
1444
+ const projects = await this.storageService.getProjects();
1445
+ const project = projects.find((p) => p.id === team.projectIds[0]);
1446
+ if (project?.path) {
1447
+ projectPath = project.path;
1448
+ }
1449
+ }
1450
+ break;
1451
+ }
1452
+ }
1453
+ }
1454
+ catch {
1455
+ // Non-critical - use default project path
1456
+ }
1457
+ // Write runtime-specific project config file so the agent CLI
1458
+ // recognizes Crewly as a legitimate project configuration (fixes #33)
1459
+ // Claude Code → .crewly/CLAUDE.md, Gemini CLI → GEMINI.md, Codex → AGENTS.md
1460
+ if (role !== ORCHESTRATOR_ROLE) {
1461
+ await this.provisionRuntimeConfigFile(projectPath, runtimeType);
1462
+ }
1463
+ // Replace project path placeholder (must happen after project path lookup above)
1464
+ prompt = prompt.replace(/\{\{PROJECT_PATH\}\}/g, projectPath);
1465
+ // Replace agent skills path placeholder
1466
+ const agentSkillsPath = path.join(this.projectRoot, 'config', 'skills', 'agent');
1467
+ prompt = prompt.replace(/\{\{AGENT_SKILLS_PATH\}\}/g, agentSkillsPath);
1468
+ // Replace orchestrator skills path placeholder — ensures skill scripts
1469
+ // resolve correctly regardless of the orchestrator's working directory
1470
+ const orchestratorSkillsPath = path.join(this.projectRoot, 'config', 'skills', 'orchestrator');
1471
+ prompt = prompt.replace(/\{\{ORCHESTRATOR_SKILLS_PATH\}\}/g, orchestratorSkillsPath);
1472
+ // Replace marketplace skills path placeholder
1473
+ const marketplaceSkillsPath = path.join(os.homedir(), '.crewly', 'marketplace', 'skills');
1474
+ prompt = prompt.replace(/\{\{MARKETPLACE_SKILLS_PATH\}\}/g, marketplaceSkillsPath);
1475
+ // #210: Inject member's systemPrompt if available
1476
+ // This contains role context and authorization info that the agent needs at startup
1477
+ if (foundTeam && foundMember?.id) {
1478
+ try {
1479
+ const memberPrompt = await this.storageService.getMemberPrompt(foundTeam.id, foundMember.id);
1480
+ if (memberPrompt && memberPrompt.trim()) {
1481
+ prompt = memberPrompt.trim() + '\n\n' + prompt;
1482
+ this.logger.info('Member systemPrompt prepended to init prompt', {
1483
+ sessionName,
1484
+ memberId: foundMember.id,
1485
+ systemPromptLength: memberPrompt.length,
1486
+ });
1487
+ }
1488
+ }
1489
+ catch (err) {
1490
+ this.logger.warn('Failed to load member systemPrompt (non-critical)', {
1491
+ sessionName,
1492
+ memberId: foundMember.id,
1493
+ error: err instanceof Error ? err.message : String(err),
1494
+ });
1495
+ }
1496
+ }
1497
+ // Inject skill authorization section — tells agents about available capabilities
1498
+ prompt += '\n\n## Available Capabilities\n\n'
1499
+ + 'This session has browser automation enabled via the Playwright MCP server. '
1500
+ + 'Browser skills (e.g. remote-browser) are the preferred way to perform browser tasks. '
1501
+ + 'Bash skills in the Crewly skills directory are available for team communication and status reporting.';
1502
+ // Inject Team Lead addon for members with canDelegate=true and subordinates
1503
+ if (foundMember?.canDelegate && foundMember.subordinateIds && foundMember.subordinateIds.length > 0 && foundTeam) {
1504
+ try {
1505
+ // Resolve subordinateIds to SubordinateInfo[]
1506
+ const subordinates = foundMember.subordinateIds
1507
+ .map((subId) => {
1508
+ const subMember = foundTeam.members?.find((m) => m.id === subId);
1509
+ if (!subMember)
1510
+ return null;
1511
+ return {
1512
+ name: subMember.name,
1513
+ sessionName: subMember.sessionName || '',
1514
+ role: subMember.role || 'developer',
1515
+ memberId: subMember.id || subId,
1516
+ };
1517
+ })
1518
+ .filter((s) => s !== null);
1519
+ if (subordinates.length > 0) {
1520
+ const tlConfig = {
1521
+ name: sessionName,
1522
+ role: role,
1523
+ systemPrompt: '',
1524
+ projectPath,
1525
+ memberId,
1526
+ teamId: foundTeam.id,
1527
+ canDelegate: true,
1528
+ subordinates,
1529
+ };
1530
+ const promptBuilder = new PromptBuilderService(this.projectRoot);
1531
+ const tlSection = await promptBuilder.buildTeamLeadSection(tlConfig);
1532
+ if (tlSection) {
1533
+ prompt += `\n\n---\n\n${tlSection}`;
1534
+ this.logger.info('TL addon injected into init prompt', {
1535
+ sessionName,
1536
+ subordinateCount: subordinates.length,
1537
+ subordinateNames: subordinates.map((s) => s.name),
1538
+ tlSectionLength: tlSection.length,
1539
+ });
1540
+ }
1541
+ }
1542
+ }
1543
+ catch (tlError) {
1544
+ this.logger.warn('Failed to inject TL addon (non-critical)', {
1545
+ sessionName,
1546
+ error: tlError instanceof Error ? tlError.message : String(tlError),
1547
+ });
1548
+ }
1549
+ }
1550
+ // Issue #395: Inject the authoritative active-work briefing FIRST,
1551
+ // so the agent's reading order is "what am I on the hook for RIGHT
1552
+ // NOW" before the supplementary memory context. Soft-fail with WARN
1553
+ // — TaskPool/Request hiccups must not block agent boot.
1554
+ try {
1555
+ // ActiveWorkBriefingService.getInstance() is async — see service
1556
+ // JSDoc. Dynamic-import migration was the build-fix for PR #494's
1557
+ // broken relative-path resolution under entry-script anchoring.
1558
+ const activeWorkService = await ActiveWorkBriefingService.getInstance();
1559
+ const activeWorkBriefing = await activeWorkService.generateActiveWorkBriefing(sessionName, role);
1560
+ const activeWorkMd = activeWorkService.formatBriefingAsMarkdown(activeWorkBriefing);
1561
+ if (activeWorkMd && activeWorkMd.length > 30) {
1562
+ prompt += `\n\n---\n\n${activeWorkMd}`;
1563
+ this.logger.info('Active-work briefing injected into prompt', {
1564
+ sessionName,
1565
+ role,
1566
+ openRequests: activeWorkBriefing.openRequests.length,
1567
+ activeWorkItems: activeWorkBriefing.activeWorkItems.length,
1568
+ pendingReviews: activeWorkBriefing.pendingReviews.length,
1569
+ outboundDelegations: activeWorkBriefing.outboundDelegations.length,
1570
+ recentlyAutoResolved: activeWorkBriefing.recentlyAutoResolved.length,
1571
+ truncated: activeWorkBriefing.truncated,
1572
+ briefingLength: activeWorkMd.length,
1573
+ });
1574
+ }
1575
+ }
1576
+ catch (activeWorkError) {
1577
+ this.logger.warn('Failed to generate active-work briefing (non-critical)', {
1578
+ sessionName,
1579
+ error: activeWorkError instanceof Error ? activeWorkError.message : String(activeWorkError),
1580
+ });
1581
+ }
1582
+ // Generate and inject startup briefing from session memory
1583
+ try {
1584
+ const sessionMemoryService = SessionMemoryService.getInstance();
1585
+ await sessionMemoryService.onSessionStart(sessionName, role, projectPath);
1586
+ const briefing = await sessionMemoryService.generateStartupBriefing(sessionName, role, projectPath);
1587
+ const briefingMd = sessionMemoryService.formatBriefingAsMarkdown(briefing);
1588
+ if (briefingMd && briefingMd.length > 30) {
1589
+ prompt += `\n\n---\n\n${briefingMd}`;
1590
+ this.logger.info('Startup briefing injected into prompt', { sessionName, role, briefingLength: briefingMd.length });
1591
+ }
1592
+ }
1593
+ catch (briefingError) {
1594
+ this.logger.warn('Failed to generate startup briefing (non-critical)', {
1595
+ sessionName,
1596
+ error: briefingError instanceof Error ? briefingError.message : String(briefingError),
1597
+ });
1598
+ }
1599
+ // Inject mandatory session recovery protocol (recall + get-my-context)
1600
+ try {
1601
+ const recoveryBuilder = new PromptBuilderService(this.projectRoot);
1602
+ const recoverySection = recoveryBuilder.buildSessionRecoverySection(sessionName, role, projectPath);
1603
+ prompt += `\n\n---\n\n${recoverySection}`;
1604
+ this.logger.info('Session recovery protocol injected into prompt', { sessionName, role });
1605
+ }
1606
+ catch (recoveryError) {
1607
+ this.logger.warn('Failed to inject session recovery protocol (non-critical)', {
1608
+ sessionName,
1609
+ error: recoveryError instanceof Error ? recoveryError.message : String(recoveryError),
1610
+ });
1611
+ }
1612
+ // Append identity section so the agent knows its session name and project path
1613
+ prompt += `\n\n---\n\n## Your Identity\n- **Session Name:** ${sessionName}\n- **Project Path:** ${projectPath}`;
1614
+ if (memberId) {
1615
+ prompt += `\n- **Member ID:** ${memberId}`;
1616
+ }
1617
+ // Conditionally append Self Evolution instructions for the orchestrator
1618
+ if (role === ORCHESTRATOR_ROLE) {
1619
+ try {
1620
+ const settings = await getSettingsService().getSettings();
1621
+ if (settings.general.enableSelfEvolution) {
1622
+ const selfEvoPath = path.join(this.projectRoot, 'config', 'roles', 'orchestrator', 'self-evolution.md');
1623
+ const selfEvoPrompt = await readFile(selfEvoPath, 'utf8');
1624
+ prompt += `\n\n---\n\n${selfEvoPrompt}`;
1625
+ this.logger.info('Self Evolution prompt injected', { sessionName });
1626
+ }
1627
+ }
1628
+ catch (selfEvoError) {
1629
+ this.logger.warn('Failed to load self-evolution prompt (non-critical)', {
1630
+ sessionName,
1631
+ error: selfEvoError instanceof Error ? selfEvoError.message : String(selfEvoError),
1632
+ });
1633
+ }
1634
+ }
1635
+ // Phase 5: Use modular prompt assembly (enabled by default, disable with CREWLY_USE_MODULAR_PROMPTS=false).
1636
+ // Replaces the entire monolithic template with PromptAssemblyService — handling identity,
1637
+ // skills, communication, recovery, lifecycle, memory, soul, and team-reference modules
1638
+ // with token budget enforcement.
1639
+ if (process.env.CREWLY_USE_MODULAR_PROMPTS !== 'false') {
1640
+ this.logger.info('Using modular prompt assembly (Phase 5)', { sessionName, role });
1641
+ const agentSkillsPath = path.join(this.projectRoot, 'config', 'skills', 'agent');
1642
+ const tlSkillsPath = path.join(this.projectRoot, 'config', 'skills', 'team-leader');
1643
+ // Pre-compute the subordinate roster — used by both the helper
1644
+ // path (passed via runtime.subordinates) and the orchestrator
1645
+ // fallback path below.
1646
+ const subordinates = foundMember?.subordinateIds
1647
+ ?.map((subId) => {
1648
+ const subMember = foundTeam?.members?.find((m) => m.id === subId);
1649
+ if (!subMember)
1650
+ return null;
1651
+ return {
1652
+ name: subMember.name,
1653
+ sessionName: subMember.sessionName || '',
1654
+ role: subMember.role || 'developer',
1655
+ memberId: subMember.id || subId,
1656
+ };
1657
+ })
1658
+ .filter((s) => s !== null);
1659
+ const teamNormsPath = foundTeam?.id
1660
+ ? path.join(os.homedir(), CREWLY_CONSTANTS.PATHS.CREWLY_HOME, 'teams', foundTeam.id, 'norms')
1661
+ : undefined;
1662
+ let moduleConfig;
1663
+ if (foundMember && foundTeam) {
1664
+ // WIRE-2: When the agent is a TeamMember of a Team, build the
1665
+ // canonical ModuleConfig via the shared helper so every
1666
+ // autonomy / org / team-level field reaches the assembler.
1667
+ // Caller-side overlay covers the two registration-specific
1668
+ // fields the helper doesn't know about (teamNormsPath +
1669
+ // the registration's `memberId` override which may differ
1670
+ // from member.id when the caller passed an explicit id).
1671
+ const runtime = {
1672
+ sessionName,
1673
+ projectPath,
1674
+ runtimeType: runtimeType,
1675
+ subordinates,
1676
+ agentSkillsPath,
1677
+ tlSkillsPath,
1678
+ projectRoot: this.projectRoot,
1679
+ };
1680
+ moduleConfig = {
1681
+ ...buildModuleConfigFromTeamMember(foundMember, foundTeam, runtime),
1682
+ memberId: memberId ?? foundMember.id ?? '',
1683
+ teamNormsPath,
1684
+ };
1685
+ }
1686
+ else {
1687
+ // Orchestrator + legacy paths: foundMember/foundTeam not
1688
+ // present (orchestrator config lives outside getTeams()).
1689
+ // Keep the inline fallback so registration still works for
1690
+ // the orchestrator and any caller without team context.
1691
+ moduleConfig = {
1692
+ sessionName,
1693
+ memberId: memberId ?? '',
1694
+ role,
1695
+ teamId: foundTeam?.id,
1696
+ projectPath,
1697
+ runtimeType: runtimeType,
1698
+ canDelegate: foundMember?.canDelegate,
1699
+ subordinates,
1700
+ agentSkillsPath,
1701
+ tlSkillsPath,
1702
+ projectRoot: this.projectRoot,
1703
+ orgRole: role === 'orchestrator' ? 'orchestrator'
1704
+ : foundMember?.canDelegate ? 'team-lead'
1705
+ : 'executor',
1706
+ autonomyLevel: foundMember?.autonomyLevel ?? 'directed',
1707
+ teamNormsPath,
1708
+ };
1709
+ }
1710
+ const assembler = new PromptAssemblyService();
1711
+ const { prompt: modularPrompt, report } = await assembler.assemble(moduleConfig);
1712
+ this.logger.info('Modular prompt assembled for registration', {
1713
+ sessionName,
1714
+ role,
1715
+ totalTokens: report.totalTokens,
1716
+ moduleCount: report.moduleBreakdown.length,
1717
+ truncatedCount: report.truncated.length,
1718
+ modules: report.moduleBreakdown.map(m => m.name),
1719
+ });
1720
+ return modularPrompt;
1721
+ }
1722
+ return prompt;
1723
+ }
1724
+ catch (error) {
1725
+ // Fallback to inline prompt if file doesn't exist
1726
+ const attemptedPath = await this.getPromptFileForRole(role);
1727
+ // Distinguish "role prompt simply absent" (team-config uses a
1728
+ // custom role string, fallback covers it) from genuine read
1729
+ // failures (permission denied, corrupt file). ENOENT is a
1730
+ // common case — e.g. CE team config currently uses
1731
+ // `role: "tech-lead"` while the filesystem canonical is
1732
+ // `team-leader`. The fallback inline prompt is functionally
1733
+ // sufficient; ERROR-level noise just buries real issues.
1734
+ const msg = error instanceof Error ? error.message : String(error);
1735
+ const isMissingFile = /ENOENT/.test(msg);
1736
+ this.logger[isMissingFile ? 'warn' : 'error']('Could not load prompt from config, using fallback', {
1737
+ role,
1738
+ promptPath: attemptedPath,
1739
+ error: msg,
1740
+ ...(isMissingFile ? {} : { stack: error instanceof Error ? error.stack : undefined }),
1741
+ });
1742
+ const memberIdJson = memberId ? `,"teamMemberId":"${memberId}"` : '';
1743
+ const skillsPath = path.join(this.projectRoot, 'config', 'skills', 'agent');
1744
+ return `Hey! I need your help with some tasks on this project.
1745
+
1746
+ ## Quick context about this setup
1747
+
1748
+ This project uses Crewly for team coordination. You have bash skills available at \`${skillsPath}/\` that communicate with the Crewly backend running locally. Read the skills catalog at \`~/.crewly/skills/AGENT_SKILLS_CATALOG.md\` for a full reference of available skills.
1749
+
1750
+ ## First thing - please check in
1751
+
1752
+ Please run the register-self skill to let the team dashboard know you're available:
1753
+ \`\`\`bash
1754
+ bash ${skillsPath}/core/register-self/execute.sh '{"role":"${role}","sessionName":"${sessionName}"${memberIdJson}}'
1755
+ \`\`\`
1756
+ All it does is update a local status flag so the web UI shows you as online - nothing more.
1757
+
1758
+ ## Default Execution Loop
1759
+
1760
+ When assigned a task, do not wait passively.
1761
+
1762
+ Loop until done, blocked, or explicitly reassigned:
1763
+ 1. Restate the expected outcome in one sentence.
1764
+ 2. Identify the fastest safe path to produce a usable result.
1765
+ 3. Execute immediately.
1766
+ 4. Run cheapest meaningful validation.
1767
+ 5. If validation fails, fix and retry.
1768
+ 6. If blocked by missing goal/outcome/eval, escalate to your TL.
1769
+ 7. If blocked by implementation detail, decide reasonably and continue.
1770
+ 8. Report only when you have a result, blocker, or decision exceeding your authority.`;
1771
+ }
1772
+ }
1773
+ /**
1774
+ * Write the registration prompt to a file on disk.
1775
+ *
1776
+ * For Claude Code agents (#207): writes to `{projectPath}/.claude/agents/{sessionName}.md`
1777
+ * with YAML frontmatter so the `--agent` flag can load it as a custom agent definition.
1778
+ * For other runtimes: writes to `~/.crewly/prompts/{sessionName}-init.md`.
1779
+ *
1780
+ * @param sessionName - Session name (used for filename)
1781
+ * @param prompt - The full prompt content
1782
+ * @param options - Optional: projectPath, runtimeType, and role for claude-code agent mode
1783
+ * @returns The absolute path to the written file, or undefined on error
1784
+ */
1785
+ async writePromptFile(sessionName, prompt, options) {
1786
+ const isClaudeAgent = options?.runtimeType === RUNTIME_TYPES.CLAUDE_CODE && options?.projectPath;
1787
+ const promptFilePath = isClaudeAgent
1788
+ ? path.join(options.projectPath, '.claude', 'agents', `${sessionName}.md`)
1789
+ : this.getInitPromptFilePath(sessionName);
1790
+ const promptsDir = path.dirname(promptFilePath);
1791
+ // Quote YAML values to prevent injection via sessionName or role containing special chars
1792
+ const yamlSafeName = sessionName.replace(/"/g, '\\"');
1793
+ const yamlSafeRole = (options?.role || 'developer').replace(/"/g, '\\"');
1794
+ const fileContent = isClaudeAgent
1795
+ ? `---\nname: "${yamlSafeName}"\ndescription: "${yamlSafeRole} agent for Crewly orchestration"\n---\n\n${prompt}`
1796
+ : prompt;
1797
+ try {
1798
+ await mkdir(promptsDir, { recursive: true });
1799
+ await writeFile(promptFilePath, fileContent, 'utf8');
1800
+ this.logger.debug('Wrote init prompt to file', {
1801
+ sessionName,
1802
+ promptFilePath,
1803
+ promptLength: fileContent.length,
1804
+ mode: isClaudeAgent ? 'claude-agent' : 'legacy',
1805
+ });
1806
+ return promptFilePath;
1807
+ }
1808
+ catch (error) {
1809
+ this.logger.warn('Failed to write init prompt file', {
1810
+ sessionName,
1811
+ promptFilePath,
1812
+ error: error instanceof Error ? error.message : String(error),
1813
+ });
1814
+ return undefined;
1815
+ }
1816
+ }
1817
+ /**
1818
+ * Build the unified init prompt path used by non-Claude-Code runtimes.
1819
+ *
1820
+ * Anchored on {@link getCrewlyHomePath} so test profiles, KR3 livewalk
1821
+ * dry-runs, and ESTestNode acceptance harnesses that export
1822
+ * `CREWLY_HOME=/tmp/...` write into the test profile, not the
1823
+ * developer's real `~/.crewly/prompts`. WI-F (2026-05-07): the
1824
+ * previous `path.join(os.homedir(), CREWLY_CONSTANTS.PATHS.CREWLY_HOME,
1825
+ * ...)` form ignored the env var and let Mia's KR3 livewalk overwrite
1826
+ * 27 occurrences of `~/.crewly/prompts/crewly-orc-init.md` and 26 of
1827
+ * `crewly-auditor-init.md` in Steve's user-prod tree at 02:37Z.
1828
+ */
1829
+ getInitPromptFilePath(sessionName) {
1830
+ return path.join(getCrewlyHomePath(), 'prompts', `${sessionName}-init.md`);
1831
+ }
1832
+ /**
1833
+ * Wait for agent registration to complete
1834
+ */
1835
+ async waitForRegistration(sessionName, role, timeout) {
1836
+ const startTime = Date.now();
1837
+ const checkInterval = 5000; // Check every 5 seconds to prevent overlapping with `/` detection
1838
+ while (Date.now() - startTime < timeout) {
1839
+ try {
1840
+ if (await this.checkAgentRegistration(sessionName, role)) {
1841
+ this.logger.info('Agent registration confirmed', { sessionName, role });
1842
+ return true;
1843
+ }
1844
+ await delay(checkInterval);
1845
+ }
1846
+ catch (error) {
1847
+ this.logger.warn('Error checking registration', {
1848
+ sessionName,
1849
+ role,
1850
+ error: error instanceof Error ? error.message : String(error),
1851
+ });
1852
+ await delay(checkInterval);
1853
+ }
1854
+ }
1855
+ this.logger.warn('Timeout waiting for agent registration', { sessionName, role, timeout });
1856
+ return false;
1857
+ }
1858
+ /**
1859
+ * Check if agent is properly registered
1860
+ */
1861
+ async checkAgentRegistration(sessionName, role) {
1862
+ try {
1863
+ if (role === ORCHESTRATOR_ROLE) {
1864
+ // For orchestrator, check agentStatus is active
1865
+ const orchestratorStatus = await this.storageService.getOrchestratorStatus();
1866
+ return orchestratorStatus?.agentStatus === CREWLY_CONSTANTS.AGENT_STATUSES.ACTIVE;
1867
+ }
1868
+ // For team members, check teams data
1869
+ const teams = await this.storageService.getTeams();
1870
+ // Find team member with matching sessionName and check agentStatus
1871
+ for (const team of teams) {
1872
+ if (team.members) {
1873
+ for (const member of team.members) {
1874
+ if (member.sessionName === sessionName && member.role === role) {
1875
+ return member.agentStatus === CREWLY_CONSTANTS.AGENT_STATUSES.ACTIVE;
1876
+ }
1877
+ }
1878
+ }
1879
+ }
1880
+ return false;
1881
+ }
1882
+ catch (error) {
1883
+ this.logger.debug('Error checking agent registration', {
1884
+ sessionName,
1885
+ role,
1886
+ error: error instanceof Error ? error.message : String(error),
1887
+ });
1888
+ return false;
1889
+ }
1890
+ }
1891
+ /**
1892
+ * Fast registration with verification and retry mechanism
1893
+ * Sends system prompt to runtime which triggers MCP registration via teams.json
1894
+ * @param skipInitialCleanup If true, skips Ctrl+C on first attempt (when runtime was just initialized)
1895
+ */
1896
+ async attemptRegistrationWithVerification(sessionName, role, timeout, memberId, maxRetries = 3, skipInitialCleanup = false, runtimeType = RUNTIME_TYPES.CLAUDE_CODE) {
1897
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
1898
+ this.logger.info('Attempting system prompt registration with MCP flow', {
1899
+ sessionName,
1900
+ role,
1901
+ runtimeType,
1902
+ attempt,
1903
+ maxRetries,
1904
+ });
1905
+ try {
1906
+ // Step 1: Send Ctrl+C to clear any pending commands (skip on first attempt if Claude was just initialized)
1907
+ if (!skipInitialCleanup || attempt > 1) {
1908
+ await (await this.getSessionHelper()).clearCurrentCommandLine(sessionName);
1909
+ await delay(500);
1910
+ this.logger.debug('Sent Ctrl+C to clear terminal state', {
1911
+ sessionName,
1912
+ attempt,
1913
+ });
1914
+ }
1915
+ else {
1916
+ this.logger.debug('Skipping Ctrl+C on first attempt (Claude was just initialized)', { sessionName, attempt });
1917
+ }
1918
+ // Step 2: Verify runtime is running (skip detection if runtime was just initialized and verified)
1919
+ let runtimeRunning = true; // Assume true if we skip detection
1920
+ if (!skipInitialCleanup || attempt > 1) {
1921
+ // Only do runtime detection on retries or when runtime wasn't just initialized
1922
+ const forceRefresh = attempt > 1; // Force refresh on retry attempts
1923
+ const runtimeService5 = this.createRuntimeService(runtimeType);
1924
+ runtimeRunning = await runtimeService5.detectRuntimeWithCommand(sessionName, forceRefresh);
1925
+ if (!runtimeRunning) {
1926
+ this.logger.warn('Runtime not detected, cannot send system prompt', {
1927
+ sessionName,
1928
+ runtimeType,
1929
+ attempt,
1930
+ forceRefresh,
1931
+ });
1932
+ // Clear detection cache before continuing to retry
1933
+ const runtimeService6 = this.createRuntimeService(runtimeType);
1934
+ runtimeService6.clearDetectionCache(sessionName);
1935
+ // Add longer delay between failed detection attempts
1936
+ if (attempt < maxRetries) {
1937
+ await delay(2000);
1938
+ }
1939
+ continue; // Try again
1940
+ }
1941
+ }
1942
+ else {
1943
+ this.logger.debug('Skipping runtime detection (runtime was just initialized and verified)', {
1944
+ sessionName,
1945
+ runtimeType,
1946
+ attempt,
1947
+ });
1948
+ }
1949
+ // Step 3: Send system prompt with robust delivery mechanism
1950
+ const prompt = await this.loadRegistrationPrompt(role, sessionName, memberId, runtimeType);
1951
+ const promptDelivered = await this.sendPromptRobustly(sessionName, prompt, runtimeType);
1952
+ if (!promptDelivered) {
1953
+ this.logger.warn('Failed to deliver system prompt reliably', {
1954
+ sessionName,
1955
+ attempt,
1956
+ promptLength: prompt.length,
1957
+ });
1958
+ continue; // Try again
1959
+ }
1960
+ this.logger.debug('System prompt delivered successfully', {
1961
+ sessionName,
1962
+ promptLength: prompt.length,
1963
+ });
1964
+ this.logger.debug('Terminal activity detected, waiting for MCP registration', {
1965
+ sessionName,
1966
+ });
1967
+ // Step 5: Wait for registration API call to update teams.json (agentStatus: activating -> active)
1968
+ const registrationTimeout = Math.min(timeout, 25000); // Max 25s per attempt
1969
+ const registered = await this.waitForRegistrationFast(sessionName, role, registrationTimeout);
1970
+ if (registered) {
1971
+ this.logger.info('MCP registration successful (teams.json updated)', {
1972
+ sessionName,
1973
+ role,
1974
+ attempt,
1975
+ });
1976
+ return true;
1977
+ }
1978
+ this.logger.warn('MCP registration timeout, retrying', {
1979
+ sessionName,
1980
+ role,
1981
+ attempt,
1982
+ nextAttempt: attempt < maxRetries,
1983
+ });
1984
+ }
1985
+ catch (error) {
1986
+ this.logger.error('System prompt registration attempt failed', {
1987
+ sessionName,
1988
+ role,
1989
+ attempt,
1990
+ error: error instanceof Error ? error.message : String(error),
1991
+ });
1992
+ }
1993
+ // Short delay before retry
1994
+ if (attempt < maxRetries) {
1995
+ await delay(1000);
1996
+ }
1997
+ }
1998
+ this.logger.warn('Background MCP registration did not complete within timeout (agent still running)', {
1999
+ sessionName,
2000
+ role,
2001
+ runtimeType,
2002
+ maxRetries,
2003
+ });
2004
+ return false;
2005
+ }
2006
+ /**
2007
+ * Fast registration polling with shorter intervals
2008
+ */
2009
+ async waitForRegistrationFast(sessionName, role, timeout) {
2010
+ const startTime = Date.now();
2011
+ const fastCheckInterval = 2000; // Check every 2 seconds (faster than original 5s)
2012
+ while (Date.now() - startTime < timeout) {
2013
+ try {
2014
+ if (await this.checkAgentRegistration(sessionName, role)) {
2015
+ this.logger.info('Fast registration confirmation', { sessionName, role });
2016
+ return true;
2017
+ }
2018
+ await delay(fastCheckInterval);
2019
+ }
2020
+ catch (error) {
2021
+ this.logger.debug('Error in fast registration check', {
2022
+ sessionName,
2023
+ role,
2024
+ error: error instanceof Error ? error.message : String(error),
2025
+ });
2026
+ await delay(1000); // Shorter error delay
2027
+ }
2028
+ }
2029
+ this.logger.warn('Fast registration timeout', { sessionName, role, timeout });
2030
+ return false;
2031
+ }
2032
+ /**
2033
+ * Create orchestrator session - extracted from the original tmux service
2034
+ */
2035
+ async createOrchestratorSession(config) {
2036
+ this.logger.info('Creating orchestrator session', {
2037
+ sessionName: config.sessionName,
2038
+ projectPath: config.projectPath,
2039
+ });
2040
+ // Check if session already exists
2041
+ if (await (await this.getSessionHelper()).sessionExists(config.sessionName)) {
2042
+ this.logger.info('Orchestrator session already exists', {
2043
+ sessionName: config.sessionName,
2044
+ });
2045
+ return;
2046
+ }
2047
+ // Create new session for orchestrator
2048
+ await (await this.getSessionHelper()).createSession(config.sessionName, config.projectPath
2049
+ // windowName not used in PTY backend
2050
+ );
2051
+ this.logger.info('Orchestrator session created successfully', {
2052
+ sessionName: config.sessionName,
2053
+ });
2054
+ }
2055
+ /**
2056
+ * Unified session creation that handles both orchestrator and team members
2057
+ * @param config Session configuration
2058
+ * @returns Promise with success/error information
2059
+ */
2060
+ async createAgentSession(config) {
2061
+ // Fail fast if role is unusable. Without this guard, an undefined role
2062
+ // flows through the entire pipeline, gets stringified to "undefined" in
2063
+ // the CREWLY_ROLE env var, and silently breaks prompt resolution. We
2064
+ // surface the misconfiguration here instead of spawning a dead session.
2065
+ try {
2066
+ validateAgentRole(config.role, 'createAgentSession');
2067
+ }
2068
+ catch (err) {
2069
+ const error = err instanceof Error ? err.message : String(err);
2070
+ this.logger.error('Refusing to create agent session with invalid role', {
2071
+ sessionName: config.sessionName,
2072
+ memberId: config.memberId,
2073
+ teamId: config.teamId,
2074
+ receivedRole: config.role,
2075
+ error,
2076
+ });
2077
+ return {
2078
+ success: false,
2079
+ sessionName: config.sessionName,
2080
+ error,
2081
+ };
2082
+ }
2083
+ // If another creation is in progress for this session, wait for it
2084
+ const existingLock = this.sessionCreationLocks.get(config.sessionName);
2085
+ if (existingLock) {
2086
+ this.logger.info('Waiting for existing session creation to complete', { sessionName: config.sessionName });
2087
+ try {
2088
+ return await existingLock;
2089
+ }
2090
+ catch {
2091
+ // Previous creation failed, proceed with our own attempt
2092
+ this.logger.info('Previous session creation failed, proceeding with new attempt', { sessionName: config.sessionName });
2093
+ }
2094
+ }
2095
+ // Create and store the lock promise
2096
+ const creationPromise = this._createAgentSessionImpl(config);
2097
+ this.sessionCreationLocks.set(config.sessionName, creationPromise);
2098
+ try {
2099
+ const result = await creationPromise;
2100
+ return result;
2101
+ }
2102
+ finally {
2103
+ // Only delete lock if it's still ours — a concurrent caller may have
2104
+ // replaced it with a new promise after our catch path above.
2105
+ if (this.sessionCreationLocks.get(config.sessionName) === creationPromise) {
2106
+ this.sessionCreationLocks.delete(config.sessionName);
2107
+ }
2108
+ }
2109
+ }
2110
+ /**
2111
+ * Internal implementation of createAgentSession, guarded by sessionCreationLocks.
2112
+ * @param config Session configuration
2113
+ * @returns Promise with success/error information
2114
+ */
2115
+ async _createAgentSessionImpl(config) {
2116
+ const { sessionName, role, projectPath = process.cwd(), windowName, memberId } = config;
2117
+ const forceRecreate = config.forceRecreate ?? false;
2118
+ // Get runtime type from config or default to claude-code
2119
+ let runtimeType = config.runtimeType || RUNTIME_TYPES.CLAUDE_CODE;
2120
+ // Resolve runtime flags from the agent's effective skills
2121
+ let runtimeFlags = [];
2122
+ // For team members, try to get runtime type from storage and resolve skill flags
2123
+ if (!config.runtimeType && role !== ORCHESTRATOR_ROLE) {
2124
+ try {
2125
+ const teams = await this.storageService.getTeams();
2126
+ for (const team of teams) {
2127
+ const member = team.members?.find((m) => m.sessionName === sessionName);
2128
+ if (member) {
2129
+ if (member.runtimeType) {
2130
+ runtimeType = member.runtimeType;
2131
+ }
2132
+ // Resolve runtime flags from role skills + member overrides
2133
+ runtimeFlags = await this.resolveRuntimeFlags(role, runtimeType, member.skillOverrides, member.excludedRoleSkills);
2134
+ break;
2135
+ }
2136
+ }
2137
+ }
2138
+ catch (error) {
2139
+ this.logger.warn('Failed to get runtime type or flags from storage, using defaults', {
2140
+ sessionName,
2141
+ role,
2142
+ error: error instanceof Error ? error.message : String(error),
2143
+ });
2144
+ }
2145
+ }
2146
+ else if (role !== ORCHESTRATOR_ROLE) {
2147
+ // Config-provided runtimeType, still resolve flags
2148
+ try {
2149
+ const teams = await this.storageService.getTeams();
2150
+ for (const team of teams) {
2151
+ const member = team.members?.find((m) => m.sessionName === sessionName);
2152
+ if (member) {
2153
+ runtimeFlags = await this.resolveRuntimeFlags(role, runtimeType, member.skillOverrides, member.excludedRoleSkills);
2154
+ break;
2155
+ }
2156
+ }
2157
+ }
2158
+ catch (error) {
2159
+ this.logger.warn('Failed to resolve runtime flags', {
2160
+ sessionName, role,
2161
+ error: error instanceof Error ? error.message : String(error),
2162
+ });
2163
+ }
2164
+ }
2165
+ // For orchestrator, try to get runtime type from orchestrator status
2166
+ if (role === ORCHESTRATOR_ROLE && !config.runtimeType) {
2167
+ try {
2168
+ const orchestratorStatus = await this.storageService.getOrchestratorStatus();
2169
+ if (orchestratorStatus?.runtimeType) {
2170
+ runtimeType = orchestratorStatus.runtimeType;
2171
+ }
2172
+ }
2173
+ catch (error) {
2174
+ this.logger.warn('Failed to get orchestrator runtime type from storage, using default', {
2175
+ sessionName,
2176
+ role,
2177
+ error: error instanceof Error ? error.message : String(error),
2178
+ });
2179
+ }
2180
+ }
2181
+ try {
2182
+ this.logger.info('Creating agent session (unified approach)', {
2183
+ sessionName,
2184
+ role,
2185
+ runtimeType,
2186
+ projectPath,
2187
+ windowName,
2188
+ memberId,
2189
+ });
2190
+ // Get session helper first
2191
+ this.logger.debug('Getting session helper for session creation');
2192
+ const sessionHelper = await this.getSessionHelper();
2193
+ this.logger.debug('Session helper obtained', {
2194
+ hasHelper: !!sessionHelper,
2195
+ });
2196
+ // Check if session already exists
2197
+ const sessionExists = sessionHelper.sessionExists(sessionName);
2198
+ this.logger.debug('Checked if session exists', {
2199
+ sessionName,
2200
+ sessionExists,
2201
+ });
2202
+ if (!sessionExists) {
2203
+ // Session doesn't exist, go directly to creating a new session
2204
+ this.logger.info('Session does not exist, will create new session', { sessionName });
2205
+ }
2206
+ else if (forceRecreate) {
2207
+ // Skip recovery, kill immediately (used during server startup for stale sessions)
2208
+ this.logger.info('Session exists but forceRecreate is set, killing for clean restart', { sessionName });
2209
+ try {
2210
+ const runtimeService = this.createRuntimeService(runtimeType);
2211
+ runtimeService.clearDetectionCache(sessionName);
2212
+ await (await this.getSessionHelper()).killSession(sessionName);
2213
+ await delay(1000);
2214
+ }
2215
+ catch (killError) {
2216
+ this.logger.warn('Failed to kill session during forceRecreate, continuing with recreation', {
2217
+ sessionName,
2218
+ error: killError instanceof Error ? killError.message : String(killError),
2219
+ });
2220
+ }
2221
+ }
2222
+ else {
2223
+ this.logger.info('Session already exists, attempting intelligent recovery instead of killing', {
2224
+ sessionName,
2225
+ });
2226
+ // Step 1: Try to detect if runtime is already running using slash command
2227
+ const runtimeService = this.createRuntimeService(runtimeType);
2228
+ let recoverySuccess = false;
2229
+ try {
2230
+ const runtimeRunning = await runtimeService.detectRuntimeWithCommand(sessionName);
2231
+ if (runtimeRunning) {
2232
+ this.logger.info('Runtime detected in existing session, attempting direct registration', {
2233
+ sessionName,
2234
+ });
2235
+ // Try to send registration prompt directly
2236
+ const registrationResult = await this.attemptRegistrationWithVerification(sessionName, role, 25000, // 25 second timeout
2237
+ memberId, 1, // single attempt for now
2238
+ true, // skip initial cleanup since runtime is confirmed running
2239
+ runtimeType);
2240
+ if (registrationResult) {
2241
+ recoverySuccess = true;
2242
+ this.logger.info('Successfully registered existing session with runtime', { sessionName });
2243
+ }
2244
+ }
2245
+ // Step 2: If runtime not detected or registration failed, try Ctrl+C restart
2246
+ // only for Claude. Gemini/Codex treat Ctrl+C as destructive at prompt
2247
+ // and can exit the runtime instead of recovering.
2248
+ if (!recoverySuccess) {
2249
+ if (runtimeType !== RUNTIME_TYPES.CLAUDE_CODE) {
2250
+ this.logger.info('Skipping Ctrl+C recovery for non-Claude runtime; falling back to recreation', { sessionName, runtimeType });
2251
+ throw new Error(`${runtimeType} recovery requires full recreation`);
2252
+ }
2253
+ this.logger.info('Runtime not detected or registration failed, attempting Ctrl+C restart', {
2254
+ sessionName,
2255
+ });
2256
+ // Send Ctrl+C twice to try to restart Claude
2257
+ await (await this.getSessionHelper()).sendCtrlC(sessionName);
2258
+ await delay(1000);
2259
+ await (await this.getSessionHelper()).sendCtrlC(sessionName);
2260
+ await delay(2000);
2261
+ // Clear runtime detection cache after Ctrl+C restart
2262
+ runtimeService.clearDetectionCache(sessionName);
2263
+ // Try registration after Ctrl+C restart
2264
+ const postCtrlCResult = await this.attemptRegistrationWithVerification(sessionName, role, 25000, memberId, 1, // single attempt
2265
+ false, // don't skip cleanup after Ctrl+C
2266
+ runtimeType);
2267
+ if (postCtrlCResult) {
2268
+ recoverySuccess = true;
2269
+ this.logger.info('Successfully recovered session after Ctrl+C restart', { sessionName });
2270
+ }
2271
+ }
2272
+ }
2273
+ catch (error) {
2274
+ this.logger.warn('Error during intelligent session recovery', {
2275
+ sessionName,
2276
+ error: error instanceof Error ? error.message : String(error),
2277
+ });
2278
+ }
2279
+ // If recovery succeeded, register for persistence, start monitoring, and return early
2280
+ if (recoverySuccess) {
2281
+ // Register session for state persistence so it survives restarts
2282
+ try {
2283
+ const persistence = getSessionStatePersistence();
2284
+ persistence.registerSession(sessionName, {
2285
+ cwd: projectPath || process.cwd(),
2286
+ command: process.env.SHELL || '/bin/bash',
2287
+ args: [],
2288
+ }, runtimeType, role, config.teamId, config.memberId);
2289
+ }
2290
+ catch (persistError) {
2291
+ this.logger.warn('Failed to register recovered session for persistence (non-critical)', {
2292
+ sessionName,
2293
+ error: persistError instanceof Error ? persistError.message : String(persistError),
2294
+ });
2295
+ }
2296
+ // Start context window monitoring for recovered non-orchestrator session
2297
+ if (role !== ORCHESTRATOR_ROLE && config.teamId && config.memberId) {
2298
+ ContextWindowMonitorService.getInstance().startSessionMonitoring(sessionName, config.memberId, config.teamId, role, runtimeType);
2299
+ }
2300
+ // Start OAuth relogin monitoring for recovered session
2301
+ OAuthReloginMonitorService.getInstance().startMonitoring(sessionName, runtimeType);
2302
+ return {
2303
+ success: true,
2304
+ sessionName,
2305
+ message: 'Agent session recovered and registered successfully',
2306
+ };
2307
+ }
2308
+ // Step 3: Last resort - fall back to killing session for clean restart
2309
+ this.logger.warn('All recovery attempts failed, falling back to session recreation', {
2310
+ sessionName,
2311
+ });
2312
+ await (await this.getSessionHelper()).killSession(sessionName);
2313
+ await delay(1000); // Wait for cleanup
2314
+ }
2315
+ // ===== In-process Crewly Agent branch =====
2316
+ // Crewly Agent runs inside the Node.js process — no PTY session needed.
2317
+ if (runtimeType === RUNTIME_TYPES.CREWLY_AGENT) {
2318
+ this.logger.info('Starting in-process Crewly Agent runtime', { sessionName, role });
2319
+ const crewlyRuntime = this.createRuntimeService(runtimeType);
2320
+ // Look up modelId for this session.
2321
+ //
2322
+ // Resolution order:
2323
+ // 1. Team-member lookup (storage.service.getTeams iterates persisted
2324
+ // team configs, matched by config.memberId).
2325
+ // 2. Orchestrator fallback — storage.service.getTeams() explicitly
2326
+ // EXCLUDES the orchestrator directory (storage.service.ts:413),
2327
+ // so the orchestrator's modelId lives in teams/orchestrator/config.json
2328
+ // and must be read separately via getOrchestratorStatus().
2329
+ //
2330
+ // Without this orchestrator branch, the orchestrator's modelId would
2331
+ // silently default to DEFAULT_MODEL even when configured.
2332
+ let memberModelId;
2333
+ if (config.memberId) {
2334
+ try {
2335
+ const teams = await this.storageService.getTeams();
2336
+ for (const team of teams) {
2337
+ const member = team.members?.find(m => m.id === config.memberId);
2338
+ if (member?.modelId) {
2339
+ memberModelId = member.modelId;
2340
+ break;
2341
+ }
2342
+ }
2343
+ }
2344
+ catch {
2345
+ // Non-critical — fall back to orchestrator/default model
2346
+ }
2347
+ }
2348
+ // Orchestrator fallback: getTeams() doesn't surface the orchestrator,
2349
+ // so look it up via getOrchestratorStatus when this is the orchestrator
2350
+ // session (or its virtual member id) and no team-level modelId was found.
2351
+ const isOrchestrator = sessionName === ORCHESTRATOR_SESSION_NAME
2352
+ || config.memberId === 'orchestrator-member';
2353
+ if (!memberModelId && isOrchestrator) {
2354
+ try {
2355
+ const orchestratorStatus = await this.storageService.getOrchestratorStatus();
2356
+ if (orchestratorStatus?.modelId) {
2357
+ memberModelId = orchestratorStatus.modelId;
2358
+ }
2359
+ }
2360
+ catch {
2361
+ // Non-critical — fall back to default model
2362
+ }
2363
+ }
2364
+ // Parse modelId into model config (falls back to DEFAULT_MODEL)
2365
+ const { parseModelId } = await import('./crewly-agent/types.js');
2366
+ const modelConfig = memberModelId ? { model: parseModelId(memberModelId) } : {};
2367
+ // Determine role name for system prompt lookup
2368
+ const roleName = role === ORCHESTRATOR_ROLE ? 'orchestrator' : role.toLowerCase().replace(/\s+/g, '-');
2369
+ const memberId = config.memberId;
2370
+ await crewlyRuntime.initializeInProcess(sessionName, { projectPath, memberId, ...modelConfig }, roleName);
2371
+ // Track the in-process runtime for message routing.
2372
+ // Mirror to the module-level registry so callers without an
2373
+ // AgentRegistrationService reference (e.g. orchestrator-status
2374
+ // service) can probe runtime liveness — fixes B0 isActive bug.
2375
+ this.inProcessRuntimes.set(sessionName, crewlyRuntime);
2376
+ registerInProcessRuntime(sessionName, crewlyRuntime);
2377
+ // Auto-register agent as active — crewly-agent cannot run bash scripts,
2378
+ // so we register on its behalf instead of relying on the register_self tool.
2379
+ try {
2380
+ const registerResult = await this.registerMemberActive(sessionName, role, memberId);
2381
+ this.logger.info('Auto-registered crewly-agent as active', {
2382
+ sessionName, role, success: registerResult,
2383
+ });
2384
+ }
2385
+ catch (registerErr) {
2386
+ this.logger.warn('Auto-registration failed (non-fatal)', {
2387
+ sessionName,
2388
+ error: registerErr instanceof Error ? registerErr.message : String(registerErr),
2389
+ });
2390
+ }
2391
+ // For crewly-agent, the system prompt is already loaded during initializeInProcess().
2392
+ // Do NOT deliver the full registration prompt as a message — that would trigger
2393
+ // a redundant generateText call (60K chars → rate limits). Instead, send a
2394
+ // lightweight activation message that tells the agent to start working.
2395
+ //
2396
+ // B0/B1 cold-start invariant: the orchestrator's `register_self` is performed
2397
+ // by registerMemberActive() above (which writes directly to storage), so the
2398
+ // orchestrator does NOT need an activation message. Skipping it here keeps
2399
+ // `conversationHistory.messageCount === 0` after a fresh setup, which B1's
2400
+ // fresh-install detector relies on. Subordinate agents still receive the
2401
+ // activation kickoff because they are not auto-registered via storage.
2402
+ if (sessionName !== ORCHESTRATOR_SESSION_NAME) {
2403
+ crewlyRuntime.handleMessage(`You are now active as "${role}" (session: ${sessionName}). ` +
2404
+ 'Your system prompt is already loaded. Begin by calling register_self, ' +
2405
+ 'then wait for tasks.').catch(promptError => {
2406
+ this.logger.warn('Initial activation message failed (non-fatal for crewly-agent)', {
2407
+ sessionName,
2408
+ error: promptError instanceof Error ? promptError.message : String(promptError),
2409
+ });
2410
+ });
2411
+ }
2412
+ else {
2413
+ this.logger.debug('Skipping initial activation message for orchestrator (cold-start invariant)', {
2414
+ sessionName,
2415
+ });
2416
+ }
2417
+ // Start context window monitoring if applicable
2418
+ if (role !== ORCHESTRATOR_ROLE && config.teamId && config.memberId) {
2419
+ ContextWindowMonitorService.getInstance().startSessionMonitoring(sessionName, config.memberId, config.teamId, role, runtimeType);
2420
+ }
2421
+ return {
2422
+ success: true,
2423
+ sessionName,
2424
+ message: 'Crewly Agent in-process runtime initialized and registered',
2425
+ };
2426
+ }
2427
+ // Create new session (same approach for both orchestrator and team members)
2428
+ // #222: Use projectRoot (crewly install dir) as fallback instead of process.cwd()
2429
+ const cwdToUse = projectPath || this.projectRoot || process.cwd();
2430
+ this.logger.info('Creating PTY session', {
2431
+ sessionName,
2432
+ cwd: cwdToUse,
2433
+ });
2434
+ try {
2435
+ const createdSession = await sessionHelper.createSession(sessionName, cwdToUse);
2436
+ this.logger.info('PTY session created successfully', {
2437
+ sessionName,
2438
+ pid: createdSession.pid,
2439
+ cwd: createdSession.cwd,
2440
+ });
2441
+ }
2442
+ catch (createError) {
2443
+ this.logger.error('Failed to create PTY session', {
2444
+ sessionName,
2445
+ cwd: cwdToUse,
2446
+ error: createError instanceof Error ? createError.message : String(createError),
2447
+ stack: createError instanceof Error ? createError.stack : undefined,
2448
+ });
2449
+ throw createError;
2450
+ }
2451
+ // Verify session was created
2452
+ const sessionCreatedCheck = sessionHelper.sessionExists(sessionName);
2453
+ this.logger.info('Session creation verification', {
2454
+ sessionName,
2455
+ exists: sessionCreatedCheck,
2456
+ });
2457
+ if (!sessionCreatedCheck) {
2458
+ throw new Error(`Session '${sessionName}' was not created successfully`);
2459
+ }
2460
+ // Register session for state persistence so it survives restarts
2461
+ try {
2462
+ const persistence = getSessionStatePersistence();
2463
+ persistence.registerSession(sessionName, {
2464
+ cwd: cwdToUse,
2465
+ command: process.env.SHELL || '/bin/bash',
2466
+ args: [],
2467
+ }, runtimeType, role, config.teamId, config.memberId);
2468
+ }
2469
+ catch (persistError) {
2470
+ this.logger.warn('Failed to register session for persistence (non-critical)', {
2471
+ sessionName,
2472
+ error: persistError instanceof Error ? persistError.message : String(persistError),
2473
+ });
2474
+ }
2475
+ // Set environment variables for MCP connection
2476
+ await sessionHelper.setEnvironmentVariable(sessionName, ENV_CONSTANTS.CREWLY_SESSION_NAME, sessionName);
2477
+ await sessionHelper.setEnvironmentVariable(sessionName, ENV_CONSTANTS.CREWLY_ROLE, role);
2478
+ await sessionHelper.setEnvironmentVariable(sessionName, ENV_CONSTANTS.CREWLY_API_URL, `http://localhost:${WEB_CONSTANTS.PORTS.BACKEND}`);
2479
+ // #187: Set project path so memory skills can auto-inject it
2480
+ await sessionHelper.setEnvironmentVariable(sessionName, ENV_CONSTANTS.CREWLY_PROJECT_PATH, cwdToUse);
2481
+ // #222: Set install dir so agents can resolve skill paths regardless of CWD
2482
+ await sessionHelper.setEnvironmentVariable(sessionName, ENV_CONSTANTS.CREWLY_INSTALL_DIR, this.projectRoot);
2483
+ // Inject API keys from settings (with override chain) for all runtimes
2484
+ const settingsService = getSettingsService();
2485
+ const runtimeContext = { runtime: runtimeType };
2486
+ // Gemini key — needed by gemini-cli and crewly-agent
2487
+ const geminiKey = await settingsService.getApiKey('gemini', runtimeContext);
2488
+ if (geminiKey) {
2489
+ await sessionHelper.setEnvironmentVariable(sessionName, 'GOOGLE_GENERATIVE_AI_API_KEY', geminiKey);
2490
+ await sessionHelper.setEnvironmentVariable(sessionName, ENV_CONSTANTS.GEMINI_API_KEY, geminiKey);
2491
+ }
2492
+ // Anthropic key — needed by claude-code and crewly-agent
2493
+ const anthropicKey = await settingsService.getApiKey('anthropic', runtimeContext);
2494
+ if (anthropicKey) {
2495
+ await sessionHelper.setEnvironmentVariable(sessionName, 'ANTHROPIC_API_KEY', anthropicKey);
2496
+ }
2497
+ // OpenAI key — needed by codex-cli and crewly-agent
2498
+ const openaiKey = await settingsService.getApiKey('openai', runtimeContext);
2499
+ if (openaiKey) {
2500
+ await sessionHelper.setEnvironmentVariable(sessionName, 'OPENAI_API_KEY', openaiKey);
2501
+ }
2502
+ // Token tracking telemetry — inject env vars for runtimes that need them
2503
+ const settings = await settingsService.getSettings();
2504
+ if (settings.general?.tokenTracking && runtimeType === RUNTIME_TYPES.CLAUDE_CODE) {
2505
+ await sessionHelper.setEnvironmentVariable(sessionName, ENV_CONSTANTS.CLAUDE_CODE_ENABLE_TELEMETRY, '1');
2506
+ this.logger.debug('Token tracking enabled: set CLAUDE_CODE_ENABLE_TELEMETRY=1', { sessionName });
2507
+ }
2508
+ this.logger.info('Agent session created and environment variables set, initializing with registration', {
2509
+ sessionName,
2510
+ role,
2511
+ runtimeType,
2512
+ });
2513
+ // Use the existing unified registration system
2514
+ // #227: Claude Code gets extended timeout for PI protection evaluation
2515
+ const timeout = role === ORCHESTRATOR_ROLE
2516
+ ? AGENT_TIMEOUTS.ORCHESTRATOR_INITIALIZATION
2517
+ : runtimeType === RUNTIME_TYPES.CLAUDE_CODE
2518
+ ? AGENT_TIMEOUTS.CLAUDE_CODE_INITIALIZATION
2519
+ : AGENT_TIMEOUTS.REGULAR_AGENT_INITIALIZATION;
2520
+ const initResult = await this.initializeAgentWithRegistration(sessionName, role, projectPath, timeout, memberId, runtimeType, runtimeFlags, config.additionalAllowlistPaths);
2521
+ if (!initResult.success) {
2522
+ return {
2523
+ success: false,
2524
+ sessionName,
2525
+ error: initResult.error || 'Failed to initialize and register agent',
2526
+ };
2527
+ }
2528
+ // Start context window monitoring for newly created non-orchestrator session
2529
+ if (role !== ORCHESTRATOR_ROLE && config.teamId && config.memberId) {
2530
+ ContextWindowMonitorService.getInstance().startSessionMonitoring(sessionName, config.memberId, config.teamId, role, runtimeType);
2531
+ }
2532
+ // Start OAuth relogin monitoring for newly created session
2533
+ OAuthReloginMonitorService.getInstance().startMonitoring(sessionName, runtimeType);
2534
+ return {
2535
+ success: true,
2536
+ sessionName,
2537
+ message: initResult.message || 'Agent session created and registered successfully',
2538
+ };
2539
+ }
2540
+ catch (error) {
2541
+ const errorMessage = error instanceof Error ? error.message : String(error);
2542
+ this.logger.error('Failed to create agent session', {
2543
+ sessionName,
2544
+ role,
2545
+ error: errorMessage,
2546
+ });
2547
+ return {
2548
+ success: false,
2549
+ sessionName,
2550
+ error: errorMessage,
2551
+ };
2552
+ }
2553
+ }
2554
+ /**
2555
+ * Unified session termination that handles both orchestrator and team members
2556
+ * @param sessionName The session to terminate
2557
+ * @param role The role for proper status updates
2558
+ * @returns Promise with success/error information
2559
+ */
2560
+ async terminateAgentSession(sessionName, role = 'unknown') {
2561
+ try {
2562
+ this.logger.info('Terminating agent session (unified approach)', { sessionName, role });
2563
+ // Stop runtime exit monitoring before killing the session
2564
+ RuntimeExitMonitorService.getInstance().stopMonitoring(sessionName);
2565
+ // Stop OAuth relogin monitoring before killing the session
2566
+ OAuthReloginMonitorService.getInstance().stopMonitoring(sessionName);
2567
+ // Stop context window monitoring before killing the session
2568
+ ContextWindowMonitorService.getInstance().stopSessionMonitoring(sessionName);
2569
+ // Shut down in-process Crewly Agent runtime if present.
2570
+ // Both maps must stay in sync: the local instance map and the
2571
+ // module-level registry consumed by orchestrator-status service.
2572
+ const inProcessRuntime = this.inProcessRuntimes.get(sessionName);
2573
+ if (inProcessRuntime) {
2574
+ inProcessRuntime.shutdown();
2575
+ this.inProcessRuntimes.delete(sessionName);
2576
+ unregisterInProcessRuntime(sessionName);
2577
+ this.logger.info('In-process Crewly Agent runtime shut down', { sessionName });
2578
+ }
2579
+ // Get session helper once to avoid repeated async calls
2580
+ const sessionHelper = await this.getSessionHelper();
2581
+ const sessionExists = sessionHelper.sessionExists(sessionName);
2582
+ if (sessionExists) {
2583
+ // Kill the tmux session
2584
+ await sessionHelper.killSession(sessionName);
2585
+ this.logger.info('Session terminated successfully', { sessionName });
2586
+ }
2587
+ else if (!inProcessRuntime) {
2588
+ this.logger.info('Session already terminated or does not exist', { sessionName });
2589
+ }
2590
+ // Capture session end for memory persistence
2591
+ try {
2592
+ const sessionMemoryService = SessionMemoryService.getInstance();
2593
+ await sessionMemoryService.onSessionEnd(sessionName, role, process.cwd());
2594
+ this.logger.info('Session memory captured on termination', { sessionName, role });
2595
+ }
2596
+ catch (memoryError) {
2597
+ this.logger.warn('Failed to capture session memory on termination (non-critical)', {
2598
+ sessionName,
2599
+ error: memoryError instanceof Error ? memoryError.message : String(memoryError),
2600
+ });
2601
+ }
2602
+ // Update agent status to inactive (works for both orchestrator and team members)
2603
+ await this.storageService.updateAgentStatus(sessionName, CREWLY_CONSTANTS.AGENT_STATUSES.INACTIVE);
2604
+ this.logger.info('Agent status updated to inactive', { sessionName, role });
2605
+ // Unregister from persistence so explicitly stopped agents don't reappear in resume popup
2606
+ try {
2607
+ const persistence = getSessionStatePersistence();
2608
+ persistence.unregisterSession(sessionName);
2609
+ }
2610
+ catch (persistError) {
2611
+ this.logger.warn('Failed to unregister session from persistence (non-critical)', {
2612
+ sessionName,
2613
+ error: persistError instanceof Error ? persistError.message : String(persistError),
2614
+ });
2615
+ }
2616
+ return {
2617
+ success: true,
2618
+ message: sessionExists
2619
+ ? 'Agent session terminated successfully'
2620
+ : 'Agent session was already terminated',
2621
+ };
2622
+ }
2623
+ catch (error) {
2624
+ const errorMessage = error instanceof Error ? error.message : String(error);
2625
+ this.logger.error('Failed to terminate agent session', {
2626
+ sessionName,
2627
+ role,
2628
+ error: errorMessage,
2629
+ });
2630
+ return {
2631
+ success: false,
2632
+ error: errorMessage,
2633
+ };
2634
+ }
2635
+ }
2636
+ /**
2637
+ * Send a message to any agent session with reliable delivery.
2638
+ * Uses robust delivery mechanism with retry logic to ensure messages
2639
+ * are properly delivered to Claude Code's input.
2640
+ *
2641
+ * @param sessionName - The agent session name
2642
+ * @param message - The message to send
2643
+ * @returns Promise with success status and optional error message
2644
+ *
2645
+ * @example
2646
+ * ```typescript
2647
+ * const result = await agentRegistrationService.sendMessageToAgent(
2648
+ * 'crewly-orc',
2649
+ * '[CHAT:123] Hello, orchestrator!'
2650
+ * );
2651
+ * if (result.success) {
2652
+ * console.log('Message delivered');
2653
+ * } else {
2654
+ * console.error('Delivery failed:', result.error);
2655
+ * }
2656
+ * ```
2657
+ */
2658
+ async sendMessageToAgent(sessionName, message, runtimeType) {
2659
+ // Per-session delivery serialization: chain this delivery after any
2660
+ // in-flight delivery to the same session. This prevents concurrent
2661
+ // sendMessageWithRetry calls that each send Ctrl+C on retry attempts,
2662
+ // which can kill the runtime when 25+ scheduled checks fire at once.
2663
+ const previousDelivery = this.sessionDeliveryMutex.get(sessionName);
2664
+ let releaseMutex;
2665
+ const currentDelivery = new Promise((r) => { releaseMutex = r; });
2666
+ this.sessionDeliveryMutex.set(sessionName, currentDelivery);
2667
+ if (previousDelivery) {
2668
+ this.logger.info('Waiting for in-flight delivery to complete before sending', {
2669
+ sessionName,
2670
+ messageLength: message.length,
2671
+ });
2672
+ await previousDelivery.catch(() => { });
2673
+ }
2674
+ try {
2675
+ if (!message || typeof message !== 'string') {
2676
+ return {
2677
+ success: false,
2678
+ error: 'Message is required and must be a string',
2679
+ };
2680
+ }
2681
+ // Auto-resolve runtime type if not provided.
2682
+ // CRITICAL: defaulting to CLAUDE_CODE is dangerous because
2683
+ // Ctrl+C cleanup (Claude Code behavior) triggers /quit on Gemini CLI.
2684
+ if (!runtimeType) {
2685
+ runtimeType = await this.resolveSessionRuntimeType(sessionName);
2686
+ }
2687
+ // ===== In-process Crewly Agent delivery =====
2688
+ // Route directly to the in-process runtime, bypassing PTY entirely.
2689
+ // Fire-and-forget: mirrors PTY runtimes where the write returns immediately.
2690
+ // The LLM processes the message asynchronously in the background.
2691
+ //
2692
+ // IMPORTANT: Always check inProcessRuntimes map first, regardless of
2693
+ // the runtimeType parameter. This makes delivery robust against runtime
2694
+ // type misresolution — e.g., when storage lookup fails and defaults to
2695
+ // claude-code for what is actually a crewly-agent session. Without this
2696
+ // fallback, messages to crewly-agent sessions silently fail when routed
2697
+ // through the PTY path (no PTY session exists).
2698
+ const crewlyRuntime = this.inProcessRuntimes.get(sessionName);
2699
+ if (crewlyRuntime || runtimeType === RUNTIME_TYPES.CREWLY_AGENT) {
2700
+ if (!crewlyRuntime || !crewlyRuntime.isReady()) {
2701
+ return {
2702
+ success: false,
2703
+ error: `Crewly Agent runtime for '${sessionName}' is not initialized`,
2704
+ };
2705
+ }
2706
+ // Extract conversationId from [CHAT:xxx] or [GCHAT:xxx ...] prefix for response routing
2707
+ const chatPrefixMatch = message.match(/^\[(?:G?CHAT):([^\]\s]+)[^\]]*\]\s*/);
2708
+ const incomingConversationId = chatPrefixMatch?.[1];
2709
+ // Extract Slack context from [SLACK:channelId:threadTs] marker if present (Bug 5).
2710
+ // This allows crewly-agent to auto-fill reply_slack with the correct thread.
2711
+ let slackMetadata;
2712
+ const slackPrefixMatch = message.match(/\[SLACK:([^:\]]+)(?::([^\]]+))?\]/);
2713
+ if (slackPrefixMatch) {
2714
+ slackMetadata = { channelId: slackPrefixMatch[1] };
2715
+ if (slackPrefixMatch[2])
2716
+ slackMetadata.threadTs = slackPrefixMatch[2];
2717
+ }
2718
+ // Process message asynchronously — don't block the caller
2719
+ crewlyRuntime.handleMessage(message, slackMetadata)
2720
+ .then((result) => {
2721
+ this.logger.info('Crewly Agent finished processing message', {
2722
+ sessionName, messageLength: message.length,
2723
+ responseLength: result.text?.length ?? 0,
2724
+ });
2725
+ // Route response text back to the originating chat conversation.
2726
+ // Without this, crewly-agent responses are discarded — the agent
2727
+ // has no terminal output for the NOTIFY pathway used by PTY runtimes.
2728
+ //
2729
+ // For Slack-sourced messages: check if the agent already sent a
2730
+ // reply via reply_slack tool during processing. If so, skip routing
2731
+ // to avoid duplicates. If not, route normally to ensure delivery.
2732
+ const agentAlreadyReplied = result.toolCalls?.some(tc => tc.toolName === 'reply_slack' || tc.toolName === 'reply-slack') ?? false;
2733
+ if (result.text && incomingConversationId && !agentAlreadyReplied) {
2734
+ this.routeInProcessResponseToChat(sessionName, result.text, incomingConversationId);
2735
+ }
2736
+ else if (agentAlreadyReplied) {
2737
+ this.logger.debug('Skipping chat routing — agent already replied via reply_slack', {
2738
+ sessionName, conversationId: incomingConversationId,
2739
+ });
2740
+ }
2741
+ // Auto-route to Slack when the message originated from Slack and the
2742
+ // agent finished with text-only output (toolCalls=0, finishReason=stop)
2743
+ // without explicitly invoking the reply_slack tool. Without this hook,
2744
+ // the in-process AI SDK runtime drops Slack replies silently — unlike
2745
+ // claude-code PTY where TerminalGateway tails stdout into SlackBridge,
2746
+ // the in-process runtime has no stream the bridge can listen to.
2747
+ //
2748
+ // Skipped when the agent already called reply_slack (which posts via
2749
+ // /slack/send) to prevent double-posting on the same thread.
2750
+ if (slackMetadata?.channelId && result.text && !agentAlreadyReplied) {
2751
+ this.routeInProcessResponseToSlack(sessionName, result.text, {
2752
+ channelId: slackMetadata.channelId,
2753
+ threadTs: slackMetadata.threadTs,
2754
+ });
2755
+ }
2756
+ })
2757
+ .catch(async (agentError) => {
2758
+ const errMsg = agentError instanceof Error ? agentError.message : String(agentError);
2759
+ this.logger.error('Crewly Agent message handling failed', {
2760
+ sessionName, error: errMsg,
2761
+ messageLength: message.length,
2762
+ messagePreview: message.substring(0, 200),
2763
+ });
2764
+ // Log to InProcessLogBuffer so orchestrator can see the error via get-agent-logs
2765
+ try {
2766
+ const { InProcessLogBuffer } = await import('./crewly-agent/in-process-log-buffer.js');
2767
+ InProcessLogBuffer.getInstance().append(sessionName, 'error', `Message handling failed: ${errMsg}`);
2768
+ }
2769
+ catch { /* non-critical */ }
2770
+ // Update workingStatus to idle so the agent is not stuck as in_progress
2771
+ try {
2772
+ const memberInfo = await this.storageService.findMemberBySessionName(sessionName);
2773
+ if (memberInfo) {
2774
+ memberInfo.member.workingStatus = 'idle';
2775
+ memberInfo.member.updatedAt = new Date().toISOString();
2776
+ await this.storageService.saveTeam(memberInfo.team);
2777
+ this.logger.info('Reset workingStatus to idle after crewly-agent error', { sessionName });
2778
+ }
2779
+ }
2780
+ catch (statusError) {
2781
+ this.logger.warn('Failed to reset workingStatus after error', {
2782
+ sessionName,
2783
+ error: statusError instanceof Error ? statusError.message : String(statusError),
2784
+ });
2785
+ }
2786
+ });
2787
+ this.logger.info('Message dispatched to in-process Crewly Agent', {
2788
+ sessionName, messageLength: message.length,
2789
+ });
2790
+ return { success: true, message: 'Message delivered to Crewly Agent' };
2791
+ }
2792
+ // Get session helper once for this method
2793
+ const sessionHelper = await this.getSessionHelper();
2794
+ // Check if session exists
2795
+ if (!sessionHelper.sessionExists(sessionName)) {
2796
+ return {
2797
+ success: false,
2798
+ error: `Session '${sessionName}' does not exist`,
2799
+ };
2800
+ }
2801
+ // Guard: refuse to deliver if the runtime process has exited.
2802
+ // When Claude Code exits (e.g. context exhaustion), the PTY shell
2803
+ // stays alive. Writing to it would execute garbage as shell commands.
2804
+ const backend = typeof sessionHelper.getBackend === 'function'
2805
+ ? sessionHelper.getBackend()
2806
+ : getSessionBackendSync();
2807
+ const childAlive = backend?.isChildProcessAlive?.(sessionName);
2808
+ if (childAlive === false) {
2809
+ this.logger.error('Runtime process not alive, refusing to deliver message', {
2810
+ sessionName,
2811
+ messageLength: message.length,
2812
+ });
2813
+ return {
2814
+ success: false,
2815
+ error: 'Runtime has exited — no child process in session',
2816
+ };
2817
+ }
2818
+ // Use robust message delivery with proper waiting mechanism.
2819
+ // Gemini CLI: use 2 attempts — one retry is needed for TUI focus
2820
+ // issues (Tab+Enter recovery). Previously set to 1 which caused
2821
+ // Google Chat messages to fail permanently on first TUI hiccup.
2822
+ const maxDeliveryAttempts = runtimeType === RUNTIME_TYPES.GEMINI_CLI ? 2 : 3;
2823
+ const delivered = await this.sendMessageWithRetry(sessionName, message, maxDeliveryAttempts, runtimeType);
2824
+ if (!delivered) {
2825
+ // Check if the agent is actively processing (busy) — queue for later delivery.
2826
+ // Use PTY idle time instead of regex patterns for robust cross-runtime detection.
2827
+ const idleMs = PtyActivityTrackerService.getInstance().getIdleTimeMs(sessionName);
2828
+ const isBusy = idleMs < SESSION_COMMAND_DELAYS.AGENT_BUSY_IDLE_THRESHOLD_MS;
2829
+ if (isBusy) {
2830
+ // #236: Queue message for delivery when agent becomes idle
2831
+ SubAgentMessageQueue.getInstance().enqueue(sessionName, message);
2832
+ this.logger.info('Agent busy — message queued for later delivery', {
2833
+ sessionName,
2834
+ messageLength: message.length,
2835
+ });
2836
+ return {
2837
+ success: true,
2838
+ queued: true,
2839
+ message: '[AGENT_BUSY] Message queued for delivery when agent becomes idle',
2840
+ };
2841
+ }
2842
+ return {
2843
+ success: false,
2844
+ error: 'Failed to deliver message after multiple attempts',
2845
+ };
2846
+ }
2847
+ this.logger.info('Message sent to agent successfully', {
2848
+ sessionName,
2849
+ messageLength: message.length,
2850
+ });
2851
+ return {
2852
+ success: true,
2853
+ message: 'Message sent to agent successfully',
2854
+ };
2855
+ }
2856
+ catch (error) {
2857
+ const errorMessage = error instanceof Error ? error.message : String(error);
2858
+ this.logger.error('Failed to send message to agent', {
2859
+ sessionName,
2860
+ error: errorMessage,
2861
+ });
2862
+ return {
2863
+ success: false,
2864
+ error: errorMessage,
2865
+ };
2866
+ }
2867
+ finally {
2868
+ releaseMutex();
2869
+ if (this.sessionDeliveryMutex.get(sessionName) === currentDelivery) {
2870
+ this.sessionDeliveryMutex.delete(sessionName);
2871
+ }
2872
+ }
2873
+ }
2874
+ /**
2875
+ * Wait for an agent session to be at a ready prompt before sending messages.
2876
+ *
2877
+ * Subscribes to terminal output and polls capturePane to detect when the
2878
+ * agent returns to an input prompt. This is critical for sequential message
2879
+ * processing: after the orchestrator responds to a message it may continue
2880
+ * working (managing agents, running commands) before returning to prompt.
2881
+ *
2882
+ * @param sessionName - The session name to wait on
2883
+ * @param timeoutMs - Maximum time to wait (default 120s)
2884
+ * @returns true if agent is ready, false if timed out
2885
+ */
2886
+ async waitForAgentReady(sessionName, timeoutMs = EVENT_DELIVERY_CONSTANTS.AGENT_READY_TIMEOUT, runtimeType) {
2887
+ // In-process Crewly Agent is always "ready" (no prompt to wait for)
2888
+ const inProcessRuntime = this.inProcessRuntimes.get(sessionName);
2889
+ if (inProcessRuntime) {
2890
+ return inProcessRuntime.isReady();
2891
+ }
2892
+ const sessionHelper = await this.getSessionHelper();
2893
+ // Check if session exists
2894
+ if (!sessionHelper.sessionExists(sessionName)) {
2895
+ this.logger.warn('Session does not exist for waitForAgentReady', { sessionName });
2896
+ return false;
2897
+ }
2898
+ // Quick check - already at prompt?
2899
+ const currentOutput = sessionHelper.capturePane(sessionName);
2900
+ if (this.isClaudeAtPrompt(currentOutput, runtimeType)) {
2901
+ this.logger.debug('Agent already at prompt', { sessionName });
2902
+ return true;
2903
+ }
2904
+ this.logger.info('Waiting for agent to return to prompt', { sessionName, timeoutMs });
2905
+ const session = sessionHelper.getSession(sessionName);
2906
+ if (!session) {
2907
+ return false;
2908
+ }
2909
+ const waitStartMs = Date.now();
2910
+ return new Promise((resolve) => {
2911
+ let resolved = false;
2912
+ let pollCount = 0;
2913
+ const pollInterval = EVENT_DELIVERY_CONSTANTS.AGENT_READY_POLL_INTERVAL;
2914
+ const cleanup = () => {
2915
+ if (resolved)
2916
+ return;
2917
+ resolved = true;
2918
+ clearTimeout(timeoutId);
2919
+ clearInterval(pollId);
2920
+ clearInterval(deepScanId);
2921
+ unsubscribe();
2922
+ };
2923
+ // Timeout - give up waiting
2924
+ const timeoutId = setTimeout(() => {
2925
+ this.logger.warn('Timed out waiting for agent to be ready', { sessionName, timeoutMs });
2926
+ cleanup();
2927
+ resolve(false);
2928
+ }, timeoutMs);
2929
+ // Poll capturePane periodically as a fallback
2930
+ const pollId = setInterval(() => {
2931
+ if (resolved)
2932
+ return;
2933
+ pollCount++;
2934
+ const output = sessionHelper.capturePane(sessionName);
2935
+ if (this.isClaudeAtPrompt(output, runtimeType)) {
2936
+ const elapsedMs = Date.now() - waitStartMs;
2937
+ this.logger.info('Agent ready after polling', { sessionName, pollCount, elapsedMs });
2938
+ cleanup();
2939
+ resolve(true);
2940
+ }
2941
+ }, pollInterval);
2942
+ // Deep scan with larger buffer every DEEP_SCAN_INTERVAL to catch prompts
2943
+ // that the fast poll misses (e.g. when prompt is beyond the default 200 lines)
2944
+ const deepScanId = setInterval(() => {
2945
+ if (resolved)
2946
+ return;
2947
+ pollCount++;
2948
+ const output = sessionHelper.capturePane(sessionName, EVENT_DELIVERY_CONSTANTS.DEEP_SCAN_LINES);
2949
+ if (this.isClaudeAtPrompt(output, runtimeType)) {
2950
+ const elapsedMs = Date.now() - waitStartMs;
2951
+ this.logger.info('Agent ready after polling', { sessionName, pollCount, elapsedMs });
2952
+ cleanup();
2953
+ resolve(true);
2954
+ }
2955
+ }, EVENT_DELIVERY_CONSTANTS.DEEP_SCAN_INTERVAL);
2956
+ // Also subscribe to terminal data for faster detection
2957
+ const unsubscribe = session.onData((data) => {
2958
+ if (resolved)
2959
+ return;
2960
+ // Strip ANSI escape sequences before testing — raw PTY data contains
2961
+ // cursor positioning, color codes, etc. that break pattern matching (#106)
2962
+ const cleanData = stripAnsiCodes(data);
2963
+ // Check each line for prompt pattern (string-based, no regex)
2964
+ const hasPromptInStream = cleanData.split('\n').some(line => line.trim().length > 0 && isPromptLine(line, runtimeType));
2965
+ if (hasPromptInStream) {
2966
+ // Double-check with capturePane to avoid false positives from partial data
2967
+ const output = sessionHelper.capturePane(sessionName);
2968
+ if (this.isClaudeAtPrompt(output, runtimeType)) {
2969
+ this.logger.debug('Agent at prompt (detected via stream)', { sessionName });
2970
+ cleanup();
2971
+ resolve(true);
2972
+ }
2973
+ }
2974
+ });
2975
+ });
2976
+ }
2977
+ /**
2978
+ * Send message with retry logic for reliable delivery to Claude Code.
2979
+ * Uses SessionCommandHelper.sendMessage() (proven two-step write pattern)
2980
+ * with stuck-message detection and retry on failure.
2981
+ *
2982
+ * @param sessionName - The session name
2983
+ * @param message - The message to send
2984
+ * @param maxAttempts - Maximum number of delivery attempts
2985
+ * @returns true if message was delivered successfully
2986
+ */
2987
+ async sendMessageWithRetry(sessionName, message, maxAttempts = 3, runtimeType = RUNTIME_TYPES.CLAUDE_CODE) {
2988
+ const sessionHelper = await this.getSessionHelper();
2989
+ const isClaudeCode = runtimeType === RUNTIME_TYPES.CLAUDE_CODE;
2990
+ const isCodexCli = runtimeType === RUNTIME_TYPES.CODEX_CLI;
2991
+ const isGeminiCli = runtimeType === RUNTIME_TYPES.GEMINI_CLI;
2992
+ // Register TUI sessions for TUI prompt-line scanning (Part 1 of scanner).
2993
+ // The scanner itself is started lazily by trackSentMessage() which
2994
+ // runs after every message send, covering all runtimes.
2995
+ if (!isClaudeCode) {
2996
+ this.tuiSessionRegistry.set(sessionName, runtimeType);
2997
+ }
2998
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2999
+ try {
3000
+ this.logger.debug('Attempting message delivery', {
3001
+ sessionName,
3002
+ attempt,
3003
+ maxAttempts,
3004
+ messageLength: message.length,
3005
+ runtimeType,
3006
+ });
3007
+ // Verify agent is at prompt before sending
3008
+ const output = sessionHelper.capturePane(sessionName);
3009
+ // Gemini CLI stuck-connectivity guard (#128): If the CLI is in a
3010
+ // "Trying to reach <model>" retry loop, it will never process
3011
+ // new messages. Bail out immediately so the caller gets a clear
3012
+ // failure signal and the runtime-exit-monitor can trigger recovery.
3013
+ if (runtimeType === RUNTIME_TYPES.GEMINI_CLI && GEMINI_STUCK_CONNECTIVITY_PATTERN.test(output)) {
3014
+ this.logger.warn('Gemini CLI stuck in connectivity retry loop, aborting message delivery (#128)', {
3015
+ sessionName,
3016
+ attempt,
3017
+ });
3018
+ return false;
3019
+ }
3020
+ if (!this.isClaudeAtPrompt(output, runtimeType)) {
3021
+ if (attempt === maxAttempts) {
3022
+ // On the final attempt, check if the agent is DEFINITELY busy
3023
+ // before force-delivering. Use PTY idle time for robust
3024
+ // cross-runtime detection instead of fragile regex patterns.
3025
+ const idleMs = PtyActivityTrackerService.getInstance().getIdleTimeMs(sessionName);
3026
+ const isBusy = idleMs < SESSION_COMMAND_DELAYS.AGENT_BUSY_IDLE_THRESHOLD_MS;
3027
+ if (isBusy) {
3028
+ this.logger.warn('Agent is busy (processing indicators detected), skipping force delivery', {
3029
+ sessionName,
3030
+ attempt,
3031
+ runtimeType,
3032
+ });
3033
+ return false;
3034
+ }
3035
+ // Not clearly busy — fall back to direct delivery. The agent
3036
+ // may be at a prompt that doesn't match our detection patterns,
3037
+ // or the terminal buffer may be stale.
3038
+ this.logger.warn('Prompt detection failed on final attempt, delivering message directly', {
3039
+ sessionName,
3040
+ attempt,
3041
+ runtimeType,
3042
+ });
3043
+ // Fall through to message delivery below
3044
+ }
3045
+ else {
3046
+ this.logger.debug('Not at prompt, waiting before retry', { sessionName, attempt });
3047
+ await delay(SESSION_COMMAND_DELAYS.CLAUDE_RECOVERY_DELAY);
3048
+ continue;
3049
+ }
3050
+ }
3051
+ // Gemini CLI shell mode guard: if the prompt shows `!` instead of `>`,
3052
+ // input will be executed as a shell command. Send Escape to exit first.
3053
+ if (runtimeType === RUNTIME_TYPES.GEMINI_CLI && this.isGeminiInShellMode(output)) {
3054
+ const escaped = await this.escapeGeminiShellMode(sessionName, sessionHelper);
3055
+ if (!escaped) {
3056
+ this.logger.warn('Could not exit Gemini shell mode, skipping attempt', {
3057
+ sessionName,
3058
+ attempt,
3059
+ });
3060
+ await delay(SESSION_COMMAND_DELAYS.MESSAGE_RETRY_DELAY);
3061
+ continue;
3062
+ }
3063
+ }
3064
+ // Clear any stale text before sending.
3065
+ // Claude Code: Ctrl+C to cancel any pending input — but ONLY on
3066
+ // retry attempts. On the first attempt, waitForAgentReady already
3067
+ // confirmed the agent is idle at the prompt, so Ctrl+C is
3068
+ // unnecessary and can disrupt Claude Code's input handler if the
3069
+ // 300ms settling delay isn't enough.
3070
+ // Gemini CLI (Ink TUI): escalating recovery to refocus input box.
3071
+ // The TUI input can lose focus after idle periods, auto-update
3072
+ // notifications, or dialog overlays. When defocused, writes are
3073
+ // silently consumed by the Ink framework but NOT routed to the
3074
+ // InputPrompt.
3075
+ // Do NOT send Escape (defocuses permanently), Ctrl+C (triggers
3076
+ // /quit on empty prompt), or Ctrl+U (ignored by TUI).
3077
+ //
3078
+ // Recovery strategies (escalating per attempt):
3079
+ // 1. Tab + Enter: Tab triggers Ink's focusNext() which cycles
3080
+ // focus through focusable components. If InputPrompt is the
3081
+ // only/next focusable element, it gets refocused. Enter then
3082
+ // dismisses any overlay or acts as safe no-op on empty prompt.
3083
+ // 2-3. PTY resize + Tab + Enter: Resize sends SIGWINCH to the
3084
+ // child process, forcing Ink to re-render the entire TUI.
3085
+ // This may restore the internal focus state. Then Tab + Enter
3086
+ // for focus cycling and overlay dismissal.
3087
+ if (isClaudeCode) {
3088
+ if (attempt > 1) {
3089
+ // On retry: Ctrl+C to cancel stale input, then PTY resize to force
3090
+ // TUI re-render (SIGWINCH), then Tab to cycle Ink focus.
3091
+ await sessionHelper.sendCtrlC(sessionName);
3092
+ await delay(300);
3093
+ }
3094
+ // PTY resize on ALL attempts to force SIGWINCH → Ink TUI re-render.
3095
+ // Claude Code's Ink TUI can lose internal input focus after stop hooks
3096
+ // (especially ones that error), state transitions, or idle periods.
3097
+ // When defocused, the `❯` prompt is visible in the terminal buffer
3098
+ // but writes are silently consumed by the framework — NOT routed to
3099
+ // the InputPrompt. Tab alone was insufficient; PTY resize forces a
3100
+ // full TUI re-render that reliably restores focus state.
3101
+ // This matches the manual workaround where pressing a key in the
3102
+ // frontend terminal "wakes up" the input handler.
3103
+ try {
3104
+ const session = sessionHelper.getSession(sessionName);
3105
+ if (session) {
3106
+ session.resize(81, 25);
3107
+ await delay(200);
3108
+ session.resize(80, 24);
3109
+ await delay(300);
3110
+ }
3111
+ }
3112
+ catch { /* non-fatal */ }
3113
+ await sessionHelper.sendKey(sessionName, 'Tab');
3114
+ await delay(300);
3115
+ }
3116
+ else if (isCodexCli) {
3117
+ // #246: Codex CLI TUI recovery — simpler than Gemini.
3118
+ // Codex uses Ink-based TUI with › prompt. Avoid Gemini-specific
3119
+ // F12/compress/error-state logic which is irrelevant for Codex.
3120
+ if (attempt > 1) {
3121
+ // PTY resize to trigger SIGWINCH → Ink re-render
3122
+ try {
3123
+ const session = sessionHelper.getSession(sessionName);
3124
+ if (session) {
3125
+ session.resize(81, 25);
3126
+ await delay(200);
3127
+ session.resize(80, 24);
3128
+ await delay(300);
3129
+ }
3130
+ }
3131
+ catch { /* non-fatal */ }
3132
+ // Ctrl+U to clear any stale input text
3133
+ await sessionHelper.sendKey(sessionName, 'C-u');
3134
+ await delay(300);
3135
+ }
3136
+ }
3137
+ else {
3138
+ // Gemini CLI TUI recovery
3139
+ // Detect recent /compress — Ink TUI loses internal focus after
3140
+ // /compress re-renders, causing subsequent messages to be silently
3141
+ // dropped even though the prompt `>` is visible (#114).
3142
+ // Always force PTY resize on attempt 1 if /compress detected.
3143
+ // Also detect ✖ error state (#130) — MCP connection errors
3144
+ // cause a persistent error indicator that steals TUI focus.
3145
+ const recentOutput = sessionHelper.capturePane(sessionName, 40);
3146
+ const compressDetected = recentOutput.includes('/compress') ||
3147
+ recentOutput.includes('Context compressed') ||
3148
+ recentOutput.includes('Compressing context');
3149
+ // Check for Gemini CLI error indicators ONLY in the status bar area (#130).
3150
+ // The ✖ marker must be in the bottom status lines, not in content area —
3151
+ // checking the full 40-line output causes false positives when ✖ appears
3152
+ // in normal response content (e.g., diffs, lists), triggering spurious
3153
+ // F12 keypresses that open DevTools or disrupt the TUI.
3154
+ const statusArea = recentOutput.split('\n').slice(-GEMINI_ERROR_STATE_CONSTANTS.STATUS_AREA_LINES).join('\n');
3155
+ const errorStateDetected = statusArea.includes(GEMINI_ERROR_STATE_CONSTANTS.ERROR_MARKER) ||
3156
+ GEMINI_ERROR_STATE_CONSTANTS.ERROR_COUNT_PATTERN.test(statusArea);
3157
+ const needsResize = attempt > 1 || compressDetected || errorStateDetected;
3158
+ // Force a PTY resize to trigger SIGWINCH, making Ink
3159
+ // re-render the TUI and potentially restore focus state.
3160
+ if (needsResize) {
3161
+ try {
3162
+ const session = sessionHelper.getSession(sessionName);
3163
+ if (session) {
3164
+ // Resize slightly then back to trigger SIGWINCH
3165
+ session.resize(81, 25);
3166
+ await delay(200);
3167
+ session.resize(80, 24);
3168
+ await delay(500);
3169
+ this.logger.debug('PTY resize sent to trigger TUI re-render', {
3170
+ sessionName,
3171
+ attempt,
3172
+ compressDetected,
3173
+ });
3174
+ }
3175
+ }
3176
+ catch (resizeErr) {
3177
+ this.logger.debug('PTY resize failed (non-fatal)', {
3178
+ sessionName,
3179
+ error: resizeErr instanceof Error ? resizeErr.message : String(resizeErr),
3180
+ });
3181
+ }
3182
+ }
3183
+ // If error state detected (✖), dismiss the error overlay first (#130).
3184
+ // F12 toggles the error details panel in Gemini CLI, Enter dismisses
3185
+ // any overlay/notification, and the combination restores the TUI to a
3186
+ // state where the InputPrompt can accept input.
3187
+ if (errorStateDetected) {
3188
+ this.logger.info('Gemini CLI error state detected (✖), dismissing before delivery (#130)', {
3189
+ sessionName,
3190
+ attempt,
3191
+ });
3192
+ // F12 to close error details panel if open
3193
+ await sessionHelper.sendKey(sessionName, 'F12');
3194
+ await delay(300);
3195
+ // Enter to dismiss any remaining overlay/notification
3196
+ await sessionHelper.sendEnter(sessionName);
3197
+ await delay(500);
3198
+ }
3199
+ // Send Tab to cycle Ink focus. In Ink v6, Tab triggers
3200
+ // focusNext() in FocusContext, which moves focus to the next
3201
+ // focusable component (InputPrompt). This works even when the
3202
+ // input is defocused because the Tab handler runs at the Ink
3203
+ // framework level, not the component level.
3204
+ // Send two Tabs to cycle through error/notification components
3205
+ // that may be in the focus chain (#130).
3206
+ await sessionHelper.sendKey(sessionName, 'Tab');
3207
+ await delay(200);
3208
+ await sessionHelper.sendKey(sessionName, 'Tab');
3209
+ await delay(300);
3210
+ // Then Enter to dismiss any notification overlay and ensure
3211
+ // the input is engaged. Enter on an empty `> ` prompt is a
3212
+ // safe no-op (just shows a new blank prompt line).
3213
+ await sessionHelper.sendEnter(sessionName);
3214
+ // Extra settling time after /compress or error state to let Ink TUI stabilize
3215
+ await delay((compressDetected || errorStateDetected) ? 1000 : 500);
3216
+ }
3217
+ // For Gemini CLI: detect and gently escape interactive modes before
3218
+ // sending the message. Avoid Ctrl-C here — Gemini interprets it as
3219
+ // destructive cancellation/quit in some states.
3220
+ if (isGeminiCli) {
3221
+ const preOutput = sessionHelper.capturePane(sessionName, 10);
3222
+ const interactiveModePatterns = [
3223
+ 'Select a file', // /resume file picker
3224
+ '❯', // Interactive list selector
3225
+ 'Choose an option', // Generic interactive prompt
3226
+ '/resume', // Resume command active
3227
+ ];
3228
+ const inInteractiveMode = interactiveModePatterns.some(p => preOutput.includes(p));
3229
+ if (inInteractiveMode) {
3230
+ this.logger.warn('Gemini CLI in interactive mode, using non-destructive recovery', {
3231
+ sessionName,
3232
+ detectedPattern: interactiveModePatterns.find(p => preOutput.includes(p)),
3233
+ });
3234
+ await sessionHelper.sendKey(sessionName, 'Tab');
3235
+ await delay(250);
3236
+ await sessionHelper.sendEnter(sessionName);
3237
+ await delay(1000);
3238
+ }
3239
+ }
3240
+ // Capture output BEFORE sending to detect changes for ALL runtimes.
3241
+ // Claude Code: progressive output-change detection prevents false-positive
3242
+ // Ctrl+C by giving Claude up to 6.5s to start rendering output.
3243
+ // TUI runtimes: same output-change detection as before.
3244
+ // Use a small pane capture (20 lines) to reduce noise from TUI
3245
+ // border redraws that can cause length changes unrelated to delivery.
3246
+ const beforeOutput = sessionHelper.capturePane(sessionName, 20);
3247
+ const beforeLength = beforeOutput.length;
3248
+ // Deduplication guard (#128): Check if the agent is already
3249
+ // processing on ALL attempts (not just retries). A previous
3250
+ // write may have succeeded but verification failed. Re-writing
3251
+ // the same message creates a duplicate in the input buffer.
3252
+ //
3253
+ // IMPORTANT: Only skip the write when we have a positive hash
3254
+ // match (isRecentDuplicate). The `attempt > 1` condition was
3255
+ // removed because the spinner could be from a DIFFERENT message
3256
+ // that the agent is still processing — skipping the write for
3257
+ // a new message causes silent message loss.
3258
+ {
3259
+ const preWriteCheck = sessionHelper.capturePane(sessionName);
3260
+ const hasSpinner = containsSpinnerOrWorkingIndicator(preWriteCheck);
3261
+ if (hasSpinner) {
3262
+ // Hash-based dedup: if the same message was recently sent
3263
+ // and the agent is processing, it's very likely our message.
3264
+ const msgHash = message.substring(0, 200);
3265
+ const lastSent = this.lastSentMessageHash.get(sessionName);
3266
+ const isRecentDuplicate = lastSent
3267
+ && lastSent.hash === msgHash
3268
+ && (Date.now() - lastSent.sentAt) < 60000;
3269
+ if (isRecentDuplicate) {
3270
+ this.logger.info('Agent already processing — skipping write to prevent duplicate (#128)', {
3271
+ sessionName,
3272
+ attempt,
3273
+ isRecentDuplicate: true,
3274
+ });
3275
+ return true;
3276
+ }
3277
+ // Agent is busy with a different message. On retries,
3278
+ // log but still proceed to write — the message will be
3279
+ // queued in the PTY input buffer for when the agent
3280
+ // finishes its current work.
3281
+ if (attempt > 1) {
3282
+ this.logger.info('Agent busy with different message, proceeding with write on retry', {
3283
+ sessionName,
3284
+ attempt,
3285
+ });
3286
+ }
3287
+ }
3288
+ // On retries: log informationally if the agent is mid-task and
3289
+ // our message text isn't visible at the bottom of the pane.
3290
+ // Historically this branch returned `true` (silent skip) on
3291
+ // `notAtPrompt && !textStuck`, which silently dropped Slack
3292
+ // messages whenever the orc was busy on a prior task and the
3293
+ // force-deliver fallback's first internal write had been
3294
+ // suppressed by the spinner-dedup guard above. Steve's
3295
+ // 2026-04-30 UTC 17:28 / 18:37 incident: messages acked by
3296
+ // the queue, never reached the orc PTY.
3297
+ //
3298
+ // Correct semantics: the only safe condition for skipping is
3299
+ // a positive recent-duplicate hash match (handled in the
3300
+ // `hasSpinner` block above via `isRecentDuplicate`). If we
3301
+ // reach this point we KNOW the new message has not been
3302
+ // confirmed-written this round, so proceed to the actual
3303
+ // `session.write` below — the PTY input buffer will queue
3304
+ // the bytes until the agent is ready to consume them. A
3305
+ // duplicate is recoverable on the agent side; a silent loss
3306
+ // is not.
3307
+ if (attempt > 1) {
3308
+ const notAtPrompt = !this.isClaudeAtPrompt(preWriteCheck, runtimeType);
3309
+ if (notAtPrompt) {
3310
+ const msgSnippet = (message.length > 20
3311
+ ? message.substring(0, 80)
3312
+ : message).replace(/\s+/g, ' ').trim();
3313
+ const bottomLines = preWriteCheck.split('\n').slice(-10).join(' ').replace(/\s+/g, ' ');
3314
+ const textStuck = bottomLines.includes(msgSnippet);
3315
+ this.logger.info('Agent busy on retry — proceeding to write (no silent skip)', {
3316
+ sessionName,
3317
+ attempt,
3318
+ textStuck,
3319
+ });
3320
+ // No early return — fall through to session.write.
3321
+ }
3322
+ }
3323
+ }
3324
+ // Use SessionCommandHelper.sendMessage() — proven two-step write:
3325
+ // 1. session.write(message) — triggers bracketed paste
3326
+ // 2. await delay(scaled) — waits for paste processing
3327
+ // 3. session.write('\r') — sends Enter separately
3328
+ // 4. await delay(KEY_DELAY) — waits for key processing
3329
+ await sessionHelper.sendMessage(sessionName, message);
3330
+ // Register for background stuck-detection safety net (all runtimes).
3331
+ // If progressive verification below misses an Enter drop, the
3332
+ // background scanner will catch it within 30s.
3333
+ this.trackSentMessage(sessionName, message);
3334
+ // Track message hash for deduplication on retry (#128)
3335
+ this.lastSentMessageHash.set(sessionName, {
3336
+ hash: message.substring(0, 200),
3337
+ sentAt: Date.now(),
3338
+ });
3339
+ // Wait for agent to start processing, then verify delivery.
3340
+ // TUI runtimes need a longer delay (3s) for the TUI to redraw
3341
+ // and show processing indicators after accepting input.
3342
+ const processingDelay = isClaudeCode
3343
+ ? SESSION_COMMAND_DELAYS.MESSAGE_PROCESSING_DELAY
3344
+ : 3000;
3345
+ await delay(processingDelay);
3346
+ // === Delivery verification ===
3347
+ // Claude Code: prompt disappears during processing, so check
3348
+ // isMessageStuckAtPrompt (text still at prompt = stuck).
3349
+ // TUI runtimes (Gemini CLI): use TWO-PHASE verification:
3350
+ // Phase 1: Direct check — is the message text sitting ON the
3351
+ // `> ` prompt line? If yes, Enter was dropped. Try
3352
+ // pressing Enter to recover before falling through to retry.
3353
+ // Phase 2: Output-change detection — compare before/after captures
3354
+ // to see if the agent started processing.
3355
+ if (isClaudeCode) {
3356
+ // Progressive verification for Claude Code.
3357
+ // Claude Code routinely takes 3-8s to start rendering with large
3358
+ // context. The old 2.5s window caused false-positive Ctrl+C that
3359
+ // cancelled valid in-progress prompts. Now we progressively check
3360
+ // at [1s, 2s, 3s] intervals (total ~6.5s including processing delay).
3361
+ //
3362
+ // Progressive verification: check at [1s, 2s, 3s] intervals whether
3363
+ // the agent accepted the message and started processing it.
3364
+ //
3365
+ // Uses runtime-agnostic detection — no hardcoded prompt characters.
3366
+ // This stays correct when the agent runtime upgrades its prompt.
3367
+ //
3368
+ // Three signals checked per interval:
3369
+ // 1. Processing indicators (⠋ ⏺ spinners) — definitive success
3370
+ // 2. isClaudeAtPrompt() — if still true, agent is idle, wait more
3371
+ // 3. Message text in bottom lines — if found, Enter was dropped
3372
+ // (message pasted at prompt but not submitted)
3373
+ // If prompt gone AND message text NOT at bottom → success
3374
+ let claudeDelivered = false;
3375
+ for (const intervalMs of SESSION_COMMAND_DELAYS.CLAUDE_VERIFICATION_INTERVALS) {
3376
+ const currentOutput = sessionHelper.capturePane(sessionName);
3377
+ // 1. Processing indicators (spinners, working marker) are
3378
+ // definitive proof the agent accepted and is working.
3379
+ // Only check spinner/⏺ chars — NOT text words like
3380
+ // "thinking" which appear in historical response text.
3381
+ if (containsSpinnerOrWorkingIndicator(currentOutput)) {
3382
+ this.logger.debug('Processing indicators detected — message accepted', {
3383
+ sessionName,
3384
+ attempt,
3385
+ });
3386
+ claudeDelivered = true;
3387
+ break;
3388
+ }
3389
+ // 2. Is the agent still at its standard idle prompt?
3390
+ // Reuse isClaudeAtPrompt() — the single source of truth
3391
+ // for prompt detection. This avoids hardcoding prompt
3392
+ // characters and stays correct when the runtime upgrades.
3393
+ if (this.isClaudeAtPrompt(currentOutput, runtimeType)) {
3394
+ // Prompt is visible. Before just waiting, check if our
3395
+ // message text is also at the bottom — this means the
3396
+ // text was pasted but Enter was dropped. Press Enter.
3397
+ // Normalize whitespace: enhanced messages contain literal \n from
3398
+ // addContinuationInstructions(). join(' ') replaces line breaks with
3399
+ // spaces, but the snippet still has \n chars, so includes() fails.
3400
+ const promptMsgSnippet = (message.length > 20
3401
+ ? message.substring(0, 80)
3402
+ : message).replace(/\s+/g, ' ').trim();
3403
+ const promptBottomLines = currentOutput.split('\n').slice(-10).join(' ').replace(/\s+/g, ' ');
3404
+ if (promptBottomLines.includes(promptMsgSnippet)) {
3405
+ this.logger.warn('At prompt with message text at bottom — pressing Tab+Enter', {
3406
+ sessionName,
3407
+ attempt,
3408
+ intervalMs,
3409
+ });
3410
+ // Tab restores Ink TUI focus before Enter — without this,
3411
+ // Enter may be consumed by the framework but not routed to
3412
+ // the input component if focus was lost during a re-render
3413
+ // (e.g., after stop hooks, state transitions).
3414
+ await sessionHelper.sendKey(sessionName, 'Tab');
3415
+ await delay(200);
3416
+ await sessionHelper.sendEnter(sessionName);
3417
+ await delay(500);
3418
+ await sessionHelper.sendEnter(sessionName); // backup
3419
+ await delay(SESSION_COMMAND_DELAYS.MESSAGE_PROCESSING_DELAY);
3420
+ // Verify recovery
3421
+ const postEnterOutput = sessionHelper.capturePane(sessionName);
3422
+ if (containsSpinnerOrWorkingIndicator(postEnterOutput) ||
3423
+ !this.isClaudeAtPrompt(postEnterOutput, runtimeType)) {
3424
+ this.logger.info('Enter recovery from prompt successful', {
3425
+ sessionName,
3426
+ attempt,
3427
+ });
3428
+ claudeDelivered = true;
3429
+ break;
3430
+ }
3431
+ }
3432
+ // Fast-response detection: if the terminal output changed since
3433
+ // we sent the message, Claude processed it and returned to prompt
3434
+ // before our first check. Without this, fast responses (e.g.,
3435
+ // simple IP queries) cause false "delivery unconfirmed" verdicts
3436
+ // and trigger duplicate sends (#retry-storm).
3437
+ const outputChanged = currentOutput !== beforeOutput;
3438
+ if (outputChanged) {
3439
+ this.logger.info('Agent at prompt but output changed — fast response detected, delivery confirmed', {
3440
+ sessionName,
3441
+ attempt,
3442
+ intervalMs,
3443
+ beforeLength: beforeOutput.length,
3444
+ currentLength: currentOutput.length,
3445
+ });
3446
+ claudeDelivered = true;
3447
+ break;
3448
+ }
3449
+ this.logger.debug('Agent still at prompt, waiting before re-check', {
3450
+ sessionName,
3451
+ attempt,
3452
+ intervalMs,
3453
+ });
3454
+ await delay(intervalMs);
3455
+ continue;
3456
+ }
3457
+ // 3. Prompt not in its standard idle form. Before declaring
3458
+ // success, check if our message text is stuck at the
3459
+ // bottom of the terminal. When Enter is dropped, the
3460
+ // pasted text appears on the prompt line — the terminal
3461
+ // shows the prompt char followed by our message text.
3462
+ // This check is runtime-agnostic: it works regardless
3463
+ // of what prompt character the runtime uses.
3464
+ // Normalize whitespace in snippet (see Step 2 comment above).
3465
+ const msgSnippet = (message.length > 20
3466
+ ? message.substring(0, 80)
3467
+ : message).replace(/\s+/g, ' ').trim();
3468
+ const bottomLines = currentOutput.split('\n').slice(-10).join(' ').replace(/\s+/g, ' ');
3469
+ if (bottomLines.includes(msgSnippet)) {
3470
+ // Message text is at the bottom of the terminal but the
3471
+ // prompt is no longer in its idle form — Enter was dropped.
3472
+ // Instead of waiting and doing a full Ctrl+C + resend retry,
3473
+ // use Tab to restore TUI focus, then Enter to submit.
3474
+ this.logger.warn('Message text stuck at bottom — pressing Tab+Enter to recover', {
3475
+ sessionName,
3476
+ attempt,
3477
+ intervalMs,
3478
+ });
3479
+ await sessionHelper.sendKey(sessionName, 'Tab');
3480
+ await delay(200);
3481
+ await sessionHelper.sendEnter(sessionName);
3482
+ await delay(500);
3483
+ await sessionHelper.sendEnter(sessionName); // backup Enter
3484
+ // Give the agent a moment to start processing after Enter
3485
+ await delay(SESSION_COMMAND_DELAYS.MESSAGE_PROCESSING_DELAY);
3486
+ // Verify recovery: check if processing started
3487
+ const recoveryOutput = sessionHelper.capturePane(sessionName);
3488
+ if (containsSpinnerOrWorkingIndicator(recoveryOutput)) {
3489
+ this.logger.info('Enter recovery successful — processing started', {
3490
+ sessionName,
3491
+ attempt,
3492
+ });
3493
+ claudeDelivered = true;
3494
+ break;
3495
+ }
3496
+ // Check if prompt reappeared (Enter submitted but processed instantly)
3497
+ // or if output changed from the stuck state
3498
+ const recoveryBottom = recoveryOutput.split('\n').slice(-10).join(' ').replace(/\s+/g, ' ');
3499
+ if (!recoveryBottom.includes(msgSnippet)) {
3500
+ this.logger.info('Enter recovery successful — stuck text cleared', {
3501
+ sessionName,
3502
+ attempt,
3503
+ });
3504
+ claudeDelivered = true;
3505
+ break;
3506
+ }
3507
+ // Still stuck after Enter recovery — continue to next interval
3508
+ this.logger.debug('Still stuck after Enter recovery, continuing verification', {
3509
+ sessionName,
3510
+ attempt,
3511
+ });
3512
+ await delay(intervalMs);
3513
+ continue;
3514
+ }
3515
+ // Prompt gone AND no stuck text → agent started processing
3516
+ this.logger.debug('Prompt gone, no stuck text — message delivered', {
3517
+ sessionName,
3518
+ attempt,
3519
+ });
3520
+ claudeDelivered = true;
3521
+ break;
3522
+ }
3523
+ // --- Extended confirmation loop ---
3524
+ // If progressive verification didn't confirm delivery, run an
3525
+ // extended check loop (every 3s, up to 30s total). Each iteration
3526
+ // captures the pane, checks for spinner/working indicators, and
3527
+ // presses Tab+Enter if message text is still stuck at the bottom.
3528
+ if (!claudeDelivered) {
3529
+ const CONFIRM_INTERVAL_MS = 3000;
3530
+ const CONFIRM_MAX_MS = 30000;
3531
+ const confirmStart = Date.now();
3532
+ let confirmAttempt = 0;
3533
+ // Normalize snippet once for reuse
3534
+ const confirmSnippet = (message.length > 20
3535
+ ? message.substring(0, 80)
3536
+ : message).replace(/\s+/g, ' ').trim();
3537
+ while (Date.now() - confirmStart < CONFIRM_MAX_MS) {
3538
+ await delay(CONFIRM_INTERVAL_MS);
3539
+ confirmAttempt++;
3540
+ const loopOutput = sessionHelper.capturePane(sessionName);
3541
+ const loopBottom = loopOutput.split('\n').slice(-10).join(' ').replace(/\s+/g, ' ');
3542
+ const textStuck = loopBottom.includes(confirmSnippet);
3543
+ // Spinner or working indicator → delivered
3544
+ if (containsSpinnerOrWorkingIndicator(loopOutput)) {
3545
+ this.logger.info('Confirmation loop: processing detected', {
3546
+ sessionName, attempt, confirmAttempt,
3547
+ });
3548
+ claudeDelivered = true;
3549
+ break;
3550
+ }
3551
+ // Text still stuck → Enter was dropped, NOT delivered. Try
3552
+ // Tab+Enter recovery and re-check next iteration. This is
3553
+ // checked BEFORE the output-change shortcut below: when our
3554
+ // own pasted message is echoed at the prompt the output DID
3555
+ // change vs the pre-send baseline, but that change is the
3556
+ // stuck echo, not a response — treating it as "delivered"
3557
+ // is the false-positive that masked the orc 假死 (#686
3558
+ // follow-up: a hung session looked "delivered" forever).
3559
+ if (textStuck) {
3560
+ this.logger.warn('Confirmation loop: text still stuck, pressing Tab+Enter', {
3561
+ sessionName, attempt, confirmAttempt,
3562
+ });
3563
+ await sessionHelper.sendKey(sessionName, 'Tab');
3564
+ await delay(200);
3565
+ await sessionHelper.sendEnter(sessionName);
3566
+ await delay(500);
3567
+ await sessionHelper.sendEnter(sessionName); // backup
3568
+ continue;
3569
+ }
3570
+ // Fast-response detection: output changed since send AND our
3571
+ // message text is NOT stuck at the prompt → a real response
3572
+ // landed and returned to prompt before we looked. Delivered.
3573
+ if (loopOutput !== beforeOutput) {
3574
+ this.logger.info('Confirmation loop: output changed from pre-send — fast response confirmed', {
3575
+ sessionName, attempt, confirmAttempt,
3576
+ });
3577
+ claudeDelivered = true;
3578
+ break;
3579
+ }
3580
+ // Not at prompt and text not stuck → delivered
3581
+ if (!this.isClaudeAtPrompt(loopOutput, runtimeType)) {
3582
+ this.logger.info('Confirmation loop: prompt gone, text cleared', {
3583
+ sessionName, attempt, confirmAttempt,
3584
+ });
3585
+ claudeDelivered = true;
3586
+ break;
3587
+ }
3588
+ }
3589
+ if (!claudeDelivered) {
3590
+ this.logger.error('Confirmation loop exhausted — delivery unconfirmed', {
3591
+ sessionName, attempt, confirmAttempts: confirmAttempt,
3592
+ });
3593
+ }
3594
+ }
3595
+ if (claudeDelivered) {
3596
+ return true;
3597
+ }
3598
+ }
3599
+ else {
3600
+ // --- Gemini CLI: lightweight verification ---
3601
+ // Gemini CLI's PTY write is reliable. The aggressive verification
3602
+ // (stuck detection + Tab+Enter recovery + output-change diff) causes
3603
+ // false "stuck" judgments that trigger visible retry error spam even
3604
+ // when the message was successfully delivered (#retry-storm fix).
3605
+ //
3606
+ // Strategy: only check for the definitive stuck signal (message text
3607
+ // literally sitting on the `> ` prompt line). If not stuck, trust the
3608
+ // write. Skip the output-change heuristics that cause false negatives.
3609
+ const isGemini = runtimeType === RUNTIME_TYPES.GEMINI_CLI;
3610
+ if (isGemini) {
3611
+ // Single stuck-at-prompt check — the only reliable failure signal
3612
+ const stuckAtPrompt = this.isTextStuckAtTuiPrompt(sessionName, message);
3613
+ if (stuckAtPrompt) {
3614
+ const recovered = await this.recoverStuckTuiMessage(sessionName, message);
3615
+ if (recovered) {
3616
+ this.logger.info('Gemini CLI: message recovered via Enter re-press', {
3617
+ sessionName,
3618
+ attempt,
3619
+ });
3620
+ return true;
3621
+ }
3622
+ this.logger.warn('Gemini CLI: message still stuck after Enter recovery', {
3623
+ sessionName,
3624
+ attempt,
3625
+ });
3626
+ // Fall through to retry cleanup below
3627
+ }
3628
+ else {
3629
+ // Not stuck at prompt — PTY write succeeded, trust delivery.
3630
+ // The old code did output-change detection here, but TUI redraws
3631
+ // and minimal length diffs caused false "not delivered" verdicts.
3632
+ this.logger.debug('Gemini CLI: message not stuck at prompt — delivery trusted', {
3633
+ sessionName,
3634
+ attempt,
3635
+ });
3636
+ return true;
3637
+ }
3638
+ }
3639
+ else {
3640
+ // --- Non-Gemini TUI runtimes: full verification ---
3641
+ // Phase 1: Direct stuck-at-prompt detection
3642
+ const stuckAtPrompt = this.isTextStuckAtTuiPrompt(sessionName, message);
3643
+ if (stuckAtPrompt) {
3644
+ const recovered = await this.recoverStuckTuiMessage(sessionName, message);
3645
+ if (recovered) {
3646
+ this.logger.info('Message recovered via Enter re-press', {
3647
+ sessionName,
3648
+ attempt,
3649
+ });
3650
+ return true;
3651
+ }
3652
+ this.logger.warn('Message still stuck after Enter recovery, will retry', {
3653
+ sessionName,
3654
+ attempt,
3655
+ });
3656
+ }
3657
+ else {
3658
+ // Phase 2: Output-change detection
3659
+ const afterOutput = sessionHelper.capturePane(sessionName, 20);
3660
+ const lengthDiff = afterOutput.length - beforeLength;
3661
+ const contentChanged = beforeOutput !== afterOutput;
3662
+ let newContent = '';
3663
+ if (contentChanged && afterOutput.length > beforeOutput.length) {
3664
+ newContent = afterOutput.slice(beforeLength);
3665
+ }
3666
+ else if (contentChanged) {
3667
+ const beforeLines = new Set(beforeOutput.split('\n'));
3668
+ newContent = afterOutput
3669
+ .split('\n')
3670
+ .filter((line) => !beforeLines.has(line))
3671
+ .join('\n');
3672
+ }
3673
+ const hasProcessingIndicators = containsProcessingIndicator(newContent || afterOutput.slice(-500));
3674
+ const significantLengthChange = Math.abs(lengthDiff) > 10;
3675
+ const delivered = (lengthDiff > 20)
3676
+ || (contentChanged && significantLengthChange)
3677
+ || hasProcessingIndicators;
3678
+ if (delivered) {
3679
+ const hasStrongSignal = hasProcessingIndicators;
3680
+ const logLevel = hasStrongSignal ? 'debug' : 'warn';
3681
+ this.logger[logLevel](`Message delivery verified (${hasStrongSignal ? 'strong' : 'weak'} signal)`, {
3682
+ sessionName,
3683
+ attempt,
3684
+ lengthDiff,
3685
+ contentChanged,
3686
+ hasProcessingIndicators,
3687
+ newContentLength: newContent.length,
3688
+ signal: hasStrongSignal ? 'processing-indicators' : 'output-length-change-only',
3689
+ });
3690
+ return true;
3691
+ }
3692
+ this.logger.warn('TUI output did not change after send — message may not have been accepted', {
3693
+ sessionName,
3694
+ attempt,
3695
+ lengthDiff,
3696
+ contentChanged,
3697
+ hasProcessingIndicators,
3698
+ newContentLength: newContent.length,
3699
+ });
3700
+ }
3701
+ }
3702
+ }
3703
+ // Message stuck at prompt — clear line and retry.
3704
+ this.logger.warn('Message stuck at prompt after send, clearing for retry', {
3705
+ sessionName,
3706
+ attempt,
3707
+ });
3708
+ if (isClaudeCode) {
3709
+ await sessionHelper.clearCurrentCommandLine(sessionName);
3710
+ }
3711
+ else {
3712
+ // Gemini CLI retry cleanup: NEVER send Ctrl+C — it triggers /quit
3713
+ // and kills the CLI entirely, regardless of whether text is in the
3714
+ // input box or not. Use Tab to cycle Ink focus back to the input
3715
+ // prompt, then Enter to submit any pending text.
3716
+ this.logger.warn('Gemini CLI message stuck — using Tab focus recovery (no Ctrl+C)', {
3717
+ sessionName,
3718
+ attempt,
3719
+ });
3720
+ await sessionHelper.sendKey(sessionName, 'Tab');
3721
+ await delay(300);
3722
+ await sessionHelper.sendEnter(sessionName);
3723
+ await delay(300);
3724
+ }
3725
+ await delay(SESSION_COMMAND_DELAYS.CLEAR_COMMAND_DELAY);
3726
+ if (attempt < maxAttempts) {
3727
+ await delay(SESSION_COMMAND_DELAYS.MESSAGE_RETRY_DELAY);
3728
+ }
3729
+ }
3730
+ catch (error) {
3731
+ this.logger.error('Error during message delivery', {
3732
+ sessionName,
3733
+ attempt,
3734
+ error: error instanceof Error ? error.message : String(error),
3735
+ });
3736
+ if (attempt < maxAttempts) {
3737
+ await delay(SESSION_COMMAND_DELAYS.MESSAGE_RETRY_DELAY);
3738
+ }
3739
+ }
3740
+ }
3741
+ this.logger.warn('Message delivery verification failed — all attempts exhausted', {
3742
+ sessionName,
3743
+ maxAttempts,
3744
+ messageLength: message.length,
3745
+ });
3746
+ this.logger.error('Message delivery failed after all retry attempts', {
3747
+ sessionName,
3748
+ maxAttempts,
3749
+ messageLength: message.length,
3750
+ });
3751
+ return false;
3752
+ }
3753
+ /**
3754
+ * Check if a sent message is stuck at the terminal prompt (Enter was not accepted).
3755
+ *
3756
+ * Captures the current terminal pane and looks for the message text still visible
3757
+ * on the last few lines. If the text is found, Enter was not processed and the
3758
+ * message is stuck.
3759
+ *
3760
+ * @param sessionName - The session to check
3761
+ * @param message - The original message that was sent
3762
+ * @returns true if the message text is still visible at the prompt (stuck)
3763
+ */
3764
+ async isMessageStuckAtPrompt(sessionName, message) {
3765
+ try {
3766
+ const sessionHelper = await this.getSessionHelper();
3767
+ const output = sessionHelper.capturePane(sessionName);
3768
+ if (!output || output.trim().length === 0) {
3769
+ return false;
3770
+ }
3771
+ // Extract a search token from the message:
3772
+ // Strip [CHAT:uuid] prefix if present, then take the first 40 chars
3773
+ const { prefixLength } = extractChatPrefix(message);
3774
+ const contentAfterPrefix = prefixLength > 0
3775
+ ? message.slice(prefixLength)
3776
+ : message;
3777
+ const searchToken = contentAfterPrefix.slice(0, 40).trim();
3778
+ // Also use [CHAT: as a secondary token if message has a CHAT prefix
3779
+ const chatToken = prefixLength > 0 ? '[CHAT:' : null;
3780
+ // Check last 20 non-empty lines for either token.
3781
+ // Gemini CLI TUI has status bars at the bottom (branch, sandbox, model info)
3782
+ // that push input content further up. 5 lines was insufficient.
3783
+ const lines = output.split('\n').filter((line) => line.trim().length > 0);
3784
+ const linesToCheck = lines.slice(-20);
3785
+ const isStuck = linesToCheck.some((line) => {
3786
+ // Strip TUI box-drawing borders before checking (Gemini CLI wraps content in │...│)
3787
+ const stripped = stripTuiLineBorders(line);
3788
+ if (searchToken && (line.includes(searchToken) || stripped.includes(searchToken)))
3789
+ return true;
3790
+ if (chatToken && (line.includes(chatToken) || stripped.includes(chatToken)))
3791
+ return true;
3792
+ return false;
3793
+ });
3794
+ this.logger.debug('isMessageStuckAtPrompt result', {
3795
+ sessionName,
3796
+ isStuck,
3797
+ searchToken: searchToken.slice(0, 20),
3798
+ linesChecked: linesToCheck.length,
3799
+ });
3800
+ return isStuck;
3801
+ }
3802
+ catch (error) {
3803
+ this.logger.warn('Error checking if message stuck at prompt', {
3804
+ sessionName,
3805
+ error: error instanceof Error ? error.message : String(error),
3806
+ });
3807
+ return false;
3808
+ }
3809
+ }
3810
+ /**
3811
+ * Check if message text is stuck at the TUI prompt line (Enter was not pressed).
3812
+ *
3813
+ * Unlike `isMessageStuckAtPrompt` which checks the last 20 lines for the message
3814
+ * text ANYWHERE, this method specifically checks whether the message text appears
3815
+ * ON the prompt line itself (the line starting with `> ` or `│ > `).
3816
+ *
3817
+ * After Enter is successfully pressed, the text moves from the prompt line to the
3818
+ * chat history area. So if the text is still on the prompt line, Enter was dropped.
3819
+ *
3820
+ * This avoids the false positive problem where `isMessageStuckAtPrompt` matches
3821
+ * the message text in the TUI's chat history (which echoes submitted messages).
3822
+ *
3823
+ * @param sessionName - The session to check
3824
+ * @param message - The original message that was sent
3825
+ * @returns true if the message text is visible on the TUI prompt line (stuck)
3826
+ */
3827
+ isTextStuckAtTuiPrompt(sessionName, message) {
3828
+ try {
3829
+ const sessionHelper = this._sessionHelper;
3830
+ if (!sessionHelper)
3831
+ return false;
3832
+ const output = sessionHelper.capturePane(sessionName);
3833
+ if (!output || output.trim().length === 0)
3834
+ return false;
3835
+ // Extract search token: first 30 chars of the message content
3836
+ // (after stripping any [CHAT:uuid] prefix)
3837
+ const { prefixLength } = extractChatPrefix(message);
3838
+ const contentAfterPrefix = prefixLength > 0
3839
+ ? message.slice(prefixLength)
3840
+ : message;
3841
+ const searchToken = contentAfterPrefix.slice(0, 30).trim();
3842
+ if (!searchToken)
3843
+ return false;
3844
+ // Find lines that look like a TUI prompt with text after it.
3845
+ // Gemini CLI TUI wraps content in box-drawing borders: │ > text │
3846
+ // Or without borders: > text
3847
+ // The prompt line is the line with `> ` followed by actual content.
3848
+ const lines = output.split('\n');
3849
+ for (let i = lines.length - 1; i >= Math.max(0, lines.length - 25); i--) {
3850
+ const line = lines[i];
3851
+ const promptContent = matchTuiPromptLine(line);
3852
+ if (promptContent !== null) {
3853
+ const trimmedContent = stripTuiLineBorders(promptContent).trim();
3854
+ // Check if the prompt line content contains our message text.
3855
+ // Only check the BOTTOMMOST prompt line — historical echoes of
3856
+ // submitted messages also appear as `> text` further up in the
3857
+ // TUI conversation history. Without this break, those echoes
3858
+ // cause false "stuck" detections even after the message was
3859
+ // successfully delivered.
3860
+ if (trimmedContent.length > 5 && trimmedContent.includes(searchToken)) {
3861
+ this.logger.warn('Text stuck at TUI prompt — Enter was not pressed', {
3862
+ sessionName,
3863
+ searchToken: searchToken.slice(0, 20),
3864
+ promptContent: promptContent.slice(0, 60),
3865
+ });
3866
+ return true;
3867
+ }
3868
+ break; // Only check the first (bottommost) prompt line
3869
+ }
3870
+ }
3871
+ return false;
3872
+ }
3873
+ catch (error) {
3874
+ this.logger.debug('Error in isTextStuckAtTuiPrompt', {
3875
+ sessionName,
3876
+ error: error instanceof Error ? error.message : String(error),
3877
+ });
3878
+ return false;
3879
+ }
3880
+ }
3881
+ /**
3882
+ * Attempt to recover from text stuck at TUI prompt by pressing Enter.
3883
+ * Checks if text is stuck, sends Enter, waits, then verifies.
3884
+ *
3885
+ * @param sessionName - The session to recover
3886
+ * @param message - The original message
3887
+ * @returns true if recovery succeeded (text is no longer at prompt)
3888
+ */
3889
+ async recoverStuckTuiMessage(sessionName, message) {
3890
+ const sessionHelper = await this.getSessionHelper();
3891
+ // Tab restores Ink TUI focus, then Enter to submit the stuck text.
3892
+ // Without Tab, Enter may be consumed by the framework but not routed
3893
+ // to the input component if focus was lost during a re-render.
3894
+ this.logger.info('Pressing Tab+Enter to recover stuck TUI message', { sessionName });
3895
+ await sessionHelper.sendKey(sessionName, 'Tab');
3896
+ await delay(200);
3897
+ await sessionHelper.sendEnter(sessionName);
3898
+ await delay(500);
3899
+ // Double-tap: send a backup Enter in case the first was consumed
3900
+ await sessionHelper.sendEnter(sessionName);
3901
+ await delay(2000);
3902
+ // Verify text is no longer at prompt
3903
+ const stillStuck = this.isTextStuckAtTuiPrompt(sessionName, message);
3904
+ if (!stillStuck) {
3905
+ this.logger.info('TUI message recovery succeeded — Enter accepted', { sessionName });
3906
+ return true;
3907
+ }
3908
+ this.logger.warn('TUI message still stuck after Enter recovery', { sessionName });
3909
+ return false;
3910
+ }
3911
+ /**
3912
+ * Start a background scanner that periodically checks all registered TUI
3913
+ * sessions for text stuck at the prompt (Enter not pressed).
3914
+ *
3915
+ * This is a safety net that catches messages missed by the per-send
3916
+ * verification in `sendMessageWithRetry`. It scans every 30 seconds.
3917
+ *
3918
+ * When stuck text is detected, the scanner presses Enter to recover.
3919
+ * If text has been sitting at the prompt for multiple consecutive scans,
3920
+ * it escalates to Tab + Enter (TUI focus recovery).
3921
+ */
3922
+ startStuckMessageDetector() {
3923
+ if (this.stuckMessageDetectorTimer) {
3924
+ this.logger.debug('Stuck message detector already running');
3925
+ return;
3926
+ }
3927
+ const SCAN_INTERVAL_MS = 30000; // 30 seconds
3928
+ this.logger.info('Starting background stuck-message detector', {
3929
+ intervalMs: SCAN_INTERVAL_MS,
3930
+ });
3931
+ this.stuckMessageDetectorTimer = setInterval(async () => {
3932
+ try {
3933
+ await this.scanForStuckMessages();
3934
+ }
3935
+ catch (error) {
3936
+ this.logger.debug('Stuck message scan error (non-fatal)', {
3937
+ error: error instanceof Error ? error.message : String(error),
3938
+ });
3939
+ }
3940
+ }, SCAN_INTERVAL_MS);
3941
+ }
3942
+ /**
3943
+ * Stop the background stuck-message detector.
3944
+ */
3945
+ stopStuckMessageDetector() {
3946
+ if (this.stuckMessageDetectorTimer) {
3947
+ clearInterval(this.stuckMessageDetectorTimer);
3948
+ this.stuckMessageDetectorTimer = null;
3949
+ this.logger.info('Stopped background stuck-message detector');
3950
+ }
3951
+ }
3952
+ /**
3953
+ * Scan all sessions for stuck messages. Two complementary strategies:
3954
+ *
3955
+ * 1. TUI prompt-line scanning (existing): checks registered TUI sessions
3956
+ * for text sitting on the `> ` prompt line.
3957
+ * 2. Tracked-message scanning (new, all runtimes): checks if any recently
3958
+ * sent message text is still visible at the bottom of the terminal
3959
+ * after 15+ seconds — runtime-agnostic Enter-drop detection.
3960
+ */
3961
+ async scanForStuckMessages() {
3962
+ const sessionHelper = this._sessionHelper;
3963
+ if (!sessionHelper)
3964
+ return;
3965
+ // --- Part 0: Rewind mode detection (all sessions) ---
3966
+ // If Claude Code is stuck in Rewind mode (triggered by ESC during
3967
+ // processing), send 'q' to exit before any other recovery attempts.
3968
+ for (const sessionName of sessionHelper.listSessions()) {
3969
+ try {
3970
+ const output = sessionHelper.capturePane(sessionName);
3971
+ if (containsRewindMode(output)) {
3972
+ this.logger.warn('Rewind mode detected, sending q to exit', { sessionName });
3973
+ sessionHelper.writeRaw(sessionName, 'q');
3974
+ await delay(500);
3975
+ }
3976
+ }
3977
+ catch {
3978
+ // Non-fatal: session may have been destroyed
3979
+ }
3980
+ }
3981
+ // --- Part 1: Existing TUI prompt-line scanning ---
3982
+ for (const [sessionName, runtimeType] of this.tuiSessionRegistry) {
3983
+ try {
3984
+ // Skip Gemini CLI sessions — their TUI prompt behaves differently
3985
+ // and false-positive stuck detection causes Tab+Enter spam.
3986
+ if (runtimeType === RUNTIME_TYPES.GEMINI_CLI) {
3987
+ continue;
3988
+ }
3989
+ // #213: Skip orchestrator — it frequently waits for long-running
3990
+ // agent tasks (30+ min), and false stuck detection causes disruptive
3991
+ // Tab+Enter key injections that interrupt its workflow.
3992
+ if (sessionName === ORCHESTRATOR_SESSION_NAME) {
3993
+ continue;
3994
+ }
3995
+ // Skip sessions that no longer exist
3996
+ if (!sessionHelper.sessionExists(sessionName)) {
3997
+ this.tuiSessionRegistry.delete(sessionName);
3998
+ continue;
3999
+ }
4000
+ const output = sessionHelper.capturePane(sessionName);
4001
+ if (!output || output.trim().length === 0)
4002
+ continue;
4003
+ // Look for any text sitting on the prompt line
4004
+ const lines = output.split('\n');
4005
+ for (let i = lines.length - 1; i >= Math.max(0, lines.length - 25); i--) {
4006
+ const promptContent = matchTuiPromptLine(lines[i]);
4007
+ if (promptContent !== null) {
4008
+ const trimmedContent = stripTuiLineBorders(promptContent).trim();
4009
+ // Only act on substantial text (> 10 chars) to avoid false positives
4010
+ // from TUI rendering artifacts or short status text
4011
+ if (trimmedContent.length > 10) {
4012
+ // Skip known idle placeholder text that sits at
4013
+ // the `> ` prompt when no user input is present. These are
4014
+ // NOT stuck messages — they are TUI decoration.
4015
+ const lowerContent = trimmedContent.toLowerCase();
4016
+ const isPlaceholder = lowerContent.startsWith('type your message') ||
4017
+ trimmedContent.startsWith('@'); // e.g., "@path/to/file"
4018
+ if (isPlaceholder) {
4019
+ break;
4020
+ }
4021
+ this.logger.warn('Background scan: text stuck at TUI prompt, pressing Tab+Enter', {
4022
+ sessionName,
4023
+ promptContent: promptContent.slice(0, 80),
4024
+ });
4025
+ // Tab restores Ink TUI focus before Enter
4026
+ await sessionHelper.sendKey(sessionName, 'Tab');
4027
+ await delay(200);
4028
+ await sessionHelper.sendEnter(sessionName);
4029
+ await delay(500);
4030
+ // Backup Enter
4031
+ await sessionHelper.sendEnter(sessionName);
4032
+ }
4033
+ break; // Only check the first prompt line found from the bottom
4034
+ }
4035
+ }
4036
+ }
4037
+ catch (error) {
4038
+ this.logger.debug('Error scanning session for stuck messages', {
4039
+ sessionName,
4040
+ error: error instanceof Error ? error.message : String(error),
4041
+ });
4042
+ }
4043
+ }
4044
+ // --- Part 2: Tracked-message scanning (non-Gemini runtimes) ---
4045
+ const now = Date.now();
4046
+ const MIN_AGE_MS = 10000; // Check messages older than 10s
4047
+ const MAX_RECOVERY_ATTEMPTS = 5; // Give up after 5 Tab+Enter attempts
4048
+ for (const [sessionName, entries] of this.sentMessageTracker) {
4049
+ if (!sessionHelper.sessionExists(sessionName)) {
4050
+ this.sentMessageTracker.delete(sessionName);
4051
+ continue;
4052
+ }
4053
+ // Skip Gemini CLI sessions — Tab+Enter recovery causes more harm
4054
+ // than good because the Gemini TUI handles input differently.
4055
+ const trackedRuntime = this.tuiSessionRegistry.get(sessionName);
4056
+ if (trackedRuntime === RUNTIME_TYPES.GEMINI_CLI) {
4057
+ continue;
4058
+ }
4059
+ try {
4060
+ const output = sessionHelper.capturePane(sessionName);
4061
+ if (!output || output.trim().length === 0)
4062
+ continue;
4063
+ const bottomText = output.split('\n').slice(-15).join(' ').replace(/\s+/g, ' ');
4064
+ for (const entry of entries) {
4065
+ if (entry.recovered)
4066
+ continue;
4067
+ if (now - entry.sentAt < MIN_AGE_MS)
4068
+ continue;
4069
+ if (entry.recoveryAttempts >= MAX_RECOVERY_ATTEMPTS) {
4070
+ // Exhausted recovery attempts — mark as recovered to stop retrying
4071
+ entry.recovered = true;
4072
+ this.logger.error('Background scan: max recovery attempts exhausted', {
4073
+ sessionName,
4074
+ snippet: entry.snippet.slice(0, 50),
4075
+ attempts: entry.recoveryAttempts,
4076
+ });
4077
+ continue;
4078
+ }
4079
+ if (bottomText.includes(entry.snippet)) {
4080
+ entry.recoveryAttempts++;
4081
+ this.logger.warn('Background scan: tracked message stuck, pressing Tab+Enter', {
4082
+ sessionName,
4083
+ snippet: entry.snippet.slice(0, 50),
4084
+ ageMs: now - entry.sentAt,
4085
+ attempt: entry.recoveryAttempts,
4086
+ maxAttempts: MAX_RECOVERY_ATTEMPTS,
4087
+ });
4088
+ // Tab restores Ink TUI focus before Enter
4089
+ await sessionHelper.sendKey(sessionName, 'Tab');
4090
+ await delay(200);
4091
+ await sessionHelper.sendEnter(sessionName);
4092
+ await delay(500);
4093
+ await sessionHelper.sendEnter(sessionName); // backup
4094
+ break; // One recovery per session per scan cycle
4095
+ }
4096
+ }
4097
+ }
4098
+ catch (error) {
4099
+ this.logger.debug('Error scanning tracked messages for session', {
4100
+ sessionName,
4101
+ error: error instanceof Error ? error.message : String(error),
4102
+ });
4103
+ }
4104
+ }
4105
+ // Clean up old tracked-message entries
4106
+ for (const [sessionName, entries] of this.sentMessageTracker) {
4107
+ const fresh = entries.filter(e => now - e.sentAt < 5 * 60 * 1000);
4108
+ if (fresh.length === 0) {
4109
+ this.sentMessageTracker.delete(sessionName);
4110
+ }
4111
+ else {
4112
+ this.sentMessageTracker.set(sessionName, fresh);
4113
+ }
4114
+ }
4115
+ }
4116
+ /**
4117
+ * Unregister a TUI session from the stuck-message detector.
4118
+ * Call this when a session is destroyed or agent is stopped.
4119
+ *
4120
+ * @param sessionName - The session to unregister
4121
+ */
4122
+ unregisterTuiSession(sessionName) {
4123
+ this.tuiSessionRegistry.delete(sessionName);
4124
+ this.sentMessageTracker.delete(sessionName);
4125
+ }
4126
+ /**
4127
+ * Register a message that was sent to a session for background
4128
+ * stuck-detection. The scanner will periodically check if the
4129
+ * message text is still visible at the bottom of the terminal.
4130
+ *
4131
+ * @param sessionName - The session the message was sent to
4132
+ * @param message - The full message text
4133
+ */
4134
+ trackSentMessage(sessionName, message) {
4135
+ // Extract a search snippet: skip [CHAT:uuid] prefix, take first 80 chars
4136
+ const { prefixLength } = extractChatPrefix(message);
4137
+ const contentStart = prefixLength;
4138
+ // Normalize whitespace: messages may contain \n from enhanced templates.
4139
+ // Terminal bottom text is join(' '), so \n in snippet would never match.
4140
+ const snippet = message.slice(contentStart, contentStart + 80).replace(/\s+/g, ' ').trim();
4141
+ if (snippet.length < 10)
4142
+ return; // Too short to reliably match
4143
+ const entries = this.sentMessageTracker.get(sessionName) || [];
4144
+ entries.push({ snippet, sentAt: Date.now(), recovered: false, recoveryAttempts: 0 });
4145
+ // Keep only last 5 minutes of entries
4146
+ const cutoff = Date.now() - 5 * 60 * 1000;
4147
+ this.sentMessageTracker.set(sessionName, entries.filter(e => e.sentAt > cutoff));
4148
+ // Ensure the background scanner is running
4149
+ this.startStuckMessageDetector();
4150
+ }
4151
+ /**
4152
+ * Check if the agent appears to be at an input prompt.
4153
+ * Delegates to the regex-free isAgentAtPrompt() from terminal-string-ops,
4154
+ * with additional logging and per-line prompt detection using isPromptLine().
4155
+ *
4156
+ * @param terminalOutput - The terminal output to check
4157
+ * @param runtimeType - The runtime type for pattern selection
4158
+ * @returns true if the agent appears to be at a prompt
4159
+ */
4160
+ isClaudeAtPrompt(terminalOutput, runtimeType) {
4161
+ // Handle null/undefined/empty input — return false since an empty buffer
4162
+ // may indicate a crashed session or one that hasn't started yet
4163
+ if (!terminalOutput || typeof terminalOutput !== 'string') {
4164
+ this.logger.debug('Terminal output is empty or invalid, cannot detect prompt', { runtimeType });
4165
+ return false;
4166
+ }
4167
+ // Only analyze the tail of the buffer to avoid matching historical prompts.
4168
+ // Use 5000 chars to accommodate large tool outputs that push the prompt
4169
+ // further back in the buffer (#106).
4170
+ const tailSection = terminalOutput.slice(-5000);
4171
+ // Check for prompt FIRST. Processing indicators like "thinking" or "analyzing"
4172
+ // can appear in the agent's previous response text and persist in the terminal
4173
+ // scroll buffer, causing false negatives if checked before the prompt.
4174
+ const lines = tailSection.split('\n').filter((line) => line.trim().length > 0);
4175
+ const linesToCheck = lines.slice(-10);
4176
+ const hasPrompt = linesToCheck.some(line => isPromptLine(line, runtimeType));
4177
+ if (hasPrompt) {
4178
+ return true;
4179
+ }
4180
+ // No prompt found — check if still processing.
4181
+ // Check last 10 lines (not just 5) because tool output can push processing
4182
+ // indicators further up while the status bar stays at the bottom.
4183
+ const recentLines = linesToCheck.join('\n');
4184
+ if (containsProcessingIndicator(recentLines)) {
4185
+ this.logger.debug('Processing indicators present near bottom of output');
4186
+ return false;
4187
+ }
4188
+ // Check for "esc to interrupt" in the status bar — this is a definitive
4189
+ // busy signal. Claude Code only shows this text while actively processing.
4190
+ // It disappears when the agent returns to idle at the prompt.
4191
+ if (containsBusyStatusBar(recentLines)) {
4192
+ this.logger.debug('Busy status bar detected (esc to interrupt)');
4193
+ return false;
4194
+ }
4195
+ this.logger.debug('No prompt detected in terminal output', {
4196
+ tailLength: tailSection.length,
4197
+ lastLines: linesToCheck.slice(-3).map(l => l.substring(0, 80)),
4198
+ runtimeType,
4199
+ });
4200
+ return false;
4201
+ }
4202
+ /**
4203
+ * Detect if Gemini CLI is currently in shell mode.
4204
+ *
4205
+ * In shell mode, Gemini CLI changes its prompt from `>` to `!`. Any input
4206
+ * sent in this mode is executed as a shell command instead of being passed
4207
+ * to the model. This method examines the last few lines of terminal output
4208
+ * for shell mode prompt indicators.
4209
+ *
4210
+ * @param terminalOutput - Captured terminal pane content
4211
+ * @returns true if the terminal shows a shell mode prompt
4212
+ */
4213
+ isGeminiInShellMode(terminalOutput) {
4214
+ if (!terminalOutput || typeof terminalOutput !== 'string') {
4215
+ return false;
4216
+ }
4217
+ const lines = terminalOutput.split('\n').filter((line) => line.trim().length > 0);
4218
+ const linesToCheck = lines.slice(-10);
4219
+ return linesToCheck.some((line) => {
4220
+ const trimmed = line.trim();
4221
+ // Strip TUI box-drawing borders
4222
+ const stripped = trimmed.replace(/^[│┃|]+\s*/, '').replace(/\s*[│┃|]+$/, '');
4223
+ // Shell mode prompt: `!` alone or `! ` with text (not `> ` which is normal mode)
4224
+ // Check stripped line — after removing box-drawing, if it starts with `! ` or equals `!`
4225
+ if (stripped === '!' || stripped.startsWith('! ')) {
4226
+ return true;
4227
+ }
4228
+ // Also check pattern-based detection for bordered prompts
4229
+ return GEMINI_SHELL_MODE_CONSTANTS.SHELL_MODE_PROMPT_PATTERNS.some((pattern) => pattern.test(trimmed));
4230
+ });
4231
+ }
4232
+ /**
4233
+ * Escape from Gemini CLI shell mode by sending Escape key.
4234
+ *
4235
+ * Sends Escape and waits for the prompt to change from `!` back to `>`.
4236
+ * Retries up to MAX_ESCAPE_ATTEMPTS times.
4237
+ *
4238
+ * @param sessionName - The session running Gemini CLI
4239
+ * @param sessionHelper - SessionCommandHelper instance
4240
+ * @returns true if successfully escaped shell mode, false if still in shell mode
4241
+ */
4242
+ async escapeGeminiShellMode(sessionName, sessionHelper) {
4243
+ for (let attempt = 1; attempt <= GEMINI_SHELL_MODE_CONSTANTS.MAX_ESCAPE_ATTEMPTS; attempt++) {
4244
+ this.logger.info('Gemini CLI in shell mode, sending Escape to exit', {
4245
+ sessionName,
4246
+ attempt,
4247
+ });
4248
+ await sessionHelper.sendEscape(sessionName);
4249
+ await delay(GEMINI_SHELL_MODE_CONSTANTS.ESCAPE_DELAY_MS);
4250
+ // Check if we're back to normal mode
4251
+ const output = sessionHelper.capturePane(sessionName);
4252
+ if (!this.isGeminiInShellMode(output)) {
4253
+ this.logger.info('Successfully exited Gemini CLI shell mode', {
4254
+ sessionName,
4255
+ attempt,
4256
+ });
4257
+ return true;
4258
+ }
4259
+ }
4260
+ this.logger.warn('Failed to exit Gemini CLI shell mode after max attempts', {
4261
+ sessionName,
4262
+ maxAttempts: GEMINI_SHELL_MODE_CONSTANTS.MAX_ESCAPE_ATTEMPTS,
4263
+ });
4264
+ return false;
4265
+ }
4266
+ /**
4267
+ * Generic key sending to any agent session
4268
+ * @param sessionName The agent session name
4269
+ * @param key The key to send (e.g., 'Enter', 'Ctrl+C')
4270
+ * @returns Promise with success/error information
4271
+ */
4272
+ async sendKeyToAgent(sessionName, key) {
4273
+ try {
4274
+ // Get session helper once to avoid repeated async calls
4275
+ const sessionHelper = await this.getSessionHelper();
4276
+ // Check if session exists
4277
+ const sessionExists = sessionHelper.sessionExists(sessionName);
4278
+ if (!sessionExists) {
4279
+ return {
4280
+ success: false,
4281
+ error: `Session '${sessionName}' does not exist`,
4282
+ };
4283
+ }
4284
+ // Send key using session command helper
4285
+ await sessionHelper.sendKey(sessionName, key);
4286
+ this.logger.info('Key sent to agent successfully', {
4287
+ sessionName,
4288
+ key,
4289
+ });
4290
+ return {
4291
+ success: true,
4292
+ message: `${key} key sent to agent successfully`,
4293
+ };
4294
+ }
4295
+ catch (error) {
4296
+ const errorMessage = error instanceof Error ? error.message : String(error);
4297
+ this.logger.error('Failed to send key to agent', {
4298
+ sessionName,
4299
+ key,
4300
+ error: errorMessage,
4301
+ });
4302
+ return {
4303
+ success: false,
4304
+ error: errorMessage,
4305
+ };
4306
+ }
4307
+ }
4308
+ /**
4309
+ * Generic health check for any agent session
4310
+ * @param sessionName The agent session name
4311
+ * @param role The agent role for additional context
4312
+ * @param timeout Timeout for health check in milliseconds
4313
+ * @returns Promise with health status information
4314
+ */
4315
+ async checkAgentHealth(sessionName, role, timeout = 1000) {
4316
+ try {
4317
+ // Lightweight health check with timeout
4318
+ const agentRunning = await Promise.race([
4319
+ (await this.getSessionHelper()).sessionExists(sessionName),
4320
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Health check timeout')), timeout)),
4321
+ ]).catch(() => false);
4322
+ return {
4323
+ success: true,
4324
+ data: {
4325
+ agent: {
4326
+ sessionName,
4327
+ role,
4328
+ running: agentRunning,
4329
+ status: agentRunning
4330
+ ? CREWLY_CONSTANTS.AGENT_STATUSES.ACTIVE
4331
+ : CREWLY_CONSTANTS.AGENT_STATUSES.INACTIVE,
4332
+ },
4333
+ timestamp: new Date().toISOString(),
4334
+ },
4335
+ };
4336
+ }
4337
+ catch (error) {
4338
+ const errorMessage = error instanceof Error ? error.message : String(error);
4339
+ this.logger.error('Failed to check agent health', {
4340
+ sessionName,
4341
+ role,
4342
+ error: errorMessage,
4343
+ });
4344
+ return {
4345
+ success: false,
4346
+ error: errorMessage,
4347
+ };
4348
+ }
4349
+ }
4350
+ /**
4351
+ * Write the prompt to a file and send a short instruction to the agent to read it.
4352
+ *
4353
+ * Instead of pasting large multi-line prompts directly into the terminal (which
4354
+ * causes bracketed paste issues, shell interpretation errors, and truncation),
4355
+ * we write the prompt to ~/.crewly/prompts/{sessionName}-init.md and send a
4356
+ * single-line instruction telling the agent to read that file.
4357
+ *
4358
+ * @param sessionName The session name
4359
+ * @param prompt The full system prompt to deliver
4360
+ * @param runtimeType The agent runtime type
4361
+ * @param abortSignal Optional signal to cancel the operation (e.g. on runtime exit)
4362
+ * @returns true if the instruction was delivered successfully
4363
+ */
4364
+ async sendPromptRobustly(sessionName, prompt, runtimeType = RUNTIME_TYPES.CLAUDE_CODE, abortSignal) {
4365
+ const isClaudeCode = runtimeType === RUNTIME_TYPES.CLAUDE_CODE;
4366
+ const sessionHelper = await this.getSessionHelper();
4367
+ // Step 1: Write prompt to a file (idempotent — may already exist from pre-launch write).
4368
+ const promptFilePath = await this.writePromptFile(sessionName, prompt);
4369
+ if (!promptFilePath) {
4370
+ // File write failure is fatal — all runtimes use the file-read approach.
4371
+ return false;
4372
+ }
4373
+ // Step 2: Build the message to send.
4374
+ // Claude Code: prompt was already loaded via --agent flag at launch (#207),
4375
+ // so the agent already has all instructions as its agent definition. Just send a short
4376
+ // kickoff trigger — no need to ask it to read the file again.
4377
+ // Gemini CLI / other runtimes: need the file-read instruction since the prompt
4378
+ // was NOT loaded via system prompt.
4379
+ const messageToSend = isClaudeCode
4380
+ ? 'Begin your work now. Follow the step-by-step instructions in your agent definition EXACTLY — start with Step 1, then Step 2, then Step 3 (register-self). Do NOT skip or reorder steps. Registration is required before the system will deliver messages to you.'
4381
+ : `Read the file at ${promptFilePath} and follow all instructions in it.`;
4382
+ // Note: kickoff message is intentionally imperative for reliable agent bootstrapping.
4383
+ // The --agent flag (Claude Code) loads the prompt as trusted system context, so this
4384
+ // short trigger is not subject to PI detection.
4385
+ // Single attempt for all runtimes. Retrying causes duplicate messages because:
4386
+ // - Claude Code: returns to its prompt between tool calls, making retry
4387
+ // checks think the first message wasn't received.
4388
+ // - Gemini CLI: the echoed message "Read the file at..." starts with `> `
4389
+ // which isClaudeAtPrompt misdetects as an idle prompt, and the TUI's
4390
+ // input box shows `> ` even during processing. Both cause false-negative
4391
+ // delivery verification and unnecessary retries.
4392
+ // The sendMessage helper reliably writes to the PTY — if it succeeds,
4393
+ // the message was delivered.
4394
+ const maxAttempts = 1;
4395
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4396
+ // Check abort before each attempt
4397
+ if (abortSignal?.aborted) {
4398
+ this.logger.info('Prompt delivery aborted (runtime exited)', { sessionName, attempt });
4399
+ return false;
4400
+ }
4401
+ try {
4402
+ this.logger.info('Sending prompt to agent', {
4403
+ sessionName,
4404
+ attempt,
4405
+ runtimeType,
4406
+ promptFilePath,
4407
+ deliveryMethod: isClaudeCode ? 'system-prompt-kickoff' : 'file-read',
4408
+ });
4409
+ // Check abort before sending to terminal
4410
+ if (abortSignal?.aborted) {
4411
+ this.logger.info('Prompt delivery aborted before send (runtime exited)', { sessionName });
4412
+ return false;
4413
+ }
4414
+ // Clear any pending input before sending the instruction (first attempt only).
4415
+ if (attempt === 1) {
4416
+ if (isClaudeCode) {
4417
+ // Claude Code: Escape closes slash menus + Ctrl+U clears line.
4418
+ await sessionHelper.sendEscape(sessionName);
4419
+ await delay(200);
4420
+ await sessionHelper.sendKey(sessionName, 'C-u');
4421
+ await delay(300);
4422
+ }
4423
+ else if (runtimeType === RUNTIME_TYPES.CODEX_CLI) {
4424
+ // #246: Codex CLI — Ctrl+U to clear any stale input text.
4425
+ // Unlike Gemini, Codex doesn't need Enter flush; its Ink TUI
4426
+ // handles Ctrl+U for line clear similar to Claude Code.
4427
+ await sessionHelper.sendKey(sessionName, 'C-u');
4428
+ await delay(300);
4429
+ }
4430
+ else {
4431
+ // Gemini CLI (Ink TUI): After /directory add processing, the TUI
4432
+ // input may lose focus or have stale invisible characters.
4433
+ // Send Enter to flush any residual input, then wait for the
4434
+ // prompt to settle before sending the real instruction.
4435
+ this.logger.debug('Gemini CLI pre-send: flushing input with Enter', { sessionName });
4436
+ await sessionHelper.sendEnter(sessionName);
4437
+ await delay(1000);
4438
+ }
4439
+ }
4440
+ // Check abort right before writing instruction to terminal
4441
+ if (abortSignal?.aborted) {
4442
+ this.logger.info('Prompt delivery aborted before instruction send (runtime exited)', { sessionName });
4443
+ return false;
4444
+ }
4445
+ // Send the kickoff / file-read instruction
4446
+ await sessionHelper.sendMessage(sessionName, messageToSend);
4447
+ // Claude Code: rapid-check verification.
4448
+ // Check every 1s for up to 24s (Claude v2 first-token can be slow: MCP/skill load) whether Claude started processing.
4449
+ // If Claude leaves the prompt at any point, the message was received.
4450
+ // This avoids the old length-comparison approach which was unreliable
4451
+ // (Claude's TUI redraws change output length unpredictably).
4452
+ if (isClaudeCode) {
4453
+ for (let i = 0; i < 24; i++) {
4454
+ await delay(1000);
4455
+ if (abortSignal?.aborted)
4456
+ return false;
4457
+ const currentOutput = sessionHelper.capturePane(sessionName);
4458
+ // Processing indicators (spinners) = definitive success
4459
+ if (containsSpinnerOrWorkingIndicator(currentOutput)) {
4460
+ this.logger.debug('Kickoff delivered — processing indicators detected', {
4461
+ sessionName, checkIndex: i,
4462
+ });
4463
+ return true;
4464
+ }
4465
+ // Claude left the prompt = started working on the message
4466
+ if (!this.isClaudeAtPrompt(currentOutput, RUNTIME_TYPES.CLAUDE_CODE)) {
4467
+ this.logger.debug('Kickoff delivered — Claude left prompt', {
4468
+ sessionName, checkIndex: i,
4469
+ });
4470
+ return true;
4471
+ }
4472
+ }
4473
+ // Claude still at prompt after 8s — message likely not received.
4474
+ // For Claude Code with --append-system-prompt-file, don't retry
4475
+ // (would cause duplicate). Log warning and return false.
4476
+ // Still at prompt — the kickoff was likely typed but not submitted (PTY submit
4477
+ // can be dropped). A missing agent costs far more than a duplicate kickoff
4478
+ // (idempotent: it just re-reads the prompt file), so resend ONCE and re-poll.
4479
+ this.logger.warn('Kickoff unconfirmed after 24s — resending once', { sessionName, runtimeType });
4480
+ await sessionHelper.sendMessage(sessionName, messageToSend);
4481
+ for (let j = 0; j < 12; j++) {
4482
+ await delay(1000);
4483
+ if (abortSignal?.aborted)
4484
+ return false;
4485
+ const out2 = sessionHelper.capturePane(sessionName);
4486
+ if (containsSpinnerOrWorkingIndicator(out2) || !this.isClaudeAtPrompt(out2, RUNTIME_TYPES.CLAUDE_CODE)) {
4487
+ this.logger.debug('Kickoff delivered on resend', { sessionName, checkIndex: j });
4488
+ return true;
4489
+ }
4490
+ }
4491
+ this.logger.warn('Kickoff delivery unconfirmed after resend — giving up', { sessionName, runtimeType });
4492
+ return false;
4493
+ }
4494
+ // Gemini CLI / other runtimes: trust the PTY delivery.
4495
+ // sendMessage writes directly to the PTY — if it didn't throw,
4496
+ // the message was delivered. Terminal-based verification is unreliable
4497
+ // because Gemini's TUI always shows `> ` (even during processing)
4498
+ // and our echoed message starts with `> `, both causing false
4499
+ // "at prompt" detection.
4500
+ this.logger.info('Prompt instruction sent to PTY (trusting delivery)', {
4501
+ sessionName, attempt, runtimeType,
4502
+ messageLength: messageToSend.length,
4503
+ });
4504
+ return true;
4505
+ }
4506
+ catch (error) {
4507
+ this.logger.error('Error during prompt instruction delivery', {
4508
+ sessionName,
4509
+ attempt,
4510
+ runtimeType,
4511
+ error: error instanceof Error ? error.message : String(error),
4512
+ });
4513
+ if (attempt === maxAttempts) {
4514
+ return false;
4515
+ }
4516
+ }
4517
+ }
4518
+ this.logger.warn('Prompt instruction delivery unconfirmed after all attempts (agent may still process it)', {
4519
+ sessionName,
4520
+ maxAttempts,
4521
+ runtimeType,
4522
+ });
4523
+ return false;
4524
+ }
4525
+ /**
4526
+ * Capture the current PTY output of a running agent session.
4527
+ *
4528
+ * Used by the queue processor to verify that a force-delivered message
4529
+ * was actually seen by the agent (pending-ack verification).
4530
+ *
4531
+ * @param sessionName - The agent session name
4532
+ * @param lines - Number of lines to capture (default 200)
4533
+ * @returns The captured terminal output, or empty string if session not found
4534
+ */
4535
+ async captureAgentOutput(sessionName, lines = 200) {
4536
+ try {
4537
+ const sessionHelper = await this.getSessionHelper();
4538
+ if (!sessionHelper.sessionExists(sessionName)) {
4539
+ return '';
4540
+ }
4541
+ return sessionHelper.capturePane(sessionName, lines);
4542
+ }
4543
+ catch {
4544
+ return '';
4545
+ }
4546
+ }
4547
+ }
4548
+ //# sourceMappingURL=agent-registration.service.js.map