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
@@ -79,6 +79,11 @@ export const RENDER_CATEGORY_EXEMPTIONS: ReadonlyMap<string, string> = new Map([
79
79
  // ⛔ 접두 전체(`dashboard`)를 풀지 «않는다» — 그러면 진짜 렌더 소음이 되살아난다. 이 둘만 뗀다.
80
80
  ['dashboard.turn-typeahead', 'Mid-turn input queueing decisions — which key was consumed, what was queued, and when it drains. Turn-control judgment, not frame noise.'],
81
81
  ['dashboard.streaming-key', 'Streaming-window key ownership — whether a key reached the streaming dispatcher at all. This is the probe that answers "is the ladder live?", so it must survive render mute.'],
82
+ // ⛔ 2026-10-17 (`TUI-H8-SEAT-BOOT-LOG-MUTED`) — 자리 부트 카드는 «항상성» 이벤트다(렌더 소음 아님).
83
+ // `/session new` 뒤 카드가 실렸는지를 `logs --all --include-test` 로도 0행으로 만들어
84
+ // «카드가 빠졌다»와 «음소거됐다»를 가를 수 없었다. 접두 전체는 풀지 않고 이것만 뗀다.
85
+ // (`tui.autoturn`·`tui.wakeup`·`tui.autowake` 는 `tui` 가 렌더 접두가 아니라 애초에 안 잘린다.)
86
+ ['dashboard.seat-boot', 'Seat boot card lifecycle — conversation-start, card-injected, card-skipped. Homeostasis evidence that the seat card reached the turn, not frame noise.'],
82
87
  ]);
83
88
 
84
89
  /** Reject an exemption without its required, human-readable rationale. */
