elanous 0.2.25 → 0.2.27

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 (244) 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/{1454-37fd16f479f5554a.js → 1454-a9c338f5bb556f42.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-9360b1285b2a346e.js → page-a1ce306364f08da9.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-07110da72c45e68f.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/board/page-d2d76d4a7eae91bd.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/botlab/page-30007380f2e2a3c1.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/bots/page-e5a9302c105725a3.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/ceo/page-b93c04b5311c97ec.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/chat/page-718a49e86bf25f9c.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/consult/{page-44eaad486bc98a45.js → page-20662900d6b4b455.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/control/{page-97fa6cf3d34c7eb9.js → page-f707a509515ac50c.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/decisions/{page-305cd294e754906e.js → page-6a16a48d1aa6e91f.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/design-check/page-fcb06e305f515527.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/editor/page-6acf5ec4e6c6d1af.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/exec/{page-45889e7070d11126.js → page-39f82acf619cca23.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/field/{page-16a2e893f83ae4d0.js → page-7c2c8f81b185f9c0.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/inside/page-ae7aff07e32676bb.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/intake/{page-56888bc744e08dac.js → page-482c60c1bc540d0a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/layout-7cb996c1aa08b6f5.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/live/{page-952c6ceec6d9de38.js → page-623e08fe4477d606.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/live-run/{page-6d0202af568f4c63.js → page-db460cd8aecb8490.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/loops/page-ff27902d0b11ef4f.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/market/{page-74366ae2693a7d95.js → page-8397a5c6ca569bf4.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-4fb6fc34e20c07b7.js → page-1917ea87ea71ab6b.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/morning/{page-e9502921f4f01b1f.js → page-25a302273b054fd7.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/{not-found-b6169ea2f09cf287.js → not-found-670510eda8b9443f.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/observatory/page-d73c3f1e00d93986.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-65caacf54389081e.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4bb7556e2e97a750.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-77e04b4f8762c666.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-b9cc8a10bc6078b4.js → page-15c0a4095baddfba.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/{page-3d3fc73dd187ed52.js → page-4eb96fc7241c2456.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/reflection/page-63e272a4cd3b6634.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-9c04dac0831c2bbb.js → page-0d64ff212dc55d03.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/sessions/page-7f096248993fc2d4.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-04de9ca6d52a3893.js → page-e47fc72049149a38.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/{page-62527677bbc2fdc4.js → page-f7d808e31e005895.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-9a2e09c4a311764c.js → page-5c2b24b5496dad73.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/{page-bfeb35d69e8b24bb.js → page-76cdb531668b5191.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/share/page-e2b63345bba1a11f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-b008aff96be56654.js → page-d376e0c83edc041a.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d6db55fe2c04d128.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/term/page-87b88f551f421d8b.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/today/{page-6445ac4cbb8dd869.js → page-dab163b338e3b966.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/trace/{page-852170303c6cd755.js → page-08019efef111ed2e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/vault/page-0260ffc7eda50db1.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-97c7361cc2a19319.js → page-980ce6fe36ad95eb.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-20162174b77f96cf.js → page-99c4e6c7345a9156.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-8c6901833f897496.js → page-0ab42e0f05823013.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ad9220422d20ba33.js +1 -0
  52. package/apps/pwa/out/approvals/index.html +1 -1
  53. package/apps/pwa/out/approvals/index.txt +13 -13
  54. package/apps/pwa/out/autopilot/index.html +1 -1
  55. package/apps/pwa/out/autopilot/index.txt +13 -13
  56. package/apps/pwa/out/board/index.html +1 -1
  57. package/apps/pwa/out/board/index.txt +13 -13
  58. package/apps/pwa/out/botlab/index.html +1 -1
  59. package/apps/pwa/out/botlab/index.txt +13 -13
  60. package/apps/pwa/out/bots/index.html +1 -1
  61. package/apps/pwa/out/bots/index.txt +13 -13
  62. package/apps/pwa/out/ceo/index.html +1 -1
  63. package/apps/pwa/out/ceo/index.txt +13 -13
  64. package/apps/pwa/out/chat/index.html +1 -1
  65. package/apps/pwa/out/chat/index.txt +13 -13
  66. package/apps/pwa/out/consult/index.html +1 -1
  67. package/apps/pwa/out/consult/index.txt +13 -13
  68. package/apps/pwa/out/control/index.html +1 -1
  69. package/apps/pwa/out/control/index.txt +13 -13
  70. package/apps/pwa/out/decisions/index.html +1 -1
  71. package/apps/pwa/out/decisions/index.txt +13 -13
  72. package/apps/pwa/out/design-check/index.html +1 -1
  73. package/apps/pwa/out/design-check/index.txt +13 -13
  74. package/apps/pwa/out/editor/index.html +1 -1
  75. package/apps/pwa/out/editor/index.txt +13 -13
  76. package/apps/pwa/out/exec/index.html +1 -1
  77. package/apps/pwa/out/exec/index.txt +13 -13
  78. package/apps/pwa/out/field/index.html +1 -1
  79. package/apps/pwa/out/field/index.txt +13 -13
  80. package/apps/pwa/out/healthz +1 -1
  81. package/apps/pwa/out/index.html +1 -1
  82. package/apps/pwa/out/index.txt +13 -13
  83. package/apps/pwa/out/inside/index.html +1 -1
  84. package/apps/pwa/out/inside/index.txt +13 -13
  85. package/apps/pwa/out/intake/index.html +1 -1
  86. package/apps/pwa/out/intake/index.txt +13 -13
  87. package/apps/pwa/out/live/index.html +1 -1
  88. package/apps/pwa/out/live/index.txt +13 -13
  89. package/apps/pwa/out/live-run/index.html +1 -1
  90. package/apps/pwa/out/live-run/index.txt +13 -13
  91. package/apps/pwa/out/loops/index.html +1 -1
  92. package/apps/pwa/out/loops/index.txt +13 -13
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +13 -13
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +13 -13
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +13 -13
  101. package/apps/pwa/out/ops/checklist/index.html +1 -1
  102. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  103. package/apps/pwa/out/ops/release/index.html +1 -1
  104. package/apps/pwa/out/ops/release/index.txt +13 -13
  105. package/apps/pwa/out/ops/seats/index.html +1 -1
  106. package/apps/pwa/out/ops/seats/index.txt +13 -13
  107. package/apps/pwa/out/outputs/index.html +1 -1
  108. package/apps/pwa/out/outputs/index.txt +13 -13
  109. package/apps/pwa/out/reflection/index.html +1 -1
  110. package/apps/pwa/out/reflection/index.txt +14 -14
  111. package/apps/pwa/out/scheduler/index.html +1 -1
  112. package/apps/pwa/out/scheduler/index.txt +13 -13
  113. package/apps/pwa/out/sessions/index.html +1 -1
  114. package/apps/pwa/out/sessions/index.txt +13 -13
  115. package/apps/pwa/out/settings/devices/index.html +1 -1
  116. package/apps/pwa/out/settings/devices/index.txt +13 -13
  117. package/apps/pwa/out/settings/index.html +1 -1
  118. package/apps/pwa/out/settings/index.txt +13 -13
  119. package/apps/pwa/out/setup/done/index.html +1 -1
  120. package/apps/pwa/out/setup/done/index.txt +13 -13
  121. package/apps/pwa/out/setup/index.html +1 -1
  122. package/apps/pwa/out/setup/index.txt +13 -13
  123. package/apps/pwa/out/share/index.html +1 -1
  124. package/apps/pwa/out/share/index.txt +13 -13
  125. package/apps/pwa/out/showroom/index.html +1 -1
  126. package/apps/pwa/out/showroom/index.txt +13 -13
  127. package/apps/pwa/out/tasks/index.html +1 -1
  128. package/apps/pwa/out/tasks/index.txt +13 -13
  129. package/apps/pwa/out/term/index.html +1 -1
  130. package/apps/pwa/out/term/index.txt +13 -13
  131. package/apps/pwa/out/today/index.html +1 -1
  132. package/apps/pwa/out/today/index.txt +13 -13
  133. package/apps/pwa/out/trace/index.html +1 -1
  134. package/apps/pwa/out/trace/index.txt +13 -13
  135. package/apps/pwa/out/vault/index.html +1 -1
  136. package/apps/pwa/out/vault/index.txt +13 -13
  137. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  138. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  139. package/apps/pwa/out/workflows/index.html +1 -1
  140. package/apps/pwa/out/workflows/index.txt +13 -13
  141. package/apps/pwa/out/workspace/index.html +1 -1
  142. package/apps/pwa/out/workspace/index.txt +13 -13
  143. package/apps/pwa/out/worktrees/index.html +1 -1
  144. package/apps/pwa/out/worktrees/index.txt +13 -13
  145. package/package.json +1 -1
  146. package/scripts/ci-typecheck-changed.ts +81 -2
  147. package/scripts/coord-msg-bridge.ts +209 -0
  148. package/scripts/coord-post.sh +40 -7
  149. package/scripts/coord-requests-bridge.ts +10 -5
  150. package/scripts/expose-rubric-suggest.ts +1 -1
  151. package/scripts/measure-tsc-affected-roots.ts +10 -1
  152. package/scripts/release-loop/export-check-node.ts +8 -5
  153. package/scripts/release-loop/node-verdict.ts +7 -2
  154. package/src/agent/task-notification.ts +30 -1
  155. package/src/agent-substrate/review-observation.ts +9 -2
  156. package/src/agent-substrate/self-review-cli.ts +28 -6
  157. package/src/boot/daemon-tools/self-implement.ts +72 -4
  158. package/src/budget/llm-cost.ts +5 -1
  159. package/src/cli/control-cli.ts +29 -0
  160. package/src/cli/doctor-cli.ts +90 -8
  161. package/src/cli/msg-cli.ts +15 -5
  162. package/src/cli/pr-cli.ts +53 -0
  163. package/src/cli/pr-land-overlap-evidence.ts +18 -10
  164. package/src/cli/release-cli.ts +32 -5
  165. package/src/cli/seat-cli.ts +121 -0
  166. package/src/cli/self-utterances-cli.ts +140 -0
  167. package/src/cli/tasks-cli.ts +139 -9
  168. package/src/context-card/cell.ts +40 -0
  169. package/src/dashboard/boot-timing.ts +35 -0
  170. package/src/dashboard/index.ts +86 -24
  171. package/src/dashboard/input/chat-main-plain-turn-runtime.ts +26 -0
  172. package/src/dashboard/slash-runtime/dashboard-handlers.ts +6 -0
  173. package/src/debug/log.ts +1 -1
  174. package/src/domains/release-tool.ts +29 -12
  175. package/src/domains/surface-events.ts +16 -0
  176. package/src/elanous-config-dir.ts +2 -2
  177. package/src/fleet/host-doctor.ts +73 -2
  178. package/src/graph-runner/runner.ts +3 -1
  179. package/src/harness/harness-cli-command.ts +46 -4
  180. package/src/harness/harness-stop.ts +11 -7
  181. package/src/index.ts +35 -4
  182. package/src/instance/current.ts +2 -6
  183. package/src/instance/resolve.ts +5 -6
  184. package/src/llm/openai-sampling-fields.ts +89 -0
  185. package/src/llm/runtime-model-family.ts +61 -0
  186. package/src/llm/zai-coding-plan.ts +27 -1
  187. package/src/llm.ts +13 -89
  188. package/src/machines/machine-ledger.ts +31 -3
  189. package/src/maturity/cli-maturity.ts +13 -3
  190. package/src/msg/msg-store.ts +69 -7
  191. package/src/ops/overview-collect.ts +3 -2
  192. package/src/ops/overview-owner.ts +29 -0
  193. package/src/plugin-hooks/shell-hook.ts +17 -2
  194. package/src/plugin-missions/evaluator-runner.ts +18 -2
  195. package/src/plugins/install/plugin-install.ts +11 -1
  196. package/src/prompt-enhance/enhance.ts +11 -0
  197. package/src/release-loop/checklist.ts +41 -4
  198. package/src/release-loop/feature-store.ts +10 -9
  199. package/src/seat-loop/seat-loop.ts +5 -1
  200. package/src/self-dev/draft-sweep.ts +23 -14
  201. package/src/self-dev/orchestrate.ts +6 -2
  202. package/src/self-dev/run-supervisor.ts +35 -1
  203. package/src/self-implement/goal-author.ts +3 -27
  204. package/src/self-implement/goal-command-execution.ts +26 -0
  205. package/src/self-implement/orchestrator.ts +2 -1
  206. package/src/self-implement/pwa-import-graph.ts +194 -7
  207. package/src/self-implement/self-implement-runtime.ts +126 -21
  208. package/src/session/index.ts +14 -4
  209. package/src/session-runtime/index.ts +55 -11
  210. package/src/task-agent/cover.ts +28 -9
  211. package/src/task-agent/live-moves.ts +14 -6
  212. package/src/task-agent/rejudge-on-head.ts +220 -23
  213. package/src/task-agent/review-result-capture.ts +64 -1
  214. package/src/task-agent/shadow.ts +137 -22
  215. package/src/task-agent/task-hand.ts +18 -2
  216. package/src/task-orchestrator/surfaces/self-implement-pod.ts +1 -0
  217. package/src/tool-runtime/tool-search-runtime.ts +25 -0
  218. package/src/tui.ts +20 -1
  219. package/src/user-config.ts +3 -59
  220. package/src/version/packed-revision.json +1 -1
  221. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f08f6529d09ffae1.js +0 -1
  222. package/apps/pwa/out/_next/static/chunks/app/board/page-f000d54d258f9ef6.js +0 -1
  223. package/apps/pwa/out/_next/static/chunks/app/botlab/page-b12149c51f7d04b1.js +0 -1
  224. package/apps/pwa/out/_next/static/chunks/app/bots/page-22f674e7e4c2c9d5.js +0 -1
  225. package/apps/pwa/out/_next/static/chunks/app/ceo/page-8888733e40cea6d1.js +0 -1
  226. package/apps/pwa/out/_next/static/chunks/app/chat/page-9717ec6964b92276.js +0 -1
  227. package/apps/pwa/out/_next/static/chunks/app/design-check/page-dea4b795741126fd.js +0 -1
  228. package/apps/pwa/out/_next/static/chunks/app/editor/page-35e05d7450461a67.js +0 -1
  229. package/apps/pwa/out/_next/static/chunks/app/inside/page-b2638013c0bf7dfd.js +0 -1
  230. package/apps/pwa/out/_next/static/chunks/app/layout-c44f93a41167536f.js +0 -1
  231. package/apps/pwa/out/_next/static/chunks/app/loops/page-1b810474507f3702.js +0 -1
  232. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6a14333d1b1b3967.js +0 -1
  233. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-8c91a9a66c9ec90a.js +0 -1
  234. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4e634c5b98fd4e93.js +0 -1
  235. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-ed3070d637850548.js +0 -1
  236. package/apps/pwa/out/_next/static/chunks/app/reflection/page-128458c8d86fbb5f.js +0 -1
  237. package/apps/pwa/out/_next/static/chunks/app/sessions/page-cf0c2b0183991d09.js +0 -1
  238. package/apps/pwa/out/_next/static/chunks/app/share/page-89cb33bced824e4a.js +0 -1
  239. package/apps/pwa/out/_next/static/chunks/app/tasks/page-814394dcbae789b1.js +0 -1
  240. package/apps/pwa/out/_next/static/chunks/app/term/page-6a87325c87f09dd2.js +0 -1
  241. package/apps/pwa/out/_next/static/chunks/app/vault/page-087ceeb975ef272b.js +0 -1
  242. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-05a4b2c9b91034aa.js +0 -1
  243. /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → Ev2jivDMTSoEc-5rIXdcy}/_buildManifest.js +0 -0
  244. /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → Ev2jivDMTSoEc-5rIXdcy}/_ssgManifest.js +0 -0
@@ -965,6 +965,7 @@ import {
965
965
  enterSessionControlMode,
966
966
  exitSessionControlMode,
967
967
  isSessionControlActive,
968
+ routeDevHarnessSubmitIntent,
968
969
  parseSessionControlSlash,
969
970
  parseSessionSurfaceSlash,
970
971
  resolveSessionInputModeFromChatMode,
@@ -1023,7 +1024,7 @@ import { expandPromptReferences } from '../prompt/references.js';
1023
1024
  import { spawnCodingAgent, CodingAgentBinaryMissing } from '../terminal/coding-agent.js';
1024
1025
  import type { TerminalSession } from '../terminal/session-registry.js';
1025
1026
  import { createSessionPickerModal } from '../session/picker-modal.js';
1026
- import { listSessions as listChatSessionsForResume } from '../session/index.js';
1027
+ import { listSessions as listChatSessionsForResume, adoptSession as adoptChatSession, setActiveSessionId as markActiveChatSession } from '../session/index.js';
1027
1028
  // TUI 부활 S-b — 타임트래블 포크 엔진 (codex ForkSnapshot 동형 · 2026-07-09
1028
1029
  // 이식 후 호출처 0 이던 방치 자산 회수). 순수 히스토리 절단 로직만 소비;
1029
1030
  // 영속은 세션 스토어(forkSessionFromHistory)가 담당한다.
@@ -1179,6 +1180,20 @@ import { buildDebugCallStack } from '../debug/call-stack.js';
1179
1180
  import { buildAstGrepHostTool } from '../skills/tools/ast-grep.js';
1180
1181
  import { buildLspHostTool } from '../skills/tools/lsp/index.js';
1181
1182
  import { PluginHost } from '../plugins/core/host.js';
1183
+
1184
+ /** Register the same L2 host tools used by the live TUI before each turn's catalog is assembled. */
1185
+ export function registerDashboardCoreHostTools(host: Pick<PluginHost, 'registerHostTool'>): void {
1186
+ const coreMod = require('../domains/core-tools.js') as typeof import('../domains/core-tools.js');
1187
+ const core = coreMod.buildCoreTools();
1188
+ for (const spec of core.specs) {
1189
+ host.registerHostTool({
1190
+ name: spec.name,
1191
+ description: spec.description,
1192
+ parameters: spec.parameters,
1193
+ handler: async (args) => core.dispatch(spec.name, args as Record<string, unknown>),
1194
+ });
1195
+ }
1196
+ }
1182
1197
  import { registerDashboardPaneHostTools } from './panes/host-tools.js';
1183
1198
  import { DEFAULT_THEME_TOKENS, mergeThemeTokens, resolveThemeTokens, type ThemeTokenInput, type ThemeTokens } from '../theme/tokens.js';
1184
1199
  import { resolveActiveTheme, setActivePresetInConfig } from './render/theme-resolver.js';
@@ -1407,6 +1422,8 @@ const isPluginBusy = (host: { active(): { plugin: { isBusy?: (s: any) => boolean
1407
1422
  // ── Main dashboard ──
1408
1423
 
1409
1424
  export interface ShowDashboardOptions {
1425
+ /** Internal startup timing signal for the first successfully flushed dashboard frame. */
1426
+ onFirstFrameRendered?: () => void;
1410
1427
  /** Force debug sinks on at launch (file + mirror). Equivalent to
1411
1428
  * typing `/debug on` immediately after startup. Paired with
1412
1429
  * chatOnly=true by the `elanous --debug` CLI flag so every traced
@@ -1501,6 +1518,16 @@ export function buildDashboardTurnTypeaheadPromptZone(input: TurnTypeaheadPrompt
1501
1518
  };
1502
1519
  }
1503
1520
 
1521
+ /** The chat-main submit branch calls this before any model dispatch. Keeping
1522
+ * the decision here lets the dashboard test execute the same entry point. */
1523
+ export function resolveDashboardHarnessSubmitIntent(
1524
+ text: string,
1525
+ options: Parameters<typeof resolveDashboardChatMainSubmitIntent>[1],
1526
+ resolveIntent: typeof resolveDashboardChatMainSubmitIntent = resolveDashboardChatMainSubmitIntent,
1527
+ ) {
1528
+ return routeDevHarnessSubmitIntent(resolveIntent(text, options));
1529
+ }
1530
+
1504
1531
  /** 프롬프트 zone 에 관측만 덧입힌다 — 렌더 산출은 `inner` 가 소유한다.
1505
1532
  *
1506
1533
  * ⭐ **왜 뽑았나**(무인 리뷰 must-fix · 2026-08-01): 종전엔 이 래퍼가 `zones.push({...})` 안에
@@ -1567,6 +1594,7 @@ export interface DashboardFrameInput {
1567
1594
  promptCaret: CursorState | null;
1568
1595
  suppressPromptArea: boolean;
1569
1596
  onEssentialFrameCursorDecision?: (observation: EssentialFrameCursorObservation) => void;
1597
+ onFrameWriteComplete?: (error?: Error | null) => void;
1570
1598
  }
1571
1599
 
1572
1600
  export interface EssentialFrameCursorObservation {
@@ -1612,6 +1640,7 @@ export function renderDashboardFrame(
1612
1640
  overlay: input.overlay,
1613
1641
  force: input.force,
1614
1642
  cursor: paintCursor(essentialDecision.cursor ?? input.coordinatorCursor ?? null),
1643
+ onWriteComplete: input.onFrameWriteComplete,
1615
1644
  });
1616
1645
  input.onEssentialFrameCursorDecision?.({
1617
1646
  caret: essentialDecision.cursor,
@@ -1901,6 +1930,7 @@ export function renderDashboardFirstScreenBand(input: {
1901
1930
  }
1902
1931
 
1903
1932
  export async function showDashboard(opts: ShowDashboardOptions = {}): Promise<DashboardAction> {
1933
+ let firstDashboardFrameRendered = false;
1904
1934
  await awaitGlobalPersonaLoad().catch(() => {});
1905
1935
  const dashboardQuitConfirmationState = createQuitConfirmationState();
1906
1936
  const signalQuitConfirmationState = createQuitConfirmationState();
@@ -1910,7 +1940,11 @@ export async function showDashboard(opts: ShowDashboardOptions = {}): Promise<Da
1910
1940
  closeTui();
1911
1941
  };
1912
1942
  const exitDashboardTui = (): void => {
1913
- closeDashboardForExit({ closeTui: closeDashboardTui, confirmationState: dashboardQuitConfirmationState });
1943
+ closeDashboardForExit({
1944
+ closeTui: closeDashboardTui,
1945
+ confirmationState: dashboardQuitConfirmationState,
1946
+ getSessionId: (): string | undefined => acpSessionIdRef.current ?? undefined,
1947
+ });
1914
1948
  };
1915
1949
  // ⭐⭐⭐ `N1` — 신호 경로(`SIGINT`/`SIGTERM`)와 저수준 `Ctrl+C` 도 같은 종료를 타게 «등록»한다.
1916
1950
  // ⛔ 종전엔 그 둘만 세션 안내를 «못 받았다»(일곱 경로 중 다섯만 받았다).
@@ -1920,6 +1954,7 @@ export async function showDashboard(opts: ShowDashboardOptions = {}): Promise<Da
1920
1954
  closeTui: () => { /* 신호 경로: cleanup() 이 이미 복원했다 */ },
1921
1955
  confirmationState: signalQuitConfirmationState,
1922
1956
  forceQuit: true,
1957
+ getSessionId: (): string | undefined => acpSessionIdRef.current ?? undefined,
1923
1958
  });
1924
1959
  });
1925
1960
  // E3 (§7.4 TS baseline reduction · 2026-05-17) — forward declarations
@@ -3342,6 +3377,8 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
3342
3377
  // ⛔ `attachedSessionId`(대화 세션)와 «다른 축»이다 — 루프는 ACP 세션으로 스코프를 연다.
3343
3378
  // ⚠️ 세션은 이 함수 «뒤»에서 만들어지므로 참조로 둔다(호출 시점엔 채워져 있다).
3344
3379
  const acpSessionIdRef: { current: string | null } = { current: null };
3380
+ const adoptedAcpSessionIds = new Set<string>();
3381
+ let activeAcpSessionId: string | null = null;
3345
3382
  const activeSessionIdForTypeahead = (): string | null => acpSessionIdRef.current;
3346
3383
  const paintTurnTypeaheadEcho = (): void => {
3347
3384
  const { rows: tr, cols: tc } = termSize();
@@ -10014,16 +10051,7 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
10014
10051
  // family(전 surface 기본) — 이로써 TUI 에서도 "P2 왜 실패?" 를 엘라누스가 툴로 상황판단한다.
10015
10052
  // fail-soft: 코어 도구 로드 실패가 대시보드 부팅을 막지 않는다.
10016
10053
  try {
10017
- const coreMod = require('../domains/core-tools.js') as typeof import('../domains/core-tools.js');
10018
- const core = coreMod.buildCoreTools();
10019
- for (const spec of core.specs) {
10020
- pluginHost.registerHostTool({
10021
- name: spec.name,
10022
- description: spec.description,
10023
- parameters: spec.parameters,
10024
- handler: async (args) => core.dispatch(spec.name, args as Record<string, unknown>),
10025
- });
10026
- }
10054
+ registerDashboardCoreHostTools(pluginHost);
10027
10055
  } catch { /* fail-soft */ }
10028
10056
  // LSP (Phase L3 of 내부 문서 `ROADMAP-lsp-integration`) — structural /
10029
10057
  // symbol-level source queries via typescript-language-server. Pool
@@ -11530,6 +11558,11 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
11530
11558
  // ⭐ Single flush: resolve the prompt caret after overlays.
11531
11559
  const frameCursorDecision = display.cursorDecision();
11532
11560
  renderDashboardFrame(baseLines, composeDashboardFrameWithCoordinatorModal(display, {
11561
+ onFrameWriteComplete: firstDashboardFrameRendered ? undefined : (error) => {
11562
+ if (error || firstDashboardFrameRendered) return;
11563
+ firstDashboardFrameRendered = true;
11564
+ try { opts.onFirstFrameRendered?.(); } catch { /* timing must not break dashboard */ }
11565
+ },
11533
11566
  overlay: modalFrame,
11534
11567
  force: forceFromCoordinator || modalKey !== lastModalKey,
11535
11568
  cursorOwner: frameCursorDecision.owner,
@@ -17422,7 +17455,7 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
17422
17455
  // resource_link blocks so dropped screenshots and PDFs flow
17423
17456
  // naturally to the agent. debug.log gates the snapshot so
17424
17457
  // off-state stays zero-cost.
17425
- const submitIntent = resolveDashboardChatMainSubmitIntent(input.text, {
17458
+ const submitIntent = resolveDashboardHarnessSubmitIntent(input.text, {
17426
17459
  stickyBackend: dashboardAcpChat.getSticky(),
17427
17460
  source: input.externalSubmitSource,
17428
17461
  });
@@ -17443,7 +17476,10 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
17443
17476
  }
17444
17477
  if (submitIntent.kind === 'submit-turn' && submitIntent.route === 'sticky-acp') {
17445
17478
  await runDashboardChatMainSubmitIntent({
17446
- submit: createDashboardChatMainTurnSubmit(submitIntent),
17479
+ submit: createDashboardChatMainTurnSubmit({
17480
+ ...submitIntent,
17481
+ sessionId: submitIntent.sessionId ?? dashboardAcpSession.currentSessionId,
17482
+ }),
17447
17483
  beforeExecute: (submit) => {
17448
17484
  maybeEmitDashboardSubmitQuickPass({
17449
17485
  submit,
@@ -19186,16 +19222,19 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
19186
19222
  do {
19187
19223
  const turnText = goalLoopUserText ?? tok.text;
19188
19224
  await runDashboardChatMainSubmitIntent({
19189
- submit: createDashboardChatMainTurnSubmit(
19190
- submitIntent.kind === 'submit-turn' && submitIntent.route === 'plain' && goalLoopUserText === null
19191
- ? submitIntent
19192
- : {
19193
- kind: 'submit-turn',
19194
- source: { kind: 'keyboard', surface: 'dashboard-chat-main' },
19195
- text: turnText,
19196
- route: goalLoopUserText === null ? 'plain' : 'plain',
19197
- },
19198
- ),
19225
+ submit: createDashboardChatMainTurnSubmit({
19226
+ ...(submitIntent.kind === 'submit-turn' && submitIntent.route === 'plain' && goalLoopUserText === null
19227
+ ? submitIntent
19228
+ : {
19229
+ kind: 'submit-turn' as const,
19230
+ source: { kind: 'keyboard' as const, surface: 'dashboard-chat-main' },
19231
+ text: turnText,
19232
+ route: 'plain' as const,
19233
+ }),
19234
+ sessionId: submitIntent.kind === 'submit-turn'
19235
+ ? submitIntent.sessionId ?? dashboardAcpSession.currentSessionId
19236
+ : dashboardAcpSession.currentSessionId,
19237
+ }),
19199
19238
  beforeExecute: (submit) => {
19200
19239
  maybeEmitDashboardSubmitQuickPass({
19201
19240
  submit,
@@ -19246,6 +19285,29 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
19246
19285
  }
19247
19286
  } while (goalLoopUserText !== null);
19248
19287
 
19288
+ // SESSION-RESUME-ISOLATED: 완료된 턴의 ACP 세션을 원장에 등록하고 active 로 지정한다.
19289
+ // 서버의 완료-채택(persistCompletedTuiAcpTurn)은 origin_session 메타가 실렸을 때만
19290
+ // 도는데, 격리 우주 첫 턴에는 origin 세션이 없어 메타가 안 실린다 — 여기가 그 갭을 닫는다.
19291
+ const completedAcpSessionId = dashboardAcpSession.currentSessionId;
19292
+ if (completedAcpSessionId) {
19293
+ acpSessionIdRef.current = completedAcpSessionId;
19294
+ // 원격 데몬 세션의 원장은 데몬이 소유한다. 로컬 등록은 세션마다 한 번만 한다.
19295
+ if (!daemonAttached && activeAcpSessionId !== completedAcpSessionId) {
19296
+ try {
19297
+ if (!adoptedAcpSessionIds.has(completedAcpSessionId)) {
19298
+ adoptChatSession(completedAcpSessionId, { source: 'tui', transport: 'acp' });
19299
+ adoptedAcpSessionIds.add(completedAcpSessionId);
19300
+ }
19301
+ markActiveChatSession(completedAcpSessionId);
19302
+ activeAcpSessionId = completedAcpSessionId;
19303
+ } catch (error) {
19304
+ // 등록 실패가 대화 흐름을 끊지 않되, 다음 턴 재시도 전 원인을 남긴다.
19305
+ debug.log('session.adopt', 'failed', { sessionId: completedAcpSessionId, error: String(error) });
19306
+ pushDebugLine(C.warning(`session adopt failed: ${String(error)}`));
19307
+ }
19308
+ }
19309
+ }
19310
+
19249
19311
  continue; // stay in input mode after chat
19250
19312
  } // end while (input mode loop)
19251
19313
  // Disarm the prompt-repaint hook: textInput's drawAll
@@ -191,10 +191,15 @@ export async function runDashboardChatMainPlainTurn(
191
191
  // 「사다리가 안 걸린 것」과 「걸렸는데 키가 딴 데로 간 것」을 ***원리상 못 갈랐다***.
192
192
  // ⭐ 이 한 줄이 그 둘을 가른다 — 이 이벤트가 «있는데» typeahead 가 0이면 후자다.
193
193
  // ⛔ 행동을 바꾸지 않는다(관측만). 처방은 `RFC-tui-mid-turn-input-queue-2026-08-19` §2 Q1.
194
+ const startedAtDate = new Date(deps.turnStartedAt);
195
+ const turnStartedAtIso = Number.isFinite(startedAtDate.getTime())
196
+ ? startedAtDate.toISOString()
197
+ : undefined;
194
198
  try {
195
199
  deps.debugLog?.('chat-main.plain-turn', 'streaming-keys-attached', {
196
200
  userTextBytes: Buffer.byteLength(userText ?? '', 'utf8'),
197
201
  turnStartedAt: deps.turnStartedAt,
202
+ turnStartedAtIso,
198
203
  });
199
204
  } catch { /* fail-soft — 관측이 턴을 막지 않는다 */ }
200
205
  let stage = 'streaming-keys-attached';
@@ -291,11 +296,32 @@ export async function runDashboardChatMainPlainTurn(
291
296
  });
292
297
 
293
298
  if (finishIfAborted()) return;
299
+ // TUI-FIRST-TURN-STALL — 09:48 표본에서 llm.stream consume-start 뒤
300
+ // first-token 이벤트가 0건이어서 「첫 토큰까지의 구간」을 잴 표면이 없었다.
301
+ // turnStartedAt 를 재사용해 첫 비어 있지 않은 text chunk 에 정확히 한 번
302
+ // chat.turn/first-token 을 남긴다. 도구 호출이 먼저 흘러도 text 가 도착해야
303
+ // 기록되므로 request→first-token 구간은 그대로 측정된다. ISO 시각 좌표는
304
+ // pty record 및 TUI-BOOT-TIME(src/index.ts) 부팅 계측과의 대응용이다.
305
+ let firstTokenLogged = false;
306
+ const logFirstToken = (chunk: string): void => {
307
+ if (firstTokenLogged || !chunk || abortCtrl.signal.aborted) return;
308
+ firstTokenLogged = true;
309
+ const firstTokenAt = Date.now();
310
+ try {
311
+ deps.debugLog?.('chat.turn', 'first-token', {
312
+ ms: firstTokenAt - deps.turnStartedAt,
313
+ turnStartedAt: deps.turnStartedAt,
314
+ turnStartedAtIso,
315
+ firstTokenAtIso: new Date(firstTokenAt).toISOString(),
316
+ });
317
+ } catch { /* fail-soft — 관측이 턴을 막지 않는다 */ }
318
+ };
294
319
  const streamResult = await runDashboardPlainTurnStream({
295
320
  userText,
296
321
  abortCtrl,
297
322
  acpSession: deps.acpSession,
298
323
  onText: (chunk, accumulated) => {
324
+ logFirstToken(chunk);
299
325
  turnStreamRuntime.onText(chunk, accumulated);
300
326
  },
301
327
  onToolCall: (call) => {
@@ -2490,6 +2490,12 @@ export function buildDashboardSlashRegistry(nowDeps?: ContextNowDeps, decideLedg
2490
2490
  });
2491
2491
  if (result.kind === 'launch') {
2492
2492
  ctx.chatLines.push(ctx.accent(` ✅ 전제 검사 통과 — 골: ${result.goalFile}`));
2493
+ const { resolveObserveOnlyDecision } = await import('../../self-implement/observe-only.js');
2494
+ if (resolveObserveOnlyDecision().enabled) {
2495
+ ctx.chatLines.push(ctx.muted(' 관측 전용이라 런을 발사하지 않았습니다 · 코드도 안 바꿨습니다'));
2496
+ ctx.chatLines.push(ctx.muted(' 조회: elanous logs --category self-implement --since 10m'));
2497
+ return;
2498
+ }
2493
2499
  // ⭐ 런을 «띄운다» — detached 라 이 TUI 를 닫아도 런은 산다.
2494
2500
  // ⛔ spawn 을 여기서 새로 쓰지 않는다 — `launchDevGoalFileDetached` «한 자리»가 인자·env 를 갖는다.
2495
2501
  // ⛔ 그리고 「띄웠다」와 「끝났다」는 다른 값이다 — 이 표면은 «띄운 것»까지만 말한다.
package/src/debug/log.ts CHANGED
@@ -949,7 +949,7 @@ const SECRET_TEXT_RULES: readonly { readonly id: string; readonly re: RegExp; re
949
949
  { id: 'gcp-api-key', re: /\b(AIza)[0-9A-Za-z_-]{35}\b/g, to: '$1***' },
950
950
  { id: 'jwt', re: /\b(ey)[A-Za-z0-9_-]{10,}\.ey[A-Za-z0-9._\\/-]{10,}/g, to: '$1***' },
951
951
  // gitleaks: generic-api-key 축소판 — `key = value` 형태.
952
- { id: 'generic-api-key', re: /\b(api[_-]?key|secret|password|passwd|token|credential)\b(\s*[:=]\s*)["']?[^\s"',;)]{4,}/gi, to: '$1$2***' },
952
+ { id: 'generic-api-key', re: /\b(access[_-]?token|refresh[_-]?token|api[_-]?key|secret|password|passwd|token|credential)\b(\s*[:=]\s*)["']?[^\s"',;)]{4,}/gi, to: '$1$2***' },
953
953
  // 환경변수 형태 `OPENROUTER_API_KEY=…` — 위 규칙은 `_API_KEY` 앞에 단어 경계가 없어 못 잡는다. 값에 글자가 있어야 한다(`MAX_TOKEN=4096` 은 그대로).
954
954
  { id: 'env-secret', re: /\b([A-Z][A-Z0-9]*_(?:[A-Z0-9]+_)*(?:API_KEY|TOKEN|SECRET|PASSWORD|ACCESS_KEY))(\s*[:=]\s*)["']?(?=[^\s"',;)]*[A-Za-z])[^\s"',;)]{8,}/g, to: '$1$2***' },
955
955
  // BEDROCK-PROVIDER — `AWS_BEARER_TOKEN_BEDROCK=…` 은 이름이 TOKEN 으로 «끝나지» 않아 위 규칙이 못 잡는다.
@@ -21,12 +21,15 @@ export const RELEASE_STATUS_SPEC: LLMToolSpec = {
21
21
  + 'topic=cell: id 칸 하나의 상태·근거와 살핀 판.\n'
22
22
  + 'topic=schedule: 발행 이후 판의 컷·착지·발행·동결 일정.\n'
23
23
  + 'topic=ops: 도는 런·발행·늦은 스케줄·자리·열린 결정의 출처 있는 개관.\n'
24
- + 'topic 과 action 을 동시에 쓰지 않는다 — 둘 중 하나만: topic 은 읽을 대상, action 은 짧은 요약.\n'
24
+ + 'topic 과 action 을 동시에 쓰면 topic 을 우선하고 action 은 무시한다. topic 은 읽을 대상, action 은 단독 조회의 짧은 요약.\n'
25
+ + 'topic=features 에서는 owner·status 로 칸을 필터링할 수 있다.\n'
25
26
  + '발행·버전·피처·체크리스트·칸·컷·일정·운영 상태 질문은 Grep·ListDir·파일 검색보다 이 도구를 먼저 부른다 — 그 답은 파일이 아니라 원장에 있다.',
26
27
  parameters: { type: 'object', properties: {
27
28
  version: { type: 'string', description: '조회할 판. 생략하면 개발 판과 다음 판.' },
28
29
  topic: { type: 'string', enum: ['status', 'features', 'cell', 'schedule', 'ops'], description: '읽을 대상(기본 status).' },
29
30
  action: { type: 'string', enum: ['schedule', 'blocked', 'landed'], description: '짧은 읽기 요약: 다음 컷 일정·막힌 칸·최근 24시간 병합 PR.' },
31
+ owner: { type: 'string', description: 'topic=features 때 담당 읽기 필터.' },
32
+ status: { type: 'string', description: 'topic=features 때 상태 읽기 필터.' },
30
33
  id: { type: 'string', description: 'topic=cell 때 찾을 칸 id(필수).' },
31
34
  }, required: [] },
32
35
  };
@@ -122,10 +125,23 @@ function cellView(item: ChecklistItem) {
122
125
  export function dispatchReleaseStatus(args: Record<string, unknown>, deps: ReleaseToolDeps = {}): { text: string; structured: Record<string, unknown> } {
123
126
  const topic = args.topic === undefined ? 'status' : args.topic;
124
127
  if (typeof topic !== 'string' || !['status', 'features', 'cell', 'schedule', 'ops'].includes(topic)) throw new Error('topic 은 status, features, cell, schedule, ops 중 하나');
125
- if (args.topic !== undefined && args.action !== undefined) {
126
- throw new Error(`topic 과 action 을 동시에 보낼 수 없습니다 — 둘 중 하나만: topic=${topic} 는 읽을 대상, action 은 짧은 요약(schedule·blocked·landed)`);
127
- }
128
+ const ignoredAction = args.topic !== undefined && args.action !== undefined ? args.action : undefined;
129
+ const withIgnoredAction = (result: { text: string; structured: Record<string, unknown> }) => ignoredAction === undefined
130
+ ? result
131
+ : { ...result, text: `action=${String(ignoredAction)} 은 무시됨 — topic 과 같이 못 쓴다\n${result.text}` };
128
132
  const requested = args.version;
133
+ if (topic === 'features') {
134
+ for (const key of ['owner', 'status'] as const) {
135
+ if (args[key] !== undefined && (typeof args[key] !== 'string' || !args[key].trim())) throw new Error(`${key} 값이 필요합니다`);
136
+ }
137
+ }
138
+ const filters = topic === 'features' ? {
139
+ ...(typeof args.owner === 'string' ? { owner: args.owner } : {}),
140
+ ...(typeof args.status === 'string' ? { status: args.status } : {}),
141
+ } : {};
142
+ const hasFeatureFilters = Object.keys(filters).length > 0;
143
+ const featureItemMatches = (item: ChecklistItem) =>
144
+ (!('owner' in filters) || item.owner === filters.owner) && (!('status' in filters) || item.status === filters.status);
129
145
  if (requested !== undefined && (typeof requested !== 'string' || !requested.trim())) throw new Error('판 이름이 필요합니다');
130
146
  const checklist = deps.checklist ?? listChecklist;
131
147
  if (topic === 'ops') {
@@ -133,10 +149,10 @@ export function dispatchReleaseStatus(args: Record<string, unknown>, deps: Relea
133
149
  const kinds: ContextFact['kind'][] = ['run', 'release', 'schedule-late', 'seat', 'decision'];
134
150
  const facts = answer.facts.filter(fact => kinds.includes(fact.kind));
135
151
  debug.log('release.tool', 'status', { action: 'status', topic, version: requested ?? null });
136
- return { text: facts.length ? facts.map(fact => JSON.stringify(fact)).join('\n') : '운영 개관: 관측된 사실 없음', structured: { facts } };
152
+ return withIgnoredAction({ text: facts.length ? facts.map(fact => JSON.stringify(fact)).join('\n') : '운영 개관: 관측된 사실 없음', structured: { facts } });
137
153
  }
138
154
  const dev = checklistDevVersion(deps.devVersion?.() ?? devVersion());
139
- if (args.action !== undefined) {
155
+ if (ignoredAction === undefined && args.action !== undefined) {
140
156
  const action = args.action;
141
157
  if (typeof action !== 'string' || !['schedule', 'blocked', 'landed'].includes(action)) {
142
158
  throw new Error('action 은 schedule, blocked, landed 중 하나여야 합니다');
@@ -178,12 +194,13 @@ export function dispatchReleaseStatus(args: Record<string, unknown>, deps: Relea
178
194
  const target = (requested as string | undefined) ?? dev;
179
195
  validateVersion(target);
180
196
  const data = checklist(target);
181
- const counts = summarizeChecklist(data);
182
- const items = data.items.slice(0, 60).map(({ id, title, status, owner, priority }) =>
197
+ const matchingItems = data.items.filter(featureItemMatches);
198
+ const counts = hasFeatureFilters ? summarizeChecklist({ ...data, items: matchingItems }) : summarizeChecklist(data);
199
+ const items = matchingItems.slice(0, 60).map(({ id, title, status, owner, priority }) =>
183
200
  ({ id, title: firstLine(title), status, owner: owner ?? null, priority: priority ?? null }));
184
- const total = data.items.length;
201
+ const total = matchingItems.length;
185
202
  result = { text: `${target} 판 피처 ${total}칸 · 초록 ${counts.green} · 노랑 ${counts.yellow} · 빨강 ${counts.red} · 끝 ${counts.done}\n${items.map(item => `${item.id} ${item.title} · ${item.status} · ${item.owner ?? '미배정'}`).join('\n')}${total > 60 ? `\n앞 60칸만 표시(전체 ${total}칸)` : ''}`,
186
- structured: { version: target, items, counts: { green: counts.green, yellow: counts.yellow, red: counts.red, done: counts.done }, total, truncated: total > 60 } };
203
+ structured: { version: target, items, counts: { green: counts.green, yellow: counts.yellow, red: counts.red, done: counts.done }, total, truncated: total > 60, ...(hasFeatureFilters ? { filters } : {}) } };
187
204
  } else if (topic === 'cell') {
188
205
  const id = requiredString(args, 'id');
189
206
  const searched = requested ? [requested as string] : [...new Set([dev, nextVersion(dev), checklist(dev).released].filter(Boolean))];
@@ -208,7 +225,7 @@ export function dispatchReleaseStatus(args: Record<string, unknown>, deps: Relea
208
225
  publishAt: row.publishAt ?? null, freezeFrom: row.freezeFrom ?? null, freezeUntil: row.freezeUntil ?? null })) } };
209
226
  }
210
227
  debug.log('release.tool', 'status', { action: 'status', topic, version: requested ?? dev });
211
- return result;
228
+ return withIgnoredAction(result);
212
229
  }
213
230
  const version = requested ? null : dev;
214
231
  const versions = requested ? [requested as string] : [version!, nextVersion(version!)];
@@ -253,7 +270,7 @@ export function dispatchReleaseStatus(args: Record<string, unknown>, deps: Relea
253
270
  : published.state === 'unreadable' ? `발행 판 ${published.version} · 발행 기록 못 읽음` : '발행 판 모름';
254
271
  const structured = { releases: entries, latestRun: latest, published };
255
272
  debug.log('release.tool', 'status', { action: 'status', topic, version: versions.join(',') });
256
- return { text: `${lines.join('\n\n')}\n${cards.map(formatElanousCard).join('\n')}\n${publishedLine}`, structured };
273
+ return withIgnoredAction({ text: `${lines.join('\n\n')}\n${cards.map(formatElanousCard).join('\n')}\n${publishedLine}`, structured });
257
274
  }
258
275
 
259
276
  function requiredString(args: Record<string, unknown>, key: string): string {
@@ -169,6 +169,22 @@ export function recordInboundTurn(
169
169
  finally { if (ownDb) db.close(); }
170
170
  }
171
171
 
172
+ export interface SurfaceUtteranceQuery {
173
+ from: string;
174
+ to: string;
175
+ surface?: string;
176
+ }
177
+
178
+ /** Read every event in a half-open UTC interval without touching recall state. */
179
+ export function querySurfaceUtterances(db: Database, opts: SurfaceUtteranceQuery): SurfaceEventRow[] {
180
+ const where = ["julianday(e.ts) >= julianday(?)", "julianday(e.ts) < julianday(?)", "e.direction = 'inbound'", "e.kind IN ('utterance', 'qna')"];
181
+ const params: string[] = [opts.from, opts.to];
182
+ if (opts.surface) { where.push('e.surface = ?'); params.push(opts.surface); }
183
+ return db.prepare(
184
+ `SELECT e.* FROM events e WHERE ${where.join(' AND ')} ORDER BY julianday(e.ts) ASC, e.id ASC`,
185
+ ).all(...params) as SurfaceEventRow[];
186
+ }
187
+
172
188
  export interface QueryEventsOpts {
173
189
  query?: string; // FTS MATCH (없으면 최근순 전체)
174
190
  surface?: string;
@@ -15,8 +15,8 @@
15
15
  // Resolution order:
16
16
  // 1. Programmatic override set via `setElanousConfigDir(dir)` (incl.
17
17
  // from the `--config-dir` CLI flag).
18
- // 2. State-dir via `effectiveInstanceRoot()` (including isolated source trees).
19
- // 3. `~/.elanous` for the installed daily-driver root.
18
+ // 2. State-dir via `effectiveInstanceRoot()` (explicit --test or parent stamp).
19
+ // 3. `~/.elanous` for both installed and source CLIs without an explicit test root.
20
20
 
21
21
  import { homedir } from 'node:os';
22
22
  import { resolve } from 'node:path';
@@ -1,5 +1,7 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { debug } from '../debug/log.js';
3
+ import type { ResourceView } from '../control-plane/ledger.js';
4
+ import type { MachineLedger, MachineFleetIntent } from '../machines/machine-ledger.js';
3
5
 
4
6
  /**
5
7
  * 호스트 닥터 — 첫 조각(FLEET-DOCTOR-1 · 읽기 전용).
@@ -295,6 +297,66 @@ export interface HostDoctorOptions {
295
297
  remote?: HostDoctorRemote;
296
298
  expected?: HostDoctorExpected;
297
299
  now?: () => Date;
300
+ /** HQ observations: null means the source could not be read; [] means a successful empty reading. */
301
+ membership?: {
302
+ intent: MachineLedger | null;
303
+ machines: readonly ResourceView[] | null;
304
+ parentHosts: readonly { host?: string; podPool?: string; approvalCapacity?: number }[] | null;
305
+ intentError?: string;
306
+ machinesError?: string;
307
+ parentHostsError?: string;
308
+ };
309
+ }
310
+
311
+ function membershipCheck(id: string, status: HostDoctorCheckStatus, observed: string, expected: string): HostDoctorCheckResult {
312
+ return { id, status, observed, expected, fixKind: 'human', fix: 'compare Nexus machine registration, machines.yaml intent, and taskAgent.parentHost' };
313
+ }
314
+
315
+ /** Pure HQ comparison. A failed query is null (unmeasured); a successful empty result is a real missing host. */
316
+ export function checkHostMembership(host: string, platform: HostDoctorPlatform | null, membership: NonNullable<HostDoctorOptions['membership']>): HostDoctorCheckResult[] {
317
+ const intent: MachineFleetIntent | undefined = membership.intent?.machines.find((row) => row.fleet?.sshName === host)?.fleet;
318
+ const registryExpected = intent ? `live machine ${host} · role ${intent.role}` : `machines.yaml fleet intent for ${host}`;
319
+ let registry: HostDoctorCheckResult;
320
+ if (membership.intent === null || membership.machines === null) {
321
+ registry = membershipCheck('registry.member', 'unmeasured', membership.intent === null
322
+ ? `intent ledger unavailable (repository checkout required)${membership.intentError ? `: ${membership.intentError}` : ''}`
323
+ : `Nexus machine query unavailable${membership.machinesError ? `: ${membership.machinesError}` : ''}`, registryExpected);
324
+ } else if (!intent) {
325
+ registry = membershipCheck('registry.member', 'fail', 'host absent from fleet intent', registryExpected);
326
+ } else {
327
+ // `machine` is the Nexus identity; `name` is a display label. A missing/expired record
328
+ // is a measured failure even when the remote platform probe did not run.
329
+ const rows = membership.machines.filter((row) => row.kind === 'machine' && row.machine === host && !row.expired);
330
+ const row = rows[0];
331
+ const role = row?.attrs.role;
332
+ if (!row) registry = membershipCheck('registry.member', 'fail', 'no live Nexus machine record', registryExpected);
333
+ else if (rows.length !== 1) registry = membershipCheck('registry.member', 'fail', `${rows.length} live machine records`, registryExpected);
334
+ else if (typeof role !== 'string') registry = membershipCheck('registry.member', 'unmeasured', 'machine attrs.role unavailable', registryExpected);
335
+ else if (role !== intent.role) registry = membershipCheck('registry.member', 'fail', `machine role ${role}`, registryExpected);
336
+ // Existence and role have been measured above; a pass still needs the platform probe.
337
+ else if (platform === null) registry = membershipCheck('registry.member', 'unmeasured', 'host platform unavailable', `intent platform ${intent.platform}`);
338
+ else if (platform !== intent.platform) registry = membershipCheck('registry.member', 'fail', `host platform ${platform}`, `intent platform ${intent.platform}`);
339
+ else registry = membershipCheck('registry.member', 'pass', `live machine ${host} · role ${role}`, registryExpected);
340
+ }
341
+
342
+ const parentExpected = intent ? `taskAgent.parentHost ${host} · ${intent.podPool} · cap ${intent.cap}` : `machines.yaml fleet intent for ${host}`;
343
+ let parent: HostDoctorCheckResult;
344
+ if (membership.intent === null || membership.parentHosts === null) {
345
+ parent = membershipCheck('parentHost.member', 'unmeasured', membership.intent === null
346
+ ? `intent ledger unavailable (repository checkout required)${membership.intentError ? `: ${membership.intentError}` : ''}`
347
+ : `taskAgent.parentHost unavailable${membership.parentHostsError ? `: ${membership.parentHostsError}` : ''}`, parentExpected);
348
+ } else if (!intent) {
349
+ parent = membershipCheck('parentHost.member', 'fail', 'host absent from fleet intent', parentExpected);
350
+ } else {
351
+ const rows = membership.parentHosts.filter((row) => row.host === host);
352
+ const row = rows[0];
353
+ if (intent.cap === 0) parent = membershipCheck('parentHost.member', rows.length === 0 ? 'pass' : 'fail', rows.length === 0 ? 'disabled in both intent and parent config' : 'disabled host still in parent config', 'host absent from taskAgent.parentHost (cap 0)');
354
+ else if (!row) parent = membershipCheck('parentHost.member', 'fail', 'host absent from taskAgent.parentHost', parentExpected);
355
+ else if (rows.length !== 1 || row.podPool !== intent.podPool || row.approvalCapacity !== intent.cap) {
356
+ parent = membershipCheck('parentHost.member', 'fail', `host entries ${rows.length} · pool ${row.podPool ?? '(missing)'} · cap ${row.approvalCapacity ?? '(missing)'}`, parentExpected);
357
+ } else parent = membershipCheck('parentHost.member', 'pass', `host ${host} · pool ${row.podPool} · cap ${row.approvalCapacity}`, parentExpected);
358
+ }
359
+ return [registry, parent];
298
360
  }
299
361
 
300
362
  /** 원격 `uname -s` ⊕ `/proc/version` 의 microsoft 로 darwin|linux|wsl. 못 재면 null. */
@@ -344,9 +406,14 @@ export function runHostDoctor(options: HostDoctorOptions): HostDoctorReport {
344
406
  const platform = reached ? detectHostDoctorPlatform(remote, host) : null;
345
407
  const arch = reached ? detectHostDoctorArch(remote, host) : null;
346
408
  const specs = HOST_DOCTOR_CHECKS.filter((spec) => spec.roles.includes(role));
347
- const checks = reached
409
+ const remoteChecks = reached
348
410
  ? specs.map((spec) => runCheck(spec, { host, remote, role, platform, arch, expected }))
349
411
  : specs.map((spec) => unmeasuredBecauseUnreached(spec, { platform, arch, expected }));
412
+ const checks = [...remoteChecks, ...(role === 'worker' && options.membership ? checkHostMembership(host, platform, options.membership) : [])];
413
+ const mismatch = checks.filter((check) => (check.id === 'registry.member' || check.id === 'parentHost.member') && check.status === 'fail');
414
+ if (mismatch.length) debug.log('fleet.doctor', 'membership-mismatch', { host, checks: mismatch.map((check) => ({ id: check.id, observed: check.observed, expected: check.expected })) });
415
+ const membershipUnmeasured = checks.filter((check) => (check.id === 'registry.member' || check.id === 'parentHost.member') && check.status === 'unmeasured');
416
+ if (membershipUnmeasured.length) debug.log('fleet.doctor', 'membership-unmeasured', { host, checks: membershipUnmeasured.map((check) => ({ id: check.id, observed: check.observed })) });
350
417
  const fail = checks.filter((check) => check.status === 'fail').map((check) => check.id);
351
418
  const unmeasured = checks.filter((check) => check.status === 'unmeasured').map((check) => check.id);
352
419
  const measuredAt = (options.now ?? (() => new Date()))().toISOString();
@@ -365,9 +432,13 @@ export function runHostDoctor(options: HostDoctorOptions): HostDoctorReport {
365
432
  };
366
433
  }
367
434
 
368
- /** 사람 출력 한 줄씩 — «✓|✗|→ <id> <한 줄>» 문법(node-setup.sh --check 와 같다). */
435
+ /** 사람 출력 한 줄씩 — 기본은 «✓|✗|→ <id> <한 줄>»(node-setup.sh --check),
436
+ * membership 미측정은 눈에 보이는 «⚠ <id> unmeasured: <원인>»을 쓴다. */
369
437
  export function formatHostDoctorLines(report: HostDoctorReport): string[] {
370
438
  return report.checks.map((check) => {
439
+ if ((check.id === 'registry.member' || check.id === 'parentHost.member') && check.status === 'unmeasured') {
440
+ return `⚠ ${check.id} unmeasured: ${check.observed}`;
441
+ }
371
442
  const mark = check.status === 'pass' ? '✓' : check.status === 'fail' ? '✗' : '→';
372
443
  const detail = check.status === 'fail' ? `${check.observed} (expected: ${check.expected})` : check.observed;
373
444
  return `${mark} ${check.id} ${detail}`;
@@ -26,6 +26,7 @@ import type { BashNode, NodeExecContext, WorkflowDeps } from '../workflow-runtim
26
26
  import { releaseResumeAcceptedRegressions, releaseResumeGateExecutor, releaseResumeWaiver, type AcceptedRegression, type GateExecutorOverride } from '../../scripts/release-loop/resume.js';
27
27
 
28
28
  type BashRun = WorkflowDeps['runBash'];
29
+ const DEFAULT_BASH_TIMEOUT_MS = 5 * 60 * 1_000;
29
30
 
30
31
  export interface GraphRunState {
31
32
  graphId: string;
@@ -878,7 +879,8 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
878
879
  env.ELANOUS_GRAPH_DRY_RUN = state.dryRun ? '1' : '0';
879
880
  env.ELANOUS_GRAPH_DIR = dirname(resolve(path));
880
881
  const ctx = { arguments: '', artifactsDir: dirname(statePath), outputs: {}, resolvedProvider: undefined, resolvedModel: undefined, toolPolicy: {}, env } as NodeExecContext;
881
- const result = await executeBashNode({ id: current, type: 'bash', bash: state.dryRun ? command.dry_run_command! : command.command, ...(command.timeout_ms ? { idle_timeout: command.timeout_ms } : {}) } as BashNode, ctx, { runBash } as WorkflowDeps);
882
+ const result = await executeBashNode({ id: current, type: 'bash', bash: state.dryRun ? command.dry_run_command! : command.command,
883
+ idle_timeout: command.timeout_ms ?? DEFAULT_BASH_TIMEOUT_MS } as BashNode, ctx, { runBash } as WorkflowDeps);
882
884
  output = result.output;
883
885
  ok = result.ok;
884
886
  exit = code;