elanous 0.2.31 → 0.2.32

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 (282) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1834-50625904996a8fa8.js → 1834-98b64172bc1efe77.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/69-0a2c8209c6d4a158.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/8097-9530c03bcc550f02.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/approvals/page-504204b58c82c253.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3535a4a69f1fafaa.js → page-a9717a67c2eecbaf.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/board/page-5c9ec30aee62dfbf.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/botlab/page-584191533721fa16.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/bots/page-73c4b4bb3ac7ab5f.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/ceo/{page-5a503541353e60a8.js → page-c0f1ca371f710b06.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/chat/{page-6025101384e361a5.js → page-46430d45a9e04eac.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/consult/page-3b2ebf1dc64ff0be.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/control/{page-a645f00a06483e9a.js → page-a27b1e5a0d1b308f.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/decisions/page-25654b82094f7d22.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-e9bab7413b028b28.js → page-8ea49a4c6dd2c6e7.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/editor/page-ce5113aefea0a8d1.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-e547e6ce55f1fb33.js → page-0e5a41fac1c75396.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-d27e1b9850fd54bf.js → page-31aaf1ed37c6df9a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-3dcc1fa411e1f4e0.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-0fec29c7ba01894b.js → page-c69247e7134d329d.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-339a07410cd6f36b.js → layout-66dc5cd3a0999abe.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-26ff7ff651a7217d.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/live-run/page-617b3976981efa96.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/loops/{page-2fd93ed8c9f16580.js → page-d256f5d5d9552823.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/market/{page-9b564dbff0fefc35.js → page-ce8f1907b8a6101c.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-33e182a316263116.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/morning/{page-814ba51385225776.js → page-32866485913d6a04.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/{not-found-397d479626e5a459.js → not-found-7607cca9b84950c6.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-af1bba4e3251b728.js → page-eeab86176b03dc3b.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-3ad3be5e7d3fdce9.js → page-5b93dbae3432b996.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-77a6cb90a9cf2b10.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-4da93ad35d4a8d28.js → page-f47e768b5b242b5a.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/outputs/page-eb41aa3367fc2505.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/page-4d13ab0b8a267e3f.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/reflection/page-84dad32ff0fc0831.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-3a658d5fa6d16a25.js → page-95306f892aa2d0ee.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-0157de6e85f8bb6b.js → page-940fb6c217f6b470.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-0a07363b130182a1.js → page-8c66a3b24a745630.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/settings/{page-841d73d4ac44ed49.js → page-7b675dfd35c36fc9.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-6f43a3d1146e6377.js → page-8bd0d3cf7665bcfc.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/setup/{page-521ee23ac8c0abfa.js → page-c219f2ed171131be.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/share/{page-a9992ee285efaa83.js → page-40f8fa21bdae955b.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-3d48e85fa89ec04f.js → page-468f8313e4f1fbd9.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/tasks/page-7bbb8fa353a246c4.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/term/page-04d00ae9b0890349.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/today/{page-233c71cd5f68f0d7.js → page-4214b855d9233ea8.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/trace/{page-70151d78de2f375e.js → page-936752c7aa58762e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/vault/{page-dcac33a3e2cb72c0.js → page-ec052ad359df1797.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-8305570551091338.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-0448145ccd5c45c1.js → page-fad650e4793adb65.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/workspace/page-d90e64743826e1d8.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-a16bcfd49af86a32.js +1 -0
  54. package/apps/pwa/out/_next/static/css/b5d88eba78a3d7bf.css +1 -0
  55. package/apps/pwa/out/approvals/index.html +1 -1
  56. package/apps/pwa/out/approvals/index.txt +14 -14
  57. package/apps/pwa/out/autopilot/index.html +1 -1
  58. package/apps/pwa/out/autopilot/index.txt +14 -14
  59. package/apps/pwa/out/board/index.html +1 -1
  60. package/apps/pwa/out/board/index.txt +14 -14
  61. package/apps/pwa/out/botlab/index.html +1 -1
  62. package/apps/pwa/out/botlab/index.txt +14 -14
  63. package/apps/pwa/out/bots/index.html +1 -1
  64. package/apps/pwa/out/bots/index.txt +14 -14
  65. package/apps/pwa/out/ceo/index.html +1 -1
  66. package/apps/pwa/out/ceo/index.txt +14 -14
  67. package/apps/pwa/out/chat/index.html +1 -1
  68. package/apps/pwa/out/chat/index.txt +14 -14
  69. package/apps/pwa/out/consult/index.html +1 -1
  70. package/apps/pwa/out/consult/index.txt +14 -14
  71. package/apps/pwa/out/control/index.html +1 -1
  72. package/apps/pwa/out/control/index.txt +14 -14
  73. package/apps/pwa/out/decisions/index.html +1 -1
  74. package/apps/pwa/out/decisions/index.txt +14 -14
  75. package/apps/pwa/out/design-check/index.html +1 -1
  76. package/apps/pwa/out/design-check/index.txt +14 -14
  77. package/apps/pwa/out/editor/index.html +1 -1
  78. package/apps/pwa/out/editor/index.txt +14 -14
  79. package/apps/pwa/out/exec/index.html +1 -1
  80. package/apps/pwa/out/exec/index.txt +14 -14
  81. package/apps/pwa/out/field/index.html +1 -1
  82. package/apps/pwa/out/field/index.txt +14 -14
  83. package/apps/pwa/out/healthz +1 -1
  84. package/apps/pwa/out/index.html +1 -1
  85. package/apps/pwa/out/index.txt +14 -14
  86. package/apps/pwa/out/inside/index.html +1 -1
  87. package/apps/pwa/out/inside/index.txt +14 -14
  88. package/apps/pwa/out/intake/index.html +1 -1
  89. package/apps/pwa/out/intake/index.txt +14 -14
  90. package/apps/pwa/out/live/index.html +1 -1
  91. package/apps/pwa/out/live/index.txt +17 -14
  92. package/apps/pwa/out/live-run/index.html +1 -1
  93. package/apps/pwa/out/live-run/index.txt +14 -14
  94. package/apps/pwa/out/loops/index.html +1 -1
  95. package/apps/pwa/out/loops/index.txt +14 -14
  96. package/apps/pwa/out/market/index.html +1 -1
  97. package/apps/pwa/out/market/index.txt +14 -14
  98. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  99. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  100. package/apps/pwa/out/morning/index.html +1 -1
  101. package/apps/pwa/out/morning/index.txt +14 -14
  102. package/apps/pwa/out/observatory/index.html +1 -1
  103. package/apps/pwa/out/observatory/index.txt +14 -14
  104. package/apps/pwa/out/ops/checklist/index.html +1 -1
  105. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  106. package/apps/pwa/out/ops/release/index.html +1 -1
  107. package/apps/pwa/out/ops/release/index.txt +14 -14
  108. package/apps/pwa/out/ops/seats/index.html +1 -1
  109. package/apps/pwa/out/ops/seats/index.txt +14 -14
  110. package/apps/pwa/out/outputs/index.html +1 -1
  111. package/apps/pwa/out/outputs/index.txt +14 -14
  112. package/apps/pwa/out/reflection/index.html +1 -1
  113. package/apps/pwa/out/reflection/index.txt +15 -15
  114. package/apps/pwa/out/scheduler/index.html +1 -1
  115. package/apps/pwa/out/scheduler/index.txt +14 -14
  116. package/apps/pwa/out/sessions/index.html +1 -1
  117. package/apps/pwa/out/sessions/index.txt +14 -14
  118. package/apps/pwa/out/settings/devices/index.html +1 -1
  119. package/apps/pwa/out/settings/devices/index.txt +14 -14
  120. package/apps/pwa/out/settings/index.html +1 -1
  121. package/apps/pwa/out/settings/index.txt +14 -14
  122. package/apps/pwa/out/setup/done/index.html +1 -1
  123. package/apps/pwa/out/setup/done/index.txt +14 -14
  124. package/apps/pwa/out/setup/index.html +1 -1
  125. package/apps/pwa/out/setup/index.txt +14 -14
  126. package/apps/pwa/out/share/index.html +1 -1
  127. package/apps/pwa/out/share/index.txt +14 -14
  128. package/apps/pwa/out/showroom/index.html +1 -1
  129. package/apps/pwa/out/showroom/index.txt +14 -14
  130. package/apps/pwa/out/tasks/index.html +1 -1
  131. package/apps/pwa/out/tasks/index.txt +14 -14
  132. package/apps/pwa/out/term/index.html +1 -1
  133. package/apps/pwa/out/term/index.txt +14 -14
  134. package/apps/pwa/out/today/index.html +1 -1
  135. package/apps/pwa/out/today/index.txt +14 -14
  136. package/apps/pwa/out/trace/index.html +1 -1
  137. package/apps/pwa/out/trace/index.txt +14 -14
  138. package/apps/pwa/out/vault/index.html +1 -1
  139. package/apps/pwa/out/vault/index.txt +14 -14
  140. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  141. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  142. package/apps/pwa/out/workflows/index.html +1 -1
  143. package/apps/pwa/out/workflows/index.txt +14 -14
  144. package/apps/pwa/out/workspace/index.html +1 -1
  145. package/apps/pwa/out/workspace/index.txt +14 -14
  146. package/apps/pwa/out/worktrees/index.html +1 -1
  147. package/apps/pwa/out/worktrees/index.txt +14 -14
  148. package/graphs/implement-loop.yaml +5 -0
  149. package/package.json +1 -1
  150. package/scripts/ci-typecheck-changed.ts +26 -1
  151. package/scripts/measure-si-task-cpu.ts +143 -0
  152. package/scripts/ratchets/test-interference-patterns.allowlist.txt +0 -2
  153. package/scripts/release-loop/prepare-node.ts +102 -4
  154. package/src/acp/core-turn-bridge.ts +16 -4
  155. package/src/acp/elanous-extensions.ts +3 -0
  156. package/src/acp/server.ts +3 -0
  157. package/src/acp/tool-trace.ts +186 -11
  158. package/src/agent/self-ambient-cache.ts +156 -0
  159. package/src/agent/self-ambient.ts +8 -1
  160. package/src/agent/task-notification.ts +5 -0
  161. package/src/agent-substrate/pr-reviewer.ts +3 -1
  162. package/src/ask-user-question/tool.ts +22 -0
  163. package/src/autopilot/build/llm-conflict-merge.ts +56 -10
  164. package/src/autopilot/pr-manager.ts +23 -5
  165. package/src/capture/frame-state-detect.ts +39 -0
  166. package/src/capture/tui-self-report.ts +134 -6
  167. package/src/chat/index.ts +61 -15
  168. package/src/chat/pickers/modal-runtime.ts +6 -0
  169. package/src/chat/pickers/modals.ts +8 -2
  170. package/src/cli/ask-bare.ts +1 -0
  171. package/src/cli/import-cli.ts +12 -0
  172. package/src/cli/log-event-names.ts +10 -0
  173. package/src/cli/msg-cli.ts +1 -0
  174. package/src/cli/pod-cli.ts +14 -3
  175. package/src/cli/pr-cli.ts +67 -32
  176. package/src/cli/pr-land-base-read.ts +34 -0
  177. package/src/cli/pr-land-heavy-remote.ts +81 -25
  178. package/src/cli/pr-land-overlap-evidence.ts +46 -2
  179. package/src/cli/pr-land-pod-shadow-job.ts +26 -3
  180. package/src/cli/pr-land-refusal.ts +20 -5
  181. package/src/cli/pty-takeover-cli.ts +88 -14
  182. package/src/cli/sim-cli.ts +18 -0
  183. package/src/context-bus/ops-events.ts +6 -2
  184. package/src/core-turn/run-core-turn.ts +1 -0
  185. package/src/core-turn/run-goal-loop.ts +21 -0
  186. package/src/core-turn/types.ts +3 -0
  187. package/src/dashboard/acp-active-model.ts +42 -1
  188. package/src/dashboard/background-jobs-status.ts +66 -0
  189. package/src/dashboard/index.ts +117 -44
  190. package/src/dashboard/input/agent-wake.ts +242 -0
  191. package/src/dashboard/input/chat-main-entry-runtime.ts +5 -0
  192. package/src/dashboard/input/external-input-host.ts +23 -9
  193. package/src/dashboard/input/mouse-wiring.ts +108 -11
  194. package/src/dashboard/log-click-runtime.ts +118 -2
  195. package/src/dashboard/turn-preamble.ts +116 -40
  196. package/src/dashboard/turn-session-layers.ts +4 -2
  197. package/src/dashboard/turn-start-segments.ts +222 -0
  198. package/src/display/prompt-row-owner.ts +146 -0
  199. package/src/display/resize-listener.ts +33 -2
  200. package/src/fold-stack.ts +4 -2
  201. package/src/git-fs/index.ts +263 -36
  202. package/src/harness/gate-reserve.ts +87 -0
  203. package/src/harness/harness-incidents.ts +13 -0
  204. package/src/harness/harness-queue.ts +15 -0
  205. package/src/index.ts +8 -6
  206. package/src/input/external-input-queue.ts +34 -9
  207. package/src/llm/route-decision.ts +36 -0
  208. package/src/llm/tool-loop-steered-rejection.ts +8 -0
  209. package/src/llm-request-shape.ts +209 -0
  210. package/src/llm.ts +166 -267
  211. package/src/loops/orchestrator/tick.ts +24 -2
  212. package/src/maturity/feature-maturity.ts +1 -1
  213. package/src/mss/logging/render-categories.ts +5 -0
  214. package/src/ops/overview.ts +59 -2
  215. package/src/pr-tool/census.ts +19 -4
  216. package/src/pr-tool/inflight-registry.ts +143 -0
  217. package/src/pty-shell/pty-manifest.ts +12 -3
  218. package/src/pty-shell/tui-run-state.ts +34 -3
  219. package/src/seat-loop/traffic-signal.ts +2 -1
  220. package/src/self-dev/ask-launch-flow.ts +13 -0
  221. package/src/self-dev/draft-residue.ts +243 -24
  222. package/src/self-dev/shard-plan.ts +11 -3
  223. package/src/self-dev/shard-run.ts +103 -10
  224. package/src/self-implement/elanous-tui-spawn.ts +14 -0
  225. package/src/self-implement/follow-up-placement.ts +79 -0
  226. package/src/self-implement/gate-artifacts.ts +15 -4
  227. package/src/self-implement/gate-baseline.ts +96 -5
  228. package/src/self-implement/gate-cli.ts +7 -2
  229. package/src/self-implement/gate-remote.ts +26 -1
  230. package/src/self-implement/host-regate.ts +36 -4
  231. package/src/self-implement/orchestrator.ts +101 -11
  232. package/src/self-implement/run-ledger.ts +16 -14
  233. package/src/session-runtime/index.ts +22 -267
  234. package/src/shell-runner/background-surface.ts +15 -5
  235. package/src/skills/tools/agent.ts +39 -6
  236. package/src/skills/tools/list-dir.ts +11 -0
  237. package/src/skills/tools/schedule-wakeup.ts +408 -0
  238. package/src/skills/tools/shell.ts +245 -3
  239. package/src/status/bar.ts +26 -0
  240. package/src/steward/digest-delivery.ts +118 -0
  241. package/src/steward/hold-hygiene.ts +120 -0
  242. package/src/steward/triage.ts +10 -0
  243. package/src/surface/shell-runner-boot.ts +5 -2
  244. package/src/task-agent/task-hand.ts +24 -0
  245. package/src/task-orchestrator/surfaces/pod-command-job.ts +65 -11
  246. package/src/task-orchestrator/surfaces/pod-lease.ts +98 -0
  247. package/src/task-orchestrator/surfaces/pod-ledger-events.ts +1 -1
  248. package/src/task-orchestrator/surfaces/pod-pool.ts +3 -1
  249. package/src/task-orchestrator/surfaces/pod-reuse-cleanup.ts +10 -6
  250. package/src/task-orchestrator/surfaces/pod-warm-worker-pod.ts +525 -0
  251. package/src/task-orchestrator/surfaces/pod-warm-worker-runner.ts +90 -0
  252. package/src/task-orchestrator/surfaces/pod-warm-worker-runtime.ts +156 -2
  253. package/src/task-orchestrator/surfaces/self-implement-pod.ts +7 -5
  254. package/src/tui/chat/execution-badge.ts +5 -1
  255. package/src/tui/input-lag-probe.ts +230 -0
  256. package/src/tui.ts +4 -0
  257. package/src/ux-sim/scenario.ts +255 -0
  258. package/src/version/packed-revision.json +1 -1
  259. package/apps/pwa/out/_next/static/chunks/8097-92f87bd108322f66.js +0 -1
  260. package/apps/pwa/out/_next/static/chunks/app/approvals/page-bf3634a117322df7.js +0 -1
  261. package/apps/pwa/out/_next/static/chunks/app/board/page-b9f51447f757fbf2.js +0 -1
  262. package/apps/pwa/out/_next/static/chunks/app/botlab/page-2ebb8fd59b9461f9.js +0 -1
  263. package/apps/pwa/out/_next/static/chunks/app/bots/page-a2fc4e49a4fa4ba8.js +0 -1
  264. package/apps/pwa/out/_next/static/chunks/app/consult/page-f4b630cc8b5273cd.js +0 -1
  265. package/apps/pwa/out/_next/static/chunks/app/decisions/page-61ac22cbab1ee4ec.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/app/editor/page-c992eafe028b642e.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/app/inside/page-805ac2e137b7a121.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/app/live/page-349f2752d8cd37ad.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/live-run/page-d5c6dcf31c25cecc.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-62f4475d3c38fb9b.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-f219ccdbc0bf27df.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/outputs/page-456666766e5ce6fc.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/page-7404e377e8adb8c5.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/reflection/page-14456240ed2a36ad.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d2ced71132fac9b2.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/term/page-f08358bdf251adb9.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-149dc1caba411bb9.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9cb0c444903da65d.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-17e504c396e0e389.js +0 -1
  280. package/apps/pwa/out/_next/static/css/3f5fc6f96e2cf7b2.css +0 -1
  281. /package/apps/pwa/out/_next/static/{LSXfY0u9QtfDbejY0Ukf_ → IVKr3AlRUVLKsw2fh7eXF}/_buildManifest.js +0 -0
  282. /package/apps/pwa/out/_next/static/{LSXfY0u9QtfDbejY0Ukf_ → IVKr3AlRUVLKsw2fh7eXF}/_ssgManifest.js +0 -0
@@ -5,7 +5,7 @@ import { dirname, join, resolve } from 'node:path';
5
5
  import { runGitCommand } from '../git-fs/runner.js';
6
6
  import { debug } from '../debug/log.js';
7
7
  import { getPty, listPty, renamePty, requestPtyTakeover } from '../pty-shell/registry.js';
8
- import { describePtyRunState, readCheckoutHead, readCurrentInstallRecord, shortSha, type InstalledRecord, type PtyRunStateView } from '../pty-shell/tui-run-state.js';
8
+ import { describePtyRunState, describeTurnCell, formatTurnCell, ptyTurnViewOfRow, readCheckoutHead, readCurrentInstallRecord, shortSha, UNKNOWN_TURN_CELL, type InstalledRecord, type PtyRunStateView, type PtyTurnCell } from '../pty-shell/tui-run-state.js';
9
9
  import { getPtyManifest, isProcessAlive, listPtyManifest, listPtyManifestRows, listPtyManifestRowsAt, ptyManifestDbPath, readPtyManifestFrame, reapDeadPtyManifest, reapDeadPtyManifestAt, reapStalePtyManifest, removePtyManifest, PTY_MANIFEST_CLOSED_TTL_MS, PTY_MANIFEST_STALE_MS, type PtyManifestExternalReapResult, type PtyManifestRow } from '../pty-shell/pty-manifest.js';
10
10
  import { readPtyEventsAfter, readPtyEventsAfterAt, type PtyEventLogReadResult, type PtyEventRow } from '../pty-shell/pty-event-log.js';
11
11
  import { resolvePtyRef, type PtyRefItem } from '../pty-shell/pty-ref.js';
@@ -27,7 +27,7 @@ import { isRunStatus } from '../self-implement/run-status-mapping.js';
27
27
  import { terminalTreeLabel } from '../pty-shell/terminal-tree.js';
28
28
  import { RemotesStore } from './remotes.js';
29
29
  import { bookmarkAttachDefaults } from './remote-resolve.js';
30
- import { classifyFrameState, type FrameState } from '../capture/frame-state-detect.js';
30
+ import { classifyFrameState, reconcileFrameStateWithTurn, type FrameState } from '../capture/frame-state-detect.js';
31
31
  import { installMissingTool, waitForPtyCompletion, type ToolInstallDeps, type ToolInstallResult } from '../agent-mission/tool-install.js';
32
32
 
33
33
  interface PtyTakeoverCommandResult { readonly exitCode: 0 | 1 | 2; readonly message: string; readonly notice?: string; }
@@ -1441,6 +1441,10 @@ interface PtyListJsonRow extends PtyWebAddress {
1441
1441
  * 그 값은 거의 항상 몇 초이고 「얼마나 오래 있었나」를 못 답한다.
1442
1442
  * ⚠️ `startedAt` 이 0(미상)이면 이 칸을 «비운다» — 「0분」과 「못 잼」은 다른 값이다. */
1443
1443
  readonly ageMs?: number;
1444
+ /** ⭐ H10 — TUI 가 스스로 적은 턴(화면 추정 아님) ⊕ 그 상태 경과 초. 턴 기록 없는 행·원격 행은 `state:'unknown'`. */
1445
+ readonly turn: PtyTurnCell;
1446
+ /** ⭐ H10 — 기동 판(실행판 sha) ⊕ 낡음. 판 기록 없는 행·판독 실패는 null. */
1447
+ readonly build: { readonly version: string; readonly sha: string; readonly source: string; readonly stale: boolean | null; readonly staleReason: string } | null;
1444
1448
  }
1445
1449
 
1446
1450
  function defaultRunTermination(runId: string, opts: { includeTest: boolean }, log: (event: string, data: Record<string, unknown>) => void): RunTermination {
@@ -1457,7 +1461,7 @@ function ptyAgeField(startedAt: number, now: number): { ageMs?: number } {
1457
1461
  return ageMs >= 0 ? { ageMs } : {};
1458
1462
  }
1459
1463
 
1460
- function ptyListJsonRow(row: PtyManifestRow, instance: string, sourceRoot: PtyManifestSourceRoot, deps: PtyTakeoverCommandDeps, opts: { includeTest: boolean }, pwa?: NexusPwaResolution): PtyListJsonRow {
1464
+ function ptyListJsonRow(row: PtyManifestRow, instance: string, sourceRoot: PtyManifestSourceRoot, deps: PtyTakeoverCommandDeps, opts: { includeTest: boolean }, pwa?: NexusPwaResolution, installed: () => InstalledRecord = () => deps.readInstalledBuild?.() ?? {}): PtyListJsonRow {
1461
1465
  // ⛔⭐ 리뷰 should-fix (2026-08-19): 기준 시각을 «한 번만» 읽는다.
1462
1466
  // 종전엔 proof(updatedAgeMs)와 ageMs 가 각각 `deps.now?.()` 를 불러, 한 행의 두 값이
1463
1467
  // ***«다른 순간»을 가리킬 수 있었다***. 두 값을 나란히 놓고 판정하는 것이 이 축의 용도인데
@@ -1498,9 +1502,56 @@ function ptyListJsonRow(row: PtyManifestRow, instance: string, sourceRoot: PtyMa
1498
1502
  runTerminationSource: resolution?.source ?? 'unknown',
1499
1503
  runStoreIo: resolution?.runStoreIo ?? 'not-checked',
1500
1504
  ownerRunUsage,
1505
+ ...ptyListRunFields(row, deps, observedAt, installed),
1501
1506
  };
1502
1507
  }
1503
1508
 
1509
+ /** `pty list --json` 의 턴·판 칸 — ⛔ 키는 «언제나» 싣는다(값으로만 갈린다 · 모르면 unknown/null).
1510
+ * ⭐ 턴은 행의 자기보고 값만 읽는다 — 판 판독(설치본·git)이 실패해도 기록된 턴은 버리지 않는다. */
1511
+ function ptyListRunFields(row: PtyManifestRow, deps: PtyTakeoverCommandDeps, observedAt: number, installed: () => InstalledRecord): Pick<PtyListJsonRow, 'turn' | 'build'> {
1512
+ const turn = describeTurnCell(ptyTurnViewOfRow(row), observedAt);
1513
+ try {
1514
+ if (!row.build) return { turn, build: null };
1515
+ // 판독 예외 계약: 판 sha 는 기록값이라 그대로 두고 낡음만 «모른다»(stale=null · unknown) ⊕ `list-build-read-failed` 관측.
1516
+ // 그 밖의 뜻밖 예외는 판을 null 로(아래 catch) — 어느 쪽이든 기록된 턴은 버리지 않는다.
1517
+ const run = describePtyRunState(row, {
1518
+ readInstalled: installed,
1519
+ readCheckoutHead: (codeRoot) => {
1520
+ try { return deps.readCheckoutHead?.(codeRoot); } catch (error) {
1521
+ deps.log('list-build-read-failed', { ptyId: row.id, source: 'checkout-head', error: error instanceof Error ? error.message : String(error) });
1522
+ return undefined;
1523
+ }
1524
+ },
1525
+ });
1526
+ return { turn, build: run.build ? { version: run.build.version, sha: run.build.sha, source: run.build.source, stale: run.stale, staleReason: run.staleReason } : null };
1527
+ } catch (error) {
1528
+ deps.log('list-build-read-failed', { ptyId: row.id, source: 'describe', error: error instanceof Error ? error.message : String(error) });
1529
+ return { turn, build: null };
1530
+ }
1531
+ }
1532
+
1533
+ /** 목록 한 번에 설치본 기록을 «한 번만» 읽는다. 예외는 «모른다»({}) ⊕ `list-build-read-failed`(목록당 한 줄) — 목록·턴 칸을 깨지 않는다. */
1534
+ function listInstalledReader(deps: PtyTakeoverCommandDeps): () => InstalledRecord {
1535
+ let once: InstalledRecord | undefined;
1536
+ return () => {
1537
+ if (once === undefined) {
1538
+ try { once = deps.readInstalledBuild?.() ?? {}; } catch (error) {
1539
+ once = {};
1540
+ deps.log('list-build-read-failed', { source: 'installed', error: error instanceof Error ? error.message : String(error) });
1541
+ }
1542
+ }
1543
+ return once;
1544
+ };
1545
+ }
1546
+
1547
+ /** 글 표의 판 칸 — 판독이 어떻게 실패해도 그 행(과 뒤따르는 턴 칸)을 깨지 않는다: 판 칸만 비우고 관측 한 줄. */
1548
+ function safeListRunColumn(row: PtyManifestRow | undefined, deps: PtyTakeoverCommandDeps, installed: () => InstalledRecord): string {
1549
+ try { return formatListRunColumn(row, deps, installed); } catch (error) {
1550
+ deps.log('list-build-read-failed', { ptyId: row?.id ?? '', source: 'run-column', error: error instanceof Error ? error.message : String(error) });
1551
+ return '';
1552
+ }
1553
+ }
1554
+
1504
1555
  function federatedManifestTargets(
1505
1556
  targets: readonly { name: string; dbPath: string }[],
1506
1557
  currentDbPath: string,
@@ -1771,6 +1822,9 @@ function remotePtyListJsonRow(row: RemoteTerminalListItem): PtyListJsonRow {
1771
1822
  runTerminationSource: 'unknown',
1772
1823
  runStoreIo: 'not-checked',
1773
1824
  ownerRunUsage: 'unknown',
1825
+ // 원격 응답은 턴·판을 안 싣는다 — «모른다»로(로컬과 같은 키).
1826
+ turn: UNKNOWN_TURN_CELL,
1827
+ build: null,
1774
1828
  };
1775
1829
  }
1776
1830
 
@@ -1893,9 +1947,11 @@ function runPtyListJson(deps: PtyTakeoverCommandDeps, opts: { all?: boolean; inc
1893
1947
  }
1894
1948
  // Keep the existing list contract: rows whose selected PTY/owner process is dead are not listable.
1895
1949
  const pwa = deps.resolveNexusPwa?.();
1950
+ // 설치본 기록은 목록 한 번에 한 번만 읽는다(행마다 다시 읽으면 I/O 와 행 사이 불일치). 실패는 «모른다» ⊕ 관측 한 줄.
1951
+ const installed = listInstalledReader(deps);
1896
1952
  const payload = rows
1897
1953
  .filter(({ row }) => row.alive && (row.ptyPid > 0 ? deps.isProcessAlive!(row.ptyPid) : deps.isProcessAlive!(row.ownerPid)))
1898
- .map(({ row, instance, sourceRoot }) => ptyListJsonRow(row, instance, sourceRoot, deps, { includeTest: opts.includeTest === true }, pwa));
1954
+ .map(({ row, instance, sourceRoot }) => ptyListJsonRow(row, instance, sourceRoot, deps, { includeTest: opts.includeTest === true }, pwa, installed));
1899
1955
  deps.log('list', { count: payload.length, federated: opts.all === true, includeTest: opts.includeTest === true, json: true });
1900
1956
  return { exitCode: 0, message: JSON.stringify(payload, null, 2), ...(payload.length === 0 ? { notice: ptyListEmptyNotice(opts) } : {}) };
1901
1957
  }
@@ -1966,12 +2022,15 @@ export function runPtyList(
1966
2022
  const pwa = deps.resolveNexusPwa?.();
1967
2023
  // 판 칸(맨 끝) — 설치본 기록은 목록 한 번에 한 번만 읽는다.
1968
2024
  // 설치본 판독기가 없는 구성(시험 deps)은 이 칸을 안 낸다 — 기계마다 다른 값이 목록에 새지 않게.
1969
- const manifestRowById = new Map((deps.readInstalledBuild ? deps.listManifestRows?.() ?? [] : []).map((row) => [row.id, row] as const));
1970
- let installedOnce: InstalledRecord | undefined;
1971
- const installed = (): InstalledRecord => (installedOnce ??= deps.readInstalledBuild?.() ?? {});
2025
+ // ⭐ H10 턴 칸은 판독기와 «무관하게» TUI ref 마다 낸다(기계마다 다른 값이 아니다) — 행을 못 얻으면 `turn=?`.
2026
+ let manifestRows: readonly PtyManifestRow[] = [];
2027
+ try { manifestRows = deps.listManifestRows?.() ?? []; } catch { manifestRows = []; }
2028
+ const manifestRowById = new Map(manifestRows.map((row) => [row.id, row] as const));
2029
+ const listedAt = deps.now?.() ?? Date.now();
2030
+ const installed = listInstalledReader(deps);
1972
2031
  const rendered = refs.map((ref) => {
1973
2032
  const provenance = (deps.resolveWorktreeProvenance ?? resolvePtyWorktreeProvenance)(ref.workdir);
1974
- return { known: ptyPurposeKnown(provenance, ref.purpose), line: `${ref.id}\t${ref.kind}\t${ref.nickname ?? '-'}\t${ref.source}\t${ref.alive ? 'alive' : 'dead'}\t${ref.mode ?? '?'}\t${ref.workdir ?? '-'}\t${ref.runId ?? '-'}${ownerRunUsageById.has(ref.id) ? `\t${ownerRunUsageById.get(ref.id)}` : ''}\t${formatTerminalOrigin(ref)}\t${formatPtyPurpose(provenance, ref.purpose)}${pwa ? `\t${formatPtyWebAddress(ptyWebAddress(ref.id, pwa))}` : ''}${formatListRunColumn(manifestRowById.get(ref.id), deps, installed)}` };
2033
+ return { known: ptyPurposeKnown(provenance, ref.purpose), line: `${ref.id}\t${ref.kind}\t${ref.nickname ?? '-'}\t${ref.source}\t${ref.alive ? 'alive' : 'dead'}\t${ref.mode ?? '?'}\t${ref.workdir ?? '-'}\t${ref.runId ?? '-'}${ownerRunUsageById.has(ref.id) ? `\t${ownerRunUsageById.get(ref.id)}` : ''}\t${formatTerminalOrigin(ref)}\t${formatPtyPurpose(provenance, ref.purpose)}${pwa ? `\t${formatPtyWebAddress(ptyWebAddress(ref.id, pwa))}` : ''}${safeListRunColumn(manifestRowById.get(ref.id), deps, installed)}${formatListTurnColumn(ref.kind, manifestRowById.get(ref.id), listedAt)}` };
1975
2034
  });
1976
2035
  return {
1977
2036
  exitCode: 0,
@@ -2067,24 +2126,30 @@ export async function runPtyState(ref: string, deps: PtyTakeoverCommandDeps = li
2067
2126
  if (run.turn === 'unknown' && run.build === null) return { exitCode: 1, message: formatSnapshotResult(acquired.id, acquired.control) };
2068
2127
  const screenUnavailable = acquired.control.reason ?? acquired.control.status;
2069
2128
  deps.log('state-screen-unavailable', { ptyId: acquired.id, reason: screenUnavailable, turn: run.turn });
2070
- return formatPtyStateResult(acquired.id, { state: 'unknown', label: null }, run, deps.now?.() ?? Date.now(), json, screenUnavailable);
2129
+ return formatPtyStateResult(acquired.id, { state: 'unknown', label: null }, run, deps.now?.() ?? Date.now(), json, deps, screenUnavailable);
2071
2130
  }
2072
2131
  const verdict = classifyFrameState(acquired.control.screen ?? '');
2073
2132
  const at = deps.now?.() ?? Date.now();
2074
2133
  if (run.turn === 'unknown' || run.stale === null) deps.log('state-run-unknown', { ptyId: acquired.id, turn: run.turn, staleReason: run.staleReason, hasBuild: run.build !== null });
2075
- return formatPtyStateResult(acquired.id, { state: verdict.state, label: verdict.matchedLabel }, run, at, json);
2134
+ return formatPtyStateResult(acquired.id, { state: verdict.state, label: verdict.matchedLabel }, run, at, json, deps);
2076
2135
  }
2077
2136
 
2078
- function formatPtyStateResult(id: string, screen: { state: string; label?: string | null }, run: PtyRunStateView, at: number, json: boolean, screenUnavailable?: string): PtyTakeoverCommandResult {
2137
+ // ⭐ H10 조각 1 — `state` 는 «하나»다: TUI 가 스스로 적은 턴이 있으면 그것이 바쁜가/한가한가를 정하고(`stateSource=turn`),
2138
+ // 화면 추정은 턴을 모를 때·턴과 모순 없는 더 구체적인 상태(blocked·waiting·done)일 때만 쓴다(`stateSource=screen`).
2139
+ // 화면 원값은 `screenState`(JSON) · `guess=`(글, 갈렸을 때만)로 남긴다 — 둘이 갈린 순간은 `state-reconciled` 관측.
2140
+ function formatPtyStateResult(id: string, screen: { state: FrameState; label?: string | null }, run: PtyRunStateView, at: number, json: boolean, deps: PtyTakeoverCommandDeps, screenUnavailable?: string): PtyTakeoverCommandResult {
2141
+ const reconciled = reconcileFrameStateWithTurn(screen.state, run.turn);
2142
+ if (reconciled.overridden) deps.log('state-reconciled', { ptyId: id, turn: run.turn, screenState: reconciled.screenState, label: screen.label ?? null, state: reconciled.state });
2143
+ const guess = reconciled.screenState !== reconciled.state && !screenUnavailable ? ` guess=${reconciled.screenState}` : '';
2079
2144
  return { exitCode: 0, message: json
2080
2145
  ? JSON.stringify({
2081
- id, state: screen.state, label: screen.label, at,
2146
+ id, state: reconciled.state, stateSource: reconciled.source, screenState: reconciled.screenState, label: screen.label, at,
2082
2147
  ...(screenUnavailable ? { screenUnavailable } : {}),
2083
2148
  turn: run.turn, ...(run.turnAt !== undefined ? { turnAt: run.turnAt } : {}),
2084
2149
  build: run.build ? { version: run.build.version, sha: run.build.sha, source: run.build.source } : null,
2085
2150
  installed: run.installed, stale: run.stale, staleReason: run.staleReason,
2086
2151
  })
2087
- : `pty state: ${id} state=${screen.state} label=${screen.label ?? '-'}${screenUnavailable ? ` screen=unavailable(${screenUnavailable})` : ''} turn=${run.turn} build=${formatRunBuild(run)} installed=${shortSha(run.installed)} stale=${run.stale ?? 'unknown'}${run.stale === null ? '' : ` (${run.staleReason})`}` };
2152
+ : `pty state: ${id} state=${reconciled.state} source=${reconciled.source}${guess} label=${screen.label ?? '-'}${screenUnavailable ? ` screen=unavailable(${screenUnavailable})` : ''} turn=${run.turn} build=${formatRunBuild(run)} installed=${shortSha(run.installed)} stale=${run.stale ?? 'unknown'}${run.stale === null ? '' : ` (${run.staleReason})`}` };
2088
2153
  }
2089
2154
 
2090
2155
  function ptyRunStateFor(id: string, deps: PtyTakeoverCommandDeps): PtyRunStateView {
@@ -2103,7 +2168,8 @@ function formatRunBuild(run: PtyRunStateView): string {
2103
2168
 
2104
2169
  /** `pty list` 한 칸 — 판을 적는 행(TUI)만. `run=<sha>` · 낡았으면 `,stale` · 옛 행 추정은 `run=?,stale`. */
2105
2170
  function formatListRunColumn(row: PtyManifestRow | undefined, deps: PtyTakeoverCommandDeps, installed: () => InstalledRecord): string {
2106
- if (!row || (!row.build && row.kind !== 'tui')) return '';
2171
+ // 설치본 판독기가 없는 구성(시험 deps)은 이 칸을 안 낸다 — 기계마다 다른 값이 목록에 새지 않게.
2172
+ if (!deps.readInstalledBuild || !row || (!row.build && row.kind !== 'tui')) return '';
2107
2173
  const run = describePtyRunState(row, {
2108
2174
  readInstalled: installed,
2109
2175
  ...(deps.readCheckoutHead ? { readCheckoutHead: deps.readCheckoutHead } : {}),
@@ -2112,6 +2178,14 @@ function formatListRunColumn(row: PtyManifestRow | undefined, deps: PtyTakeoverC
2112
2178
  return `\trun=${run.build?.sha ? shortSha(run.build.sha) : '?'}${run.stale === true ? ',stale' : ''}`;
2113
2179
  }
2114
2180
 
2181
+ /** ⭐ H10 «턴 칸» — ref 가 TUI 면 «언제나» 낸다: `turn=busy,42s` · `turn=idle,300s` · 행 없음·종류 불일치·기록 없음 `turn=?`.
2182
+ * OP·TF 가 화면(«Streaming…/완료»)을 긁지 않고 «지금 턴 중인가·얼마나 됐나»를 읽는다. 턴은 자기보고 값만 쓴다(설치본·git 무관). */
2183
+ function formatListTurnColumn(refKind: string, row: PtyManifestRow | undefined, now: number): string {
2184
+ if (refKind !== 'tui') return '';
2185
+ if (!row || row.kind !== 'tui') return '\tturn=?';
2186
+ return `\tturn=${formatTurnCell(describeTurnCell(ptyTurnViewOfRow(row), now))}`;
2187
+ }
2188
+
2115
2189
  export async function runPtyWait(ref: string, until: string, options: { readonly timeoutMs?: number; readonly pollMs?: number } = {}, deps: PtyTakeoverCommandDeps = liveDeps): Promise<PtyTakeoverCommandResult> {
2116
2190
  if (!WAITABLE_FRAME_STATES.includes(until as Exclude<FrameState, 'unknown'>)) return { exitCode: 2, message: `pty wait: --until must be one of ${WAITABLE_FRAME_STATES.join(', ')} (got ${until})` };
2117
2191
  const target = until as Exclude<FrameState, 'unknown'>;
@@ -0,0 +1,18 @@
1
+ import type { Command } from 'commander';
2
+ import { runScenarioFile } from '../ux-sim/scenario.js';
3
+
4
+ export function registerSimCommands(program: Command): void {
5
+ const sim = program.command('sim').description('격리 시나리오를 실행하고 화면·로그 기대값을 대조한다');
6
+ sim.command('run <scenario-file>')
7
+ .description('등록된 UX simulation 시나리오를 test 우주에서 실행한다')
8
+ .action(async (scenarioFile: string) => {
9
+ try {
10
+ const result = await runScenarioFile(scenarioFile);
11
+ console.log(JSON.stringify(result));
12
+ if (!result.pass) process.exitCode = 1;
13
+ } catch (error) {
14
+ console.error(`sim run: ${error instanceof Error ? error.message : String(error)}`);
15
+ process.exitCode = 2;
16
+ }
17
+ });
18
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Database } from 'bun:sqlite';
2
2
  import { debug, redactSecretText } from '../debug/log.js';
3
3
  import { openSurfaceEventsDb, recordEvent, surfaceEventsDbPath } from '../domains/surface-events.js';
4
+ import { canonicalSeatId } from '../msg/msg-store.js';
4
5
 
5
6
  export const OPS_EVENT_KINDS = [
6
7
  'land.start', 'land.refused', 'land.merged',
@@ -17,6 +18,8 @@ export interface OpsEventInput {
17
18
  text: string;
18
19
  /** Optional locator — PR number, checklist cell id, config key, release version. */
19
20
  ref?: string;
21
+ /** Optional seat identity, normalized by the shared message-address registry. */
22
+ seat?: string;
20
23
  /** Optional ISO timestamp; unparseable values are dropped. */
21
24
  at?: string;
22
25
  }
@@ -47,6 +50,7 @@ export function emitOpsEvent(input: OpsEventInput, deps: OpsEventDeps = {}): str
47
50
  const text = oneLine(input.text);
48
51
  if (!text) return null;
49
52
  const ref = oneLine(input.ref) || null;
53
+ const seat = typeof input.seat === 'string' && input.seat.trim() ? canonicalSeatId(input.seat) : undefined;
50
54
  const at = typeof input.at === 'string' && !Number.isNaN(Date.parse(input.at)) ? input.at : undefined;
51
55
  if (!db) {
52
56
  // Test runners must never fall through to the user's production memory DB
@@ -61,11 +65,11 @@ export function emitOpsEvent(input: OpsEventInput, deps: OpsEventDeps = {}): str
61
65
  return record(db, {
62
66
  surface: 'ops', direction: 'outbound', domain: 'elanous', category: 'ops',
63
67
  kind: input.kind, text, summary: text.slice(0, 120),
64
- refs: JSON.stringify({ kind: input.kind, ref, url: null }),
68
+ refs: JSON.stringify({ kind: input.kind, ref, url: null, ...(seat ? { seat } : {}) }),
65
69
  ...(at ? { ts: at } : {}),
66
70
  });
67
71
  } catch (error) {
68
- try { debug.log('ops.event', 'record-failed', {
72
+ try { debug.log('context.bus', 'write-failed', {
69
73
  kind: input.kind, reason: error instanceof Error ? error.message : String(error),
70
74
  }, { level: 'warn' }); } catch { /* A broken logger cannot change the caller's result. */ }
71
75
  return null;
@@ -289,6 +289,7 @@ export async function runCoreTurn(
289
289
  ...(ctx.callbacks?.onResolvedProvider !== undefined ? { onResolvedProvider: ctx.callbacks.onResolvedProvider } : {}),
290
290
  ...(ctx.budgetGrant !== undefined ? { budgetGrant: ctx.budgetGrant } : {}),
291
291
  ...(ctx.terminalToolNames !== undefined ? { terminalToolNames: ctx.terminalToolNames } : {}),
292
+ ...(ctx.terminalPriorAnswerVisible !== undefined ? { terminalPriorAnswerVisible: ctx.terminalPriorAnswerVisible } : {}),
292
293
  // ⭐⭐⭐ `B3`(2026-08-19 · 대표 지시 ②) — 도는 턴 «안»으로 들어오는 사용자 발화의 배수구.
293
294
  // ⛔ 종전엔 스트리밍 중 친 발화가 ***턴이 끝난 뒤***에야 나갔다.
294
295
  // ⭐ 이제 «다음 모델 요청을 만들기 직전»(루프 경계)에 이력으로 들어가고,
@@ -397,6 +397,10 @@ function findRejectedToolCalls(newMessages: readonly LLMMessage[]): string[] {
397
397
  return [...rejected];
398
398
  }
399
399
 
400
+ /** `streamLLMWithTools` 의 빈 턴·예산 포기 경고(`buildNoFinalSynthesisNotice`) 문면 — 완료 판정 관측에서 «답 없음»으로 센다.
401
+ * 머리 표지만 보지 않고 런타임 문장까지 맞춰, 그 표지를 인용한 정상 답을 «경고»로 세지 않는다. */
402
+ const NO_FINAL_SYNTHESIS_NOTICE = /\[NO FINAL SYNTHESIS\] The model (?:stopped after repeated empty turns|used the available tool-loop turns without writing a plain-text answer)\./;
403
+
400
404
  /** 이 turn 의 새 메시지에서 마지막 `update_goal` 도구 호출을 찾아 구조화 종결 상태를
401
405
  * 반환한다. status 가 enum 밖이면 무시(null). evidence/reason 은 문자열만 수용. */
402
406
  function findGoalUpdate(
@@ -559,6 +563,20 @@ export async function runGoalLoop(
559
563
  ): GoalLoopResult => {
560
564
  if (terminalPublished) return result;
561
565
  terminalPublished = true;
566
+ // TUI-H1-GOALLOOP-EMPTY-TAIL — 완료 판정 한 줄: 목표가 닫혔나(goal-complete) · 사람에게 보일 답이 있나
567
+ // (answer-present) · 둘 다 아니다(none — 빈 답이거나 «[NO FINAL SYNTHESIS]» 경고가 최종 답).
568
+ try {
569
+ const finalTrimmed = result.finalText.trim();
570
+ const reason = result.goalComplete
571
+ ? 'goal-complete'
572
+ : finalTrimmed.length > 0 && !NO_FINAL_SYNTHESIS_NOTICE.test(finalTrimmed) ? 'answer-present' : 'none';
573
+ debug.log('goal.loop', 'completion-verdict', {
574
+ sessionId: ctx.sessionId, ...runAttribution,
575
+ iterations, reason, stopReason: result.stopReason, finalChars: result.finalText.length,
576
+ }, { level: 'info' });
577
+ } catch {
578
+ // Observability must not change the loop result.
579
+ }
562
580
  try {
563
581
  if (result.goalComplete) {
564
582
  publishGoalLifecycle(bus, 'complete', completePayload ?? {
@@ -778,6 +796,9 @@ export async function runGoalLoop(
778
796
  ...(surfaceUserText !== undefined ? { userText: surfaceUserText } : {}),
779
797
  // TUI-GOAL-LOOP-FRICTION(#46·#63) — update_goal 은 도구 예산 거부를 받지 않고, 답과 함께 오면 그 턴으로 마감한다.
780
798
  terminalToolNames: [...(ctx.terminalToolNames ?? []), UPDATE_GOAL_TOOL_NAME],
799
+ // TUI-H1-GOALLOOP-EMPTY-TAIL — 앞 iteration 의 답이 이미 화면에 있으면 «update_goal 만 → 빈 턴» 은 할 말이 없는
800
+ // 것이다 — 재촉·경고 없이 빈 답으로 끝내고 아래 완료 판정이 앞 답으로 마감한다(중복 접기와 같은 길).
801
+ ...(previousFinalText.trim().length > 0 ? { terminalPriorAnswerVisible: true } : {}),
781
802
  // update_goal 은 종결 신호 — 실 dispatcher 로 새지 않게 가로채 ack 만 돌려준다.
782
803
  dispatchTool: (name, args, dctx) => {
783
804
  dispatchCount += 1;
@@ -158,6 +158,9 @@ export interface CoreTurnContext {
158
158
  /** TUI-GOAL-LOOP-FRICTION — 종결 신호 도구 이름. `streamLLMWithTools` 의 `terminalToolNames` 로 그대로 간다
159
159
  * (예산 거부 면제 · 신호뿐인 턴 ⊕ 답 = 마감). goal loop 가 `update_goal` 을 넣는다. */
160
160
  terminalToolNames?: LLMOpts['terminalToolNames'];
161
+ /** TUI-H1-GOALLOOP-EMPTY-TAIL — 이 턴 «앞»에 사람에게 답을 이미 보였다. `streamLLMWithTools` 의
162
+ * `terminalPriorAnswerVisible` 로 그대로 간다(종결 신호 뒤 빈 턴 = 경고 없이 빈 답). goal loop 가 넣는다. */
163
+ terminalPriorAnswerVisible?: LLMOpts['terminalPriorAnswerVisible'];
161
164
  /** Optional model override (forwarded to `streamLLMWithTools`). */
162
165
  modelOverride?: string;
163
166
  }
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { debug } from '../debug/log.js';
8
8
  import { effectiveReasoningLevel } from '../llm.js';
9
- import { recordCurrentRouteDecision, resolveRouteDecision, sessionModelPin } from '../llm/route-decision.js';
9
+ import { clearSurfaceRouteDecision, currentRouteDecision, recordCurrentRouteDecision, recordRouteExecution, resolveRouteDecision, sessionModelPin } from '../llm/route-decision.js';
10
10
  import { inspectActiveProvider } from '../provider-summary.js';
11
11
  import { getUserConfig } from '../user-config.js';
12
12
 
@@ -38,3 +38,44 @@ export function resolveDashboardActiveModel(userText: string): string | null {
38
38
  });
39
39
  return decision.model || null;
40
40
  }
41
+
42
+ /** 턴 사용량(ACP `elanous/ui/usage`)이 실어 온 «실제» 공급자·모델을 이 표면의 실행으로 적는다
43
+ * (TUI-MODEL-BADGE-ACTUAL · 10-16). 완료 배지가 결정과 다르면 «A → B» 로 보인다.
44
+ * 폴백(결정과 다른 모델)이면 관측 이벤트 `llm.route-decision` / `dashboard-executed-fallback` 를 «항상» 남긴다. */
45
+ export function recordDashboardTurnExecution(
46
+ usage: { model?: string; routeProvider?: string } | null | undefined,
47
+ /** 필수 — 세션 위치를 모르면(null) 적지 않는다(원격 사용량을 로컬 실행으로 꾸미지 않게). */
48
+ session: { remote: boolean } | null,
49
+ ): void {
50
+ // ⛔ 순서: 원격·세션 미상 판정(⊕ 비우기)을 model 검사보다 «먼저» 한다 — 사용량 봉투의 model 은 선택 칸이라,
51
+ // 모델 없는 원격 사용량이 먼저 반환하면 지난 로컬 턴의 «A → B» 가 남는다(r6 리뷰).
52
+ const model = usage?.model?.trim() || null;
53
+ if (session === null) {
54
+ // 짝을 모르는 턴 — 지난 턴의 결정·실행이 이 턴의 배지로 «다시» 보이지 않게 비운다.
55
+ clearSurfaceRouteDecision('dashboard');
56
+ debug.log('llm.route-decision', 'dashboard-executed-session-unknown', { model }, { level: 'warn' });
57
+ return;
58
+ }
59
+ // ⛔ 원격 데몬 세션은 적지 않는다 — 결정은 데몬 쪽에서 서므로 이 프로세스의 결정과 짝이 안 맞는다.
60
+ // 후속 칸 TUI-MODEL-BADGE-REMOTE.
61
+ if (session.remote) {
62
+ // 원격 턴은 로컬 결정과 짝이 없다 — 지난 로컬 턴의 «A → B» 가 이 턴의 배지로 다시 나오지 않게 비운다(r5 리뷰).
63
+ clearSurfaceRouteDecision('dashboard');
64
+ if (debug.enabled) debug.log('llm.route-decision', 'dashboard-executed-remote-skipped', { model });
65
+ return;
66
+ }
67
+ if (!model) return;
68
+ const provider = usage?.routeProvider?.trim() || undefined;
69
+ const decision = currentRouteDecision('dashboard');
70
+ // 결정이 없는 실행은 짝이 없다 — 적지 않는다.
71
+ if (!decision) return;
72
+ recordRouteExecution('dashboard', { model, ...(provider ? { provider } : {}) });
73
+ if (decision.model !== '' && decision.model !== model) {
74
+ debug.log('llm.route-decision', 'dashboard-executed-fallback', {
75
+ planned: { provider: decision.provider, model: decision.model },
76
+ actual: { provider: provider ?? null, model },
77
+ }, { level: 'warn' });
78
+ } else if (debug.enabled) {
79
+ debug.log('llm.route-decision', 'dashboard-executed', { provider: provider ?? null, model });
80
+ }
81
+ }
@@ -0,0 +1,66 @@
1
+ // ── 상태줄 «⧗ 백그라운드 N» 상태 (TUI-BG-JOB-VISIBILITY · TUI 결함 #71 · 0.2.31) ──────────────
2
+ //
3
+ // 🚨 무엇을 푸는가: 10-16 07:3x OPT 가 «남은 리뷰 4건 실행 중 · 다음 점검 = …» 으로 턴을 «✔ 완료» 로
4
+ // 끝냈다. 상태줄엔 백그라운드 표시가 없어 바깥(OP·대표)에선 «12분 유휴» 로 보였다.
5
+ // ⭐ 이 모듈은 «상태줄에 몇 개를 그릴지» 와 그 관측만 쥔다 — 렌더는 `backgroundJobsSegment`
6
+ // (src/status/bar.ts), 집계는 BackgroundSurface 의 `background`(src/shell-runner/background-surface.ts).
7
+ // ⛔ 깨우기(끝나면 턴을 여는 것)는 하지 않는다 — TUI-AGENT-IDLE-WAKE 칸(onEnqueue 배선 · H2 문) 몫.
8
+ // ⛔ 입력칸·외부 입력 대기열 주입 0(대표 비침투).
9
+ //
10
+ // 세 얼굴:
11
+ // 관측 = debug.log('status.bar', 'bg-jobs', { running, backgrounded, background, trigger:'rollup' })
12
+ // — 등록된 백그라운드 수가 «바뀔 때만»(렌더마다·같은 수 반복엔 안 남긴다 · 처음 기준 = 0)
13
+ // 인지 = 등록된 백그라운드 일 > 0 이면 턴이 유휴여도 «일하는 중»
14
+ // 힐링 = 상태줄 «⧗ 백그라운드 N» — 사람·OP 가 «유휴인데 일하는 중» 을 본다 · 수가 바뀌면 다시 그린다 ·
15
+ // 읽기 실패는 0 으로 보고 상태줄을 막지 않는다 · 실패 시 TF(관측 로그)
16
+
17
+ import { debug } from '../debug/log.js';
18
+ import type { BackgroundJobsSummary } from '../status/bar.js';
19
+
20
+ export interface ShellRollupCounts {
21
+ running: number;
22
+ backgrounded: number;
23
+ /** 턴을 넘어 사는 핸들(bg 모드 ⊕ backgrounded). 필수 — backgrounded 로 추정하면 도는 bg 모드 일을 빠뜨린다. */
24
+ background: number;
25
+ }
26
+
27
+ export interface BackgroundJobsStatusDeps {
28
+ log?: (event: string, data: Record<string, unknown>) => void;
29
+ }
30
+
31
+ export interface BackgroundJobsStatus {
32
+ /** 집계가 왔다 — 등록된 백그라운드 수가 바뀌었으면 관측 한 줄 ⊕ true(호출자가 다시 그린다). */
33
+ noteRollup(rollup: ShellRollupCounts): boolean;
34
+ /** 상태줄 입력. */
35
+ summary(): BackgroundJobsSummary;
36
+ }
37
+
38
+ function countOf(n: number): number {
39
+ return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
40
+ }
41
+
42
+ export function createBackgroundJobsStatus(deps: BackgroundJobsStatusDeps = {}): BackgroundJobsStatus {
43
+ const log = deps.log ?? ((event, data) => { debug.log('status.bar', event, data); });
44
+ // 처음 기준은 0 — 백그라운드 0 인 첫 집계(인라인 실행 등)는 «변화 없음» 이라 안 남긴다.
45
+ let background = 0;
46
+
47
+ return {
48
+ noteRollup(rollup) {
49
+ const next = countOf(rollup.background);
50
+ if (next === background) return false;
51
+ background = next;
52
+ try {
53
+ log('bg-jobs', {
54
+ running: countOf(rollup.running),
55
+ backgrounded: countOf(rollup.backgrounded),
56
+ background: next,
57
+ trigger: 'rollup',
58
+ });
59
+ } catch { /* 관측 실패가 상태줄을 막지 않는다 */ }
60
+ return true;
61
+ },
62
+ summary() {
63
+ return { background };
64
+ },
65
+ };
66
+ }