@ouro.bot/cli 0.1.0-alpha.37 → 0.1.0-alpha.370

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 (329) hide show
  1. package/README.md +106 -14
  2. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +3 -2
  3. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +1 -1
  4. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
  5. package/changelog.json +2222 -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 +832 -0
  16. package/dist/heart/agent-entry.js +37 -2
  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 +378 -0
  28. package/dist/heart/bridges/manager.js +358 -0
  29. package/dist/heart/bridges/state-machine.js +135 -0
  30. package/dist/heart/bridges/store.js +123 -0
  31. package/dist/heart/bundle-state.js +168 -0
  32. package/dist/heart/commitments.js +111 -0
  33. package/dist/heart/config-registry.js +304 -0
  34. package/dist/heart/config.js +107 -61
  35. package/dist/heart/core.js +803 -259
  36. package/dist/heart/cross-chat-delivery.js +131 -0
  37. package/dist/heart/daemon/agent-config-check.js +382 -0
  38. package/dist/heart/daemon/agent-discovery.js +79 -3
  39. package/dist/heart/daemon/agent-service.js +360 -0
  40. package/dist/heart/daemon/agentic-repair.js +205 -0
  41. package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
  42. package/dist/heart/daemon/cadence.js +70 -0
  43. package/dist/heart/daemon/cli-defaults.js +538 -0
  44. package/dist/heart/daemon/cli-exec.js +3081 -0
  45. package/dist/heart/daemon/cli-help.js +312 -0
  46. package/dist/heart/daemon/cli-parse.js +1023 -0
  47. package/dist/heart/daemon/cli-render-doctor.js +57 -0
  48. package/dist/heart/daemon/cli-render.js +560 -0
  49. package/dist/heart/daemon/cli-types.js +8 -0
  50. package/dist/heart/daemon/daemon-cli.js +29 -1171
  51. package/dist/heart/daemon/daemon-entry.js +356 -3
  52. package/dist/heart/daemon/daemon-health.js +141 -0
  53. package/dist/heart/daemon/daemon-runtime-sync.js +157 -12
  54. package/dist/heart/daemon/daemon-tombstone.js +236 -0
  55. package/dist/heart/daemon/daemon.js +757 -58
  56. package/dist/heart/daemon/doctor-types.js +8 -0
  57. package/dist/heart/daemon/doctor.js +445 -0
  58. package/dist/heart/daemon/health-monitor.js +79 -1
  59. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  60. package/dist/heart/daemon/hooks/bundle-meta.js +115 -1
  61. package/dist/heart/daemon/http-health-probe.js +80 -0
  62. package/dist/heart/daemon/inner-status.js +89 -0
  63. package/dist/heart/daemon/interactive-repair.js +148 -0
  64. package/dist/heart/daemon/launchd.js +46 -9
  65. package/dist/heart/daemon/log-tailer.js +82 -12
  66. package/dist/heart/daemon/logs-prune.js +105 -0
  67. package/dist/heart/daemon/message-router.js +17 -8
  68. package/dist/heart/daemon/os-cron-deps.js +134 -0
  69. package/dist/heart/daemon/ouro-bot-entry.js +1 -1
  70. package/dist/heart/daemon/process-manager.js +201 -0
  71. package/dist/heart/daemon/provider-discovery.js +113 -0
  72. package/dist/heart/daemon/pulse.js +475 -0
  73. package/dist/heart/daemon/run-hooks.js +2 -0
  74. package/dist/heart/daemon/runtime-logging.js +67 -16
  75. package/dist/heart/daemon/runtime-metadata.js +101 -0
  76. package/dist/heart/daemon/runtime-mode.js +67 -0
  77. package/dist/heart/daemon/safe-mode.js +161 -0
  78. package/dist/heart/daemon/sense-manager.js +72 -3
  79. package/dist/heart/daemon/session-id-resolver.js +131 -0
  80. package/dist/heart/daemon/skill-management-installer.js +94 -0
  81. package/dist/heart/daemon/socket-client.js +307 -0
  82. package/dist/heart/daemon/stale-bundle-prune.js +96 -0
  83. package/dist/heart/daemon/startup-tui.js +237 -0
  84. package/dist/heart/daemon/task-scheduler.js +3 -25
  85. package/dist/heart/daemon/thoughts.js +510 -0
  86. package/dist/heart/daemon/up-progress.js +135 -0
  87. package/dist/heart/delegation.js +62 -0
  88. package/dist/heart/habits/habit-migration.js +181 -0
  89. package/dist/heart/habits/habit-parser.js +140 -0
  90. package/dist/heart/habits/habit-scheduler.js +371 -0
  91. package/dist/heart/{daemon → hatch}/hatch-flow.js +55 -126
  92. package/dist/heart/{daemon → hatch}/hatch-specialist.js +3 -3
  93. package/dist/heart/{daemon → hatch}/specialist-prompt.js +11 -8
  94. package/dist/heart/{daemon → hatch}/specialist-tools.js +77 -11
  95. package/dist/heart/identity.js +154 -59
  96. package/dist/heart/kept-notes.js +357 -0
  97. package/dist/heart/kicks.js +2 -20
  98. package/dist/heart/machine-identity.js +161 -0
  99. package/dist/heart/mcp/mcp-server.js +653 -0
  100. package/dist/heart/migrate-config.js +100 -0
  101. package/dist/heart/model-capabilities.js +59 -0
  102. package/dist/heart/outlook/outlook-http-hooks.js +64 -0
  103. package/dist/heart/outlook/outlook-http-response.js +7 -0
  104. package/dist/heart/outlook/outlook-http-routes.js +232 -0
  105. package/dist/heart/outlook/outlook-http-static.js +99 -0
  106. package/dist/heart/outlook/outlook-http-transport.js +116 -0
  107. package/dist/heart/outlook/outlook-http.js +99 -0
  108. package/dist/heart/outlook/outlook-read.js +28 -0
  109. package/dist/heart/outlook/outlook-types.js +27 -0
  110. package/dist/heart/outlook/outlook-view.js +195 -0
  111. package/dist/heart/outlook/readers/agent-machine.js +359 -0
  112. package/dist/heart/outlook/readers/continuity-readers.js +332 -0
  113. package/dist/heart/outlook/readers/runtime-readers.js +660 -0
  114. package/dist/heart/outlook/readers/sessions.js +232 -0
  115. package/dist/heart/outlook/readers/shared.js +111 -0
  116. package/dist/heart/platform.js +81 -0
  117. package/dist/heart/progress-story.js +42 -0
  118. package/dist/heart/provider-attempt.js +133 -0
  119. package/dist/heart/provider-binding-resolver.js +239 -0
  120. package/dist/heart/provider-credentials.js +379 -0
  121. package/dist/heart/provider-failover.js +266 -0
  122. package/dist/heart/provider-models.js +81 -0
  123. package/dist/heart/provider-ping.js +237 -0
  124. package/dist/heart/provider-state.js +216 -0
  125. package/dist/heart/provider-visibility.js +180 -0
  126. package/dist/heart/providers/anthropic-token.js +131 -0
  127. package/dist/heart/providers/anthropic.js +193 -55
  128. package/dist/heart/providers/azure.js +103 -12
  129. package/dist/heart/providers/error-classification.js +63 -0
  130. package/dist/heart/providers/github-copilot.js +145 -0
  131. package/dist/heart/providers/minimax-vlm.js +189 -0
  132. package/dist/heart/providers/minimax.js +29 -7
  133. package/dist/heart/providers/openai-codex.js +39 -29
  134. package/dist/heart/session-activity.js +190 -0
  135. package/dist/heart/session-events.js +855 -0
  136. package/dist/heart/session-transcript.js +167 -0
  137. package/dist/heart/start-of-turn-packet.js +345 -0
  138. package/dist/heart/streaming.js +36 -27
  139. package/dist/heart/sync.js +332 -0
  140. package/dist/heart/target-resolution.js +127 -0
  141. package/dist/heart/tempo.js +93 -0
  142. package/dist/heart/temporal-view.js +41 -0
  143. package/dist/heart/tool-activity-callbacks.js +36 -0
  144. package/dist/heart/tool-description.js +135 -0
  145. package/dist/heart/tool-friction.js +55 -0
  146. package/dist/heart/tool-loop.js +200 -0
  147. package/dist/heart/turn-context.js +362 -0
  148. package/dist/heart/turn-coordinator.js +28 -0
  149. package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +1 -1
  150. package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
  151. package/dist/heart/versioning/ouro-path-installer.js +301 -0
  152. package/dist/heart/versioning/ouro-version-manager.js +295 -0
  153. package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
  154. package/dist/heart/{daemon → versioning}/update-checker.js +12 -2
  155. package/dist/heart/{daemon → versioning}/update-hooks.js +63 -59
  156. package/dist/mind/bundle-manifest.js +7 -1
  157. package/dist/mind/context.js +141 -94
  158. package/dist/mind/diary-integrity.js +60 -0
  159. package/dist/mind/{memory.js → diary.js} +84 -96
  160. package/dist/mind/embedding-provider.js +60 -0
  161. package/dist/mind/file-state.js +179 -0
  162. package/dist/mind/first-impressions.js +14 -1
  163. package/dist/mind/friends/channel.js +56 -0
  164. package/dist/mind/friends/group-context.js +144 -0
  165. package/dist/mind/friends/resolver.js +38 -1
  166. package/dist/mind/friends/store-file.js +58 -3
  167. package/dist/mind/friends/trust-explanation.js +74 -0
  168. package/dist/mind/friends/types.js +9 -1
  169. package/dist/mind/journal-index.js +161 -0
  170. package/dist/mind/note-search.js +268 -0
  171. package/dist/mind/obligation-steering.js +221 -0
  172. package/dist/mind/pending.js +74 -7
  173. package/dist/mind/prompt-refresh.js +3 -2
  174. package/dist/mind/prompt.js +1030 -118
  175. package/dist/mind/provenance-trust.js +26 -0
  176. package/dist/mind/scrutiny.js +173 -0
  177. package/dist/mind/token-estimate.js +8 -12
  178. package/dist/nerves/cli-logging.js +7 -1
  179. package/dist/nerves/coverage/audit-rules.js +15 -6
  180. package/dist/nerves/coverage/audit.js +28 -2
  181. package/dist/nerves/coverage/cli.js +1 -1
  182. package/dist/nerves/coverage/file-completeness.js +83 -5
  183. package/dist/nerves/coverage/run-artifacts.js +1 -1
  184. package/dist/nerves/event-buffer.js +111 -0
  185. package/dist/nerves/index.js +224 -4
  186. package/dist/nerves/observation.js +20 -0
  187. package/dist/nerves/redact.js +79 -0
  188. package/dist/nerves/runtime.js +5 -1
  189. package/dist/outlook-ui/assets/index-LwChZTgL.css +1 -0
  190. package/dist/outlook-ui/assets/index-xTdv64BV.js +61 -0
  191. package/dist/outlook-ui/index.html +15 -0
  192. package/dist/repertoire/ado-client.js +15 -56
  193. package/dist/repertoire/ado-semantic.js +11 -10
  194. package/dist/repertoire/api-client.js +97 -0
  195. package/dist/repertoire/bitwarden-store.js +365 -0
  196. package/dist/repertoire/bundle-templates.js +72 -0
  197. package/dist/repertoire/bw-installer.js +79 -0
  198. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  199. package/dist/repertoire/coding/context-pack.js +330 -0
  200. package/dist/repertoire/coding/feedback.js +197 -30
  201. package/dist/repertoire/coding/manager.js +158 -9
  202. package/dist/repertoire/coding/spawner.js +55 -9
  203. package/dist/repertoire/coding/tools.js +170 -7
  204. package/dist/repertoire/commerce-errors.js +109 -0
  205. package/dist/repertoire/commerce-self-test.js +156 -0
  206. package/dist/repertoire/credential-access.js +107 -0
  207. package/dist/repertoire/duffel-client.js +185 -0
  208. package/dist/repertoire/github-client.js +14 -55
  209. package/dist/repertoire/graph-client.js +11 -52
  210. package/dist/repertoire/guardrails.js +375 -0
  211. package/dist/repertoire/mcp-client.js +255 -0
  212. package/dist/repertoire/mcp-manager.js +305 -0
  213. package/dist/repertoire/mcp-tools.js +63 -0
  214. package/dist/repertoire/shell-sessions.js +133 -0
  215. package/dist/repertoire/skills.js +15 -24
  216. package/dist/repertoire/stripe-client.js +131 -0
  217. package/dist/repertoire/tasks/board.js +43 -5
  218. package/dist/repertoire/tasks/fix.js +182 -0
  219. package/dist/repertoire/tasks/index.js +28 -10
  220. package/dist/repertoire/tasks/lifecycle.js +2 -2
  221. package/dist/repertoire/tasks/parser.js +3 -2
  222. package/dist/repertoire/tasks/scanner.js +194 -37
  223. package/dist/repertoire/tasks/transitions.js +16 -79
  224. package/dist/repertoire/tool-results.js +29 -0
  225. package/dist/repertoire/tools-attachments.js +317 -0
  226. package/dist/repertoire/tools-base.js +45 -771
  227. package/dist/repertoire/tools-bluebubbles.js +1 -0
  228. package/dist/repertoire/tools-bridge.js +141 -0
  229. package/dist/repertoire/tools-bundle.js +984 -0
  230. package/dist/repertoire/tools-config.js +185 -0
  231. package/dist/repertoire/tools-continuity.js +248 -0
  232. package/dist/repertoire/tools-credential.js +182 -0
  233. package/dist/repertoire/tools-files.js +342 -0
  234. package/dist/repertoire/tools-flight.js +224 -0
  235. package/dist/repertoire/tools-flow.js +105 -0
  236. package/dist/repertoire/tools-github.js +1 -7
  237. package/dist/repertoire/tools-notes.js +376 -0
  238. package/dist/repertoire/tools-session.js +739 -0
  239. package/dist/repertoire/tools-shell.js +120 -0
  240. package/dist/repertoire/tools-stripe.js +180 -0
  241. package/dist/repertoire/tools-surface.js +243 -0
  242. package/dist/repertoire/tools-teams.js +12 -62
  243. package/dist/repertoire/tools-travel.js +125 -0
  244. package/dist/repertoire/tools-user-profile.js +144 -0
  245. package/dist/repertoire/tools-vault.js +40 -0
  246. package/dist/repertoire/tools.js +144 -138
  247. package/dist/repertoire/travel-api-client.js +360 -0
  248. package/dist/repertoire/user-profile.js +118 -0
  249. package/dist/repertoire/vault-setup.js +241 -0
  250. package/dist/repertoire/vault-unlock.js +359 -0
  251. package/dist/scripts/claude-code-hook.js +41 -0
  252. package/dist/scripts/claude-code-stop-hook.js +47 -0
  253. package/dist/senses/attention-queue.js +116 -0
  254. package/dist/senses/bluebubbles/attachment-cache.js +53 -0
  255. package/dist/senses/bluebubbles/attachment-download.js +137 -0
  256. package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +260 -9
  257. package/dist/senses/bluebubbles/entry.js +13 -0
  258. package/dist/senses/bluebubbles/inbound-log.js +113 -0
  259. package/dist/senses/bluebubbles/index.js +1620 -0
  260. package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -70
  261. package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +43 -12
  262. package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +46 -6
  263. package/dist/senses/bluebubbles/replay.js +129 -0
  264. package/dist/senses/bluebubbles/runtime-state.js +109 -0
  265. package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
  266. package/dist/senses/cli/bracketed-paste.js +82 -0
  267. package/dist/senses/cli/image-paste.js +287 -0
  268. package/dist/senses/cli/image-ref-navigation.js +75 -0
  269. package/dist/senses/cli/ink-app.js +156 -0
  270. package/dist/senses/cli/inline-diff.js +64 -0
  271. package/dist/senses/cli/input-keys.js +174 -0
  272. package/dist/senses/cli/kill-ring.js +86 -0
  273. package/dist/senses/cli/message-list.js +51 -0
  274. package/dist/senses/cli/ouro-tui.js +605 -0
  275. package/dist/senses/cli/spinner-imperative.js +135 -0
  276. package/dist/senses/cli/spinner.js +101 -0
  277. package/dist/senses/cli/status-line.js +60 -0
  278. package/dist/senses/cli/streaming-markdown.js +526 -0
  279. package/dist/senses/cli/tool-display.js +83 -0
  280. package/dist/senses/cli/tool-render.js +85 -0
  281. package/dist/senses/cli/tui-store.js +240 -0
  282. package/dist/senses/cli/virtual-list.js +35 -0
  283. package/dist/senses/cli-entry.js +1 -1
  284. package/dist/senses/cli-layout.js +187 -0
  285. package/dist/senses/cli.js +588 -250
  286. package/dist/senses/commands.js +66 -3
  287. package/dist/senses/continuity.js +94 -0
  288. package/dist/senses/habit-turn-message.js +108 -0
  289. package/dist/senses/inner-dialog-worker.js +112 -19
  290. package/dist/senses/inner-dialog.js +636 -86
  291. package/dist/senses/pipeline.js +602 -0
  292. package/dist/senses/proactive-content-guard.js +51 -0
  293. package/dist/senses/shared-turn.js +205 -0
  294. package/dist/senses/surface-tool.js +68 -0
  295. package/dist/senses/teams.js +693 -160
  296. package/dist/senses/trust-gate.js +112 -2
  297. package/package.json +29 -7
  298. package/skills/agent-commerce.md +106 -0
  299. package/skills/browser-navigation.md +110 -0
  300. package/skills/commerce-setup-guide.md +116 -0
  301. package/skills/commerce-setup.md +84 -0
  302. package/skills/configure-dev-tools.md +101 -0
  303. package/skills/travel-planning.md +138 -0
  304. package/dist/heart/daemon/ouro-path-installer.js +0 -178
  305. package/dist/heart/daemon/subagent-installer.js +0 -134
  306. package/dist/mind/associative-recall.js +0 -197
  307. package/dist/senses/bluebubbles-entry.js +0 -11
  308. package/dist/senses/bluebubbles.js +0 -558
  309. package/dist/senses/debug-activity.js +0 -127
  310. package/subagents/README.md +0 -60
  311. package/subagents/work-doer.md +0 -235
  312. package/subagents/work-merger.md +0 -618
  313. package/subagents/work-planner.md +0 -382
  314. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
  315. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
  316. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
  317. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
  318. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
  319. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
  320. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
  321. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
  322. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
  323. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
  324. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
  325. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
  326. /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
  327. /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
  328. /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
  329. /package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +0 -0
