elanous 0.2.9 → 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 (361) 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-f972fc1c00a62e88.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-f8116559c7424da7.js +1 -0
  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-54e1f966ab60a4dc.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/editor/{page-973996d584f008c1.js → page-7ed95f296f5ee8e9.js} +1 -1
  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-8b0402a46a749a3d.js → page-b31610deda76337b.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-037f83e2a1edc8e2.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-0df30e0c7e50148a.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-4fddc90b23feb215.js +1 -0
  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-90670589b87abea8.js → page-26102fd7350f171e.js} +1 -1
  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-35bedde6d885f08f.js → page-139ba5c367bbf60a.js} +1 -1
  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 +14 -14
  64. package/apps/pwa/out/autopilot/index.html +1 -1
  65. package/apps/pwa/out/autopilot/index.txt +14 -14
  66. package/apps/pwa/out/board/index.html +1 -1
  67. package/apps/pwa/out/board/index.txt +14 -14
  68. package/apps/pwa/out/botlab/index.html +1 -1
  69. package/apps/pwa/out/botlab/index.txt +14 -14
  70. package/apps/pwa/out/bots/index.html +1 -1
  71. package/apps/pwa/out/bots/index.txt +14 -14
  72. package/apps/pwa/out/chat/index.html +1 -1
  73. package/apps/pwa/out/chat/index.txt +14 -14
  74. package/apps/pwa/out/consult/index.html +1 -1
  75. package/apps/pwa/out/consult/index.txt +14 -14
  76. package/apps/pwa/out/control/index.html +1 -1
  77. package/apps/pwa/out/control/index.txt +14 -14
  78. package/apps/pwa/out/design-check/index.html +1 -1
  79. package/apps/pwa/out/design-check/index.txt +14 -14
  80. package/apps/pwa/out/editor/index.html +1 -1
  81. package/apps/pwa/out/editor/index.txt +14 -14
  82. package/apps/pwa/out/exec/index.html +1 -1
  83. package/apps/pwa/out/exec/index.txt +14 -14
  84. package/apps/pwa/out/field/index.html +1 -1
  85. package/apps/pwa/out/field/index.txt +14 -14
  86. package/apps/pwa/out/healthz +1 -1
  87. package/apps/pwa/out/index.html +1 -1
  88. package/apps/pwa/out/index.txt +14 -14
  89. package/apps/pwa/out/intake/index.html +1 -1
  90. package/apps/pwa/out/intake/index.txt +14 -14
  91. package/apps/pwa/out/live/index.html +1 -1
  92. package/apps/pwa/out/live/index.txt +14 -14
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +14 -14
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +14 -14
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +14 -14
  101. package/apps/pwa/out/ops/checklist/index.html +1 -1
  102. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  103. package/apps/pwa/out/ops/release/index.html +1 -1
  104. package/apps/pwa/out/ops/release/index.txt +14 -14
  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 +15 -15
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +14 -14
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +14 -14
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +14 -14
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +14 -14
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +14 -14
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +14 -14
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +14 -14
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +14 -14
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +14 -14
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +14 -14
  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 +14 -14
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +14 -14
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +14 -14
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +14 -14
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +14 -14
  143. package/graphs/field/field-feed.yaml +2 -0
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/package.json +1 -1
  146. package/plugins/card-followup/graphs/run-step.ts +48 -13
  147. package/plugins/doc-draft/graphs/doc-draft.yaml +3 -3
  148. package/plugins/doc-draft/graphs/run-step.ts +13 -6
  149. package/scripts/ci-pwa-build-gate.ts +160 -0
  150. package/scripts/coord-channel-watch.sh +42 -8
  151. package/scripts/coord-digest.ts +199 -0
  152. package/scripts/coord-post.sh +60 -3
  153. package/scripts/install.ps1 +68 -9
  154. package/scripts/install.sh +39 -12
  155. package/scripts/lib/pwa-scenarios/scenarios.ts +20 -0
  156. package/scripts/lib/tui-regress-checks.ts +146 -0
  157. package/scripts/memory-lifecycle-cycle.ts +2 -2
  158. package/scripts/ops-now-probe.ts +327 -0
  159. package/scripts/publish-internal-dist.ts +11 -1
  160. package/scripts/release-loop/cutoff.ts +19 -8
  161. package/scripts/release-loop/ops-upgrade-node.ts +72 -16
  162. package/scripts/release-loop/release-readiness.ts +9 -0
  163. package/scripts/release-loop/version-node.ts +58 -10
  164. package/scripts/repo-watch-cycle.ts +1 -1
  165. package/scripts/retro-cycle.ts +2 -2
  166. package/scripts/schedule-health-report.ts +1 -1
  167. package/scripts/storyboard/storyboard.ts +5 -161
  168. package/scripts/test-diet/lib.ts +30 -1
  169. package/scripts/test-diet/node.ts +82 -7
  170. package/scripts/test-timeout-audit.ts +114 -0
  171. package/scripts/tree-sync-alert.ts +1 -1
  172. package/scripts/tui-regress.ts +71 -0
  173. package/scripts/waitlist-daily-digest.ts +1 -1
  174. package/skills/explainer-video/SKILL.md +4 -0
  175. package/skills/explainer-video/engine/reel.mjs +18 -13
  176. package/skills/explainer-video/engine/reel.sh +24 -5
  177. package/skills/explainer-video/engine/stitch.mjs +145 -0
  178. package/skills/pitch-deck/SKILL.md +36 -0
  179. package/skills/pitch-deck/template.html +80 -0
  180. package/src/acp/codex-approval-adapter.ts +9 -3
  181. package/src/acp/core-turn-bridge.ts +117 -18
  182. package/src/acp/fact-question.ts +11 -0
  183. package/src/acp/server.ts +48 -7
  184. package/src/acp/tool-approval.ts +126 -0
  185. package/src/acp/turn-runner.ts +36 -0
  186. package/src/agent/monad-agent-turn.ts +8 -0
  187. package/src/ask-user-question/pending-questions.ts +25 -10
  188. package/src/ask-user-question/types.ts +9 -0
  189. package/src/auth/setup-link-tokens.ts +16 -5
  190. package/src/autopilot/build/integrity-gate.ts +12 -0
  191. package/src/boot/daemon-prompt-turn.ts +3 -0
  192. package/src/boot/daemon-tools/index.ts +8 -0
  193. package/src/boot/daemon-tools/types.ts +2 -0
  194. package/src/boot/daemon-tools/web-search.ts +1 -0
  195. package/src/chat/ctrl-c-in-input.ts +14 -0
  196. package/src/chat/index.ts +52 -0
  197. package/src/chat/pickers/state.ts +6 -2
  198. package/src/cli/agent-cli.ts +7 -0
  199. package/src/cli/decisions-cli.ts +25 -3
  200. package/src/cli/doctor-bundle-upload.ts +68 -0
  201. package/src/cli/doctor-cli.ts +38 -4
  202. package/src/cli/ops-cli.ts +20 -0
  203. package/src/cli/pod-cli.ts +49 -0
  204. package/src/cli/pr-cli.ts +31 -2
  205. package/src/cli/pwa-stop.ts +163 -13
  206. package/src/cli/pwa-test.ts +3 -0
  207. package/src/cli/release-cli.ts +63 -20
  208. package/src/cli/self-update.ts +14 -0
  209. package/src/cli/storyboard-cli.ts +37 -0
  210. package/src/cli/test-state-dir-flag.ts +11 -4
  211. package/src/connectors/linear.ts +146 -0
  212. package/src/context-bus/coord-events.ts +115 -0
  213. package/src/dashboard/dashboard-boot-fixture.ts +23 -0
  214. package/src/dashboard/index.ts +58 -21
  215. package/src/dashboard/slash-runtime/dashboard-handlers.ts +7 -0
  216. package/src/dashboard/slash-runtime/help-from-registry.ts +43 -28
  217. package/src/decisions/decision-cards.ts +49 -1
  218. package/src/decisions/decision-ledger.ts +75 -4
  219. package/src/decisions/decision-linear-projection.ts +157 -0
  220. package/src/decisions/web-push-decision-cards.ts +50 -0
  221. package/src/directive/interpretation-card.ts +36 -0
  222. package/src/discord.ts +2 -1
  223. package/src/domains/coo-admin-tool.ts +62 -0
  224. package/src/domains/core-tools.ts +11 -3
  225. package/src/domains/elanous-card.ts +29 -0
  226. package/src/domains/ops-facts-tool.ts +76 -0
  227. package/src/domains/outbound-alert.ts +41 -13
  228. package/src/domains/release-tool.ts +168 -0
  229. package/src/domains/telegram-kind-route.ts +19 -1
  230. package/src/exec-requests/planner.ts +98 -28
  231. package/src/exec-requests/runner.ts +107 -10
  232. package/src/exec-requests/store.ts +8 -2
  233. package/src/field/field-media.ts +10 -7
  234. package/src/field/field-reel.ts +14 -4
  235. package/src/git-fs/worktree.ts +45 -6
  236. package/src/graph-runner/graph-approval-card.ts +113 -0
  237. package/src/graph-runner/graph-cli.ts +129 -4
  238. package/src/graph-runner/graph-notify.ts +25 -3
  239. package/src/graph-runner/runner.ts +142 -16
  240. package/src/harness/browser-verify.ts +25 -2
  241. package/src/harness/harness-cli-command.ts +231 -35
  242. package/src/harness/harness-incidents-writer.fixture.ts +24 -0
  243. package/src/harness/harness-incidents.ts +129 -0
  244. package/src/harness/harness-stop.ts +53 -3
  245. package/src/hitl/clarification-policy.ts +3 -1
  246. package/src/index.ts +156 -37
  247. package/src/intake-plane/author-goals.ts +37 -4
  248. package/src/intake-plane/check.ts +3 -0
  249. package/src/intake-plane/discord-seat-work.ts +47 -2
  250. package/src/intake-plane/seat-doc-route.ts +170 -6
  251. package/src/intake-plane/telegram-seat-work.ts +44 -2
  252. package/src/intake-plane/telegram-work.ts +1 -0
  253. package/src/maturity/cli-maturity.ts +34 -0
  254. package/src/maturity/feature-maturity.ts +98 -1
  255. package/src/maturity/tui-slash-maturity.ts +31 -0
  256. package/src/memory.ts +32 -4
  257. package/src/nexus/api/exec-requests.ts +33 -3
  258. package/src/nexus/api/http-server.ts +53 -1
  259. package/src/nexus/api/meta-api.ts +11 -0
  260. package/src/nexus/api/ops-api.ts +10 -1
  261. package/src/nexus/api/ops-seats-sources.ts +37 -7
  262. package/src/nexus/api/ops-seats.ts +16 -6
  263. package/src/nexus/api/seat-requests.ts +88 -8
  264. package/src/nexus/api/sessions-store.ts +63 -0
  265. package/src/nexus/api/setup-codex-login.ts +160 -0
  266. package/src/nexus/api/usage-api.ts +102 -0
  267. package/src/nexus/index.ts +45 -4
  268. package/src/onboarding/first-run.ts +89 -0
  269. package/src/onboarding/lan-address.ts +45 -0
  270. package/src/onboarding/web-first.ts +119 -24
  271. package/src/onboarding.ts +4 -0
  272. package/src/ops-now/ops-now-refresh.ts +100 -0
  273. package/src/project/project-store.ts +115 -0
  274. package/src/release-loop/checklist.ts +78 -4
  275. package/src/release-loop/feature-store.ts +154 -21
  276. package/src/release-loop/release-schedule.ts +53 -0
  277. package/src/repl/index.ts +7 -0
  278. package/src/seat-dispatch/ceo-commands.ts +41 -8
  279. package/src/seat-dispatch/ceo-intent.ts +38 -0
  280. package/src/seat-loop/seat-loop.ts +231 -0
  281. package/src/seat-loop/seat-report.ts +64 -0
  282. package/src/self-dev/decompose.ts +66 -11
  283. package/src/self-dev/run-store.ts +10 -1
  284. package/src/self-implement/execution-clarification.ts +115 -0
  285. package/src/self-implement/gate-cli.ts +6 -3
  286. package/src/self-implement/orchestrator.ts +37 -3
  287. package/src/self-implement/pipeline-shape.ts +1 -1
  288. package/src/self-implement/run-status-mapping.ts +5 -3
  289. package/src/session/chat.ts +2 -0
  290. package/src/session/index.ts +9 -0
  291. package/src/shell-env-bootstrap.ts +17 -0
  292. package/src/steward/directive.ts +51 -2
  293. package/src/steward/launch.ts +250 -0
  294. package/src/steward/outcome-notify.ts +115 -0
  295. package/src/steward/steward-cards.ts +8 -2
  296. package/src/steward/triage-plan.ts +32 -4
  297. package/src/steward/triage.ts +93 -9
  298. package/src/steward/working-backwards.ts +64 -0
  299. package/src/storyboard/storyboard.ts +141 -0
  300. package/src/task-cards/card-store.ts +2 -2
  301. package/src/task-orchestrator/surfaces/pod-lease.ts +260 -0
  302. package/src/telegram-card-followup.ts +233 -0
  303. package/src/telegram-report.ts +15 -9
  304. package/src/telegram-run.ts +7 -0
  305. package/src/telegram.ts +29 -8
  306. package/src/tool-runtime/types.ts +4 -0
  307. package/src/tui-client/dashboard-session.ts +2 -0
  308. package/src/user-config.ts +118 -11
  309. package/src/version/packed-revision.json +1 -1
  310. package/src/web-push/notify-exec-request.ts +32 -0
  311. package/apps/pwa/out/_next/static/bFI5EEwg7T6aG88ibX4Zq/_buildManifest.js +0 -1
  312. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  313. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  314. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/8630-cdfc61e82c653ffc.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/8894-e263b332208e3957.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/9454-139040d37c79dbc9.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/app/approvals/page-fb7c2d458f32fcad.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-35e2713597dd24d1.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/board/page-918b656f064577ba.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/bots/page-5f30e00693527ebd.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/chat/page-7fbae173b3c103a7.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/consult/page-3b4edcce364062ea.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/control/page-c5794f484c251f44.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1130d8eefcd87e76.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/exec/page-016e2bda04eecb1f.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/field/page-b4d6d9a3b56f2940.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/intake/page-ec49a466121233ba.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/layout-f72db356744ceade.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/live/page-a6fd9b50d1b85378.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/morning/page-adfbd2ba9598e59a.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/not-found-46fedff57d4bec67.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/observatory/page-0cf318e9dcee2bbf.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-899b327cb8b6845c.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-6c1e6ba743a29dfe.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/page-1a8e4807292f6b17.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/reflection/page-28deba6bcb834916.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-de68e2205794037d.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/sessions/page-c71c33a573230672.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/settings/page-4fc56790fe0d2682.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-59df2aebb38a7c1e.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/app/setup/page-4647be5dbcbd0795.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/share/page-e025892bda74b743.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a0955de73b89f078.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/tasks/page-369846d2921af584.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/term/page-72cbc1c8cdd0f65d.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/trace/page-77b28bfb0996d5ea.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/vault/page-3220c4934f49c6c2.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/workflows/page-7df9243ed021a4cc.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/workspace/page-4498c3853f4f688d.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +0 -1
  359. package/apps/pwa/out/_next/static/css/f92f63a5df0ba229.css +0 -1
  360. package/src/dashboard/turn-typeahead-echo.ts +0 -23
  361. /package/apps/pwa/out/_next/static/{bFI5EEwg7T6aG88ibX4Zq → 2DsiDfDCks44g1cPUMhiW}/_ssgManifest.js +0 -0
