elanous 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/2DsiDfDCks44g1cPUMhiW/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/24-e6d28380efab7faf.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/3615-740dd270764ffa24.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/3734-1015fe991eb9b7e7.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/5012-a7fe6f3c8b6fe45a.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/5783-ef40ebfc91d55230.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/62-ff227c3edb8b248b.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/7633-1b3709b0854c9412.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/7951-8b03dbb10b24cd06.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/8067-87ce253ec2f23697.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8195-6fde3d7a39859206.js} +5 -5
  14. package/apps/pwa/out/_next/static/chunks/8894-b5fb36bab837cd29.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/9454-84d0d5d77446bd92.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/988-5f92e7ab7ccea3dd.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/9989-5582582f06429840.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d2253d7e58ef1890.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5441d4a9ab35e518.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/board/page-d9b12d680922d980.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-d4b6df905f185055.js → page-54b32bd27726588d.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/bots/page-e268d6d145827a22.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/chat/{page-186fc6952f027acc.js → page-f8116559c7424da7.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/consult/page-1085399fd0f32112.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/control/page-1df686a14f65895a.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-cbbe03c0da7b081a.js → page-54e1f966ab60a4dc.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/editor/page-7ed95f296f5ee8e9.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/exec/page-11edac668d77e5c3.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/field/page-59e2b9a22cd5a6d5.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/intake/page-2976df21ad74ab48.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/layout-34d211a67f16d510.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/live/page-8ff652d5d950639b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/market/{page-c0c0d19d51342d3e.js → page-b31610deda76337b.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-2e002ad8faa43291.js → page-6cb0e1bbefe5fb3d.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/morning/page-a286a152cf584576.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/not-found-d9237581e7003bcb.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/observatory/page-83274e761643fdd2.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ceee6cfb1d4e6d68.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-9c9f8bb3e4a64091.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c17217603ea3426a.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/page-efb6177cbf1e8a51.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d307523703bc4df3.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-462f4e4bab45ce7d.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8990e96f33af3837.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-7464c1987dd76e6d.js → page-e168fa1030af0548.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/settings/page-1233e05a4068bc9c.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-13a1840b1fac337a.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/setup/page-64a29fc80b11ab55.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/share/page-68e787339a16b3dc.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-d8ab4d1489223459.js → page-4fddc90b23feb215.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/tasks/page-886ba8260dc79c79.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/term/page-0f26507a1722e40b.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/today/page-109d4ccd42259c9d.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/trace/page-f5c2a4e510726774.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/vault/page-b2e0039416f42522.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-26102fd7350f171e.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/workflows/page-708948bc8a802794.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/workspace/page-5e5611620f78fb0b.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-139ba5c367bbf60a.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/main-app-202b4e81f33895ea.js +1 -0
  61. package/apps/pwa/out/_next/static/css/43feca5fd387dcbd.css +1 -0
  62. package/apps/pwa/out/approvals/index.html +1 -1
  63. package/apps/pwa/out/approvals/index.txt +30 -29
  64. package/apps/pwa/out/autopilot/index.html +1 -1
  65. package/apps/pwa/out/autopilot/index.txt +30 -29
  66. package/apps/pwa/out/board/index.html +1 -1
  67. package/apps/pwa/out/board/index.txt +30 -29
  68. package/apps/pwa/out/botlab/index.html +1 -1
  69. package/apps/pwa/out/botlab/index.txt +27 -26
  70. package/apps/pwa/out/bots/index.html +1 -1
  71. package/apps/pwa/out/bots/index.txt +27 -26
  72. package/apps/pwa/out/chat/index.html +1 -1
  73. package/apps/pwa/out/chat/index.txt +27 -26
  74. package/apps/pwa/out/consult/index.html +1 -1
  75. package/apps/pwa/out/consult/index.txt +30 -29
  76. package/apps/pwa/out/control/index.html +1 -1
  77. package/apps/pwa/out/control/index.txt +30 -29
  78. package/apps/pwa/out/design-check/index.html +1 -1
  79. package/apps/pwa/out/design-check/index.txt +30 -29
  80. package/apps/pwa/out/editor/index.html +1 -1
  81. package/apps/pwa/out/editor/index.txt +30 -29
  82. package/apps/pwa/out/exec/index.html +1 -1
  83. package/apps/pwa/out/exec/index.txt +30 -29
  84. package/apps/pwa/out/field/index.html +1 -1
  85. package/apps/pwa/out/field/index.txt +30 -29
  86. package/apps/pwa/out/healthz +1 -1
  87. package/apps/pwa/out/index.html +1 -1
  88. package/apps/pwa/out/index.txt +27 -26
  89. package/apps/pwa/out/intake/index.html +1 -1
  90. package/apps/pwa/out/intake/index.txt +30 -29
  91. package/apps/pwa/out/live/index.html +1 -1
  92. package/apps/pwa/out/live/index.txt +30 -29
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +27 -26
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +27 -26
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +30 -29
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +30 -29
  101. package/apps/pwa/out/ops/checklist/index.html +1 -0
  102. package/apps/pwa/out/ops/checklist/index.txt +30 -0
  103. package/apps/pwa/out/ops/release/index.html +1 -0
  104. package/apps/pwa/out/ops/release/index.txt +30 -0
  105. package/apps/pwa/out/ops/seats/index.html +1 -0
  106. package/apps/pwa/out/ops/seats/index.txt +30 -0
  107. package/apps/pwa/out/reflection/index.html +1 -1
  108. package/apps/pwa/out/reflection/index.txt +28 -27
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +30 -29
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +27 -26
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +30 -29
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +30 -29
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +30 -29
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +30 -29
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +30 -29
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +27 -26
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +30 -29
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +30 -29
  129. package/apps/pwa/out/today/index.html +1 -0
  130. package/apps/pwa/out/today/index.txt +33 -0
  131. package/apps/pwa/out/trace/index.html +1 -1
  132. package/apps/pwa/out/trace/index.txt +30 -29
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +30 -29
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +30 -29
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +30 -29
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +30 -29
  143. package/graphs/field/field-feed.yaml +2 -0
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/graphs/release/recipes.yaml +3 -0
  146. package/graphs/release/release-loop.yaml +3 -1
  147. package/graphs/test-diet/nightly-audit.yaml +18 -0
  148. package/graphs/test-diet/recipes.yaml +3 -0
  149. package/package.json +1 -1
  150. package/plugins/card-followup/graphs/run-step.ts +48 -13
  151. package/plugins/doc-draft/graphs/doc-draft.yaml +3 -3
  152. package/plugins/doc-draft/graphs/run-step.ts +14 -7
  153. package/scripts/ci-pwa-build-gate.ts +160 -0
  154. package/scripts/coord-channel-watch.sh +42 -8
  155. package/scripts/coord-digest.ts +199 -0
  156. package/scripts/coord-post.sh +60 -3
  157. package/scripts/install.ps1 +68 -9
  158. package/scripts/install.sh +39 -12
  159. package/scripts/lib/pwa-scenarios/scenarios.ts +20 -0
  160. package/scripts/lib/tui-regress-checks.ts +146 -0
  161. package/scripts/memory-lifecycle-cycle.ts +2 -2
  162. package/scripts/ops-now-probe.ts +327 -0
  163. package/scripts/publish-internal-dist.ts +11 -1
  164. package/scripts/release-loop/cutoff.ts +31 -2
  165. package/scripts/release-loop/export-check-node.ts +85 -0
  166. package/scripts/release-loop/gate-node.ts +18 -4
  167. package/scripts/release-loop/node-verdict.ts +8 -1
  168. package/scripts/release-loop/ops-upgrade-node.ts +72 -16
  169. package/scripts/release-loop/release-readiness.ts +9 -0
  170. package/scripts/release-loop/version-node.ts +58 -10
  171. package/scripts/repo-watch-cycle.ts +1 -1
  172. package/scripts/retro-cycle.ts +2 -2
  173. package/scripts/schedule-health-report.ts +1 -1
  174. package/scripts/storyboard/storyboard.ts +5 -161
  175. package/scripts/test-diet/lib.ts +31 -1
  176. package/scripts/test-diet/node.ts +134 -17
  177. package/scripts/test-timeout-audit.ts +114 -0
  178. package/scripts/tree-sync-alert.ts +1 -1
  179. package/scripts/tui-regress.ts +71 -0
  180. package/scripts/waitlist-daily-digest.ts +1 -1
  181. package/skills/explainer-video/SKILL.md +4 -0
  182. package/skills/explainer-video/engine/reel.mjs +18 -13
  183. package/skills/explainer-video/engine/reel.sh +24 -5
  184. package/skills/explainer-video/engine/stitch.mjs +145 -0
  185. package/skills/pitch-deck/SKILL.md +36 -0
  186. package/skills/pitch-deck/template.html +80 -0
  187. package/src/acp/codex-approval-adapter.ts +9 -3
  188. package/src/acp/core-turn-bridge.ts +117 -18
  189. package/src/acp/fact-question.ts +11 -0
  190. package/src/acp/server.ts +48 -7
  191. package/src/acp/tool-approval.ts +126 -0
  192. package/src/acp/turn-runner.ts +36 -0
  193. package/src/agent/monad-agent-turn.ts +8 -0
  194. package/src/ask-user-question/pending-questions.ts +25 -10
  195. package/src/ask-user-question/types.ts +9 -0
  196. package/src/auth/setup-link-tokens.fixture.ts +19 -0
  197. package/src/auth/setup-link-tokens.ts +164 -0
  198. package/src/autopilot/build/integrity-gate.ts +12 -0
  199. package/src/boot/daemon-prompt-turn.ts +3 -0
  200. package/src/boot/daemon-tools/index.ts +8 -0
  201. package/src/boot/daemon-tools/types.ts +2 -0
  202. package/src/boot/daemon-tools/web-search.ts +1 -0
  203. package/src/budget/unified-usage.ts +75 -0
  204. package/src/channel-bot-token.ts +37 -10
  205. package/src/chat/ctrl-c-in-input.ts +14 -0
  206. package/src/chat/index.ts +61 -9
  207. package/src/chat/pickers/state.ts +6 -2
  208. package/src/cli/agent-cli.ts +7 -0
  209. package/src/cli/decisions-cli.ts +25 -3
  210. package/src/cli/doctor-bundle-upload.ts +68 -0
  211. package/src/cli/doctor-cli.ts +38 -4
  212. package/src/cli/op-proxy-sync.ts +83 -0
  213. package/src/cli/ops-cli.ts +20 -0
  214. package/src/cli/pod-cli.ts +49 -0
  215. package/src/cli/pr-cli.ts +37 -4
  216. package/src/cli/pwa-stop.ts +163 -13
  217. package/src/cli/pwa-test.ts +3 -0
  218. package/src/cli/release-cli.ts +98 -15
  219. package/src/cli/self-update.ts +14 -0
  220. package/src/cli/storyboard-cli.ts +37 -0
  221. package/src/cli/test-state-dir-flag.ts +11 -4
  222. package/src/connectors/linear.ts +146 -0
  223. package/src/context-bus/coord-events.ts +115 -0
  224. package/src/dashboard/dashboard-boot-fixture.ts +23 -0
  225. package/src/dashboard/dashboard-status-lines.ts +45 -0
  226. package/src/dashboard/first-screen-band.ts +35 -0
  227. package/src/dashboard/index.ts +128 -34
  228. package/src/dashboard/provider-slash-runtime.ts +28 -11
  229. package/src/dashboard/slash-runtime/dashboard-handlers.ts +17 -0
  230. package/src/dashboard/slash-runtime/help-from-registry.ts +43 -28
  231. package/src/dashboard/turn-stream-formatter.ts +1 -1
  232. package/src/decisions/decision-cards.ts +50 -2
  233. package/src/decisions/decision-ledger.ts +75 -4
  234. package/src/decisions/decision-linear-projection.ts +157 -0
  235. package/src/decisions/web-push-decision-cards.ts +50 -0
  236. package/src/directive/interpretation-card.ts +36 -0
  237. package/src/discord.ts +2 -1
  238. package/src/domains/coo-admin-tool.ts +62 -0
  239. package/src/domains/core-tools.ts +11 -3
  240. package/src/domains/elanous-card.ts +29 -0
  241. package/src/domains/ops-facts-tool.ts +76 -0
  242. package/src/domains/outbound-alert.ts +41 -13
  243. package/src/domains/release-tool.ts +168 -0
  244. package/src/domains/telegram-kind-route.ts +19 -1
  245. package/src/exec-requests/planner.ts +98 -28
  246. package/src/exec-requests/runner.ts +107 -10
  247. package/src/exec-requests/store.ts +8 -2
  248. package/src/field/field-media.ts +10 -7
  249. package/src/field/field-reel.ts +14 -4
  250. package/src/git-fs/worktree.ts +45 -6
  251. package/src/graph-runner/graph-approval-card.ts +113 -0
  252. package/src/graph-runner/graph-cli.ts +129 -4
  253. package/src/graph-runner/graph-notify.ts +25 -3
  254. package/src/graph-runner/runner.ts +142 -16
  255. package/src/harness/browser-verify.ts +25 -2
  256. package/src/harness/harness-cli-command.ts +231 -35
  257. package/src/harness/harness-incidents-writer.fixture.ts +24 -0
  258. package/src/harness/harness-incidents.ts +129 -0
  259. package/src/harness/harness-stop.ts +53 -3
  260. package/src/hitl/clarification-policy.ts +3 -1
  261. package/src/index.ts +172 -41
  262. package/src/intake-plane/author-goals.ts +37 -4
  263. package/src/intake-plane/check.ts +3 -0
  264. package/src/intake-plane/discord-seat-work.ts +47 -2
  265. package/src/intake-plane/seat-doc-route.ts +170 -6
  266. package/src/intake-plane/telegram-seat-work.ts +44 -2
  267. package/src/intake-plane/telegram-work.ts +1 -0
  268. package/src/maturity/cli-maturity.ts +34 -0
  269. package/src/maturity/feature-maturity.ts +100 -1
  270. package/src/maturity/tui-slash-maturity.ts +31 -0
  271. package/src/memory.ts +32 -4
  272. package/src/nexus/api/exec-requests.ts +33 -3
  273. package/src/nexus/api/http-server.ts +73 -1
  274. package/src/nexus/api/meta-api.ts +38 -0
  275. package/src/nexus/api/operator.ts +61 -0
  276. package/src/nexus/api/ops-api.ts +148 -0
  277. package/src/nexus/api/ops-seats-sources.ts +95 -0
  278. package/src/nexus/api/ops-seats.ts +124 -0
  279. package/src/nexus/api/public-routes.ts +14 -1
  280. package/src/nexus/api/seat-requests.ts +88 -8
  281. package/src/nexus/api/sessions-store.ts +63 -0
  282. package/src/nexus/api/setup-claim.ts +21 -0
  283. package/src/nexus/api/setup-codex-login.ts +160 -0
  284. package/src/nexus/api/usage-api.ts +102 -0
  285. package/src/nexus/config/secrets/file-backend.ts +56 -18
  286. package/src/nexus/index.ts +45 -4
  287. package/src/onboarding/first-run-doctor.ts +54 -0
  288. package/src/onboarding/first-run.ts +89 -0
  289. package/src/onboarding/lan-address.ts +45 -0
  290. package/src/onboarding/web-first.ts +119 -24
  291. package/src/onboarding.ts +4 -0
  292. package/src/ops-now/ops-now-refresh.ts +100 -0
  293. package/src/project/project-store.ts +115 -0
  294. package/src/release-loop/checklist.ts +76 -61
  295. package/src/release-loop/feature-store.ts +460 -0
  296. package/src/release-loop/manifest.ts +59 -5
  297. package/src/release-loop/release-schedule.ts +53 -0
  298. package/src/repl/index.ts +7 -0
  299. package/src/seat-dispatch/ceo-commands.ts +98 -0
  300. package/src/seat-dispatch/ceo-intent.ts +38 -0
  301. package/src/seat-loop/seat-loop.ts +231 -0
  302. package/src/seat-loop/seat-report.ts +64 -0
  303. package/src/self-dev/decompose.ts +66 -11
  304. package/src/self-dev/run-store.ts +10 -1
  305. package/src/self-implement/execution-clarification.ts +115 -0
  306. package/src/self-implement/gate-cli.ts +6 -3
  307. package/src/self-implement/orchestrator.ts +37 -3
  308. package/src/self-implement/pipeline-shape.ts +1 -1
  309. package/src/self-implement/run-status-mapping.ts +5 -3
  310. package/src/session/chat.ts +2 -0
  311. package/src/session/index.ts +9 -0
  312. package/src/shell-env-bootstrap.ts +17 -0
  313. package/src/skills/tools/usage-slash.ts +8 -7
  314. package/src/steward/directive.ts +51 -2
  315. package/src/steward/launch.ts +250 -0
  316. package/src/steward/outcome-notify.ts +115 -0
  317. package/src/steward/steward-cards.ts +8 -2
  318. package/src/steward/triage-plan.ts +32 -4
  319. package/src/steward/triage.ts +93 -9
  320. package/src/steward/working-backwards.ts +64 -0
  321. package/src/storyboard/storyboard.ts +141 -0
  322. package/src/task-cards/card-store.ts +2 -2
  323. package/src/task-orchestrator/surfaces/pod-lease.ts +260 -0
  324. package/src/telegram-card-followup.ts +233 -0
  325. package/src/telegram-commands.ts +16 -0
  326. package/src/telegram-report.ts +15 -9
  327. package/src/telegram-run.ts +90 -13
  328. package/src/telegram.ts +29 -8
  329. package/src/thinking-line.ts +28 -20
  330. package/src/tool-runtime/types.ts +4 -0
  331. package/src/tui/chat/execution-badge.ts +1 -1
  332. package/src/tui-client/dashboard-session.ts +2 -0
  333. package/src/user-config.ts +118 -11
  334. package/src/ux-sim/turn-states.ts +1 -1
  335. package/src/version/packed-revision.json +1 -1
  336. package/src/web-push/notify-exec-request.ts +32 -0
  337. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/4836-3084c58c1725a40b.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/approvals/page-9aa47b0bc6d570ff.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/app/intake/page-6a8158b71d1fe9d7.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/setup/page-085847b088675978.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +0 -1
  382. package/apps/pwa/out/_next/static/css/5ef72a618a8994f3.css +0 -1
  383. package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_buildManifest.js +0 -1
  384. package/src/dashboard/turn-typeahead-echo.ts +0 -23
  385. /package/apps/pwa/out/_next/static/{lywc8m_HDzH9MKSDQhyEj → 2DsiDfDCks44g1cPUMhiW}/_ssgManifest.js +0 -0
package/src/cli/pr-cli.ts CHANGED
@@ -13,6 +13,7 @@ import { runGate as runChangedTypecheckGate } from '../../scripts/ci-typecheck-c
13
13
  import { runIsolationHardcodeGate } from '../../scripts/ci-isolation-hardcode-gate.js';
14
14
  import { androidFilesIn, runAndroidUnitTestGate } from '../../scripts/ci-android-unit-tests.js';
15
15
  import { iosFilesIn, runIosUnitTestGate } from '../../scripts/ci-ios-unit-tests.js';
16
+ import { runPwaBuildGate } from '../../scripts/ci-pwa-build-gate.js';
16
17
  import { runMockModuleRestoreGate } from '../../scripts/ci-mock-module-restore-gate.js';
17
18
  import { runModelHardcodeGate } from '../../scripts/ci-model-hardcode-gate.js';
18
19
  import { runDaemonPortGate } from '../../scripts/ci-daemon-port-gate.js';
@@ -51,6 +52,7 @@ import {
51
52
  unionPaths,
52
53
  } from './pr-granularity.js';
53
54
  import { branchLineageSlug, findSiblingPrs } from './pr-lineage.js';
55
+ import { parseReleaseNoteSection } from '../release-loop/release-note.js';
54
56
 
55
57
  export { branchLineageSlug, findSiblingPrs };
56
58
 
@@ -114,7 +116,10 @@ export interface PrLandDeps {
114
116
  runAndroidGate?: (out: { log: (message: string) => void; error: (message: string) => void }, changedFiles: readonly string[]) => boolean;
115
117
  /** iOS 시험 게이트 심(시험 주입용). */
116
118
  runIosGate?: (out: { log: (message: string) => void; error: (message: string) => void }, changedFiles: readonly string[]) => boolean;
119
+ runPwaGate?: (out: { log: (message: string) => void; error: (message: string) => void }, changedFiles: readonly string[]) => boolean;
117
120
  isInteractive?: () => boolean;
121
+ /** Clock for the recent-landings window (test seam · default Date.now). */
122
+ now?: () => number;
118
123
  requestConfirmation?: (opts: ConfirmOpts) => Promise<ConfirmResult>;
119
124
  overlapConfirmChannels?: ConfirmChannel[];
120
125
  overlapConfirmTimeoutMs?: number;
@@ -404,13 +409,14 @@ function gateVerdict(
404
409
  label: string,
405
410
  gate: (out: { log: (message: string) => void; error: (message: string) => void }) => boolean,
406
411
  out: { log: (message: string) => void; error: (message: string) => void },
412
+ failClosed = false,
407
413
  ): { ok: boolean; measured: boolean } {
408
414
  try {
409
415
  return { ok: gate(out), measured: true };
410
416
  } catch (error) {
411
417
  out.error(`⚠ ${label}: 게이트가 «못 쟀다» — ${error instanceof Error ? error.message : String(error)}`);
412
- out.error(`⚠ ${label}: 「위반 없음」이 아니라 「검사 못 함」이다. 이 착지는 통과시키되 게이트를 고쳐라.`);
413
- return { ok: true, measured: false };
418
+ out.error(`⚠ ${label}: 「위반 없음」이 아니라 「검사 못 함」이다. ${failClosed ? '착지를 막는다.' : '이 착지는 통과시키되 게이트를 고쳐라.'}`);
419
+ return { ok: !failClosed, measured: false };
414
420
  }
415
421
  }
416
422
 
@@ -424,6 +430,21 @@ function record(step: string, ok: boolean, extra: Record<string, unknown> = {}):
424
430
  debug.log('pr.land', 'step', { step, ok, ...extra });
425
431
  }
426
432
 
433
+ export function nextMdWarning(body: string, changedFiles: readonly string[], out: { log: (message: string) => void }): void {
434
+ const section = body.split(/^## 릴리스 노트[ \t]*\r?$/m).at(-1)!;
435
+ const parsed = parseReleaseNoteSection(body);
436
+ const hasNote = parsed.fragment !== undefined || parsed.problems.length > 0;
437
+ const kind = parsed.fragment?.kind ?? (hasNote
438
+ ? /^[ \t]*- 종류:[ \t]*(feat|fix|security|internal)[ \t]*\r?$/m.exec(section.split(/^#{1,6}[ \t]+/m)[0]!)?.[1]
439
+ : undefined);
440
+ const reason = hasNote
441
+ ? (kind === 'feat' || kind === 'fix' || kind === 'security' ? `release-note:${kind}` : undefined)
442
+ : (changedFiles.some((file) => /^(?:apps\/pwa\/src\/|src\/cli\/|src\/dashboard\/)/.test(file)) ? 'user-facing-path' : undefined);
443
+ const warn = !!reason && !changedFiles.includes('release/next.md');
444
+ record('next-md', true, { warnOnly: true, reason: reason ?? (hasNote ? 'non-user-facing-note' : 'no-user-facing-path') });
445
+ if (warn) out.log('⚠ next-md: 사용자에게 보이는 변경인데 release/next.md 에 줄이 없습니다 — 공개 노트에서 빠집니다. 「- <kind> — <영어 한 문장>. Documentation: … Target: next.」 한 줄을 더하십시오.');
446
+ }
447
+
427
448
  type PrRemoteCapability = 'github' | 'local-path' | 'no-remote' | 'other';
428
449
 
429
450
  function isLocalRemote(raw: string): boolean {
@@ -822,11 +843,12 @@ function emitRecentLandingRateAdvisory(
822
843
  cwd: string,
823
844
  commits: readonly { subject: string; authorEmail: string; committedAtMs: number }[] | null,
824
845
  out: { log: (message: string) => void },
846
+ now: () => number = Date.now,
825
847
  ): boolean {
826
848
  const windowMinutes = RECENT_LANDING_WINDOW_MINUTES;
827
849
  const authorEmail = currentAuthorEmail(run, cwd);
828
850
  if (!authorEmail || commits === null) {
829
- const previous = commits === null ? undefined : previousLandingSummary(commits, Date.now());
851
+ const previous = commits === null ? undefined : previousLandingSummary(commits, now());
830
852
  record('recent-landing-rate', true, {
831
853
  recentCount: 0,
832
854
  windowMinutes,
@@ -836,7 +858,7 @@ function emitRecentLandingRateAdvisory(
836
858
  });
837
859
  return false;
838
860
  }
839
- const nowMs = Date.now();
861
+ const nowMs = now();
840
862
  const recentCount = countRecentLandings(commits, {
841
863
  authorEmail,
842
864
  nowMs,
@@ -1077,6 +1099,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1077
1099
  cwd,
1078
1100
  collectResolvedBaseLandings(run, cwd, DEFAULT_GRANULARITY_SINCE, base),
1079
1101
  out,
1102
+ deps.now,
1080
1103
  );
1081
1104
 
1082
1105
  // ⛔ local-path / no-remote 는 gh 가 원리상 못 한다. 이미 아는 자리에서 멈춘다.
@@ -1319,6 +1342,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1319
1342
  }
1320
1343
 
1321
1344
  const changedPaths = currentChangePaths(run, cwd, staged, base, out);
1345
+ nextMdWarning(body ?? '', changedPaths, out);
1322
1346
  publicLeakWarning(changedPaths, deps.runPublicLeakGate ?? runPrLandPublicLeakGate, out);
1323
1347
  if (!exportLeakBlock(changedPaths, () => (deps.runExportLeakCheck ?? runPrLandExportLeakCheck)(changedPaths ?? [], cwd), opts.allowPublicLeak === true, out)) return 1;
1324
1348
  if (publicDocPaths(changedPaths, () => true).length > 0) {
@@ -1367,6 +1391,15 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1367
1391
  out.log('✓ ios-gate: scripts/ci-ios-unit-tests.ts PASS — iOS 순수-로직 시험이 «실제로 돌았고» 실패 0.');
1368
1392
  }
1369
1393
 
1394
+ // PWA 는 bun test/tsc 밖의 정적 번들이다. 측정 불가도 착지 불가로 판정한다.
1395
+ const pwaGate = gateVerdict('pwa-gate', deps.runPwaGate
1396
+ ? (o) => deps.runPwaGate!(o, changedPaths)
1397
+ : (o) => runPwaBuildGate({ args: ['--changed-files', ...changedPaths], cwd, log: o.log, error: o.error }) === 0, out, true);
1398
+ record('pwa-gate', pwaGate.ok && pwaGate.measured, { measured: pwaGate.measured });
1399
+ if (!pwaGate.ok || !pwaGate.measured) {
1400
+ out.error('✗ pwa-gate: scripts/ci-pwa-build-gate.ts blocked pr land — PWA 빌드 실패 또는 측정 불가.');
1401
+ return 1;
1402
+ }
1370
1403
 
1371
1404
  const overlapFound = emitLandingOverlapAdvisory(run, cwd, base, changedPaths, out);
1372
1405
  const siblingFound = emitSiblingPrAdvisory(
@@ -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,8 +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
+ import * as features from '../release-loop/feature-store.js';
28
+ import { formatSchedule, getSchedule, listSchedules, setSchedule } from '../release-loop/release-schedule.js';
27
29
  import { writeStdoutJson } from './stdout-json.js';
30
+ import { CliUserError } from './cli-user-error.js';
28
31
  import { runUnattendedRelease, type UnattendedReleaseDeps } from '../../scripts/release-loop/unattended-release.js';
29
32
  import { releaseReadiness } from '../../scripts/release-loop/release-readiness.js';
30
33
  import { runReleaseIfReady } from './release-run-if-ready.js';
@@ -467,15 +470,23 @@ function checklistCodenames(): Record<string, string> {
467
470
  function checklistVersion(input: string | undefined, codenames: Record<string, string>): string {
468
471
  const v = input ?? devVersion().replace(/-dev\.\d+$/, '');
469
472
  const resolved = isReleaseVersion(v) ? v : Object.entries(codenames).find(([, name]) => name === v)?.[0] ?? v;
470
- if (!isReleaseVersion(resolved)) throw new Error(`체크리스트 판 또는 별칭이 아니다: ${v}`);
473
+ if (!isReleaseVersion(resolved)) throw new CliUserError(`체크리스트 판 또는 별칭이 아니다: ${v}`);
471
474
  return resolved;
472
475
  }
473
476
 
474
- async function checklistOutput(v: string, codenames: Record<string, string>, mode: 'list' | 'status', json?: boolean): Promise<void> {
475
- 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)) };
476
481
  const summary = summarizeChecklist(data);
477
- 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 });
478
488
  if (json) { await writeStdoutJson(`${JSON.stringify(result)}\n`); return; }
489
+ console.log(`⚠ 짝 경고 ${parity.length}: ${parity.map(({ id }) => id).join(', ') || '없음'}`);
479
490
  console.log(`${v}${result.codename ? ` (${result.codename})` : ''} · 공개 ${data.released || '없음'} · 개발 ${data.dev}`);
480
491
  if (mode === 'list') {
481
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}` : ''}`);
@@ -488,24 +499,44 @@ async function checklistOutput(v: string, codenames: Record<string, string>, mod
488
499
 
489
500
  export function registerReleaseCommands(program: Command, releaseRunDeps: UnattendedReleaseDeps = {}): void {
490
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
+ });
491
522
  const checklist = release.command('checklist').description('판별 확인표 조회·갱신')
492
523
  .option('--version <v>', '판 또는 별칭(기본: package.json 의 개발판에서 -dev.N 제거)')
493
524
  .option('--json', '결과 JSON');
494
525
  const context = (cmd: Command) => {
495
- const opts = { ...(cmd.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.opts() as { version?: string; json?: boolean }) };
526
+ const opts = { ...(cmd.parent?.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.opts() as { version?: string; json?: boolean }) };
496
527
  const codenames = checklistCodenames();
497
528
  return { version: checklistVersion(opts.version, codenames), codenames, json: opts.json };
498
529
  };
499
530
  const withContext = (cmd: Command) => cmd.option('--version <v>', '판 또는 별칭').option('--json', '결과 JSON');
500
- 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) => {
501
532
  const { version, codenames, json } = context(cmd);
502
- await checklistOutput(version, codenames, 'list', json);
533
+ await checklistOutput(version, codenames, 'list', json, opts.owner);
503
534
  });
504
- 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) => {
505
536
  const { version, codenames, json } = context(cmd);
506
- await checklistOutput(version, codenames, 'status', json);
537
+ await checklistOutput(version, codenames, 'status', json, opts.owner);
507
538
  });
508
- 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 … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 … 한 줄')
509
540
  .action((id: string, title: string, opts: { owner?: string; kind?: string }, cmd: Command) => {
510
541
  const { version, json } = context(cmd);
511
542
  const data = addItem(version, { id, title, ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}) });
@@ -513,15 +544,28 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
513
544
  });