@@ -33,7 +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
+ exports.formatPendingPrefix = formatPendingPrefix;
38
+ exports.getCliContinuityIngressTexts = getCliContinuityIngressTexts;
39
+ exports.formatTimeAgo = formatTimeAgo;
40
+ exports.writeCliAsyncAssistantMessage = writeCliAsyncAssistantMessage;
41
+ exports.pauseActiveSpinner = pauseActiveSpinner;
42
+ exports.resumeActiveSpinner = resumeActiveSpinner;
43
+ exports.setActiveSpinner = setActiveSpinner;
37
44
  exports.handleSigint = handleSigint;
38
45
  exports.addHistory = addHistory;
39
46
  exports.renderMarkdown = renderMarkdown;
@@ -49,11 +56,12 @@ const prompt_1 = require("../mind/prompt");
49
56
  const phrases_1 = require("../mind/phrases");
50
57
  const format_1 = require("../mind/format");
51
58
  const config_1 = require("../heart/config");
59
+ const session_events_1 = require("../heart/session-events");
52
60
  const context_1 = require("../mind/context");
53
61
  const pending_1 = require("../mind/pending");
54
- const prompt_refresh_1 = require("../mind/prompt-refresh");
55
62
  const commands_1 = require("./commands");
56
63
  const identity_1 = require("../heart/identity");