@@ -1,62 +1,150 @@
1
1
  import { spawnSync } from 'node:child_process';
2
+ import { hostname, networkInterfaces, userInfo } from 'node:os';
3
+ import { findNexusLifecycleState } from '../nexus/supervisor/lock.js';
4
+ import { isLanReachableBind, isPrivateIpv4, pickLanAddress } from './lan-address.js';
2
5
  import { isHeadlessEnv } from '../acp/codex-auth.js';
3
- import { runNexusShow, type NexusShowResult } from '../cli/nexus-show.js';
6
+ import { runBgLaunch, type BgLaunchResult } from '../cli/bg-launch.js';
7
+ import { joinRestHealthUrl, runNexusShow, type NexusShowResult } from '../cli/nexus-show.js';
8
+ import { issueSetupLinkToken } from '../auth/setup-link-tokens.js';
4
9
  import { debug } from '../debug/log.js';
5
10
  import { browserUnavailableReason } from '../oauth/browser-availability.js';
6
11
  import type { UserConfig } from '../user-config.js';
7
12
 
8
13
  export interface WebFirstSetupDeps {
9
14
  config: Pick<UserConfig, 'onboarding'>;
15
+ /** Whether a person is at a terminal (stdin TTY). Without one the web path only uses a daemon that is already up. */
10
16
  isTty?: boolean;
11
17
  terminal?: boolean;
18
+ startedAt?: number;
12
19
  showNexus?: () => Promise<NexusShowResult>;
20
+ startDaemon?: () => Promise<BgLaunchResult>;
21
+ probeHealth?: (url: string, timeoutMs: number) => Promise<boolean>;
13
22
  issueSetupLinkToken?: () => Promise<string | { token: string }>;
14
23
  renderQr?: (link: string) => string | undefined;
15
24
  openBrowser?: (link: string) => boolean;
16
25
  isHeadless?: () => boolean;
17
26
  browserEnv?: NodeJS.ProcessEnv;
18
27
  browserPlatform?: NodeJS.Platform;
28
+ httpHost?: () => string | undefined;
29
+ interfaces?: typeof networkInterfaces;
30
+ defaultRouteInterface?: () => string | undefined;
31
+ hostname?: typeof hostname;
32
+ username?: () => string;
19
33
  print?: (line: string) => void;
20
34
  }