514
545
  withContext(checklist.command('set <id>').description('칸 상태·근거·담당·처분 갱신'))
515
546
  .option('--status <status>', 'green|yellow|red|done').option('--evidence <evidence>', '근거').option('--owner <owner>', '담당')
516
- .option('--disposition <disposition>', 'move|known-issue|block').option('--kind <kind>', 'screen = 다섯 화면 짝 칸')
547
+ .option('--disposition <disposition>', 'move|known-issue|block').option('--kind <kind>', 'screen = 다섯 화면 짝 칸 · 근거에 짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 … 한 줄')
517
548
  .action((id: string, opts: { status?: string; evidence?: string; owner?: string; disposition?: string; kind?: string }, cmd: Command) => {
518
549
  const { version, json } = context(cmd);
519
- if (opts.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(opts.status)) throw new Error(`잘못된 상태: ${opts.status}`);
520
- if (opts.disposition !== undefined && !['move', 'known-issue', 'block'].includes(opts.disposition)) throw new Error(`잘못된 처분: ${opts.disposition}`);
521
- 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 중 하나');
522
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
+ }
523
560
  if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 갱신`);
524
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
+ });
525
569
  withContext(checklist.command('rm <id>').description('칸 삭제')).action((id: string, _opts: unknown, cmd: Command) => {
526
570
  const { version, json } = context(cmd);
527
571
  const data = removeItem(version, id, process.env.ELANOUS_TRACK || 'cli');
@@ -533,6 +577,45 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
533
577
  const data = seedFromRoadmap(version, readFileSync(opts.from, 'utf8'));
534
578
  if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${version} ${data.items.length}칸`);