64
+ const mcp_manager_1 = require("../repertoire/mcp-manager");
57
65
  const nerves_1 = require("../nerves");
58
66
  const store_file_1 = require("../mind/friends/store-file");
59
67
  const resolver_1 = require("../mind/friends/resolver");
@@ -61,72 +69,78 @@ const tokens_1 = require("../mind/friends/tokens");
61
69
  const cli_logging_1 = require("../nerves/cli-logging");
62
70
  const runtime_1 = require("../nerves/runtime");
63
71
  const trust_gate_1 = require("./trust-gate");
72
+ const pipeline_1 = require("./pipeline");
73
+ const channel_1 = require("../mind/friends/channel");
64
74
  const session_lock_1 = require("./session-lock");
65
- const update_hooks_1 = require("../heart/daemon/update-hooks");
75
+ const update_hooks_1 = require("../heart/versioning/update-hooks");
66
76
  const bundle_meta_1 = require("../heart/daemon/hooks/bundle-meta");
77
+ const agent_config_v2_1 = require("../heart/daemon/hooks/agent-config-v2");
67
78
  const bundle_manifest_1 = require("../mind/bundle-manifest");
68
- // spinner that only touches stderr, cleans up after itself
69
- // exported for direct testability (stop-without-start branch)
70
- class Spinner {
71
- frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
72
- i = 0;
73
- iv = null;
74
- piv = null;
75
- msg = "";
76
- phrases = null;
77
- lastPhrase = "";
78
- stopped = false;
79
- constructor(m = "working", phrases) {
80
- this.msg = m;
81
- if (phrases && phrases.length > 0)
82
- this.phrases = phrases;
83
- }
84
- start() {
85
- this.stopped = false;
86
- process.stderr.write("\r\x1b[K");
87
- this.spin();
88
- this.iv = setInterval(() => this.spin(), 80);
89
- if (this.phrases) {
90
- this.piv = setInterval(() => this.rotatePhrase(), 1500);
91
- }
92
- }
93
- spin() {
94
- // Guard: clearInterval can't prevent already-dequeued callbacks
95
- /* v8 ignore next -- race guard: timer callback fires after stop() @preserve */
96
- if (this.stopped)
97
- return;
98
- process.stderr.write(`\r\x1b[K${this.frames[this.i]} ${this.msg}... `);
99
- this.i = (this.i + 1) % this.frames.length;
100
- }
101
- rotatePhrase() {
102
- /* v8 ignore next -- race guard: timer callback fires after stop() @preserve */
103
- if (this.stopped)
104
- return;
105
- const next = (0, phrases_1.pickPhrase)(this.phrases, this.lastPhrase);
106
- this.lastPhrase = next;
107
- this.msg = next;
108
- }
109
- stop(ok) {
110
- this.stopped = true;
111
- if (this.iv) {
112
- clearInterval(this.iv);
113
- this.iv = null;
114
- }
115
- if (this.piv) {
116
- clearInterval(this.piv);
117
- this.piv = null;
118
- }
119
- process.stderr.write("\r\x1b[K");
120
- /* v8 ignore next -- ok parameter currently unused by callers @preserve */
121
- if (ok)
122
- process.stderr.write(`\x1b[32m\u2713\x1b[0m ${ok}\n`);
123
- }
124
- fail(msg) {
125
- this.stop();
126
- process.stderr.write(`\x1b[31m\u2717\x1b[0m ${msg}\n`);
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; } });
87
+ /**
88
+ * Format pending messages as content-prefix strings for injection into
89
+ * the next user message. Self-messages (from === agentName) become
90
+ * `[inner thought: {content}]`, inter-agent messages become
91
+ * `[message from {name}: {content}]`.
92
+ */
93
+ function formatPendingPrefix(messages, agentName) {
94
+ return messages
95
+ .map((msg) => msg.from === agentName
96
+ ? `[inner thought: ${msg.content}]`
97
+ : `[message from ${msg.from}: ${msg.content}]`)
98
+ .join("\n");
99
+ }
100
+ function getCliContinuityIngressTexts(input) {
101
+ const trimmed = input.trim();
102
+ return trimmed ? [trimmed] : [];
103
+ }
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);
127
131
  }
