elanous 0.2.8 → 0.2.10

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 (385) 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/2DsiDfDCks44g1cPUMhiW/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/24-e6d28380efab7faf.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/3615-740dd270764ffa24.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/3734-1015fe991eb9b7e7.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/5012-a7fe6f3c8b6fe45a.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/5783-ef40ebfc91d55230.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/62-ff227c3edb8b248b.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/7633-1b3709b0854c9412.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/7951-8b03dbb10b24cd06.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/8067-87ce253ec2f23697.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8195-6fde3d7a39859206.js} +5 -5
  14. package/apps/pwa/out/_next/static/chunks/8894-b5fb36bab837cd29.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/9454-84d0d5d77446bd92.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/988-5f92e7ab7ccea3dd.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/9989-5582582f06429840.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d2253d7e58ef1890.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5441d4a9ab35e518.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/board/page-d9b12d680922d980.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-d4b6df905f185055.js → page-54b32bd27726588d.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/bots/page-e268d6d145827a22.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/chat/{page-186fc6952f027acc.js → page-f8116559c7424da7.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/consult/page-1085399fd0f32112.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/control/page-1df686a14f65895a.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-cbbe03c0da7b081a.js → page-54e1f966ab60a4dc.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/editor/page-7ed95f296f5ee8e9.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/exec/page-11edac668d77e5c3.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/field/page-59e2b9a22cd5a6d5.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/intake/page-2976df21ad74ab48.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/layout-34d211a67f16d510.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/live/page-8ff652d5d950639b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/market/{page-c0c0d19d51342d3e.js → page-b31610deda76337b.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-2e002ad8faa43291.js → page-6cb0e1bbefe5fb3d.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/morning/page-a286a152cf584576.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/not-found-d9237581e7003bcb.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/observatory/page-83274e761643fdd2.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ceee6cfb1d4e6d68.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-9c9f8bb3e4a64091.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c17217603ea3426a.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/page-efb6177cbf1e8a51.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d307523703bc4df3.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-462f4e4bab45ce7d.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8990e96f33af3837.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-7464c1987dd76e6d.js → page-e168fa1030af0548.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/settings/page-1233e05a4068bc9c.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-13a1840b1fac337a.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/setup/page-64a29fc80b11ab55.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/share/page-68e787339a16b3dc.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-d8ab4d1489223459.js → page-4fddc90b23feb215.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/tasks/page-886ba8260dc79c79.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/term/page-0f26507a1722e40b.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/today/page-109d4ccd42259c9d.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/trace/page-f5c2a4e510726774.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/vault/page-b2e0039416f42522.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-26102fd7350f171e.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/workflows/page-708948bc8a802794.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/workspace/page-5e5611620f78fb0b.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-139ba5c367bbf60a.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/main-app-202b4e81f33895ea.js +1 -0
  61. package/apps/pwa/out/_next/static/css/43feca5fd387dcbd.css +1 -0
  62. package/apps/pwa/out/approvals/index.html +1 -1
  63. package/apps/pwa/out/approvals/index.txt +30 -29
  64. package/apps/pwa/out/autopilot/index.html +1 -1
  65. package/apps/pwa/out/autopilot/index.txt +30 -29
  66. package/apps/pwa/out/board/index.html +1 -1
  67. package/apps/pwa/out/board/index.txt +30 -29
  68. package/apps/pwa/out/botlab/index.html +1 -1
  69. package/apps/pwa/out/botlab/index.txt +27 -26
  70. package/apps/pwa/out/bots/index.html +1 -1
  71. package/apps/pwa/out/bots/index.txt +27 -26
  72. package/apps/pwa/out/chat/index.html +1 -1
  73. package/apps/pwa/out/chat/index.txt +27 -26
  74. package/apps/pwa/out/consult/index.html +1 -1
  75. package/apps/pwa/out/consult/index.txt +30 -29
  76. package/apps/pwa/out/control/index.html +1 -1
  77. package/apps/pwa/out/control/index.txt +30 -29
  78. package/apps/pwa/out/design-check/index.html +1 -1
  79. package/apps/pwa/out/design-check/index.txt +30 -29
  80. package/apps/pwa/out/editor/index.html +1 -1
  81. package/apps/pwa/out/editor/index.txt +30 -29
  82. package/apps/pwa/out/exec/index.html +1 -1
  83. package/apps/pwa/out/exec/index.txt +30 -29
  84. package/apps/pwa/out/field/index.html +1 -1
  85. package/apps/pwa/out/field/index.txt +30 -29
  86. package/apps/pwa/out/healthz +1 -1
  87. package/apps/pwa/out/index.html +1 -1
  88. package/apps/pwa/out/index.txt +27 -26
  89. package/apps/pwa/out/intake/index.html +1 -1
  90. package/apps/pwa/out/intake/index.txt +30 -29
  91. package/apps/pwa/out/live/index.html +1 -1
  92. package/apps/pwa/out/live/index.txt +30 -29
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +27 -26
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +27 -26
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +30 -29
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +30 -29
  101. package/apps/pwa/out/ops/checklist/index.html +1 -0
  102. package/apps/pwa/out/ops/checklist/index.txt +30 -0
  103. package/apps/pwa/out/ops/release/index.html +1 -0
  104. package/apps/pwa/out/ops/release/index.txt +30 -0
  105. package/apps/pwa/out/ops/seats/index.html +1 -0
  106. package/apps/pwa/out/ops/seats/index.txt +30 -0
  107. package/apps/pwa/out/reflection/index.html +1 -1
  108. package/apps/pwa/out/reflection/index.txt +28 -27
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +30 -29
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +27 -26
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +30 -29
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +30 -29
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +30 -29
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +30 -29
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +30 -29
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +27 -26
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +30 -29
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +30 -29
  129. package/apps/pwa/out/today/index.html +1 -0
  130. package/apps/pwa/out/today/index.txt +33 -0
  131. package/apps/pwa/out/trace/index.html +1 -1
  132. package/apps/pwa/out/trace/index.txt +30 -29
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +30 -29
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +30 -29
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +30 -29
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +30 -29
  143. package/graphs/field/field-feed.yaml +2 -0
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/graphs/release/recipes.yaml +3 -0
  146. package/graphs/release/release-loop.yaml +3 -1
  147. package/graphs/test-diet/nightly-audit.yaml +18 -0
  148. package/graphs/test-diet/recipes.yaml +3 -0
  149. package/package.json +1 -1
  150. package/plugins/card-followup/graphs/run-step.ts +48 -13
  151. package/plugins/doc-draft/graphs/doc-draft.yaml +3 -3
  152. package/plugins/doc-draft/graphs/run-step.ts +14 -7
  153. package/scripts/ci-pwa-build-gate.ts +160 -0
  154. package/scripts/coord-channel-watch.sh +42 -8
  155. package/scripts/coord-digest.ts +199 -0
  156. package/scripts/coord-post.sh +60 -3
  157. package/scripts/install.ps1 +68 -9
  158. package/scripts/install.sh +39 -12
  159. package/scripts/lib/pwa-scenarios/scenarios.ts +20 -0
  160. package/scripts/lib/tui-regress-checks.ts +146 -0
  161. package/scripts/memory-lifecycle-cycle.ts +2 -2
  162. package/scripts/ops-now-probe.ts +327 -0
  163. package/scripts/publish-internal-dist.ts +11 -1
  164. package/scripts/release-loop/cutoff.ts +31 -2
  165. package/scripts/release-loop/export-check-node.ts +85 -0
  166. package/scripts/release-loop/gate-node.ts +18 -4
  167. package/scripts/release-loop/node-verdict.ts +8 -1
  168. package/scripts/release-loop/ops-upgrade-node.ts +72 -16
  169. package/scripts/release-loop/release-readiness.ts +9 -0
  170. package/scripts/release-loop/version-node.ts +58 -10
  171. package/scripts/repo-watch-cycle.ts +1 -1
  172. package/scripts/retro-cycle.ts +2 -2
  173. package/scripts/schedule-health-report.ts +1 -1
  174. package/scripts/storyboard/storyboard.ts +5 -161
  175. package/scripts/test-diet/lib.ts +31 -1
  176. package/scripts/test-diet/node.ts +134 -17
  177. package/scripts/test-timeout-audit.ts +114 -0
  178. package/scripts/tree-sync-alert.ts +1 -1
  179. package/scripts/tui-regress.ts +71 -0
  180. package/scripts/waitlist-daily-digest.ts +1 -1
  181. package/skills/explainer-video/SKILL.md +4 -0
  182. package/skills/explainer-video/engine/reel.mjs +18 -13
  183. package/skills/explainer-video/engine/reel.sh +24 -5
  184. package/skills/explainer-video/engine/stitch.mjs +145 -0
  185. package/skills/pitch-deck/SKILL.md +36 -0
  186. package/skills/pitch-deck/template.html +80 -0
  187. package/src/acp/codex-approval-adapter.ts +9 -3
  188. package/src/acp/core-turn-bridge.ts +117 -18
  189. package/src/acp/fact-question.ts +11 -0
  190. package/src/acp/server.ts +48 -7
  191. package/src/acp/tool-approval.ts +126 -0
  192. package/src/acp/turn-runner.ts +36 -0
  193. package/src/agent/monad-agent-turn.ts +8 -0
  194. package/src/ask-user-question/pending-questions.ts +25 -10
  195. package/src/ask-user-question/types.ts +9 -0
  196. package/src/auth/setup-link-tokens.fixture.ts +19 -0
  197. package/src/auth/setup-link-tokens.ts +164 -0
  198. package/src/autopilot/build/integrity-gate.ts +12 -0
  199. package/src/boot/daemon-prompt-turn.ts +3 -0
  200. package/src/boot/daemon-tools/index.ts +8 -0
  201. package/src/boot/daemon-tools/types.ts +2 -0
  202. package/src/boot/daemon-tools/web-search.ts +1 -0
  203. package/src/budget/unified-usage.ts +75 -0
  204. package/src/channel-bot-token.ts +37 -10
  205. package/src/chat/ctrl-c-in-input.ts +14 -0
  206. package/src/chat/index.ts +61 -9
  207. package/src/chat/pickers/state.ts +6 -2
  208. package/src/cli/agent-cli.ts +7 -0
  209. package/src/cli/decisions-cli.ts +25 -3
  210. package/src/cli/doctor-bundle-upload.ts +68 -0
  211. package/src/cli/doctor-cli.ts +38 -4
  212. package/src/cli/op-proxy-sync.ts +83 -0
  213. package/src/cli/ops-cli.ts +20 -0
  214. package/src/cli/pod-cli.ts +49 -0
  215. package/src/cli/pr-cli.ts +37 -4
  216. package/src/cli/pwa-stop.ts +163 -13
  217. package/src/cli/pwa-test.ts +3 -0
  218. package/src/cli/release-cli.ts +98 -15
  219. package/src/cli/self-update.ts +14 -0
  220. package/src/cli/storyboard-cli.ts +37 -0
  221. package/src/cli/test-state-dir-flag.ts +11 -4
  222. package/src/connectors/linear.ts +146 -0
  223. package/src/context-bus/coord-events.ts +115 -0
  224. package/src/dashboard/dashboard-boot-fixture.ts +23 -0
  225. package/src/dashboard/dashboard-status-lines.ts +45 -0
  226. package/src/dashboard/first-screen-band.ts +35 -0
  227. package/src/dashboard/index.ts +128 -34
  228. package/src/dashboard/provider-slash-runtime.ts +28 -11
  229. package/src/dashboard/slash-runtime/dashboard-handlers.ts +17 -0
  230. package/src/dashboard/slash-runtime/help-from-registry.ts +43 -28
  231. package/src/dashboard/turn-stream-formatter.ts +1 -1
  232. package/src/decisions/decision-cards.ts +50 -2
  233. package/src/decisions/decision-ledger.ts +75 -4
  234. package/src/decisions/decision-linear-projection.ts +157 -0
  235. package/src/decisions/web-push-decision-cards.ts +50 -0
  236. package/src/directive/interpretation-card.ts +36 -0
  237. package/src/discord.ts +2 -1
  238. package/src/domains/coo-admin-tool.ts +62 -0
  239. package/src/domains/core-tools.ts +11 -3
  240. package/src/domains/elanous-card.ts +29 -0
  241. package/src/domains/ops-facts-tool.ts +76 -0
  242. package/src/domains/outbound-alert.ts +41 -13
  243. package/src/domains/release-tool.ts +168 -0
  244. package/src/domains/telegram-kind-route.ts +19 -1
  245. package/src/exec-requests/planner.ts +98 -28
  246. package/src/exec-requests/runner.ts +107 -10
  247. package/src/exec-requests/store.ts +8 -2
  248. package/src/field/field-media.ts +10 -7
  249. package/src/field/field-reel.ts +14 -4
  250. package/src/git-fs/worktree.ts +45 -6
  251. package/src/graph-runner/graph-approval-card.ts +113 -0
  252. package/src/graph-runner/graph-cli.ts +129 -4
  253. package/src/graph-runner/graph-notify.ts +25 -3
  254. package/src/graph-runner/runner.ts +142 -16
  255. package/src/harness/browser-verify.ts +25 -2
  256. package/src/harness/harness-cli-command.ts +231 -35
  257. package/src/harness/harness-incidents-writer.fixture.ts +24 -0
  258. package/src/harness/harness-incidents.ts +129 -0
  259. package/src/harness/harness-stop.ts +53 -3
  260. package/src/hitl/clarification-policy.ts +3 -1
  261. package/src/index.ts +172 -41
  262. package/src/intake-plane/author-goals.ts +37 -4
  263. package/src/intake-plane/check.ts +3 -0
  264. package/src/intake-plane/discord-seat-work.ts +47 -2
  265. package/src/intake-plane/seat-doc-route.ts +170 -6
  266. package/src/intake-plane/telegram-seat-work.ts +44 -2
  267. package/src/intake-plane/telegram-work.ts +1 -0
  268. package/src/maturity/cli-maturity.ts +34 -0
  269. package/src/maturity/feature-maturity.ts +100 -1
  270. package/src/maturity/tui-slash-maturity.ts +31 -0
  271. package/src/memory.ts +32 -4
  272. package/src/nexus/api/exec-requests.ts +33 -3
  273. package/src/nexus/api/http-server.ts +73 -1
  274. package/src/nexus/api/meta-api.ts +38 -0
  275. package/src/nexus/api/operator.ts +61 -0
  276. package/src/nexus/api/ops-api.ts +148 -0
  277. package/src/nexus/api/ops-seats-sources.ts +95 -0
  278. package/src/nexus/api/ops-seats.ts +124 -0
  279. package/src/nexus/api/public-routes.ts +14 -1
  280. package/src/nexus/api/seat-requests.ts +88 -8
  281. package/src/nexus/api/sessions-store.ts +63 -0
  282. package/src/nexus/api/setup-claim.ts +21 -0
  283. package/src/nexus/api/setup-codex-login.ts +160 -0
  284. package/src/nexus/api/usage-api.ts +102 -0
  285. package/src/nexus/config/secrets/file-backend.ts +56 -18
  286. package/src/nexus/index.ts +45 -4
  287. package/src/onboarding/first-run-doctor.ts +54 -0
  288. package/src/onboarding/first-run.ts +89 -0
  289. package/src/onboarding/lan-address.ts +45 -0
  290. package/src/onboarding/web-first.ts +119 -24
  291. package/src/onboarding.ts +4 -0
  292. package/src/ops-now/ops-now-refresh.ts +100 -0
  293. package/src/project/project-store.ts +115 -0
  294. package/src/release-loop/checklist.ts +76 -61
  295. package/src/release-loop/feature-store.ts +460 -0
  296. package/src/release-loop/manifest.ts +59 -5
  297. package/src/release-loop/release-schedule.ts +53 -0
  298. package/src/repl/index.ts +7 -0
  299. package/src/seat-dispatch/ceo-commands.ts +98 -0
  300. package/src/seat-dispatch/ceo-intent.ts +38 -0
  301. package/src/seat-loop/seat-loop.ts +231 -0
  302. package/src/seat-loop/seat-report.ts +64 -0
  303. package/src/self-dev/decompose.ts +66 -11
  304. package/src/self-dev/run-store.ts +10 -1
  305. package/src/self-implement/execution-clarification.ts +115 -0
  306. package/src/self-implement/gate-cli.ts +6 -3
  307. package/src/self-implement/orchestrator.ts +37 -3
  308. package/src/self-implement/pipeline-shape.ts +1 -1
  309. package/src/self-implement/run-status-mapping.ts +5 -3
  310. package/src/session/chat.ts +2 -0
  311. package/src/session/index.ts +9 -0
  312. package/src/shell-env-bootstrap.ts +17 -0
  313. package/src/skills/tools/usage-slash.ts +8 -7
  314. package/src/steward/directive.ts +51 -2
  315. package/src/steward/launch.ts +250 -0
  316. package/src/steward/outcome-notify.ts +115 -0
  317. package/src/steward/steward-cards.ts +8 -2
  318. package/src/steward/triage-plan.ts +32 -4
  319. package/src/steward/triage.ts +93 -9
  320. package/src/steward/working-backwards.ts +64 -0
  321. package/src/storyboard/storyboard.ts +141 -0
  322. package/src/task-cards/card-store.ts +2 -2
  323. package/src/task-orchestrator/surfaces/pod-lease.ts +260 -0
  324. package/src/telegram-card-followup.ts +233 -0
  325. package/src/telegram-commands.ts +16 -0
  326. package/src/telegram-report.ts +15 -9
  327. package/src/telegram-run.ts +90 -13
  328. package/src/telegram.ts +29 -8
  329. package/src/thinking-line.ts +28 -20
  330. package/src/tool-runtime/types.ts +4 -0
  331. package/src/tui/chat/execution-badge.ts +1 -1
  332. package/src/tui-client/dashboard-session.ts +2 -0
  333. package/src/user-config.ts +118 -11
  334. package/src/ux-sim/turn-states.ts +1 -1
  335. package/src/version/packed-revision.json +1 -1
  336. package/src/web-push/notify-exec-request.ts +32 -0
  337. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/4836-3084c58c1725a40b.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/approvals/page-9aa47b0bc6d570ff.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/app/intake/page-6a8158b71d1fe9d7.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/setup/page-085847b088675978.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +0 -1
  382. package/apps/pwa/out/_next/static/css/5ef72a618a8994f3.css +0 -1
  383. package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_buildManifest.js +0 -1
  384. package/src/dashboard/turn-typeahead-echo.ts +0 -23
  385. /package/apps/pwa/out/_next/static/{lywc8m_HDzH9MKSDQhyEj → 2DsiDfDCks44g1cPUMhiW}/_ssgManifest.js +0 -0
@@ -11,7 +11,7 @@ set -euo pipefail
11
11
 
12
12
  usage() {
13
13
  cat <<'EOF'
14
- Usage: bash scripts/install.sh [--prefix PATH] [--source PATH.tgz|URL] [--no-modify-path] [--no-bootstrap-bun] [--no-setup] [--yes] [--allow-downgrade] [--help]
14
+ Usage: bash scripts/install.sh [--prefix PATH] [--source PATH.tgz|URL] [--no-modify-path] [--no-bootstrap-bun] [--no-install-deps] [--no-setup] [--yes] [--allow-downgrade] [--help]
15
15
  curl -fsSL https://github.com/ElanvitalAI/elanous/releases/latest/download/install.sh | bash
16
16
 
17
17
  Install elanous without contacting a package registry.
@@ -23,6 +23,7 @@ Install elanous without contacting a package registry.
23
23
  (or set $ELANOUS_INSTALL_SOURCE; without either, fetch the verified latest release)
24
24
  --no-modify-path do not append the elanous PATH block to a shell startup file
25
25
  --no-bootstrap-bun fail instead of installing bun with its official installer when bun is missing
26
+ --no-install-deps do not install missing prerequisites with apt even when running as root
26
27
  --no-setup do not start first-time setup in this window after installing (interactive terminals only)
27
28
  --yes do not ask before updating an existing installation
28
29
  --allow-downgrade allow installing a version older than the one already installed
@@ -38,6 +39,7 @@ PREFIX="${ELANOUS_INSTALL_PREFIX:-${XDG_DATA_HOME:-${HOME:?HOME is required}/.lo
38
39
  SOURCE="${ELANOUS_INSTALL_SOURCE:-}"
39
40
  MODIFY_PATH=1
40
41
  BOOTSTRAP_BUN=1
42
+ INSTALL_DEPS=1
41
43
  RUN_SETUP=1
42
44
  ASSUME_YES=0
43
45
  ALLOW_DOWNGRADE=0
@@ -53,6 +55,7 @@ while [ "$#" -gt 0 ]; do
53
55
  SOURCE="$2"; shift 2 ;;
54
56
  --no-modify-path) MODIFY_PATH=0; shift ;;
55
57
  --no-bootstrap-bun) BOOTSTRAP_BUN=0; shift ;;
58
+ --no-install-deps) INSTALL_DEPS=0; shift ;;
56
59
  --no-setup) RUN_SETUP=0; shift ;;
