@ouro.bot/cli 0.1.0-alpha.48 → 0.1.0-alpha.480

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 (358) hide show
  1. package/README.md +132 -19
  2. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +3 -2
  3. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +2 -2
  4. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
  5. package/changelog.json +3061 -0
  6. package/dist/arc/attention-types.js +8 -0
  7. package/dist/arc/cares.js +140 -0
  8. package/dist/arc/episodes.js +117 -0
  9. package/dist/arc/intentions.js +133 -0
  10. package/dist/arc/json-store.js +117 -0
  11. package/dist/arc/obligations.js +237 -0
  12. package/dist/arc/packets.js +193 -0
  13. package/dist/arc/presence.js +185 -0
  14. package/dist/arc/task-lifecycle.js +65 -0
  15. package/dist/heart/active-work.js +857 -0
  16. package/dist/heart/agent-entry.js +58 -3
  17. package/dist/heart/attachments/image-normalize.js +194 -0
  18. package/dist/heart/attachments/materialize.js +97 -0
  19. package/dist/heart/attachments/originals.js +88 -0
  20. package/dist/heart/attachments/render.js +29 -0
  21. package/dist/heart/attachments/sources/adapter.js +2 -0
  22. package/dist/heart/attachments/sources/bluebubbles.js +156 -0
  23. package/dist/heart/attachments/sources/cli-local-file.js +78 -0
  24. package/dist/heart/attachments/sources/index.js +16 -0
  25. package/dist/heart/attachments/store.js +103 -0
  26. package/dist/heart/attachments/types.js +93 -0
  27. package/dist/heart/auth/auth-flow.js +426 -0
  28. package/dist/heart/background-operations.js +234 -0
  29. package/dist/heart/bridges/manager.js +358 -0
  30. package/dist/heart/bridges/state-machine.js +135 -0
  31. package/dist/heart/bridges/store.js +123 -0
  32. package/dist/heart/bundle-state.js +168 -0
  33. package/dist/heart/commitments.js +111 -0
  34. package/dist/heart/config-registry.js +304 -0
  35. package/dist/heart/config.js +110 -128
  36. package/dist/heart/core.js +745 -227
  37. package/dist/heart/cross-chat-delivery.js +131 -0
  38. package/dist/heart/daemon/agent-config-check.js +490 -0
  39. package/dist/heart/daemon/agent-discovery.js +79 -3
  40. package/dist/heart/daemon/agent-service.js +360 -0
  41. package/dist/heart/daemon/agentic-repair.js +216 -0
  42. package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
  43. package/dist/heart/daemon/cadence.js +70 -0
  44. package/dist/heart/daemon/cli-defaults.js +640 -0
  45. package/dist/heart/daemon/cli-exec.js +6933 -0
  46. package/dist/heart/daemon/cli-help.js +487 -0
  47. package/dist/heart/daemon/cli-parse.js +1527 -0
  48. package/dist/heart/daemon/cli-render-doctor.js +57 -0
  49. package/dist/heart/daemon/cli-render.js +561 -0
  50. package/dist/heart/daemon/cli-types.js +8 -0
  51. package/dist/heart/daemon/connect-bay.js +323 -0
  52. package/dist/heart/daemon/daemon-cli.js +29 -1616
  53. package/dist/heart/daemon/daemon-entry.js +345 -3
  54. package/dist/heart/daemon/daemon-health.js +141 -0
  55. package/dist/heart/daemon/daemon-runtime-sync.js +190 -12
  56. package/dist/heart/daemon/daemon-tombstone.js +236 -0
  57. package/dist/heart/daemon/daemon.js +677 -58
  58. package/dist/heart/daemon/dns-workflow.js +394 -0
  59. package/dist/heart/daemon/doctor-types.js +8 -0
  60. package/dist/heart/daemon/doctor.js +486 -0
  61. package/dist/heart/daemon/health-monitor.js +92 -1
  62. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  63. package/dist/heart/daemon/hooks/bundle-meta.js +115 -1
  64. package/dist/heart/daemon/http-health-probe.js +80 -0
  65. package/dist/heart/daemon/human-command-screens.js +234 -0
  66. package/dist/heart/daemon/human-readiness.js +114 -0
  67. package/dist/heart/daemon/inner-status.js +89 -0
  68. package/dist/heart/daemon/interactive-repair.js +394 -0
  69. package/dist/heart/daemon/launchd.js +25 -5
  70. package/dist/heart/daemon/log-tailer.js +82 -12
  71. package/dist/heart/daemon/logs-prune.js +110 -0
  72. package/dist/heart/daemon/message-router.js +2 -2
  73. package/dist/heart/daemon/os-cron-deps.js +134 -0
  74. package/dist/heart/daemon/ouro-bot-entry.js +4 -2
  75. package/dist/heart/daemon/ouro-entry.js +3 -1
  76. package/dist/heart/daemon/process-manager.js +214 -0
  77. package/dist/heart/daemon/provider-discovery.js +137 -0
  78. package/dist/heart/daemon/provider-ping-progress.js +83 -0
  79. package/dist/heart/daemon/pulse.js +475 -0
  80. package/dist/heart/daemon/readiness-repair.js +365 -0
  81. package/dist/heart/daemon/run-hooks.js +2 -0
  82. package/dist/heart/daemon/runtime-logging.js +67 -16
  83. package/dist/heart/daemon/runtime-metadata.js +73 -0
  84. package/dist/heart/daemon/runtime-mode.js +67 -0
  85. package/dist/heart/daemon/safe-mode.js +161 -0
  86. package/dist/heart/daemon/sense-manager.js +178 -37
  87. package/dist/heart/daemon/session-id-resolver.js +131 -0
  88. package/dist/heart/daemon/skill-management-installer.js +94 -0
  89. package/dist/heart/daemon/socket-client.js +109 -4
  90. package/dist/heart/daemon/stale-bundle-prune.js +96 -0
  91. package/dist/heart/daemon/startup-tui.js +264 -0
  92. package/dist/heart/daemon/task-scheduler.js +3 -25
  93. package/dist/heart/daemon/terminal-ui.js +499 -0
  94. package/dist/heart/daemon/thoughts.js +149 -10
  95. package/dist/heart/daemon/up-progress.js +366 -0
  96. package/dist/heart/daemon/vault-items.js +56 -0
  97. package/dist/heart/delegation.js +62 -0
  98. package/dist/heart/habits/habit-migration.js +189 -0
  99. package/dist/heart/habits/habit-parser.js +140 -0
  100. package/dist/heart/habits/habit-runtime-state.js +100 -0
  101. package/dist/heart/habits/habit-scheduler.js +372 -0
  102. package/dist/heart/{daemon → hatch}/hatch-flow.js +52 -117
  103. package/dist/heart/{daemon → hatch}/hatch-specialist.js +3 -3
  104. package/dist/heart/{daemon → hatch}/specialist-prompt.js +12 -9
  105. package/dist/heart/{daemon → hatch}/specialist-tools.js +35 -12
  106. package/dist/heart/identity.js +201 -66
  107. package/dist/heart/kept-notes.js +357 -0
  108. package/dist/heart/kicks.js +1 -1
  109. package/dist/heart/machine-identity.js +161 -0
  110. package/dist/heart/mcp/mcp-server.js +653 -0
  111. package/dist/heart/migrate-config.js +100 -0
  112. package/dist/heart/model-capabilities.js +59 -0
  113. package/dist/heart/outlook/outlook-http-hooks.js +66 -0
  114. package/dist/heart/outlook/outlook-http-response.js +7 -0
  115. package/dist/heart/outlook/outlook-http-routes.js +244 -0
  116. package/dist/heart/outlook/outlook-http-static.js +99 -0
  117. package/dist/heart/outlook/outlook-http-transport.js +116 -0
  118. package/dist/heart/outlook/outlook-http.js +99 -0
  119. package/dist/heart/outlook/outlook-read.js +31 -0
  120. package/dist/heart/outlook/outlook-types.js +27 -0
  121. package/dist/heart/outlook/outlook-view.js +195 -0
  122. package/dist/heart/outlook/readers/agent-machine.js +359 -0
  123. package/dist/heart/outlook/readers/continuity-readers.js +332 -0
  124. package/dist/heart/outlook/readers/mail.js +362 -0
  125. package/dist/heart/outlook/readers/runtime-readers.js +644 -0
  126. package/dist/heart/outlook/readers/sessions.js +232 -0
  127. package/dist/heart/outlook/readers/shared.js +111 -0
  128. package/dist/heart/platform.js +81 -0
  129. package/dist/heart/progress-story.js +42 -0
  130. package/dist/heart/provider-attempt.js +134 -0
  131. package/dist/heart/provider-binding-resolver.js +255 -0
  132. package/dist/heart/provider-credentials.js +424 -0
  133. package/dist/heart/provider-failover.js +266 -0
  134. package/dist/heart/provider-models.js +81 -0
  135. package/dist/heart/provider-ping.js +262 -0
  136. package/dist/heart/provider-state.js +216 -0
  137. package/dist/heart/provider-visibility.js +188 -0
  138. package/dist/heart/providers/anthropic-token.js +131 -0
  139. package/dist/heart/providers/anthropic.js +193 -55
  140. package/dist/heart/providers/azure.js +103 -12
  141. package/dist/heart/providers/error-classification.js +63 -0
  142. package/dist/heart/providers/github-copilot.js +145 -0
  143. package/dist/heart/providers/minimax-vlm.js +189 -0
  144. package/dist/heart/providers/minimax.js +29 -7
  145. package/dist/heart/providers/openai-codex.js +62 -38
  146. package/dist/heart/runtime-capability-check.js +170 -0
  147. package/dist/heart/runtime-credentials.js +260 -0
  148. package/dist/heart/sense-truth.js +11 -4
  149. package/dist/heart/session-activity.js +190 -0
  150. package/dist/heart/session-events.js +855 -0
  151. package/dist/heart/session-transcript.js +167 -0
  152. package/dist/heart/start-of-turn-packet.js +345 -0
  153. package/dist/heart/streaming.js +36 -27
  154. package/dist/heart/sync.js +332 -0
  155. package/dist/heart/target-resolution.js +127 -0
  156. package/dist/heart/tempo.js +93 -0
  157. package/dist/heart/temporal-view.js +41 -0
  158. package/dist/heart/tool-activity-callbacks.js +36 -0
  159. package/dist/heart/tool-description.js +135 -0
  160. package/dist/heart/tool-friction.js +55 -0
  161. package/dist/heart/tool-loop.js +200 -0
  162. package/dist/heart/turn-context.js +361 -0
  163. package/dist/heart/turn-coordinator.js +24 -1
  164. package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +1 -1
  165. package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
  166. package/dist/heart/versioning/ouro-path-installer.js +425 -0
  167. package/dist/heart/versioning/ouro-version-manager.js +295 -0
  168. package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
  169. package/dist/heart/{daemon → versioning}/update-checker.js +5 -1
  170. package/dist/heart/{daemon → versioning}/update-hooks.js +63 -59
  171. package/dist/mailroom/attention.js +167 -0
  172. package/dist/mailroom/autonomy.js +209 -0
  173. package/dist/mailroom/blob-store.js +558 -0
  174. package/dist/mailroom/core.js +658 -0
  175. package/dist/mailroom/entry.js +160 -0
  176. package/dist/mailroom/file-store.js +400 -0
  177. package/dist/mailroom/mbox-import.js +341 -0
  178. package/dist/mailroom/outbound.js +380 -0
  179. package/dist/mailroom/policy.js +263 -0
  180. package/dist/mailroom/reader.js +197 -0
  181. package/dist/mailroom/smtp-ingress.js +176 -0
  182. package/dist/mailroom/source-state.js +176 -0
  183. package/dist/mailroom/travel-extract.js +89 -0
  184. package/dist/mind/bundle-manifest.js +7 -1
  185. package/dist/mind/context.js +132 -93
  186. package/dist/mind/diary-integrity.js +60 -0
  187. package/dist/mind/{memory.js → diary.js} +74 -93
  188. package/dist/mind/embedding-provider.js +60 -0
  189. package/dist/mind/file-state.js +179 -0
  190. package/dist/mind/friends/channel.js +30 -0
  191. package/dist/mind/friends/group-context.js +144 -0
  192. package/dist/mind/friends/resolver.js +38 -1
  193. package/dist/mind/friends/store-file.js +39 -3
  194. package/dist/mind/friends/trust-explanation.js +74 -0
  195. package/dist/mind/friends/types.js +2 -2
  196. package/dist/mind/journal-index.js +161 -0
  197. package/dist/mind/note-search.js +268 -0
  198. package/dist/mind/obligation-steering.js +221 -0
  199. package/dist/mind/pending.js +66 -7
  200. package/dist/mind/prompt-refresh.js +3 -2
  201. package/dist/mind/prompt.js +978 -169
  202. package/dist/mind/provenance-trust.js +26 -0
  203. package/dist/mind/scrutiny.js +173 -0
  204. package/dist/nerves/cli-logging.js +7 -1
  205. package/dist/nerves/coverage/audit-rules.js +15 -6
  206. package/dist/nerves/coverage/audit.js +28 -2
  207. package/dist/nerves/coverage/cli.js +1 -1
  208. package/dist/nerves/coverage/contract.js +5 -5
  209. package/dist/nerves/coverage/file-completeness.js +84 -5
  210. package/dist/nerves/coverage/run-artifacts.js +1 -1
  211. package/dist/nerves/event-buffer.js +111 -0
  212. package/dist/nerves/index.js +224 -4
  213. package/dist/nerves/observation.js +20 -0
  214. package/dist/nerves/redact.js +79 -0
  215. package/dist/nerves/runtime.js +5 -1
  216. package/dist/outlook-ui/assets/index-BPr5vNuM.css +1 -0
  217. package/dist/outlook-ui/assets/index-CPfhbn13.js +61 -0
  218. package/dist/outlook-ui/index.html +15 -0
  219. package/dist/repertoire/ado-client.js +15 -56
  220. package/dist/repertoire/ado-semantic.js +11 -10
  221. package/dist/repertoire/api-client.js +97 -0
  222. package/dist/repertoire/bitwarden-store.js +774 -0
  223. package/dist/repertoire/bundle-templates.js +72 -0
  224. package/dist/repertoire/bw-installer.js +180 -0
  225. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  226. package/dist/repertoire/coding/context-pack.js +330 -0
  227. package/dist/repertoire/coding/feedback.js +197 -30
  228. package/dist/repertoire/coding/manager.js +158 -9
  229. package/dist/repertoire/coding/spawner.js +55 -9
  230. package/dist/repertoire/coding/tools.js +170 -7
  231. package/dist/repertoire/commerce-errors.js +109 -0
  232. package/dist/repertoire/commerce-self-test.js +156 -0
  233. package/dist/repertoire/credential-access.js +111 -0
  234. package/dist/repertoire/duffel-client.js +185 -0
  235. package/dist/repertoire/github-client.js +14 -55
  236. package/dist/repertoire/graph-client.js +11 -52
  237. package/dist/repertoire/guardrails.js +396 -0
  238. package/dist/repertoire/mcp-client.js +255 -0
  239. package/dist/repertoire/mcp-manager.js +305 -0
  240. package/dist/repertoire/mcp-tools.js +63 -0
  241. package/dist/repertoire/shell-sessions.js +133 -0
  242. package/dist/repertoire/skills.js +15 -24
  243. package/dist/repertoire/stripe-client.js +131 -0
  244. package/dist/repertoire/tasks/board.js +43 -5
  245. package/dist/repertoire/tasks/fix.js +182 -0
  246. package/dist/repertoire/tasks/index.js +37 -4
  247. package/dist/repertoire/tasks/lifecycle.js +2 -2
  248. package/dist/repertoire/tasks/parser.js +3 -2
  249. package/dist/repertoire/tasks/scanner.js +194 -37
  250. package/dist/repertoire/tasks/transitions.js +16 -78
  251. package/dist/repertoire/tool-results.js +29 -0
  252. package/dist/repertoire/tools-attachments.js +317 -0
  253. package/dist/repertoire/tools-base.js +44 -740
  254. package/dist/repertoire/tools-bluebubbles.js +1 -0
  255. package/dist/repertoire/tools-bridge.js +141 -0
  256. package/dist/repertoire/tools-bundle.js +984 -0
  257. package/dist/repertoire/tools-config.js +185 -0
  258. package/dist/repertoire/tools-continuity.js +248 -0
  259. package/dist/repertoire/tools-credential.js +381 -0
  260. package/dist/repertoire/tools-files.js +342 -0
  261. package/dist/repertoire/tools-flight.js +224 -0
  262. package/dist/repertoire/tools-flow.js +105 -0
  263. package/dist/repertoire/tools-github.js +1 -7
  264. package/dist/repertoire/tools-mail.js +896 -0
  265. package/dist/repertoire/tools-notes.js +376 -0
  266. package/dist/repertoire/tools-session.js +746 -0
  267. package/dist/repertoire/tools-shell.js +120 -0
  268. package/dist/repertoire/tools-stripe.js +180 -0
  269. package/dist/repertoire/tools-surface.js +243 -0
  270. package/dist/repertoire/tools-teams.js +9 -39
  271. package/dist/repertoire/tools-travel.js +125 -0
  272. package/dist/repertoire/tools-user-profile.js +144 -0
  273. package/dist/repertoire/tools-vault.js +40 -0
  274. package/dist/repertoire/tools.js +144 -113
  275. package/dist/repertoire/travel-api-client.js +360 -0
  276. package/dist/repertoire/user-profile.js +131 -0
  277. package/dist/repertoire/vault-setup.js +246 -0
  278. package/dist/repertoire/vault-unlock.js +561 -0
  279. package/dist/scripts/claude-code-hook.js +41 -0
  280. package/dist/scripts/claude-code-stop-hook.js +47 -0
  281. package/dist/senses/attention-queue.js +116 -0
  282. package/dist/senses/bluebubbles/attachment-cache.js +53 -0
  283. package/dist/senses/bluebubbles/attachment-download.js +137 -0
  284. package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +219 -18
  285. package/dist/senses/bluebubbles/entry.js +73 -0
  286. package/dist/senses/{bluebubbles-inbound-log.js → bluebubbles/inbound-log.js} +7 -3
  287. package/dist/senses/{bluebubbles.js → bluebubbles/index.js} +705 -116
  288. package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -70
  289. package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +33 -12
  290. package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +3 -3
  291. package/dist/senses/bluebubbles/replay.js +129 -0
  292. package/dist/senses/{bluebubbles-runtime-state.js → bluebubbles/runtime-state.js} +2 -2
  293. package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
  294. package/dist/senses/cli/bracketed-paste.js +82 -0
  295. package/dist/senses/cli/image-paste.js +287 -0
  296. package/dist/senses/cli/image-ref-navigation.js +75 -0
  297. package/dist/senses/cli/ink-app.js +156 -0
  298. package/dist/senses/cli/inline-diff.js +64 -0
  299. package/dist/senses/cli/input-keys.js +174 -0
  300. package/dist/senses/cli/kill-ring.js +86 -0
  301. package/dist/senses/cli/message-list.js +51 -0
  302. package/dist/senses/cli/ouro-tui.js +605 -0
  303. package/dist/senses/cli/spinner-imperative.js +135 -0
  304. package/dist/senses/cli/spinner.js +101 -0
  305. package/dist/senses/cli/status-line.js +60 -0
  306. package/dist/senses/cli/streaming-markdown.js +526 -0
  307. package/dist/senses/cli/tool-display.js +83 -0
  308. package/dist/senses/cli/tool-render.js +85 -0
  309. package/dist/senses/cli/tui-store.js +240 -0
  310. package/dist/senses/cli/virtual-list.js +35 -0
  311. package/dist/senses/cli-entry.js +60 -8
  312. package/dist/senses/cli-layout.js +187 -0
  313. package/dist/senses/cli.js +516 -211
  314. package/dist/senses/commands.js +66 -3
  315. package/dist/senses/habit-turn-message.js +108 -0
  316. package/dist/senses/inner-dialog-worker.js +97 -17
  317. package/dist/senses/inner-dialog.js +404 -14
  318. package/dist/senses/mail-entry.js +66 -0
  319. package/dist/senses/mail.js +232 -0
  320. package/dist/senses/pipeline.js +533 -72
  321. package/dist/senses/proactive-content-guard.js +51 -0
  322. package/dist/senses/shared-turn.js +205 -0
  323. package/dist/senses/surface-tool.js +68 -0
  324. package/dist/senses/teams-entry.js +60 -8
  325. package/dist/senses/teams.js +413 -163
  326. package/dist/senses/trust-gate.js +5 -5
  327. package/package.json +37 -7
  328. package/skills/agent-commerce.md +106 -0
  329. package/skills/browser-navigation.md +117 -0
  330. package/skills/commerce-setup-guide.md +116 -0
  331. package/skills/commerce-setup.md +84 -0
  332. package/skills/configure-dev-tools.md +101 -0
  333. package/skills/travel-planning.md +138 -0
  334. package/dist/heart/daemon/ouro-path-installer.js +0 -178
  335. package/dist/heart/daemon/subagent-installer.js +0 -166
  336. package/dist/mind/associative-recall.js +0 -209
  337. package/dist/senses/bluebubbles-entry.js +0 -13
  338. package/dist/senses/debug-activity.js +0 -127
  339. package/subagents/README.md +0 -86
  340. package/subagents/work-doer.md +0 -237
  341. package/subagents/work-merger.md +0 -618
  342. package/subagents/work-planner.md +0 -390
  343. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
  344. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
  345. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
  346. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
  347. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
  348. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
  349. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
  350. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
  351. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
  352. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
  353. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
  354. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
  355. /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
  356. /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
  357. /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
  358. /package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +0 -0
