elanous 0.2.7 → 0.2.9

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 (296) 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/bFI5EEwg7T6aG88ibX4Zq/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/8630-cdfc61e82c653ffc.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/8894-e263b332208e3957.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/9454-139040d37c79dbc9.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/approvals/page-fb7c2d458f32fcad.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-35e2713597dd24d1.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/board/page-918b656f064577ba.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-624f7a2f89b60c8f.js → page-f972fc1c00a62e88.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/bots/{page-5a5fb385a4df0ffe.js → page-5f30e00693527ebd.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/chat/{page-80497e93a878ddc5.js → page-7fbae173b3c103a7.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/consult/{page-7457f52683562161.js → page-3b4edcce364062ea.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/control/page-c5794f484c251f44.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1130d8eefcd87e76.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/editor/page-973996d584f008c1.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/exec/{page-b7dcf5d48a854b76.js → page-016e2bda04eecb1f.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/field/{page-51480757e7d68b92.js → page-b4d6d9a3b56f2940.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/intake/page-ec49a466121233ba.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/layout-f72db356744ceade.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/live/page-a6fd9b50d1b85378.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/market/page-8b0402a46a749a3d.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-07267f1391b96aba.js → page-037f83e2a1edc8e2.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/morning/{page-55d50e9b0006a91d.js → page-adfbd2ba9598e59a.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/not-found-46fedff57d4bec67.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/observatory/page-0cf318e9dcee2bbf.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-899b327cb8b6845c.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-6c1e6ba743a29dfe.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/{page-a7e37af7d0399cb8.js → page-1a8e4807292f6b17.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/reflection/page-28deba6bcb834916.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-de68e2205794037d.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-15e5a15db84c56aa.js → page-c71c33a573230672.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-a2e572e4511f2a16.js → page-0df30e0c7e50148a.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/page-4fc56790fe0d2682.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-59df2aebb38a7c1e.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/setup/page-4647be5dbcbd0795.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/share/page-e025892bda74b743.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a0955de73b89f078.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-c1c95739c0bff3b8.js → page-369846d2921af584.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/term/page-72cbc1c8cdd0f65d.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/trace/{page-71d18eec33b9c667.js → page-77b28bfb0996d5ea.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/vault/page-3220c4934f49c6c2.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-2c4e59079ee357ac.js → page-90670589b87abea8.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/workflows/page-7df9243ed021a4cc.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d422f38c052967e.js → page-4498c3853f4f688d.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-35bedde6d885f08f.js +1 -0
  51. package/apps/pwa/out/_next/static/css/f92f63a5df0ba229.css +1 -0
  52. package/apps/pwa/out/approvals/index.html +1 -1
  53. package/apps/pwa/out/approvals/index.txt +30 -29
  54. package/apps/pwa/out/autopilot/index.html +1 -1
  55. package/apps/pwa/out/autopilot/index.txt +30 -29
  56. package/apps/pwa/out/board/index.html +1 -1
  57. package/apps/pwa/out/board/index.txt +30 -29
  58. package/apps/pwa/out/botlab/index.html +1 -1
  59. package/apps/pwa/out/botlab/index.txt +27 -26
  60. package/apps/pwa/out/bots/index.html +1 -1
  61. package/apps/pwa/out/bots/index.txt +27 -26
  62. package/apps/pwa/out/chat/index.html +1 -1
  63. package/apps/pwa/out/chat/index.txt +27 -26
  64. package/apps/pwa/out/consult/index.html +1 -1
  65. package/apps/pwa/out/consult/index.txt +30 -29
  66. package/apps/pwa/out/control/index.html +1 -1
  67. package/apps/pwa/out/control/index.txt +30 -29
  68. package/apps/pwa/out/design-check/index.html +1 -1
  69. package/apps/pwa/out/design-check/index.txt +30 -29
  70. package/apps/pwa/out/editor/index.html +1 -1
  71. package/apps/pwa/out/editor/index.txt +30 -29
  72. package/apps/pwa/out/exec/index.html +1 -1
  73. package/apps/pwa/out/exec/index.txt +30 -29
  74. package/apps/pwa/out/field/index.html +1 -1
  75. package/apps/pwa/out/field/index.txt +30 -29
  76. package/apps/pwa/out/healthz +1 -1
  77. package/apps/pwa/out/index.html +1 -1
  78. package/apps/pwa/out/index.txt +27 -26
  79. package/apps/pwa/out/intake/index.html +1 -1
  80. package/apps/pwa/out/intake/index.txt +30 -29
  81. package/apps/pwa/out/live/index.html +1 -1
  82. package/apps/pwa/out/live/index.txt +30 -29
  83. package/apps/pwa/out/market/index.html +1 -1
  84. package/apps/pwa/out/market/index.txt +27 -26
  85. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  86. package/apps/pwa/out/missions/placeholder/index.txt +27 -26
  87. package/apps/pwa/out/morning/index.html +1 -1
  88. package/apps/pwa/out/morning/index.txt +30 -29
  89. package/apps/pwa/out/observatory/index.html +1 -1
  90. package/apps/pwa/out/observatory/index.txt +30 -29
  91. package/apps/pwa/out/ops/checklist/index.html +1 -0
  92. package/apps/pwa/out/ops/checklist/index.txt +30 -0
  93. package/apps/pwa/out/ops/release/index.html +1 -0
  94. package/apps/pwa/out/ops/release/index.txt +30 -0
  95. package/apps/pwa/out/reflection/index.html +1 -1
  96. package/apps/pwa/out/reflection/index.txt +28 -27
  97. package/apps/pwa/out/scheduler/index.html +1 -1
  98. package/apps/pwa/out/scheduler/index.txt +30 -29
  99. package/apps/pwa/out/sessions/index.html +1 -1
  100. package/apps/pwa/out/sessions/index.txt +27 -26
  101. package/apps/pwa/out/settings/devices/index.html +1 -1
  102. package/apps/pwa/out/settings/devices/index.txt +30 -29
  103. package/apps/pwa/out/settings/index.html +1 -1
  104. package/apps/pwa/out/settings/index.txt +30 -29
  105. package/apps/pwa/out/setup/done/index.html +1 -1
  106. package/apps/pwa/out/setup/done/index.txt +30 -29
  107. package/apps/pwa/out/setup/index.html +1 -1
  108. package/apps/pwa/out/setup/index.txt +30 -29
  109. package/apps/pwa/out/share/index.html +1 -1
  110. package/apps/pwa/out/share/index.txt +30 -29
  111. package/apps/pwa/out/showroom/index.html +1 -1
  112. package/apps/pwa/out/showroom/index.txt +27 -26
  113. package/apps/pwa/out/tasks/index.html +1 -1
  114. package/apps/pwa/out/tasks/index.txt +30 -29
  115. package/apps/pwa/out/term/index.html +1 -1
  116. package/apps/pwa/out/term/index.txt +30 -29
  117. package/apps/pwa/out/trace/index.html +1 -1
  118. package/apps/pwa/out/trace/index.txt +30 -29
  119. package/apps/pwa/out/vault/index.html +1 -1
  120. package/apps/pwa/out/vault/index.txt +30 -29
  121. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  122. package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
  123. package/apps/pwa/out/workflows/index.html +1 -1
  124. package/apps/pwa/out/workflows/index.txt +30 -29
  125. package/apps/pwa/out/workspace/index.html +1 -1
  126. package/apps/pwa/out/workspace/index.txt +30 -29
  127. package/apps/pwa/out/worktrees/index.html +1 -1
  128. package/apps/pwa/out/worktrees/index.txt +30 -29
  129. package/graphs/release/recipes.yaml +3 -0
  130. package/graphs/release/release-loop.yaml +3 -1
  131. package/graphs/test-diet/nightly-audit.yaml +18 -0
  132. package/graphs/test-diet/recipes.yaml +12 -0
  133. package/graphs/test-diet/test-diet.yaml +22 -0
  134. package/package.json +2 -1
  135. package/persona-presets/_index.yaml +10 -0
  136. package/persona-presets/_schema.json +146 -0
  137. package/persona-presets/dana.yaml +26 -0
  138. package/persona-presets/haru.yaml +27 -0
  139. package/persona-presets/kai.yaml +27 -0
  140. package/persona-presets/leo.yaml +27 -0
  141. package/persona-presets/mira.yaml +30 -0
  142. package/persona-presets/nari.yaml +27 -0
  143. package/persona-presets/sora.yaml +27 -0
  144. package/persona-presets/yuna.yaml +27 -0
  145. package/plugins/doc-draft/graphs/run-step.ts +1 -1
  146. package/plugins/geo-check/graphs/run-step.ts +34 -10
  147. package/scripts/field-feed/deliver-node.ts +8 -3
  148. package/scripts/field-feed/lib.ts +2 -0
  149. package/scripts/install.sh +20 -11
  150. package/scripts/persona-presets/lint.ts +79 -0
  151. package/scripts/release-loop/checklist-gate-node.ts +2 -2
  152. package/scripts/release-loop/cutoff.ts +20 -2
  153. package/scripts/release-loop/export-check-node.ts +85 -0
  154. package/scripts/release-loop/gate-node.ts +45 -7
  155. package/scripts/release-loop/node-verdict.ts +8 -1
  156. package/scripts/release-loop/npm-publish-node.ts +10 -4
  157. package/scripts/release-loop/ops-upgrade-node.ts +11 -3
  158. package/scripts/release-loop/publish-node.ts +23 -1
  159. package/scripts/release-loop/shard-plan.ts +17 -0
  160. package/scripts/storyboard/storyboard.ts +167 -0
  161. package/scripts/test-diet/lib.ts +89 -0
  162. package/scripts/test-diet/node.ts +144 -0
  163. package/src/acp/auth-methods.ts +30 -0
  164. package/src/acp/server.ts +5 -5
  165. package/src/auth/setup-link-tokens.fixture.ts +19 -0
  166. package/src/auth/setup-link-tokens.ts +153 -0
  167. package/src/budget/unified-usage.ts +75 -0
  168. package/src/channel-bot-token.ts +37 -10
  169. package/src/channel-import/detect.ts +191 -0
  170. package/src/channel-import/run.ts +111 -0
  171. package/src/chat/index.ts +9 -9
  172. package/src/cli/config-test-sync.ts +4 -0
  173. package/src/cli/decisions-cli.ts +13 -3
  174. package/src/cli/doctor-bundle.ts +128 -0
  175. package/src/cli/doctor-cli.ts +36 -1
  176. package/src/cli/doctor-local-llm.ts +95 -0
  177. package/src/cli/import-cli.ts +60 -0
  178. package/src/cli/mcp-login.ts +13 -4
  179. package/src/cli/op-proxy-sync.ts +83 -0
  180. package/src/cli/persona-cli.ts +116 -0
  181. package/src/cli/pr-cli.ts +6 -2
  182. package/src/cli/release-cli.ts +49 -8
  183. package/src/cli/skills-cli.ts +173 -0
  184. package/src/cli/start-cli.ts +13 -4
  185. package/src/codex/setup.ts +15 -13
  186. package/src/dashboard/dashboard-status-lines.ts +45 -0
  187. package/src/dashboard/first-screen-band.ts +35 -0
  188. package/src/dashboard/index.ts +87 -14
  189. package/src/dashboard/provider-slash-runtime.ts +28 -11
  190. package/src/dashboard/slash-runtime/dashboard-handlers.ts +12 -2
  191. package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
  192. package/src/dashboard/turn-stream-formatter.ts +1 -1
  193. package/src/decisions/decision-cards.ts +291 -0
  194. package/src/decisions/decision-ledger.ts +5 -2
  195. package/src/decisions/discord-decision-cards.ts +133 -0
  196. package/src/decisions/telegram-decision-cards.ts +100 -0
  197. package/src/directives/version-at.ts +77 -0
  198. package/src/discord.ts +17 -0
  199. package/src/domains/outbound-alert.ts +44 -1
  200. package/src/domains/repeated-failure.ts +1 -1
  201. package/src/error-report/report.ts +200 -0
  202. package/src/exec-requests/planner.ts +8 -2
  203. package/src/exec-requests/runner.ts +64 -24
  204. package/src/exec-requests/store.ts +2 -0
  205. package/src/field/field-feed-auto.ts +48 -0
  206. package/src/field/field-reel.ts +8 -1
  207. package/src/hooks/error-report.ts +210 -0
  208. package/src/hooks/receiver.ts +41 -1
  209. package/src/import/apply.ts +128 -0
  210. package/src/import/plan.ts +197 -0
  211. package/src/index.ts +98 -11
  212. package/src/input-core/text-entry.ts +13 -5
  213. package/src/intake-plane/seat-answer.ts +96 -0
  214. package/src/intake-plane/seat-doc-route.ts +17 -3
  215. package/src/maturity/feature-maturity.ts +2 -0
  216. package/src/nexus/api/consult-requests.ts +19 -8
  217. package/src/nexus/api/error-reports.ts +108 -0
  218. package/src/nexus/api/http-server.ts +41 -1
  219. package/src/nexus/api/meta-api.ts +27 -0
  220. package/src/nexus/api/operator.ts +61 -0
  221. package/src/nexus/api/ops-api.ts +139 -0
  222. package/src/nexus/api/ops-seats-sources.ts +65 -0
  223. package/src/nexus/api/ops-seats.ts +114 -0
  224. package/src/nexus/api/outbound-report.ts +7 -0
  225. package/src/nexus/api/public-routes.ts +14 -1
  226. package/src/nexus/api/running-runs-background.ts +68 -0
  227. package/src/nexus/api/running-runs-child.ts +6 -0
  228. package/src/nexus/api/setup-claim.ts +21 -0
  229. package/src/nexus/api/share-captures.ts +91 -0
  230. package/src/nexus/api/skill-problems.ts +56 -0
  231. package/src/nexus/api/terminals.ts +24 -8
  232. package/src/nexus/config/secrets/file-backend.ts +56 -18
  233. package/src/nexus/index.ts +25 -1
  234. package/src/nexus/setup-status.ts +7 -2
  235. package/src/oauth/browser-availability.ts +14 -0
  236. package/src/oauth/codex-quota-policy.ts +16 -0
  237. package/src/onboarding/continue-here.ts +13 -8
  238. package/src/onboarding/first-run-doctor.ts +54 -0
  239. package/src/onboarding/web-first.ts +8 -1
  240. package/src/onboarding.ts +66 -12
  241. package/src/persona/presets.ts +105 -0
  242. package/src/release-loop/checklist.ts +33 -69
  243. package/src/release-loop/feature-store.ts +327 -0
  244. package/src/release-loop/manifest.ts +59 -5
  245. package/src/seat-dispatch/ceo-commands.ts +65 -0
  246. package/src/self-dev/orchestrate.ts +18 -11
  247. package/src/shell-env-bootstrap.ts +44 -7
  248. package/src/skills/connect-claude-plugin.ts +142 -0
  249. package/src/skills/connect.ts +70 -0
  250. package/src/skills/index.ts +29 -1
  251. package/src/skills/repair.ts +124 -0
  252. package/src/skills/runner.ts +11 -2
  253. package/src/skills/tools/usage-slash.ts +8 -7
  254. package/src/skills/trigger-extract.ts +1 -1
  255. package/src/task-orchestrator/surfaces/pod-command-job.ts +16 -8
  256. package/src/telegram-commands.ts +29 -0
  257. package/src/telegram-run.ts +83 -13
  258. package/src/telegram.ts +34 -0
  259. package/src/thinking-line.ts +28 -20
  260. package/src/tui/chat/execution-badge.ts +1 -1
  261. package/src/user-config.ts +111 -13
  262. package/src/ux-sim/turn-states.ts +1 -1
  263. package/src/version/packed-revision.json +1 -1
  264. package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +0 -1
  265. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-10ed24f413ed66e9.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/board/page-211810b6c93ab66a.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/live/page-30533117beb53625.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/settings/page-dd107bf1c7d52a90.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/vault/page-56adbe1a1f1379bd.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +0 -1
  294. package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +0 -1
  295. package/apps/pwa/out/_next/static/keajW7DDyghEs1y8oJe6T/_buildManifest.js +0 -1
  296. /package/apps/pwa/out/_next/static/{keajW7DDyghEs1y8oJe6T → bFI5EEwg7T6aG88ibX4Zq}/_ssgManifest.js +0 -0
@@ -2609,10 +2609,16 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
2609
2609
  runTurnImpl,
2610
2610
  getBot: () => getBotRef.current,
2611
2611
  });
2612
+ // DEC-TG(대표 10-02) — decision cards in the owner's Discord DM. Filled once the bot exists (below).
2613
+ let decisionWire: import('../decisions/discord-decision-cards.js').DiscordDecisionWire | null = null;
2612
2614
  const composedOnMessage = async (
2613
2615
  ctx: import('../discord.js').DcIncoming,
2614
2616
  streamer?: import('../discord.js').DcMessageStreamer,
2615
2617
  ): Promise<string | void> => {
2618
+ if (decisionWire && ctx.text.trim() === '/decisions') {
2619
+ const { discordDecisionsCommand } = await import('../decisions/discord-decision-cards.js');
2620
+ return discordDecisionsCommand(decisionWire.service, ctx);
2621
+ }
2616
2622
  const voiceReply = await voiceWire.dispatchVoiceCommand(ctx);
2617
2623
  if (voiceReply !== null) return voiceReply;
2618
2624
  return selfOnMessage(ctx, streamer as never);
@@ -2635,7 +2641,8 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
2635
2641
  dispatch: (event) => workflowDaemon!.dispatchDiscord(event),
2636
2642
  onMessage: composedOnMessage,
2637
2643
  onInteraction: async (raw) => {
2638
- // C1 버튼 탭 우선(elanous-q: 소비) → 아니면 C3 슬래시.
2644
+ // DEC-TG 결정 카드(dec:/decmemo:) → C1 버튼 탭(elanous-q: 소비) → 아니면 C3 슬래시.
2645
+ if (decisionWire && await decisionWire.onInteraction(raw)) return;
2639
2646
  if (await questionRuntime.handleComponentInteraction(raw)) return;
2640
2647
  await slashWire.onInteraction(raw);
2641
2648
  },
@@ -2644,6 +2651,14 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
2644
2651
  if (handle) {
2645
2652
  getBotRef.current = handle.bot;
2646
2653
  workflowDiscordBot = handle;
2654
+ if ((cfg.raw?.decisions as { cards?: unknown } | undefined)?.cards !== false) {
2655
+ try {
2656
+ const { attachDiscordDecisionCards } = await import('../decisions/discord-decision-cards.js');
2657
+ decisionWire = attachDiscordDecisionCards(handle.bot, cfg);
2658
+ } catch (err) {
2659
+ console.warn(`[nexus] decision cards (discord) not attached: ${err instanceof Error ? err.message : String(err)}`);
2660
+ }
2661
+ }
2647
2662
  void slashWire.registerCommands().catch((err: unknown) => {
2648
2663
  console.warn(`[nexus] slash registration failed: ${err instanceof Error ? err.message : String(err)}`);
2649
2664
  });
@@ -2919,6 +2934,15 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
2919
2934
  : readyCount === 1 ? readyServerIds[0] : undefined,
2920
2935
  readyCount,
2921
2936
  });
2937
+ // TERM1 ④ — the first web-terminal spawn used to pay the login-shell env capture (spawnSync · ~3 s with the
2938
+ // daemon frozen) and the terminal module import (~0.9 s). Warm both off the event loop shortly after boot.
2939
+ if (!opts.startNexusHttpServerFn) {
2940
+ const warm = setTimeout(() => {
2941
+ void import('../shell-env-bootstrap.js').then(({ warmCapturedEnv }) => warmCapturedEnv()).catch(() => undefined);
2942
+ void import('../web-terminal/preview-tap-registry.js').then(() => import('../preview/terminal.js')).catch(() => undefined);
2943
+ }, 1_500);
2944
+ (warm as { unref?: () => void }).unref?.();
2945
+ }
2922
2946
  httpServer = startHttpServer({
2923
2947
  state,
2924
2948
  registry,
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
 
3
3
  import { debug } from '../debug/log.js';
4
- import { buildUserConfig, type UserConfig as MainUserConfig } from '../user-config.js';
4
+ import { buildUserConfig, resolveSkillSources, type UserConfig as MainUserConfig } from '../user-config.js';
5
5
  import { decideProviderForConfig } from '../llm.js';
6
6
  import { resolveGrokCredential } from '../grok/credential.js';
7
7
  import { loadTokens } from '../oauth/store.js';
@@ -177,7 +177,12 @@ function checkChannelBot(cfg: NexusUserConfig): SetupItem {
177
177
  }
178
178
 
179
179
  function checkSkillDirs(cfg: MainUserConfig, exists: (path: string) => boolean): SetupItem {
180
- const dirs = Array.isArray(cfg.skills.dirs) ? cfg.skills.dirs.filter(hasText) : [];
180
+ // EN5 — the same list the loader reads (resolveSkillSources): the preset and what `elanous connect` registered.
181
+ // Derived roots (shared ~/.agents/skills, Claude package roots) are optional extras, not «configured» dirs.
182
+ const dirs = resolveSkillSources(cfg)
183
+ .filter((source) => source.enabled && (source.kind === 'preset' || source.kind === 'connected'))
184
+ .map((source) => source.path)
185
+ .filter(hasText);
181
186
  const existingDirs = dirs.filter(exists);
182
187
  const missingDirs = dirs.filter((dir) => !exists(dir));
183
188
  const detail = `${dirs.length} dir${dirs.length === 1 ? '' : 's'} · ${existingDirs.length} exist`;
@@ -0,0 +1,14 @@
1
+ // OB5d — 브라우저를 열 수 없는 세션이면 «왜 못 여는지 ⊕ 대안»을 한 줄로 말한다(대표 10-01 node-c 실측: ssh 에서 아무 말 없이 기기 코드로 갔다).
2
+ // null = 이 세션에서 브라우저 로그인을 먼저 시도해도 된다.
3
+ export function browserUnavailableReason(env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform): string | null {
4
+ const ssh = !!(env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY);
5
+ if (platform === 'darwin' || platform === 'win32') {
6
+ if (!ssh) return null;
7
+ const where = platform === 'darwin' ? '이 Mac' : '이 PC';
8
+ return `원격(ssh) 접속이라 브라우저 로그인을 마칠 수 없습니다 — ${where} 화면의 터미널에서 같은 명령을 실행하면 브라우저가 바로 열립니다(붙여넣기 없이). 지금은 아래 코드로 로그인합니다.`;
9
+ }
10
+ if (env.DISPLAY || env.WAYLAND_DISPLAY) return null;
11
+ return ssh
12
+ ? '원격(ssh) 접속이고 화면이 없어 브라우저를 열 수 없습니다 — 아무 기기의 브라우저에서 아래 주소를 열고 코드를 넣으세요.'
13
+ : '화면(디스플레이)이 없는 세션이라 브라우저를 열 수 없습니다 — 아무 기기의 브라우저에서 아래 주소를 열고 코드를 넣으세요.';
14
+ }
@@ -53,3 +53,19 @@ export function codexPolicyAllowsCredits(policy: CodexQuotaPolicy): boolean {
53
53
  export function codexPolicyAllowsFallback(policy: CodexQuotaPolicy): boolean {
54
54
  return policy !== 'within-quota';
55
55
  }
56
+
57
+ /** POL1 — the one line a harness launch prints about the codex quota policy it will use, and a loud warning when a
58
+ * test universe disagrees with production (10-01: work-tree launches resolved «fallback» and leaked to grok). */
59
+ export function describeLaunchQuotaPolicy(input: {
60
+ current: ResolvedCodexQuotaPolicy;
61
+ universe: { kind: 'prod' | 'test'; root: string };
62
+ production?: ResolvedCodexQuotaPolicy;
63
+ }): { line: string; warning?: string } {
64
+ const { current, universe, production } = input;
65
+ const line = `[pod] codex 한도 정책 = ${current.policy}(${CODEX_QUOTA_POLICY_LABEL[current.policy]}) · 출처 ${current.source} · 우주 ${universe.kind} ${universe.root}`;
66
+ if (universe.kind === 'test' && production && production.policy !== current.policy) {
67
+ return { line, warning: `⚠️ 이 시험 우주의 한도 정책(${current.policy})이 운영(${production.policy})과 다르다 — 운영과 같게 하려면: bun bin/elanous.mjs config sync-test` };
68
+ }
69
+ return { line };
70
+ }
71
+
@@ -5,25 +5,30 @@ export interface ContinueHereOptions {
5
5
  /** Already-issued short-lived phone connection link; never pass its token separately. */
6
6
  phoneLink?: string;
7
7
  telegramEnabled: boolean;
8
+ /** Tailscale signed in. `host` only when MagicDNS gives a name that serves HTTPS; otherwise the command alone. */
9
+ tailscale?: { host?: string };
8
10
  /** Non-interactive output must not include links (or QR payloads). */
9
11
  interactive: boolean;
10
12
  }
11
13
 
12
14
  /** Render post-setup directions without inspecting the daemon, issuing credentials, or performing IO. */
13
15
  export function continueHereLines(opts: ContinueHereOptions): string[] {
14
- const lines = [' Continue here:'];
15
- if (!opts.daemonRunning) lines.push(' Start the daemon: `elanous nexus run`');
16
+ const lines = [' 여기서 이어가세요:'];
17
+ if (!opts.daemonRunning) lines.push(' 데몬 켜기: `elanous nexus run`');
16
18
  if (opts.interactive && opts.daemonRunning && opts.pwaUrl) {
17
- lines.push(` Browser: ${opts.pwaUrl}`);
19
+ lines.push(` 브라우저: ${opts.pwaUrl}`);
18
20
  } else {
19
- lines.push(' Browser: `elanous nexus show`');
21
+ lines.push(' 브라우저: `elanous nexus show`');
20
22
  }
21
23
  if (opts.interactive && opts.daemonRunning && opts.phoneLink) {
22
- lines.push(` Phone: ${opts.phoneLink}`);
23
- lines.push(' ⚠ This link contains a connection token — do not share it or include it in a public screenshot.');
24
+ lines.push(` 폰: ${opts.phoneLink}`);
25
+ lines.push(' ⚠ 이 링크에는 연결 토큰이 들어 있습니다 — 남에게 보내거나 공개 캡처에 넣지 마세요.');
24
26
  } else {
25
- lines.push(' Phone: `elanous phone link --temp --ttl 24h`');
27
+ lines.push(' 폰: `elanous phone link --temp --ttl 24h`');
28
+ }
29
+ if (opts.telegramEnabled) lines.push(' 텔레그램: `elanous telegram`');
30
+ if (opts.tailscale) {
31
+ lines.push(` 어디서든(Tailscale): \`elanous nexus pwa share enable\`${opts.interactive && opts.tailscale.host ? ` → https://${opts.tailscale.host}/app/` : ''}`);
26
32
  }
27
- if (opts.telegramEnabled) lines.push(' Telegram: `elanous telegram`');
28
33
  return lines;
29
34
  }
@@ -0,0 +1,54 @@
1
+ import { delimiter, join, win32 } from 'node:path';
2
+ import { applyDoctorFixes, planDoctorFixes, type DoctorFixDeps, type DoctorFixItem } from '../cli/doctor-fix.js';
3
+ import { staticToolBinDir } from '../cli/doctor-static-tools.js';
4
+ import { debug } from '../debug/log.js';
5
+
6
+ const AUTOMATIC = new Set<DoctorFixItem['id']>(['private-files', 'key-cache-permissions', 'static-tools']);
7
+ const DEFERRED = new Set<DoctorFixItem['id']>(['install-path', 'bun-tmpdir']);
8
+ const LATER_LINE = '나중에: elanous doctor --fix (셸 시작 파일 수정)';
9
+
10
+ export interface FirstRunDoctorResult {
11
+ fixed: string[];
12
+ skipped: string[];
13
+ failed: string[];
14
+ later: string[];
15
+ lines: string[];
16
+ }
17
+
18
+ /** 첫 실행에서는 동의가 필요 없는 수리만 실행하고, 셸 시작 파일은 손대지 않는다. */
19
+ export function runFirstRunDoctor(deps: DoctorFixDeps): FirstRunDoctorResult {
20
+ const result: FirstRunDoctorResult = { fixed: [], skipped: [], failed: [], later: [], lines: [] };
21
+ const record = (id: string, outcome: 'fixed' | 'skipped' | 'failed', reason?: string) => {
22
+ result[outcome].push(id);
23
+ if (outcome === 'failed') result.lines.push(`doctor: ${id} 수리 실패 — ${reason || '원인 불명'}`);
24
+ try { debug.log('doctor.first-run', outcome, { id }); } catch { /* 관측 실패가 첫 실행을 막지 않는다. */ }
25
+ };
26
+
27
+ try {
28
+ const windows = deps.readiness?.platform === 'win32';
29
+ const prefix = deps.readiness?.installPrefix;
30
+ const bin = typeof prefix === 'string' && prefix.trim()
31
+ ? windows ? win32.join(prefix.trim(), 'bin') : join(prefix.trim(), 'bin')
32
+ : staticToolBinDir(deps.env ?? process.env, deps.home);
33
+ const separator = windows ? ';' : delimiter;
34
+ const entries = (process.env.PATH ?? '').split(separator);
35
+ if (!entries.some((entry) => windows ? entry.toLowerCase() === bin.toLowerCase() : entry === bin)) {
36
+ process.env.PATH = [bin, process.env.PATH].filter(Boolean).join(separator);
37
+ }
38
+
39
+ const plan = planDoctorFixes(deps);
40
+ for (const item of plan.items) {
41
+ if (DEFERRED.has(item.id) && item.status === 'fixable') result.later.push(item.id);
42
+ }
43
+ const applied = applyDoctorFixes(deps, true, AUTOMATIC);
44
+ for (const item of applied.items) {
45
+ if (AUTOMATIC.has(item.id) && item.result === 'failed') record(item.id, 'failed', item.reason);
46
+ else if (AUTOMATIC.has(item.id) && item.result === 'fixed') record(item.id, 'fixed');
47
+ else record(item.id, 'skipped');
48
+ }
49
+ } catch {
50
+ record('doctor', 'failed', '계획 또는 적용 중 예외');
51
+ }
52
+ if (result.later.length) result.lines.push(LATER_LINE);
53
+ return result;
54
+ }
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process';
2
2
  import { isHeadlessEnv } from '../acp/codex-auth.js';
3
3
  import { runNexusShow, type NexusShowResult } from '../cli/nexus-show.js';
4
4
  import { debug } from '../debug/log.js';
5
+ import { browserUnavailableReason } from '../oauth/browser-availability.js';
5
6
  import type { UserConfig } from '../user-config.js';
6
7
 
7
8
  export interface WebFirstSetupDeps {
@@ -13,6 +14,8 @@ export interface WebFirstSetupDeps {
13
14
  renderQr?: (link: string) => string | undefined;
14
15
  openBrowser?: (link: string) => boolean;
15
16
  isHeadless?: () => boolean;
17
+ browserEnv?: NodeJS.ProcessEnv;
18
+ browserPlatform?: NodeJS.Platform;
16
19
  print?: (line: string) => void;
17
20
  }
18
21
 
@@ -70,7 +73,11 @@ export async function runWebFirstSetup(deps: WebFirstSetupDeps): Promise<'link-s
70
73
  }
71
74
  print('터미널에서 하려면: `elanous setup --terminal`');
72
75
  let browserOpened = false;
73
- if (!(deps.isHeadless ?? (() => isHeadlessEnv(process.env)))()) {
76
+ const why = browserUnavailableReason(deps.browserEnv ?? process.env, deps.browserPlatform ?? process.platform);
77
+ if (why) {
78
+ print(why);
79
+ debug.log('browser.open', 'skipped', { reason: why.includes('ssh') ? 'ssh' : 'no-display' });
80
+ } else if (!(deps.isHeadless ?? (() => isHeadlessEnv(deps.browserEnv ?? process.env)))()) {
74
81
  try {
75
82
  browserOpened = (deps.openBrowser ?? ((url: string) => {
76
83
  const command = process.platform === 'darwin' ? 'open' : process.platform === 'linux' || process.platform === 'freebsd' || process.platform === 'openbsd' ? 'xdg-open' : undefined;
package/src/onboarding.ts CHANGED
@@ -34,7 +34,7 @@ import { stdin as input, stdout as output } from 'node:process';
34
34
  import {
35
35
  type UserConfig, type LLMProviderName, type SkillSetName,
36
36
  saveUserConfig, markOnboardingComplete, buildUserConfig,
37
- skillSetDir, SKILL_SET_NAMES, userConfigPath, urlRoutingDefaults, devRequestRoutingDefaults,
37
+ skillSetDir, SKILL_SET_NAMES, userConfigPath, urlRoutingDefaults, devRequestRoutingDefaults, defaultSkillDirs, resolveSkillSources,
38
38
  } from './user-config.js';
39
39
  import {
40
40
  GROK_MODEL, OPENAI_MODEL, ANTHROPIC_MODEL, LOCAL_LLM_MODEL, GEMINI_MODEL,
@@ -45,6 +45,7 @@ import { suggestSetupModel } from './model-tier/index.js';
45
45
  import { loginWithCodex, CODEX_DEVICE_LOGIN_URL } from './oauth/codex.js';
46
46
  import { loginWithCodexBrowser } from './oauth/codex-browser-login.js';
47
47
  import { loadTokens } from './oauth/store.js';
48
+ import { browserUnavailableReason } from './oauth/browser-availability.js';
48
49
  import { TelegramBot } from './telegram.js';
49
50
  import { DiscordBot } from './discord.js';
50
51
  import { pickCodexAuthMode, pickCodexModel } from './codex/setup.js';
@@ -61,6 +62,7 @@ import { stepTransition } from './onboarding/transition.js';
61
62
  import { unattendedSetupHint } from './onboarding/entry-hints.js';
62
63
  import { continueHereLines } from './onboarding/continue-here.js';
63
64
  import type { NexusShowResult } from './cli/nexus-show.js';
65
+ import type { TailscaleProbe } from './nexus/onboarding/tailscale-probe.js';
64
66
  import type { IssuedTempToken } from './auth/temp-tokens.js';
65
67
  import {
66
68
  askValidated,
@@ -385,7 +387,11 @@ function assistCodexDeviceLogin(
385
387
  const platform = deps.authPlatform ?? process.platform;
386
388
  const execute = deps.runAuthCommand ?? runAuthCommand;
387
389
  const local = !(deps.isHeadless ?? isHeadlessEnv)(env);
388
- if (local) {
390
+ const unavailable = browserUnavailableReason(env, platform);
391
+ if (unavailable) {
392
+ io.print(` ${unavailable}`);
393
+ debug.log('browser.open', 'skipped', { reason: unavailable.includes('ssh') ? 'ssh' : 'no-display' });
394
+ } else if (local) {
389
395
  try {
390
396
  if (platform === 'darwin') execute('open', [url]);
391
397
  else if (platform === 'linux' || platform === 'freebsd' || platform === 'openbsd') execute('xdg-open', [url]);
@@ -582,7 +588,12 @@ async function askLLM(
582
588
  if (mode === 'oauth') {
583
589
  const env = grokDeps.codexEnv ?? process.env;
584
590
  let signedIn = false;
585
- if (!(grokDeps.isHeadless ?? isHeadlessEnv)(env)) {
591
+ const why = browserUnavailableReason(env, grokDeps.authPlatform ?? process.platform);
592
+ if (why) {
593
+ io.print(` ${why}`);
594
+ debug.log('browser.open', 'skipped', { reason: why.includes('ssh') ? 'ssh' : 'no-display' });
595
+ }
596
+ if (!why && !(grokDeps.isHeadless ?? isHeadlessEnv)(env)) {
586
597
  // Full-screen IO retains print() messages across redraws. Keep this
587
598
  // waiting hint transient so it cannot reappear on the device-code screen.
588
599
  if (io.showStep) output.write(' 브라우저에서 로그인을 마치세요 — 끝나면 자동으로 이어집니다.\n');
@@ -600,23 +611,23 @@ async function askLLM(
600
611
  } catch { /* device code remains available */ }
601
612
  }
602
613
  }
603
- if (signedIn) io.print(' → using your ChatGPT/Codex login.');
614
+ if (signedIn) io.print(' → ChatGPT/Codex 로그인을 씁니다.');
604
615
  else io.print(' 브라우저 로그인이 안 됐습니다 — 코드로 이어갑니다');
605
616
  }
606
617
  if (!signedIn) {
607
- io.print(' Launching device-code flow…');
618
+ io.print(' 코드로 로그인합니다…');
608
619
  try {
609
620
  await (grokDeps.loginWithCodex ?? loginWithCodex)({
610
621
  onProgress: (p) => {
611
622
  if (p.type === 'user_code') {
612
623
  io.print('');
613
- io.print(` 1) Open in any browser: ${p.loginUrl}`);
614
- io.print(` 2) Enter code: ${p.userCode}`);
624
+ io.print(` 1) 아무 브라우저에서 열기: ${p.loginUrl}`);
625
+ io.print(` 2) 코드 입력: ${p.userCode}`);
615
626
  io.print('');
616
627
  assistCodexDeviceLogin(io, p.loginUrl ?? CODEX_DEVICE_LOGIN_URL, p.userCode ?? '', grokDeps);
617
- io.print(' Waiting for sign-in…');
628
+ io.print(' 로그인을 기다리는 중…');
618
629
  }
619
- if (p.type === 'saved') io.print(' Signed in. Tokens saved.');
630
+ if (p.type === 'saved') io.print(' 로그인했습니다. 저장했습니다.');
620
631
  },
621
632
  });
622
633
  } catch (err: any) {
@@ -1316,6 +1327,7 @@ export interface RunWizardOpts {
1316
1327
  /** Post-save connections; injection keeps wizard tests off the real daemon/token store. */
1317
1328
  continueHereDeps?: {
1318
1329
  showNexus?: () => Promise<Pick<NexusShowResult, 'urls' | 'status' | 'instance'>>;
1330
+ probeTailscale?: () => Promise<TailscaleProbe>;
1319
1331
  issueToken?: () => Pick<IssuedTempToken, 'token'>;
1320
1332
  renderQr?: (link: string) => string | undefined;
1321
1333
  };
@@ -1531,9 +1543,28 @@ export async function runOnboarding(opts: RunWizardOpts = {}): Promise<UserConfi
1531
1543
  io.print('');
1532
1544
  io.print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1533
1545
  io.print(` ${m.setupComplete}`);
1546
+ // ER1 — first-run consent notice (default on · 대표 10-01): what is sent and how to turn it off.
1547
+ io.print(' 오류가 나면 운영팀에 자동으로 알립니다(오류 내용·버전·무작위 설치 ID — 이름·메일 없이). 끄기: elanous config set errorReports.enabled false');
1534
1548
  io.print(` Provider : ${marked.llm.provider}${marked.llm.model ? ` (${marked.llm.model})` : ''}`);
1535
- io.print(` Skills : ${marked.skills.activeSet} — ${marked.skills.dirs.length} dir(s)`);
1536
- for (const d of marked.skills.dirs) io.print(` ${d}`);
1549
+ // EN5 — show what the loader will actually read (resolveSkillSources), not the raw preset fields.
1550
+ const skillSources = resolveSkillSources(marked).filter((source) => source.enabled);
1551
+ io.print(` Skills : ${marked.skills.activeSet} — ${skillSources.length} dir(s)`);
1552
+ for (const source of skillSources) io.print(` ${source.path}${source.kind === 'preset' ? '' : ` (${source.id})`}`);
1553
+ // EN9 — skills the person already has in another agent: one line, one command (registers in place, no copy).
1554
+ try {
1555
+ const { CONNECT_AGENTS, planConnect } = await import('./skills/connect.js');
1556
+ const active = defaultSkillDirs(marked);
1557
+ for (const agent of CONNECT_AGENTS) {
1558
+ const plan = planConnect(agent, active);
1559
+ if (plan.state === 'new') io.print(` 쓰던 스킬 ${plan.skills.length}개 찾음(${agent}) — 가져오려면: elanous connect ${agent}`);
1560
+ }
1561
+ } catch { /* advisory */ }
1562
+ // EN13 — a bot the person already runs elsewhere: one line, one command (values never shown).
1563
+ try {
1564
+ const { channelImportHint } = await import('./channel-import/run.js');
1565
+ const hint = channelImportHint();
1566
+ if (hint) io.print(` ${hint}`);
1567
+ } catch { /* advisory */ }
1537
1568
  io.print(` Obsidian : ${marked.obsidian.vault}`);
1538
1569
  io.print(` Telegram : ${marked.telegram.enabled ? 'enabled' : 'disabled'}`);
1539
1570
  io.print(` Discord : ${marked.discord.enabled ? 'enabled' : 'disabled'}`);
@@ -1571,7 +1602,30 @@ export async function runOnboarding(opts: RunWizardOpts = {}): Promise<UserConfi
1571
1602
  }
1572
1603
  } catch { /* No token/link on failure; show the manual command instead. */ }
1573
1604
  }
1574
- const directions = continueHereLines({ daemonRunning, pwaUrl, phoneLink, telegramEnabled: marked.telegram.enabled, interactive });
1605
+ let tailscale: { host?: string } | undefined;
1606
+ let installed = false;
1607
+ let alive = false;
1608
+ let probeTimer: ReturnType<typeof setTimeout> | undefined;
1609
+ try {
1610
+ const probe = opts.continueHereDeps?.probeTailscale ?? (async () => {
1611
+ const { probeTailscale } = await import('./nexus/onboarding/tailscale-probe.js');
1612
+ return probeTailscale({ timeoutMs: 2000 });
1613
+ });
1614
+ const status = await Promise.race([
1615
+ probe(),
1616
+ new Promise<undefined>(resolve => { probeTimer = setTimeout(() => resolve(undefined), 2000); }),
1617
+ ]);
1618
+ if (status) {
1619
+ installed = status.installed;
1620
+ alive = status.alive;
1621
+ // A bare hostname has no HTTPS certificate — offer the command without an address.
1622
+ if (installed && alive) tailscale = status.magicDnsHost ? { host: status.magicDnsHost } : {};
1623
+ }
1624
+ } catch { /* Tailscale discovery is advisory. */ }
1625
+ finally { if (probeTimer) clearTimeout(probeTimer); }
1626
+ try { debug.log('onboarding.tailscale', 'probed', { installed, alive, offered: !!tailscale }); }
1627
+ catch { /* Logging must not interrupt setup. */ }
1628
+ const directions = continueHereLines({ daemonRunning, pwaUrl, phoneLink, telegramEnabled: marked.telegram.enabled, interactive, tailscale });
1575
1629
  if (io.nonInteractive) {
1576
1630
  for (const line of directions) process.stdout.write(`${line}\n`);
1577
1631
  } else {
@@ -0,0 +1,105 @@
1
+ // PS1 — 페르소나 프리셋(MK 문면 · persona-presets/<id>.yaml)을 «내 페르소나»로 만든다.
2
+ // 용어: «페르소나»(대표 10-01) — 사용자가 고르고 꾸미는 AI 동료. ⚠️ `SessionSurfaceId`(이름만 Surface 인 캐릭터 축) ·
3
+ // `modelTier.persona`(모델 등급 축)와는 «다른 것»이다 — 여기서 만드는 것은 기존 페르소나 저장소(`resolveStatePersonaDir()`,
4
+ // Discord·PWA 가 이미 읽는 `<id>.yaml`)의 한 파일이다. 프리셋 원본은 고치지 않는다.
5
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { parse, stringify } from 'yaml';
8
+
9
+ export interface PersonaPreset {
10
+ personaId: string;
11
+ displayName: string;
12
+ names: string[];
13
+ role: string;
14
+ title?: string | null;
15
+ oneLine: string;
16
+ voice: { rule: string; examples: string[] };
17
+ tasks: Array<{ when: string; what: string }>;
18
+ tools: Array<{ name: string; from: string; optional?: boolean }>;
19
+ firstQuestions: string[];
20
+ askBefore?: string[];
21
+ doesNot: string[];
22
+ forWhom: string;
23
+ }
24
+
25
+ export interface PresetCommon { loop_start?: string; always_ask?: string[]; memory?: string; title?: string }
26
+
27
+ /** `persona-presets/` shipped with this package (src/persona → ../../persona-presets). */
28
+ export function presetsDir(): string {
29
+ return join(import.meta.dir, '..', '..', 'persona-presets');
30
+ }
31
+
32
+ export function loadPresetIndex(dir = presetsDir()): { order: string[]; common: PresetCommon } {
33
+ const index = parse(readFileSync(join(dir, '_index.yaml'), 'utf8')) as { presets?: string[]; common?: PresetCommon };
34
+ return { order: index.presets ?? [], common: index.common ?? {} };
35
+ }
36
+
37
+ /** Presets in the index order; files not in the index come last (sorted). */
38
+ export function loadPresets(dir = presetsDir()): PersonaPreset[] {
39
+ if (!existsSync(dir)) return [];
40
+ const { order } = loadPresetIndex(dir);
41
+ const files = readdirSync(dir).filter((f) => f.endsWith('.yaml') && !f.startsWith('_')).map((f) => f.slice(0, -5));
42
+ const ids = [...order.filter((id) => files.includes(id)), ...files.filter((id) => !order.includes(id)).sort()];
43
+ return ids.map((id) => parse(readFileSync(join(dir, `${id}.yaml`), 'utf8')) as PersonaPreset);
44
+ }
45
+
46
+ /** Find by id or by any of its names (case-insensitive). */
47
+ export function findPreset(presets: readonly PersonaPreset[], query: string): PersonaPreset | undefined {
48
+ const q = query.trim().toLowerCase();
49
+ return presets.find((p) => p.personaId === q) ?? presets.find((p) => p.names.some((n) => n.toLowerCase() === q) || p.role === query.trim());
50
+ }
51
+
52
+ /** The persona's working instructions, assembled from the preset (and the shared rules). */
53
+ export function presetSystemPrompt(p: PersonaPreset, common: PresetCommon, name: string): string {
54
+ const lines = [
55
+ `너는 «${name}», ${p.role}다. ${p.oneLine}`,
56
+ '',
57
+ `말투: ${p.voice.rule}`,
58
+ ...p.voice.examples.map((e) => ` 예) ${e}`),
59
+ '',
60
+ '맡은 일:',
61
+ ...p.tasks.map((t) => `- ${t.when}: ${t.what}`),
62
+ '',
63
+ `처음 만나면 묻는다: ${p.firstQuestions.join(' / ')}`,
64
+ `먼저 확인을 받는다: ${[...(p.askBefore ?? []), ...(common.always_ask ?? [])].join(' · ')}`,
65
+ '하지 않는다:',
66
+ ...p.doesNot.map((d) => `- ${d}`),
67
+ ];
68
+ if (common.loop_start) lines.push('', `시작 방식: ${common.loop_start}`);
69
+ return lines.join('\n');
70
+ }
71
+
72
+ /** ASCII id for the profile file: from --name when it is latin, else the preset id; `-2`, `-3`… when taken. */
73
+ export function choosePersonaId(preset: PersonaPreset, name: string | undefined, taken: ReadonlySet<string>): string {
74
+ const fromName = name?.toLowerCase().normalize('NFKD').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
75
+ const base = fromName && /^[a-z0-9]/.test(fromName) ? fromName.slice(0, 32) : preset.personaId;
76
+ if (!taken.has(base)) return base;
77
+ for (let i = 2; ; i++) if (!taken.has(`${base}-${i}`)) return `${base}-${i}`;
78
+ }
79
+
80
+ /** The profile YAML written to the persona store (the existing loader reads it; `preset` records where it came from). */
81
+ export function presetToProfileYaml(p: PersonaPreset, common: PresetCommon, opts: { personaId: string; name?: string; title?: string }): string {
82
+ const name = opts.name?.trim() || p.names[0] || p.displayName;
83
+ const title = opts.title ?? p.title ?? undefined;
84
+ const profile = {
85
+ personaId: opts.personaId,
86
+ displayName: opts.name?.trim() ? `${opts.name.trim()} · ${p.role}` : p.displayName,
87
+ description: p.oneLine,
88
+ systemPrompt: presetSystemPrompt(p, common, name),
89
+ preset: { id: p.personaId, role: p.role, ...(title ? { title } : {}), tools: p.tools.map((t) => t.name), firstQuestions: p.firstQuestions },
90
+ };
91
+ return `# 프리셋 «${p.personaId}» 에서 만든 페르소나 — elanous persona add (원본: persona-presets/${p.personaId}.yaml)\n${stringify(profile, { lineWidth: 0 })}`;
92
+ }
93
+
94
+ /** Short block for `persona show` and `persona list`. */
95
+ export function describePreset(p: PersonaPreset): string[] {
96
+ return [
97
+ `${p.displayName} — ${p.role}${p.title ? ` (${p.title})` : ''}`,
98
+ ` ${p.oneLine}`,
99
+ ` 누구에게: ${p.forWhom}`,
100
+ ` 맡는 일: ${p.tasks.map((t) => `${t.when} ${t.what}`).join(' / ')}`,
101
+ ` 쓰는 도구: ${p.tools.map((t) => (t.optional ? `${t.name}(선택 · 따로 설치)` : t.name)).join(', ')}`,
102
+ ` 처음 묻는 것: ${p.firstQuestions.join(' / ')}`,
103
+ ` 하지 않는 것: ${p.doesNot.join(' / ')}`,
104
+ ];
105
+ }