21
35
 
36
+ const STARTUP_TIMEOUT_MS = 12_000;
37
+
38
+ async function probeHealth(url: string, timeoutMs: number): Promise<boolean> {
39
+ try {
40
+ const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
41
+ return response.ok;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
22
47
  function setupUrl(pwa: string, token: string): string {
23
48
  return `${pwa.replace(/\/app\/?$/, '').replace(/\/$/, '')}/setup#t=${encodeURIComponent(token)}`;
24
49
  }
25
50
 
26
51
  export async function runWebFirstSetup(deps: WebFirstSetupDeps): Promise<'link-shown' | 'fallback'> {
27
- const observe = (event: 'link-shown' | 'fallback', reason?: string, qr = false, browserOpened = false, remote: 'loopback' | 'tailnet' = 'loopback') => {
28
- try { debug.log('onboarding.web-first', event, { ...(reason ? { reason } : {}), qr, browserOpened, remote }); }
52
+ const startedAt = deps.startedAt ?? Date.now();
53
+ const print = deps.print ?? console.log;
54
+ const observe = (event: 'link-shown' | 'fallback', reason?: string, qr = false, browserOpened = false, remote: 'loopback' | 'tailnet' = 'loopback', ms?: number, lan?: 'shown' | 'loopback-bind' | 'no-address', ssh?: boolean) => {
55
+ try { debug.log('onboarding.web-first', event, { ...(reason ? { reason } : {}), qr, browserOpened, remote, ...(ms !== undefined ? { ms } : {}), ...(lan ? { lan, ssh } : {}) }); }
29
56
  catch { /* Observability must not block onboarding. */ }
30
57
  };
31
- if (deps.config.onboarding.webFirst !== true) { observe('fallback', 'disabled'); return 'fallback'; }
32
- if ((deps.isTty ?? (process.stdin.isTTY === true)) !== true) { observe('fallback', 'non-tty'); return 'fallback'; }
58
+ const fallback = (reason: string): 'fallback' => {
59
+ print(`웹 셋업을 사용할 수 없습니다 (${reason}). 터미널 설정으로 전환합니다.`);
60
+ observe('fallback', reason);
61
+ return 'fallback';
62
+ };
63
+ if (deps.config.onboarding.webFirst === false) { observe('fallback', 'disabled'); return 'fallback'; }
33
64
  if (deps.terminal) { observe('fallback', 'terminal'); return 'fallback'; }
34
65
 
35
- let shown: NexusShowResult;
36
- try {
37
- shown = await (deps.showNexus ?? (() => runNexusShow({ format: 'json', out: { log: () => {}, error: () => {} } })))();
38
- } catch { observe('fallback', 'daemon-unavailable'); return 'fallback'; }
39
- const running = shown.status === 'unregistered' || (shown.status === 'registered' && shown.instance?.alive === true);
40
- if (!running || !shown.urls?.pwa.loopback) { observe('fallback', 'daemon-unavailable'); return 'fallback'; }
66
+ const show = deps.showNexus ?? (() => runNexusShow({ format: 'json', out: { log: () => {}, error: () => {} } }));
67
+ let shown: NexusShowResult | undefined;
68
+ try { shown = await show(); } catch { /* A missing daemon may make inspection fail. */ }
69
+ const running = shown?.status === 'unregistered' || (shown?.status === 'registered' && shown.instance?.alive === true);
70
+ // A script, pipe or CI run never starts a daemon on its own: without a terminal the web path
71
+ // only uses a daemon that already answers health once. (install.sh runs first setup with
72
+ // stdin on /dev/tty, so the install → web path still starts one.)
73
+ if ((deps.isTty ?? (process.stdin.isTTY === true)) !== true) {
74
+ const rest = running ? shown?.urls?.rest.loopback : undefined;
75
+ const up = rest && shown?.urls?.pwa.loopback
76
+ ? await (deps.probeHealth ?? probeHealth)(joinRestHealthUrl(rest), 500) : false;
77
+ if (!up) { observe('fallback', 'non-tty'); return 'fallback'; }
78
+ }
79
+ const deadline = Date.now() + STARTUP_TIMEOUT_MS;
80
+ if (!running || !shown?.urls?.pwa.loopback) {
81
+ let launch: BgLaunchResult;
82
+ try {
83
+ launch = await (deps.startDaemon ?? (() => runBgLaunch({ out: { log: () => {}, error: () => {} } })))();
84
+ } catch { return fallback('daemon-start-failed'); }
85
+ if (launch.exitCode !== 0) return fallback('daemon-start-failed');
86
+ }
87
+
88
+ let healthy = false;
89
+ while (Date.now() < deadline) {
90
+ try {
91
+ shown = await show();
92
+ const live = shown.status === 'unregistered' || (shown.status === 'registered' && shown.instance?.alive === true);
93
+ if (live && shown.urls?.pwa.loopback && shown.urls.rest.loopback) {
94
+ const remaining = deadline - Date.now();
95
+ if (remaining > 0 && await (deps.probeHealth ?? probeHealth)(joinRestHealthUrl(shown.urls.rest.loopback), Math.min(remaining, 500))) {
96
+ healthy = true;
97
+ break;
98
+ }
99
+ }
100
+ } catch { /* A daemon may not have published its URLs yet. */ }
101
+ const remaining = deadline - Date.now();
102
+ if (remaining > 0) await Bun.sleep(Math.min(200, remaining));
103
+ }
104
+ if (!healthy) return fallback(running ? 'daemon-health-unavailable' : 'daemon-health-timeout');
105
+
106
+ if (!shown?.urls?.pwa.loopback) return fallback('daemon-unavailable');
41
107
 
42
108
  let token: string;
43
109
  try {
44
- const issue = deps.issueSetupLinkToken ?? (async () => {
45
- const modulePath = '../auth/setup-link-token.js';
46
- const mod = await import(modulePath) as { issueSetupLinkToken?: (options: Record<string, never>) => Promise<string | { token: string }> };
47
- if (typeof mod.issueSetupLinkToken !== 'function') throw new Error('issuer-missing');
48
- return mod.issueSetupLinkToken({});
49
- });
50
- const issued = await issue();
110
+ const issued = await (deps.issueSetupLinkToken ?? (() => Promise.resolve(issueSetupLinkToken({}))))();
51
111
  token = typeof issued === 'string' ? issued : issued?.token;
52
112
  if (!token) throw new Error('issuer-missing');
53
- } catch { observe('fallback', 'issuer-missing'); return 'fallback'; }
113
+ } catch { return fallback('issuer-missing'); }
54
114
 
55
- const print = deps.print ?? console.log;
56
115
  const link = setupUrl(shown.urls.pwa.loopback, token);
57
- const phoneLink = shown.urls.pwa.tailnet ? setupUrl(shown.urls.pwa.tailnet, token) : undefined;
116
+ const localUrl = new URL(shown.urls.pwa.loopback);
117
+ const port = localUrl.port || (localUrl.protocol === 'https:' ? '443' : '80');
118
+ let httpHost: string | undefined;
119
+ try { httpHost = (deps.httpHost ?? (() => findNexusLifecycleState()?.runtime?.httpHost))(); }
120
+ catch { /* Unknown bind must not produce a LAN link. */ }
121
+ const confirmedBind = typeof httpHost === 'string';
122
+ const reachable = confirmedBind && isLanReachableBind(httpHost);
123
+ let lanAddress: string | null = null;
124
+ if (reachable && httpHost !== undefined) {
125
+ if (isPrivateIpv4(httpHost.trim())) {
126
+ lanAddress = httpHost.trim();
127
+ } else {
128
+ try {
129
+ lanAddress = pickLanAddress({
130
+ interfaces: (deps.interfaces ?? networkInterfaces)(),
131
+ defaultRouteInterface: deps.defaultRouteInterface,
132
+ });
133
+ } catch { /* Optional LAN discovery must not block the local setup link. */ }
134
+ }
135
+ }
136
+ const lan: 'shown' | 'loopback-bind' | 'no-address' = !confirmedBind ? 'no-address' : !reachable ? 'loopback-bind' : lanAddress ? 'shown' : 'no-address';
137
+ const lanLink = lanAddress ? setupUrl(`http://${lanAddress}:${port}/app/`, token) : undefined;
138
+ const phoneLink = shown.urls.pwa.tailnet ? setupUrl(shown.urls.pwa.tailnet, token) : lanLink;
58
139
  print('브라우저에서 셋업을 이어가세요');
59
140
  print(link);
141
+ if (lanLink) {
142
+ print(`같은 와이파이의 폰이면: ${lanLink}`);
143
+ print('이 링크는 10분 안에 한 번만 열 수 있습니다');
144
+ } else if (lan === 'loopback-bind') {
145
+ print('같은 와이파이에서 열려면 `elanous nexus run` 을 루프백 없이 다시 띄우세요');
146
+ }
147
+ const linkShownMs = Date.now() - startedAt;
60
148
  let qr = false;
61
149
  if (phoneLink) {
62
150
  try {
@@ -66,14 +154,16 @@ export async function runWebFirstSetup(deps: WebFirstSetupDeps): Promise<'link-s
66
154
  }))(phoneLink);
67
155
  if (rendered) {
68
156
  print(rendered);
69
- print('같은 와이파이·tailnet 의 폰 카메라로 스캔하세요');
157
+ print(shown.urls.pwa.tailnet ? '같은 와이파이·tailnet 의 폰 카메라로 스캔하세요' : '같은 와이파이의 폰 카메라로 스캔하세요');
70
158
  qr = true;
71
159
  }
72
160
  } catch { /* QR is optional. */ }
73
161
  }