128
132
  }
129
- 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; } });
130
144
  // Input controller: pauses readline during model/tool execution.
131
145
  // Does NOT touch raw mode — readline with terminal:true manages raw mode
132
146
  // internally. Touching it causes ^C to be echoed by the terminal driver.
@@ -294,59 +308,55 @@ function createCliCallbacks() {
294
308
  meta: {},
295
309
  });
296
310
  let currentSpinner = null;
297
- let hadReasoning = false;
311
+ function setSpinner(s) { currentSpinner = s; setActiveSpinner(s); }
298
312
  let hadToolRun = false;
299
313
  let textDirty = false; // true when text/reasoning was written without a trailing newline
300
314
  const streamer = new MarkdownStreamer();
315
+ const wrapper = new cli_layout_1.StreamingWordWrapper();
301
316
  return {
302
317
  onModelStart: () => {
303
318
  currentSpinner?.stop();
304
- currentSpinner = null;
305
- hadReasoning = false;
319
+ setSpinner(null);
306
320
  textDirty = false;
307
321
  streamer.reset();
322
+ wrapper.reset();
308
323
  const phrases = (0, phrases_1.getPhrases)();
309
324
  const pool = hadToolRun ? phrases.followup : phrases.thinking;
310
325
  const first = (0, phrases_1.pickPhrase)(pool);
311
- currentSpinner = new Spinner(first, pool);
326
+ setSpinner(new spinner_imperative_1.ImperativeSpinner(first, pool));
312
327
  currentSpinner.start();
313
328
  },
314
329
  onModelStreamStart: () => {
315
330
  // No-op: content callbacks (onTextChunk, onReasoningChunk) handle
316
331
  // stopping the spinner. onModelStreamStart fires too early and
317
- // doesn't fire at all for final_answer tool streaming.
332
+ // doesn't fire at all for settle tool streaming.
318
333
  },
319
334
  onClearText: () => {
320
335
  streamer.reset();
336
+ wrapper.reset();
321
337
  },
322
338
  onTextChunk: (text) => {
323
- // Stop spinner if still running — final_answer streaming and Anthropic
339
+ // Stop spinner if still running — settle streaming and Anthropic
324
340
  // tool-only responses bypass onModelStreamStart, so the spinner would
325
341
  // otherwise keep running (and its \r writes overwrite response text).
326
342
  if (currentSpinner) {
327
343
  currentSpinner.stop();
328
- currentSpinner = null;
329
- }
330
- if (hadReasoning) {
331
- // Single newline to separate reasoning from reply — reasoning
332
- // output often ends with its own trailing newline(s)
333
- process.stdout.write("\n");
334
- hadReasoning = false;
344
+ setSpinner(null);
335
345
  }
336
346
  const rendered = streamer.push(text);
337
- if (rendered)
338
- process.stdout.write(rendered);
339
- textDirty = text.length > 0 && !text.endsWith("\n");
340
- },
341
- onReasoningChunk: (text) => {
342
- if (currentSpinner) {
343
- currentSpinner.stop();
344
- 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);
345
352
  }
346
- hadReasoning = true;
347
- process.stdout.write(`\x1b[2m${text}\x1b[0m`);
353
+ /* v8 ignore stop */
348
354
  textDirty = text.length > 0 && !text.endsWith("\n");
349
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
+ },
350
360
  onToolStart: (_name, _args) => {
351
361
  // Stop the model-start spinner: when the model returns only tool calls
352
362
  // (no content/reasoning), onModelStreamStart never fires, so the old
@@ -360,31 +370,29 @@ function createCliCallbacks() {
360
370
  }
361
371
  const toolPhrases = (0, phrases_1.getPhrases)().tool;
362
372
  const first = (0, phrases_1.pickPhrase)(toolPhrases);
363
- currentSpinner = new Spinner(first, toolPhrases);
373
+ setSpinner(new spinner_imperative_1.ImperativeSpinner(first, toolPhrases));
364
374
  currentSpinner.start();
365
375
  hadToolRun = true;
366
376
  },
