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
@@ -23,6 +23,12 @@ import { probeTailscale, type TailscaleProbe } from '../nexus/onboarding/tailsca
23
23
  import { unregisterPwaInstance } from './pwa-registry.js';
24
24
  import { readNexusLock } from '../nexus/supervisor/lock.js';
25
25
  import { resolveNexusPwa, type NexusPwaResolution } from './nexus-show.js';
26
+ import { debug } from '../debug/log.js';
27
+ import { isPidAlive } from '../process/pid-liveness.js';
28
+ import { execFileSync } from 'node:child_process';
29
+ import { join as joinPath, resolve as resolvePath } from 'node:path';
30
+ import { hostname } from 'node:os';
31
+ import { safeReadLock } from '../telegram-lock.js';
26
32
 
27
33
  export interface PwaStopOpts {
28
34
  out?: { log: (s: string) => void; error: (s: string) => void };
@@ -45,7 +51,19 @@ export interface PwaStopOpts {
45
51
  * from `~/.elanous/pwa-registry.json`. */
46
52
  unregisterFn?: (pid: number) => void;
47
53
  /** P4 — read the nexus lock to recover daemon pid. Test seam. */
48
- readLockFn?: () => { pid: number } | null;
54
+ readLockFn?: () => { pid: number; host?: string; startedAt?: string } | null;
55
+ /** Isolated test daemon stop only: the project-local root to inspect for orphans. */
56
+ isolatedRoot?: string;
57
+ /** Test seams for signals, liveness and bounded polling. */
58
+ signalFn?: (pid: number, signal: 'SIGINT' | 'SIGTERM') => void;
59
+ pidAliveFn?: (pid: number) => boolean;
60
+ sleepFn?: (ms: number) => Promise<void>;
61
+ stopTimeoutMs?: number;
62
+ nowFn?: () => number;
63
+ /** Process table for isolated daemon identity checks and orphan discovery (pid, start time, argv). */
64
+ processTableFn?: () => Array<{ pid: number; startedAt: string; args: string }>;
65
+ /** Test seam for the raw `ps -eo pid=,lstart=,args=` response. */
66
+ psOutputFn?: () => string;
49
67
  }
50
68
 
51
69
  export type PwaStopShareUnmount =
@@ -57,7 +75,7 @@ export interface PwaStopResult {
57
75
  exitCode: number;
58
76
  /** True when the dev BG was actually killed (vs no-lock no-op). */
59
77
  devKilled: boolean;
60
- /** True when the nexus stop signal was sent. */
78
+ /** True when the daemon stop completed (or the ordinary stop trigger succeeded). */
61
79
  nexusStopped: boolean;
62
80
  /** True when `tailscale serve reset` was issued (not skipped). */
63
81
  shareReset: boolean;
@@ -73,6 +91,126 @@ async function defaultNexusStop(): Promise<{ exitCode: number }> {
73
91
  return { exitCode: 0 };
74
92
  }
75
93
 
94
+ function runningProcesses(output: string): Array<{ pid: number; startedAt: string; args: string }> {
95
+ const lines = output.split('\n');
96
+ return lines.flatMap(line => {
97
+ const match = line.match(/^\s*(\d+)\s+((?:\S+\s+){4}\d{4})\s+(.+)$/);
98
+ return match ? [{ pid: Number(match[1]), startedAt: match[2]!, args: match[3]! }] : [];
99
+ });
100
+ }
101
+
102
+ type ProcessRow = ReturnType<typeof runningProcesses>[number];
103
+
104
+ function isolatedDaemons(root: string, table: ProcessRow[]): ProcessRow[] {
105
+ return table.filter(row => {
106
+ if (!/^(?:(?:\S*\/)?(?:bun|node)\s+)?(?:\S*\/)?elanous(?:\.(?:mjs|cjs))?\s/.test(row.args)
107
+ || !/(?:^|\s)nexus\s+run(?:\s|$)/.test(row.args)) return false;
108
+ const roots = ['--config-dir', '--test-state-dir'].flatMap(flag => {
109
+ const value = row.args.match(new RegExp(`(?:^|\\s)${flag}(?:=|\\s+)(?:"([^"]+)"|'([^']+)'|(\\S+))`));
110
+ const arg = value?.[1] ?? value?.[2] ?? value?.[3];
111
+ return arg === undefined ? [] : [resolvePath(arg)];
112
+ });
113
+ return roots.length > 0 && roots.every(value => value === resolvePath(root));
114
+ });
115
+ }
116
+
117
+ async function stopIsolatedNexus(
118
+ opts: PwaStopOpts,
119
+ out: NonNullable<PwaStopOpts['out']>,
120
+ lock: ReturnType<NonNullable<PwaStopOpts['readLockFn']>>,
121
+ ): Promise<{ exitCode: number }> {
122
+ const root = opts.isolatedRoot!;
123
+ const inspect = (): ProcessRow[] => isolatedDaemons(root, opts.processTableFn
124
+ ? opts.processTableFn()
125
+ : runningProcesses((opts.psOutputFn ?? (() => execFileSync('ps', ['-eo', 'pid=,lstart=,args='], { encoding: 'utf8' })))()));
126
+ const reportOrphans = (): { exitCode: number } => {
127
+ const orphans = inspect();
128
+ debug.log('nexus.stop', 'orphans', { pid: 0, waitedMs: 0, count: orphans.length });
129
+ if (orphans.length === 0) {
130
+ out.log(' nothing to stop (no lock)');
131
+ return { exitCode: 0 };
132
+ }
133
+ for (const row of orphans) out.error(` orphan pid ${row.pid} · started ${row.startedAt} · root ${root}`);
134
+ out.error(` cleanup: ${orphans.map(row => `kill ${row.pid}`).join(' ; ')}`);
135
+ return { exitCode: 1 };
136
+ };
137
+ if (!lock) {
138
+ try { return reportOrphans(); }
139
+ catch (error) {
140
+ out.error(` cannot inspect nexus processes: ${error instanceof Error ? error.message : String(error)}`);
141
+ return { exitCode: 1 };
142
+ }
143
+ }
144
+ const pid = lock.pid;
145
+ if (lock.host && lock.host !== hostname()) {
146
+ out.error(` nexus lock held by remote host ${lock.host}; cannot stop from here`);
147
+ return { exitCode: 1 };
148
+ }
149
+ if (!Number.isSafeInteger(pid) || pid <= 0) {
150
+ out.error(` invalid nexus lock pid ${pid}`);
151
+ return { exitCode: 1 };
152
+ }
153
+ const signal = opts.signalFn ?? ((target: number, name: 'SIGINT' | 'SIGTERM') => process.kill(target, name));
154
+ const alive = opts.pidAliveFn ?? isPidAlive;
155
+ const sleep = opts.sleepFn ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)));
156
+ const timeout = opts.stopTimeoutMs ?? 10_000;
157
+ const now = opts.nowFn ?? Date.now;
158
+ let waitedMs = 0;
159
+ const wait = async (): Promise<boolean> => {
160
+ const start = now();
161
+ let elapsed = 0;
162
+ while (alive(pid) && elapsed < timeout) {
163
+ const interval = Math.min(100, timeout - elapsed);
164
+ await sleep(interval);
165
+ elapsed = Math.max(elapsed + interval, now() - start);
166
+ }
167
+ waitedMs += elapsed;
168
+ debug.log('nexus.stop', 'wait', { pid, waitedMs, count: 1 });
169
+ return !alive(pid);
170
+ };
171
+ try {
172
+ if (!alive(pid)) return reportOrphans();
173
+ const identity = inspect().find(row => row.pid === pid);
174
+ if (!identity) {
175
+ out.error(` pid ${pid} does not match this tree's isolated nexus daemon; no signal sent`);
176
+ return { exitCode: 1 };
177
+ }
178
+ if (lock.startedAt) {
179
+ const processStart = Date.parse(identity.startedAt);
180
+ const lockStart = Date.parse(lock.startedAt);
181
+ if (!Number.isFinite(processStart) || !Number.isFinite(lockStart) || processStart > lockStart + 1_000) {
182
+ out.error(` pid ${pid} start time cannot match the nexus lock; no signal sent`);
183
+ return { exitCode: 1 };
184
+ }
185
+ }
186
+ // Recheck the argv and start time immediately before each signal: a stale
187
+ // lock or a PID recycled during the wait must never target another process.
188
+ const stillOwned = (): boolean => inspect().some(row => row.pid === pid && row.startedAt === identity.startedAt && row.args === identity.args);
189
+ if (!stillOwned()) {
190
+ out.error(` pid ${pid} no longer matches the locked daemon; no signal sent`);
191
+ return { exitCode: 1 };
192
+ }
193
+ signal(pid, 'SIGINT');
194
+ out.log(` SIGINT sent to pid ${pid}`);
195
+ if (await wait()) return { exitCode: 0 };
196
+ if (!alive(pid)) return { exitCode: 0 };
197
+ if (!stillOwned()) {
198
+ out.error(` pid ${pid} no longer matches the locked daemon; SIGTERM not sent`);
199
+ return { exitCode: 1 };
200
+ }
201
+ signal(pid, 'SIGTERM');
202
+ debug.log('nexus.stop', 'escalated', { pid, waitedMs, count: 1 });
203
+ if (await wait()) return { exitCode: 0 };
204
+ } catch (error) {
205
+ out.error(` pid ${pid} 이 멈추지 않았다: ${error instanceof Error ? error.message : String(error)}`);
206
+ debug.log('nexus.stop', 'stuck', { pid, waitedMs, count: 1 });
207
+ return { exitCode: 1 };
208
+ }
209
+ out.error(` pid ${pid} 이 멈추지 않았다`);
210
+ debug.log('nexus.stop', 'stuck', { pid, waitedMs, count: 1 });
211
+ return { exitCode: 1 };
212
+ }
213
+
76
214
  async function defaultShareReset(binary: string, port: number): Promise<{ exitCode: number }> {
77
215
  const r = await unmountTailscaleServe({
78
216
  mode: { kind: 'tls-tcp', port },
@@ -111,11 +249,12 @@ export async function runPwaStop(opts: PwaStopOpts = {}): Promise<PwaStopResult>
111
249
  const out = opts.out ?? console;
112
250
  const portResolution = resolveShareUnmountPort(opts);
113
251
  const devStopFn = opts.devStopFn ?? (() => runPwaDevStop({ out }));
114
- const nexusStopFn = opts.nexusStopFn ?? defaultNexusStop;
115
252
  const shareProbeFn = opts.shareProbeFn ?? (() => probeTailscale());
116
253
  const shareResetFn = opts.shareResetFn ?? defaultShareReset;
117
- const unregisterFn = opts.unregisterFn ?? unregisterPwaInstance;
118
- const readLockFn = opts.readLockFn ?? (() => readNexusLock());
254
+ const unregisterFn = opts.unregisterFn ?? (opts.isolatedRoot ? undefined : unregisterPwaInstance);
255
+ const readLockFn = opts.readLockFn ?? (opts.isolatedRoot
256
+ ? () => safeReadLock(joinPath(opts.isolatedRoot!, 'nexus', '.lock'))
257
+ : () => readNexusLock());
119
258
 
120
259
  out.log('elanous nexus pwa stop: cascade');
121
260
 
@@ -157,19 +296,30 @@ export async function runPwaStop(opts: PwaStopOpts = {}): Promise<PwaStopResult>
157
296
  }
158
297
 
159
298
  // P4 registry unregister — read the lock for the daemon pid before
160
- // we send SIGINT (the lock is removed during stop). Best-effort: if
161
- // the lock is already gone or pid missing, just skip.
299
+ // we send SIGINT (the lock is removed during stop). The isolated stop
300
+ // never writes to the production registry. Missing lock/pid is best-effort.
162
301
  let unregisteredPid: number | undefined;
302
+ let lock: ReturnType<typeof readLockFn> = null;
303
+ try { lock = readLockFn(); }
304
+ catch (error) {
305
+ if (opts.isolatedRoot) {
306
+ out.error(` cannot read nexus lock: ${error instanceof Error ? error.message : String(error)}`);
307
+ return { exitCode: 1, devKilled: false, nexusStopped: false, shareReset, shareUnmount };
308
+ }
309
+ }
163
310
  try {
164
- const lock = readLockFn();
165
- if (lock && typeof lock.pid === 'number') {
166
- unregisteredPid = lock.pid;
167
- unregisterFn(lock.pid);
311
+ if (lock && typeof lock.pid === 'number' && (!opts.isolatedRoot || !lock.host || lock.host === hostname())) {
312
+ if (unregisterFn) {
313
+ unregisterFn(lock.pid);
314
+ unregisteredPid = lock.pid;
315
+ }
168
316
  }
169
- } catch { /* lock read or unregister failure — non-fatal */ }
317
+ } catch { /* registry unregister failure — non-fatal */ }
170
318
 
171
319
  const devR = await devStopFn();
172
- const nexusR = await nexusStopFn();
320
+ const nexusR = await (opts.nexusStopFn ?? (opts.isolatedRoot
321
+ ? () => stopIsolatedNexus(opts, out, lock)
322
+ : defaultNexusStop))();
173
323
  const exitCode = devR.exitCode === 0 && nexusR.exitCode === 0 ? 0 : 1;
174
324
  return {
175
325
  exitCode,
@@ -720,8 +720,11 @@ async function runStop(
720
720
  out,
721
721
  shareProbeFn: async () => ({ installed: false, alive: false }),
722
722
  shareResetFn: async () => ({ exitCode: 0 }),
723
+ isolatedRoot: layout.stateDir,
723
724
  });
724
725
 
726
+ // Keep the record when the daemon did not stop; status must not imply a clean shutdown.
727
+ if (stopRes.exitCode !== 0) return { exitCode: stopRes.exitCode };
725
728
  // Cleanup state files — leave logs/ for post-mortem.
726
729
  const stateFile = joinPath(layout.stateDir, 'test-state.json');
727
730
  if (existsSync(stateFile)) {
@@ -23,9 +23,11 @@ import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js
23
23
  import { getElanousConfigDirOverride } from '../elanous-config-dir.js';
24
24
  import { envLiteral } from '../platform/env-literal.js';
25
25
  import { userConfigPath } from '../user-config.js';
26
- import { addItem, devVersion, listChecklist, removeItem, seedFromRoadmap, setItem, summarizeChecklist, type ChecklistStatus, type ChecklistDisposition, type ChecklistKind } from '../release-loop/checklist.js';
26
+ import { addItem, claimItem, decodeClaimHistoryEntry, devVersion, listChecklist, ownerMatches, parseOwner, parityGap, removeItem, seedFromRoadmap, setItem, summarizeChecklist, type ChecklistStatus, type ChecklistDisposition, type ChecklistKind } from '../release-loop/checklist.js';
27
27
  import * as features from '../release-loop/feature-store.js';
28
+ import { formatSchedule, getSchedule, listSchedules, setSchedule } from '../release-loop/release-schedule.js';
28
29
  import { writeStdoutJson } from './stdout-json.js';
30
+ import { CliUserError } from './cli-user-error.js';
29
31
  import { runUnattendedRelease, type UnattendedReleaseDeps } from '../../scripts/release-loop/unattended-release.js';
30
32
  import { releaseReadiness } from '../../scripts/release-loop/release-readiness.js';
31
33
  import { runReleaseIfReady } from './release-run-if-ready.js';
@@ -468,15 +470,23 @@ function checklistCodenames(): Record<string, string> {
468
470
  function checklistVersion(input: string | undefined, codenames: Record<string, string>): string {
469
471
  const v = input ?? devVersion().replace(/-dev\.\d+$/, '');
470
472
  const resolved = isReleaseVersion(v) ? v : Object.entries(codenames).find(([, name]) => name === v)?.[0] ?? v;
471
- if (!isReleaseVersion(resolved)) throw new Error(`체크리스트 판 또는 별칭이 아니다: ${v}`);
473
+ if (!isReleaseVersion(resolved)) throw new CliUserError(`체크리스트 판 또는 별칭이 아니다: ${v}`);
472
474
  return resolved;
473
475
  }
474
476
 
475
- async function checklistOutput(v: string, codenames: Record<string, string>, mode: 'list' | 'status', json?: boolean): Promise<void> {
476
- const data = listChecklist(v);
477
+ async function checklistOutput(v: string, codenames: Record<string, string>, mode: 'list' | 'status', json?: boolean, owner?: string): Promise<void> {
478
+ if (owner !== undefined) parseOwner(owner);
479
+ const snapshot = listChecklist(v);
480
+ const data = owner === undefined ? snapshot : { ...snapshot, items: snapshot.items.filter((item) => ownerMatches(item.owner, owner)) };
477
481
  const summary = summarizeChecklist(data);
478
- const result = { ...data, codename: codenames[v] ?? '', ...summary };
482
+ const parity = data.items.flatMap((item) => {
483
+ const why = item.kind === 'screen' ? parityGap(item.evidence) : null;
484
+ return why ? [{ id: item.id, why }] : [];
485
+ });
486
+ const result = { ...data, codename: codenames[v] ?? '', ...summary, parity };
487
+ for (const { id, why } of parity) debug.log('release.checklist', 'parity-warning', { version: v, id, why });
479
488
  if (json) { await writeStdoutJson(`${JSON.stringify(result)}\n`); return; }
489
+ console.log(`⚠ 짝 경고 ${parity.length}: ${parity.map(({ id }) => id).join(', ') || '없음'}`);
480
490
  console.log(`${v}${result.codename ? ` (${result.codename})` : ''} · 공개 ${data.released || '없음'} · 개발 ${data.dev}`);
481
491
  if (mode === 'list') {
482
492
  for (const item of data.items) console.log(`${({ green: '🟢', yellow: '🟡', red: '🔴', done: '✅' } as const)[item.status]} ${item.id} ${item.title}${item.owner ? ` · ${item.owner}` : ''}`);
@@ -489,6 +499,26 @@ async function checklistOutput(v: string, codenames: Record<string, string>, mod
489
499
 
490
500
  export function registerReleaseCommands(program: Command, releaseRunDeps: UnattendedReleaseDeps = {}): void {
491
501
  const release = program.command('release').description('공개 배포 한 판 — prepare(로컬) → publish(--yes) → verify (docs/manual/MANUAL-versioning-and-release-2026-09-25.md)');
502
+ const schedule = release.command('schedule').description('판별 컷·착지 마감 조회·갱신');
503
+ const scheduleLedgerRoot = () => releaseRunDeps.ledgerRoot ?? releaseLedgerRoot();
504
+ schedule.command('list').option('--json', '결과 JSON').action((o: { json?: boolean }) => {
505
+ const rows = listSchedules(scheduleLedgerRoot());
506
+ if (o.json) console.log(JSON.stringify(rows));
507
+ else for (const row of rows) console.log(formatSchedule(row));
508
+ });
509
+ schedule.command('show').requiredOption('--version <v>', '조회할 판').option('--json', '결과 JSON')
510
+ .action((o: { version: string; json?: boolean }) => {
511
+ const row = getSchedule(o.version, scheduleLedgerRoot());
512
+ if (!row) throw new CliUserError(`없는 판 일정: ${o.version}`, '판 일정 등록: elanous release schedule set --version <v> --cut-at <iso> --land-by <iso>');
513
+ if (o.json) console.log(JSON.stringify(row)); else console.log(formatSchedule(row));
514
+ });
515
+ schedule.command('set').requiredOption('--version <v>', '갱신할 판')
516
+ .option('--cut-at <iso>', '오프셋 포함 컷 시각').option('--land-by <iso>', '오프셋 포함 착지 마감').option('--json', '결과 JSON')
517
+ .action((o: { version: string; cutAt?: string; landBy?: string; json?: boolean }) => {
518
+ if (o.cutAt === undefined && o.landBy === undefined) throw new CliUserError('갱신할 시각을 지정하라', '--cut-at <iso> 또는 --land-by <iso>');
519
+ const row = setSchedule(o.version, { cutAt: o.cutAt, landBy: o.landBy }, process.env.ELANOUS_TRACK || 'cli', scheduleLedgerRoot());
520
+ if (o.json) console.log(JSON.stringify(row)); else console.log(formatSchedule(row));
521
+ });
492
522
  const checklist = release.command('checklist').description('판별 확인표 조회·갱신')
493
523
  .option('--version <v>', '판 또는 별칭(기본: package.json 의 개발판에서 -dev.N 제거)')
494
524
  .option('--json', '결과 JSON');
@@ -498,15 +528,15 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
498
528
  return { version: checklistVersion(opts.version, codenames), codenames, json: opts.json };
499
529
  };
500
530
  const withContext = (cmd: Command) => cmd.option('--version <v>', '판 또는 별칭').option('--json', '결과 JSON');
501
- withContext(checklist.command('list').description('모든 칸')).action(async (_opts: unknown, cmd: Command) => {
531
+ withContext(checklist.command('list').description('모든 칸')).option('--owner <owner>', '자리 또는 하위 자리로 거르기').action(async (opts: { owner?: string }, cmd: Command) => {
502
532
  const { version, codenames, json } = context(cmd);
503
- await checklistOutput(version, codenames, 'list', json);
533
+ await checklistOutput(version, codenames, 'list', json, opts.owner);
504
534
  });
505
- withContext(checklist.command('status').description('상태 요약')).action(async (_opts: unknown, cmd: Command) => {
535
+ withContext(checklist.command('status').description('상태 요약')).option('--owner <owner>', '자리 또는 하위 자리로 거르기').action(async (opts: { owner?: string }, cmd: Command) => {
506
536
  const { version, codenames, json } = context(cmd);
507
- await checklistOutput(version, codenames, 'status', json);
537
+ await checklistOutput(version, codenames, 'status', json, opts.owner);
508
538
  });
509
- withContext(checklist.command('add <id> <title>').description('칸 추가')).option('--owner <owner>', '담당').option('--kind <kind>', 'screen = 다섯 화면 짝 칸')
539
+ withContext(checklist.command('add <id> <title>').description('칸 추가')).option('--owner <owner>', '담당').option('--kind <kind>', 'screen = 다섯 화면 짝 칸 · 근거에 짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 … 한 줄')
510
540
  .action((id: string, title: string, opts: { owner?: string; kind?: string }, cmd: Command) => {
511
541
  const { version, json } = context(cmd);
512
542
  const data = addItem(version, { id, title, ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}) });
@@ -514,15 +544,28 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
514
544
  });
515
545
  withContext(checklist.command('set <id>').description('칸 상태·근거·담당·처분 갱신'))
516
546
  .option('--status <status>', 'green|yellow|red|done').option('--evidence <evidence>', '근거').option('--owner <owner>', '담당')
517
- .option('--disposition <disposition>', 'move|known-issue|block').option('--kind <kind>', 'screen = 다섯 화면 짝 칸')
547
+ .option('--disposition <disposition>', 'move|known-issue|block').option('--kind <kind>', 'screen = 다섯 화면 짝 칸 · 근거에 짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 … 한 줄')
518
548
  .action((id: string, opts: { status?: string; evidence?: string; owner?: string; disposition?: string; kind?: string }, cmd: Command) => {
519
549
  const { version, json } = context(cmd);
520
- if (opts.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(opts.status)) throw new Error(`잘못된 상태: ${opts.status}`);
521
- if (opts.disposition !== undefined && !['move', 'known-issue', 'block'].includes(opts.disposition)) throw new Error(`잘못된 처분: ${opts.disposition}`);
522
- if (opts.status === undefined && opts.evidence === undefined && opts.owner === undefined && opts.disposition === undefined && opts.kind === undefined) throw new Error('갱신할 칸을 지정하라');
550
+ if (opts.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(opts.status)) throw new CliUserError(`잘못된 상태: ${opts.status}`, 'green|yellow|red|done');
551
+ if (opts.disposition !== undefined && !['move', 'known-issue', 'block'].includes(opts.disposition)) throw new CliUserError(`잘못된 처분: ${opts.disposition}`, 'move|known-issue|block');
552
+ if (opts.status === undefined && opts.evidence === undefined && opts.owner === undefined && opts.disposition === undefined && opts.kind === undefined) throw new CliUserError('갱신할 칸을 지정하라', '--status · --evidence · --owner · --disposition · --kind 중 하나');
523
553
  const data = setItem(version, id, { ...(opts.status !== undefined ? { status: opts.status as ChecklistStatus } : {}), ...(opts.evidence !== undefined ? { evidence: opts.evidence } : {}), ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.disposition !== undefined ? { disposition: opts.disposition as ChecklistDisposition } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}) }, process.env.ELANOUS_TRACK || 'cli');
554
+ const item = data.items.find((entry) => entry.id === id);
555
+ const why = item?.kind === 'screen' && item.status === 'green' ? parityGap(item.evidence) : null;
556
+ if (why) {
557
+ console.error(`⚠ 짝: ${why} — 근거에 «짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 …» 한 줄`);
558
+ debug.log('release.checklist', 'parity-warning', { version, id, why });
559
+ }
524
560
  if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 갱신`);
525
561
  });
562
+ withContext(checklist.command('claim <id>').description('칸의 단일 주인을 잡는다'))
563
+ .requiredOption('--by <owner>', '잡을 자리 또는 하위 자리').option('--force', '기존 주인에서 강제로 바꾼다')
564
+ .action((id: string, opts: { by: string; force?: boolean }, cmd: Command) => {
565
+ const { version, json } = context(cmd);
566
+ const data = claimItem(version, id, opts.by, { force: opts.force });
567
+ if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 주인 ${opts.by}`);
568
+ });
526
569
  withContext(checklist.command('rm <id>').description('칸 삭제')).action((id: string, _opts: unknown, cmd: Command) => {
527
570
  const { version, json } = context(cmd);
528
571
  const data = removeItem(version, id, process.env.ELANOUS_TRACK || 'cli');
@@ -540,8 +583,8 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
540
583
  .action((id: string, opts: { from: string; to: string }, cmd: Command) => {
541
584
  const { codenames, json } = context(cmd);
542
585
  const from = checklistVersion(opts.from, codenames), to = checklistVersion(opts.to, codenames);
543
- const data = features.move(id, from, to, actor());
544
- if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} ${from} → ${to}`);
586
+ features.move(id, from, to, actor());
587
+ if (json) console.log(JSON.stringify(listChecklist(to))); else console.log(`✅ ${id} ${from} → ${to}`);
545
588
  });
546
589
  withContext(checklist.command('retitle <id> <title>').description('칸 제목 수정')).action((id: string, title: string, _opts: unknown, cmd: Command) => {
547
590
  const { version, json } = context(cmd);
@@ -554,11 +597,11 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
554
597
  const { version, json } = context(cmd);
555
598
  const snapshot = listChecklist(version);
556
599
  features.evidenceAdd(id, version, ref, actor(), snapshot.released, snapshot.dev);
557
- if (json) console.log(JSON.stringify(features.history(id))); else console.log(`✅ ${id} 근거 ${ref}`);
600
+ if (json) console.log(JSON.stringify(features.history(id).map(decodeClaimHistoryEntry))); else console.log(`✅ ${id} 근거 ${ref}`);
558
601
  });
559
602
  withContext(checklist.command('history <id>').description('칸의 판 이동·변경 이력')).action((id: string, _opts: unknown, cmd: Command) => {
560
603
  const { json } = context(cmd);
561
- const rows = features.history(id);
604
+ const rows = features.history(id).map(decodeClaimHistoryEntry);
562
605
  if (json) { console.log(JSON.stringify(rows)); return; }
563
606
  const info = features.details(id);
564
607
  if (info) {
@@ -570,8 +613,8 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
570
613
  withContext(checklist.command('export').description('SQLite 에서 checklist.json 스냅샷 생성')).action((_opts: unknown, cmd: Command) => {
571
614
  const { version, json } = context(cmd);
572
615
  const snapshot = listChecklist(version);
573
- const data = features.exportJson(version, snapshot.released, snapshot.dev);
574
- if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${version} checklist.json 내보냄`);
616
+ features.exportJson(version, snapshot.released, snapshot.dev);
617
+ if (json) console.log(JSON.stringify(listChecklist(version))); else console.log(`✅ ${version} checklist.json 내보냄`);
575
618
  });
576
619
  release.command('prepare')
577
620
  .description('깨끗한 원본 → 공개본 → 공개 저장소 이력 위 커밋 → PWA → 묶음·체크섬 → 로컬 끝까지 (네트워크 쓰기 없음)')
@@ -343,6 +343,14 @@ function pruneReleaseVersions(plan: { current: string; previous: string; keep: n
343
343
  return { removed, kept };
344
344
  }
345
345
 
346
+ /** The build identity of an install.json — version, version folder and commit; empty when it cannot be read. */
347
+ function installedBuild(metadata: unknown): string {
348
+ if (typeof metadata !== 'object' || metadata === null) return '';
349
+ const { version, versionDir, commit } = metadata as { version?: unknown; versionDir?: unknown; commit?: unknown };
350
+ if (typeof version !== 'string' || !version || typeof versionDir !== 'string' || !versionDir) return '';
351
+ return JSON.stringify([version, versionDir, typeof commit === 'string' ? commit : '']);
352
+ }
353
+
346
354
  /** A release update never packs the checkout: the downloaded installer verifies its own release tarball. */
347
355
  export async function runReleaseUpdate(options: ReleaseUpdateOptions = {}, deps: ReleaseUpdateDeps = {}): Promise<SelfUpdateResult> {
348
356
  const out = deps.out ?? console;
@@ -390,8 +398,10 @@ export async function runReleaseUpdate(options: ReleaseUpdateOptions = {}, deps:
390
398
  return fail(1, `설치기 다운로드 실패 (${url}): ${String(error)}`);
391
399
  }
392
400
  let previous = '';
401
+ let previousBuild = '';
393
402
  try {
394
403
  const metadata: unknown = JSON.parse(readFileSync(join(prefix, 'install.json'), 'utf8'));
404
+ previousBuild = installedBuild(metadata);
395
405
  if (typeof metadata === 'object' && metadata !== null && 'versionDir' in metadata && typeof metadata.versionDir === 'string') {
396
406
  const dir = metadata.versionDir;
397
407
  if (/^versions\/[a-zA-Z0-9._-]+$/.test(dir) && !dir.includes('..')) previous = basename(dir);
@@ -421,6 +431,10 @@ export async function runReleaseUpdate(options: ReleaseUpdateOptions = {}, deps:
421
431
  prune = { removed: [], kept: [], skipped: `정리 실패: ${String(error)}` };
422
432
  }
423
433
  if (!options.restart) return finish({ exitCode: 0, installedVersion, decision: null, restarted: false, prune, reason: '--restart 없음: 재시작하지 않음' });
434
+ // REL6: the same build again (version · folder · commit unchanged) leaves the running daemon as it is.
435
+ if (previousBuild && previousBuild === installedBuild(metadata)) {
436
+ return finish({ exitCode: 0, installedVersion, decision: null, restarted: false, prune, reason: `already at ${installedVersion} · restart skipped` });
437
+ }
424
438
  const os = deps.os ?? platform();
425
439
  const command = os === 'darwin' ? 'launchctl' : os === 'linux' ? 'systemctl' : null;
426
440
  if (!command) return finish({ exitCode: 1, installedVersion, decision: null, restarted: false, prune, reason: `지원하지 않는 플랫폼: ${os}` });
@@ -0,0 +1,37 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import { parse } from 'yaml';
4
+ import { debug } from '../debug/log.js';
5
+ import { files, lint, render, STORYBOARD_ROOT, type Storyboard } from '../storyboard/storyboard.js';
6
+
7
+ export function runStoryboard(cmd: 'lint' | 'render', args: string[], json = false): number {
8
+ const reports: Array<{ file: string; errors: string[]; warnings: string[] }> = [];
9
+ let errorCount = 0, warningCount = 0;
10
+ for (const f of files(args)) {
11
+ const file = relative(STORYBOARD_ROOT, f);
12
+ let sb: Storyboard | undefined;
13
+ let errors: string[], warnings: string[];
14
+ try {
15
+ sb = parse(readFileSync(f, 'utf8')) as Storyboard;
16
+ ({ errors, warnings } = lint(sb, file));
17
+ } catch (cause) {
18
+ errors = [cause instanceof Error ? cause.message : String(cause)];
19
+ warnings = [];
20
+ }
21
+ reports.push({ file, errors, warnings });
22
+ errorCount += errors.length;
23
+ warningCount += warnings.length;
24
+ if (json) continue;
25
+ for (const e of errors) console.log(`✗ ${file}: ${e}`);
26
+ for (const w of warnings) console.log(`⚠ ${file}: ${w}`);
27
+ if (errors.length || !sb) continue;
28
+ if (cmd === 'render') {
29
+ const out = join(STORYBOARD_ROOT, 'docs', 'marketing', `STORYBOARD-${sb.id}-v${sb.version}.md`);
30
+ writeFileSync(out, render(sb, file) + '\n');
31
+ console.log(`✓ ${file} → ${relative(STORYBOARD_ROOT, out)}`);
32
+ } else console.log(`✓ ${file} · v${sb.version} · ${sb.status} · ${sb.kind === 'site' ? `사이트 ${sb.sites?.length ?? 0}` : `샷 ${sb.shots.length}`}`);
33
+ }
34
+ if (cmd === 'lint') debug.log('storyboard.lint', 'checked', { files: reports.length, errorCount, warningCount });
35
+ if (json) console.log(JSON.stringify({ files: reports, errorCount, warningCount }));
36
+ return errorCount ? 1 : 0;
37
+ }
@@ -71,7 +71,8 @@ export function applyTestStateDirFlagFromArgv(): string | undefined {
71
71
  * 뒤에만 있어 사람이 부를 입구가 없었고, 그래서 문서 174곳에 수동 주문이 화석으로 남았다.
72
72
  *
73
73
  * 하는 일: nexus state root ⊕ ELANOUS_STATE_DIR ⊕ config-dir 을 **한 뿌리로** 세우고,
74
- * config 사본이 없으면 물질화하며, config 경로가 test 루트 밖이면 **기동을 거부**한다. */
74
+ * 운영 config 가 있으면 test-safe 사본을 물질화하고, 없으면 격리 기본값을 사용하며,
75
+ * config 경로가 test 루트 밖이면 **기동을 거부**한다. */
75
76
  export function applyIsolatedRoot(dir: string): void {
76
77
  const { setTestStateRoot } = require('../nexus/paths.js') as typeof import('../nexus/paths.js');
77
78
  setTestStateRoot(dir);
@@ -98,7 +99,7 @@ export function applyIsolatedRoot(dir: string): void {
98
99
  }
99
100
  cfgDir.setElanousConfigDir(dir);
100
101
  }
101
- // config 사본 보장 — 없으면 운영에서 물질화(최초 무마찰), 있으면 drift 경고만
102
+ // config 사본이 없고 운영 원본이 있으면 물질화하고, 있으면 drift 경고만
102
103
  // (자동 덮어쓰기 금지 — 명시적 sync 원칙). 유닛 테스트(NODE_ENV=test)는
103
104
  // 실 운영 config/secrets 를 temp 로 복사하지 않도록 sync 스킵.
104
105
  if (process.env.NODE_ENV !== 'test') {
@@ -106,8 +107,14 @@ export function applyIsolatedRoot(dir: string): void {
106
107
  const { existsSync } = require('node:fs') as typeof import('node:fs');
107
108
  const { join } = require('node:path') as typeof import('node:path');
108
109
  if (!existsSync(join(dir, 'config.json'))) {
109
- const r = sync.syncTestConfig(dir);
110
- console.error(`[test-isolation] 운영 config 물질화 → ${r.testConfigPath} (telegram=${r.telegramMode})`);
110
+ // A fresh build/test environment may have no operating config to copy.
111
+ // Keep the isolated config root and use the ordinary safe defaults there.
112
+ if (existsSync(join(sync.prodConfigDir(), 'config.json'))) {
113
+ const r = sync.syncTestConfig(dir);
114
+ console.error(`[test-isolation] 운영 config 물질화 → ${r.testConfigPath} (telegram=${r.telegramMode})`);
115
+ } else {
116
+ console.error(`[test-isolation] 운영 config 없음 — ${dir} 의 기본 설정 사용`);
117
+ }
111
118
  } else if (sync.isTestConfigStale(dir)) {
112
119
  console.error(`[test-isolation] ⚠️ 운영 config 가 테스트 사본보다 최신 — 'elanous config sync-test' 로 갱신 권장`);
113
120
  }