74
162
  print('터미널에서 하려면: `elanous setup --terminal`');
75
163
  let browserOpened = false;
76
- const why = browserUnavailableReason(deps.browserEnv ?? process.env, deps.browserPlatform ?? process.platform);
164
+ const env = deps.browserEnv ?? process.env;
165
+ const ssh = !!(env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY);
166
+ const why = browserUnavailableReason(env, deps.browserPlatform ?? process.platform);
77
167
  if (why) {
78
168
  print(why);
79
169
  debug.log('browser.open', 'skipped', { reason: why.includes('ssh') ? 'ssh' : 'no-display' });
@@ -85,6 +175,11 @@ export async function runWebFirstSetup(deps: WebFirstSetupDeps): Promise<'link-s
85
175
  }))(link);
86
176
  } catch { /* The displayed link remains usable. */ }
87
177
  }
88
- observe('link-shown', undefined, qr, browserOpened, phoneLink ? 'tailnet' : 'loopback');
178
+ if (ssh) {
179
+ try {
180
+ print(`ssh -L ${port}:localhost:${port} ${(deps.username ?? (() => userInfo().username))()}@${(deps.hostname ?? hostname)()}`);
181
+ } catch { /* Optional SSH guidance must not block the setup link. */ }
182
+ }
183
+ observe('link-shown', undefined, qr, browserOpened, shown.urls.pwa.tailnet ? 'tailnet' : 'loopback', linkShownMs, lan, ssh);
89
184
  return 'link-shown';