@@ -33,9 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.MarkdownStreamer = exports.InputController = exports.Spinner = void 0;
36
+ exports.InputQueue = exports.MarkdownStreamer = exports.InputController = exports.Spinner = exports.StreamingWordWrapper = exports.wrapCliText = exports.formatEchoedInputSummary = void 0;
37
37
  exports.formatPendingPrefix = formatPendingPrefix;
38
38
  exports.getCliContinuityIngressTexts = getCliContinuityIngressTexts;
39
+ exports.formatTimeAgo = formatTimeAgo;
40
+ exports.writeCliAsyncAssistantMessage = writeCliAsyncAssistantMessage;
41
+ exports.pauseActiveSpinner = pauseActiveSpinner;
42
+ exports.resumeActiveSpinner = resumeActiveSpinner;
43
+ exports.setActiveSpinner = setActiveSpinner;
39
44
  exports.handleSigint = handleSigint;
40
45
  exports.addHistory = addHistory;
41
46
  exports.renderMarkdown = renderMarkdown;
@@ -51,10 +56,12 @@ const prompt_1 = require("../mind/prompt");
51
56
  const phrases_1 = require("../mind/phrases");
52
57
  const format_1 = require("../mind/format");
53
58
  const config_1 = require("../heart/config");
59
+ const session_events_1 = require("../heart/session-events");
54
60
  const context_1 = require("../mind/context");