57
60
  --yes|-y) ASSUME_YES=1; shift ;;
58
61
  --allow-downgrade) ALLOW_DOWNGRADE=1; shift ;;
@@ -91,8 +94,8 @@ if ! command -v bun >/dev/null 2>&1 && [ -x "${BUN_INSTALL:-$HOME/.bun}/bin/bun"
91
94
  export PATH="${BUN_INSTALL:-$HOME/.bun}/bin:$PATH"
92
95
  fi
93
96
  # Match doctor-distro.ts families when a required command is absent; do not source os-release as shell code.
94
- required_command_hint() {
95
- local package="$1" id='' id_like='' version='' key value family='unknown' like
97
+ required_command_family() {
98
+ local id='' id_like='' version='' key value family='unknown' like
96
99
  if [ "$(uname -s)" = 'Darwin' ]; then
97
100
  family='darwin'
98
101
  elif [ "$(uname -s)" = 'Linux' ] && [ -r "${ELANOUS_INSTALL_OS_RELEASE_FILE:-/etc/os-release}" ]; then
@@ -121,6 +124,12 @@ required_command_hint() {
121
124
  fi
122
125
  fi
123
126
  fi
127
+ printf '%s' "$family"
128
+ }
129
+
130
+ required_command_hint() {
131
+ local package="$1" family
132
+ family="$(required_command_family)"
124
133
  if [ "$package" = 'bun' ] && [ "$family" != 'unknown' ]; then
125
134
  echo ' curl -fsSL https://bun.sh/install | bash' >&2
126
135
  return
@@ -138,16 +147,33 @@ required_command_hint() {
138
147
  SUDO='sudo '
139
148
  [ "$(id -u 2>/dev/null)" = 0 ] && SUDO=''
140
149
 
141
- # 빠진 선행 명령을 «한 번에» 모아 한 줄로 댄다.
142
- # 🩸 09-25 베어 ubuntu:24.04 실측: unzip 으로 멈추고, 깔고 다시 돌리면 git 으로 또 멈췄다(세 판).
143
- # 패키지 매니저 권한은 사람 몫이라 대신 깔지 않는다. bun 은 여기서 세지 않는다(없으면 아래에서 공식 설치기로 깐다).
144
- MISSING=()
145
- if ! command -v bun >/dev/null 2>&1 && [ "$BOOTSTRAP_BUN" -eq 1 ]; then
146
- command -v curl >/dev/null 2>&1 || MISSING+=(curl)
147
- command -v unzip >/dev/null 2>&1 || MISSING+=(unzip) # bun 공식 설치기가 쓴다
150
+ # 빠진 선행 명령을 «한 번에» 모아 한 줄로 댄다. bun 은 여기서 세지 않는다(없으면 아래에서 공식 설치기로 깐다).
151
+ missing_prerequisites() {
152
+ MISSING=()
153
+ if ! command -v bun >/dev/null 2>&1 && [ "$BOOTSTRAP_BUN" -eq 1 ]; then
154
+ command -v curl >/dev/null 2>&1 || MISSING+=(curl)
155
+ command -v unzip >/dev/null 2>&1 || MISSING+=(unzip) # bun 공식 설치기가 쓴다
156
+ fi
157
+ command -v git >/dev/null 2>&1 || MISSING+=(git)
158
+ }
159
+ missing_prerequisites
160
+ INSTALLED_DEPS=''
161
+ DEPS_INSTALL_FAILED=0
162
+ if [ "${#MISSING[@]}" -gt 0 ] && [ "$INSTALL_DEPS" -eq 1 ] && [ "$SUDO" = '' ] &&
163
+ command -v apt-get >/dev/null 2>&1 && [ "$(required_command_family)" = debian ]; then
164
+ DEPS_TO_INSTALL=("${MISSING[@]}")
165
+ # Fresh container images ship with empty package lists — refresh once, then install without prompts.
166
+ if DEBIAN_FRONTEND=noninteractive apt-get update -qq >&2 &&
167
+ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${DEPS_TO_INSTALL[@]}" >&2; then
168
+ INSTALLED_DEPS="${DEPS_TO_INSTALL[*]}"
169
+ echo "installed: $INSTALLED_DEPS" >&2
170
+ missing_prerequisites
171
+ else
172
+ DEPS_INSTALL_FAILED=1
173
+ echo '⛔ 자동 설치 실패 — 아래를 직접 실행하라 (automatic install failed — run the command below yourself):' >&2
174
+ fi
148
175
  fi
149
- command -v git >/dev/null 2>&1 || MISSING+=(git)
150
- if [ "${#MISSING[@]}" -gt 0 ]; then
176
+ if [ "${#MISSING[@]}" -gt 0 ] || [ "$DEPS_INSTALL_FAILED" -eq 1 ]; then
151
177
  echo "⛔ required command missing: ${MISSING[*]}. Install it, then rerun this script:" >&2
152
178
  required_command_hint "${MISSING[*]}"
153
179
  exit 127
@@ -429,6 +455,7 @@ if [ "$MODIFY_PATH" -eq 1 ] && [ -n "${LOGIN_STARTUP:-}" ] && ! grep -Fqx "$MARK
429
455
  fi
430
456
 
431
457
  echo "$(t "Installed elanous $VERSION at $PREFIX/bin/elanous" "elanous $VERSION 을(를) 설치했습니다: $PREFIX/bin/elanous")"
458
+ [ -z "$INSTALLED_DEPS" ] || echo "installed: $INSTALLED_DEPS"
432
459
 
433
460
  # ── 다음 걸음 (2026-09-23 · Phase 3 「사람 손」) ─────────────────────────
434
461
  # ⛔ 설치가 끝나도 «무엇을 더 쳐야 하나»를 안 말하면, 빠뜨린 손이 나중에 «다른 원인의 얼굴»로 나타난다
@@ -79,6 +79,9 @@ export function createScenarios(): Scenario[] {
79
79
  export function createScenariosWithReplay(readReplay: typeof readTerminalReplay): Scenario[] {
80
80
  const marker = `pwa-scn-${randomBytes(8).toString('hex')}`;
81
81
  const t1bMarker = `pwa-scn-${randomBytes(8).toString('hex')}`;
82
+ const t5bFirst = `ELANOUS_T5B_${randomBytes(8).toString('hex')}`;
83
+ const t5bSecond = `ELANOUS_T5B_${randomBytes(8).toString('hex')}`;
84
+ const screenContains = (text: string): string => `(() => { const host = document.querySelector('[data-xterm-host]'); const term = host?.__elanousTerm; if (!term || !host.getClientRects().length) return false; const buffer = term.buffer.active; for (let row = buffer.viewportY; row < buffer.viewportY + term.rows; row++) if (buffer.getLine(row)?.translateToString().includes(${JSON.stringify(text)})) return true; return false; })()`;
82
85
  // C1 — the answer (n+1) never appears in the question, so finding it on the page means a model replied.
83
86
  const c1Base = 1000 + (randomBytes(2).readUInt16BE(0) % 8000);
84
87
  return [
@@ -149,6 +152,23 @@ export function createScenariosWithReplay(readReplay: typeof readTerminalReplay)
149
152
  return body.terminals.filter((row) => row.producer === 'web-registration' && row.id === ids.terminalId).length === 1;
150
153
  } }],
151
154
  },
155
+ {
156
+ id: 'T5b', surface: 'terminal', path: '/app/term/', title: 'terminal shows output on open and after reload',
157
+ steps: [
158
+ { kind: 'goto' }, { kind: 'waitFor', selector: terminal, timeoutMs: 20_000 },
159
+ { kind: 'waitFor', jsPredicate: `Array.from(document.querySelectorAll(${JSON.stringify(connected)})).some(node => node.textContent?.includes('ACP: 연결됨'))`, timeoutMs: 20_000 },
160
+ { kind: 'click', selector: terminal }, { kind: 'type', text: `echo ${t5bFirst}` }, { kind: 'press', key: 'Enter' },
161
+ { kind: 'waitFor', jsPredicate: screenContains(t5bFirst), timeoutMs: 20_000 },
162
+ { kind: 'goto' }, { kind: 'waitFor', selector: terminal, timeoutMs: 20_000 },
163
+ { kind: 'waitFor', jsPredicate: screenContains(t5bFirst), timeoutMs: 20_000 },
164
+ { kind: 'click', selector: terminal }, { kind: 'type', text: `echo ${t5bSecond}` }, { kind: 'press', key: 'Enter' },
165
+ { kind: 'waitFor', jsPredicate: screenContains(t5bSecond), timeoutMs: 20_000 },
166
+ ],
167
+ expect: [
168
+ { js: screenContains(t5bFirst), expected: 'first marker still visible after reload' },
169
+ { js: screenContains(t5bSecond), expected: 'new output visible after reload' },
170
+ ],
171
+ },
152
172
  {
153
173
  id: 'C1', surface: 'chat', path: '/app/chat/', title: 'first message streams a model reply', costly: true,
154
174
  steps: [{ kind: 'goto' }, { kind: 'waitFor', selector: chatInput, timeoutMs: 20_000 }, { kind: 'click', selector: chatInput }, { kind: 'type', text: `Reply with only the number that is one more than ${c1Base}.` }, { kind: 'press', key: 'Enter' }],
@@ -0,0 +1,146 @@
1
+ // V1 — TUI regression checks that UX measured by hand on 10-02 (U1a · U1b · MAT1c · ONB1), as data.
2
+ // Each check says what to type and how to judge the rendered screen text. The verdicts are pure
3
+ // (screen string → ok/reason) so they are unit-tested without a PTY; scripts/tui-regress.ts drives the PTY.
4
+
5
+ export type Step =
6
+ | { kind: 'type'; text: string }
7
+ | { kind: 'key'; data: string }
8
+ | { kind: 'wait'; ms: number }
9
+ | { kind: 'snap'; as: string }
10
+ | { kind: 'erase'; count: number };
11
+
12
+ export interface Verdict { ok: boolean; reason: string }
13
+
14
+ export interface TuiCheck {
15
+ id: string;
16
+ title: string;
17
+ /** `tui.role` to set in the temp config before launch (owner when omitted). */
18
+ role?: 'owner' | 'contributor' | 'general';
19
+ steps: Step[];
20
+ judge: (snaps: Record<string, string>) => Verdict;
21
+ }
22
+
23
+ const CTRL_C = '\x03';
24
+ const BACKSPACE = '\x7f';
25
+ const HINT = /한 번 더 누르면 나갑니다/;
26
+
27
+ const ok = (reason: string): Verdict => ({ ok: true, reason });
28
+ const no = (reason: string): Verdict => ({ ok: false, reason });
29
+
30
+ /** The input row: the last screen line that starts with the prompt marker. */
31
+ export function inputRow(screen: string): string | undefined {
32
+ return screen.split('\n').filter((line) => line.trimStart().startsWith('❯')).at(-1);
33
+ }
34
+
35
+ /** Palette entries visible after typing a slash prefix (lines other than the input row that show `/name`). */
36
+ export function paletteHas(screen: string, name: string): boolean {
37
+ const row = inputRow(screen);
38
+ return screen.split('\n').some((line) => line !== row && line.includes(`/${name}`));
39
+ }
40
+
41
+ export function judgeFirstScreen(screen: string): Verdict {
42
+ if (/Step 1/.test(screen)) return no('«Step 1» 마법사가 떴다');
43
+ if (!inputRow(screen)) return no('입력칸 «❯» 가 없다');
44
+ return ok('입력칸이 보이고 «Step 1» 이 없다');
45
+ }
46
+
47
+ export function judgeHintAppearsThenClears(armed: string, later: string): Verdict {
48
+ if (!HINT.test(armed)) return no('빈 입력 Ctrl+C 뒤 안내가 안 보인다');
49
+ if (HINT.test(later)) return no('2.6초 뒤에도 안내가 남아 있다');
50
+ return ok('안내가 떴다가 2초 뒤 사라졌다');
51
+ }
52
+
53
+ export function judgeCtrlCClearsText(typed: string, after: string, text: string): Verdict {
54
+ if (!(inputRow(typed) ?? '').includes(text)) return no('친 글이 입력칸에 안 보인다(준비 실패)');
55
+ if ((inputRow(after) ?? '').includes(text)) return no('Ctrl+C 뒤에도 글이 남아 있다');
56
+ if (HINT.test(after)) return no('글을 지우는 Ctrl+C 에 나가기 안내가 떴다');
57
+ return ok('글이 지워지고 안내는 없다');
58
+ }
59
+
60
+ export function judgePalette(screens: Record<string, string>, expectations: Record<string, boolean>): Verdict {
61
+ for (const [name, want] of Object.entries(expectations)) {
62
+ const screen = screens[name];
63
+ if (screen === undefined) return no(`/${name} 화면이 없다`);
64
+ if (paletteHas(screen, name) !== want) return no(`/${name} 가 팔레트에 ${want ? '없다' : '보인다'}`);
65
+ }
66
+ return ok(Object.entries(expectations).map(([n, w]) => `/${n} ${w ? '보임' : '숨김'}`).join(' · '));
67
+ }
68
+
69
+ /** U1b — the help is drawn (key section ⊕ a run of command rows) and the prompt survives below it.
70
+ * Not «the whole grid fits»: an owner sees 60+ commands, which exceed a 50-row screen and scroll (PgUp). */
71
+ export function judgeHelpShown(screen: string): Verdict {
72
+ const lines = screen.split('\n');
73
+ const commandRows = lines.filter((line) => /^\s+\/[a-z][a-z-]*/.test(line)).length;
74
+ if (!lines.some((line) => /^\s*Keys\s*$/.test(line))) return no('/help 의 Keys 절이 안 보인다');
75
+ if (commandRows < 10) return no(`명령 줄이 ${commandRows}개뿐이다`);
76
+ if (!inputRow(screen)) return no('도움말을 연 뒤 입력칸이 화면 밖으로 밀렸다');
77
+ return ok(`도움말이 그려졌다(명령 줄 ${commandRows} · Keys 절 · 입력칸 유지)`);
78
+ }
79
+
80
+ /** U1c2 — lines typed while a reply streams show up as «my» lines, in order, each above its own answer. */
81
+ export function judgeQueuedEcho(screen: string): Verdict {
82
+ const lines = screen.split('\n');
83
+ const at = (re: RegExp, after = -1) => lines.findIndex((line, i) => i > after && re.test(line));
84
+ const u2 = at(/Q2MARK 라고만/);
85
+ if (u2 < 0) return no('줄 서 있던 «Q2MARK 라고만» 이 내 말로 안 보인다');
86
+ const a2 = lines.findIndex((line, i) => i > u2 && /Q2MARK/.test(line) && !/라고만/.test(line));
87
+ const u3 = at(/Q3MARK 라고만/, u2);
88
+ if (u3 < 0) return no('«Q3MARK 라고만» 이 내 말로 안 보인다(또는 순서가 뒤집혔다)');
89
+ const a3 = lines.findIndex((line, i) => i > u3 && /Q3MARK/.test(line) && !/라고만/.test(line));
90
+ if (!(a2 > u2 && u3 > a2 && a3 > u3)) return no(`순서가 내 말2(${u2}) → 답2(${a2}) → 내 말3(${u3}) → 답3(${a3}) 가 아니다`);
91
+ return ok('내 말2 → 답2 → 내 말3 → 답3');
92
+ }
93
+
94
+ const PALETTE_STEPS = (names: string[]): Step[] => names.flatMap((name) => [
95
+ { kind: 'type', text: `/${name.slice(0, 3)}` } as Step,
96
+ { kind: 'wait', ms: 800 } as Step,
97
+ { kind: 'snap', as: name } as Step,
98
+ { kind: 'erase', count: 4 } as Step,
99
+ { kind: 'wait', ms: 300 } as Step,
100
+ ]);
101
+
102
+ export const TUI_REGRESS_CHECKS: TuiCheck[] = [
103
+ {
104
+ id: 'R1', title: '첫 화면 — 입력칸 · «Step 1» 0 (ONB1)',
105
+ steps: [{ kind: 'snap', as: 'boot' }],
106
+ judge: (s) => judgeFirstScreen(s.boot ?? ''),
107
+ },
108
+ {
109
+ id: 'R2', title: '빈 입력 Ctrl+C → 안내 → 2초 뒤 사라짐 (U1a · #22868)',
110
+ steps: [{ kind: 'key', data: CTRL_C }, { kind: 'wait', ms: 300 }, { kind: 'snap', as: 'armed' }, { kind: 'wait', ms: 2600 }, { kind: 'snap', as: 'later' }],
111
+ judge: (s) => judgeHintAppearsThenClears(s.armed ?? '', s.later ?? ''),
112
+ },
113
+ {
114
+ id: 'R3', title: '글 친 뒤 Ctrl+C → 입력 지움 · 안내 없음 (U1a)',
115
+ steps: [{ kind: 'type', text: 'regress-abc' }, { kind: 'wait', ms: 400 }, { kind: 'snap', as: 'typed' }, { kind: 'key', data: CTRL_C }, { kind: 'wait', ms: 400 }, { kind: 'snap', as: 'after' }],
116
+ judge: (s) => judgeCtrlCClearsText(s.typed ?? '', s.after ?? '', 'regress-abc'),
117
+ },
118
+ {
119
+ id: 'R4', title: 'owner 팔레트 — /directive · /help 보임 (MAT1c)',
120
+ steps: PALETTE_STEPS(['directive', 'help']),
121
+ judge: (s) => judgePalette(s, { directive: true, help: true }),
122
+ },
123
+ {
124
+ id: 'R5', title: 'general 팔레트 — /directive 숨김 · /help 보임 (MAT1c)',
125
+ role: 'general',
126
+ steps: PALETTE_STEPS(['directive', 'help']),
127
+ judge: (s) => judgePalette(s, { directive: false, help: true }),
128
+ },
129
+ {
130
+ id: 'R6', title: '/help 가 그려지고 입력칸이 남는다 (U1b · #22802)',
131
+ steps: [{ kind: 'type', text: '/help' }, { kind: 'key', data: '\r' }, { kind: 'wait', ms: 1500 }, { kind: 'snap', as: 'help' }],
132
+ judge: (s) => judgeHelpShown(s.help ?? ''),
133
+ },
134
+ {
135
+ id: 'R7', title: '답 오는 중 쳐 둔 두 줄이 내 말로 차례로 (U1c2 · #23026 · 실제 LLM 턴)',
136
+ steps: [
137
+ { kind: 'type', text: '1부터 25까지 숫자를 한 줄에 하나씩 써 줘. 도구는 쓰지 마.' }, { kind: 'key', data: '\r' }, { kind: 'wait', ms: 2500 },
138
+ { kind: 'type', text: 'Q2MARK 라고만 답해' }, { kind: 'key', data: '\r' }, { kind: 'wait', ms: 500 },
139
+ { kind: 'type', text: 'Q3MARK 라고만 답해' }, { kind: 'key', data: '\r' }, { kind: 'wait', ms: 60_000 },
140
+ { kind: 'snap', as: 'after' },
141
+ ],
142
+ judge: (s) => judgeQueuedEcho(s.after ?? ''),
143
+ },
144
+ ];
145
+
146
+ export const ERASE = BACKSPACE;
@@ -79,7 +79,7 @@ async function main(): Promise<void> {
79
79
  if (proposals.length) {
80
80
  const body = proposals.map((p, i) => `${i + 1}. ${p.label} (강도 ${p.score.toFixed(2)})\n ${p.rationale}`).join('\n\n');
81
81
  const msg = `💡 taste 제안 ${proposals.length}건 — 반복 관심에서 창발한 미션 후보입니다.\n(제안일 뿐 미션 생성 아님 · 승인/기각: \`/taste approve <테마>\` · \`/taste reject <테마>\`)\n\n${body}`;
82
- try { sendOutbound(msg, 'report'); } catch (e) { log(`taste 제안 발송 오류: ${e instanceof Error ? e.message : String(e)}`); }
82
+ try { sendOutbound(msg, 'ops-report'); } catch (e) { log(`taste 제안 발송 오류: ${e instanceof Error ? e.message : String(e)}`); }
83
83
  log(`taste 제안 표면화: ${proposals.length}건`);
84
84
  }
85
85
  }
@@ -101,7 +101,7 @@ async function main(): Promise<void> {
101
101
  const changed = (report.recaps?.promoted ?? 0) + (report.consolidate?.consolidated ?? 0)
102
102
  + (report.archive?.archived ?? 0) + (report.prunedEvents ?? 0);
103
103
  if (changed > 0) {
104
- try { sendOutbound(`🧠 기억 회고(새벽) — ${summary}`, 'report'); } catch (e) { log(`발송 오류: ${e instanceof Error ? e.message : String(e)}`); }
104
+ try { sendOutbound(`🧠 기억 회고(새벽) — ${summary}`, 'ops-report'); } catch (e) { log(`발송 오류: ${e instanceof Error ? e.message : String(e)}`); }
105
105
  }
106
106
 
107
107
  // 자율행동 회상 로깅(autopilot P0.2) — 회고 루프도 자율행동.
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env bun
2
+ import { spawn } from 'node:child_process';
3
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
4
+ import { join, dirname } from 'node:path';
5
+ import { debug, redactSecrets } from '../src/debug/log.js';
6
+ import { sendOutbound } from '../src/domains/outbound-alert.js';
7
+ import { DecisionLedger } from '../src/decisions/decision-ledger.js';
8
+ import { effectiveInstanceRoot, prodInstanceRoot, releaseLedgerRoot } from '../src/instance/resolve.js';
9
+ import { listMemories } from '../src/memory.js';
10
+ import { listSchedules, getSchedule } from '../src/release-loop/release-schedule.js';
11
+ import { listChecklist, summarizeChecklist } from '../src/release-loop/checklist.js';
12
+ import { getUserConfig } from '../src/user-config.js';
13
+ import { registerStandaloneLogSink } from '../src/domains/standalone-log-sink.js';
14
+
15
+ export type ProbeKind = 'cut' | 'green' | 'open';
16
+ export type ProbeVerdict = 'match' | 'mismatch' | 'pending' | 'unmeasured';
17
+ export interface ProbeRow {
18
+ ts: string;
19
+ question: string;
20
+ verdict: ProbeVerdict;
21
+ botValue: string | number | null;
22
+ ledgerValue: string | number | null;
23
+ opsNowUpdatedAt: string | null;
24
+ reason: string | null;
25
+ botReply: string | null;
26
+ }
27
+ export const STANDARD_QUESTIONS: ReadonlyArray<{ kind: ProbeKind; ask: (version: string) => string }> = [
28
+ { kind: 'cut', ask: (version) => `${version} 컷 언제야?` },
29
+ { kind: 'green', ask: (version) => `${version} 체크리스트 초록 몇 개야?` },
30
+ { kind: 'open', ask: () => '지금 미결 결정 몇 개야?' },
31
+ ];
32
+
33
+ export interface ProbeDeps {
34
+ schedules?: () => Promise<Array<{ version: string; cutAt: string }>> | Array<{ version: string; cutAt: string }>;
35
+ published?: (version: string) => Promise<boolean> | boolean;
36
+ cut?: (version: string) => Promise<string | null> | string | null;
37
+ green?: (version: string) => Promise<number> | number;
38
+ open?: () => Promise<number> | number;
39
+ ask?: (question: string, model?: string) => Promise<string>;
40
+ send?: typeof sendOutbound;
41
+ memoryUpdatedAt?: () => string | null;
42
+ model?: () => string | undefined;
43
+ root?: string;
44
+ now?: () => Date;
45
+ print?: (line: string) => void;
46
+ observe?: (question: string, verdict: ProbeVerdict, reason: string | null) => void;
47
+ }
48
+ export interface ProbeOptions { version?: string; dryRun?: boolean; json?: boolean }
49
+
50
+ function kstDate(date: Date): string {
51
+ return new Date(date.getTime() + 9 * 3_600_000).toISOString().slice(0, 10);
52
+ }
53
+
54
+ const CUT_ISO = /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})(?!\d)/g;
55
+ const CUT_LOCAL = /(?:(\d{4})\s*[년./-]\s*)?(\d{1,2})\s*(?:월|[./-])\s*(\d{1,2})\s*(?:일)?(?:\s*\([^)]*\))?(?:\s*(?:오전|오후|AM|PM))?\s*(\d{1,2})\s*(?:시\s*(\d{1,2})\s*분?|:\s*(\d{2}))(?!\d)\s*(?:KST|한국시간)?/gi;
56
+
57
+ function cutValue(text: string): string | null {
58
+ const iso = new RegExp(CUT_ISO.source).exec(text)?.[0];
59
+ if (iso && Number.isFinite(Date.parse(iso))) return new Date(Date.parse(iso) + 9 * 3_600_000).toISOString().slice(0, 16);
60
+ const match = new RegExp(CUT_LOCAL.source, 'i').exec(text);
61
+ if (!match) return null;
62
+ const year = match[1] ? Number(match[1]) : null;
63
+ const month = Number(match[2]), day = Number(match[3]);
64
+ let hour = Number(match[4]);
65
+ const minute = Number(match[5] ?? match[6]);
66
+ if (/오후|PM/i.test(match[0]) && hour < 12) hour += 12;
67
+ if (/오전|AM/i.test(match[0]) && hour === 12) hour = 0;
68
+ if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59) return null;
69
+ return `${year ?? '????'}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}T${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
70
+ }
71
+
72
+ function normalizedCut(value: string, reference?: string): string | null {
73
+ const parsed = cutValue(value);
74
+ if (!parsed) return null;
75
+ if (parsed.startsWith('????-') && reference) return `${reference.slice(0, 4)}${parsed.slice(4)}`;
76
+ return parsed;
77
+ }
78
+
79
+ function ledgerCutValue(value: string): string | null {
80
+ const millis = Date.parse(value);
81
+ if (!Number.isFinite(millis)) return null;
82
+ return new Date(millis + 9 * 3_600_000).toISOString().slice(0, 16);
83
+ }
84
+
85
+ type ParsedValue<T> = { value: T | null; ambiguous: boolean };
86
+ type Candidate<T> = { start: number; end: number; value: T };
87
+
88
+ function selectClaim<T>(reply: string, candidates: Candidate<T>[]): ParsedValue<T> {
89
+ if (candidates.length === 1) {
90
+ const after = reply.slice(candidates[0]!.end);
91
+ if (/^\s*(?:개|건|KST|한국시간)?\s*(?:가|이|은|는)?\s*(?:아닙니다|아니다|아니에요|아님|아닌|말고)/i.test(after)) {
92
+ return { value: null, ambiguous: true };
93
+ }
94
+ return { value: candidates[0]!.value, ambiguous: false };
95
+ }
96
+ if (candidates.length === 2) {
97
+ const between = reply.slice(candidates[0]!.end, candidates[1]!.start);
98
+ // «57개가 아니라 50개» · «3개, 아니 4개» — the later value is the asserted one.
99
+ if (/^(?:\s*(?:개|건|KST|한국시간))?\s*(?:가|이|은|는)?\s*(?:아니라|아닌|말고|[,,]?\s*아니(?:요|고)?\s*[,,]?)\s*$/i.test(between)) {
100
+ return { value: candidates[1]!.value, ambiguous: false };
101
+ }
102
+ }
103
+ return { value: null, ambiguous: candidates.length > 1 };
104
+ }
105
+
106
+ const NEGATED_AFTER = /^\s*(?:개|건)?\s*(?:가|이|은|는)?\s*(?:아닙니다|아니다|아니에요|아님|아닌|말고)/i;
107
+ // A number counts as the metric only when it carries a unit (개·건) or sits right after the label («초록은 57»).
108
+ // Anything else (a version «0.2.10판», a date) is not a claim about the count — unmeasured, never a false drift.
109
+ const DIRECT_AFTER_LABEL = /^\s*(?:은|는|이|가|도|:|=)?\s*(?:총|현재|지금|모두|전부)?\s*$/;
110
+
111
+ function countValue(reply: string, kind: 'green' | 'open'): ParsedValue<number> {
112
+ const label = kind === 'green' ? /(?:초록|녹색|green|🟢)/i : /(?:미결\s*결정|열린\s*결정|open\s*decisions?|미결)/i;
113
+ const found = label.exec(reply);
114
+ if (!found) {
115
+ const bare = /^\s*(\d[\d,]*)\s*(?:개|건)?[.。!\s]*$/.exec(reply);
116
+ if (!bare) return { value: null, ambiguous: false };
117
+ const value = Number(bare[1]!.replaceAll(',', ''));
118
+ return Number.isSafeInteger(value) && value >= 0 ? { value, ambiguous: false } : { value: null, ambiguous: false };
119
+ }
120
+ const answer = reply.slice(found.index + found[0].length);
121
+ const candidates: Candidate<number>[] = [];
122
+ for (const match of answer.matchAll(/(?<![\d.])\d[\d,]*(?![\d.])/g)) {
123
+ const value = Number(match[0].replaceAll(',', ''));
124
+ if (!Number.isSafeInteger(value) || value < 0) continue;
125
+ const end = match.index + match[0].length;
126
+ const unit = /^\s*(?:개|건)/.test(answer.slice(end));
127
+ const direct = DIRECT_AFTER_LABEL.test(answer.slice(0, match.index));
128
+ if (unit || direct) candidates.push({ start: match.index, end, value });
129
+ }
130
+ if (candidates.length) return selectClaim(answer, candidates);
131
+ // Number before the label («57개가 초록», «57개 초록이 아닙니다») — keep the negation that follows the label.
132
+ const reverse = new RegExp(`(?<![\\d.])(\\d[\\d,]*)(?![\\d.])\\s*(?:개|건)?\\s*(?:가|이|의)?\\s*${label.source}`, 'i').exec(reply);
133
+ if (!reverse) return { value: null, ambiguous: false };
134
+ const tail = reply.slice(reverse.index + reverse[0].length);
135
+ if (NEGATED_AFTER.test(tail)) return { value: null, ambiguous: true };
136
+ const value = Number(reverse[1]!.replaceAll(',', ''));
137
+ return Number.isSafeInteger(value) && value >= 0 ? { value, ambiguous: false } : { value: null, ambiguous: false };
138
+ }
139
+
140
+ function replyValue(kind: ProbeKind, reply: string, ledger: string | number): ParsedValue<string | number> {
141
+ if (kind !== 'cut') return countValue(reply, kind);
142
+ const candidates: Candidate<string>[] = [];
143
+ for (const match of reply.matchAll(CUT_ISO)) {
144
+ const value = normalizedCut(match[0], String(ledger));
145
+ if (value) candidates.push({ start: match.index, end: match.index + match[0].length, value });
146
+ }
147
+ for (const match of reply.matchAll(CUT_LOCAL)) {
148
+ if (candidates.some((candidate) => match.index < candidate.end && match.index + match[0].length > candidate.start)) continue;
149
+ let value = normalizedCut(match[0], String(ledger));
150
+ const zone = /^\s*(UTC|GMT|Z\b|[A-Z]{2,4}T\b|[+-]\d{1,2}(?::?\d{2})?\b)\s*([+-]\d{1,2}(?::?\d{2})?)?/i.exec(reply.slice(match.index + match[0].length));
151
+ if (value && zone && !/KST/i.test(match[0])) {
152
+ // A written zone other than KST: UTC/GMT converts (+9h); any other zone or offset is not guessed — unmeasured.
153
+ if (/^(?:UTC|GMT|Z)$/i.test(zone[1]!) && !zone[2] && !value.startsWith('????')) {
154
+ const millis = Date.parse(`${value}:00Z`);
155
+ value = Number.isFinite(millis) ? new Date(millis + 9 * 3_600_000).toISOString().slice(0, 16) : null;
156
+ } else value = null;
157
+ }
158
+ if (value) candidates.push({ start: match.index, end: match.index + match[0].length + (zone && !/KST/i.test(match[0]) ? zone[0].length : 0), value });
159
+ }
160
+ candidates.sort((a, b) => a.start - b.start);
161
+ const valid = candidates.filter(({ value: parsed }) => {
162
+ const millis = Date.parse(`${parsed}:00Z`);
163
+ return Number.isFinite(millis) && new Date(millis).toISOString().slice(0, 16) === parsed;
164
+ });
165
+ return selectClaim(reply, valid);
166
+ }
167
+
168
+ function askBot(question: string, model?: string): Promise<string> {
169
+ const root = effectiveInstanceRoot();
170
+ return new Promise((resolve, reject) => {
171
+ const args = ['bin/elanous.mjs', ...(root === prodInstanceRoot() ? [] : [`--test=${root}`]), 'ask', question, '--json'];
172
+ const child = spawn('bun', args, {
173
+ cwd: join(import.meta.dir, '..'),
174
+ env: { ...process.env, ...(model ? { ELANOUS_LLM_MODEL: model } : {}) },
175
+ stdio: ['ignore', 'pipe', 'ignore'],
176
+ });
177
+ let out = '';
178
+ let tooLong = false;
179
+ let timedOut = false;
180
+ const timer = setTimeout(() => { timedOut = true; child.kill(); reject(new Error('timeout')); }, 90_000);
181
+ child.stdout.on('data', (chunk: Buffer) => {
182
+ out += chunk.toString();
183
+ if (out.length > 1024 * 1024) { tooLong = true; child.kill(); }
184
+ });
185
+ child.on('error', (error) => { clearTimeout(timer); reject(error); });
186
+ child.on('close', (code) => {
187
+ clearTimeout(timer);
188
+ if (timedOut) return reject(new Error('timeout'));
189
+ if (tooLong) return reject(new Error('bot-output-too-large'));
190
+ if (code !== 0) return reject(new Error('bot-error'));
191
+ try {
192
+ const result = JSON.parse(out) as { reply?: unknown; error?: unknown };
193
+ if (typeof result.reply !== 'string') throw new Error('bot-no-reply');
194
+ resolve(result.reply);
195
+ } catch { reject(new Error('bot-no-reply')); }
196
+ });
197
+ });
198
+ }
199
+
200
+ function isPublished(version: string, releaseRoot: string): boolean {
201
+ try {
202
+ const record = JSON.parse(readFileSync(join(releaseRoot, 'release', version, 'release.json'), 'utf8')) as { publishedAt?: unknown; version?: unknown };
203
+ return record.version === version && typeof record.publishedAt === 'string';
204
+ } catch (error) {
205
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
206
+ throw error;
207
+ }
208
+ }
209
+
210
+ function ledgerOpen(): number { return new DecisionLedger().list({ status: 'open' }).length; }
211
+ function memoryUpdatedAt(): string | null { return listMemories().find((entry) => entry.name === 'ops-now')?.updatedAt ?? null; }
212
+ function probeModel(): string | undefined {
213
+ const config = getUserConfig().raw.opsNowProbe;
214
+ if (!config || typeof config !== 'object' || Array.isArray(config)) return undefined;
215
+ const model = (config as Record<string, unknown>).model;
216
+ return typeof model === 'string' && model.trim() ? model.trim() : undefined;
217
+ }
218
+
219
+ export async function runOpsNowProbe(options: ProbeOptions = {}, deps: ProbeDeps = {}): Promise<{ rows: ProbeRow[]; exitCode: number; alert: string | null }> {
220
+ const now = (deps.now ?? (() => new Date()))();
221
+ const root = deps.root ?? effectiveInstanceRoot();
222
+ const releaseRoot = releaseLedgerRoot();
223
+ let version = options.version;
224
+ if (!version) {
225
+ try {
226
+ if (!deps.schedules && !existsSync(join(releaseRoot, 'release', 'features.sqlite'))) throw new Error('ledger-unreadable');
227
+ const schedules = await (deps.schedules ?? (() => listSchedules(releaseRoot)))();
228
+ const ordered = schedules.filter((row) => Number.isFinite(Date.parse(row.cutAt)))
229
+ .sort((a, b) => Date.parse(a.cutAt) - Date.parse(b.cutAt));
230
+ for (const row of ordered) {
231
+ if (!await (deps.published ?? ((v) => isPublished(v, releaseRoot)))(row.version)) {
232
+ version = row.version;
233
+ break;
234
+ }
235
+ }
236
+ } catch { /* A missing version makes the two release questions unmeasured. */ }
237
+ }
238
+ const updatedAt = (() => { try { return (deps.memoryUpdatedAt ?? memoryUpdatedAt)(); } catch { return null; } })();
239
+ let model: string | undefined;
240
+ try { model = deps.model ? deps.model() : probeModel(); } catch { /* Use the ask path's configured model. */ }
241
+ const rows: ProbeRow[] = [];
242
+ for (const item of STANDARD_QUESTIONS) {
243
+ const question = item.ask(version ?? '(판 미상)');
244
+ let ledgerValue: string | number | null = null;
245
+ let reason: string | null = null;
246
+ try {
247
+ if (item.kind !== 'open' && !version) throw new Error('ledger-unreadable');
248
+ if (item.kind === 'green' && !deps.green && !existsSync(join(releaseRoot, 'release', 'features.sqlite')) && !existsSync(join(releaseRoot, 'release', version!, 'checklist.json'))) throw new Error('ledger-unreadable');
249
+ if (item.kind === 'open' && !deps.open && !existsSync(new DecisionLedger().path)) throw new Error('ledger-unreadable');
250
+ const value = item.kind === 'cut' ? await (deps.cut ?? ((v) => getSchedule(v, releaseRoot)?.cutAt ?? null))(version!)
251
+ : item.kind === 'green' ? await (deps.green ?? ((v) => summarizeChecklist(listChecklist(v)).green))(version!) : await (deps.open ?? ledgerOpen)();
252
+ if (item.kind === 'cut') ledgerValue = typeof value === 'string' ? ledgerCutValue(value) : null;
253
+ else ledgerValue = typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
254
+ if (ledgerValue === null) reason = 'ledger-unreadable';
255
+ } catch { reason = 'ledger-unreadable'; }
256
+ let reply: string | null = null;
257
+ let botValue: string | number | null = null;
258
+ try { reply = await (deps.ask ?? askBot)(question, model); }
259
+ catch (error) { if (!reason) reason = (error as Error).message === 'timeout' ? 'timeout' : 'bot-unresponsive'; }
260
+ let verdict: ProbeVerdict = 'unmeasured';
261
+ if (!reason) {
262
+ if (!reply?.trim()) reason = 'bot-unresponsive';
263
+ else if (/확인\s*(?:중|해\s*보겠|해\s*볼게|해\s*보도록|이\s*필요)|조회\s*중|살펴\s*보겠/.test(reply)) { verdict = 'pending'; reason = 'bot-pending'; }
264
+ else {
265
+ const parsed = replyValue(item.kind, reply, ledgerValue!);
266
+ botValue = parsed.value;
267
+ if (botValue === null) reason = parsed.ambiguous ? 'value-ambiguous' : 'value-unparseable';
268
+ else verdict = botValue === ledgerValue ? 'match' : 'mismatch';
269
+ }
270
+ }
271
+ const row: ProbeRow = { ts: now.toISOString(), question, verdict, botValue, ledgerValue, opsNowUpdatedAt: updatedAt,
272
+ reason, botReply: reply === null ? null : redactSecrets({ reply }).reply.slice(0, 200) };
273
+ rows.push(row);
274
+ debug.log('ops-now.probe', 'verdict', { question, verdict, reason });
275
+ deps.observe?.(question, verdict, reason);
276
+ }
277
+ const path = join(root, 'ops-now-probe', `${kstDate(now)}.jsonl`);
278
+ mkdirSync(dirname(path), { recursive: true });
279
+ appendFileSync(path, rows.map((row) => JSON.stringify(row)).join('\n') + '\n', { mode: 0o600 });
280
+ const mismatches = rows.filter((row) => row.verdict === 'mismatch');
281
+ const alert = mismatches.length ? `🚨 ops-now 봇 답 불일치 — ${mismatches.map((row) => `${row.question} 봇 ${row.botValue} / 원장 ${row.ledgerValue}`).join(' · ')} · ops-now updatedAt ${updatedAt ?? '미상'}` : null;
282
+ let alertDelivery: 'sent' | 'failed' | 'dry-run' | null = alert ? (options.dryRun ? 'dry-run' : 'failed') : null;
283
+ if (alert && !options.dryRun) {
284
+ try {
285
+ if (await (deps.send ?? sendOutbound)(alert, 'ops-alert')) alertDelivery = 'sent';
286
+ } catch {
287
+ // A throwing sender has the same undelivered outcome as a false return.
288
+ }
289
+ if (alertDelivery === 'failed') debug.log('ops-now.probe', 'alert-failed', { kind: 'ops-alert' });
290
+ }
291
+ const print = deps.print ?? console.log;
292
+ const warning = [
293
+ ...(rows.some((row) => row.verdict === 'unmeasured') ? ['⚠ 측정하지 못한 질문이 있습니다.'] : []),
294
+ ...(alertDelivery === 'failed' ? ['⚠ ops-alert 전송 실패'] : []),
295
+ ].join(' ') || null;
296
+ if (options.json) print(JSON.stringify({ version: version ?? null, rows, alert, alertDelivery, warning, wouldSend: Boolean(alert && options.dryRun), exitCode: mismatches.length ? 1 : 0 }));
297
+ else {
298
+ for (const row of rows) print(`${row.verdict} · ${row.question} · 봇 ${row.botValue ?? '-'} · 원장 ${row.ledgerValue ?? '-'}${row.reason ? ` · ${row.reason}` : ''}`);
299
+ if (warning) print(warning);
300
+ if (alert) {
301
+ if (alertDelivery === 'dry-run') print(`보냈을 것: ${alert}`);
302
+ else if (alertDelivery === 'sent') print(alert);
303
+ }
304
+ }
305
+ return { rows, exitCode: mismatches.length ? 1 : 0, alert };
306
+ }
307
+
308
+ export async function main(args: string[] = process.argv.slice(2)): Promise<number> {
309
+ let version: string | undefined;
310
+ let dryRun = false, json = false, once = false;
311
+ for (let i = 0; i < args.length; i++) {
312
+ const arg = args[i];
313
+ if (arg === '--once') once = true;
314
+ else if (arg === '--dry-run') dryRun = true;
315
+ else if (arg === '--json') json = true;
316
+ else if (arg === '--version' && args[i + 1] && !args[i + 1]!.startsWith('--')) version = args[++i];
317
+ else throw new Error(`알 수 없는 옵션: ${arg}`);
318
+ }
319
+ if (!once) throw new Error('--once 필요');
320
+ await registerStandaloneLogSink('ops-now-probe');
321
+ return (await runOpsNowProbe({ version, dryRun, json })).exitCode;
322
+ }
323
+
324
+ if (import.meta.main) {
325
+ try { process.exitCode = await main(); }
326
+ catch (error) { console.error(`ops-now-probe: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 2; }
327
+ }