elanous 0.2.27 → 0.2.28

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 (285) 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/chunks/{1454-a9c338f5bb556f42.js → 1454-29d1d773d5996a49.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{599-a914cc0468f56143.js → 599-56f2c0a5cc4a7295.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/page-5ab1e47978577c5e.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-07110da72c45e68f.js → page-24453f6a302816b4.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/board/{page-d2d76d4a7eae91bd.js → page-6c131d30bb1cf871.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/page-fa46d4588e8442c2.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/bots/{page-e5a9302c105725a3.js → page-dc0772b11861af51.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/page-66ad46ed0279feb9.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/chat/page-5159dc8752c6cd29.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/consult/page-c5a2478861c08b87.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-f722a9dec084e08b.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/decisions/{page-6a16a48d1aa6e91f.js → page-256ffd365d32e100.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e0de78ae7b99f01c.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/editor/page-cfbdc44b708d9801.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/exec/{page-39f82acf619cca23.js → page-4c741eb760f2f77c.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/field/{page-7c2c8f81b185f9c0.js → page-e9ba98f5f1440a21.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/inside/{page-ae7aff07e32676bb.js → page-dafc901bb1b72b5f.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/intake/{page-482c60c1bc540d0a.js → page-2f6f303fa114ec24.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/{layout-7cb996c1aa08b6f5.js → layout-1634081c5272006c.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/live/{page-623e08fe4477d606.js → page-ced0bcfc6542b3fc.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live-run/{page-db460cd8aecb8490.js → page-bb1f05cd51c38944.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/loops/{page-ff27902d0b11ef4f.js → page-646b50f63b6fe732.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/market/page-db069b48a59c8c00.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-1917ea87ea71ab6b.js → page-f736324afa2117c1.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-25a302273b054fd7.js → page-6f131b51193a36f9.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/{not-found-670510eda8b9443f.js → not-found-435f5e2faf02bc01.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-497e0eedf20e7890.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a8d28fdf995e16ea.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4bb7556e2e97a750.js → page-8966452228753c5e.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fd5d18656cb432b6.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/page-5663779cb1a37609.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/{page-4eb96fc7241c2456.js → page-66a67eb19ebd976b.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/page-3b492aa78cf3c4a6.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-0d64ff212dc55d03.js → page-c71267e7681da982.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-7f096248993fc2d4.js → page-a21d1e1797172692.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-e47fc72049149a38.js → page-cb249e81ed339318.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-f7d808e31e005895.js → page-781cfc5114e097da.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-5c2b24b5496dad73.js → page-478157214db6a023.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/{page-76cdb531668b5191.js → page-86141f12cf090b7f.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/share/page-7256c73bb1c36751.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/page-5c738ca7adef3de5.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/page-0138c8091ec17224.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/term/page-cc2f24f16d85e735.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/today/{page-dab163b338e3b966.js → page-979d318a534e92da.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-182d394f7e6e096b.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-0260ffc7eda50db1.js → page-21050928a697ed7a.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-44b9d807005a9030.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-99c4e6c7345a9156.js → page-601fb77340e63c9a.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/page-75d3fb5d47af0edf.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-11bf9e5725a72c22.js +1 -0
  53. package/apps/pwa/out/approvals/index.html +1 -1
  54. package/apps/pwa/out/approvals/index.txt +13 -13
  55. package/apps/pwa/out/autopilot/index.html +1 -1
  56. package/apps/pwa/out/autopilot/index.txt +13 -13
  57. package/apps/pwa/out/board/index.html +1 -1
  58. package/apps/pwa/out/board/index.txt +13 -13
  59. package/apps/pwa/out/botlab/index.html +1 -1
  60. package/apps/pwa/out/botlab/index.txt +13 -13
  61. package/apps/pwa/out/bots/index.html +1 -1
  62. package/apps/pwa/out/bots/index.txt +13 -13
  63. package/apps/pwa/out/ceo/index.html +1 -1
  64. package/apps/pwa/out/ceo/index.txt +13 -13
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +13 -13
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +13 -13
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +13 -13
  71. package/apps/pwa/out/decisions/index.html +1 -1
  72. package/apps/pwa/out/decisions/index.txt +13 -13
  73. package/apps/pwa/out/design-check/index.html +1 -1
  74. package/apps/pwa/out/design-check/index.txt +13 -13
  75. package/apps/pwa/out/editor/index.html +1 -1
  76. package/apps/pwa/out/editor/index.txt +13 -13
  77. package/apps/pwa/out/exec/index.html +1 -1
  78. package/apps/pwa/out/exec/index.txt +13 -13
  79. package/apps/pwa/out/field/index.html +1 -1
  80. package/apps/pwa/out/field/index.txt +13 -13
  81. package/apps/pwa/out/healthz +1 -1
  82. package/apps/pwa/out/index.html +1 -1
  83. package/apps/pwa/out/index.txt +13 -13
  84. package/apps/pwa/out/inside/index.html +1 -1
  85. package/apps/pwa/out/inside/index.txt +13 -13
  86. package/apps/pwa/out/intake/index.html +1 -1
  87. package/apps/pwa/out/intake/index.txt +13 -13
  88. package/apps/pwa/out/live/index.html +1 -1
  89. package/apps/pwa/out/live/index.txt +13 -13
  90. package/apps/pwa/out/live-run/index.html +1 -1
  91. package/apps/pwa/out/live-run/index.txt +13 -13
  92. package/apps/pwa/out/loops/index.html +1 -1
  93. package/apps/pwa/out/loops/index.txt +13 -13
  94. package/apps/pwa/out/market/index.html +1 -1
  95. package/apps/pwa/out/market/index.txt +13 -13
  96. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  97. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  98. package/apps/pwa/out/morning/index.html +1 -1
  99. package/apps/pwa/out/morning/index.txt +13 -13
  100. package/apps/pwa/out/observatory/index.html +1 -1
  101. package/apps/pwa/out/observatory/index.txt +13 -13
  102. package/apps/pwa/out/ops/checklist/index.html +1 -1
  103. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  104. package/apps/pwa/out/ops/release/index.html +1 -1
  105. package/apps/pwa/out/ops/release/index.txt +13 -13
  106. package/apps/pwa/out/ops/seats/index.html +1 -1
  107. package/apps/pwa/out/ops/seats/index.txt +13 -13
  108. package/apps/pwa/out/outputs/index.html +1 -1
  109. package/apps/pwa/out/outputs/index.txt +13 -13
  110. package/apps/pwa/out/reflection/index.html +1 -1
  111. package/apps/pwa/out/reflection/index.txt +14 -14
  112. package/apps/pwa/out/scheduler/index.html +1 -1
  113. package/apps/pwa/out/scheduler/index.txt +13 -13
  114. package/apps/pwa/out/sessions/index.html +1 -1
  115. package/apps/pwa/out/sessions/index.txt +13 -13
  116. package/apps/pwa/out/settings/devices/index.html +1 -1
  117. package/apps/pwa/out/settings/devices/index.txt +13 -13
  118. package/apps/pwa/out/settings/index.html +1 -1
  119. package/apps/pwa/out/settings/index.txt +13 -13
  120. package/apps/pwa/out/setup/done/index.html +1 -1
  121. package/apps/pwa/out/setup/done/index.txt +13 -13
  122. package/apps/pwa/out/setup/index.html +1 -1
  123. package/apps/pwa/out/setup/index.txt +13 -13
  124. package/apps/pwa/out/share/index.html +1 -1
  125. package/apps/pwa/out/share/index.txt +13 -13
  126. package/apps/pwa/out/showroom/index.html +1 -1
  127. package/apps/pwa/out/showroom/index.txt +13 -13
  128. package/apps/pwa/out/tasks/index.html +1 -1
  129. package/apps/pwa/out/tasks/index.txt +13 -13
  130. package/apps/pwa/out/term/index.html +1 -1
  131. package/apps/pwa/out/term/index.txt +13 -13
  132. package/apps/pwa/out/today/index.html +1 -1
  133. package/apps/pwa/out/today/index.txt +13 -13
  134. package/apps/pwa/out/trace/index.html +1 -1
  135. package/apps/pwa/out/trace/index.txt +13 -13
  136. package/apps/pwa/out/vault/index.html +1 -1
  137. package/apps/pwa/out/vault/index.txt +13 -13
  138. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  139. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  140. package/apps/pwa/out/workflows/index.html +1 -1
  141. package/apps/pwa/out/workflows/index.txt +13 -13
  142. package/apps/pwa/out/workspace/index.html +1 -1
  143. package/apps/pwa/out/workspace/index.txt +13 -13
  144. package/apps/pwa/out/worktrees/index.html +1 -1
  145. package/apps/pwa/out/worktrees/index.txt +13 -13
  146. package/graphs/implement-loop.yaml +38 -1
  147. package/graphs/steward/recipes.yaml +6 -0
  148. package/graphs/steward/release-day.yaml +23 -0
  149. package/graphs/steward/steward.yaml +9 -1
  150. package/graphs/work/code-pr.yaml +23 -0
  151. package/package.json +1 -1
  152. package/scripts/browser-observe-cycle.ts +76 -2
  153. package/scripts/ci-typecheck-changed.ts +50 -17
  154. package/scripts/detect-test-interference.ts +31 -1
  155. package/scripts/release-loop/docs-node.ts +51 -22
  156. package/scripts/release-loop/gate-node.ts +37 -4
  157. package/scripts/release-loop/node-verdict.ts +3 -1
  158. package/scripts/release-loop/publish-node.ts +3 -0
  159. package/scripts/release-loop/tui-node-checks.ts +5 -2
  160. package/scripts/skills/public-check.ts +5 -2
  161. package/scripts/test-deterministic.ts +520 -37
  162. package/scripts/ux-sim.ts +7 -1
  163. package/scripts/verify-kpack6-live.ts +230 -0
  164. package/src/acp/codex-plugins.ts +17 -0
  165. package/src/agent/task-notification.ts +2 -1
  166. package/src/agent-mission/in-agent-plugin.ts +8 -3
  167. package/src/agent-substrate/self-review-cli.ts +34 -10
  168. package/src/briefing/brief-items.ts +151 -35
  169. package/src/budget/llm-cost.ts +25 -2
  170. package/src/budget/usage-outcomes.ts +32 -0
  171. package/src/claims/claims-ledger.ts +23 -0
  172. package/src/claims/claims-render.ts +22 -2
  173. package/src/cli/brief-cli.ts +131 -10
  174. package/src/cli/claims-cli.ts +24 -4
  175. package/src/cli/msg-cli.ts +165 -9
  176. package/src/cli/plugin-cli.ts +11 -3
  177. package/src/cli/pr-cli.ts +80 -18
  178. package/src/cli/pr-land-refusal.ts +20 -0
  179. package/src/cli/release-cli.ts +2 -0
  180. package/src/cli/rfc-cli.ts +24 -0
  181. package/src/cli/tailscale-serve.ts +5 -8
  182. package/src/cli/tasks-cli.ts +393 -26
  183. package/src/cli/usage-cli.ts +9 -4
  184. package/src/dashboard/first-screen-band.ts +63 -13
  185. package/src/dashboard/index.ts +8 -3
  186. package/src/dashboard/skill-detection-runtime.ts +24 -4
  187. package/src/domains/morning-report.ts +3 -1
  188. package/src/github/pr-labels.ts +10 -4
  189. package/src/graph-runner/runner.ts +101 -14
  190. package/src/harness/harness-clean.ts +21 -3
  191. package/src/harness/harness-cli-command.ts +22 -6
  192. package/src/harness/harness-pod-dispatch.ts +11 -4
  193. package/src/hq/seats.ts +17 -3
  194. package/src/index.ts +34 -5
  195. package/src/intelligence-map/model-bench-scores.ts +102 -0
  196. package/src/loops/orchestrator/inbox.ts +73 -0
  197. package/src/loops/orchestrator/resync-shadow.ts +100 -12
  198. package/src/loops/orchestrator/tick.ts +73 -2
  199. package/src/maturity/feature-maturity.ts +1 -1
  200. package/src/msg/msg-sse-server.ts +141 -0
  201. package/src/msg/msg-store.ts +84 -14
  202. package/src/patents/ledger.ts +128 -0
  203. package/src/plugins/install/market-fetch.ts +32 -1
  204. package/src/pod-lease/admission.ts +116 -41
  205. package/src/release-loop/checklist.ts +11 -0
  206. package/src/release-loop/merged-pr-checklist.ts +24 -5
  207. package/src/release-loop/rubric.ts +70 -0
  208. package/src/rfc-registry/registry.ts +88 -0
  209. package/src/seat-dispatch/seat-request-ledger.ts +4 -0
  210. package/src/seat-loop/knowhow-lint.ts +233 -0
  211. package/src/seat-loop/seat-pack.ts +16 -4
  212. package/src/seat-loop/traffic-signal.ts +7 -0
  213. package/src/self-dev/ask-launch-flow.ts +48 -8
  214. package/src/self-dev/ask-path-candidates.ts +69 -14
  215. package/src/self-dev/decompose-proposal.ts +43 -1
  216. package/src/self-dev/graph-journey-nodes.ts +2 -0
  217. package/src/self-dev/launch-preflight-text.ts +5 -1
  218. package/src/self-dev/launch-preflight.ts +143 -10
  219. package/src/self-implement/gate-cli.ts +6 -2
  220. package/src/self-implement/goal-author.ts +2 -1
  221. package/src/self-implement/goal-new-paths.ts +4 -5
  222. package/src/self-implement/graph-authority.ts +17 -3
  223. package/src/self-implement/graph-yaml.ts +15 -7
  224. package/src/self-implement/headless-elanous-driver.ts +16 -1
  225. package/src/self-implement/host-regate.ts +44 -9
  226. package/src/self-implement/orchestrator.ts +3 -10
  227. package/src/self-implement/rework-policy.ts +80 -2
  228. package/src/self-implement/speed-claims.ts +145 -6
  229. package/src/skills/tools/persistent-grounding.ts +59 -4
  230. package/src/skills/tools/shell.ts +79 -1
  231. package/src/steward/rejudge-tick.ts +12 -1
  232. package/src/steward/release-day.ts +158 -0
  233. package/src/steward/resync-tick.ts +123 -0
  234. package/src/task-agent/actions.ts +1 -0
  235. package/src/task-agent/adopt-pr.ts +140 -0
  236. package/src/task-agent/board.ts +11 -1
  237. package/src/task-agent/card-evidence.ts +37 -5
  238. package/src/task-agent/cover.ts +84 -17
  239. package/src/task-agent/failure-notify.ts +61 -0
  240. package/src/task-agent/green-sweep.ts +170 -0
  241. package/src/task-agent/land-worktree.ts +52 -5
  242. package/src/task-agent/live-move-history.ts +1 -0
  243. package/src/task-agent/live-moves.ts +43 -12
  244. package/src/task-agent/parent-host.ts +1 -1
  245. package/src/task-agent/postland-card.ts +139 -0
  246. package/src/task-agent/rejudge-on-head.ts +106 -7
  247. package/src/task-agent/remote-rejudge-bind.ts +62 -0
  248. package/src/task-agent/repair-branch-cas.ts +182 -0
  249. package/src/task-agent/repair-cycle.ts +275 -0
  250. package/src/task-agent/repair-next-move.ts +31 -0
  251. package/src/task-agent/repair-review.ts +24 -0
  252. package/src/task-agent/repair-worker.ts +87 -0
  253. package/src/task-agent/review-result-capture.ts +2 -1
  254. package/src/task-agent/shadow.ts +13 -7
  255. package/src/task-agent/task-hand.ts +115 -6
  256. package/src/task-orchestrator/surfaces/pod-repair-commit.ts +96 -0
  257. package/src/task-orchestrator/surfaces/self-implement-pod.ts +70 -12
  258. package/src/telegram.ts +30 -1
  259. package/src/user-config.ts +8 -1
  260. package/src/ux-sim/fault-sqlite-busy.ts +50 -0
  261. package/src/version/packed-revision.json +1 -1
  262. package/apps/pwa/out/_next/static/chunks/app/approvals/page-a1ce306364f08da9.js +0 -1
  263. package/apps/pwa/out/_next/static/chunks/app/botlab/page-30007380f2e2a3c1.js +0 -1
  264. package/apps/pwa/out/_next/static/chunks/app/ceo/page-b93c04b5311c97ec.js +0 -1
  265. package/apps/pwa/out/_next/static/chunks/app/chat/page-718a49e86bf25f9c.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/app/consult/page-20662900d6b4b455.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/app/control/page-f707a509515ac50c.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/app/design-check/page-fcb06e305f515527.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/editor/page-6acf5ec4e6c6d1af.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/market/page-8397a5c6ca569bf4.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/observatory/page-d73c3f1e00d93986.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-65caacf54389081e.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-77e04b4f8762c666.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/outputs/page-15c0a4095baddfba.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/reflection/page-63e272a4cd3b6634.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/share/page-e2b63345bba1a11f.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/showroom/page-d376e0c83edc041a.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d6db55fe2c04d128.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/term/page-87b88f551f421d8b.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/trace/page-08019efef111ed2e.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-980ce6fe36ad95eb.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/workspace/page-0ab42e0f05823013.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ad9220422d20ba33.js +0 -1
  284. /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_buildManifest.js +0 -0
  285. /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_ssgManifest.js +0 -0
@@ -4,7 +4,15 @@
4
4
  // 기본 MCP 전송은 기존 McpClient 의 initialize + Mcp-Session-Id 수명주기를 탄다.
5
5
  // 스케줄러 진입점 배선은 다음 조각의 몫이다. 이 파일은 호출 가능한 함수만 내보낸다.
6
6
 
7
- import { McpClient, McpServerError, type McpHttpFetch } from '../src/mcp/client.js';
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import {
10
+ McpClient,
11
+ McpConnectionError,
12
+ McpServerError,
13
+ type McpHttpFetch,
14
+ } from '../src/mcp/client.js';
15
+ import { getElanousConfigDir } from '../src/elanous-config-dir.js';
8
16
  import { debug } from '../src/debug/log.js';
9
17
 
10
18
  export const ELANOUS_PWA_URL = 'http://127.0.0.1:31415/app';
@@ -21,6 +29,10 @@ export const BROWSER_OBSERVE_OK = 'ok';
21
29
  export const BROWSER_OBSERVE_WRONG_SCREEN = 'wrong-screen';
22
30
  /** 관측하지 못했다 */
23
31
  export const BROWSER_OBSERVE_UNOBSERVED = 'unobserved';
32
+ /** `unobserved` 사유 — 루프백 owner 토큰을 못 읽어 Authorization 헤더를 못 붙였다 */
33
+ export const BROWSER_OBSERVE_REASON_AUTH_MISSING = 'auth-missing';
34
+ /** `unobserved` 사유 — 루프백 owner 토큰을 붙였는데 서버가 401 로 거부했다 */
35
+ export const BROWSER_OBSERVE_REASON_AUTH_REJECTED = 'auth-rejected';
24
36
 
25
37
  export type BrowserObserveStatus =
26
38
  | typeof BROWSER_OBSERVE_OK
@@ -50,6 +62,39 @@ export interface BrowserObserveResult extends BrowserObserveClassification {
50
62
  request: BrowserObserveMcpRequest;
51
63
  }
52
64
 
65
+ /** 루프백 owner 토큰을 어디서 읽는지 — `src/cli/mcp-reload.ts` 의 기존 꼴과 같은 자리다. */
66
+ export function browserObserveTokenPath(): string {
67
+ return join(getElanousConfigDir(), 'acp-token');
68
+ }
69
+
70
+ /**
71
+ * 루프백 ACP 토큰을 **읽기 전용**으로 얻는다(실패는 null).
72
+ * ⛔ `ensureAuthToken`(src/auth/acp-token.ts) 을 쓰지 않는다 — 그것은 «없으면 만든다».
73
+ * 이 조각은 새 토큰 생성 0: 파일이 없거나 비면 그대로 auth-missing 이다.
74
+ */
75
+ export function readBrowserObserveToken(): string | null {
76
+ const tokenPath = browserObserveTokenPath();
77
+ try {
78
+ if (!existsSync(tokenPath)) return null;
79
+ const token = readFileSync(tokenPath, 'utf8').trim();
80
+ return token.length > 0 ? token : null;
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+
86
+ /** MCP POST 전부에 `Authorization: Bearer <token>` 을 얹는 전송 래퍼. */
87
+ export function withBrowserObserveAuthorization(
88
+ fetchImpl: BrowserObserveHttpFetch,
89
+ token: string,
90
+ ): BrowserObserveHttpFetch {
91
+ return (url, init) =>
92
+ fetchImpl(url, {
93
+ ...init,
94
+ headers: { ...init.headers, authorization: `Bearer ${token}` },
95
+ });
96
+ }
97
+
53
98
  export type BrowserObserveMcpCall = (request: BrowserObserveMcpRequest) => Promise<unknown>;
54
99
  export type BrowserObserveLog = (category: string, event: string, data?: unknown) => void;
55
100
  export type BrowserObserveHttpFetch = McpHttpFetch;
@@ -58,6 +103,8 @@ export interface BrowserObserveDeps {
58
103
  mcpCall?: BrowserObserveMcpCall;
59
104
  fetch?: BrowserObserveHttpFetch;
60
105
  log?: BrowserObserveLog;
106
+ /** 토큰 획득 주입 자리 — 시험이 파일 시스템 없이 결정성을 얻는다. 생략하면 `readBrowserObserveToken` */
107
+ readToken?: () => string | null;
61
108
  }
62
109
 
63
110
  /**
@@ -328,6 +375,15 @@ function emit(
328
375
  }
329
376
 
330
377
  function unobservedFromCaught(caught: unknown): BrowserObserveClassification {
378
+ // 루프백 토큰을 붙였는데 서버가 401 로 거부한 경우 — McpClient 가
379
+ // McpConnectionError('auth-required') 로 던진다. 긴 오류 문자열 대신 이유 한 칸으로 가른다.
380
+ if (caught instanceof McpConnectionError && caught.reason === 'auth-required') {
381
+ return {
382
+ status: BROWSER_OBSERVE_UNOBSERVED,
383
+ observedUrl: null,
384
+ reason: BROWSER_OBSERVE_REASON_AUTH_REJECTED,
385
+ };
386
+ }
331
387
  if (caught instanceof McpServerError) {
332
388
  return {
333
389
  status: BROWSER_OBSERVE_UNOBSERVED,
@@ -342,14 +398,32 @@ function unobservedFromCaught(caught: unknown): BrowserObserveClassification {
342
398
  };
343
399
  }
344
400
 
401
+ /** 기본 전송(McpClient) 에 루프백 토큰을 얹는다. 토큰이 없으면 null — 401 이 보장된 호출은 아예 안 한다. */
402
+ function defaultAuthorizedMcpCall(deps: BrowserObserveDeps): BrowserObserveMcpCall | null {
403
+ const token = deps.readToken ? deps.readToken() : readBrowserObserveToken();
404
+ if (token === null) return null;
405
+ const base: BrowserObserveHttpFetch = deps.fetch ?? ((url, init) => fetch(url, init));
406
+ const authorized = withBrowserObserveAuthorization(base, token);
407
+ return (request) => callAsideReplViaMcpClient(request, authorized);
408
+ }
409
+
345
410
  /** MCP 로 브라우저에서 값을 가져와 공개 분류 함수에 넘긴다. 의존성 주입으로 실 데몬 없이 검증한다. */
346
411
  export async function observeBrowserCycle(
347
412
  deps: BrowserObserveDeps = {},
348
413
  ): Promise<BrowserObserveResult> {
349
414
  if (!deps.log) await ensureBrowserObserveLogSink();
350
415
  const log = deps.log ?? defaultLog;
351
- const mcpCall = deps.mcpCall ?? ((request) => callAsideReplViaMcpClient(request, deps.fetch));
352
416
  const request = buildBrowserObserveMcpRequest();
417
+ const mcpCall = deps.mcpCall ?? defaultAuthorizedMcpCall(deps);
418
+ if (mcpCall === null) {
419
+ const classified: BrowserObserveClassification = {
420
+ status: BROWSER_OBSERVE_UNOBSERVED,
421
+ observedUrl: null,
422
+ reason: BROWSER_OBSERVE_REASON_AUTH_MISSING,
423
+ };
424
+ emit(log, classified);
425
+ return { ...classified, request };
426
+ }
353
427
  try {
354
428
  const response = await mcpCall(request);
355
429
  const rpcError = jsonRpcErrorMessage(response);
@@ -759,7 +759,7 @@ export function exportedLiteralUnionMembersInChanges(
759
759
  pathsResult: ChangedPathResult = changedPathPairs(base),
760
760
  readBase: GitFileReader = readGitFile,
761
761
  readHead = (file: string) => readFileSync(file, 'utf8'),
762
- sources?: RepositorySources,
762
+ sources?: RepositorySources | (() => RepositorySources),
763
763
  ): LiteralUnionMemberCheck {
764
764
  if (pathsResult.failure) return { additions: [], failure: pathsResult.failure };
765
765
  const additions: LiteralUnionAddition[] = [];
@@ -780,7 +780,7 @@ export function exportedLiteralUnionMembersInChanges(
780
780
  }
781
781
  if (removals.length === 0) return { additions };
782
782
  try {
783
- return { additions, removals: literalUnionReferencesInSource(removals, changed, sources ?? repositoryTypeScriptSources()) };
783
+ return { additions, removals: literalUnionReferencesInSource(removals, changed, typeof sources === 'function' ? sources() : sources ?? repositoryTypeScriptSources()) };
784
784
  } catch (error: unknown) {
785
785
  return { additions, failure: `저장소 리터럴 참조 탐색 실패: ${error instanceof Error ? error.message : String(error)}` };
786
786
  }
@@ -873,6 +873,7 @@ export type GateIo = {
873
873
  literalUnionMembers?: (changed: ReadonlySet<string>, base: string) => LiteralUnionMember[] | LiteralUnionMemberCheck;
874
874
  readBaselineDiagnostics?: (base: string, command: string) => BaselineDiagnostics;
875
875
  runTscCmd: (cmd: string) => TscRun;
876
+ resolveTscScript?: (cwd: string) => string | undefined;
876
877
  /** Landing tree. Git, existsSync, tsc, and the test baseline are measured here — not in process.cwd(). */
877
878
  cwd?: string;
878
879
  /** Explicit base revision. Same rule as TSC_BASE_REF, without reading the process environment. */
@@ -982,16 +983,30 @@ export function planRootCheck(input: {
982
983
  if (roots.length > TSC_GATE_ROOT_LIMIT) return { mode: 'full', fallbackReason: `루트 ${roots.length}개 > 상한 ${TSC_GATE_ROOT_LIMIT}`, roots: roots.length };
983
984
  return { mode: 'affected', roots, changedCount: gateChanged.size };
984
985
  }
985
- /**
986
- * K-OP-039 · 노하우를 «도구 출력»에 싣는 첫 조각(대표 2026-10-12 «어떤 에이전트가 와도 같은 수준»).
987
- * 승격된 전체 판정은 `apps/pwa/node_modules` 가 없으면 «가짜» PWA 타입 에러만 낸다 —
988
- * 10-12 하루 TC·UX 두 자리가 같은 가짜 TS2322(ChatMessage.tsx)로 코드를 고치려 했다.
989
- * 카드(내부 문서 `KNOWHOW` K-OP-039)의 처방 문면을 그대로 도구 출력에 싣는다.
990
- */
991
- const PWA_DEPS_MISSING_HINT = '환경: apps/pwa 의존성 없음 — (cd apps/pwa && bun install --frozen-lockfile) 뒤 재실행 · 코드 결함 아님(K-OP-039)';
986
+ /** Target checkout first, then the installed CLI's top-level and nested dependencies. */
987
+ export function resolveTscScript(cwd: string, scriptsDir = import.meta.dir, present: (path: string) => boolean = existsSync): string | undefined {
988
+ return [
989
+ join(cwd, 'node_modules/typescript/bin/tsc'),
990
+ join(scriptsDir, '../../typescript/bin/tsc'),
991
+ join(scriptsDir, '../node_modules/typescript/bin/tsc'),
992
+ ].find(present);
993
+ }
994
+
995
+ /** The installed compiler cannot resolve declarations from a checkout without its own dependencies. */
996
+ export function missingCheckoutTypeDependencies(cwd: string, needsPwa = false, present: (path: string) => boolean = existsSync): string[] {
997
+ const missing: string[] = [];
998
+ if (!present(join(cwd, 'node_modules'))) missing.push('node_modules');
999
+ if (!present(join(cwd, 'node_modules/bun-types'))) missing.push('bun-types');
1000
+ if (needsPwa && !present(join(cwd, 'apps/pwa/node_modules'))) missing.push('apps/pwa/node_modules');
1001
+ return missing;
1002
+ }
992
1003
 
993
1004
  export function runGate(io: Partial<GateIo> = {}): void {
994
1005
  const cwd = io.cwd ?? process.cwd();
1006
+ const targetGit: GitRunner = (command, args, options) => execFileSync(command, args, { ...options, cwd, env: gitEnv() });
1007
+ const targetPaths = (base: string) => changedPathPairs(base, targetGit);
1008
+ const targetFile = (ref: string, path: string) => readGitFile(ref, path, targetGit);
1009
+ const targetSource = (file: string) => readFileSync(join(cwd, file), 'utf8');
995
1010
  const defaultChanged = () => changedTsFiles((cmd) => sh(cmd, cwd), process.env, cwd, io.baseRef);
996
1011
  const defaultRunTsc = (cmd: string) => runTsc(cmd, ((command, options) => execSync(command, { ...options, cwd, env: tscExecEnv(options?.env) })) as typeof execSync);
997
1012
  const { changedTsFiles: getChanged = defaultChanged, runTscCmd = defaultRunTsc, log = (message: string) => console.log(message), warn = (message: string) => console.warn(message), error = (message: string) => console.error(message), exit = ((code: number) => process.exit(code)) as GateIo['exit'] } = io;
@@ -999,10 +1014,10 @@ export function runGate(io: Partial<GateIo> = {}): void {
999
1014
  // Set 주입은 기존 테스트 심이며 기준 revision이 없다. 실제 ChangedTsFiles 경로만 Git 관측을 요구한다.
1000
1015
  const requiredExportFields = io.requiredExportFields ?? ((changed: ReadonlySet<string>, base: string) => base === 'injected test seam'
1001
1016
  ? { fields: [], removedSymbols: [], parameterIncreases: [] }
1002
- : requiredExportFieldsInChanges(changed, base));
1017
+ : requiredExportFieldsInChanges(changed, base, targetPaths(base), targetFile, targetSource));
1003
1018
  const literalUnionMembers = io.literalUnionMembers ?? ((changed: ReadonlySet<string>, base: string) => base === 'injected test seam'
1004
1019
  ? { additions: [] }
1005
- : exportedLiteralUnionMembersInChanges(changed, base));
1020
+ : exportedLiteralUnionMembersInChanges(changed, base, targetPaths(base), targetFile, targetSource, () => repositoryTypeScriptSources(cwd)));
1006
1021
  const changedResult = getChanged();
1007
1022
  const { files: changed, base, baseSource, head, diffFiles, warnings = [], failures = [] } = changedResult instanceof Set
1008
1023
  ? { files: changedResult, base: 'injected test seam', baseSource: 'injected test seam', head: 'injected test seam', diffFiles: new Set<string>(), warnings: [], failures: [] }
@@ -1099,15 +1114,33 @@ export function runGate(io: Partial<GateIo> = {}): void {
1099
1114
  }, { level: 'info' });
1100
1115
  log(`[tsc-gate] root 설정 범위 ${rootScope.size}개 관측 (apps/pwa/** 제외).`);
1101
1116
  if (runsPwa) log(`[tsc-gate] PWA 설정 범위 ${pwaScope.size}개 관측.`);
1102
- // ⛔ K-OP-039: 승격된 전체 판정은 apps/pwa 의존성 없이는 «가짜» PWA 타입 에러만 낸다(환경 문제 · 코드 결함 아님).
1103
- // 그 오류를 내기 «전에» 이 한 줄로 멈춘다(환경 줄이 곧 마지막 줄 = 판정) — 의존성이 있으면 아래
1104
- // 판정 규칙·출력 형식은 기존 그대로다. «못 쟀다 ≠ 통과» — PWA 미측정을 통과로 접지 않는다.
1105
- if (wholeRepository && !existsSync(join(cwd, 'apps/pwa/node_modules'))) {
1106
- error(`[tsc-gate] ${PWA_DEPS_MISSING_HINT}`);
1117
+ // K-OP-039: PWA checks without app dependencies produce fake type errors.
1118
+ // The target checkout's declarations are required even when a compiler is
1119
+ // supplied by the installed CLI or injected by another gate caller.
1120
+ const missing = missingCheckoutTypeDependencies(cwd, runsPwa || wholeRepository);
1121
+ if (missing.length > 0) {
1122
+ const quotedCwd = `'${cwd.replaceAll("'", "'\\''")}'`;
1123
+ const install = `cd ${quotedCwd} && bun install --frozen-lockfile${missing.includes('apps/pwa/node_modules') ? ' && (cd apps/pwa && bun install --frozen-lockfile)' : ''}`;
1124
+ error(`[tsc-gate] ⛔ 대상 트리 타입 의존성 없음: ${missing.join(', ')} — ${install}`);
1125
+ error('[tsc-gate] FAIL — 대상 의존성을 확인하지 못했다(설치본 tsc 존재 ≠ 대상 타입 검사 가능).');
1126
+ exit(1);
1127
+ return;
1128
+ }
1129
+ // The .bin shim uses `#!/usr/bin/env node`. A LaunchAgent can have Bun but no
1130
+ // node on PATH, so invoke the installed compiler with this process's absolute
1131
+ // runtime instead. A missing dependency must fail the gate, never fetch via npx.
1132
+ // Preserve the target checkout's compiler version when available. The
1133
+ // installed CLI may resolve TypeScript from a sibling top-level node_modules
1134
+ // or its own nested node_modules; verify each path before starting a child.
1135
+ const tscScript = (io.resolveTscScript ?? resolveTscScript)(cwd);
1136
+ if (!tscScript) {
1137
+ error('[tsc-gate] ⛔ TypeScript 실행 파일 없음 — 대상 트리 또는 설치본에 bun install --frozen-lockfile 뒤 재실행하십시오.');
1138
+ error('[tsc-gate] FAIL — tsc 를 실행하지 못했다(의존성 없음 ≠ 진단 0건).');
1107
1139
  exit(1);
1108
1140
  return;
1109
1141
  }
1110
- const tscBin = existsSync(join(cwd, 'node_modules/.bin/tsc')) ? 'node_modules/.bin/tsc' : 'npx tsc';
1142
+ const shellArg = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
1143
+ const tscBin = `${shellArg(process.execPath)} ${shellArg(tscScript)}`;
1111
1144
  const baseline = readTestTypecheckBaseline(cwd);
1112
1145
  const rootCommand = `${tscBin} --noEmit -p ${TYPECHECK_GATE_CONFIG}`;
1113
1146
  const pwaCommand = `${tscBin} --noEmit -p apps/pwa/tsconfig.json`;
@@ -26,6 +26,9 @@ export type MeasuredRun = {
26
26
  fail: number | null;
27
27
  status: 'measured' | 'unmeasurable';
28
28
  reason?: string;
29
+ /** Names observed in Bun's failed-test output. Absent names must not be inferred from the selected files. */
30
+ failedFiles?: string[];
31
+ failedTests?: string[];
29
32
  };
30
33
 
31
34
  export type InterferenceReport = {
@@ -61,6 +64,32 @@ export function parseFailureCount(output: string): number | null {
61
64
  return Number(failures[0]![1]);
62
65
  }
63
66
 
67
+ /** Bun prints a file heading before its `(fail) name` lines. Only retain names actually printed by that run. */
68
+ export function parseFailedTestNames(output: string): { files: string[]; tests: string[] } {
69
+ const files = new Set<string>();
70
+ const tests = new Set<string>();
71
+ let currentFile: string | undefined;
72
+ for (const raw of output.replace(/\x1b\[[0-9;]*m/g, '').split(/\r?\n/)) {
73
+ const line = raw.trim();
74
+ if (/^(?:\d+\s+(?:pass|fail|skip)|Ran\s+\d+\s+tests?|Bun test v)/i.test(line)) { currentFile = undefined; continue; }
75
+ const failure = /^\(fail\)\s+(.+?)(?:\s+\[[^\]]+\])?$/.exec(line);
76
+ if (failure) {
77
+ tests.add(failure[1]!.trim());
78
+ if (currentFile) files.add(currentFile);
79
+ continue;
80
+ }
81
+ const heading = /^(.+):$/.exec(line);
82
+ if (heading) currentFile = /\.(?:test|spec)\.[cm]?[jt]sx?$/.test(heading[1]!) ? heading[1] : undefined;
83
+ }
84
+ return { files: [...files], tests: [...tests] };
85
+ }
86
+
87
+ export function failedNamesFromStreams(result: RunnerResult): { files: string[]; tests: string[] } {
88
+ const stdout = parseFailedTestNames(result.stdout);
89
+ const stderr = parseFailedTestNames(result.stderr);
90
+ return { files: [...new Set([...stdout.files, ...stderr.files])], tests: [...new Set([...stdout.tests, ...stderr.tests])] };
91
+ }
92
+
64
93
  async function measure(files: readonly string[], runner: TestRunner): Promise<MeasuredRun> {
65
94
  try {
66
95
  const result = await runner(files);
@@ -68,9 +97,10 @@ async function measure(files: readonly string[], runner: TestRunner): Promise<Me
68
97
  return { files: [...files], fail: null, status: 'unmeasurable', reason: result.signal ? `runner terminated by ${result.signal}` : 'runner exit code unavailable' };
69
98
  }
70
99
  const fail = parseFailureCount(`${result.stdout}${result.stderr}`);
100
+ const names = fail !== null && fail > 0 ? failedNamesFromStreams(result) : undefined;
71
101
  return fail === null
72
102
  ? { files: [...files], fail: null, status: 'unmeasurable', reason: 'test summary unavailable, ambiguous, or ran zero files' }
73
- : { files: [...files], fail, status: 'measured' };
103
+ : { files: [...files], fail, status: 'measured', ...(names?.files.length ? { failedFiles: names.files } : {}), ...(names?.tests.length ? { failedTests: names.tests } : {}) };
74
104
  } catch (error) {
75
105
  return { files: [...files], fail: null, status: 'unmeasurable', reason: error instanceof Error ? error.message : String(error) };
76
106
  }
@@ -130,7 +130,11 @@ export function runDocs(
130
130
  const SHA = /^[0-9a-f]{40}$/i;
131
131
  const PREFETCH_DEADLINE_MS = 15 * 60_000;
132
132
  type Cut = { version: string; commit: string; manifestHash: string };
133
- type PrefetchCache = Cut & { main: string; tree: string; docs: DocsResult; tui: Record<string, unknown> | null };
133
+ // Two separately published cache files: a finished TUI must survive a docs failure or delay,
134
+ // so it never lives inside the same file as the docs result (should-fix ②).
135
+ type TuiCache = Cut & { tui: Record<string, unknown> };
136
+ type DocsCache = Cut & { main: string; tree: string; docs: DocsResult };
137
+ type PrefetchCache = DocsCache & { tui: Record<string, unknown> | null };
134
138
  type Runner = (command: string, args: string[], cwd?: string, timeout?: number, env?: NodeJS.ProcessEnv) => string;
135
139
  export const commandRunner: Runner = (command, args, cwd, timeout, env) => {
136
140
  const output = spawnSync(command, args, { cwd, env: env ?? process.env, timeout: timeout ?? 15 * 60_000, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
@@ -162,11 +166,27 @@ function claimPrefetch(dir: string, owner: 'worker' | 'docs'): boolean {
162
166
  function cacheFile(dir: string, cut: Cut): string {
163
167
  return join(dir, `${cut.commit}-${cut.manifestHash}.json`);
164
168
  }
165
- function loadCache(dir: string, cut: Cut): PrefetchCache | null {
169
+ function tuiCacheFile(dir: string, cut: Cut): string {
170
+ return join(dir, `${cut.commit}-${cut.manifestHash}.tui.json`);
171
+ }
172
+ function loadTuiCache(dir: string, cut: Cut): TuiCache | null {
173
+ try {
174
+ const value = JSON.parse(readFileSync(tuiCacheFile(dir, cut), 'utf8')) as TuiCache;
175
+ if (value.version !== cut.version || value.commit !== cut.commit || value.manifestHash !== cut.manifestHash) return null;
176
+ return value;
177
+ } catch { return null; }
178
+ }
179
+ /** Publish `data` to `path` atomically (temp + rename), so a reader never sees a partial file. */
180
+ function publish(path: string, data: unknown): void {
181
+ const staged = `${path}.${process.pid}.${randomUUID()}.tmp`;
182
+ writeFileSync(staged, JSON.stringify(data) + '\n');
183
+ try { renameSync(staged, path); } catch (error) { rmSync(staged, { force: true }); throw error; }
184
+ }
185
+ function loadCache(dir: string, cut: Cut): DocsCache | null {
166
186
  try {
167
- const value = JSON.parse(readFileSync(cacheFile(dir, cut), 'utf8')) as PrefetchCache;
187
+ const value = JSON.parse(readFileSync(cacheFile(dir, cut), 'utf8')) as DocsCache;
168
188
  if (value.version !== cut.version || value.commit !== cut.commit || value.manifestHash !== cut.manifestHash
169
- || !SHA.test(value.main) || value.tree !== join(dir, 'tree') || !existsSync(value.tree)
189
+ || !SHA.test(value.main) || dirname(value.tree) !== dir || !/^tree-[0-9a-f-]{36}$/.test(basename(value.tree)) || !existsSync(value.tree)
170
190
  || value.docs?.outcome !== 'ok' || value.docs.version !== cut.version
171
191
  || value.docs.notes !== join(value.tree, 'release/public/docs/releases', `${cut.version}.md`)
172
192
  || value.docs.pages !== join(value.tree, 'website/pages.json')
@@ -191,21 +211,34 @@ export function stagePrefetch(context: GraphContext, env: NodeJS.ProcessEnv = pr
191
211
  const cut = cutFor(context, stateRoot);
192
212
  mkdirSync(dir, { recursive: true });
193
213
  const already = loadCache(dir, cut);
194
- if (already) return already;
195
- const tree = join(dir, 'tree');
214
+ if (already) return { ...already, tui: loadTuiCache(dir, cut)?.tui ?? null };
215
+ // An attempt gets its own path. A failed add can only remove a tree belonging to this attempt,
216
+ // even when an earlier cut or a concurrent worker has a checkout in the same run directory.
217
+ const tree = join(dir, `tree-${randomUUID()}`);
196
218
  let added = false;
219
+ // `git worktree add` can fail AFTER creating the tree directory (e.g. a failed checkout): clean up
220
+ // whenever the add succeeded OR the tree is on disk, so a retry of this run is never blocked (should-fix ①).
221
+ const removeTree = () => {
222
+ if (!(added || existsSync(tree))) return;
223
+ try { runner('git', ['worktree', 'remove', '--force', tree], repo, 60_000); } catch { /* preserve original failure */ }
224
+ // A remove that failed (or could not unregister the tree) must still leave no blocking directory behind.
225
+ if (existsSync(tree)) rmSync(tree, { recursive: true, force: true });
226
+ };
197
227
  try {
198
228
  const main = run('git', ['rev-parse', '--verify', 'origin/main^{commit}'], repo);
199
229
  if (!SHA.test(main)) throw new Error('invalid origin/main commit');
200
- // TUI runs against the cut, on its own isolated worktree, before touching the docs checkout.
230
+ const cachedTui = loadTuiCache(dir, cut);
231
+ // TUI runs against the cut, on its own isolated session, before touching the docs checkout.
201
232
  // tui-sim-node.ts prints the RAW result (no `outcome`); judge it the way node-verdict.ts tui would before reuse.
202
- const tuiData = deps.tui ? deps.tui(cut.commit) : (() => {
233
+ // Publish the reusable TUI in its OWN file the moment it is confirmed — a later docs failure or
234
+ // delay must not throw away a finished TUI (should-fix ②).
235
+ const tui = cachedTui ? cachedTui.tui : reusableTui(withTuiOutcome(0, deps.tui ? deps.tui(cut.commit) : (() => {
203
236
  const tuiRun = spawnSync('bun', ['scripts/release-loop/tui-sim-node.ts', '--commit', cut.commit, '--json'], {
204
237
  cwd: repo, timeout: Math.min(6 * 60_000, left()), encoding: 'utf8', maxBuffer: 16 * 1024 * 1024,
205
238
  });
206
239
  return tuiRun.status === 0 ? lastResult({ status: tuiRun.status, stdout: tuiRun.stdout ?? '', stderr: tuiRun.stderr ?? '' }) ?? null : null;
207
- })();
208
- const tui = reusableTui(withTuiOutcome(0, tuiData), cut.commit);
240
+ })()), cut.commit);
241
+ if (tui && !cachedTui) publish(tuiCacheFile(dir, cut), { ...cut, tui });
209
242
  run('git', ['worktree', 'add', '--detach', tree, main], repo);
210
243
  added = true;
211
244
  run('bun', ['install', '--frozen-lockfile'], tree);
@@ -220,19 +253,14 @@ export function stagePrefetch(context: GraphContext, env: NodeJS.ProcessEnv = pr
220
253
  const finalCut = cutFor(context, stateRoot);
221
254
  if (finalCut.manifestHash !== cut.manifestHash) throw new Error('manifest changed during prefetch');
222
255
  const result: PrefetchCache = { ...cut, main, tree, docs, tui };
223
- const final = cacheFile(dir, cut);
224
- const staged = `${final}.${process.pid}.${randomUUID()}.tmp`;
225
- writeFileSync(staged, JSON.stringify(result) + '\n');
256
+ const docsCache: DocsCache = { ...cut, main, tree, docs };
226
257
  // Publication handshake: whoever creates `claim` first (O_EXCL) wins. The docs node claims before it reads,
227
258
  // so a worker that loses can never publish a cache that a docs attempt would consume.
228
- if (!claimPrefetch(dir, 'worker')) {
229
- rmSync(staged, { force: true });
230
- throw new Error('docs node already ran; late prefetch is discarded');
231
- }
232
- renameSync(staged, final);
259
+ if (!claimPrefetch(dir, 'worker')) throw new Error('docs node already ran; late prefetch is discarded');
260
+ publish(cacheFile(dir, cut), docsCache);
233
261
  return result;
234
262
  } catch (error) {
235
- if (added) { try { runner('git', ['worktree', 'remove', '--force', tree], repo, 60_000); } catch { /* preserve original failure */ } }
263
+ removeTree();
236
264
  throw error;
237
265
  }
238
266
  }
@@ -247,9 +275,10 @@ export function joinTui(context: GraphContext, env: NodeJS.ProcessEnv, repo: str
247
275
  }, stateRoot = effectiveInstanceRoot()): Record<string, unknown> {
248
276
  if (context.outputs.upgrade?.outcome !== 'ok') throw new Error('upgrade did not pass');
249
277
  // Any failure to key the cache (no manifest, cutoff mismatch, no run dir) is a cache miss → original TUI path.
278
+ // The TUI cache file is independent of docs (should-fix ②): a docs failure or delay never blocks this read.
250
279
  let cut: Cut | null = null;
251
- let cached: PrefetchCache | null = null;
252
- try { cut = cutFor(context, stateRoot); cached = loadCache(runCacheDir(env), cut); } catch { /* cache miss */ }
280
+ let cached: TuiCache | null = null;
281
+ try { cut = cutFor(context, stateRoot); cached = loadTuiCache(runCacheDir(env), cut); } catch { /* cache miss */ }
253
282
  const reused = cut && cached?.tui && reusableTui(cached.tui, cut.commit);
254
283
  if (reused) return { ...reused, outcome: 'ok', summary: `tui ${reused.verdict}` };
255
284
  // Preserve node-verdict.ts's exit convention and last-JSON output on a cache miss.
@@ -287,7 +316,7 @@ export function graphDocs(context: GraphContext, env: NodeJS.ProcessEnv, repo: s
287
316
  try {
288
317
  // No fetch (nor any write to remote) until every gate is verified.
289
318
  for (const node of ['gate', 'prepare', 'upgrade', 'tui']) if (context.outputs[node]?.outcome !== 'ok') throw new Error(`${node} did not pass`);
290
- let cached: PrefetchCache | null = null;
319
+ let cached: DocsCache | null = null;
291
320
  // A cache key that cannot be computed is a miss — the original fresh-worktree path runs.
292
321
  if (mayConsume && cacheDir) { try { const cut = cutFor(context, stateRoot); cached = loadCache(cacheDir, cut); } catch { cached = null; } }
293
322
  const branch = `release-docs/${version}`;
@@ -117,6 +117,8 @@ export interface GateResult {
117
117
  const sha = /^[0-9a-f]{7,40}$/i;
118
118
  const versionPattern = /^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/;
119
119
  const failureFile = (root: string, version: string) => join(root, 'release', version, 'gate-failures.json');
120
+ /** Judged identities live beside the machine release ledger; gate-failures.json keeps its raw baseline schema. */
121
+ const verdictFile = (root: string, version: string) => join(root, 'release', version, 'gate-verdict.json');
120
122
  const quote = (s: string) => `'${s.replaceAll("'", "'\\''")}'`;
121
123
  const defaultRemoteMirror = '~/mirror/elanous-agent.git';
122
124
 
@@ -1431,6 +1433,8 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
1431
1433
  };
1432
1434
  const repo = resolve(opts.repo ?? process.cwd());
1433
1435
  const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], stalledEnv: [], rcNonzeroNoFailures: [], durationMs: 0 };
1436
+ const preexistingIds: string[] = [];
1437
+ let knownEnvIds: string[] = [];
1434
1438
  let work: string | undefined;
1435
1439
  let baseSnapshot = false;
1436
1440
  let cutFailures: SweepFailures | undefined;
@@ -1598,11 +1602,13 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
1598
1602
  const baselineIds = [...baseline.failures, ...baseline.errors];
1599
1603
  const countedCut = splitKnownEnv(cutIds, known);
1600
1604
  const countedBaseline = splitKnownEnv(baselineIds, known);
1601
- result.knownEnv = countedCut.knownEnv.length;
1605
+ knownEnvIds = countedCut.knownEnv;
1606
+ result.knownEnv = knownEnvIds.length;
1602
1607
  result.knownEnvCleared = [...new Set(cutRun.passedIds ?? [])].filter((id) => known.has(id) && !cutIds.includes(id) && !cutRun.stalledEnv?.some(({ file }) => fileOf(id) === file)).sort();
1603
1608
  const diff = diffFailures(countedCut.counted, countedBaseline.counted);
1604
1609
  result.fixed = result.baselineDeferred ? null : diff.fixed.length;
1605
- result.preexisting = diff.common.length;
1610
+ preexistingIds.push(...diff.common);
1611
+ result.preexisting = preexistingIds.length;
1606
1612
  // GATE-BASELINE-CACHE — only the files whose new failures reproduce in isolation reach the baseline, and each
1607
1613
  // (commit, file, host) is run at most once across gates: this gate's cut results also seed the next gate's baseline.
1608
1614
  const cacheHost = opts.remote ?? 'local';
@@ -1703,10 +1709,11 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
1703
1709
  debug.log('release-loop.gate', 'isolated-rc-nonzero-no-failures', { file, label: 'cut isolated', rc: rcOnly.rc, pass: rcOnly.pass, baseline });
1704
1710
  }
1705
1711
  for (const id of candidates) {
1706
- if (oldFailures.has(id)) result.preexisting++;
1712
+ if (oldFailures.has(id)) preexistingIds.push(id);
1707
1713
  else result.introduced.push(id);
1708
1714
  }
1709
1715
  }
1716
+ result.preexisting = preexistingIds.length;
1710
1717
  result.baselineCache = cacheStats;
1711
1718
  debug.log('release.gate', 'baseline-cache', { hits: cacheStats.hits, misses: cacheStats.misses, commit: baseSha });
1712
1719
  result.outcome = result.introduced.length ? 'regression' : 'ok';
@@ -1751,7 +1758,33 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
1751
1758
  result.error = `baseline persistence: ${String(error)}`;
1752
1759
  }
1753
1760
  }
1754
- if (result.outcome !== 'error') debug.log('release-loop.gate', 'judged', { version: opts.version, commit: opts.commit, introduced: result.introduced.length, preexisting: result.preexisting });
1761
+ if (result.outcome !== 'error') {
1762
+ const ids = {
1763
+ introduced: [...result.introduced].sort(),
1764
+ knownEnv: [...knownEnvIds].sort(),
1765
+ preexisting: [...preexistingIds].sort(),
1766
+ loadFlaky: [...(result.loadFlaky ?? [])].sort(),
1767
+ };
1768
+ try {
1769
+ const path = verdictFile(ledger, opts.version);
1770
+ mkdirSync(dirname(path), { recursive: true });
1771
+ const temp = join(dirname(path), `.gate-verdict-${process.pid}-${randomUUID()}.tmp`);
1772
+ try {
1773
+ writeFileSync(temp, JSON.stringify({ version: opts.version, commit: opts.commit, outcome: result.outcome, ...ids }, null, 2) + '\n', { mode: 0o600 });
1774
+ chmodSync(temp, 0o600);
1775
+ renameSync(temp, path);
1776
+ } finally { if (existsSync(temp)) rmSync(temp); }
1777
+ } catch (error) {
1778
+ result.outcome = 'error';
1779
+ result.error = `verdict persistence: ${String(error)}`;
1780
+ }
1781
+ if (result.outcome !== 'error') debug.log('release-loop.gate', 'judged', {
1782
+ version: opts.version, commit: opts.commit,
1783
+ introduced: result.introduced.length, preexisting: result.preexisting,
1784
+ knownEnv: result.knownEnv, loadFlaky: ids.loadFlaky.length,
1785
+ introducedIds: ids.introduced, knownEnvIds: ids.knownEnv, preexistingIds: ids.preexisting, loadFlakyIds: ids.loadFlaky,
1786
+ });
1787
+ }
1755
1788
  // GATE-LIVE-OBS-RC-WORDING ⓒ — the shards board says «판정 전» until this lands; then «새 실패 N» only (UX · gate-shards.ts).
1756
1789
  // Same ledger as the board's own writes (sweep's shardsFile). A write failure is logged and never changes the verdict.
1757
1790
  if (result.outcome !== 'error' && opts.pod) {
@@ -7,6 +7,7 @@ import { debug } from '../../src/debug/log.js';
7
7
  export type NodeOutcome = 'ok' | 'fail' | 'error';
8
8
  export type NodeVerdict = 'pass' | 'flaky' | 'fail';
9
9
  export interface GraphContext {
10
+ nodeId?: string;
10
11
  input: { version: string; previousVersion: string; [key: string]: unknown };
11
12
  outputs: Record<string, Record<string, unknown>>;
12
13
  }
@@ -20,7 +21,8 @@ export function readGraphContext(env: NodeJS.ProcessEnv = process.env): GraphCon
20
21
  const input = context.input as Record<string, unknown> | undefined;
21
22
  if (!input || typeof input.version !== 'string' || !isReleaseVersion(input.version)
22
23
  || typeof input.previousVersion !== 'string' || !/^\d+\.\d+\.\d+$/.test(input.previousVersion)) throw new Error('version and previousVersion required');
23
- return { input: input as GraphContext['input'], outputs: context.outputs && typeof context.outputs === 'object' ? context.outputs as GraphContext['outputs'] : {} };
24
+ return { ...(typeof context.nodeId === 'string' ? { nodeId: context.nodeId } : {}),
25
+ input: input as GraphContext['input'], outputs: context.outputs && typeof context.outputs === 'object' ? context.outputs as GraphContext['outputs'] : {} };
24
26
  }
25
27
 
26
28
  export function nodeOutput(context: GraphContext, node: string, key: string): string {
@@ -48,6 +48,9 @@ export function waitForAssets(run: CommandRunner, repo: string, tag: string, nam
48
48
  export function runPublish(run: CommandRunner = runCommand, prodFreezeRoot?: string,
49
49
  clock: { now: () => number; sleep: (ms: number) => void } = { now: Date.now, sleep: Bun.sleepSync }) {
50
50
  const context = readGraphContext();
51
+ // A resumed graph once invoked this irreversible node at npm-publish, docs-land, and failed.
52
+ // The runner's context is the authority: only the publish visit may publish.
53
+ if (context.nodeId !== 'publish') throw new Error(`publish node context mismatch: expected publish, got ${context.nodeId ?? 'missing'}`);
51
54
  const version = context.input.version;
52
55
  // A measured automatic decision or the existing human approval must authorize publication.
53
56
  const auto = context.outputs['auto-approve'];
@@ -11,6 +11,9 @@ export interface TuiScreens {
11
11
  export interface TuiCheck { id: string; pass: boolean; detail: string }
12
12
 
13
13
  const ERROR_TEXT = /\b(TypeError|ReferenceError|SyntaxError|Unhandled|panic|Segmentation fault)\b|Error: /;
14
+ // The startup band can say "Commands — /help lists every command" (#26954).
15
+ // Only the modal's title proves that the help overlay itself remains on screen.
16
+ const HELP_MODAL_TITLE = 'Dashboard help';
14
17
 
15
18
  export function tuiChecks(s: TuiScreens, readyLimitMs = 60_000): TuiCheck[] {
16
19
  const all = [s.first, s.help, s.afterEsc].join('\n');
@@ -18,8 +21,8 @@ export function tuiChecks(s: TuiScreens, readyLimitMs = 60_000): TuiCheck[] {
18
21
  { id: 'boot', pass: s.readyMs !== null && s.readyMs <= readyLimitMs, detail: s.readyMs === null ? 'never ready' : `ready in ${s.readyMs}ms` },
19
22
  { id: 'prompt', pass: s.first.includes('❯'), detail: 'input line (❯) on the first screen' },
20
23
  { id: 'status-bar', pass: /│.*ctx \d+/.test(s.first), detail: 'status bar with model and ctx' },
21
- { id: 'help-opens', pass: s.help.includes('help') && s.help.includes('Commands') && s.help.includes('/help'), detail: '/help shows the help overlay' },
22
- { id: 'help-closes', pass: !s.afterEsc.includes('Commands') && s.afterEsc.includes('❯'), detail: 'Esc closes the overlay' },
24
+ { id: 'help-opens', pass: s.help.includes(HELP_MODAL_TITLE) && s.help.includes('Commands') && s.help.includes('/help'), detail: '/help shows the help overlay' },
25
+ { id: 'help-closes', pass: !s.afterEsc.includes(HELP_MODAL_TITLE) && s.afterEsc.includes('❯'), detail: 'Esc closes the overlay' },
23
26
  { id: 'no-error-text', pass: !ERROR_TEXT.test(all), detail: 'no error text on any screen' },
24
27
  ];
25
28
  }
@@ -17,7 +17,9 @@ export interface PublicCheckReport {
17
17
  readonly ok: boolean;
18
18
  }
19
19
 
20
- const PERSONAL_OR_INTERNAL = /\/Users\/|~\/source(?:\/|\b)|\b(?:INTERNAL_ONLY|ELANOUS_INTERNAL|PRIVATE_INTERNAL|internal-only)\b|@internal\b|내부\s*전용/iu;
20
+ // 개인 경로(`/Users/`·`~/source`)는 대소문자를 가리고, 내부 표지는 기존처럼 `iu`.
21
+ const PERSONAL_PATH = /\/Users\/|~\/source(?:\/|\b)/u;
22
+ const INTERNAL_MARKER = /\b(?:INTERNAL_ONLY|ELANOUS_INTERNAL|PRIVATE_INTERNAL|internal-only)\b|@internal\b|내부\s*전용/iu;
21
23
  const EXECUTABLE = /\.(?:ts|tsx|js|mjs|cjs|py|sh|bash)$/iu;
22
24
  const ENTRY_DIR = /^(?:scripts|src|bin)\//u;
23
25
  const NON_ENTRY = /(?:\.d\.ts|\.(?:test|spec)\.[^.]+)$/iu;
@@ -165,7 +167,8 @@ export function checkPublicSkills(root: string, boundary: SkillBoundaryReport =
165
167
  const bytes = readFileSync(file);
166
168
  if (bytes.includes(0)) continue;
167
169
  try {
168
- if (PERSONAL_OR_INTERNAL.test(new TextDecoder('utf-8', { fatal: true }).decode(bytes))) matches.push(relative(root, file));
170
+ const decoded = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
171
+ if (PERSONAL_PATH.test(decoded) || INTERNAL_MARKER.test(decoded)) matches.push(relative(root, file));
169
172
  } catch {
170
173
  // Binary assets are not text; extensionless and dotfiles are checked when decodable.
171
174
  }