@@ -96,6 +96,62 @@ export interface VerdictResult { verdict: Verdict; reasons: string[]; nextAction
96
96
  /** 부하/코어가 이 값을 넘으면 «기계 압박»(흐름 저하의 사유). 디스크 I/O 신호는 OVW-MACHINE-HB 전까지 없다. */
97
97
  export const LOAD_PER_CORE_PRESSURE = 3;
98
98
 
99
+ /** 자원 지도의 축 어휘(닫힌 집합 · RES-FLOOR-BINDING). I/O·게이트 예약은 이 칸에 원천이 없어 늘 제외다. */
100
+ export type ResourceKind = 'memory' | 'cpu' | 'io' | 'slots' | 'gate';
101
+
102
+ /** 구속 조건 — 남은 비율이 최소인 축. 남은 비율 = 남은 양/전체 양(CPU 는 압박선 LOAD_PER_CORE_PRESSURE×코어를 전체로 본다). */
103
+ export interface ResourceBinding {
104
+ kind: ResourceKind;
105
+ label: string;
106
+ remainingRatio: number;
107
+ /** 남은 양/전체 양의 사람 표기 — `5/64GB` · `0/54코어(압박선 3/코어)` · `0/46자리`. */
108
+ remainingText: string;
109
+ }
110
+
111
+ function ratio01(n: number): number {
112
+ return Math.min(1, Math.max(0, n));
113
+ }
114
+
115
+ function remainingAmount(n: number): string {
116
+ return Number.isInteger(n) ? n.toFixed(0) : String(Number(n.toPrecision(3)));
117
+ }
118
+
119
+ /**
120
+ * 구속 조건 규칙(RES-FLOOR-BINDING) — 메모리·CPU·I/O·슬롯·게이트 예약 중 «남은 비율 최소»인 축이 흐름을 묶는다.
121
+ * 못 잰 축(하트비트 없는 호스트 · 용량 못 잼 · 원천 없는 I/O·게이트 예약)은 후보에서 뺀다 — «넉넉»으로도 «바닥»으로도 읽지 않는다.
122
+ * 측정 축이 하나도 없으면 null. 동율은 정해진 순서(호스트 순 메모리→CPU, 그 다음 슬롯)의 첫 축이 이긴다.
123
+ */
124
+ export function bindingResource(s: OverviewSnapshot): ResourceBinding | null {
125
+ const candidates: ResourceBinding[] = [];
126
+ for (const host of s.machines.hosts) {
127
+ const l = host.load.value;
128
+ if (!l) continue;
129
+ if (l.totalGb > 0) {
130
+ const remaining = Math.max(0, l.freeGb);
131
+ candidates.push({ kind: 'memory', label: `${host.name} 메모리`, remainingRatio: ratio01(remaining / l.totalGb), remainingText: `${remainingAmount(remaining)}/${l.totalGb.toFixed(0)}GB` });
132
+ }
133
+ if (l.cpuCount > 0) {
134
+ const cpuCap = l.cpuCount * LOAD_PER_CORE_PRESSURE;
135
+ const remaining = Math.max(0, cpuCap - l.load5);
136
+ candidates.push({ kind: 'cpu', label: `${host.name} CPU`, remainingRatio: ratio01(remaining / cpuCap), remainingText: `${remainingAmount(remaining)}/${cpuCap.toFixed(0)}코어(압박선 ${LOAD_PER_CORE_PRESSURE}/코어)` });
137
+ }
138
+ }
139
+ const capacity = s.tasks.capacity.value;
140
+ if (capacity && capacity.podTarget + capacity.localMax > 0) {
141
+ const total = capacity.podTarget + capacity.localMax;
142
+ candidates.push({ kind: 'slots', label: '슬롯', remainingRatio: ratio01(capacity.free / total), remainingText: `${capacity.free}/${total}자리` });
143
+ }
144
+ let binding: ResourceBinding | null = null;
145
+ for (const c of candidates) if (binding === null || c.remainingRatio < binding.remainingRatio) binding = c;
146
+ return binding;
147
+ }
148
+
149
+ /** 판정 띠 문구 — 측정된 축 중 «무엇이 흐름을 묶고 있나»를 이름으로 단다. 측정 축이 없으면 null. */
150
+ export function bindingPhrase(b: ResourceBinding | null): string | null {
151
+ if (!b) return null;
152
+ return `구속 자원 ${b.label} — 남은 비율 최소 ${Math.round(b.remainingRatio * 100)}% (${b.remainingText})`;
153
+ }
154
+
99
155
  export function computeVerdict(s: OverviewSnapshot): VerdictResult {
100
156
  const hits = new Map<Verdict, { reasons: string[]; actions: string[] }>();
101
157
  const hit = (v: Verdict, reason: string, action?: string): void => {
@@ -155,14 +211,15 @@ export function computeVerdict(s: OverviewSnapshot): VerdictResult {
155
211
  }
156
212
  }
157
213
 
214
+ const phrase = bindingPhrase(bindingResource(s));
158
215
  for (const v of VERDICTS) {
159
216
  const row = hits.get(v);
160
217
  if (!row) continue;
161
218
  // 이유 사슬: 이긴 판정의 사유 먼저, 그 밑에 진 판정들의 사유도 «부가»로 남긴다(한 사슬에 두 원인이 보이게).
162
219
  const rest = VERDICTS.filter((o) => o !== v && hits.has(o)).flatMap((o) => hits.get(o)!.reasons.map((r) => `(+${VERDICT_LABEL[o]}) ${r}`));
163
- return { verdict: v, reasons: [...row.reasons, ...rest], nextActions: row.actions };
220
+ return { verdict: v, reasons: [...row.reasons, ...(phrase ? [phrase] : []), ...rest], nextActions: row.actions };
164
221
  }
165
- return { verdict: 'flowing', reasons: [], nextActions: [] };
222
+ return { verdict: 'flowing', reasons: phrase ? [phrase] : [], nextActions: [] };
166
223
  }
167
224
 
168
225
  /** exit code — flowing·degraded 0 · 그 밖의 판정 3(수집기 자체 실패 1 은 호출자 몫). */
@@ -28,7 +28,9 @@ export type Census = { status: 'checked'; total: number; rows: readonly CensusRo
28
28
  | { status: 'unknown'; reason: string };
29
29
  export interface DiffHunk { path: string; start: number; end: number }
30
30
  export type GuardCode = 'inventory-unknown' | 'candidate-unknown' | 'goal-only' | 'file-overlap' | 'hunk-overlap' | 'same-cell' | 'seat-cap' | 'seat-unattributed';
31
- export interface GuardReason { code: GuardCode; detail: string; prs?: readonly number[] }
31
+ export interface GuardReason { code: GuardCode; detail: string; prs?: readonly number[]; files?: readonly string[] }
32
+ /** PR-GUARD-HUNK-WARN (대표 10-17): 줄 겹침은 기본 경고 — 착지 쪽 스마트 머지가 푼다. 'refuse' = 옛 거부 동작. */
33
+ export type HunkOverlapMode = 'refuse' | 'warn';
32
34
  export interface GuardVerdict { status: 'allow' | 'refuse' | 'unknown'; reasons: readonly GuardReason[]; warnings: readonly GuardReason[] }
33
35
 
34
36
  const SEATS = ['OP', 'TC', 'MK', 'UX'] as const;
@@ -209,7 +211,7 @@ export function censusOpenPrs(inventory: Inventory, now = Date.now()): Census {
209
211
  return result;
210
212
  }
211
213
 
212
- export function guardNewPr(input: { files: readonly string[] | null; seat?: Seat | null; seatCap?: number; cellId?: string | null; hunks?: readonly DiffHunk[] }, inventory: Inventory): GuardVerdict {
214
+ export function guardNewPr(input: { files: readonly string[] | null; seat?: Seat | null; seatCap?: number; cellId?: string | null; hunks?: readonly DiffHunk[]; hunkOverlap?: HunkOverlapMode }, inventory: Inventory): GuardVerdict {
213
215
  if (!input.files || input.files.length === 0 || input.files.some(file => !file || file.startsWith('/') || file.includes('..'))) {
214
216
  const reasons: GuardReason[] = [{ code: 'candidate-unknown', detail: '새 PR 변경 파일을 확인할 수 없음' }];
215
217
  debug.log('pr.tool', 'guard-refuse', { status: 'unknown', reasons });
@@ -226,8 +228,21 @@ export function guardNewPr(input: { files: readonly string[] | null; seat?: Seat
226
228
  const sameCell = input.cellId ? inventory.prs.filter(pr => pr.cellId === input.cellId) : [];
227
229
  if (sameCell.length) reasons.push({ code: 'same-cell', detail: `같은 칸 ${input.cellId} OPEN PR이 있음`, prs: sameCell.map(pr => pr.number) });
228
230
  const overlaps = inventory.prs.filter(pr => pr.files.some(file => candidate.has(file)));
229
- const sameHunk = overlaps.filter(pr => pr.hunks?.some(other => input.hunks?.some(own => own.path === other.path && own.start <= other.end && other.start <= own.end)));
230
- if (sameHunk.length) reasons.push({ code: 'hunk-overlap', detail: '같은 파일의 수정 줄 범위가 겹침', prs: sameHunk.map(pr => pr.number) });
231
+ const hunkFiles = new Set<string>();
232
+ const sameHunk = overlaps.filter(pr => {
233
+ const paths = (pr.hunks ?? []).filter(other => input.hunks?.some(own => own.path === other.path && own.start <= other.end && other.start <= own.end)).map(other => other.path);
234
+ for (const path of paths) hunkFiles.add(path);
235
+ return paths.length > 0;
236
+ });
237
+ if (sameHunk.length) {
238
+ const refuseHunk = input.hunkOverlap === 'refuse';
239
+ (refuseHunk ? reasons : warnings).push({
240
+ code: 'hunk-overlap',
241
+ detail: refuseHunk ? '같은 파일의 수정 줄 범위가 겹침' : '같은 파일의 수정 줄 범위가 겹침 · 착지 때 머지로 푼다',
242
+ prs: sameHunk.map(pr => pr.number),
243
+ files: [...hunkFiles],
244
+ });
245
+ }
231
246
  if (overlaps.length) warnings.push({ code: 'file-overlap', detail: '같은 파일의 OPEN PR이 있음 · 줄 겹침은 별도 판정', prs: overlaps.map(pr => pr.number) });
232
247
  const seat = seatValue(input.seat);
233
248
  if (input.seat !== undefined && input.seat !== null && !seat) reasons.push({ code: 'candidate-unknown', detail: '호출자 자리 값 오류' });
@@ -0,0 +1,143 @@
1
+ /**
2
+ * INFLIGHT-REGISTRY — 비행 중 등록부(대표 10-17 «출발 때 맥락 남기고 머지 때 참조 · 착지하면 지움 · 폭증 막기»).
3
+ *
4
+ * 발사하는 런이 runId·칸·한 줄 의도·대상 파일을 한 줄 등록하고, 머지 순간(스마트 머지 LLM)에 같은 파일을
5
+ * 만지는 줄들의 의도를 읽는다. 착지하면 지운다. 정본은 PR 본문이고 이것은 «색인»이다 — 못 읽으면 빈 목록
6
+ * (머지는 종전 그대로 간다), 못 쓰면 발사·착지를 막지 않는다(호출부가 fail-soft).
7
+ *
8
+ * 폭증 막기: 런당 1줄(같은 칸 재발사는 덮어씀) · 의도 ≤300자 · 파일 ≤20 · 48h 지난 줄은 읽을 때 거르고 쓸 때 걷는다.
9
+ */
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
11
+ import { dirname, join } from 'node:path';
12
+ import { debug } from '../debug/log.js';
13
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
14
+ import { withFileLockSync } from '../storage/file-lock.js';
15
+
16
+ export const INFLIGHT_INTENT_MAX_CHARS = 300;
17
+ export const INFLIGHT_FILES_MAX = 20;
18
+ export const INFLIGHT_TTL_MS = 48 * 60 * 60 * 1000;
19
+ export const INFLIGHT_REGISTRY_FILE = 'inflight-registry.json';
20
+
21
+ export interface InflightEntry {
22
+ runId: string;
23
+ /** 칸 id(없으면 카드 id) — 같은 칸 재발사는 이 키로 덮어쓴다. */
24
+ cell: string;
25
+ intent: string;
26
+ files: string[];
27
+ registeredAt: string;
28
+ cardId?: string;
29
+ }
30
+
31
+ export interface InflightOptions {
32
+ path?: string;
33
+ now?: () => Date;
34
+ }
35
+
36
+ /** 기본 자리 — 과제 카드 상태(`task-agent-actions.json`)와 같은 우주 뿌리. */
37
+ export function inflightRegistryPath(): string {
38
+ return join(effectiveInstanceRoot(), INFLIGHT_REGISTRY_FILE);
39
+ }
40
+
41
+ function readEntries(path: string): InflightEntry[] {
42
+ if (!existsSync(path)) return [];
43
+ try {
44
+ const parsed = JSON.parse(readFileSync(path, 'utf8')) as { entries?: unknown };
45
+ return Array.isArray(parsed.entries) ? parsed.entries.filter(isEntry) : [];
46
+ } catch { return []; }
47
+ }
48
+
49
+ function isEntry(value: unknown): value is InflightEntry {
50
+ const entry = value as Partial<InflightEntry> | null;
51
+ return !!entry && typeof entry.runId === 'string' && typeof entry.cell === 'string' && typeof entry.intent === 'string'
52
+ && Array.isArray(entry.files) && typeof entry.registeredAt === 'string';
53
+ }
54
+
55
+ function isExpired(entry: InflightEntry, now: Date): boolean {
56
+ const at = Date.parse(entry.registeredAt);
57
+ return !Number.isFinite(at) || now.getTime() - at > INFLIGHT_TTL_MS;
58
+ }
59
+
60
+ /** 잠금 안에서 읽고 고쳐 쓴다 — 48h 지난 줄은 쓸 때 걷는다(`expire` 관측). */
61
+ function mutate(opts: InflightOptions, update: (entries: InflightEntry[]) => InflightEntry[]): void {
62
+ const path = opts.path ?? inflightRegistryPath();
63
+ const now = (opts.now ?? (() => new Date()))();
64
+ withFileLockSync(`${path}.lock`, () => {
65
+ const all = readEntries(path);
66
+ const live = all.filter((entry) => !isExpired(entry, now));
67
+ if (live.length < all.length) {
68
+ try { debug.log('pr-tool.inflight', 'expire', { phase: 'write', runIds: all.filter((entry) => isExpired(entry, now)).map((entry) => entry.runId) }); } catch { /* fail-soft */ }
69
+ }
70
+ mkdirSync(dirname(path), { recursive: true });
71
+ writeFileSync(path, JSON.stringify({ entries: update(live) }));
72
+ });
73
+ }
74
+
75
+ /** 한 줄 등록 — 같은 runId 또는 같은 칸의 옛 줄을 덮어쓴다. 의도·파일은 상한에서 자른다. */
76
+ export function registerInflight(input: { runId: string; cell: string; intent: string; files: readonly string[]; cardId?: string }, opts: InflightOptions = {}): InflightEntry {
77
+ const now = (opts.now ?? (() => new Date()))();
78
+ const files = [...new Set(input.files.map((file) => file.trim()).filter(Boolean))];
79
+ // 한 줄 칸만 — 줄바꿈·제어 문자는 공백으로(머지 프롬프트 인용 블록을 깨지 못하게 · 프롬프트 쪽도 따로 무력화한다).
80
+ const oneLine = (value: string) => value.replace(/[\p{Cc}\s]+/gu, ' ').trim();
81
+ const entry: InflightEntry = {
82
+ runId: oneLine(input.runId),
83
+ cell: oneLine(input.cell),
84
+ intent: oneLine(input.intent).slice(0, INFLIGHT_INTENT_MAX_CHARS),
85
+ files: files.slice(0, INFLIGHT_FILES_MAX),
86
+ registeredAt: now.toISOString(),
87
+ ...(input.cardId ? { cardId: input.cardId } : {}),
88
+ };
89
+ let replaced: string[] = [];
90
+ mutate(opts, (entries) => {
91
+ replaced = entries.filter((old) => old.runId === entry.runId || old.cell === entry.cell).map((old) => old.runId);
92
+ return [...entries.filter((old) => old.runId !== entry.runId && old.cell !== entry.cell), entry];
93
+ });
94
+ try { debug.log('pr-tool.inflight', 'register', { runId: entry.runId, cell: entry.cell, files: entry.files.length, filesDropped: files.length - entry.files.length, intentChars: entry.intent.length, replaced }); } catch { /* fail-soft */ }
95
+ return entry;
96
+ }
97
+
98
+ /** 줄을 지운다 — runId 또는 카드 id 로. 지운 수를 돌려준다(0 = 그런 줄 없음). */
99
+ export function removeInflight(key: { runId: string } | { cardId: string }, reason: string, opts: InflightOptions = {}): number {
100
+ let removed: string[] = [];
101
+ const matches = (entry: InflightEntry): boolean => 'runId' in key ? entry.runId === key.runId : entry.cardId === key.cardId;
102
+ mutate(opts, (entries) => {
103
+ removed = entries.filter(matches).map((entry) => entry.runId);
104
+ return entries.filter((entry) => !matches(entry));
105
+ });
106
+ try { debug.log('pr-tool.inflight', 'remove', { ...key, reason, removed }); } catch { /* fail-soft */ }
107
+ return removed.length;
108
+ }
109
+
110
+ /** 살아 있는 줄 — `files` 를 주면 그중 하나라도 만지는 줄만. 48h 지난 줄은 거른다(읽기는 쓰지 않는다). */
111
+ export function listInflight(filter: { files?: readonly string[] } = {}, opts: InflightOptions = {}): InflightEntry[] {
112
+ const now = (opts.now ?? (() => new Date()))();
113
+ const all = readEntries(opts.path ?? inflightRegistryPath());
114
+ const live = all.filter((entry) => !isExpired(entry, now));
115
+ if (live.length < all.length) {
116
+ try { debug.log('pr-tool.inflight', 'expire', { phase: 'read', count: all.length - live.length }); } catch { /* fail-soft */ }
117
+ }
118
+ if (!filter.files) return live;
119
+ const wanted = new Set(filter.files);
120
+ return live.filter((entry) => entry.files.some((file) => wanted.has(file)));
121
+ }
122
+
123
+ /**
124
+ * 착지한 PR 의 줄을 지운다 — 그 PR 을 연 과제 카드(`card.pr`)의 카드 id·runId 로 찾는다.
125
+ * 카드가 없으면 디스크를 건드리지 않는다(사람 PR · 다른 우주 카드 = 지울 줄 없음).
126
+ */
127
+ export function removeInflightForPr(
128
+ pr: number,
129
+ cards: Iterable<{ id: string; runId?: string; pr?: number | { number: number } }>,
130
+ opts: InflightOptions = {},
131
+ ): number {
132
+ const owners = [...cards].filter((card) => (typeof card.pr === 'number' ? card.pr : card.pr?.number) === pr);
133
+ if (owners.length === 0) {
134
+ try { debug.log('pr-tool.inflight', 'remove', { pr, reason: 'pr-landed', removed: [], cards: 0 }); } catch { /* fail-soft */ }
135
+ return 0;
136
+ }
137
+ let removed = 0;
138
+ for (const card of owners) {
139
+ removed += removeInflight({ cardId: card.id }, `pr-landed:${pr}`, opts);
140
+ if (card.runId) removed += removeInflight({ runId: card.runId }, `pr-landed:${pr}`, opts);
141
+ }
142
+ return removed;
143
+ }
@@ -277,6 +277,9 @@ export function upsertPtyManifest(row: {
277
277
  };
278
278
  }): void {
279
279
  try {
280
+ // 행을 (다시) 세우면 snapshot·frame 칸이 비워진다 — 같은 프로세스의 쓰기 스로틀도 비워 다음 쓰기가 바로 닿게 한다.
281
+ _lastSnapshotAt.delete(row.id);
282
+ _lastFrameAt.delete(row.id);
280
283
  const d = db(); if (!d) return;
281
284
  const runId = getHarnessRunId(process.env);
282
285
  const runIdSource = getHarnessRunIdSource();
@@ -800,12 +803,18 @@ export function listPtyManifestByRun(runId: string): PtyManifestRow[] {
800
803
  }
801
804
  }
802
805
 
803
- export function getPtyManifest(id: string): PtyManifestRow | null {
806
+ /** `strict` — 조회 실패(db 못 엶·질의 오류)를 «행 없음»(null)과 가르도록 던진다. 기본은 둘 다 null(fail-soft). */
807
+ export function getPtyManifest(id: string, opts: { readonly strict?: boolean } = {}): PtyManifestRow | null {
804
808
  try {
805
- const d = db(); if (!d) return null;
809
+ const d = db();
810
+ if (!d) {
811
+ if (opts.strict) throw new Error('pty-manifest-db-unavailable');
812
+ return null;
813
+ }
806
814
  const row = d.query('SELECT * FROM pty_manifest WHERE id=?').get(id) as Record<string, unknown> | null;
807
815
  return row ? mapRow(row) : null;
808
- } catch {
816
+ } catch (error) {
817
+ if (opts.strict) throw error;
809
818
  return null;
810
819
  }
811
820
  }
@@ -176,10 +176,9 @@ export function describePtyRunState(
176
176
  ...(checkoutHead ? { checkoutHead } : {}),
177
177
  ...(row && row.kind === 'tui' ? { startedAt: row.startedAt } : {}),
178
178
  });
179
- const turn: PtyTurnView = row?.alive && row.turnState ? row.turnState : 'unknown';
179
+ const turnView = ptyTurnViewOfRow(row);
180
180
  return {
181
- turn,
182
- ...(turn !== 'unknown' && row?.turnAt ? { turnAt: row.turnAt } : {}),
181
+ ...turnView,
183
182
  build: build ?? null,
184
183
  installed: installed.sha ?? null,
185
184
  stale: staleness.stale,
@@ -191,6 +190,38 @@ export function shortSha(sha: string | null | undefined): string {
191
190
  return sha ? sha.slice(0, 7) : '-';
192
191
  }
193
192
 
193
+ /** 행의 자기보고 턴만 — 설치본·git 판독과 «무관»하다(판독이 실패해도 기록된 턴은 그대로 읽힌다). 죽은 행·기록 없음 = unknown. */
194
+ export function ptyTurnViewOfRow(row: Pick<PtyManifestRow, 'alive' | 'turnState' | 'turnAt'> | null | undefined): Pick<PtyRunStateView, 'turn' | 'turnAt'> {
195
+ const turn: PtyTurnView = row?.alive && row.turnState ? row.turnState : 'unknown';
196
+ return { turn, ...(turn !== 'unknown' && row?.turnAt ? { turnAt: row.turnAt } : {}) };
197
+ }
198
+
199
+ // ── 턴 칸(H10 · `pty list` 가 화면 긁기 없이 «지금 턴 중인가·얼마나 됐나»를 낸다) ──
200
+
201
+ export interface PtyTurnCell {
202
+ readonly state: PtyTurnView;
203
+ /** 지금 턴 상태로 바뀐 시각(epoch ms) — 모르면 null. */
204
+ readonly at: number | null;
205
+ /** 지금 턴 상태에 머문 초(busy 면 «이 턴 경과»). ⛔ 모르면 null — 「0초」와 「못 잼」은 다른 값이다. */
206
+ readonly sinceSec: number | null;
207
+ }
208
+
209
+ export const UNKNOWN_TURN_CELL: PtyTurnCell = { state: 'unknown', at: null, sinceSec: null };
210
+
211
+ /** `turnAt` 은 턴 값이 «바뀔 때만» 적힌다(`reportTuiTurn` 이 같은 값을 거른다) ⇒ now−turnAt = 그 상태의 경과. */
212
+ export function describeTurnCell(view: Pick<PtyRunStateView, 'turn' | 'turnAt'>, now: number): PtyTurnCell {
213
+ if (view.turn === 'unknown' || view.turnAt === undefined || !Number.isFinite(view.turnAt) || view.turnAt <= 0) {
214
+ return { state: view.turn, at: null, sinceSec: null };
215
+ }
216
+ return { state: view.turn, at: view.turnAt, sinceSec: Math.max(0, Math.floor((now - view.turnAt) / 1000)) };
217
+ }
218
+
219
+ /** 표 한 칸 값: `busy,42s` · `idle,300s` · 시각 모름 `busy,?s` · 턴 모름 `?`. */
220
+ export function formatTurnCell(cell: PtyTurnCell): string {
221
+ if (cell.state === 'unknown') return '?';
222
+ return `${cell.state},${cell.sinceSec === null ? '?' : cell.sinceSec}s`;
223
+ }
224
+
194
225
  // ── 기록(TUI) ──
195
226
 
196
227
  export interface TuiRunStateRecorderDeps {
@@ -18,7 +18,8 @@ export function classifySeatTrafficProposal(message: string): 'hold' | 'UNHOLD'
18
18
  return null;
19
19
  }
20
20
 
21
- const defaultPath = (name: string): string => join(homedir(), 'elanous-hq', 'seat-state', 'OP', name);
21
+ export const opSeatStatePath = (name: string): string => join(homedir(), 'elanous-hq', 'seat-state', 'OP', name);
22
+ const defaultPath = opSeatStatePath;
22
23
  const missing = (error: unknown): boolean => (error as NodeJS.ErrnoException).code === 'ENOENT';
23
24
 
24
25
  /** A missing installation has no signal; an invalid or unreadable one cannot authorize a checklist launch. */
@@ -13,6 +13,7 @@
13
13
  import { linesOutsideFencedCode } from '../self-implement/goal-author.js';
14
14
  import { existsSync } from 'node:fs';
15
15
  import { debug } from '../debug/log.js';
16
+ import { appendRunLedgerEntry, runLedgerDir } from '../self-implement/run-ledger.js';
16
17
  import { recordAuthoredGoal } from './authored-goal-trace.js';
17
18
  import { findAskPathCandidates, type AskPathCandidateOptions } from './ask-path-candidates.js';
18
19
  import { retiredEntranceNotice, type EntranceDeclaration } from './entrance-registry.js';
@@ -849,6 +850,15 @@ export async function runAskLaunchFlow(
849
850
  const entrance = input.entrance;
850
851
  const askStartedAt = deps.now();
851
852
  const selfResolveClarifications = resolveSelfResolveClarifications(input.selfResolveClarifications);
853
+ // The Pod's logs.db disappears with the Job. Journal the two auto-authoring boundaries in its
854
+ // existing run ledger so the host's terminal collection can reemit them even on a failed Job.
855
+ const recordPodAuthoring = (event: 'ask-auto-target-candidates' | 'ask-auto-reauthor', data: Record<string, unknown>): void => {
856
+ if (process.env.ELANOUS_SUBSTRATE !== 'pod') return;
857
+ const runId = process.env.ELANOUS_RUN_ID ?? input.runId;
858
+ if (!runId) return;
859
+ try { appendRunLedgerEntry({ runId, event, data }, runLedgerDir()); }
860
+ catch (error) { deps.log('ask-auto-author-ledger-unavailable', { event, reason: error instanceof Error ? error.message : String(error) }, 'warn'); }
861
+ };
852
862
  deps.log('harness.entrance', {
853
863
  entrance: entrance.id,
854
864
  entranceStatus: entrance.status,
@@ -1082,6 +1092,7 @@ export async function runAskLaunchFlow(
1082
1092
  selection: { path: selectedPath ?? null, reason: selectionReason },
1083
1093
  ...(candidates.error === undefined ? {} : { error: candidates.error }),
1084
1094
  }, candidates.error === undefined && selectedPath ? 'info' : 'warn');
1095
+ recordPodAuthoring('ask-auto-target-candidates', { candidatePathCount: candidates.paths.length, selected: Boolean(selectedPath), outcome: selectionReason });
1085
1096
  }
1086
1097
  const selectedPaths = candidates.selection?.path
1087
1098
  ? [candidates.selection.path]
@@ -1093,6 +1104,7 @@ export async function runAskLaunchFlow(
1093
1104
  } else {
1094
1105
  deps.print('[ask] ⑴ ⚠️ 자동 재저작에 넣을 대상 경로 후보가 없다. 대상 경로를 확인해야 한다');
1095
1106
  }
1107
+ recordPodAuthoring('ask-auto-reauthor', { candidatePathCount: candidates.paths.length, firstPathCount: first.decision.result.paths.length, outcome: 'started' });
1096
1108
  authored = await authorAndPreflight(selectedPaths.length > 0
1097
1109
  ? `대상 경로: ${selectedPaths.join(' · ')}\n${input.askText}`
1098
1110
  : input.askText, selectedPaths);
@@ -1104,6 +1116,7 @@ export async function runAskLaunchFlow(
1104
1116
  secondPathCount: authored.decision.result.paths.length,
1105
1117
  helped: authored.decision.shouldLaunch,
1106
1118
  }, authored.decision.shouldLaunch ? 'info' : 'warn');
1119
+ recordPodAuthoring('ask-auto-reauthor', { candidatePathCount: candidates.paths.length, firstPathCount: first.decision.result.paths.length, secondPathCount: authored.decision.result.paths.length, helped: authored.decision.shouldLaunch });
1107
1120
  }
1108
1121
 
1109
1122
  const { goalFile: effectiveGoalFile, decision } = authored;