367
377
  onToolEnd: (name, argSummary, success) => {
368
378
  currentSpinner?.stop();
369
- currentSpinner = null;
370
- const msg = (0, format_1.formatToolResult)(name, argSummary, success);
371
- const color = success ? "\x1b[32m" : "\x1b[31m";
372
- process.stderr.write(`${color}${msg}\x1b[0m\n`);
379
+ setSpinner(null);
380
+ (0, tool_display_1.writeToolEnd)(name, argSummary, success);
373
381
  },
374
382
  onError: (error, severity) => {
375
383
  if (severity === "transient") {
376
384
  currentSpinner?.fail(error.message);
377
- currentSpinner = null;
385
+ setSpinner(null);
378
386
  }
379
387
  else {
380
388
  currentSpinner?.stop();
381
- currentSpinner = null;
389
+ setSpinner(null);
382
390
  process.stderr.write(`\x1b[31m${(0, format_1.formatError)(error)}\x1b[0m\n`);
383
391
  }
384
392
  },
385
393
  onKick: () => {
386
394
  currentSpinner?.stop();
387
- currentSpinner = null;
395
+ setSpinner(null);
388
396
  if (textDirty) {
389
397
  process.stdout.write("\n");
390
398
  textDirty = false;
@@ -393,10 +401,18 @@ function createCliCallbacks() {
393
401
  },
394
402
  flushMarkdown: () => {
395
403
  currentSpinner?.stop();
396
- currentSpinner = null;
404
+ setSpinner(null);
405
+ /* v8 ignore start -- wrapper flush: tested via cli.test.ts flushMarkdown tests @preserve */
397
406
  const remaining = streamer.flush();
398
- if (remaining)
399
- 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 */
400
416
  },
401
417
  };
402
418
  }