55
61
  const pending_1 = require("../mind/pending");
56
62
  const commands_1 = require("./commands");
57
63
  const identity_1 = require("../heart/identity");
64
+ const mcp_manager_1 = require("../repertoire/mcp-manager");
58
65
  const nerves_1 = require("../nerves");
59
66
  const store_file_1 = require("../mind/friends/store-file");
60
67
  const resolver_1 = require("../mind/friends/resolver");
@@ -65,9 +72,18 @@ const trust_gate_1 = require("./trust-gate");
65
72
  const pipeline_1 = require("./pipeline");
66
73
  const channel_1 = require("../mind/friends/channel");
67
74
  const session_lock_1 = require("./session-lock");
68
- const update_hooks_1 = require("../heart/daemon/update-hooks");
75
+ const update_hooks_1 = require("../heart/versioning/update-hooks");
69
76
  const bundle_meta_1 = require("../heart/daemon/hooks/bundle-meta");
77
+ const agent_config_v2_1 = require("../heart/daemon/hooks/agent-config-v2");
70
78
  const bundle_manifest_1 = require("../mind/bundle-manifest");
79
+ const cli_layout_1 = require("./cli-layout");
80
+ const image_paste_1 = require("./cli/image-paste");
81
+ const spinner_imperative_1 = require("./cli/spinner-imperative");
82
+ const tool_display_1 = require("./cli/tool-display");
83
+ var cli_layout_2 = require("./cli-layout");
84
+ Object.defineProperty(exports, "formatEchoedInputSummary", { enumerable: true, get: function () { return cli_layout_2.formatEchoedInputSummary; } });
85
+ Object.defineProperty(exports, "wrapCliText", { enumerable: true, get: function () { return cli_layout_2.wrapCliText; } });
86
+ Object.defineProperty(exports, "StreamingWordWrapper", { enumerable: true, get: function () { return cli_layout_2.StreamingWordWrapper; } });
71
87
  /**
72
88
  * Format pending messages as content-prefix strings for injection into
73
89
  * the next user message. Self-messages (from === agentName) become
@@ -85,68 +101,46 @@ function getCliContinuityIngressTexts(input) {
85
101
  const trimmed = input.trim();
86
102
  return trimmed ? [trimmed] : [];
87
103
  }
88
- // spinner that only touches stderr, cleans up after itself
89
- // exported for direct testability (stop-without-start branch)
90
- class Spinner {
91
- frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
92
- i = 0;
93
- iv = null;
94
- piv = null;
95
- msg = "";
96
- phrases = null;
97
- lastPhrase = "";
98
- stopped = false;
99
- constructor(m = "working", phrases) {
100
- this.msg = m;
101
- if (phrases && phrases.length > 0)
102
- this.phrases = phrases;
103
- }
104
- start() {
105
- this.stopped = false;
106
- process.stderr.write("\r\x1b[K");
107
- this.spin();
108
- this.iv = setInterval(() => this.spin(), 80);
109
- if (this.phrases) {
110
- this.piv = setInterval(() => this.rotatePhrase(), 1500);
111
- }
112
- }
113
- spin() {
114
- // Guard: clearInterval can't prevent already-dequeued callbacks
115
- /* v8 ignore next -- race guard: timer callback fires after stop() @preserve */
116
- if (this.stopped)
117
- return;
118
- process.stderr.write(`\r\x1b[K${this.frames[this.i]} ${this.msg}... `);
119
- this.i = (this.i + 1) % this.frames.length;
120
- }
121
- rotatePhrase() {
122
- /* v8 ignore next -- race guard: timer callback fires after stop() @preserve */
123
- if (this.stopped)
124
- return;
125
- const next = (0, phrases_1.pickPhrase)(this.phrases, this.lastPhrase);
126
- this.lastPhrase = next;
127
- this.msg = next;
128
- }
129
- stop(ok) {
130
- this.stopped = true;
131
- if (this.iv) {
132
- clearInterval(this.iv);
133
- this.iv = null;
134
- }
135
- if (this.piv) {
136
- clearInterval(this.piv);
137
- this.piv = null;
138
- }
139
- process.stderr.write("\r\x1b[K");
140
- /* v8 ignore next -- ok parameter currently unused by callers @preserve */
141
- if (ok)
142
- process.stderr.write(`\x1b[32m\u2713\x1b[0m ${ok}\n`);
143
- }
144
- fail(msg) {
145
- this.stop();
146
- process.stderr.write(`\x1b[31m\u2717\x1b[0m ${msg}\n`);
104
+ /* v8 ignore start -- cosmetic time formatting @preserve */
105
+ function formatTimeAgo(date) {
106
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
107
+ if (seconds < 60)
108
+ return "just now";
109
+ const minutes = Math.floor(seconds / 60);
110
+ if (minutes < 60)
111
+ return `${minutes}m ago`;
112
+ const hours = Math.floor(minutes / 60);
113
+ if (hours < 24)
114
+ return `${hours}h ago`;
115
+ const days = Math.floor(hours / 24);
116
+ return `${days}d ago`;
117
+ }
118
+ const CLI_PROMPT = "\x1b[36m) \x1b[0m";
119
+ function writeCliAsyncAssistantMessage(rl, message, stdout = process.stdout) {
120
+ const rlInt = rl;
121
+ const currentLine = rlInt.line ?? "";
122
+ const currentCursor = rlInt.cursor ?? currentLine.length;
123
+ stdout.write("\r\x1b[K");
124
+ stdout.write(`${renderMarkdown(message)}\n`);
125
+ stdout.write(CLI_PROMPT);
126
+ if (!currentLine)
127
+ return;
128
+ stdout.write(currentLine);
129
+ if (currentCursor < currentLine.length) {
130
+ readline.cursorTo(process.stdout, 2 + currentCursor);
147
131
  }
148
132
  }
149
- exports.Spinner = Spinner;
133
+ // Module-level active spinner for log coordination.
134
+ // The terminal log sink calls these to avoid interleaving with spinner output.
135
+ let _activeSpinner = null;
136
+ /* v8 ignore start -- spinner coordination: exercised at runtime, not unit-testable without real terminal @preserve */
137
+ function pauseActiveSpinner() { _activeSpinner?.pause(); }
138
+ function resumeActiveSpinner() { _activeSpinner?.resume(); }
139
+ /* v8 ignore stop */
140
+ function setActiveSpinner(s) { _activeSpinner = s; }
141
+ // Re-export ImperativeSpinner as Spinner for backward compatibility
142
+ var spinner_imperative_2 = require("./cli/spinner-imperative");
143
+ Object.defineProperty(exports, "Spinner", { enumerable: true, get: function () { return spinner_imperative_2.ImperativeSpinner; } });
150
144
  // Input controller: pauses readline during model/tool execution.
151
145
  // Does NOT touch raw mode — readline with terminal:true manages raw mode
152
146
  // internally. Touching it causes ^C to be echoed by the terminal driver.
@@ -314,59 +308,55 @@ function createCliCallbacks() {
314
308
  meta: {},
315
309
  });