535
579
  });
580
+ const actor = () => process.env.ELANOUS_TRACK || 'cli';
581
+ withContext(checklist.command('move <id>').description('칸을 한 트랜잭션으로 다른 판에 옮긴다'))
582
+ .requiredOption('--from <v>', '현재 판').requiredOption('--to <v>', '새 판')
583
+ .action((id: string, opts: { from: string; to: string }, cmd: Command) => {
584
+ const { codenames, json } = context(cmd);
585
+ const from = checklistVersion(opts.from, codenames), to = checklistVersion(opts.to, codenames);
586
+ features.move(id, from, to, actor());
587
+ if (json) console.log(JSON.stringify(listChecklist(to))); else console.log(`✅ ${id} ${from} → ${to}`);
588
+ });
589
+ withContext(checklist.command('retitle <id> <title>').description('칸 제목 수정')).action((id: string, title: string, _opts: unknown, cmd: Command) => {
590
+ const { version, json } = context(cmd);
591
+ const snapshot = listChecklist(version);
592
+ features.retitle(id, title, actor(), snapshot.released, snapshot.dev);
593
+ if (json) console.log(JSON.stringify(listChecklist(version))); else console.log(`✅ ${id} 제목 수정`);
594
+ });
595
+ const evidence = checklist.command('evidence').description('칸의 PR·커밋 근거');
596
+ withContext(evidence.command('add <id> <ref>').description('근거 참조 추가')).action((id: string, ref: string, _opts: unknown, cmd: Command) => {
597
+ const { version, json } = context(cmd);
598
+ const snapshot = listChecklist(version);
599
+ features.evidenceAdd(id, version, ref, actor(), snapshot.released, snapshot.dev);
600
+ if (json) console.log(JSON.stringify(features.history(id).map(decodeClaimHistoryEntry))); else console.log(`✅ ${id} 근거 ${ref}`);
601
+ });
602
+ withContext(checklist.command('history <id>').description('칸의 판 이동·변경 이력')).action((id: string, _opts: unknown, cmd: Command) => {
603
+ const { json } = context(cmd);
604
+ const rows = features.history(id).map(decodeClaimHistoryEntry);
605
+ if (json) { console.log(JSON.stringify(rows)); return; }
606
+ const info = features.details(id);
607
+ if (info) {
608
+ console.log(`${info.id} · ${info.title} · 담당 ${info.owner ?? '-'} · 종류 ${info.kind ?? '-'} · 처음 ${info.createdAt}`);
609
+ for (const ev of info.evidence) console.log(` 근거 ${ev.version} ${ev.ref} (${ev.at} ${ev.by})`);
610
+ }
611
+ for (const row of rows) console.log(`${row.at} ${row.version} ${row.by} ${row.field}: ${JSON.stringify(row.from)} → ${JSON.stringify(row.to)}`);
612
+ });
613
+ withContext(checklist.command('export').description('SQLite 에서 checklist.json 스냅샷 생성')).action((_opts: unknown, cmd: Command) => {
614
+ const { version, json } = context(cmd);
615
+ const snapshot = listChecklist(version);
616
+ features.exportJson(version, snapshot.released, snapshot.dev);
617
+ if (json) console.log(JSON.stringify(listChecklist(version))); else console.log(`✅ ${version} checklist.json 내보냄`);
618
+ });
536
619
  release.command('prepare')
537
620
  .description('깨끗한 원본 → 공개본 → 공개 저장소 이력 위 커밋 → PWA → 묶음·체크섬 → 로컬 끝까지 (네트워크 쓰기 없음)')
538
621
  .requiredOption('--version <x.y.z>', 'package.json 과 같은 버전(먼저 버전 PR 을 착지)')
@@ -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
  }