elanous 0.2.18 → 0.2.20

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 (350) 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/{1307-30dff22c9e1f3073.js → 1307-77fad80cf54477c6.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/7748-6e51bfe927019f5c.js +25 -0
  14. package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/a9f2b54d-33c0689b70ed8cf3.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d92f7ee3994e84c7.js → page-c855c24de7db74fe.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-4733c967b56a4277.js → page-85b316bb4235980b.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/board/{page-d22fd849cc22f104.js → page-9b2b26a33cb2cd42.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-bb742f995c6769e7.js → page-c9955fde67b1bcb5.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/bots/{page-02b073a2ac793b83.js → page-7a852956c74015a3.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/chat/{page-8a9b3594bc23289b.js → page-4ba4abe096b6c2a8.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/control/{page-308111abadf35670.js → page-f5d56ff5f45ad164.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-5d97b2901e0d1ea6.js → page-7c058069c5e7fa63.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/exec/{page-8c3d8e63ae2e1c98.js → page-466bf0ee16ad1432.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/field/{page-9b72a07d424701c6.js → page-a23bc71713bcc08f.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/inside/page-ed4a6f799f005d09.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/intake/{page-a572f58cd3ca5864.js → page-9cd565005b93aca6.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/live/{page-be5ea965a07fbc41.js → page-474cb5888ea0e8f3.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/market/{page-fa244bd146a00ae8.js → page-9ca21f9ad3208a61.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-44d56d6391965a6a.js → page-f9ccb1b1adbea14d.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/morning/{page-83a700d4c754d802.js → page-ec102c9b8b691e37.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/{not-found-7b3170f374f77b9d.js → not-found-a9c2cfa8dda922a5.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1467047104141297.js → page-db03ae3589a81c9e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-102310930791197a.js → page-75bffecc9aa1004e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/settings/{page-6ed0b73bae8351d8.js → page-31d5b875760bb62d.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/setup/{page-43336055451f6e37.js → page-68793c6e10331d66.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/share/{page-3cc47dca0922ab9a.js → page-46d5201d2207a511.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-a77ee355a2304ff0.js → page-d81c07a7b4f618ae.js} +1 -1
  55. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-b17cd46cf0e9b8cf.js → page-be1c4e8697f22360.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/trace/{page-7bf54c9bb9274af8.js → page-0fd895fab9b62c1b.js} +1 -1
  59. package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-134da468538baf48.js → page-34882c84f84021c5.js} +1 -1
  63. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/{webpack-c2782b330447ffb6.js → webpack-c260571513bf43c2.js} +1 -1
  65. package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
  66. package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
  67. package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
  68. package/apps/pwa/out/approvals/index.html +1 -1
  69. package/apps/pwa/out/approvals/index.txt +14 -14
  70. package/apps/pwa/out/autopilot/index.html +1 -1
  71. package/apps/pwa/out/autopilot/index.txt +14 -14
  72. package/apps/pwa/out/board/index.html +1 -1
  73. package/apps/pwa/out/board/index.txt +14 -14
  74. package/apps/pwa/out/botlab/index.html +1 -1
  75. package/apps/pwa/out/botlab/index.txt +14 -14
  76. package/apps/pwa/out/bots/index.html +1 -1
  77. package/apps/pwa/out/bots/index.txt +14 -14
  78. package/apps/pwa/out/ceo/index.html +1 -1
  79. package/apps/pwa/out/ceo/index.txt +14 -14
  80. package/apps/pwa/out/chat/index.html +1 -1
  81. package/apps/pwa/out/chat/index.txt +14 -14
  82. package/apps/pwa/out/consult/index.html +1 -1
  83. package/apps/pwa/out/consult/index.txt +14 -14
  84. package/apps/pwa/out/control/index.html +1 -1
  85. package/apps/pwa/out/control/index.txt +14 -14
  86. package/apps/pwa/out/decisions/index.html +1 -1
  87. package/apps/pwa/out/decisions/index.txt +14 -14
  88. package/apps/pwa/out/design-check/index.html +1 -1
  89. package/apps/pwa/out/design-check/index.txt +14 -14
  90. package/apps/pwa/out/editor/index.html +1 -1
  91. package/apps/pwa/out/editor/index.txt +16 -15
  92. package/apps/pwa/out/exec/index.html +1 -1
  93. package/apps/pwa/out/exec/index.txt +14 -14
  94. package/apps/pwa/out/field/index.html +1 -1
  95. package/apps/pwa/out/field/index.txt +14 -14
  96. package/apps/pwa/out/healthz +1 -1
  97. package/apps/pwa/out/index.html +1 -1
  98. package/apps/pwa/out/index.txt +14 -14
  99. package/apps/pwa/out/inside/index.html +1 -1
  100. package/apps/pwa/out/inside/index.txt +16 -15
  101. package/apps/pwa/out/intake/index.html +1 -1
  102. package/apps/pwa/out/intake/index.txt +14 -14
  103. package/apps/pwa/out/live/index.html +1 -1
  104. package/apps/pwa/out/live/index.txt +14 -14
  105. package/apps/pwa/out/loops/index.html +1 -1
  106. package/apps/pwa/out/loops/index.txt +24 -23
  107. package/apps/pwa/out/market/index.html +1 -1
  108. package/apps/pwa/out/market/index.txt +14 -14
  109. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  110. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  111. package/apps/pwa/out/morning/index.html +1 -1
  112. package/apps/pwa/out/morning/index.txt +14 -14
  113. package/apps/pwa/out/observatory/index.html +1 -1
  114. package/apps/pwa/out/observatory/index.txt +14 -14
  115. package/apps/pwa/out/ops/checklist/index.html +1 -1
  116. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  117. package/apps/pwa/out/ops/release/index.html +1 -1
  118. package/apps/pwa/out/ops/release/index.txt +14 -14
  119. package/apps/pwa/out/ops/seats/index.html +1 -1
  120. package/apps/pwa/out/ops/seats/index.txt +14 -14
  121. package/apps/pwa/out/outputs/index.html +1 -1
  122. package/apps/pwa/out/outputs/index.txt +14 -14
  123. package/apps/pwa/out/reflection/index.html +1 -1
  124. package/apps/pwa/out/reflection/index.txt +15 -15
  125. package/apps/pwa/out/scheduler/index.html +1 -1
  126. package/apps/pwa/out/scheduler/index.txt +14 -14
  127. package/apps/pwa/out/sessions/index.html +1 -1
  128. package/apps/pwa/out/sessions/index.txt +14 -14
  129. package/apps/pwa/out/settings/devices/index.html +1 -1
  130. package/apps/pwa/out/settings/devices/index.txt +14 -14
  131. package/apps/pwa/out/settings/index.html +1 -1
  132. package/apps/pwa/out/settings/index.txt +14 -14
  133. package/apps/pwa/out/setup/done/index.html +1 -1
  134. package/apps/pwa/out/setup/done/index.txt +14 -14
  135. package/apps/pwa/out/setup/index.html +1 -1
  136. package/apps/pwa/out/setup/index.txt +14 -14
  137. package/apps/pwa/out/share/index.html +1 -1
  138. package/apps/pwa/out/share/index.txt +14 -14
  139. package/apps/pwa/out/showroom/index.html +1 -1
  140. package/apps/pwa/out/showroom/index.txt +14 -14
  141. package/apps/pwa/out/tasks/index.html +1 -1
  142. package/apps/pwa/out/tasks/index.txt +14 -14
  143. package/apps/pwa/out/term/index.html +1 -1
  144. package/apps/pwa/out/term/index.txt +14 -14
  145. package/apps/pwa/out/today/index.html +1 -1
  146. package/apps/pwa/out/today/index.txt +14 -14
  147. package/apps/pwa/out/trace/index.html +1 -1
  148. package/apps/pwa/out/trace/index.txt +14 -14
  149. package/apps/pwa/out/vault/index.html +1 -1
  150. package/apps/pwa/out/vault/index.txt +14 -14
  151. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  152. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  153. package/apps/pwa/out/workflows/index.html +1 -1
  154. package/apps/pwa/out/workflows/index.txt +14 -14
  155. package/apps/pwa/out/workspace/index.html +1 -1
  156. package/apps/pwa/out/workspace/index.txt +14 -14
  157. package/apps/pwa/out/worktrees/index.html +1 -1
  158. package/apps/pwa/out/worktrees/index.txt +14 -14
  159. package/graphs/demo/editor-recipes.yaml +22 -0
  160. package/graphs/ops/guardian.yaml +26 -0
  161. package/graphs/ops/recipes.yaml +3 -0
  162. package/graphs/retro/daily.yaml +25 -0
  163. package/graphs/retro/recipes.yaml +24 -0
  164. package/package.json +1 -1
  165. package/scripts/ask-marker-check.ts +3 -1
  166. package/scripts/coord-channel-watch.sh +2 -2
  167. package/scripts/discord-seat-probe.ts +3 -1
  168. package/scripts/land-global-checks.ts +89 -0
  169. package/scripts/lib/pch-regress-map.ts +24 -0
  170. package/scripts/morning-report.ts +18 -4
  171. package/scripts/pch-regress.ts +88 -0
  172. package/scripts/release-loop/gate-diff.ts +18 -0
  173. package/scripts/release-loop/gate-node.ts +628 -65
  174. package/scripts/release-loop/gate-partial.ts +106 -0
  175. package/scripts/release-loop/gate-timing.ts +229 -0
  176. package/scripts/release-loop/preflight.ts +6 -4
  177. package/scripts/release-loop/publish-window.ts +141 -0
  178. package/scripts/release-loop/release-docs.ts +1 -4
  179. package/scripts/release-loop/release-universe.ts +38 -0
  180. package/scripts/release-loop/resume-release.ts +67 -5
  181. package/scripts/release-loop/shard-plan.ts +65 -0
  182. package/scripts/release-loop/unattended-release.ts +12 -2
  183. package/scripts/release-story/draft.ts +5 -1
  184. package/scripts/retro/shadow-compare.ts +71 -0
  185. package/scripts/rhythm/daily.ts +6 -1
  186. package/scripts/video/scene-table.ts +159 -0
  187. package/src/acp/core-turn-bridge.ts +39 -2
  188. package/src/agent-mission/driver.ts +34 -6
  189. package/src/autopilot/build/llm-conflict-merge.ts +132 -7
  190. package/src/autopilot/build/merge-intent.ts +135 -7
  191. package/src/away/presence.ts +40 -0
  192. package/src/away/release-watch.ts +272 -0
  193. package/src/chat/index.ts +2 -1
  194. package/src/chat/short-question.ts +3 -40
  195. package/src/cli/away-cli.ts +68 -0
  196. package/src/cli/config-drift.ts +513 -0
  197. package/src/cli/guardian-cli.ts +49 -0
  198. package/src/cli/intake-cli.ts +13 -7
  199. package/src/cli/log-event-names.ts +5 -0
  200. package/src/cli/pod-cli.ts +5 -1
  201. package/src/cli/pr-cli.ts +20 -0
  202. package/src/cli/pty-drive-cli.ts +14 -3
  203. package/src/cli/release-cli.ts +52 -12
  204. package/src/cli/retro-cli.ts +78 -0
  205. package/src/cli/setup-cli.ts +31 -1
  206. package/src/cli/tasks-cli.ts +193 -14
  207. package/src/context-bus/context-now-public.ts +1 -1
  208. package/src/context-bus/context-now-surfaces.ts +16 -6
  209. package/src/context-bus/context-now.ts +97 -3
  210. package/src/core-turn/short-question.ts +100 -0
  211. package/src/dashboard/dashboard-status-lines.ts +24 -4
  212. package/src/dashboard/index.ts +50 -5
  213. package/src/dashboard/input/slash-executor.ts +6 -2
  214. package/src/dashboard/slash-runtime/dashboard-handlers.ts +68 -40
  215. package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
  216. package/src/dashboard/slash-runtime/help-from-registry.ts +62 -23
  217. package/src/dashboard/slash-runtime/help-groups.ts +44 -0
  218. package/src/dashboard/slash-runtime/model-choices.ts +125 -0
  219. package/src/domains/guardian-tick.ts +138 -0
  220. package/src/domains/outbound-alert.ts +44 -13
  221. package/src/domains/outbound-log-probe.ts +6 -1
  222. package/src/domains/schedule-registry.ts +23 -0
  223. package/src/domains/telegram-kind-route.ts +39 -0
  224. package/src/domains/watch-default-scheduler.ts +95 -0
  225. package/src/esc-abort-gate.ts +44 -0
  226. package/src/graph-runner/graph-run-api.ts +205 -0
  227. package/src/graph-runner/runner.ts +36 -6
  228. package/src/harness/harness-cli-command.ts +420 -51
  229. package/src/harness/harness-queue.ts +315 -25
  230. package/src/harness/harness-salvage-cli.ts +140 -0
  231. package/src/harness/harness-worktree-add.ts +68 -0
  232. package/src/harness/ledger-write-guard.ts +6 -16
  233. package/src/hq/fence-outcomes.ts +63 -0
  234. package/src/hq/fence-wrapper.ts +33 -0
  235. package/src/hq/hq.ts +17 -2
  236. package/src/index.ts +118 -6
  237. package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
  238. package/src/instance/test-write-guard.ts +200 -0
  239. package/src/intake-plane/digest.ts +39 -15
  240. package/src/intake-plane/harness-target.ts +4 -3
  241. package/src/intake-plane/intake-sources.ts +1 -0
  242. package/src/intake-plane/items.ts +1 -0
  243. package/src/loops/orchestrator/degradation.ts +57 -0
  244. package/src/loops/orchestrator/finish-rate.ts +33 -3
  245. package/src/loops/orchestrator/tick.ts +166 -45
  246. package/src/maturity/feature-maturity.ts +6 -4
  247. package/src/maturity/tui-slash-maturity.ts +1 -0
  248. package/src/mss/logging/log-store.ts +3 -0
  249. package/src/nexus/api/graph-kinds.ts +10 -4
  250. package/src/nexus/api/graphs-api.ts +178 -6
  251. package/src/nexus/api/http-server.ts +54 -8
  252. package/src/nexus/api/loop-edges.ts +292 -17
  253. package/src/nexus/api/ops-api.ts +112 -29
  254. package/src/nexus/api/ops-board-route.ts +48 -0
  255. package/src/nexus/index.ts +7 -0
  256. package/src/onboarding/entry-hints.ts +5 -0
  257. package/src/onboarding.ts +24 -6
  258. package/src/release-loop/checklist.ts +2 -0
  259. package/src/release-loop/feature-store.ts +68 -18
  260. package/src/release-loop/gate-shards.ts +158 -0
  261. package/src/release-loop/light-rc.ts +173 -0
  262. package/src/release-loop/placement.ts +51 -17
  263. package/src/release-loop/release-schedule.ts +6 -2
  264. package/src/release-loop/rubric.ts +57 -1
  265. package/src/retro/daily.ts +372 -0
  266. package/src/seat-loop/op-decide.ts +173 -0
  267. package/src/seat-loop/seat-loop.ts +60 -3
  268. package/src/seat-loop/seat-report.ts +58 -3
  269. package/src/self-dev/ask-launch-flow.ts +5 -4
  270. package/src/self-dev/ask-launch-io.ts +49 -1
  271. package/src/self-dev/ask-path-candidates.ts +40 -2
  272. package/src/self-dev/dev-cli.ts +36 -0
  273. package/src/self-dev/draft-sweep.ts +371 -16
  274. package/src/self-dev/draft-triage-rules.ts +19 -4
  275. package/src/self-dev/orchestrate.ts +1 -0
  276. package/src/self-dev/pr-preview.ts +247 -0
  277. package/src/self-dev/shard-run.ts +23 -7
  278. package/src/self-implement/blocked-draft-policy.ts +101 -0
  279. package/src/self-implement/execution-clarification.ts +11 -0
  280. package/src/self-implement/gate-cli.ts +29 -8
  281. package/src/self-implement/gate-env-retry.ts +83 -0
  282. package/src/self-implement/goal-author-clarification.ts +88 -13
  283. package/src/self-implement/goal-clarification-escalation.ts +2 -0
  284. package/src/self-implement/orchestrator.ts +220 -6
  285. package/src/self-implement/run-stop.ts +123 -0
  286. package/src/self-implement/run-ttl.ts +258 -0
  287. package/src/self-implement/salvage-branches.ts +254 -0
  288. package/src/self-implement/salvage-retention.ts +123 -0
  289. package/src/self-implement/seams.ts +4 -0
  290. package/src/self-implement/self-implement-cli.ts +2 -0
  291. package/src/self-implement/sibling-resync.ts +223 -0
  292. package/src/self-implement/stop-autoheal.ts +204 -0
  293. package/src/task-agent/actions.ts +63 -2
  294. package/src/task-agent/board.ts +132 -0
  295. package/src/task-agent/card-evidence.ts +292 -0
  296. package/src/task-agent/completion-evidence.ts +126 -0
  297. package/src/task-agent/judge.ts +25 -0
  298. package/src/task-agent/live-moves.ts +276 -0
  299. package/src/task-agent/mission.ts +33 -7
  300. package/src/task-agent/shadow.ts +192 -15
  301. package/src/task-agent/task-hand.ts +124 -17
  302. package/src/task-orchestrator/surfaces/pod-command-job.ts +82 -8
  303. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
  304. package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
  305. package/src/task-orchestrator/surfaces/pod-lease.ts +233 -21
  306. package/src/task-orchestrator/surfaces/pod-pool.ts +155 -34
  307. package/src/task-orchestrator/surfaces/self-implement-pod.ts +21 -5
  308. package/src/telegram-away-command.ts +38 -0
  309. package/src/telegram-commands.ts +52 -0
  310. package/src/telegram-report-log-probe.ts +21 -0
  311. package/src/telegram-report.ts +27 -3
  312. package/src/testing/real-cli-spawn.ts +106 -0
  313. package/src/tui-client/dashboard-session.ts +1 -0
  314. package/src/user-config.ts +190 -5
  315. package/src/version/packed-revision.json +1 -1
  316. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
  324. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
  348. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
  349. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +0 -0
  350. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
@@ -1,11 +1,16 @@
1
- import { readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
1
+ import { closeSync, mkdirSync, openSync, readFileSync, readSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { loadRunLedger as loadRunLedgerSync } from '../self-implement/run-ledger.js';
4
+ import { join, resolve } from 'node:path';
3
5
  import { InvalidArgumentError, Option, type Command } from 'commander';
4
6
  import { debug } from '../debug/log.js';
5
7
  import { getElanousConfigDir } from '../elanous-config-dir.js';
6
8
  import { readNexusRuntime } from '../nexus/runtime.js';
9
+ import { checklistDevVersion } from '../release-loop/checklist.js';
10
+ import { cardsBoard, countBoardTasks, INTERNAL_PACK_PROJECT, mergeBoards, releasePackBoard, type BoardNode } from '../task-agent/board.js';
7
11
  import type { Task } from '../task-orchestrator/types.js';
8
- import { handTask, nextMoveFor, readTaskAgentState, readTaskCard, taskAgentStatePath as taskStatePathDefault, shellQuote, TASK_CARD_PREFIX, TASK_SEATS, type TaskCard, type TaskLauncher, type TaskSeat } from '../task-agent/task-hand.js';
12
+ import { cardPrNumber, COMPLETION_KINDS, handTask, nextMoveFor, readTaskAgentState, readTaskCard, taskAgentStatePath as taskStatePathDefault, shellQuote, TASK_CARD_PREFIX, TASK_SEATS, type CompletionKind, type TaskCard, type TaskLaunchContext, type TaskLaunchReceipt, type TaskLauncher, type TaskSeat } from '../task-agent/task-hand.js';
13
+ import { readCardSalvageBranch, refreshCardRunBinding, type CardRunEvidenceDeps } from '../task-agent/card-evidence.js';
9
14
  import { advanceMission, handMission, MissionAdvanceError, openMissionIds, recordPieceRef, type AdvanceMissionResult, type MissionDecompose, type PieceEvidenceReader } from '../task-agent/mission.js';
10
15
 
11
16
  type TaskRow = Pick<Task, 'id' | 'title' | 'priority' | 'status' | 'createdAt' | 'approval' | 'generatedBy'>;
@@ -25,6 +30,10 @@ export interface TasksCliDeps {
25
30
  missionDecompose?: MissionDecompose;
26
31
  /** `task advance` 의 착지 근거 읽기(시험 주입). 기본 = `predecessorState`(gh · 런 원장). */
27
32
  pieceEvidence?: PieceEvidenceReader;
33
+ /** `task board` 의 내부 팩 어댑터(시험 주입). 기본 = 현재 판(`checklistDevVersion()`)의 `releasePackBoard` — 읽기만. */
34
+ releaseBoard?: () => BoardNode[];
35
+ /** `tasks show`·`advance` 의 런 근거 읽기(시험 주입) — 기본 = 런 원장(이 우주 ⊕ 연합) ⊕ 호스트 logs 의 Pod job-finished. */
36
+ cardRunEvidence?: CardRunEvidenceDeps;
28
37
  }
29
38
 
30
39
  /** `<조각id>=<값>` 반복 인자. */
@@ -65,16 +74,103 @@ export async function registerTaskAgentSink(register?: (surface: string) => Prom
65
74
  * config-dir 은 명시(`--config-dir`/`--test`)가 있으면 그것, 없으면 운영 루트(`prodInstanceRoot()`)다.
66
75
  * (작업 트리에서 그냥 띄우면 test 우주로 가서 동결·몫·풀 상한 밖에서 돈다.)
67
76
  */
68
- export async function defaultTaskLauncher(args: string[]): Promise<void> {
69
- const { spawn } = await import('node:child_process');
77
+ /** This package's CLI entry — never `process.argv[1]`, which is whatever script started this process
78
+ * (ORCH-LIVE-1008: the orchestrator tick runs `bun …/tick.ts delegate`, so the hand launched `tick.ts --config-dir …
79
+ * harness say …`, which exited 2 with stdio ignored — the card said `launched` and no run ever started). */
80
+ export const ELANOUS_CLI_ENTRY = resolve(import.meta.dir, '../../bin/elanous.mjs');
81
+
82
+ /** argv for a detached `elanous` child that runs `args` in `configDir`. */
83
+ export function taskLauncherArgv(configDir: string, args: readonly string[]): string[] {
84
+ return [ELANOUS_CLI_ENTRY, '--config-dir', configDir, ...args];
85
+ }
86
+
87
+ /** STOP-RECORD launch-failed: 발사 직후 이 창 안에 자식이 비0 으로 끝나면 «발사 실패»로 본다. */
88
+ export const LAUNCH_EARLY_EXIT_WINDOW_MS = 3_000;
89
+
90
+ export interface DefaultTaskLauncherDeps {
91
+ spawn?: DetachedSpawn;
92
+ /** 조기 종료 감시 창(ms) — context(런 id)가 있을 때만 · 0 이면 감시하지 않는다(옛 동작). */
93
+ earlyExitWindowMs?: number;
94
+ /** 자식 stderr 를 받을 디렉토리(시험 주입). 기본 = `<tmpdir>/elanous-task-launch`. */
95
+ stderrDir?: string;
96
+ /** stop 줄 기록기(시험 주입). 기본 = 런 원장. */
97
+ writeStop?: import('../self-implement/run-ledger.js').RunLedgerWriter;
98
+ /** 이미 stop 이 있나(시험 주입). 기본 = 이 우주 런 원장 ⊕ 이 프로세스 기록. */
99
+ hasStop?: (runId: string) => boolean;
100
+ }
101
+
102
+ /** stderr 파일의 첫 비어 있지 않은 줄(최대 4KB 만 읽는다 — 원 로그 꼬리를 싣지 않는다). */
103
+ function firstStderrLine(path: string): string {
104
+ try {
105
+ const fd = openSync(path, 'r');
106
+ try {
107
+ const buf = Buffer.alloc(4096);
108
+ const n = readSync(fd, buf, 0, buf.length, 0);
109
+ return buf.subarray(0, n).toString('utf8').split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0) ?? '';
110
+ } finally { closeSync(fd); }
111
+ } catch { return ''; }
112
+ }
113
+
114
+ export async function defaultTaskLauncher(args: string[], cwd?: string, context?: TaskLaunchContext, deps: DefaultTaskLauncherDeps = {}): Promise<void | TaskLaunchReceipt> {
115
+ const spawn = deps.spawn ?? ((await import('node:child_process')).spawn as unknown as DetachedSpawn);
70
116
  const { getElanousConfigDirOverride } = await import('../elanous-config-dir.js');
71
117
  const { prodInstanceRoot } = await import('../instance/resolve.js');
72
118
  const configDir = getElanousConfigDirOverride() ?? prodInstanceRoot();
73
- await spawnDetachedConfirmed(spawn as unknown as DetachedSpawn, process.execPath, [process.argv[1]!, '--config-dir', configDir, ...args]);
119
+ const argv = taskLauncherArgv(configDir, args);
120
+ const window = context ? deps.earlyExitWindowMs ?? LAUNCH_EARLY_EXIT_WINDOW_MS : 0;
121
+ if (!context || window <= 0) {
122
+ // TA-CARD-RUN-LINK: 런 id 를 자식 환경에 얹는다 — `harness say` 의 런(호스트 orchestrate)이 그 id 를 상속한다.
123
+ await spawnDetachedConfirmed(spawn, process.execPath, argv, SPAWN_CONFIRM_TIMEOUT_MS, cwd, context?.env);
124
+ return context ? { runId: context.runId } : undefined;
125
+ }
126
+ // STOP-RECORD launch-failed: stdio 를 버리면 자식이 exit 2 로 죽어도 흔적이 0 이고 카드는 «launched» 로 남는다
127
+ // (ORCH-LIVE-1008). stderr 만 파일로 받고(파이프는 부모가 먼저 끝나면 자식을 EPIPE 로 죽인다), 짧은 창 안의 종료를 본다.
128
+ const stderrDir = deps.stderrDir ?? join(tmpdir(), 'elanous-task-launch');
129
+ mkdirSync(stderrDir, { recursive: true });
130
+ const stderrPath = join(stderrDir, `${context.runId}.stderr`);
131
+ // 'w' — 같은 runId 재발사가 지난 시도의 stderr 를 이번 cause 로 읽지 않게(리뷰 must-fix).
132
+ const fd = openSync(stderrPath, 'w');
133
+ let exited: Promise<{ code: number | null; signal: string | null }> | undefined;
134
+ const watched: DetachedSpawn = (command, spawnArgs, options) => {
135
+ const child = (spawn as unknown as (c: string, a: string[], o: unknown) => ReturnType<DetachedSpawn>)(command, spawnArgs, { ...options, stdio: ['ignore', 'ignore', fd] });
136
+ const emitter = child as unknown as { once?: (event: 'exit', listener: (code: number | null, signal: string | null) => void) => unknown };
137
+ exited = new Promise((done) => { emitter.once?.('exit', (code, signal) => done({ code, signal })); });
138
+ return child;
139
+ };
140
+ try {
141
+ await spawnDetachedConfirmed(watched, process.execPath, argv, SPAWN_CONFIRM_TIMEOUT_MS, cwd, context.env);
142
+ } finally {
143
+ try { closeSync(fd); } catch { /* the child keeps its own copy */ }
144
+ }
145
+ let timer: ReturnType<typeof setTimeout> | undefined;
146
+ const early = exited
147
+ ? await Promise.race([exited, new Promise<undefined>((done) => { timer = setTimeout(() => done(undefined), window); })])
148
+ : undefined;
149
+ if (timer) clearTimeout(timer);
150
+ if (early && early.code !== 0) {
151
+ const first = firstStderrLine(stderrPath);
152
+ const cause = `child rc=${early.code ?? early.signal ?? 'unknown'}${first ? `: ${first}` : ''}`;
153
+ const { boundRunStopCause, recordRunStop, runStopRecorded, RUN_STOP_EVENT } = await import('../self-implement/run-stop.js');
154
+ const hasStop = deps.hasStop ?? ((id: string) => {
155
+ if (runStopRecorded(id)) return true;
156
+ try { return (loadRunLedgerSync(id) ?? []).some((entry) => entry.event === RUN_STOP_EVENT); } catch { return false; }
157
+ });
158
+ // 자식이 같은 runId 로 이미 stop 을 썼으면 더하지 않는다(런마다 한 줄 · 리뷰 must-fix).
159
+ if (hasStop(context.runId)) throw new Error(`발사 직후 자식이 종료했다 — ${boundRunStopCause(cause)}`);
160
+ const entry = recordRunStop({
161
+ runId: context.runId, site: 'launch-failed', class: 'launch-failed',
162
+ cause,
163
+ evidenceRef: stderrPath,
164
+ nextMove: 'Fix the launch argv/config from the stderr evidence, then re-hand the card',
165
+ }, ...(deps.writeStop ? [deps.writeStop] : []));
166
+ // handTask 가 이 예외로 카드를 `launch-failed` 로 적는다(«launched» 로 남지 않게).
167
+ throw new Error(`발사 직후 자식이 종료했다 — ${String(entry.data.cause)}`);
168
+ }
169
+ return { runId: context.runId };
74
170
  }
75
171
 
76
172
  /** `spawn` 의 필요한 면만(시험 주입). */
77
- export type DetachedSpawn = (command: string, args: string[], options: { detached: true; stdio: 'ignore' }) => {
173
+ export type DetachedSpawn = (command: string, args: string[], options: { detached: true; stdio: 'ignore'; cwd?: string; env?: NodeJS.ProcessEnv }) => {
78
174
  pid?: number;
79
175
  once(event: 'spawn', listener: () => void): unknown;
80
176
  once(event: 'error', listener: (error: Error) => void): unknown;
@@ -89,10 +185,10 @@ export const SPAWN_CONFIRM_TIMEOUT_MS = 2_000;
89
185
  * 떼어 띄우고 «떴는지» 확인한다 — 'spawn' 이면 성공, 'error'(ENOENT·EACCES 등)면 던진다.
90
186
  * 확인 없이 돌려주면 비동기 error 가 처리되지 않고 카드가 `launched` 로 남는다(사후 리뷰 must-fix).
91
187
  */
92
- export function spawnDetachedConfirmed(spawnFn: DetachedSpawn, command: string, args: string[], timeoutMs = SPAWN_CONFIRM_TIMEOUT_MS): Promise<void> {
188
+ export function spawnDetachedConfirmed(spawnFn: DetachedSpawn, command: string, args: string[], timeoutMs = SPAWN_CONFIRM_TIMEOUT_MS, cwd?: string, env?: Record<string, string>): Promise<void> {
93
189
  return new Promise<void>((resolve, reject) => {
94
190
  let child: ReturnType<DetachedSpawn>;
95
- try { child = spawnFn(command, args, { detached: true, stdio: 'ignore' }); } catch (error) { reject(error); return; }
191
+ try { child = spawnFn(command, args, { detached: true, stdio: 'ignore', ...(cwd ? { cwd } : {}), ...(env ? { env: { ...process.env, ...env } } : {}) }); } catch (error) { reject(error); return; }
96
192
  let settled = false;
97
193
  const finish = (error?: Error) => {
98
194
  if (settled) return;
@@ -122,10 +218,20 @@ function headLine(text: string): string {
122
218
  return first.length > 100 ? `${first.slice(0, 100)}…` : first;
123
219
  }
124
220
 
221
+ /** 런 연결 한 줄 — 런 id 가 없으면 없다(옛 카드 · shadow). */
222
+ function runLinkLine(card: TaskCard): string[] {
223
+ if (!card.runId) return [];
224
+ const number = cardPrNumber(card);
225
+ const url = typeof card.pr === 'object' ? card.pr.url : undefined;
226
+ const pr = number !== undefined ? `#${number}${url ? ` ${url}` : ''}` : '-';
227
+ return [`런: ${card.runId} · 골: ${card.goalId ?? '-'} · PR: ${pr}${card.runChildId ? ` · 자식 런: ${card.runChildId}` : ''}`];
228
+ }
229
+
125
230
  function taskCardLines(card: TaskCard): string[] {
126
231
  return [
127
232
  [card.id, card.status, card.seat ?? '-', card.checklistId ?? '-', card.createdAt, card.text].join('\t'),
128
233
  ...(card.mission ? [`미션: ${card.mission} · after: ${card.after?.length ? card.after.join(', ') : '-'}`] : []),
234
+ ...runLinkLine(card),
129
235
  `다음 수: ${moveLine(card)}`,
130
236
  ];
131
237
  }
@@ -135,7 +241,8 @@ function missionLines(mission: TaskCard, pieces: TaskCard[]): string[] {
135
241
  const edges = pieces.flatMap((piece) => (piece.after ?? []).map((before) => `간선: ${before} -> ${piece.id}`));
136
242
  return [
137
243
  [mission.id, 'mission', mission.seat ?? '-', mission.checklistId ?? '-', mission.createdAt, `조각 ${pieces.length} · 간선 ${edges.length} · 출처 ${mission.splitSource ?? '-'}`, headLine(mission.text)].join('\t'),
138
- ...pieces.map((piece) => [`조각: ${piece.id}`, piece.status, nextMoveFor(piece).kind, `after: ${piece.after?.length ? piece.after.join(',') : '-'}`, headLine(piece.text)].join('\t')),
244
+ ...pieces.map((piece) => [`조각: ${piece.id}`, piece.status, nextMoveFor(piece).kind, `after: ${piece.after?.length ? piece.after.join(',') : '-'}`, headLine(piece.text),
245
+ ...(piece.runId ? [`런: ${piece.runId}${cardPrNumber(piece) !== undefined ? ` · PR #${cardPrNumber(piece)}` : ''}`] : [])].join('\t')),
139
246
  ...edges,
140
247
  ...(mission.greenProposal ? [`green 제안: ${mission.greenProposal.at} · 칸 ${mission.greenProposal.checklistId ?? '-'} · 근거 ${Object.entries(mission.greenProposal.evidence).map(([id, ref]) => `${id}=${ref}`).join(', ')}`] : []),
141
248
  ];
@@ -255,16 +362,31 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
255
362
  .description('TASK-AGENT 에 과제 한 줄을 넘긴다 — 카드를 적고 첫 수(harness say --substrate pod --merge-by-host)를 고른다 · 기본 shadow(발사 0) · `--mission` 이면 조각 카드 ⊕ 선후로 쪼개 선행 없는 조각만 넘긴다')
256
363
  .addOption(new Option('--seat <seat>', `과제 자리 (${TASK_SEATS.join('|')})`).choices([...TASK_SEATS]))
257
364
  .option('--checklist <cellId>', '릴리스 체크리스트 칸 id')
365
+ .addOption(new Option('--completion <kind>', `종결 종류 (${COMPLETION_KINDS.join('|')} · 기본 code-pr = PR 병합)`).choices([...COMPLETION_KINDS]))
258
366
  .option('--mission <text>', '미션 문면 — 조각(`- …`·`① …`·`1. …` ⊕ `after: 1`)으로 나열돼 있으면 결정론, 아니면 분해기로 쪼갠다')
367
+ .option('--project <id>', '프로젝트 id — --target 과 함께')
368
+ .option('--goal <id>', '보드 소속 목표 id (task board)')
369
+ .option('--milestone <id>', '보드 소속 이정표 id (task board)')
370
+ .option('--target <dir>', '프로젝트 대상 디렉터리(있어야 한다 · 상대 경로는 절대로 · git 이 아니면 --completion 이 code-pr 밖이어야 한다) — 발사기를 이 디렉터리에서 띄운다')
259
371
  .option('--live', '실제로 발사한다(기본은 shadow — 명령만 출력)')
260
372
  .option('--json', 'JSON 출력')
261
- .action(async (text: string | undefined, opts: { seat?: TaskSeat; checklist?: string; mission?: string; live?: boolean; json?: boolean }) => {
373
+ .action(async (text: string | undefined, opts: { seat?: TaskSeat; checklist?: string; completion?: CompletionKind; mission?: string; project?: string; target?: string; goal?: string; milestone?: string; live?: boolean; json?: boolean }) => {
262
374
  await registerTaskAgentSink(deps.registerSink);
263
375
  if ((text === undefined) === (opts.mission === undefined)) {
264
376
  out('과제 한 줄 «또는» --mission <문면> 중 하나만 준다');
265
377
  process.exitCode = 1;
266
378
  return;
267
379
  }
380
+ if ((opts.project === undefined) !== (opts.target === undefined)) {
381
+ out('--project <id> 와 --target <dir> 를 함께 줘야 한다');
382
+ process.exitCode = 1;
383
+ return;
384
+ }
385
+ if (opts.mission !== undefined && (opts.completion !== undefined || opts.project !== undefined || opts.goal !== undefined || opts.milestone !== undefined)) {
386
+ out('--completion/--project/--target/--goal/--milestone 은 과제 한 줄에만 준다 — --mission 조각은 종전처럼 code-pr 이다');
387
+ process.exitCode = 1;
388
+ return;
389
+ }
268
390
  if (opts.mission !== undefined) {
269
391
  try {
270
392
  const result = await handMission({
@@ -297,8 +419,12 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
297
419
  try {
298
420
  const result = await handTask({
299
421
  text: text!,
422
+ ...(opts.project !== undefined && opts.target !== undefined ? { project: { id: opts.project, target: opts.target } } : {}),
423
+ ...(opts.goal !== undefined ? { goal: opts.goal } : {}),
424
+ ...(opts.milestone !== undefined ? { milestone: opts.milestone } : {}),
300
425
  ...(opts.seat ? { seat: opts.seat } : {}),
301
426
  ...(opts.checklist ? { checklistId: opts.checklist } : {}),
427
+ ...(opts.completion ? { completion: opts.completion } : {}),
302
428
  live: opts.live === true,
303
429
  ...(deps.taskStatePath ? { statePath: deps.taskStatePath } : {}),
304
430
  launcher: deps.taskLauncher ?? defaultTaskLauncher,
@@ -307,6 +433,7 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
307
433
  else {
308
434
  out(`${result.card.id}\t${result.mode}\t${result.launched ? 'launched' : 'not launched'}`);
309
435
  out(`elanous ${result.move.command!.map(shellQuote).join(' ')}`);
436
+ if (result.cwd) out(`(cwd: ${result.cwd})`);
310
437
  if (!result.launched) out('(shadow — 띄우지 않았다 · 띄우려면 --live)');
311
438
  }
312
439
  } catch (error) {
@@ -315,6 +442,37 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
315
442
  }
316
443
  });
317
444
 
445
+ tasks.command('board')
446
+ .description('보드 — 프로젝트 › 목표 › 이정표 › 과제. 과제 카드(project·goal·milestone)와 내부 팩(릴리스 체크리스트 = elanous › 판 › 칸)을 합쳐 읽는다(쓰지 않는다)')
447
+ .option('--project <id>', '이 프로젝트만')
448
+ .option('--json', 'JSON 출력')
449
+ .action(async (opts: { project?: string; json?: boolean }) => {
450
+ await registerTaskAgentSink(deps.registerSink);
451
+ try {
452
+ // 내부 팩은 elanous 프로젝트만 낸다 — 다른 프로젝트로 거르면 릴리스 원장을 열지 않는다(그 원장의 오류가 카드 보드를 막지 않게).
453
+ const wantsPack = opts.project === undefined || opts.project === INTERNAL_PACK_PROJECT;
454
+ const board = mergeBoards(
455
+ cardsBoard(deps.taskStatePath ?? taskStatePathDefault()),
456
+ wantsPack ? (deps.releaseBoard ?? (() => releasePackBoard(checklistDevVersion())))() : [],
457
+ );
458
+ // 무소속(unassigned: true)은 어떤 --project 로도 고르지 않는다 — 실제 프로젝트 id 와 겹치지 않게.
459
+ const selected = opts.project !== undefined ? board.filter((project) => project.unassigned !== true && project.id === opts.project) : board;
460
+ try { debug.log('task-agent', 'board', { projects: selected.length, tasks: countBoardTasks(selected), project: opts.project ?? null }); } catch { /* fail-soft */ }
461
+ if (opts.json) out(JSON.stringify(selected));
462
+ else {
463
+ const show = (node: BoardNode, depth: number): void => {
464
+ out(`${' '.repeat(depth)}${node.level}\t${node.id}\t${node.status}\t${headLine(node.title)}`);
465
+ for (const next of node.children ?? []) show(next, depth + 1);
466
+ };
467
+ if (selected.length === 0) out(opts.project !== undefined ? `프로젝트 없음: ${opts.project}` : '보드가 비어 있다');
468
+ for (const project of selected) show(project, 0);
469
+ }
470
+ } catch (error) {
471
+ out(error instanceof Error ? error.message : '보드 읽기 실패');
472
+ process.exitCode = 1;
473
+ }
474
+ });
475
+
318
476
  tasks.command('advance [missionId]')
319
477
  .description('TASK-AGENT 미션 ② — 조각의 PR 병합(근거: `--pr`/`--goal` 로 카드에 적은 PR 번호·골 id)으로 착지를 가르고 새로 준비된 조각만 한 번 넘긴다 · 모두 착지면 green «제안»만 적는다 · 기본 shadow(발사 0)')
320
478
  .option('--all', 'green 제안이 아직 없는 미션 전부')
@@ -366,6 +524,7 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
366
524
  ...(statePath ? { statePath } : {}),
367
525
  launcher: deps.taskLauncher ?? defaultTaskLauncher,
368
526
  ...(deps.pieceEvidence ? { readEvidence: deps.pieceEvidence } : {}),
527
+ ...(deps.cardRunEvidence ? { runEvidence: deps.cardRunEvidence } : {}),
369
528
  }));
370
529
  } catch (error) {
371
530
  // 미션 id 하나면 그대로 실패 · --all 이면 그 미션만 실패로 적고 다음 미션으로 간다(크론이 한 미션에 막히지 않게).
@@ -405,7 +564,18 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
405
564
  if (id.startsWith(TASK_CARD_PREFIX)) {
406
565
  await registerTaskAgentSink(deps.registerSink);
407
566
  let card: TaskCard | undefined;
408
- try { card = readTaskCard(id, deps.taskStatePath); } catch (error) {
567
+ try {
568
+ card = readTaskCard(id, deps.taskStatePath);
569
+ // 런 id 가 있으면 원장에서 골 id · PR 을 묶는다(찾았을 때만 적는다 · 실패는 그대로 보인다).
570
+ const bindable = card?.pieces ? card.pieces : card ? [card.id] : [];
571
+ for (const cardId of bindable) {
572
+ try { await refreshCardRunBinding(cardId, deps.taskStatePath ?? taskStatePathDefault(), deps.cardRunEvidence ?? {}); } catch (error) {
573
+ // 근거 없음으로 보인다 — 원장 부재와 갈리게 실패는 남긴다.
574
+ try { debug.log('task-agent', 'card-bind-failed', { card: cardId, via: 'show', error: error instanceof Error ? error.message : String(error) }); } catch { /* fail-soft */ }
575
+ }
576
+ }
577
+ if (card) card = readTaskCard(id, deps.taskStatePath);
578
+ } catch (error) {
409
579
  out(error instanceof Error ? error.message : '과제 카드 읽기 실패');
410
580
  process.exitCode = 1;
411
581
  return;
@@ -430,8 +600,17 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
430
600
  }
431
601
  return;
432
602
  }
433
- if (opts.json) out(JSON.stringify({ card, nextMove: nextMoveFor(card) }));
434
- else for (const line of taskCardLines(card)) out(line);
603
+ // DRAFT-NOT-ARCHIVE: 멈춘 런이 draft PR 대신 남긴 수확 가지(원장 `salvaged`). 못 읽었으면 «못 읽음»(⛔ 없음 아님).
604
+ let salvage: Awaited<ReturnType<typeof readCardSalvageBranch>> = { salvageBranch: null };
605
+ try { salvage = await readCardSalvageBranch(card, deps.cardRunEvidence ?? {}); }
606
+ catch (error) { salvage = { salvageBranch: null, readErrors: [error instanceof Error ? error.message : String(error)] }; }
607
+ const salvageUnread = !salvage.salvageBranch && !!salvage.readErrors?.length;
608
+ if (opts.json) out(JSON.stringify({ card, nextMove: nextMoveFor(card), salvageBranch: salvage.salvageBranch, ...(salvageUnread ? { salvageBranchUnread: true } : {}) }));
609
+ else {
610
+ for (const line of taskCardLines(card)) out(line);
611
+ if (salvage.salvageBranch) out(`수확 가지: ${salvage.salvageBranch} (draft PR 대신)`);
612
+ else if (salvageUnread) out('수확 가지: 못 읽음(원장 읽기 실패)');
613
+ }
435
614
  return;
436
615
  }
437
616
  try {
@@ -72,7 +72,7 @@ export function filterPublicDemoContext(answer: ContextNowAnswer, brandCheck: ty
72
72
  id: fact.id ? (rulesMissing || privateIds.has(fact.id) ? '내부 항목' : safe(fact.id, '공개 항목')) : null,
73
73
  title: fact.title === null ? null : rulesMissing ? internalItem(1) : privateIds.has(fact.id ?? '') ? '내부 항목' : safe(fact.title, '공개 항목'),
74
74
  source: source(fact.source) });
75
- } else facts.push({ ...fact, version: safe(fact.version, '공개 판'), source: source(fact.source) });
75
+ } else if (fact.kind === 'version') facts.push({ ...fact, version: safe(fact.version, '공개 판'), source: source(fact.source) });
76
76
  }
77
77
  for (const kind of ['cell', 'decision'] as const) {
78
78
  if (!collapsed[kind]) continue;
@@ -34,6 +34,9 @@ function factText(fact: ContextFact): string {
34
34
  case 'cell': return `${fact.id} ${fact.title} (${fact.status})`;
35
35
  case 'decision': return `${fact.id} ${fact.title} (${fact.status})`;
36
36
  case 'seat': return `${fact.seat} ${fact.id ?? ''} ${fact.title ?? ''} (${fact.status})`.trim();
37
+ case 'run': return fact.unreadable ? `못 읽음 · ${fact.unreadable}` : `${fact.goal} · ${fact.phase} · ${fact.elapsed}`;
38
+ case 'release': return fact.unreadable ? `못 읽음 · ${fact.unreadable}` : `${fact.version} · ${fact.node} · ${fact.status}`;
39
+ case 'schedule-late': return fact.unreadable ? `못 읽음 · ${fact.unreadable}` : `${fact.count}개 · ${fact.names.join(', ') || '없음'}`;
37
40
  }
38
41
  }
39
42
 
@@ -61,20 +64,27 @@ export function renderTuiNow(input: ContextNowAnswer, audience: ContextNowAudien
61
64
  if (source.startsWith('elanous://release/')) return '원장';
62
65
  return Array.from(source).slice(0, 40).join('');
63
66
  };
64
- const factLabels = { version: '판', cell: '칸', decision: '결정', seat: '자리' } as const;
67
+ const factLabels = { run: '도는 런', release: '발행 런', 'schedule-late': '지연 스케줄', version: '판', cell: '칸', decision: '결정', seat: '자리' } as const;
65
68
  const eventLabels: Record<string, string> = { report: '보고', dispatch: '발사', decision: '결정' };
66
69
  const events = answer.events.slice().sort((a, b) => Date.parse(b.at) - Date.parse(a.at));
67
70
  const rows = [
68
- ...answer.facts.map(f => [factLabels[f.kind], shortText(factText(f)), shortSource(f.source)]),
71
+ ...answer.facts.slice().sort((a, b) => {
72
+ const priority = (kind: ContextFact['kind']) => ({ run: 0, release: 1, 'schedule-late': 2 } as Partial<Record<ContextFact['kind'], number>>)[kind] ?? 3;
73
+ return priority(a.kind) - priority(b.kind);
74
+ }).map(f => [factLabels[f.kind], shortText(factText(f)), shortSource(f.source)]),
69
75
  ...events.slice(0, 8).map(e => [eventLabels[e.kind] ?? '소식', shortText(e.summary), shortSource(e.source)]),
70
76
  ...answer.guide.map(guide => ['안내', guide, '']),
71
77
  ...(events.length > 8 ? [['안내', `… 사건 ${events.length - 8}개 더(/now <주제> 로 좁히기)`, '']] : []),
72
78
  ];
79
+ const clean = (text: string) => text.replaceAll('|', ' ').replaceAll(/\r?\n/g, ' ');
80
+ const width = (text: string) => Array.from(text).reduce((total, char) => total + (/[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe6f\uff01-\uff60\uffe0-\uffe6]/u.test(char) ? 2 : 1), 0);
81
+ const data = [['종류', '사실', '출처'], ...rows].map(row => row.map(clean));
82
+ const widths = [0, 1].map(i => Math.max(...data.map(row => width(row[i] ?? ''))));
83
+ const format = (row: string[]) => `${row[0]}${' '.repeat(widths[0]! - width(row[0]!) + 2)}${row[1]}${' '.repeat(widths[1]! - width(row[1]!) + 2)}${row[2]}`;
84
+ const time = new Intl.DateTimeFormat('en-GB', { timeZone: 'Asia/Seoul', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).format(new Date(answer.at));
73
85
  return [
74
- `지금${answer.topic ? ` · ${answer.topic}` : ''} (${answer.at})`,
75
- '| 종류 | 사실 | 출처 |',
76
- '| --- | --- | --- |',
77
- ...rows.map(row => `| ${row.map(cell => cell.replaceAll('|', '\\|').replaceAll(/\r?\n/g, ' ')).join(' | ')} |`),
86
+ `지금${answer.topic ? ` · ${answer.topic}` : ''} (${time} KST)`,
87
+ ...data.map(format),
78
88
  ];
79
89
  }
80
90
 
@@ -1,4 +1,8 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { queryRunningRuns, type QueriedRunningRunsResult } from '../self-implement/running-runs.js';
4
+ import { loadRunLedger } from '../self-implement/run-ledger.js';
5
+ import { localLoopRows } from '../dashboard/slash-runtime/loops-table.js';
2
6
  import { checkBrand } from '../../scripts/brand/check.js';
3
7
  import { debug, redactSecretText } from '../debug/log.js';
4
8
  import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
@@ -12,7 +16,10 @@ export type ContextFact =
12
16
  | { kind: 'version'; version: string; source: string }
13
17
  | { kind: 'cell'; version: string; id: string; title: string; status: string; owner: string | null; source: string }
14
18
  | { kind: 'decision'; id: string; title: string; status: string; dueAt: string | null; source: string }
15
- | { kind: 'seat'; seat: string; at: string; status: string; id: string | null; title: string | null; source: string };
19
+ | { kind: 'seat'; seat: string; at: string; status: string; id: string | null; title: string | null; source: string }
20
+ | { kind: 'run'; goal: string; phase: string; elapsed: string; source: string; unreadable?: string }
21
+ | { kind: 'release'; version: string; node: string; status: string; source: string; unreadable?: string }
22
+ | { kind: 'schedule-late'; count: number | null; names: string[]; source: string; unreadable?: string };
16
23
  export type ContextEvent = { at: string; kind: string; summary: string; source: string };
17
24
  export type ContextNowAnswer = { at: string; topic: string | null; facts: ContextFact[]; events: ContextEvent[]; guide: string[]; hiddenCount?: number };
18
25
 
@@ -23,6 +30,10 @@ export interface ContextNowDeps {
23
30
  decisions?: () => DecisionEntry[];
24
31
  seatEntries?: (now: Date) => Array<{ entry: SeatEntry; source: string }>;
25
32
  events?: (since: string) => CoordEvent[];
33
+ runningRuns?: (now: Date) => Extract<ContextFact, { kind: 'run' }>[];
34
+ queryRuns?: () => QueriedRunningRunsResult;
35
+ releaseRun?: () => Extract<ContextFact, { kind: 'release' }> | null;
36
+ lateSchedules?: (now: Date) => Extract<ContextFact, { kind: 'schedule-late' }> | null;
26
37
  brandCheck?: typeof checkBrand;
27
38
  }
28
39
 
@@ -39,6 +50,75 @@ function todaySeatEntries(now: Date): Array<{ entry: SeatEntry; source: string }
39
50
  });
40
51
  }
41
52
 
53
+ const runSource = 'elanous://self-implement/running-runs';
54
+ const releaseSource = 'elanous://graph-runs/release-loop';
55
+ const scheduleSource = 'elanous://schedules';
56
+ const oneLine = (text: string) => redactSecretText(text.split(/\r?\n/, 1)[0] ?? '').slice(0, 120);
57
+
58
+ function runningRunFacts(now: Date, queryRuns: () => QueriedRunningRunsResult = () => queryRunningRuns({ includeTest: false, caller: 'context-now' })): Extract<ContextFact, { kind: 'run' }>[] {
59
+ const observed = queryRuns();
60
+ const unreadable = observed.completeness === 'partial' || observed.pty.unreadable.length || !!observed.phases?.unreadableTargetCount;
61
+ const facts = observed.entries.filter(entry => observed.countedStatuses.includes(entry.status)).slice(0, 8).flatMap(entry => {
62
+ let ledgerReadFailure = false;
63
+ const starts = entry.ledgerDirectories.flatMap(directory => {
64
+ try { return loadRunLedger(entry.runId, directory)?.filter(record => record.event === 'start') ?? []; }
65
+ catch { ledgerReadFailure = true; return []; }
66
+ });
67
+ const goalRecord = starts.find(record => typeof record.data.feature === 'string' && record.data.feature.trim());
68
+ const goal = typeof goalRecord?.data.feature === 'string' ? goalRecord.data.feature : ledgerReadFailure ? '골 관측 불가' : '골 미기록';
69
+ const started = starts.map(record => record.timestamp ? Date.parse(record.timestamp) : NaN).find(Number.isFinite) ?? NaN;
70
+ const minutes = Number.isFinite(started)
71
+ ? Math.max(0, Math.floor((now.getTime() - started) / 60_000)) : null;
72
+ const fact = { kind: 'run' as const, goal: oneLine(goal),
73
+ phase: entry.lastPhase ?? '단계 미관측', elapsed: minutes === null ? ledgerReadFailure ? '경과 관측 불가' : '경과 미관측' : `${minutes}분`,
74
+ source: `${runSource}/${encodeURIComponent(entry.runId)}` };
75
+ return ledgerReadFailure
76
+ ? [
77
+ ...(goalRecord || minutes !== null ? [fact] : []),
78
+ { kind: 'run' as const, goal: '', phase: '', elapsed: '', source: fact.source, unreadable: '런 원장 일부 관측 불가' },
79
+ ] : [fact];
80
+ });
81
+ return unreadable ? [...facts, { kind: 'run', goal: '', phase: '', elapsed: '', source: runSource, unreadable: '런 원장 또는 관측 대상 일부' }] : facts;
82
+ }
83
+
84
+ function latestReleaseRun(): Extract<ContextFact, { kind: 'release' }> | null {
85
+ const dir = join(effectiveInstanceRoot(), 'graph-runs', 'release-loop');
86
+ let filenames: string[];
87
+ try { filenames = readdirSync(dir).filter(name => /^[A-Za-z0-9-]+\.json$/.test(name)); }
88
+ catch (error) {
89
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
90
+ throw error;
91
+ }
92
+ const runs = filenames.flatMap(filename => {
93
+ const path = join(dir, filename);
94
+ let run: { startedAt?: unknown; status?: unknown; input?: { version?: unknown }; path?: unknown; nodes?: unknown };
95
+ try {
96
+ if (!lstatSync(path).isFile()) return [];
97
+ run = JSON.parse(readFileSync(path, 'utf8'));
98
+ } catch (error) {
99
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT' || error instanceof SyntaxError) return [];
100
+ throw error;
101
+ }
102
+ if (typeof run.startedAt !== 'string' || typeof run.status !== 'string' || !Array.isArray(run.path) || !Array.isArray(run.nodes)) return [];
103
+ const steps = run.path.filter((step): step is string => typeof step === 'string');
104
+ const nodes = run.nodes.filter((node): node is { nodeId: string } => !!node && typeof node.nodeId === 'string');
105
+ const version = typeof run.input?.version === 'string' && /^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/.test(run.input.version) ? run.input.version : '판 미기록';
106
+ const index = Math.min(steps.length, nodes.length + (run.status === 'running' || run.status === 'awaiting-approval' ? 1 : 0));
107
+ return [{ startedAt: run.startedAt, fact: { kind: 'release' as const, version, node: `${index}/${steps.length}${steps[index - 1] ? ` ${oneLine(steps[index - 1]!)}` : ''}`,
108
+ status: oneLine(run.status), source: `${releaseSource}/${encodeURIComponent(filename.slice(0, -5))}` } }];
109
+ });
110
+ return runs.sort((a, b) => b.startedAt.localeCompare(a.startedAt))[0]?.fact ?? null;
111
+ }
112
+
113
+ function lateScheduleFact(now: Date): Extract<ContextFact, { kind: 'schedule-late' }> {
114
+ const names = localLoopRows(now).filter(row => row.verdict === 'late').map(row => oneLine(row.name));
115
+ return { kind: 'schedule-late', count: names.length, names, source: scheduleSource };
116
+ }
117
+
118
+ function unreadableReason(error: unknown): string {
119
+ return oneLine(error instanceof Error ? error.message : String(error)) || '원천 조회 실패';
120
+ }
121
+
42
122
  /** Read a bounded, source-labelled view of current ledgers, never a transcript or decision body. */
43
123
  export function contextNow(options: { topic?: string; limit?: number; audience?: 'operator' | 'user' | 'public-demo' } = {}, deps: ContextNowDeps = {}): ContextNowAnswer {
44
124
  const now = (deps.now ?? (() => new Date()))();
@@ -71,6 +151,20 @@ export function contextNow(options: { topic?: string; limit?: number; audience?:
71
151
  const rawEvents = (deps.events ?? (since => listCoordEvents({ since })))(new Date(now.getTime() - 7 * 86_400_000).toISOString());
72
152
  const chronologicalEvents = rawEvents.slice().sort((a, b) => a.at.localeCompare(b.at));
73
153
  const matches = (text: string) => !topic || text.toLocaleLowerCase().includes(topic.toLocaleLowerCase());
154
+ const operational: ContextFact[] = [];
155
+ if (options.audience !== 'public-demo') {
156
+ try { operational.push(...(deps.runningRuns ?? (date => runningRunFacts(date, deps.queryRuns)))(now)); }
157
+ catch (error) { operational.push({ kind: 'run', goal: '', phase: '', elapsed: '', source: runSource, unreadable: unreadableReason(error) }); }
158
+ try {
159
+ const release = (deps.releaseRun ?? latestReleaseRun)();
160
+ if (release) operational.push(release);
161
+ } catch (error) { operational.push({ kind: 'release', version: '', node: '', status: '', source: releaseSource, unreadable: unreadableReason(error) }); }
162
+ try { const schedules = (deps.lateSchedules ?? lateScheduleFact)(now); if (schedules) operational.push(schedules); }
163
+ catch (error) { operational.push({ kind: 'schedule-late', count: null, names: [], source: scheduleSource, unreadable: unreadableReason(error) }); }
164
+ }
165
+ const selectedOperational = operational.filter(f => !topic || ('unreadable' in f && !!f.unreadable) || (f.kind === 'run' && matches(`${f.goal} ${f.phase}`))
166
+ || (f.kind === 'release' && matches(`${f.version} ${f.node} ${f.status}`))
167
+ || (f.kind === 'schedule-late' && matches(f.names.join(' '))));
74
168
  const selectedFacts = facts.filter(f => !topic || (f.kind === 'cell' && matches(`${f.id} ${f.title}`))
75
169
  || (f.kind === 'seat' && matches(`${f.id ?? ''} ${f.title ?? ''}`)));
76
170
  const groups = (['version', 'cell', 'decision', 'seat'] as const).map(kind => selectedFacts.filter(f => f.kind === kind));
@@ -92,7 +186,7 @@ export function contextNow(options: { topic?: string; limit?: number; audience?:
92
186
  chosen[index]!.push(...extra);
93
187
  remaining -= extra.length;
94
188
  }
95
- const boundedFacts = chosen.flat();
189
+ const boundedFacts = [...selectedOperational.slice(0, limit), ...chosen.flat()];
96
190
  const summaries = chronologicalEvents.map(event => ({
97
191
  at: event.at, kind: event.kind, summary: redactSecretText(event.summary.split(/\r?\n/, 1)[0] ?? '').slice(0, 120),
98
192
  source: event.refs.source ?? event.refs.url ?? `elanous://context/event/${encodeURIComponent(event.id)}`,
@@ -0,0 +1,100 @@
1
+ // B1(UX · 0.2.7) — 짧은 «물음»은 도구 없이 빠른 경로로(첫 글자 ≤2초 · 내부 문서 `EVAL-bot-chat-vs-grok-2026-09-30`).
2
+ // 허용 방식(골 ASK-bot-short-question-fast-path-b1b): 아래가 «모두» 참일 때만 fast — 하나라도 거짓이면 fast 아님(보수 쪽).
3
+ // ⓐ 한 줄 · 80자 이하 · 첨부 없음 ⓑ 인사이거나 «물음 꼴» ⓒ 요청·작업 표지 · 경로·URL·코드 꼴 · 실시간 낱말이 없다.
4
+ // 거부 목록으로 변형을 다 막을 수는 없다 — 오분류 비용은 «확인해 볼까요?» 한 줄로 막힌다(도구가 안 실릴 뿐 대화는 이어진다).
5
+ // 판정 표본 = src/chat/short-question.test.ts 의 고정 표 20개(⊕ 리뷰가 든 변형).
6
+
7
+ export interface ShortQuestionContext {
8
+ hasAttachments?: boolean;
9
+ }
10
+ export interface ShortQuestionVerdict {
11
+ fast: boolean;
12
+ reason: string;
13
+ }
14
+
15
+ const GREETING = /^(?:안녕(?:하세요|하십니까)?|반가워(?:요)?|고마워(?:요)?|감사합니다|하이|hi|hello|hey|thanks|thank you)[!?.~\s]*$/i;
16
+ // «한 줄로» 같은 짧은 꼬리를 떼고 본다.
17
+ const TAIL = /\s*(?:한\s*줄로|짧게|간단히|briefly|in one line)[.!]?\s*$/i;
18
+ const KO_QUESTION_END = /(?:\?|까|니|나요|냐|야|어때|뭐|몇|언제|어디|누구|왜|인가|일까)[?.!~]*$/;
19
+ const EN_QUESTION = /^(?:what|why|how|who|where|when|which|is|are|does|do)\b.*\?$/i;
20
+ const REQUEST_KO = /(?:해\s*줘|해\s*주세요|줘(?:요)?(?=[?.!\s]|$)|주세요|줄래|할래|볼래|줄\s*수\s*있|돌려|실행|고쳐|만들어\s*줘|찾아|검색|확인해|열어|보내|올려|지워|써\s*줘|작성해|요약해|정리해|분석해)/;
21
+ const REQUEST_EN = /\b(?:please|can you|could you|would you|run|test|tests|fix|build|open|search|check|send|create|write|deploy|install|summarize)\b/i;
22
+ const TASK = /(?:작업|파일|폴더|디렉터리|레포|저장소|커밋|브랜치|\bPR\b|코드|스크립트|테스트|빌드|배포|설치|설정|명령어|로그|버그|이슈)/i;
23
+ const PATH_OR_CODE = /(?:https?:\/\/|www\.|[\w.-]+\/[\w./-]+|\.(?:ts|tsx|js|py|md|json|yaml|swift|kt)\b|[`{};=<>])/i;
24
+ const LIVE = /(?:오늘\s*(?:날짜|며칠|무슨\s*요일)|지금\s*몇\s*시|최신|뉴스|속보|주가|시세|환율|날씨|today'?s|latest|news|stock|weather|right now)/i;
25
+
26
+ export type TuiChatRoute = '조회' | '설정 변경' | '조사' | '상시 일' | '코딩·구현';
27
+
28
+ export interface TuiChatVerdict {
29
+ route: TuiChatRoute;
30
+ reason: string;
31
+ goalLoop: boolean;
32
+ }
33
+
34
+ /** Request verbs a quick lookup/settings/research/scheduling turn can carry out without the repository. */
35
+ const QUICK_REQUEST_VERBS = /^(?:보여|알려|확인해|찾아|찾아봐|조사해|검색해|정리해|요약해|바꿔|변경해|전환해|설정해|맞춰|수정해|고쳐|만들어|등록해|예약해|추가해|보내)$/;
36
+
37
+ /** True when the verb never appears, or each occurrence directly follows an allowed object. */
38
+ function everyVerbTakes(text: string, verb: RegExp, object: RegExp): boolean {
39
+ for (const match of text.matchAll(verb)) {
40
+ if (!object.test(text.slice(0, match.index))) return false;
41
+ }
42
+ return true;
43
+ }
44
+
45
+ /** Uncertain or mixed requests retain the full coding goal-loop and tool surface. */
46
+ export function classifyTuiChatIntent(text: string, ctx: ShortQuestionContext = {}): TuiChatVerdict {
47
+ const q = text.trim();
48
+ const coding = (reason: string): TuiChatVerdict => ({ route: '코딩·구현', reason, goalLoop: true });
49
+ if (!q || ctx.hasAttachments) return coding(ctx.hasAttachments ? 'attachment' : 'empty');
50
+ if (/(?:하니스|구현|코딩|코드|스크립트|버그|소스|리팩터|커밋|패치|테스트|시험\s*돌|빌드|배포|브랜치|서버|파일|레포|저장소|\bPR\b|\b(?:implement|coding|code|script|refactor|commit|patch|tests?|build|deploy|branch|server|file|repo)\b)/i.test(q)) return coding('coding');
51
+ // Destructive or executing verbs in any form (not only «…줘») are never a quick turn.
52
+ if (/(?:지워|지우|삭제|제거|옮겨|옮기|돌려|실행|설치|되돌려|롤백|재시작|죽여|\b(?:delete|remove|rm|rename|move|run|install|revert|rollback|drop|kill|restart)\b)/i.test(q)) return coding('write-verb');
53
+ // A file path or code shape means the request touches the repository, whatever else it asks.
54
+ if (PATH_OR_CODE.test(q)) return coding('path-or-code');
55
+ // Allowlist, not denylist: every «…줘» request verb must be one a quick turn can do; anything else
56
+ // (지워줘 · 돌려줘 · 옮겨줘 …) keeps the coding goal loop.
57
+ for (const match of q.matchAll(/([가-힣]+?)\s*(?:줘|주세요|줄래|주라)/g)) {
58
+ if (!QUICK_REQUEST_VERBS.test(match[1]!)) return coding('request-verb');
59
+ }
60
+ // Edit and create verbs are coding requests unless every one of them takes a quick object directly
61
+ // («모델 설정 수정해줘» · «알림 만들어줘» stay quick; «… 그리고 화면 고쳐줘» does not).
62
+ if (!everyVerbTakes(q, /(?:수정|고쳐|바꿔|변경|전환|교체|\b(?:edit|fix|modify|change|switch|replace)\b)/gi, /(?:모델|설정|config|provider|예약|일정|알림|리마인더|스케줄|reminder|schedule)\s*(?:을|를|로|으로)?\s*$/i)) return coding('edit');
63
+ if (!everyVerbTakes(q, /(?:만들어|작성|추가해|생성|등록해|\b(?:create|write|make|add)\b)/gi, /(?:알림|일정|예약|리마인더|스케줄|reminder|schedule)\s*(?:을|를)?\s*$/i)) return coding('create');
64
+
65
+ const routes: Array<{ route: Exclude<TuiChatRoute, '코딩·구현'>; reason: string; pattern: RegExp }> = [
66
+ { route: '조회', reason: 'lookup', pattern: /(?:결정\s*카드|결정\s*대기|상태|현황|로그|알림\s*(?:왔|있|보여)|\b(?:status|logs?|pending decisions?)\b)/i },
67
+ { route: '설정 변경', reason: 'settings', pattern: /(?:(?:모델|설정|config|provider).*(?:바꿔|변경|전환|설정해|맞춰|수정|고쳐|써\s*줘|set|change|switch|edit|fix|modify)|(?:바꿔|변경|전환).*(?:모델|설정|config|provider)|답변.*빠른\s*모델)/i },
68
+ { route: '조사', reason: 'research', pattern: /(?:조사|검색|찾아\s*봐|뉴스|업계\s*소식|\b(?:research|search|news)\b)/i },
69
+ { route: '상시 일', reason: 'recurring', pattern: /(?:매일|매주|매달|매\s*아침|[가-힣]+마다|정기적|반복\s*일정|예약|리마인더|\b(?:daily|weekly|every|schedule|remind)\b)/i },
70
+ ];
71
+ const matches = routes.filter(({ pattern }) => pattern.test(q));
72
+ // A recurring job that looks something up or researches it is still a scheduling request.
73
+ if (matches.some(({ route }) => route === '상시 일')
74
+ && matches.every(({ route }) => route === '상시 일' || route === '조사' || route === '조회')) {
75
+ return { route: '상시 일', reason: matches.length > 1 ? 'recurring-research' : 'recurring', goalLoop: false };
76
+ }
77
+ if (matches.length !== 1) {
78
+ if (matches.length) return coding('mixed');
79
+ const shortQuestion = classifyShortQuestion(q, ctx);
80
+ return coding(shortQuestion.fast ? `short-${shortQuestion.reason}` : 'ambiguous');
81
+ }
82
+ const { route, reason } = matches[0]!;
83
+ return { route, reason, goalLoop: false };
84
+ }
85
+
86
+ export function classifyShortQuestion(text: string, ctx: ShortQuestionContext = {}): ShortQuestionVerdict {
87
+ const q = text.trim();
88
+ if (!q) return { fast: false, reason: 'empty' };
89
+ if (ctx.hasAttachments) return { fast: false, reason: 'attachment' };
90
+ if (/[\r\n\u2028\u2029]/.test(q)) return { fast: false, reason: 'multiline' };
91
+ if ([...q].length > 80) return { fast: false, reason: 'long' };
92
+ if (PATH_OR_CODE.test(q)) return { fast: false, reason: 'path-or-code' };
93
+ if (LIVE.test(q)) return { fast: false, reason: 'live' };
94
+ if (REQUEST_KO.test(q) || REQUEST_EN.test(q)) return { fast: false, reason: 'request' };
95
+ if (TASK.test(q)) return { fast: false, reason: 'task' };
96
+ if (GREETING.test(q)) return { fast: true, reason: 'greeting' };
97
+ const core = q.replace(TAIL, '').trim();
98
+ if (KO_QUESTION_END.test(core) || EN_QUESTION.test(core)) return { fast: true, reason: 'question' };
99
+ return { fast: false, reason: 'not-a-question' };
100
+ }