@@ -437,29 +453,243 @@ async function* createDebouncedLines(source, debounceMs) {
437
453
  yield lines.join("\n");
438
454
  }
439
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;
440
507
  async function runCliSession(options) {
441
508
  /* v8 ignore start -- integration: runCliSession is interactive, tested via E2E @preserve */
442
- const pasteDebounceMs = options.pasteDebounceMs ?? 50;
443
509
  const registry = (0, commands_1.createCommandRegistry)();
444
510
  if (!options.disableCommands) {
445
511
  (0, commands_1.registerDefaultCommands)(registry);
446
512
  }
447
513
  const messages = options.messages
448
- ?? [{ role: "system", content: await (0, prompt_1.buildSystem)("cli") }];
449
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
450
- 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;
451
517
  let currentAbort = null;
452
- const history = [];
453
518
  let closed = false;
454
- rl.on("close", () => { closed = true; });
455
- if (options.banner !== false) {
456
- const bannerText = typeof options.banner === "string"
457
- ? options.banner
458
- : `${options.agentName} (type /commands for help)`;
459
- // eslint-disable-next-line no-console -- terminal UX: startup banner
460
- 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
+ }
461
636
  }
462
- const cliCallbacks = createCliCallbacks();
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
+ }
647
+ }
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
+ };
463
693
  // exitOnToolCall machinery: wrap execTool to detect target tool
464
694
  let exitToolResult;
465
695
  let exitToolFired = false;
@@ -470,8 +700,6 @@ async function runCliSession(options) {
470
700
  if (name === options.exitOnToolCall) {
471
701
  exitToolResult = result;
472
702
  exitToolFired = true;
473
- // Abort immediately so the model doesn't generate more output
474
- // (e.g. reasoning about calling final_answer after complete_adoption)
475
703
  currentAbort?.abort();
476
704
  }
477
705
  return result;
@@ -479,28 +707,6 @@ async function runCliSession(options) {
479
707
  : resolvedExecTool;
480
708
  // Resolve toolChoiceRequired: use explicit option if set, else fall back to toggle
481
709
  const getEffectiveToolChoiceRequired = () => options.toolChoiceRequired !== undefined ? options.toolChoiceRequired : (0, commands_1.getToolChoiceRequired)();
482
- // Ctrl-C at the input prompt: clear line or warn/exit
483
- rl.on("SIGINT", () => {
484
- const rlInt = rl;
485
- const currentLine = rlInt.line || "";
486
- const result = handleSigint(rl, currentLine);
487
- if (result === "clear") {
488
- rlInt.line = "";
489
- rlInt.cursor = 0;
490
- process.stdout.write("\r\x1b[K\x1b[36m> \x1b[0m");
491
- }
492
- else if (result === "warn") {
493
- rlInt.line = "";
494
- rlInt.cursor = 0;
495
- process.stdout.write("\r\x1b[K");
496
- process.stderr.write("press Ctrl-C again to exit\n");
497
- process.stdout.write("\x1b[36m> \x1b[0m");
498
- }
499
- else {
500
- rl.close();
501
- }
502
- });
503
- const debouncedLines = (source) => createDebouncedLines(source, pasteDebounceMs);
504
710
  (0, runtime_1.emitNervesEvent)({
505
711
  component: "senses",
506
712
  event: "senses.cli_session_start",
@@ -513,7 +719,7 @@ async function runCliSession(options) {
513
719
  if (options.autoFirstTurn && messages.length > 0 && messages[messages.length - 1]?.role === "user") {
514
720
  currentAbort = new AbortController();
515
721
  const traceId = (0, nerves_1.createTraceId)();
516
- ctrl.suppress(() => currentAbort.abort());
722
+ display.suppressInput();
517
723
  let result;
518
724
  try {
519
725
  result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
@@ -521,115 +727,145 @@ async function runCliSession(options) {
521
727
  traceId,
522
728
  tools: options.tools,
523
729
  execTool: wrappedExecTool,
524
- toolContext: options.toolContext,
730
+ toolContext: effectiveToolContext,
525
731
  });
526
732
  }
527
733
  catch (err) {
528
- // AbortError (Ctrl-C) -- silently continue to prompt
529
- // All other errors: show the user what happened
530
734
  if (!(err instanceof DOMException && err.name === "AbortError")) {
531
- 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));
532
736
  }
533
737
  }
534
738
  cliCallbacks.flushMarkdown();
535
- ctrl.restore();
739
+ display.restoreInput();
536
740
  currentAbort = null;
537
741
  if (exitToolFired) {
538
742
  exitReason = "tool_exit";
539
- rl.close();
743
+ closed = true;
540
744
  }
541
745
  else {
542
746
  const lastMsg = messages[messages.length - 1];
543
747
  if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
544
- process.stderr.write("\x1b[33m(empty response)\x1b[0m\n");
748
+ display.warn("(empty response)");
545
749
  }
546
- process.stdout.write("\n\n");
547
750
  if (options.onTurnEnd) {
548
751
  await options.onTurnEnd(messages, result ?? { usage: undefined });
549
752
  }
550
753
  }
551
754
  }
552
- if (!exitToolFired) {
553
- process.stdout.write("\x1b[36m> \x1b[0m");
554
- }
555
755
  try {
556
- 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) {
557
775
  if (closed)
558
776
  break;
559
- if (!input.trim()) {
560
- process.stdout.write("\x1b[36m> \x1b[0m");
777
+ if (!input.trim())
561
778
  continue;
562
- }
779
+ // Remove from TUI queue display as the agent picks up this message
780
+ tuiStore?.dequeueInput(input);
563
781
  // Optional input gate (e.g. trust gate in main)
564
782
  if (options.onInput) {
565
783
  const gate = options.onInput(input);
566
784
  if (!gate.allowed) {
567
785
  if (gate.reply) {
568
- process.stdout.write(`${gate.reply}\n`);
786
+ display.text(gate.reply);
569
787
  }
570
788
  if (closed)
571
789
  break;
572
- process.stdout.write("\x1b[36m> \x1b[0m");
573
790
  continue;
574
791
  }
575
792
  }
576
- // Check for slash commands
577
- const parsed = (0, commands_1.parseSlashCommand)(input);
578
- if (parsed) {
579
- const dispatchResult = registry.dispatch(parsed.command, { channel: "cli" });
580
- if (dispatchResult.handled && dispatchResult.result) {
581
- if (dispatchResult.result.action === "exit") {
582
- break;
583
- }
584
- else if (dispatchResult.result.action === "new") {
585
- messages.length = 0;
586
- messages.push({ role: "system", content: await (0, prompt_1.buildSystem)("cli") });
587
- await options.onNewSession?.();
588
- // eslint-disable-next-line no-console -- terminal UX: session cleared
589
- console.log("session cleared");
590
- process.stdout.write("\x1b[36m> \x1b[0m");
591
- continue;
592
- }
593
- else if (dispatchResult.result.action === "response") {
594
- // eslint-disable-next-line no-console -- terminal UX: command dispatch result
595
- console.log(dispatchResult.result.message || "");
596
- process.stdout.write("\x1b[36m> \x1b[0m");
597
- 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
+ }
598
806
  }
599
807
  }
600
808
  }
601
- // Re-style the echoed input lines
602
- const cols = process.stdout.columns || 80;
603
- const inputLines = input.split("\n");
604
- let echoRows = 0;
605
- for (const line of inputLines) {
606
- echoRows += Math.ceil((2 + line.length) / cols);
607
- }
608
- process.stdout.write(`\x1b[${echoRows}A\x1b[K` + `\x1b[1m> ${inputLines[0]}${inputLines.length > 1 ? ` (+${inputLines.length - 1} lines)` : ""}\x1b[0m\n\n`);
609
- messages.push({ role: "user", content: input });
610
- addHistory(history, input);
809
+ // Track user message in TUI for display
810
+ if (tuiStore)
811
+ tuiStore.addUserMessage(input);
611
812
  currentAbort = new AbortController();
612
- const traceId = (0, nerves_1.createTraceId)();
613
- 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
+ }
614
823
  let result;
