elanous 0.2.16 → 0.2.18

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 (354) 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/P6YTVgzuZz5oTZhY4Din_/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{1307-b46d45b705e8c9ae.js → 1307-30dff22c9e1f3073.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{6158.9c0449a9a8d0064b.js → 6158.af2e93b7adff709b.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d92f7ee3994e84c7.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3cb52e5ea8fc2fa5.js → page-4733c967b56a4277.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/board/page-d22fd849cc22f104.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-c1e20d7d338713f1.js → page-bb742f995c6769e7.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/bots/{page-482c5dbde699947c.js → page-02b073a2ac793b83.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/control/{page-a0e2a6d1bfb52fc5.js → page-308111abadf35670.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-c53598cfce6156f2.js → page-5d97b2901e0d1ea6.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/editor/{page-13a63953f53c6a23.js → page-c39554c321576982.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/exec/{page-5d3c8429d04f3831.js → page-8c3d8e63ae2e1c98.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/field/{page-3193bdc0bbab047a.js → page-9b72a07d424701c6.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/intake/{page-6107ca8ddd5e3bdd.js → page-a572f58cd3ca5864.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/market/page-fa244bd146a00ae8.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-44d56d6391965a6a.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/morning/{page-4167348049166852.js → page-83a700d4c754d802.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/{not-found-de52e8fdf9095f7a.js → not-found-7b3170f374f77b9d.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-ca3952cd78a83ca2.js → page-a3c58f5c72a5f3f9.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1ebfe049c640e171.js → page-1467047104141297.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-abe8e3b74bea1ffc.js → page-44d5f767491692bf.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/share/{page-d28573d6792b4275.js → page-3cc47dca0922ab9a.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-d751de5f8062ff5e.js → page-b17cd46cf0e9b8cf.js} +1 -1
  58. package/apps/pwa/out/_next/static/chunks/app/term/{page-fe1759f94003b32b.js → page-61553851aa606146.js} +1 -1
  59. package/apps/pwa/out/_next/static/chunks/app/today/{page-d64c984e855c8c68.js → page-a1f585e154e61c59.js} +1 -1
  60. package/apps/pwa/out/_next/static/chunks/app/trace/page-7bf54c9bb9274af8.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/vault/{page-9afdee66b820c76e.js → page-5150ef17b5383a57.js} +1 -1
  62. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-3544f154c87b43ee.js → page-134da468538baf48.js} +1 -1
  65. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
  67. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.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 -0
  87. package/apps/pwa/out/decisions/index.txt +30 -0
  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 +14 -14
  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 +14 -14
  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 +14 -14
  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/intake/intake-daily.yaml +9 -2
  160. package/graphs/intake/news-sources.yaml +4 -0
  161. package/graphs/intake/recipes.yaml +6 -1
  162. package/package.json +2 -1
  163. package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
  164. package/samples/workflows/templates/chat-driven-research.yaml +35 -0
  165. package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
  166. package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
  167. package/samples/workflows/templates/manual-quickstart.yaml +21 -0
  168. package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
  169. package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
  170. package/scripts/__fixtures__/intake-news.xml +7 -0
  171. package/scripts/ci-public-export-import-gate.ts +7 -2
  172. package/scripts/ci-typecheck-changed.ts +21 -6
  173. package/scripts/discord-seat-probe.ts +3 -2
  174. package/scripts/docs/generate-cli-anatomy.ts +162 -0
  175. package/scripts/goals/authoring-metrics.ts +73 -0
  176. package/scripts/intake-news.ts +169 -0
  177. package/scripts/landing-heal/must-fix-match.ts +7 -7
  178. package/scripts/landing-heal/run-mission.ts +3 -2
  179. package/scripts/public-export-test-run.ts +37 -2
  180. package/scripts/release-loop/checklist-gate-node.ts +13 -11
  181. package/scripts/release-loop/cut-branch.ts +2 -1
  182. package/scripts/release-loop/cutoff.ts +4 -2
  183. package/scripts/release-loop/docs-land-node.ts +77 -11
  184. package/scripts/release-loop/docs-node.ts +1 -1
  185. package/scripts/release-loop/gate-node.ts +99 -14
  186. package/scripts/release-loop/node-verdict.ts +2 -1
  187. package/scripts/release-loop/npm-publish-node.ts +11 -7
  188. package/scripts/release-loop/ops-upgrade-node.ts +8 -1
  189. package/scripts/release-loop/preflight.ts +246 -0
  190. package/scripts/release-loop/publish-node.ts +10 -3
  191. package/scripts/release-loop/release-docs.ts +134 -0
  192. package/scripts/release-loop/release-readiness.ts +11 -6
  193. package/scripts/release-loop/release-version.ts +48 -0
  194. package/scripts/release-loop/resume-release.ts +115 -0
  195. package/scripts/release-loop/unattended-release.ts +43 -7
  196. package/scripts/release-loop/version-node.ts +149 -41
  197. package/scripts/release-story/draft.ts +42 -14
  198. package/scripts/rhythm/daily.ts +25 -4
  199. package/scripts/skills/public-check.ts +62 -12
  200. package/scripts/test-deterministic.ts +51 -33
  201. package/scripts/test-diet/effectiveness.ts +62 -1
  202. package/scripts/test-diet/lib.ts +1 -1
  203. package/scripts/test-diet/node.ts +8 -1
  204. package/src/agent-mission/driver.ts +53 -10
  205. package/src/agent-mission/review-loop.ts +4 -3
  206. package/src/agent-substrate/pr-comment-meta.ts +20 -0
  207. package/src/autopilot/build/llm-conflict-merge.ts +28 -8
  208. package/src/autopilot/track-agent.ts +5 -3
  209. package/src/briefing/brief-items.ts +86 -17
  210. package/src/cli/autopilot-cli.ts +86 -1
  211. package/src/cli/brief-cli.ts +36 -2
  212. package/src/cli/decisions-cli.ts +4 -1
  213. package/src/cli/freeze-cli.ts +3 -2
  214. package/src/cli/intake-cli.ts +74 -2
  215. package/src/cli/log-event-names.ts +1 -0
  216. package/src/cli/notify-cli.ts +75 -0
  217. package/src/cli/pr-cli.ts +12 -1
  218. package/src/cli/provider-cli.ts +24 -0
  219. package/src/cli/release-cli.ts +101 -17
  220. package/src/cli/self-update-canary.ts +46 -0
  221. package/src/cli/self-update.ts +200 -1
  222. package/src/cli/tasks-cli.ts +309 -3
  223. package/src/context-bus/condense.ts +19 -3
  224. package/src/context-bus/context-day.ts +77 -24
  225. package/src/context-bus/context-first.ts +33 -0
  226. package/src/context-card/index.ts +45 -0
  227. package/src/context-card/task.ts +76 -0
  228. package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
  229. package/src/decisions/decision-ledger.ts +10 -1
  230. package/src/discord-self-message.ts +20 -2
  231. package/src/discord-slash-wire.ts +1 -0
  232. package/src/domains/outbound-alert.ts +200 -27
  233. package/src/domains/outbound-log-probe.ts +16 -1
  234. package/src/domains/release-tool.ts +2 -2
  235. package/src/execution-loop/gates.ts +11 -2
  236. package/src/flow-loop/split.ts +8 -4
  237. package/src/git-fs/worktree.ts +23 -3
  238. package/src/harness/harness-cli-command.ts +151 -11
  239. package/src/harness/harness-pod-dispatch.ts +11 -0
  240. package/src/harness/harness-queue.ts +466 -32
  241. package/src/harness/helper-scan.ts +10 -3
  242. package/src/harness/nested-elanous-policy.ts +23 -4
  243. package/src/hq/fence-wrapper.ts +123 -0
  244. package/src/hq/hq.ts +8 -0
  245. package/src/hq/seats.ts +369 -0
  246. package/src/index.ts +190 -23
  247. package/src/intake-plane/adopt-gate.ts +49 -0
  248. package/src/intake-plane/check.ts +3 -0
  249. package/src/intake-plane/collect-github.ts +57 -25
  250. package/src/intake-plane/digest.ts +39 -4
  251. package/src/intake-plane/discord-seat-work.ts +8 -2
  252. package/src/intake-plane/requirement-funnel.ts +316 -0
  253. package/src/knowledge/know-find.ts +10 -1
  254. package/src/loops/budget.ts +36 -0
  255. package/src/loops/orchestrator/finish-rate.ts +73 -1
  256. package/src/loops/orchestrator/grid-place.ts +11 -0
  257. package/src/loops/orchestrator/tick.ts +295 -20
  258. package/src/maturity/feature-maturity.ts +3 -2
  259. package/src/nexus/api/decisions-route.ts +11 -1
  260. package/src/nexus/api/grid.ts +61 -0
  261. package/src/nexus/api/http-server.ts +19 -1
  262. package/src/nexus/api/loop-edges.ts +69 -13
  263. package/src/nexus/api/ops-seats-sources.ts +2 -2
  264. package/src/nexus/api/task-cards-api.ts +19 -0
  265. package/src/nexus/api/workflow-templates.ts +3 -0
  266. package/src/oauth/codex.ts +120 -2
  267. package/src/onboarding/first-chat-gate.ts +29 -0
  268. package/src/release-loop/checklist.ts +104 -6
  269. package/src/release-loop/feature-store.ts +10 -9
  270. package/src/release-loop/landing-freeze.ts +24 -14
  271. package/src/release-loop/placement.ts +13 -3
  272. package/src/release-loop/rubric.ts +80 -0
  273. package/src/seat-dispatch/seat-ask.ts +79 -16
  274. package/src/seat-loop/seat-loop.ts +107 -37
  275. package/src/self-dev/ask-launch-flow.ts +50 -10
  276. package/src/self-dev/ask-path-candidates.ts +68 -0
  277. package/src/self-dev/dev-pipeline.ts +4 -0
  278. package/src/self-dev/draft-triage-rules.ts +1 -1
  279. package/src/self-dev/orchestrate-cli.ts +22 -0
  280. package/src/self-dev/round-pr-supersede.ts +244 -0
  281. package/src/self-dev/run-store.ts +5 -1
  282. package/src/self-dev/run-supervisor.ts +21 -1
  283. package/src/self-dev/supervisor-verdict-edges.ts +46 -0
  284. package/src/self-implement/follow-up-goals.ts +2 -2
  285. package/src/self-implement/frozen-merges.ts +25 -20
  286. package/src/self-implement/gate-remote.ts +427 -0
  287. package/src/self-implement/goal-author-clarification.ts +81 -0
  288. package/src/self-implement/goal-author-cli.ts +33 -5
  289. package/src/self-implement/goal-author.ts +93 -12
  290. package/src/self-implement/goal-clarification-escalation.ts +37 -1
  291. package/src/self-implement/goal-document-write.ts +28 -0
  292. package/src/self-implement/heal-intake.ts +4 -0
  293. package/src/self-implement/host-regate.ts +3 -1
  294. package/src/self-implement/l8-merge-queue.ts +29 -5
  295. package/src/self-implement/orchestrator.ts +78 -13
  296. package/src/self-implement/prior-draft-findings.ts +4 -3
  297. package/src/skills/skill-feedback.ts +76 -0
  298. package/src/task-agent/actions.ts +151 -0
  299. package/src/task-agent/judge.ts +122 -0
  300. package/src/task-agent/mission.ts +423 -0
  301. package/src/task-agent/shadow.ts +90 -0
  302. package/src/task-agent/task-hand.ts +235 -0
  303. package/src/task-cards/card-store.ts +4 -1
  304. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +43 -4
  305. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  306. package/src/task-orchestrator/surfaces/pod-pool.ts +81 -13
  307. package/src/task-orchestrator/surfaces/self-implement-pod.ts +225 -35
  308. package/src/telegram-commands.ts +1 -1
  309. package/src/telegram-loops-command.ts +22 -15
  310. package/src/telegram.ts +5 -20
  311. package/src/user-config.ts +86 -4
  312. package/src/version/packed-revision.json +1 -1
  313. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
  314. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/chat/page-8c2d9526164f5d09.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/layout-dce686e582c23937.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/market/page-51f811fa119308b6.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-57924393e3e4e539.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/settings/page-0f5cd57ca928f42b.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +0 -1
  352. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
  353. package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +0 -1
  354. /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
@@ -1,13 +1,14 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
1
+ import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { join } from 'node:path';
4
- import { effectiveInstanceRoot } from '../instance/resolve.js';
4
+ import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
5
5
 
6
6
  export interface LandingFreeze {
7
7
  reason: string;
8
8
  startedAt: string;
9
9
  until: string | null;
10
10
  by: string;
11
+ holdLaunches?: boolean;
11
12
  }
12
13
 
13
14
  export function landingFreezePath(root = effectiveInstanceRoot()): string {
@@ -25,21 +26,22 @@ export function readLandingFreeze(root = effectiveInstanceRoot(), now = new Date
25
26
  const freeze = record as Partial<LandingFreeze>;
26
27
  if (typeof freeze.reason !== 'string' || typeof freeze.by !== 'string' ||
27
28
  typeof freeze.startedAt !== 'string' || !Number.isFinite(Date.parse(freeze.startedAt)) ||
28
- !(freeze.until === null || (typeof freeze.until === 'string' && Number.isFinite(Date.parse(freeze.until))))) {
29
+ !(freeze.until === null || (typeof freeze.until === 'string' && Number.isFinite(Date.parse(freeze.until)))) ||
30
+ (freeze.holdLaunches !== undefined && typeof freeze.holdLaunches !== 'boolean')) {
29
31
  throw new Error(`invalid landing freeze: ${path}`);
30
32
  }
31
33
  // An expired freeze reads as «off». The file is left alone: deleting it here could race a concurrent `freeze on`
32
34
  // that just wrote a new one (only `freeze off` removes it; the next `freeze on` overwrites it).
33
35
  if (freeze.until && Date.parse(freeze.until) <= now.getTime()) return null;
34
- return freeze as LandingFreeze;
36
+ return { ...freeze, holdLaunches: freeze.holdLaunches ?? false } as LandingFreeze;
35
37
  }
36
38
 
37
- export function enableLandingFreeze(opts: { reason?: string; until?: string; by?: string }, root = effectiveInstanceRoot(), now = new Date()): LandingFreeze {
39
+ export function enableLandingFreeze(opts: { reason?: string; until?: string; by?: string; holdLaunches?: boolean }, root = effectiveInstanceRoot(), now = new Date()): LandingFreeze {
38
40
  const until = opts.until === undefined ? null : opts.until;
39
41
  if (until !== null && (!/^\d{4}-\d\d-\d\dT\d\d:\d\d(?::\d\d(?:\.\d+)?)?(?:Z|[+-]\d\d:\d\d)$/.test(until) || !Number.isFinite(Date.parse(until)) || Date.parse(until) <= now.getTime())) {
40
42
  throw new Error('--until must be a future ISO timestamp with timezone');
41
43
  }
42
- const freeze: LandingFreeze = { reason: opts.reason?.trim() || 'operator freeze', startedAt: now.toISOString(), until: until === null ? null : new Date(until).toISOString(), by: opts.by?.trim() || process.env.ELANOUS_TRACK || process.env.USER || 'cli' };
44
+ const freeze: LandingFreeze = { reason: opts.reason?.trim() || 'operator freeze', startedAt: now.toISOString(), until: until === null ? null : new Date(until).toISOString(), by: opts.by?.trim() || process.env.ELANOUS_TRACK || process.env.USER || 'cli', holdLaunches: opts.holdLaunches ?? false };
43
45
  const path = landingFreezePath(root);
44
46
  mkdirSync(root, { recursive: true });
45
47
  const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
@@ -60,15 +62,23 @@ function mergesDir(root: string): string { return join(root, 'landing-merges');
60
62
 
61
63
  export interface LandingMergeGuard { frozen: LandingFreeze | null; end: () => void }
62
64
 
63
- export function beginLandingMerge(root = effectiveInstanceRoot(), now = new Date()): LandingMergeGuard {
64
- const dir = mergesDir(root);
65
- mkdirSync(dir, { recursive: true });
66
- const marker = join(dir, `${process.pid}-${randomUUID()}.json`);
67
- writeFileSync(marker, `${JSON.stringify({ pid: process.pid, startedAt: now.toISOString() })}\n`, { mode: 0o600 });
68
- const end = () => { rmSync(marker, { force: true }); };
65
+ /** Mark both authorities before reading either freeze: the host and the child each drain their own in-flight merges. */
66
+ export function beginLandingMerge(root = effectiveInstanceRoot(), now = new Date(), prodFreezeRoot = prodInstanceRoot(), forceReason?: string): LandingMergeGuard {
67
+ const markers: string[] = [];
68
+ const end = () => { for (const marker of markers) rmSync(marker, { force: true }); };
69
+ try {
70
+ for (const authority of new Set([prodFreezeRoot, root])) {
71
+ const dir = mergesDir(authority);
72
+ mkdirSync(dir, { recursive: true });
73
+ const marker = join(dir, `${process.pid}-${randomUUID()}.json`);
74
+ markers.push(marker);
75
+ writeFileSync(marker, `${JSON.stringify({ pid: process.pid, startedAt: now.toISOString() })}\n`, { mode: 0o600 });
76
+ }
77
+ } catch (error) { end(); throw error; }
69
78
  let frozen: LandingFreeze | null;
70
- try { frozen = readLandingFreeze(root, now); } catch (error) { end(); throw error; }
71
- if (frozen) { end(); return { frozen, end: () => {} }; }
79
+ try { frozen = readLandingFreeze(prodFreezeRoot, now) ?? (root === prodFreezeRoot ? null : readLandingFreeze(root, now)); }
80
+ catch (error) { end(); throw error; }
81
+ if (frozen && !forceReason?.trim()) { end(); return { frozen, end: () => {} }; }
72
82
  return { frozen: null, end };
73
83
  }
74
84
 
@@ -8,6 +8,7 @@ import { userConfigPath } from '../user-config.js';
8
8
  import { addItem, listChecklist, ownerMatches, parseOwner, setItem, validateCeoLoad, type ChecklistItem } from './checklist.js';
9
9
  import { move, releasedVersion } from './feature-store.js';
10
10
  import { listSchedules, type ReleaseSchedule } from './release-schedule.js';
11
+ import { parseRubric, rubricScore } from './rubric.js';
11
12
 
12
13
  export type PlacementPriority = 'P0' | 'P1' | 'P2';
13
14
  export interface PlacementCell { id: string; title: string; owner: string; priority: PlacementPriority; predecessors: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }
@@ -271,20 +272,29 @@ export function rebalance(version: string, deps: PlacementDeps = {}): RebalanceR
271
272
  const backlog = deps.backlog ?? BACKLOG_VERSIONS;
272
273
  const next = rows.find((row) => versionOrder(row.version, version) > 0 && !backlog.includes(row.version));
273
274
  const snapshots = new Map(rows.map((row) => [row.version, [...(deps.checklist ?? listChecklist)(row.version).items]]));
274
- const unstarted = snapshots.get(version)!.filter((item) => item.status === 'yellow' && !item.evidence);
275
+ const unstarted = snapshots.get(version)!.filter((item) => item.status === 'yellow' && !item.evidence)
276
+ .map((item, index) => {
277
+ const rubric = parseRubric(`${item.title}\n${item.evidence ?? ''}`);
278
+ return { item, index, score: rubric ? rubricScore(rubric) : null };
279
+ })
280
+ .sort((a, b) => {
281
+ if (a.score === null) return b.score === null ? a.index - b.index : 1;
282
+ if (b.score === null) return -1;
283
+ return a.score - b.score || a.index - b.index;
284
+ });
275
285
  if (unstarted.length && !next) throw new CliUserError(`${version} 다음 판이 없다`);
276
286
  if (!unstarted.length) return { decisions: [], blocked: [] };
277
287
  const rate = deps.merged24h ?? mergedPrsLast24h(new Date(now));
278
288
  const caps = deps.seatCap ?? placementSeatCap();
279
289
  const ceoCap = validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap());
280
290
  const result: RebalanceResult = { decisions: [], blocked: [] };
281
- for (const item of unstarted) {
291
+ for (const { item, score } of unstarted) {
282
292
  const violation = moveConstraint(item, next!, backlog, snapshots, now, rate, caps, rows, ceoCap);
283
293
  if (violation) {
284
294
  result.blocked.push({ id: item.id, from: version, to: next!.version, reason: violation });
285
295
  continue;
286
296
  }
287
- const reason = `${version} 착지 마감 2시간 전 미시작 칸 이월`;
297
+ const reason = `${version} 착지 마감 2시간 전 미시작 칸 이월${score === null ? '' : ` · 루브릭 ${score}`}`;
288
298
  result.decisions.push({ id: item.id, from: version, version: next!.version, reason, displaced: [] });
289
299
  snapshots.set(version, snapshots.get(version)!.filter((cell) => cell.id !== item.id));
290
300
  snapshots.get(next!.version)!.push(item);
@@ -0,0 +1,80 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import type { ChecklistItem } from './checklist.js';
5
+
6
+ export interface Rubric {
7
+ A: number;
8
+ E: number;
9
+ R: number;
10
+ D: number;
11
+ M: number;
12
+ B: number;
13
+ S: number;
14
+ X: number;
15
+ }
16
+
17
+ const AXES = ['A', 'E', 'R', 'D', 'M', 'B', 'S', 'X'] as const;
18
+
19
+ /** Read one explicit rubric line; an old score in parentheses is not an axis. */
20
+ export function parseRubric(text: string): Rubric | null {
21
+ for (const line of text.split(/\r?\n/)) {
22
+ const match = /(?:^|\s)루브릭:\s*([^\r\n]*)/.exec(line);
23
+ if (!match) continue;
24
+ const values: Partial<Rubric> = {};
25
+ const body = match[1]!.replace(/\([^)]*\)/g, ' ');
26
+ for (const token of body.trim().split(/\s+/)) {
27
+ const axis = /^([AERDMBSX])([0-3])$/.exec(token);
28
+ if (axis) values[axis[1] as keyof Rubric] = Number(axis[2]);
29
+ }
30
+ if (AXES.every((axis) => values[axis] !== undefined)) return values as Rubric;
31
+ }
32
+ return null;
33
+ }
34
+
35
+ export function rubricScore(r: Rubric): number {
36
+ return 2 * r.A + 2 * r.E + 2 * r.R + 1.5 * r.D + r.M + r.B - 0.5 * r.S - 0.5 * r.X;
37
+ }
38
+
39
+ export function rubricGrade(score: number): 'P1' | 'P2' | 'P3' | 'P4' {
40
+ if (score >= 11) return 'P1';
41
+ if (score >= 8) return 'P2';
42
+ if (score >= 5) return 'P3';
43
+ return 'P4';
44
+ }
45
+
46
+ /** Inspect an existing checklist without initializing, importing, or migrating its ledger. */
47
+ export function readRubricItems(version: string, ledgerRoot: string): Pick<ChecklistItem, 'id' | 'title' | 'evidence' | 'priority'>[] {
48
+ const dbPath = join(ledgerRoot, 'release', 'features.sqlite');
49
+ if (existsSync(dbPath)) {
50
+ const db = new Database(dbPath, { readonly: true, strict: true });
51
+ try {
52
+ const columns = new Set((db.query('PRAGMA table_info(assignments)').all() as Array<{ name: string }>).map((row) => row.name));
53
+ const priority = columns.has('priority') ? 'a.priority' : 'NULL';
54
+ const rows = db.query(`SELECT f.id, COALESCE(a.title_override, f.title) AS title, a.evidence, ${priority} AS priority
55
+ FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.version = ? ORDER BY a.rowid`)
56
+ .all(version) as Array<{ id: string; title: string; evidence: string | null; priority: ChecklistItem['priority'] | null }>;
57
+ const imported = db.query('SELECT 1 FROM imported_versions WHERE version = ?').get(version);
58
+ if (rows.length || imported) {
59
+ const refs = db.query('SELECT feature_id, ref FROM evidence WHERE version = ? ORDER BY rowid')
60
+ .all(version) as Array<{ feature_id: string; ref: string }>;
61
+ const refsById = new Map<string, string[]>();
62
+ for (const { feature_id, ref } of refs) {
63
+ const list = refsById.get(feature_id) ?? [];
64
+ list.push(ref);
65
+ refsById.set(feature_id, list);
66
+ }
67
+ return rows.map((row) => {
68
+ const evidence = [row.evidence, ...(refsById.get(row.id) ?? [])]
69
+ .filter((value): value is string => value !== null).join('\n');
70
+ return { id: row.id, title: row.title, ...(evidence ? { evidence } : {}), ...(row.priority ? { priority: row.priority } : {}) };
71
+ });
72
+ }
73
+ } finally { db.close(); }
74
+ }
75
+ const path = join(ledgerRoot, 'release', version, 'checklist.json');
76
+ if (!existsSync(path)) return [];
77
+ const legacy = JSON.parse(readFileSync(path, 'utf8')) as { version: string; items: ChecklistItem[] };
78
+ if (legacy.version !== version || !Array.isArray(legacy.items)) throw new Error(`잘못된 체크리스트: ${path}`);
79
+ return legacy.items;
80
+ }
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { join } from 'node:path';
3
3
  import { debug } from '../debug/log.js';
4
4
  import { openMsgStore, type MsgStore } from '../msg/msg-store.js';
5
- import { dispatchCeoTask, type CeoCommandDeps } from './ceo-commands.js';
5
+ import { CEO_SEAT_COMMANDS, dispatchCeoTask, type CeoCommandDeps } from './ceo-commands.js';
6
6
  import type { SeatId } from './seat-questions.js';
7
7
 
8
8
  export type AskOrigin =
@@ -23,7 +23,7 @@ function createTable(store: MsgStore): void {
23
23
  answer TEXT, answered_at INTEGER, asked_at INTEGER
24
24
  ); CREATE TABLE IF NOT EXISTS seat_ask_outbox (
25
25
  id TEXT PRIMARY KEY, origin TEXT NOT NULL, text TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending',
26
- claim_until INTEGER NOT NULL DEFAULT 0, claim_token TEXT
26
+ claim_until INTEGER NOT NULL DEFAULT 0, claim_token TEXT, sent_at INTEGER
27
27
  )`);
28
28
  const askColumns = store.db.query('PRAGMA table_info(seat_asks)').all() as Array<{ name: string }>;
29
29
  if (!askColumns.some(({ name }) => name === 'answer')) store.db.exec('ALTER TABLE seat_asks ADD COLUMN answer TEXT');
@@ -32,6 +32,7 @@ function createTable(store: MsgStore): void {
32
32
  const columns = store.db.query('PRAGMA table_info(seat_ask_outbox)').all() as Array<{ name: string }>;
33
33
  if (!columns.some(({ name }) => name === 'claim_until')) store.db.exec('ALTER TABLE seat_ask_outbox ADD COLUMN claim_until INTEGER NOT NULL DEFAULT 0');
34
34
  if (!columns.some(({ name }) => name === 'claim_token')) store.db.exec('ALTER TABLE seat_ask_outbox ADD COLUMN claim_token TEXT');
35
+ if (!columns.some(({ name }) => name === 'sent_at')) store.db.exec('ALTER TABLE seat_ask_outbox ADD COLUMN sent_at INTEGER');
35
36
  }
36
37
 
37
38
  export interface SeatAskShadow {
@@ -77,10 +78,24 @@ export interface SeatAskDeps {
77
78
  botId?: string;
78
79
  }
79
80
 
80
- /** Persist the return address before delivering through the /cto channel and seat inbox. */
81
- export async function askSeat(text: string, origin: AskOrigin, command: CeoCommandDeps, deps: SeatAskDeps): Promise<string> {
81
+ const seatTitle = (seat: SeatId): string => Object.entries(CEO_SEAT_COMMANDS)
82
+ .find(([, id]) => id === seat)![0]!.toUpperCase();
83
+
84
+ /** Persist the return address before delivering through the coordination channel and seat inbox. */
85
+ export function askSeatAs(seat: SeatId, body: string, origin: AskOrigin, command: CeoCommandDeps, deps: SeatAskDeps,
86
+ kind: 'task' | 'question' = 'task'): Promise<string> {
87
+ return recordAndDispatchSeatAsk(seat, body, origin, command, deps, kind);
88
+ }
89
+
90
+ /** Preserve the CTO question form and its original receipt. */
91
+ export function askSeat(text: string, origin: AskOrigin, command: CeoCommandDeps, deps: SeatAskDeps): Promise<string> {
82
92
  const body = parseSeatAsk(text);
83
93
  if (!body) throw new Error('not a CTO ask');
94
+ return askSeatAs('TC', body, origin, command, deps, 'question');
95
+ }
96
+
97
+ async function recordAndDispatchSeatAsk(seat: SeatId, body: string, origin: AskOrigin, command: CeoCommandDeps, deps: SeatAskDeps,
98
+ kind: 'task' | 'question'): Promise<string> {
84
99
  const id = randomUUID();
85
100
  const timeout = deps.timeoutMs ?? DEFAULT_ASK_TIMEOUT_MS;
86
101
  if (!Number.isSafeInteger(timeout) || timeout < 1) throw new Error('invalid seat ask timeout');
@@ -90,13 +105,14 @@ export async function askSeat(text: string, origin: AskOrigin, command: CeoComma
90
105
  createTable(store);
91
106
  const askedAt = (deps.now ?? Date.now)();
92
107
  store.db.query('INSERT INTO seat_asks (id, seat, origin, deadline, timeout_minutes, status, asked_at) VALUES (?, ?, ?, ?, ?, ?, ?)')
93
- .run(id, 'TC', JSON.stringify(origin), askedAt + timeout, minutes, 'pending', askedAt);
108
+ .run(id, seat, JSON.stringify(origin), askedAt + timeout, minutes, 'pending', askedAt);
94
109
  } finally { store.close(); }
95
110
  try {
96
- const result = await (deps.dispatch ?? dispatchCeoTask)('TC', `질문: ${body}\n답장 요청: ${id}\n회신: elanous seat answer ${id} "<답>"`, command,
111
+ const result = await (deps.dispatch ?? dispatchCeoTask)(seat, `${kind === 'question' ? '질문' : '일'}: ${body}\n답장 요청: ${id}\n회신: elanous seat answer ${id} "<답>"`, command,
97
112
  { via: origin.channel, ref: `ask:${id}` });
98
- debug.log('seat.ask', 'sent', { seat: 'TC', id, via: origin.channel, channel: result.channel });
99
- return `CTO에게 물었습니다. 답을 기다립니다 (최대 ${minutes}분). 요청: ${id}${result.channel === 'failed' ? ` · 조율 채널 전송 실패: ${result.channelError}` : ''}`;
113
+ debug.log('seat.ask', 'sent', { seat, id, via: origin.channel, channel: result.channel });
114
+ const title = seatTitle(seat);
115
+ return `${kind === 'question' ? `${title}에게 물었습니다. 답을 기다립니다` : `${title}에게 맡겼습니다. 결과를 이 대화로 돌려드립니다`} (최대 ${minutes}분). 요청: ${id}${result.channel === 'failed' ? ` · 조율 채널 전송 실패: ${result.channelError}` : ''}`;
100
116
  } catch (error) {
101
117
  const failed = (deps.open ?? openMsgStore)();
102
118
  try { createTable(failed); failed.db.query('DELETE FROM seat_asks WHERE id = ?').run(id); }
@@ -114,10 +130,11 @@ export function answerSeatAsk(id: string, text: string, deps: Pick<SeatAskDeps,
114
130
  createTable(store);
115
131
  const now = (deps.now ?? Date.now)();
116
132
  const updated = store.db.query(`UPDATE seat_asks SET answer = ?, answered_at = ?
117
- WHERE id = ? AND seat = 'TC' AND status = 'pending' AND answer IS NULL AND deadline > ?`)
133
+ WHERE id = ? AND status = 'pending' AND answer IS NULL AND deadline > ?`)
118
134
  .run(answer, now, id, now);
119
135
  if (updated.changes !== 1) throw new Error(`seat ask not pending or deadline passed: ${id}`);
120
- debug.log('seat.ask', 'reply-recorded', { seat: 'TC', id });
136
+ const row = store.db.query('SELECT seat FROM seat_asks WHERE id = ?').get(id) as { seat: string };
137
+ debug.log('seat.ask', 'reply-recorded', { seat: row.seat, id });
121
138
  } finally { store.close(); }
122
139
  }
123
140
 
@@ -133,6 +150,49 @@ export function seatAskReplyInfo(id: string, open: () => MsgStore = openMsgStore
133
150
  } finally { store.close(); }
134
151
  }
135
152
 
153
+ export interface SeatAskRoundTrip {
154
+ id: string;
155
+ surface: AskOrigin['channel'];
156
+ askedAt: number | null;
157
+ answeredAt: number | null;
158
+ deliveredAt: number | null;
159
+ roundTripMs: number | null;
160
+ verdict: '10분 안' | '10분 넘음' | '미답' | '답 대기' | '전달 대기' | '시각 미상';
161
+ }
162
+
163
+ /** Read one row per CTO question, including undelivered and legacy rows. since is an epoch-millisecond lower bound. */
164
+ export function listSeatAskRoundTrips({ since, now = Date.now(), open = openMsgStore }: {
165
+ since: number; now?: number; open?: () => MsgStore;
166
+ }): SeatAskRoundTrip[] {
167
+ if (!Number.isFinite(since) || !Number.isFinite(now)) throw new Error('invalid seat ask time window');
168
+ const store = open();
169
+ try {
170
+ createTable(store);
171
+ const rows = store.db.query(`SELECT a.id, a.origin, a.asked_at, a.answered_at, a.deadline, a.status,
172
+ a.answer IS NOT NULL AS has_answer, o.status AS outbox_status, o.sent_at
173
+ FROM seat_asks a LEFT JOIN seat_ask_outbox o ON o.id = a.id
174
+ WHERE a.seat = 'TC' AND a.status != 'shadow'
175
+ AND COALESCE(a.asked_at, a.deadline - a.timeout_minutes * 60000) >= ?
176
+ ORDER BY COALESCE(a.asked_at, a.deadline - a.timeout_minutes * 60000) DESC, a.id DESC`)
177
+ .all(since) as Array<{ id: string; origin: string; asked_at: number | null; answered_at: number | null;
178
+ deadline: number; status: string; has_answer: number; outbox_status: string | null; sent_at: number | null }>;
179
+ return rows.map(row => {
180
+ const surface = (JSON.parse(row.origin) as AskOrigin).channel;
181
+ const deliveredAt = row.outbox_status === 'sent' ? row.sent_at : null;
182
+ const roundTripMs = row.answered_at === null || row.asked_at === null || deliveredAt === null ? null : deliveredAt - row.asked_at;
183
+ const verdict: SeatAskRoundTrip['verdict'] = row.asked_at === null ? '시각 미상'
184
+ : row.status === 'expired' || (!row.has_answer && row.status === 'pending' && now >= row.deadline) ? '미답'
185
+ : row.status === 'answered' || row.has_answer ? row.outbox_status === 'sent' && deliveredAt === null ? '시각 미상'
186
+ : deliveredAt === null ? '전달 대기'
187
+ : row.answered_at === null || roundTripMs === null ? '시각 미상'
188
+ : roundTripMs <= 600_000 ? '10분 안' : '10분 넘음'
189
+ : '답 대기';
190
+ return { id: row.id, surface, askedAt: row.asked_at, answeredAt: row.answered_at,
191
+ deliveredAt, roundTripMs, verdict };
192
+ });
193
+ } finally { store.close(); }
194
+ }
195
+
136
196
  /** Deliver recorded seat answers or expiration notices via the original surface. */
137
197
  export async function deliverSeatAnswers(deps: SeatAskDeps & { askId?: string }): Promise<void> {
138
198
  const store = (deps.open ?? openMsgStore)();
@@ -150,9 +210,11 @@ export async function deliverSeatAnswers(deps: SeatAskDeps & { askId?: string })
150
210
  : reply && Date.parse(reply.created_at) < ask.deadline ? reply.body.slice(id.length + 2).trim() : '';
151
211
  const status = answer ? 'answered' : now >= ask.deadline ? 'expired' : null;
152
212
  if (!status) return null;
153
- const text = status === 'answered' ? `CTO 답변 (${id}): ${answer}`
154
- : `CTO 미답 (${id}): 아직 답 없음 (${ask.timeout_minutes}분 경과).`;
155
- const updated = store.db.query("UPDATE seat_asks SET status = ? WHERE id = ? AND status = 'pending'").run(status, id);
213
+ const title = seatTitle(ask.seat as SeatId);
214
+ const text = status === 'answered' ? `${title} 답변 (${id}): ${answer}`
215
+ : `${title} 미답 (${id}): 아직 답 없음 (${ask.timeout_minutes}분 경과).`;
216
+ const updated = store.db.query("UPDATE seat_asks SET status = ?, answered_at = COALESCE(answered_at, ?) WHERE id = ? AND status = 'pending'")
217
+ .run(status, status === 'answered' && reply ? Date.parse(reply.created_at) : null, id);
156
218
  if (updated.changes !== 1) return null;
157
219
  store.db.query('INSERT INTO seat_ask_outbox (id, origin, text) VALUES (?, ?, ?)').run(id, ask.origin, text);
158
220
  return status;
@@ -184,7 +246,8 @@ export async function deliverSeatAnswers(deps: SeatAskDeps & { askId?: string })
184
246
  if (claimed.changes !== 1) continue;
185
247
  try {
186
248
  await deps.send(origin, item.text);
187
- store.db.query("UPDATE seat_ask_outbox SET status = 'sent', claim_token = NULL WHERE id = ? AND claim_token = ?").run(item.id, token);
249
+ store.db.query("UPDATE seat_ask_outbox SET status = 'sent', sent_at = ?, claim_token = NULL WHERE id = ? AND claim_token = ?")
250
+ .run((deps.now ?? Date.now)(), item.id, token);
188
251
  } catch (error) {
189
252
  store.db.query('UPDATE seat_ask_outbox SET claim_until = 0, claim_token = NULL WHERE id = ? AND claim_token = ?').run(item.id, token);
190
253
  throw error;
@@ -209,7 +272,7 @@ export function acknowledgeSeatAnswers(clientId: string, ids: string[], open: ()
209
272
  const store = open();
210
273
  try {
211
274
  createTable(store);
212
- const ack = store.db.query("UPDATE seat_ask_outbox SET status = 'sent' WHERE id = ? AND origin = ? AND status = 'pending'");
213
- for (const id of ids) ack.run(id, JSON.stringify({ channel: 'pwa', clientId }));
275
+ const ack = store.db.query("UPDATE seat_ask_outbox SET status = 'sent', sent_at = ? WHERE id = ? AND origin = ? AND status = 'pending'");
276
+ for (const id of ids) ack.run(Date.now(), id, JSON.stringify({ channel: 'pwa', clientId }));
214
277
  } finally { store.close(); }
215
278
  }