90
185
  }
package/src/onboarding.ts CHANGED
@@ -1656,6 +1656,10 @@ export function needsOnboarding(cfg: UserConfig): boolean {
1656
1656
  return !cfg.onboarding.completed;
1657
1657
  }
1658
1658
 
1659
+ export function needsFirstRun(cfg: UserConfig): boolean {
1660
+ return !cfg.onboarding.completed && cfg.onboarding.ready !== true;
1661
+ }
1662
+
1659
1663
  /** γ (2026-04-28) · per-step wizard. Runs ONE of the 5 steps,
1660
1664
  * preserving every other field. Used by:
1661
1665
  *
@@ -0,0 +1,100 @@
1
+ import { debug } from '../debug/log.js';
2
+ import { listMemories, makeMemoryJudge, memoryRoot, saveMemory } from '../memory.js';
3
+
4
+ export interface OpsNowComment { body: string; createdAt: string; url: string }
5
+ export interface OpsNowDeps {
6
+ listComments?: (sinceIso: string) => Promise<OpsNowComment[] | null>;
7
+ summarize?: (prompt: string) => Promise<string>;
8
+ now?: () => Date;
9
+ root?: string;
10
+ dryRun?: boolean;
11
+ }
12
+ export interface OpsNowResult {
13
+ outcome: 'updated' | 'unchanged' | 'no-notices' | 'summarize-failed' | 'channel-unreadable';
14
+ notices: number;
15
+ memoryId?: string;
16
+ /** Only populated for dry-run, so callers can inspect the proposed write. */
17
+ body?: string;
18
+ }
19
+
20
+ const HEADER = /^갱신: [^\n]* · 출처 글 \d+개\n/;
21
+ const CITED_ITEM = /^- .+ \(출처: (https?:\/\/[^\s)]+)\)$/;
22
+
23
+ function citedItems(body: string): string[] | null {
24
+ const lines = body.trim().split('\n').filter((line) => line.trim());
25
+ if (!lines.length) return null;
26
+ const sources: string[] = [];
27
+ for (const line of lines) {
28
+ const match = CITED_ITEM.exec(line);
29
+ if (!match || line.match(/\(출처:/g)?.length !== 1) return null;
30
+ sources.push(match[1]!);
31
+ }
32
+ return sources;
33
+ }
34
+
35
+ function channelIssue(url: string): string | null {
36
+ try {
37
+ const parsed = new URL(url);
38
+ return parsed.hostname === 'github.com' && /^\/[^/]+\/[^/]+\/(?:issues|pull)\/\d+$/.test(parsed.pathname) &&
39
+ /^#issuecomment-\d+$/.test(parsed.hash) ? `${parsed.origin}${parsed.pathname}` : null;
40
+ } catch { return null; }
41
+ }
42
+
43
+ export async function refreshOpsNow(deps: OpsNowDeps = {}): Promise<OpsNowResult> {
44
+ const now = (deps.now ?? (() => new Date()))();
45
+ const root = deps.root ?? memoryRoot();
46
+ const finish = (outcome: OpsNowResult['outcome'], notices: number, chars: number, memoryId?: string, body?: string): OpsNowResult => {
47
+ debug.log('ops-now.refresh', outcome, { notices, chars });
48
+ return { outcome, notices, ...(memoryId ? { memoryId } : {}), ...(body !== undefined ? { body } : {}) };
49
+ };
50
+ let comments: OpsNowComment[] | null;
51
+ try {
52
+ const listComments = deps.listComments ?? (await import('../nexus/api/ops-seats-sources.js')).channelCommentsSince;
53
+ comments = await listComments(new Date(now.getTime() - 86_400_000).toISOString());
54
+ } catch {
55
+ comments = null;
56
+ }
57
+ if (!comments) return finish('channel-unreadable', 0, 0);
58
+ const notices = comments.filter((c) => c.body.includes('📌안내') &&
59
+ Number.isFinite(Date.parse(c.createdAt)) && Date.parse(c.createdAt) >= now.getTime() - 86_400_000 && Date.parse(c.createdAt) <= now.getTime())
60
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
61
+ if (!notices.length) return finish('no-notices', 0, 0);
62
+
63
+ const existing = listMemories({}, root).find((m) => m.name === 'ops-now');
64
+ const oldBody = existing?.body.trim() ?? '';
65
+ const issue = channelIssue(notices[0]!.url);
66
+ const oldSources = (issue ? oldBody.replace(HEADER, '').split('\n').map((line) => CITED_ITEM.exec(line)?.[1]) : [])
67
+ .filter((url): url is string => !!url && channelIssue(url) === issue);
68
+ const allowedSources = new Set([...notices.map((c) => c.url), ...oldSources]);
69
+ const prompt = [
70
+ '너는 봇의 ops-now 고정 기억을 diff 갱신한다. 기존 본문과 채널 📌안내 글을 대조해 지금 유효한 안내만 한국어로 반환한다.',
71
+ '새 글의 정정이 앞 글과 충돌하면 새 글이 이긴다. 이미 유효한 기존 안내는 유지하되 낡거나 정정된 안내는 제거한다.',
72
+ '각 항목은 한 줄의 "- 안내 (출처: <코멘트 URL>)" 꼴로 쓰고 다른 문장은 출력하지 않는다. 출처는 아래 새 글 URL 또는 기존 본문의 검증 가능한 채널 URL만 사용한다. 출처가 확인되지 않는 항목은 쓰지 않는다.',
73
+ '비밀 값·토큰·개인 메모·계정 이름은 옮기지 않는다.',
74
+ '본문만 출력한다. 갱신 시각 헤더는 출력하지 않는다. 2,000자 이하.',
75
+ '## 기존 ops-now 본문', oldBody || '(없음)', '## 📌안내 (오래된 글 → 새로운 글)',
76
+ ...notices.map((c) => {
77
+ const seat = /^\s*\*\*\[(OP|MK|TC|UX)\]\*/.exec(c.body)?.[1] ?? '미상';
78
+ return `시각: ${c.createdAt} · 자리: ${seat} · URL: ${c.url}\n${c.body.slice(0, 1500)}`;
79
+ }),
80
+ ].join('\n\n');
81
+ let summary: string;
82
+ try {
83
+ summary = (await (deps.summarize ?? makeMemoryJudge())(prompt)).trim().replace(HEADER, '').trim();
84
+ const sources = citedItems(summary);
85
+ if (!sources || sources.some((url) => !allowedSources.has(url)) ||
86
+ `갱신: ${now.toISOString()} · 출처 글 ${notices.length}개\n${summary}`.length > 2000) throw new Error('invalid summary');
87
+ } catch {
88
+ return finish('summarize-failed', notices.length, 0, existing?.id);
89
+ }
90
+ if (summary === oldBody.replace(HEADER, '').trim() && existing?.pinned && existing.staleAfterMinutes === 120) {
91
+ return finish('unchanged', notices.length, summary.length, existing.id, deps.dryRun ? existing.body.trimStart() : undefined);
92
+ }
93
+ const body = `갱신: ${now.toISOString()} · 출처 글 ${notices.length}개\n${summary}`;
94
+ if (deps.dryRun) return finish('updated', notices.length, body.length, existing?.id, body);
95
+ const saved = saveMemory({
96
+ type: 'project', name: 'ops-now', description: existing?.description ?? '지금 운영 상태 — 채널 📌안내 갱신',
97
+ body, id: existing?.id, priority: existing?.priority, pinned: true, staleAfterMinutes: 120,
98
+ }, root);
99
+ return finish('updated', notices.length, body.length, saved.id);
100
+ }
@@ -0,0 +1,115 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { linkSync, mkdirSync, readFileSync, readdirSync, realpathSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
4
+ import { parse, stringify } from 'yaml';
5
+ import { debug } from '../debug/log.js';
6
+ import { getElanousConfigDir } from '../elanous-config-dir.js';
7
+
8
+ export interface Project {
9
+ id: string;
10
+ name: string;
11
+ primaryFolder?: string;
12
+ createdAt: string;
13
+ }
14
+
15
+ const PROJECT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
16
+
17
+ function validId(id: unknown): id is string {
18
+ return typeof id === 'string' && PROJECT_ID.test(id);
19
+ }
20
+
21
+ function validateProject(value: unknown): Project {
22
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('invalid project record');
23
+ const item = value as Record<string, unknown>;
24
+ if (!validId(item.id) || typeof item.name !== 'string' || !item.name.trim() || item.name.length > 80
25
+ || typeof item.createdAt !== 'string' || !Number.isFinite(Date.parse(item.createdAt))
26
+ || (item.primaryFolder !== undefined && (typeof item.primaryFolder !== 'string' || !isAbsolute(item.primaryFolder)))) {
27
+ throw new Error('invalid project record');
28
+ }
29
+ return value as Project;
30
+ }
31
+
32
+ export class ProjectStore {
33
+ readonly dir: string;
34
+
35
+ constructor(configRoot = getElanousConfigDir()) {
36
+ this.dir = join(configRoot, 'projects');
37
+ }
38
+
39
+ create(input: { name: string; primaryFolder?: string }): Project {
40
+ const item = validateProject({ id: randomUUID(), name: input.name, createdAt: new Date().toISOString(),
41
+ ...(input.primaryFolder !== undefined ? { primaryFolder: input.primaryFolder } : {}) });
42
+ mkdirSync(this.dir, { recursive: true, mode: 0o700 });
43
+ const target = join(this.dir, `${item.id}.yaml`);
44
+ const temp = join(this.dir, `.${item.id}.${randomUUID()}.tmp`);
45
+ try {
46
+ writeFileSync(temp, stringify(item), { flag: 'wx', mode: 0o600 });
47
+ // Link publishes a complete file without replacing an existing project.
48
+ linkSync(temp, target);
49
+ } finally {
50
+ try { unlinkSync(temp); } catch (error) {
51
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
52
+ }
53
+ }
54
+ return item;
55
+ }
56
+
57
+ get(id: string): Project | null {
58
+ if (!validId(id)) return null;
59
+ let text: string;
60
+ try { text = readFileSync(join(this.dir, `${id}.yaml`), 'utf8'); }
61
+ catch (error) {
62
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
63
+ throw error;
64
+ }
65
+ const item = validateProject(parse(text) as unknown);
66
+ if (item.id !== id) throw new Error('project id does not match filename');
67
+ return item;
68
+ }
69
+
70
+ list(): Project[] {
71
+ let files: string[];
72
+ try { files = readdirSync(this.dir); }
73
+ catch (error) {
74
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
75
+ throw error;
76
+ }
77
+ return files.filter(file => PROJECT_ID.test(file.slice(0, -5)) && file.endsWith('.yaml'))
78
+ .flatMap(file => { const item = this.get(file.slice(0, -5)); return item ? [item] : []; })
79
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt) || a.id.localeCompare(b.id));
80
+ }
81
+
82
+ suggestForFolder(folder: string): Project | null {
83
+ if (!isAbsolute(folder)) throw new Error('folder must be absolute');
84
+ // Compare real paths: macOS reports cwd as /private/var/… for a folder stored as /var/… (symlinked roots).
85
+ const real = (path: string): string => { try { return realpathSync(path); } catch { return resolve(path); } };
86
+ const cwd = real(folder);
87
+ let best: Project | null = null;
88
+ let bestLength = -1;
89
+ for (const item of this.list()) {
90
+ if (!item.primaryFolder) continue;
91
+ const base = real(item.primaryFolder);
92
+ const rel = relative(base, cwd);
93
+ if (rel !== '' && (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))) continue;
94
+ if (base.length > bestLength) { best = item; bestLength = base.length; }
95
+ }
96
+ debug.log('project.suggestion', 'checked', { matched: best !== null });
97
+ return best;
98
+ }
99
+ }
100
+
101
+ export function createProject(input: { name: string; primaryFolder?: string }): Project {
102
+ return new ProjectStore().create(input);
103
+ }
104
+
105
+ export function listProjects(): Project[] {
106
+ return new ProjectStore().list();
107
+ }
108
+
109
+ export function getProject(id: string): Project | null {
110
+ return new ProjectStore().get(id);
111
+ }
112
+
113
+ export function suggestProjectForFolder(folder: string): Project | null {
114
+ return new ProjectStore().suggestForFolder(folder);
115
+ }
@@ -28,6 +28,7 @@ export interface ChecklistHistory {
28
28
  to: unknown;
29
29
  released: string;
30
30
  dev: string;
31
+ force?: true;
31
32
  }