615
824
  try {
616
- result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
617
- toolChoiceRequired: getEffectiveToolChoiceRequired(),
618
- traceId,
619
- tools: options.tools,
620
- execTool: wrappedExecTool,
621
- toolContext: options.toolContext,
622
- });
825
+ if (options.runTurn) {
826
+ // Pipeline-based turn: the runTurn callback handles user message assembly,
827
+ // pending drain, trust gate, runAgent, postTurn, and token accumulation.
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
+ }
838
+ }
839
+ else {
840
+ // Legacy path: inline runAgent (used by serpent guide and tests)
841
+ const prefix = options.getContentPrefix?.();
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);
848
+ const traceId = (0, nerves_1.createTraceId)();
849
+ result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
850
+ toolChoiceRequired: getEffectiveToolChoiceRequired(),
851
+ traceId,
852
+ tools: options.tools,
853
+ execTool: wrappedExecTool,
854
+ toolContext: effectiveToolContext,
855
+ });
856
+ }
623
857
  }
624
858
  catch (err) {
625
- // AbortError (Ctrl-C) -- silently return to prompt
626
- // All other errors: show the user what happened
627
859
  if (!(err instanceof DOMException && err.name === "AbortError")) {
628
- 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));
629
861
  }
630
862
  }
631
863
  cliCallbacks.flushMarkdown();
632
- 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();
633
869
  currentAbort = null;
634
870
  // Check if exit tool was fired during this turn
635
871
  if (exitToolFired) {
@@ -639,20 +875,35 @@ async function runCliSession(options) {
639
875
  // Safety net: never silently swallow an empty response
640
876
  const lastMsg = messages[messages.length - 1];
641
877
  if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
642
- process.stderr.write("\x1b[33m(empty response)\x1b[0m\n");
878
+ display.warn("(empty response)");
643
879
  }
644
- process.stdout.write("\n\n");
645
880
  // Post-turn hook (session persistence, pending drain, prompt refresh, etc.)
646
881
  if (options.onTurnEnd) {
647
882
  await options.onTurnEnd(messages, result ?? { usage: undefined });
648
883
  }
649
884
  if (closed)
650
885
  break;
651
- process.stdout.write("\x1b[36m> \x1b[0m");
652
886
  }
653
887
  }
