@ouro.bot/cli 0.1.0-alpha.58 → 0.1.0-alpha.581

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