32
33
  export interface Checklist {
33
34
  version: string;
@@ -41,12 +42,44 @@ export function devVersion(): string {
41
42
  return (JSON.parse(readFileSync(join(import.meta.dir, '..', '..', 'package.json'), 'utf8')) as { version: string }).version;
42
43
  }
43
44
 
45
+ export function parseOwner(value: string): { seat: string; sub?: string } {
46
+ const match = /^([A-Z]{2,8})(?:\/([a-z][a-z0-9-]{0,31}))?$/.exec(value);
47
+ if (!match || match[0] !== value) throw new CliUserError(`잘못된 담당: ${value}`, 'TC 또는 TC/rel');
48
+ return match[2] ? { seat: match[1]!, sub: match[2] } : { seat: match[1]! };
49
+ }
50
+
51
+ export function ownerMatches(owner: string | undefined, filter: string): boolean {
52
+ if (owner === undefined) return false;
53
+ try {
54
+ const parsed = parseOwner(owner);
55
+ const requested = parseOwner(filter);
56
+ return parsed.seat === requested.seat && (requested.sub === undefined || parsed.sub === requested.sub);
57
+ } catch { return false; }
58
+ }
59
+
60
+ // The ledger event schema has no force column; decode its claim target for every checklist snapshot.
61
+ export function decodeClaimHistoryEntry<T extends ChecklistHistory>(entry: T): T {
62
+ if (entry.field !== 'claim' || !entry.to || typeof entry.to !== 'object') return entry;
63
+ const target = entry.to as { owner?: unknown; force?: unknown };
64
+ return typeof target.owner === 'string' && target.force === true ? { ...entry, to: target.owner, force: true } as T : entry;
65
+ }
66
+
67
+ function decodeClaimHistory(data: Checklist): Checklist {
68
+ return { ...data, history: data.history.map(decodeClaimHistoryEntry) };
69
+ }
70
+
44
71
  export function listChecklist(v: string): Checklist {
45
- return store.list(v);
72
+ return decodeClaimHistory(store.list(v));
73
+ }
74
+
75
+ /** Status events in the requested window; other ledger fields never become status changes. */
76
+ export function statusChangesSince(history: readonly ChecklistHistory[], sinceIso: string): ChecklistHistory[] {
77
+ const since = Date.parse(sinceIso);
78
+ return history.filter((entry) => entry.field === 'status' && Date.parse(entry.at) >= since);
46
79
  }
47
80
 
48
81
  function mutate(v: string, apply: (data: Checklist) => boolean): Checklist {
49
- return store.mutate(v, store.releasedVersion(), devVersion(), apply);
82
+ return decodeClaimHistory(store.mutate(v, store.releasedVersion(), devVersion(), apply));
50
83
  }
51
84
 
52
85
  function change(data: Checklist, id: string, field: string, from: unknown, to: unknown, by: string, at: string): void {
@@ -58,6 +91,7 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
58
91
  if (!input.id.trim()) throw new CliUserError('칸 id 가 비었다');
59
92
  if (!input.title.trim()) throw new CliUserError('칸 제목이 비었다');
60
93
  if (input.kind !== undefined && input.kind !== 'screen') throw new CliUserError(`잘못된 종류: ${input.kind}`, 'screen');
94
+ if (input.owner !== undefined) parseOwner(input.owner);
61
95
  return mutate(v, (data) => {
62
96
  if (data.items.some((item) => item.id === input.id)) throw new CliUserError(`이미 있는 칸: ${input.id}`, 'set <id> 로 고친다');
63
97
  const by = process.env.ELANOUS_TRACK || 'cli';
@@ -76,6 +110,12 @@ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus
76
110
  if (patch.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(patch.status)) throw new CliUserError(`잘못된 상태: ${patch.status}`);
77
111
  if (patch.disposition !== undefined && !['move', 'known-issue', 'block'].includes(patch.disposition)) throw new CliUserError(`잘못된 처분: ${patch.disposition}`);
78
112
  if (patch.kind !== undefined && patch.kind !== 'screen') throw new CliUserError(`잘못된 종류: ${patch.kind}`, 'screen');
113
+ if (patch.owner !== undefined) {
114
+ parseOwner(patch.owner);
115
+ if (item.owner && patch.owner !== item.owner) {
116
+ throw new CliUserError(`지금 주인: ${item.owner} — --force 로만 바꾼다`, 'release checklist claim <id> --by <자리> --force');
117
+ }
118
+ }
79
119
  const fields = (['evidence', 'owner', 'status', 'disposition', 'kind'] as const).filter((field) => patch[field] !== undefined && patch[field] !== item[field]);
80
120
  if (fields.length === 0) return false;
81
121
  const at = new Date().toISOString();
@@ -91,6 +131,34 @@ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus
91
131
  });
92
132
  }
93
133
 
134
+ export function claimItem(v: string, id: string, by: string, options: { force?: boolean } = {}): Checklist {
135
+ const requested = parseOwner(by);
136
+ let outcome: 'claimed' | 'same' | 'refused' = 'refused';
137
+ let from: string | null = null;
138
+ try {
139
+ const result = mutate(v, (data) => {
140
+ const item = data.items.find((i) => i.id === id);
141
+ if (!item) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
142
+ from = item.owner ?? null;
143
+ if (from === by) { outcome = 'same'; return false; }
144
+ if (from && !options.force && !(from === requested.seat && requested.sub)) {
145
+ throw new CliUserError(`지금 주인: ${from} — --force 로만 바꾼다`);
146
+ }
147
+ const at = new Date().toISOString();
148
+ item.owner = by;
149
+ item.updatedAt = at;
150
+ item.updatedBy = by;
151
+ data.history.push({ at, by, id, field: 'claim', from, to: options.force ? { owner: by, force: true } : by,
152
+ released: data.released, dev: data.dev });
153
+ outcome = 'claimed';
154
+ return true;
155
+ });
156
+ return result;
157
+ } finally {
158
+ debug.log('release-loop.checklist', 'claim', { version: v, id, from, to: by, force: options.force === true, outcome });
159
+ }
160
+ }
161
+
94
162
  export function removeItem(v: string, id: string, by: string): Checklist {
95
163
  return mutate(v, (data) => {
96
164
  const index = data.items.findIndex((i) => i.id === id);
@@ -106,15 +174,21 @@ export function summarize(v: string): ChecklistSummary {
106
174
  return summarizeChecklist(listChecklist(v));
107
175
  }
108
176
 
109
- export interface ChecklistSummary { green: number; yellow: number; red: number; done: number; blocked: string[]; byOwner: Record<string, number> }
177
+ export interface ChecklistSummary { green: number; yellow: number; red: number; done: number; blocked: string[]; byOwner: Record<string, number>; bySeat: Record<string, number> }
110
178
 
111
179
  export function summarizeChecklist(data: Checklist): ChecklistSummary {
112
- const summary = { green: 0, yellow: 0, red: 0, done: 0, blocked: [] as string[], byOwner: Object.create(null) as Record<string, number> };
180
+ const summary = { green: 0, yellow: 0, red: 0, done: 0, blocked: [] as string[], byOwner: Object.create(null) as Record<string, number>, bySeat: Object.create(null) as Record<string, number> };
113
181
  for (const item of data.items) {
114
182
  summary[item.status]++;
115
183
  if (item.status === 'red') summary.blocked.push(item.id);
116
184
  const owner = item.owner ?? 'unassigned';
117
185
  summary.byOwner[owner] = (summary.byOwner[owner] ?? 0) + 1;
186
+ if (item.owner !== undefined) {
187
+ try {
188
+ const { seat } = parseOwner(item.owner);
189
+ summary.bySeat[seat] = (summary.bySeat[seat] ?? 0) + 1;
190
+ } catch { /* Preserve legacy owner in byOwner without assigning it to a seat. */ }
191
+ }
118
192
  }
119
193
  return summary;
120
194
  }