654
888
  finally {
655
- 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
+ }
656
907
  if (options.banner !== false) {
657
908
  // eslint-disable-next-line no-console -- terminal UX: goodbye
658
909
  console.log("bye");
@@ -665,17 +916,32 @@ async function main(agentName, options) {
665
916
  if (agentName)
666
917
  (0, identity_1.setAgentName)(agentName);
667
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);
668
934
  // Fallback: apply pending updates for daemon-less direct CLI usage
669
935
  (0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
936
+ (0, update_hooks_1.registerUpdateHook)(agent_config_v2_1.agentConfigV2Hook);
670
937
  await (0, update_hooks_1.applyPendingUpdates)((0, identity_1.getAgentBundlesRoot)(), (0, bundle_manifest_1.getPackageVersion)());
671
938
  // Fail fast if provider is misconfigured (triggers human-readable error + exit)
672
- (0, core_1.getProvider)();
939
+ (0, core_1.getProvider)("human");
673
940
  // Resolve context kernel (identity + channel) for CLI
674
941
  const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
675
942
  const friendStore = new store_file_1.FileFriendStore(friendsPath);
676
943
  const username = os.userInfo().username;
677
- const hostname = os.hostname();
678
- const localExternalId = `${username}@${hostname}`;
944
+ const localExternalId = username;
679
945
  const resolver = new resolver_1.FriendResolver(friendStore, {
680
946
  provider: "local",
681
947
  externalId: localExternalId,
@@ -683,13 +949,6 @@ async function main(agentName, options) {
683
949
  channel: "cli",
684
950
  });
685
951
  const resolvedContext = await resolver.resolve();
686
- const cliToolContext = {
687
- /* v8 ignore next -- CLI has no OAuth sign-in; this no-op satisfies the interface @preserve */
688
- signin: async () => undefined,
689
- context: resolvedContext,
690
- friendStore,
691
- summarize: (0, core_1.createSummarize)(),
692
- };
693
952
  const friendId = resolvedContext.friend.id;
694
953
  const agentConfig = (0, identity_1.loadAgentConfig)();
695
954
  (0, cli_logging_1.configureCliRuntimeLogger)(friendId, {
@@ -712,59 +971,138 @@ async function main(agentName, options) {
712
971
  }
713
972
  // Load existing session or start fresh
714
973
  const existing = (0, context_1.loadSession)(sessPath);
974
+ let sessionState = existing?.state;
975
+ let sessionEvents = existing?.events ?? [];
976
+ const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
715
977
  const sessionMessages = existing?.messages && existing.messages.length > 0
716
978
  ? existing.messages
717
- : [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", undefined, resolvedContext) }];
718
- // Pending queue drain: inject pending messages as harness-context + assistant-content pairs
719
- const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "cli", "session");
720
- const drainToMessages = () => {
721
- const pending = (0, pending_1.drainPending)(pendingDir);
722
- if (pending.length === 0)
723
- return 0;
724
- for (const msg of pending) {
725
- sessionMessages.push({ role: "user", name: "harness", content: `[proactive message from ${msg.from}]` });
726
- sessionMessages.push({ role: "assistant", content: msg.content });
727
- }
728
- return pending.length;
729
- };
730
- // Startup drain: deliver offline messages
731
- const startupCount = drainToMessages();
732
- if (startupCount > 0) {
733
- (0, context_1.saveSession)(sessPath, sessionMessages);
734
- }
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);
982
+ // Per-turn pipeline input: CLI capabilities and pending dir
983
+ const cliCapabilities = (0, channel_1.getChannelCapabilities)("cli");
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 };
735
988
  try {
736
989
  await runCliSession({
737
- agentName: (0, identity_1.getAgentName)(),
990
+ agentName: currentAgentName,
738
991
  pasteDebounceMs,
739
992
  messages: sessionMessages,
740
- toolContext: cliToolContext,
741
- onInput: () => {
742
- const trustGate = (0, trust_gate_1.enforceTrustGate)({
743
- friend: resolvedContext.friend,
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) => {
1002
+ // Run the full per-turn pipeline: resolve -> gate -> session -> drain -> runAgent -> postTurn -> tokens
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 */
1026
+ const result = await (0, pipeline_1.handleInboundTurn)({
1027
+ channel: "cli",
1028
+ sessionKey: "session",
1029
+ capabilities: cliCapabilities,
1030
+ messages: [{ role: "user", content: userContent ?? userInput }],
1031
+ continuityIngressTexts: getCliContinuityIngressTexts(userInput),
1032
+ callbacks: failoverAwareCallbacks,
1033
+ friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
1034
+ sessionLoader: {
1035
+ loadOrCreate: () => Promise.resolve({
1036
+ messages,
1037
+ sessionPath: sessPath,
1038
+ state: sessionState,
1039
+ events: sessionEvents,
1040
+ }),
1041
+ },
1042
+ pendingDir,
1043
+ friendStore,
744
1044
  provider: "local",
745
1045
  externalId: localExternalId,
746
- channel: "cli",
1046
+ enforceTrustGate: trust_gate_1.enforceTrustGate,
1047
+ drainPending: pending_1.drainPending,
1048
+ drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(currentAgentName, deferredFriendId),
1049
+ runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
1050
+ ...opts,
1051
+ toolContext: {
1052
+ /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
1053
+ signin: async () => undefined,
1054
+ ...opts?.toolContext,
1055
+ summarize,
1056
+ },
1057
+ }),
1058
+ 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);
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
+ });
1067
+ },
1068
+ accumulateFriendTokens: tokens_1.accumulateFriendTokens,
1069
+ signal,
1070
+ runAgentOptions: {
1071
+ toolChoiceRequired: (0, commands_1.getToolChoiceRequired)(),
1072
+ traceId: (0, nerves_1.createTraceId)(),
1073
+ mcpManager,
1074
+ toolContext,
1075
+ },
1076
+ failoverState,
747
1077
  });
748
- if (!trustGate.allowed) {
749
- return {
750
- allowed: false,
751
- reply: trustGate.reason === "stranger_first_reply" ? trustGate.autoReply : undefined,
752
- };
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`);
753
1082
  }
754
- return { allowed: true };
755
- },
756
- onTurnEnd: async (msgs, result) => {
757
- (0, context_1.postTurn)(msgs, sessPath, result.usage);
758
- await (0, tokens_1.accumulateFriendTokens)(friendStore, resolvedContext.friend.id, result.usage);
759
- drainToMessages();
760
- await (0, prompt_refresh_1.refreshSystemPrompt)(msgs, "cli", undefined, resolvedContext);
761
- },
762
- onNewSession: () => {
763
- (0, context_1.deleteSession)(sessPath);
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 */
1088
+ // Handle gate rejection: display auto-reply if present
1089
+ if (!result.gateResult.allowed) {
1090
+ if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
1091
+ process.stdout.write(`${result.gateResult.autoReply}\n`);
1092
+ }
1093
+ }
1094
+ return { usage: result.usage, turnOutcome: result.turnOutcome, commandAction: result.commandAction };
764
1095
  },
765
1096
  });
766
1097
  }
767
1098
  finally {
768
1099
  sessionLock?.release();
769
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);
770
1107
  }
1108
+ // CI trigger