316
310
  let currentSpinner = null;
317
- let hadReasoning = false;
311
+ function setSpinner(s) { currentSpinner = s; setActiveSpinner(s); }
318
312
  let hadToolRun = false;
319
313
  let textDirty = false; // true when text/reasoning was written without a trailing newline
320
314
  const streamer = new MarkdownStreamer();
315
+ const wrapper = new cli_layout_1.StreamingWordWrapper();
321
316
  return {
322
317
  onModelStart: () => {
323
318
  currentSpinner?.stop();
324
- currentSpinner = null;
325
- hadReasoning = false;
319
+ setSpinner(null);
326
320
  textDirty = false;
327
321
  streamer.reset();
322
+ wrapper.reset();
328
323
  const phrases = (0, phrases_1.getPhrases)();
329
324
  const pool = hadToolRun ? phrases.followup : phrases.thinking;
330
325
  const first = (0, phrases_1.pickPhrase)(pool);
331
- currentSpinner = new Spinner(first, pool);
326
+ setSpinner(new spinner_imperative_1.ImperativeSpinner(first, pool));
332
327
  currentSpinner.start();
333
328
  },
334
329
  onModelStreamStart: () => {
335
330
  // No-op: content callbacks (onTextChunk, onReasoningChunk) handle
336
331
  // stopping the spinner. onModelStreamStart fires too early and
337
- // doesn't fire at all for final_answer tool streaming.
332
+ // doesn't fire at all for settle tool streaming.
338
333
  },
339
334
  onClearText: () => {
340
335
  streamer.reset();
336
+ wrapper.reset();
341
337
  },
342
338
  onTextChunk: (text) => {
343
- // Stop spinner if still running — final_answer streaming and Anthropic
339
+ // Stop spinner if still running — settle streaming and Anthropic
344
340
  // tool-only responses bypass onModelStreamStart, so the spinner would
345
341
  // otherwise keep running (and its \r writes overwrite response text).
346
342
  if (currentSpinner) {
347
343
  currentSpinner.stop();
348
- currentSpinner = null;
349
- }
350
- if (hadReasoning) {
351
- // Single newline to separate reasoning from reply — reasoning
352
- // output often ends with its own trailing newline(s)
353
- process.stdout.write("\n");
354
- hadReasoning = false;
344
+ setSpinner(null);
355
345
  }
356
346
  const rendered = streamer.push(text);
357
- if (rendered)
358
- process.stdout.write(rendered);
359
- textDirty = text.length > 0 && !text.endsWith("\n");
360
- },
361
- onReasoningChunk: (text) => {
362
- if (currentSpinner) {
363
- currentSpinner.stop();
364
- currentSpinner = null;
347
+ /* v8 ignore start -- wrapper integration: tested via cli.test.ts onTextChunk tests @preserve */
348
+ if (rendered) {
349
+ const wrapped = wrapper.push(rendered);
350
+ if (wrapped)
351
+ process.stdout.write(wrapped);
365
352
  }
366
- hadReasoning = true;
367
- process.stdout.write(`\x1b[2m${text}\x1b[0m`);
353
+ /* v8 ignore stop */
368
354
  textDirty = text.length > 0 && !text.endsWith("\n");
369
355
  },
356
+ onReasoningChunk: (_text) => {
357
+ // Keep reasoning private in the CLI surface. The spinner continues to
358
+ // represent active thinking until actual tool or answer output arrives.
359
+ },
370
360
  onToolStart: (_name, _args) => {
371
361
  // Stop the model-start spinner: when the model returns only tool calls
372
362
  // (no content/reasoning), onModelStreamStart never fires, so the old
@@ -380,31 +370,29 @@ function createCliCallbacks() {
380
370
  }
381
371
  const toolPhrases = (0, phrases_1.getPhrases)().tool;
382
372
  const first = (0, phrases_1.pickPhrase)(toolPhrases);
383
- currentSpinner = new Spinner(first, toolPhrases);
373
+ setSpinner(new spinner_imperative_1.ImperativeSpinner(first, toolPhrases));
384
374
  currentSpinner.start();
385
375
  hadToolRun = true;
386
376
  },
387
377
  onToolEnd: (name, argSummary, success) => {
388
378
  currentSpinner?.stop();
389
- currentSpinner = null;
390
- const msg = (0, format_1.formatToolResult)(name, argSummary, success);
391
- const color = success ? "\x1b[32m" : "\x1b[31m";
392
- process.stderr.write(`${color}${msg}\x1b[0m\n`);
379
+ setSpinner(null);
380
+ (0, tool_display_1.writeToolEnd)(name, argSummary, success);
393
381
  },
394
382
  onError: (error, severity) => {
395
383
  if (severity === "transient") {
396
384
  currentSpinner?.fail(error.message);
397
- currentSpinner = null;
385
+ setSpinner(null);
398
386
  }
399
387
  else {
400
388
  currentSpinner?.stop();
401
- currentSpinner = null;
389
+ setSpinner(null);
402
390
  process.stderr.write(`\x1b[31m${(0, format_1.formatError)(error)}\x1b[0m\n`);
403
391
  }
404
392
  },
405
393
  onKick: () => {
406
394
  currentSpinner?.stop();
407
- currentSpinner = null;
395
+ setSpinner(null);
408
396
  if (textDirty) {
409
397
  process.stdout.write("\n");
410
398
  textDirty = false;
@@ -413,10 +401,18 @@ function createCliCallbacks() {
413
401
  },
414
402
  flushMarkdown: () => {
415
403
  currentSpinner?.stop();
416
- currentSpinner = null;
404
+ setSpinner(null);
405
+ /* v8 ignore start -- wrapper flush: tested via cli.test.ts flushMarkdown tests @preserve */
417
406
  const remaining = streamer.flush();
418
- if (remaining)
419
- process.stdout.write(remaining);
407
+ if (remaining) {
408
+ const wrapped = wrapper.push(remaining);
409
+ if (wrapped)
410
+ process.stdout.write(wrapped);
411
+ }
412
+ const tail = wrapper.flush();
413
+ if (tail)
414
+ process.stdout.write(tail);
415
+ /* v8 ignore stop */
420
416
  },
421
417
  };
422
418
  }
@@ -457,29 +453,243 @@ async function* createDebouncedLines(source, debounceMs) {
457
453
  yield lines.join("\n");
458
454
  }
459
455
  }
456
+ /**
457
+ * Async queue that bridges push-based Ink input to pull-based async iteration.
458
+ * Input from Ink's onSubmit callback is pushed; the business logic loop awaits via for-await.
459
+ */
460
+ class InputQueue {
461
+ queue = [];
462
+ resolve = null;
463
+ done = false;
464
+ push(input) {
465
+ if (this.done)
466
+ return;
467
+ if (this.resolve) {
468
+ const r = this.resolve;
469
+ this.resolve = null;
470
+ r({ value: input, done: false });
471
+ }
472
+ else {
473
+ this.queue.push(input);
474
+ }
475
+ }
476
+ close() {
477
+ this.done = true;
478
+ if (this.resolve) {
479
+ const r = this.resolve;
480
+ this.resolve = null;
481
+ r({ value: undefined, done: true });
482
+ }
483
+ }
484
+ /** Drain all buffered items, leaving any pending async awaiter untouched. */
485
+ drainAll() {
486
+ const items = [...this.queue];
487
+ this.queue = [];
488
+ return items;
489
+ }
490
+ [Symbol.asyncIterator]() {
491
+ return {
492
+ next: () => {
493
+ if (this.queue.length > 0) {
494
+ return Promise.resolve({ value: this.queue.shift(), done: false });
495
+ }
496
+ if (this.done) {
497
+ return Promise.resolve({ value: undefined, done: true });
498
+ }
499
+ return new Promise((resolve) => {
500
+ this.resolve = resolve;
501
+ });
502
+ },
503
+ };
504
+ }
505
+ }
506
+ exports.InputQueue = InputQueue;
460
507
  async function runCliSession(options) {
461
508
  /* v8 ignore start -- integration: runCliSession is interactive, tested via E2E @preserve */
462
- const pasteDebounceMs = options.pasteDebounceMs ?? 50;
463
509
  const registry = (0, commands_1.createCommandRegistry)();
464
510
  if (!options.disableCommands) {
465
511
  (0, commands_1.registerDefaultCommands)(registry);
466
512
  }
467
513
  const messages = options.messages
468
- ?? [{ role: "system", content: await (0, prompt_1.buildSystem)("cli") }];
469
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
470
- const ctrl = new InputController(rl);
514
+ ?? [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)("cli")) }];
515
+ // ─── Rendering: TUI (Ink + Static) for TTY, imperative for tests/pipes ───
516
+ const useTui = !options._testInputSource && process.stdin.isTTY === true;
471
517
  let currentAbort = null;
472
- const history = [];
473
518
  let closed = false;
474
- rl.on("close", () => { closed = true; });
475
- if (options.banner !== false) {
476
- const bannerText = typeof options.banner === "string"
477
- ? options.banner
478
- : `${options.agentName} (type /commands for help)`;
479
- // eslint-disable-next-line no-console -- terminal UX: startup banner
480
- console.log(`\n${bannerText}\n`);
519
+ // eslint-disable-next-line prefer-const -- set by onImageMap callback during input
520
+ let pendingImages = null;
521
+ let cliCallbacks;
522
+ let tuiStore = null;
523
+ let inkRef = null;
524
+ const inputQueue = useTui ? new InputQueue() : null;
525
+ let rl = null;
526
+ if (useTui) {
527
+ try {
528
+ const [ink, React, tuiMod, storeMod] = await Promise.all([
529
+ Promise.resolve().then(() => __importStar(require("ink"))),
530
+ Promise.resolve().then(() => __importStar(require("react"))),
531
+ Promise.resolve().then(() => __importStar(require("./cli/ouro-tui"))),
532
+ Promise.resolve().then(() => __importStar(require("./cli/tui-store"))),
533
+ ]);
534
+ const { OuroTui } = tuiMod;
535
+ const { TuiStore, createTuiCallbacks } = storeMod;
536
+ tuiStore = new TuiStore();
537
+ cliCallbacks = createTuiCallbacks(tuiStore);
538
+ // Seed input history from previous session (for up/down arrows) — NOT display
539
+ const prevUserMsgs = messages
540
+ .filter((msg) => msg.role === "user" && typeof msg.content === "string")
541
+ .map(msg => msg.content);
542
+ tuiStore.seedHistory(prevUserMsgs);
543
+ // Show session resume context: last 2 exchanges as normal messages
544
+ if (messages.length > 1) {
545
+ const userAssistantMsgs = messages.filter((m) => (m.role === "user" || m.role === "assistant") && typeof m.content === "string" && m.content.trim().length > 0);
546
+ // Extract last 2 exchanges (up to 4 messages)
547
+ const lastExchanges = [];
548
+ for (let i = userAssistantMsgs.length - 1; i >= 0 && lastExchanges.length < 4; i--) {
549
+ lastExchanges.unshift({ role: userAssistantMsgs[i].role, content: userAssistantMsgs[i].content });
550
+ }
551
+ tuiStore.addResumeMessages(lastExchanges);
552
+ }
553
+ // Compute resumeInfo for header banner
554
+ const resumeInfo = messages.length > 1
555
+ ? {
556
+ messageCount: messages.filter(m => m.role === "user" || m.role === "assistant").length,
557
+ timeAgo: options.lastActivityAt ? formatTimeAgo(new Date(options.lastActivityAt)) : "unknown",
558
+ }
559
+ : undefined;
560
+ // Ctrl-C state machine (Claude Code behavior):
561
+ // During generation: abort current request
562
+ // Idle with text: clear input
563
+ // Idle empty (first): warn
564
+ // Idle empty (second): exit
565
+ let ctrlCWarned = false;
566
+ let ctrlCTimer = null;
567
+ const handleCtrlC = (hasInput) => {
568
+ if (currentAbort) {
569
+ currentAbort.abort();
570
+ ctrlCWarned = false;
571
+ return "abort";
572
+ }
573
+ if (hasInput) {
574
+ ctrlCWarned = false;
575
+ return "clear";
576
+ }
577
+ if (ctrlCWarned) {
578
+ ctrlCWarned = false;
579
+ if (ctrlCTimer) {
580
+ clearTimeout(ctrlCTimer);
581
+ ctrlCTimer = null;
582
+ }
583
+ closed = true;
584
+ inputQueue.close();
585
+ return "exit";
586
+ }
587
+ ctrlCWarned = true;
588
+ // Reset after 2 seconds — must press twice within window
589
+ ctrlCTimer = setTimeout(() => { ctrlCWarned = false; }, 2000);
590
+ return "warn";
591
+ };
592
+ // TUI root: subscribes to store, passes props to OuroTui
593
+ // Elapsed timer is local React state (no store.notify overhead)
594
+ const storeRef = tuiStore;
595
+ function TuiRoot() {
596
+ const [, forceUpdate] = React.useState(0);
597
+ const [elapsed, setElapsed] = React.useState(0);
598
+ React.useEffect(() => storeRef.subscribe(() => {
599
+ forceUpdate((n) => n + 1);
600
+ // Reset ctrlC warning on any state change (new turn, etc.)
601
+ }), []);
602
+ React.useEffect(() => {
603
+ const iv = setInterval(() => setElapsed(storeRef.getElapsed()), 1000);
604
+ return () => clearInterval(iv);
605
+ }, []);
606
+ return React.createElement(OuroTui, {
607
+ agentName: options.agentName,
608
+ model: (0, identity_1.loadAgentConfig)().humanFacing?.model ?? "",
609
+ completedMessages: storeRef.completedMessages,
610
+ inputHistory: storeRef.inputHistory,
611
+ queuedInputs: storeRef.queuedInputs,
612
+ live: storeRef.live,
613
+ elapsedSeconds: elapsed,
614
+ contextPercent: 0,
615
+ onSubmit: (text) => { ctrlCWarned = false; inputQueue.push(text); storeRef.enqueueInput(text); },
616
+ onCtrlC: handleCtrlC,
617
+ onPopQueue: () => { const items = storeRef.popAllQueuedForEditing(); inputQueue.drainAll(); return items; },
618
+ headerShown: storeRef.headerShown,
619
+ cwd: process.cwd().replace(process.env.HOME ?? "", "~"),
620
+ resumeInfo,
621
+ onImageMap: (images) => { pendingImages = images; },
622
+ onHistoryAdd: (text) => { storeRef.addToHistoryOnly(text); },
623
+ });
624
+ }
625
+ inkRef = ink.render(React.createElement(TuiRoot), { exitOnCtrlC: false, patchConsole: false });
626
+ }
627
+ catch (err) {
628
+ // Ink failed to load (CJS compat, missing deps, etc.) — fall through to imperative
629
+ (0, runtime_1.emitNervesEvent)({
630
+ component: "senses",
631
+ event: "senses.tui_fallback",
632
+ message: `TUI failed to load, falling back to imperative: ${err instanceof Error ? err.message : String(err)}`,
633
+ meta: {},
634
+ });
635
+ }
636
+ }
637
+ // Fallback to imperative callbacks if TUI didn't initialize
638
+ if (!tuiStore) {
639
+ cliCallbacks = createCliCallbacks();
640
+ if (options.banner !== false) {
641
+ const bannerText = typeof options.banner === "string"
642
+ ? options.banner
643
+ : `${options.agentName} (type /commands for help)`;
644
+ // eslint-disable-next-line no-console -- terminal UX: startup banner
645
+ console.log(`\n${bannerText}\n`);
646
+ }
481
647
  }
482
- const cliCallbacks = createCliCallbacks();
648
+ // Display helpers: route to TUI store or imperative stderr/stdout
649
+ const display = {
650
+ error: (msg) => {
651
+ if (tuiStore)
652
+ tuiStore.setError(msg);
653
+ else
654
+ process.stderr.write(`\x1b[31m${msg}\x1b[0m\n`);
655
+ },
656
+ warn: (msg) => {
657
+ if (tuiStore)
658
+ tuiStore.setError(msg); // TUI shows warnings as errors (amber color handled in component)
659
+ else
660
+ process.stderr.write(`\x1b[33m${msg}\x1b[0m\n`);
661
+ },
662
+ text: (msg) => {
663
+ if (tuiStore)
664
+ tuiStore.appendText(msg);
665
+ else
666
+ process.stdout.write(`${msg}\n`);
667
+ },
668
+ suppressInput: () => {
669
+ if (tuiStore)
670
+ tuiStore.suppressInput();
671
+ },
672
+ restoreInput: () => {
673
+ if (tuiStore)
674
+ tuiStore.restoreInput();
675
+ },
676
+ };
677
+ const effectiveToolContext = {
678
+ signin: options.toolContext?.signin ?? (async () => undefined),
679
+ ...options.toolContext,
680
+ codingFeedback: {
681
+ send: async (message) => {
682
+ const assistantMessage = {
683
+ role: "assistant",
684
+ content: message,
685
+ };
686
+ messages.push(assistantMessage);
687
+ await options.onAsyncAssistantMessage?.(messages, assistantMessage);
688
+ display.text(message);
689
+ await options.toolContext?.codingFeedback?.send(message);
690
+ },
691
+ },
692
+ };
483
693
  // exitOnToolCall machinery: wrap execTool to detect target tool
484
694
  let exitToolResult;
485
695
  let exitToolFired = false;
@@ -490,8 +700,6 @@ async function runCliSession(options) {
490
700
  if (name === options.exitOnToolCall) {
491
701
  exitToolResult = result;
492
702
  exitToolFired = true;
493
- // Abort immediately so the model doesn't generate more output
494
- // (e.g. reasoning about calling final_answer after complete_adoption)
495
703
  currentAbort?.abort();
496
704
  }
497
705
  return result;
@@ -499,28 +707,6 @@ async function runCliSession(options) {
499
707
  : resolvedExecTool;
500
708
  // Resolve toolChoiceRequired: use explicit option if set, else fall back to toggle
501
709
  const getEffectiveToolChoiceRequired = () => options.toolChoiceRequired !== undefined ? options.toolChoiceRequired : (0, commands_1.getToolChoiceRequired)();
502
- // Ctrl-C at the input prompt: clear line or warn/exit
503
- rl.on("SIGINT", () => {
504
- const rlInt = rl;
505
- const currentLine = rlInt.line || "";
506
- const result = handleSigint(rl, currentLine);
507
- if (result === "clear") {
508
- rlInt.line = "";
509
- rlInt.cursor = 0;
510
- process.stdout.write("\r\x1b[K\x1b[36m> \x1b[0m");
511
- }
512
- else if (result === "warn") {
513
- rlInt.line = "";
514
- rlInt.cursor = 0;
515
- process.stdout.write("\r\x1b[K");
516
- process.stderr.write("press Ctrl-C again to exit\n");
517
- process.stdout.write("\x1b[36m> \x1b[0m");
518
- }
519
- else {
520
- rl.close();
521
- }
522
- });
523
- const debouncedLines = (source) => createDebouncedLines(source, pasteDebounceMs);
524
710
  (0, runtime_1.emitNervesEvent)({
525
711
  component: "senses",
526
712
  event: "senses.cli_session_start",
@@ -533,7 +719,7 @@ async function runCliSession(options) {
533
719
  if (options.autoFirstTurn && messages.length > 0 && messages[messages.length - 1]?.role === "user") {
534
720
  currentAbort = new AbortController();
535
721
  const traceId = (0, nerves_1.createTraceId)();
536
- ctrl.suppress(() => currentAbort.abort());
722
+ display.suppressInput();
537
723
  let result;
538
724
  try {
539
725
  result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
@@ -541,124 +727,145 @@ async function runCliSession(options) {
541
727
  traceId,
542
728
  tools: options.tools,
543
729
  execTool: wrappedExecTool,
544
- toolContext: options.toolContext,
730
+ toolContext: effectiveToolContext,
545
731
  });
546
732
  }
547
733
  catch (err) {
548
- // AbortError (Ctrl-C) -- silently continue to prompt
549
- // All other errors: show the user what happened
550
734
  if (!(err instanceof DOMException && err.name === "AbortError")) {
551
- process.stderr.write(`\x1b[31m${err instanceof Error ? err.message : String(err)}\x1b[0m\n`);
735
+ display.error(err instanceof Error ? err.message : String(err));
552
736
  }
553
737
  }
554
738
  cliCallbacks.flushMarkdown();
555
- ctrl.restore();
739
+ display.restoreInput();
556
740
  currentAbort = null;
557
741
  if (exitToolFired) {
558
742
  exitReason = "tool_exit";
559
- rl.close();
743
+ closed = true;
560
744
  }
561
745
  else {
562
746
  const lastMsg = messages[messages.length - 1];
563
747
  if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
564
- process.stderr.write("\x1b[33m(empty response)\x1b[0m\n");
748
+ display.warn("(empty response)");
565
749
  }
566
- process.stdout.write("\n\n");
567
750
  if (options.onTurnEnd) {
568
751
  await options.onTurnEnd(messages, result ?? { usage: undefined });
569
752
  }
570
753
  }
571
754
  }
572
- if (!exitToolFired) {
573
- process.stdout.write("\x1b[36m> \x1b[0m");
574
- }
575
755
  try {
576
- for await (const input of debouncedLines(rl)) {
756
+ // Input source: TUI queue for Ink, test source for tests, readline fallback
757
+ let inputSource;
758
+ let inputCtrl = null;
759
+ if (tuiStore && inputQueue) {
760
+ // TUI path: input comes from Ink's onSubmit → InputQueue
761
+ inputSource = inputQueue;
762
+ }
763
+ else if (options._testInputSource) {
764
+ inputSource = options._testInputSource;
765
+ }
766
+ else {
767
+ // Imperative fallback: readline
768
+ const isTTY = process.stdin.isTTY === true;
769
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: isTTY });
770
+ inputCtrl = new InputController(rl);
771
+ inputSource = rl;
772
+ process.stdout.write(CLI_PROMPT);
773
+ }
774
+ for await (const input of inputSource) {
577
775
  if (closed)
578
776
  break;
579
- if (!input.trim()) {
580
- process.stdout.write("\x1b[36m> \x1b[0m");
777
+ if (!input.trim())
581
778
  continue;
582
- }
779
+ // Remove from TUI queue display as the agent picks up this message
780
+ tuiStore?.dequeueInput(input);
583
781
  // Optional input gate (e.g. trust gate in main)
584
782
  if (options.onInput) {
585
783
  const gate = options.onInput(input);
586
784
  if (!gate.allowed) {
587
785
  if (gate.reply) {
588
- process.stdout.write(`${gate.reply}\n`);
786
+ display.text(gate.reply);
589
787
  }
590
788
  if (closed)
591
789
  break;
592
- process.stdout.write("\x1b[36m> \x1b[0m");
593
790
  continue;
594
791
  }
595
792
  }
596
- // Check for slash commands
597
- const parsed = (0, commands_1.parseSlashCommand)(input);
598
- if (parsed) {
599
- const dispatchResult = registry.dispatch(parsed.command, { channel: "cli" });
600
- if (dispatchResult.handled && dispatchResult.result) {
601
- if (dispatchResult.result.action === "exit") {
602
- break;
603
- }
604
- else if (dispatchResult.result.action === "new") {
605
- messages.length = 0;
606
- messages.push({ role: "system", content: await (0, prompt_1.buildSystem)("cli") });
607
- await options.onNewSession?.();
608
- // eslint-disable-next-line no-console -- terminal UX: session cleared
609
- console.log("session cleared");
610
- process.stdout.write("\x1b[36m> \x1b[0m");
611
- continue;
612
- }
613
- else if (dispatchResult.result.action === "response") {
614
- // eslint-disable-next-line no-console -- terminal UX: command dispatch result
615
- console.log(dispatchResult.result.message || "");
616
- process.stdout.write("\x1b[36m> \x1b[0m");
617
- continue;
793
+ // Check for slash commands (legacy path only — pipeline handles commands for runTurn path)
794
+ if (!options.runTurn) {
795
+ const parsed = (0, commands_1.parseSlashCommand)(input);
796
+ if (parsed) {
797
+ const dispatchResult = registry.dispatch(parsed.command, { channel: "cli" });
798
+ if (dispatchResult.handled && dispatchResult.result) {
799
+ if (dispatchResult.result.action === "exit") {
800
+ break;
801
+ }
802
+ else if (dispatchResult.result.action === "response") {
803
+ display.text(dispatchResult.result.message || "");
804
+ continue;
805
+ }
618
806
  }
619
807
  }
620
808
  }
621
- // Re-style the echoed input lines
622
- const cols = process.stdout.columns || 80;
623
- const inputLines = input.split("\n");
624
- let echoRows = 0;
625
- for (const line of inputLines) {
626
- echoRows += Math.ceil((2 + line.length) / cols);
627
- }
628
- process.stdout.write(`\x1b[${echoRows}A\x1b[K` + `\x1b[1m> ${inputLines[0]}${inputLines.length > 1 ? ` (+${inputLines.length - 1} lines)` : ""}\x1b[0m\n\n`);
629
- addHistory(history, input);
809
+ // Track user message in TUI for display
810
+ if (tuiStore)
811
+ tuiStore.addUserMessage(input);
630
812
  currentAbort = new AbortController();
631
- ctrl.suppress(() => currentAbort.abort());
813
+ if (tuiStore)
814
+ tuiStore.suppressInput();
815
+ inputCtrl?.suppress(() => { currentAbort?.abort(); });
816
+ // Resolve pending image content before the turn executes
817
+ let contentParts = null;
818
+ const currentImages = pendingImages;
819
+ if (currentImages && currentImages.size > 0) {
820
+ contentParts = await (0, image_paste_1.resolveImageContent)(input, currentImages);
821
+ pendingImages = null;
822
+ }
632
823
  let result;
633
824
  try {
634
825
  if (options.runTurn) {
635
826
  // Pipeline-based turn: the runTurn callback handles user message assembly,
636
827
  // pending drain, trust gate, runAgent, postTurn, and token accumulation.
637
- result = await options.runTurn(messages, input, cliCallbacks, currentAbort.signal);
828
+ result = await options.runTurn(messages, input, cliCallbacks, currentAbort.signal, effectiveToolContext, contentParts ?? undefined);
829
+ // Handle pipeline-intercepted commands with loop-control side effects
830
+ if (result?.turnOutcome === "command") {
831
+ if (result.commandAction === "exit") {
832
+ break;
833
+ }
834
+ // For "response" commands: the pipeline already emitted the response via onTextChunk
835
+ cliCallbacks.flushMarkdown();
836
+ continue;
837
+ }
638
838
  }
639
839
  else {
640
- // Legacy path: inline runAgent (used by adoption specialist and tests)
840
+ // Legacy path: inline runAgent (used by serpent guide and tests)
641
841
  const prefix = options.getContentPrefix?.();
642
- messages.push({ role: "user", content: prefix ? `${prefix}\n\n${input}` : input });
842
+ const userContent = contentParts
843
+ ? contentParts
844
+ : (prefix ? `${prefix}\n\n${input}` : input);
845
+ const userMsg = { role: "user", content: userContent };
846
+ (0, session_events_1.stampIngressTime)(userMsg);
847
+ messages.push(userMsg);
643
848
  const traceId = (0, nerves_1.createTraceId)();
644
849
  result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
645
850
  toolChoiceRequired: getEffectiveToolChoiceRequired(),
646
851
  traceId,
647
852
  tools: options.tools,
648
853
  execTool: wrappedExecTool,
649
- toolContext: options.toolContext,
854
+ toolContext: effectiveToolContext,
650
855
  });
651
856
  }
652
857
  }
653
858
  catch (err) {
654
- // AbortError (Ctrl-C) -- silently return to prompt
655
- // All other errors: show the user what happened
656
859
  if (!(err instanceof DOMException && err.name === "AbortError")) {
657
- process.stderr.write(`\x1b[31m${err instanceof Error ? err.message : String(err)}\x1b[0m\n`);
860
+ display.error(err instanceof Error ? err.message : String(err));
658
861
  }
659
862
  }
660
863
  cliCallbacks.flushMarkdown();
661
- ctrl.restore();
864
+ if (!tuiStore)
865
+ process.stdout.write("\n"); // ensure response ends with newline before prompt (imperative only)
866
+ if (tuiStore)
867
+ tuiStore.restoreInput();
868
+ inputCtrl?.restore();
662
869
  currentAbort = null;
663
870
  // Check if exit tool was fired during this turn
664
871
  if (exitToolFired) {
@@ -668,20 +875,35 @@ async function runCliSession(options) {
668
875
  // Safety net: never silently swallow an empty response
669
876
  const lastMsg = messages[messages.length - 1];
670
877
  if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
671
- process.stderr.write("\x1b[33m(empty response)\x1b[0m\n");
878
+ display.warn("(empty response)");
672
879
  }
673
- process.stdout.write("\n\n");
674
880
  // Post-turn hook (session persistence, pending drain, prompt refresh, etc.)
675
881
  if (options.onTurnEnd) {
676
882
  await options.onTurnEnd(messages, result ?? { usage: undefined });
677
883
  }
678
884
  if (closed)
679
885
  break;
680
- process.stdout.write("\x1b[36m> \x1b[0m");
681
886
  }
682
887
  }
683
888
  finally {
684
- rl.close();
889
+ rl?.close();
890
+ if (inkRef) {
891
+ // Suppress React "state update on unmounted component" warnings during cleanup.
892
+ // Ink's useInput hook fires after unmount — this is harmless but noisy.
893
+ // This also covers the SerpentGuide exitOnToolCall path, which aborts the
894
+ // current request and breaks out of the loop into this finally block.
895
+ // eslint-disable-next-line no-console -- intentional console.warn/error override for cleanup
896
+ const origWarn = console.warn;
897
+ const origError = console.error; // eslint-disable-line no-console
898
+ // eslint-disable-next-line no-console -- suppress React unmount warnings
899
+ console.warn = (...args) => { if (typeof args[0] === "string" && args[0].includes("Can't perform a React state update"))
900
+ return; origWarn.apply(console, args); };
901
+ // eslint-disable-next-line no-console -- suppress React unmount warnings
902
+ console.error = (...args) => { if (typeof args[0] === "string" && args[0].includes("Can't perform a React state update"))
903
+ return; origError.apply(console, args); };
904
+ inkRef.unmount();
905
+ setTimeout(() => { console.warn = origWarn; console.error = origError; }, 100); // eslint-disable-line no-console
906
+ }
685
907
  if (options.banner !== false) {
686
908
  // eslint-disable-next-line no-console -- terminal UX: goodbye
687
909
  console.log("bye");
@@ -694,17 +916,32 @@ async function main(agentName, options) {
694
916
  if (agentName)
695
917
  (0, identity_1.setAgentName)(agentName);
696
918
  const pasteDebounceMs = options?.pasteDebounceMs ?? 50;
919
+ // Safety net: process-level SIGINT handler ensures Ctrl+C always exits,
920
+ // even when Ink's event loop is blocked by expensive renders.
921
+ /* v8 ignore start -- process signal handler @preserve */
922
+ let sigintCount = 0;
923
+ const sigintHandler = () => {
924
+ sigintCount++;
925
+ if (sigintCount >= 2)
926
+ process.exit(1);
927
+ };
928
+ if (!options?._testInputSource) {
929
+ process.on("SIGINT", sigintHandler);
930
+ }
931
+ /* v8 ignore stop */
932
+ // Register spinner hooks so log output clears the spinner before printing
933
+ (0, nerves_1.registerSpinnerHooks)(pauseActiveSpinner, resumeActiveSpinner);
697
934
  // Fallback: apply pending updates for daemon-less direct CLI usage
698
935
  (0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
936
+ (0, update_hooks_1.registerUpdateHook)(agent_config_v2_1.agentConfigV2Hook);
699
937
  await (0, update_hooks_1.applyPendingUpdates)((0, identity_1.getAgentBundlesRoot)(), (0, bundle_manifest_1.getPackageVersion)());
700
938
  // Fail fast if provider is misconfigured (triggers human-readable error + exit)
701
- (0, core_1.getProvider)();
939
+ (0, core_1.getProvider)("human");
702
940
  // Resolve context kernel (identity + channel) for CLI
703
941
  const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
704
942
  const friendStore = new store_file_1.FileFriendStore(friendsPath);
705
943
  const username = os.userInfo().username;
706
- const hostname = os.hostname();
707
- const localExternalId = `${username}@${hostname}`;
944
+ const localExternalId = username;
708
945
  const resolver = new resolver_1.FriendResolver(friendStore, {
709
946
  provider: "local",
710
947
  externalId: localExternalId,
@@ -735,35 +972,80 @@ async function main(agentName, options) {
735
972
  // Load existing session or start fresh
736
973
  const existing = (0, context_1.loadSession)(sessPath);
737
974
  let sessionState = existing?.state;
975
+ let sessionEvents = existing?.events ?? [];
976
+ const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
738
977
  const sessionMessages = existing?.messages && existing.messages.length > 0
739
978
  ? existing.messages
740
- : [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", undefined, resolvedContext) }];
979
+ : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)("cli", {}, resolvedContext)) }];
980
+ // Repair any orphaned tool calls from a crash mid-turn
981
+ (0, core_1.repairOrphanedToolCalls)(sessionMessages);
741
982
  // Per-turn pipeline input: CLI capabilities and pending dir
742
983
  const cliCapabilities = (0, channel_1.getChannelCapabilities)("cli");
743
- const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "cli", "session");
744
- const summarize = (0, core_1.createSummarize)();
984
+ const currentAgentName = (0, identity_1.getAgentName)();
985
+ const pendingDir = (0, pending_1.getPendingDir)(currentAgentName, friendId, "cli", "session");
986
+ const summarize = (0, core_1.createSummarize)("human");
987
+ const cliFailoverState = { pending: null };
745
988
  try {
746
989
  await runCliSession({
747
- agentName: (0, identity_1.getAgentName)(),
990
+ agentName: currentAgentName,
748
991
  pasteDebounceMs,
749
992
  messages: sessionMessages,
750
- runTurn: async (messages, userInput, callbacks, signal) => {
993
+ lastActivityAt: sessionState?.lastFriendActivityAt,
994
+ _testInputSource: options?._testInputSource,
995
+ onAsyncAssistantMessage: async (messages, _assistantMessage) => {
996
+ const prepared = (0, context_1.postTurnTrim)(messages);
997
+ const events = (0, context_1.postTurnPersist)(sessPath, prepared, undefined, sessionState);
998
+ /* v8 ignore next -- defensive: postTurnPersist always returns events in practice @preserve */
999
+ sessionEvents = events.length > 0 ? events : sessionEvents;
1000
+ },
1001
+ runTurn: async (messages, userInput, callbacks, signal, toolContext, userContent) => {
751
1002
  // Run the full per-turn pipeline: resolve -> gate -> session -> drain -> runAgent -> postTurn -> tokens
752
1003
  // User message passed via input.messages so the pipeline can prepend pending messages to it.
1004
+ const failoverState = cliFailoverState;
1005
+ // Capture terminal errors instead of displaying immediately — the failover
1006
+ // message replaces the raw error if failover triggers successfully.
1007
+ let capturedTerminalError = null;
1008
+ /* v8 ignore start -- failover-aware callback wrapper: tested via pipeline integration @preserve */
1009
+ const failoverAwareCallbacks = {
1010
+ ...callbacks,
1011
+ // Save session after each tool result for crash recovery (deferred to avoid blocking)
1012
+ onToolResult: (turnMessages) => {
1013
+ const prepared = (0, context_1.postTurnTrim)(turnMessages);
1014
+ (0, context_1.deferPostTurnPersist)(sessPath, prepared, undefined, sessionState);
1015
+ },
1016
+ onError: (error, severity) => {
1017
+ if (severity === "terminal" && failoverState) {
1018
+ capturedTerminalError = error;
1019
+ callbacks.onError(new Error(""), "transient");
1020
+ return;
1021
+ }
1022
+ callbacks.onError(error, severity);
1023
+ },
1024
+ };
1025
+ /* v8 ignore stop */
753
1026
  const result = await (0, pipeline_1.handleInboundTurn)({
754
1027
  channel: "cli",
1028
+ sessionKey: "session",
755
1029
  capabilities: cliCapabilities,
756
- messages: [{ role: "user", content: userInput }],
1030
+ messages: [{ role: "user", content: userContent ?? userInput }],
757
1031
  continuityIngressTexts: getCliContinuityIngressTexts(userInput),
758
- callbacks,
1032
+ callbacks: failoverAwareCallbacks,
759
1033
  friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
760
- sessionLoader: { loadOrCreate: () => Promise.resolve({ messages, sessionPath: sessPath, state: sessionState }) },
1034
+ sessionLoader: {
1035
+ loadOrCreate: () => Promise.resolve({
1036
+ messages,
1037
+ sessionPath: sessPath,
1038
+ state: sessionState,
1039
+ events: sessionEvents,
1040
+ }),
1041
+ },
761
1042
  pendingDir,
762
1043
  friendStore,
763
1044
  provider: "local",
764
1045
  externalId: localExternalId,
765
1046
  enforceTrustGate: trust_gate_1.enforceTrustGate,
766
1047
  drainPending: pending_1.drainPending,
1048
+ drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(currentAgentName, deferredFriendId),
767
1049
  runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
768
1050
  ...opts,
769
1051
  toolContext: {
@@ -774,30 +1056,53 @@ async function main(agentName, options) {
774
1056
  },
775
1057
  }),
776
1058
  postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
777
- (0, context_1.postTurn)(turnMessages, sessionPathArg, usage, hooks, state);
1059
+ // Trim synchronously (mutates turnMessages for next turn),
1060
+ // then defer envelope build + disk I/O to avoid blocking the TUI.
1061
+ const prepared = (0, context_1.postTurnTrim)(turnMessages, usage, hooks);
778
1062
  sessionState = state;
1063
+ (0, context_1.deferPostTurnPersist)(sessionPathArg, prepared, usage, state).then((events) => {
1064
+ /* v8 ignore next -- defensive: deferPostTurnPersist always resolves events in practice @preserve */
1065
+ sessionEvents = events.length > 0 ? events : sessionEvents;
1066
+ });
779
1067
  },
780
1068
  accumulateFriendTokens: tokens_1.accumulateFriendTokens,
781
1069
  signal,
782
1070
  runAgentOptions: {
783
1071
  toolChoiceRequired: (0, commands_1.getToolChoiceRequired)(),
784
1072
  traceId: (0, nerves_1.createTraceId)(),
1073
+ mcpManager,
1074
+ toolContext,
785
1075
  },
1076
+ failoverState,
786
1077
  });
1078
+ /* v8 ignore start -- failover display: tested via pipeline integration tests @preserve */
1079
+ if (result.failoverMessage) {
1080
+ // Failover handled it — show the actionable message instead of the raw error
1081
+ process.stdout.write(`\x1b[33m${result.failoverMessage}\x1b[0m\n`);
1082
+ }
1083
+ else if (capturedTerminalError) {
1084
+ // Failover didn't trigger (no failoverState, or sequence failed) — show the raw error
1085
+ process.stderr.write(`\x1b[31m${(0, format_1.formatError)(capturedTerminalError)}\x1b[0m\n`);
1086
+ }
1087
+ /* v8 ignore stop */
787
1088
  // Handle gate rejection: display auto-reply if present
788
1089
  if (!result.gateResult.allowed) {
789
1090
  if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
790
1091
  process.stdout.write(`${result.gateResult.autoReply}\n`);
791
1092
  }
792
1093
  }
793
- return { usage: result.usage };
794
- },
795
- onNewSession: () => {
796
- (0, context_1.deleteSession)(sessPath);
1094
+ return { usage: result.usage, turnOutcome: result.turnOutcome, commandAction: result.commandAction };
797
1095
  },
798
1096
  });
799
1097
  }
800
1098
  finally {
801
1099
  sessionLock?.release();
802
1100
  }
1101
+ // Force exit: lingering handles (Ink cleanup timers, MCP connections) keep the
1102
+ // event loop alive after the interactive session ends. This is safe because all
1103
+ // session persistence has already completed in the finally block above.
1104
+ /* v8 ignore next -- process.exit not callable in vitest @preserve */
1105
+ if (!options?._testInputSource)
1106
+ process.exit(0);
803
1107
  }
1108
+ // CI trigger