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
@@ -106,6 +106,8 @@ export interface FirstScreenBandInput {
106
106
  readonly width: number;
107
107
  readonly daemon: boolean;
108
108
  readonly link?: TuiDaemonLink;
109
+ /** Renders the three available-action rows and the example row in this locale; `undefined` keeps the historical band. */
110
+ readonly locale?: 'ko' | 'en';
109
111
  readonly pwa?: {
110
112
  readonly tailnet?: string | null;
111
113
  readonly lan?: string | null;
@@ -138,10 +140,37 @@ function fitCells(text: string, limit: number): string {
138
140
  return result;
139
141
  }
140
142
 
143
+ // First-run guidance: three rows of available actions plus one immediately-typeable
144
+ // example row, per locale. Every row names something the empty TUI can really do.
145
+ const GUIDANCE: Record<'ko' | 'en', {
146
+ readonly actions: readonly [string, string, string];
147
+ readonly example: string;
148
+ }> = {
149
+ ko: {
150
+ actions: [
151
+ '· 대화 — 무엇이든 자연어로 물어보고 시켜보세요',
152
+ '· 명령 — /help 로 전체 명령을 보세요',
153
+ '· 스킬 — /run-skill <이름> 으로 설치된 스킬을 돌리세요',
154
+ ],
155
+ example: '바로 입력해 보세요: 무엇을 할 수 있어?',
156
+ },
157
+ en: {
158
+ actions: [
159
+ '· Chat — ask for anything in plain language',
160
+ '· Commands — /help lists every command',
161
+ '· Skills — /run-skill <name> runs an installed skill',
162
+ ],
163
+ example: 'Try typing: what can you do?',
164
+ },
165
+ };
166
+
141
167
  /** A startup transcript band; never includes credentials or a partial address. */
142
168
  export function buildFirstScreenBand(input: FirstScreenBandInput): string[] {
143
169
  const width = Number.isFinite(input.width) ? Math.max(0, Math.floor(input.width)) : 0;
144
- const fit = (text: string): string => text.slice(0, width);
170
+ // The historical band is mixed: the link row speaks Korean, the remaining lines English.
171
+ // An explicit locale unifies them; `undefined` must keep every pinned line byte-identical.
172
+ const linkKorean = input.locale !== 'en';
173
+ const plainKorean = input.locale === 'ko';
145
174
  const address = [input.pwa?.tailnet, input.pwa?.lan, input.pwa?.loopback]
146
175
  .map((candidate) => {
147
176
  if (!candidate) return null;
@@ -154,32 +183,53 @@ export function buildFirstScreenBand(input: FirstScreenBandInput): string[] {
154
183
  }
155
184
  })
156
185
  .find((candidate) => candidate !== null);
157
- const fallback = input.pwa === undefined ? 'PWA: not checked' : 'PWA: unavailable';
186
+ const fallback = input.pwa === undefined
187
+ ? (plainKorean ? 'PWA: 확인 안 함' : 'PWA: not checked')
188
+ : (plainKorean ? 'PWA: 사용할 수 없음' : 'PWA: unavailable');
158
189
  const addressLine = address ? `PWA: ${address}` : fallback;
159
190
 
160
191
  const daemonLine = (() => {
161
192
  const link = input.link;
162
- if (!link) return `elanous | daemon: ${input.daemon ? 'online' : 'offline'}`;
163
- if (link.status === 'checking') return 'elanous | 데몬: 확인 중';
193
+ if (!link) return plainKorean
194
+ ? `elanous | 데몬: ${input.daemon ? '켜짐' : '꺼짐'}`
195
+ : `elanous | daemon: ${input.daemon ? 'online' : 'offline'}`;
196
+ if (link.status === 'checking') return linkKorean ? 'elanous | 데몬: 확인 중' : 'elanous | daemon: checking';
164
197
  if (link.status === 'connected') {
165
198
  let address: string;
166
199
  try {
167
200
  const url = new URL(link.address);
168
201
  address = ['http:', 'https:'].includes(url.protocol) && url.hostname ? url.origin : '';
169
202
  } catch { address = ''; }
170
- const line = `elanous | 데몬: 연결됨${address ? ` (${address})` : ''}`;
171
- return cellWidth(line) <= width ? line : 'elanous | 데몬: 연결됨';
203
+ const bare = linkKorean ? 'elanous | 데몬: 연결됨' : 'elanous | daemon: connected';
204
+ const line = `${bare}${address ? ` (${address})` : ''}`;
205
+ return cellWidth(line) <= width ? line : bare;
206
+ }
207
+ if (link.status === 'error') {
208
+ return linkKorean ? `elanous | 데몬: ${link.stage} 조회 오류` : `elanous | daemon: ${link.stage} lookup error`;
172
209
  }
173
- if (link.status === 'error') return `elanous | 데몬: ${link.stage} 조회 오류`;
174
- const bare = `elanous | 데몬: ${link.status === 'absent' ? '없음' : '응답 없음'}`;
210
+ const bare = linkKorean
211
+ ? `elanous | 데몬: ${link.status === 'absent' ? '없음' : '응답 없음'}`
212
+ : `elanous | daemon: ${link.status === 'absent' ? 'absent' : 'unhealthy'}`;
175
213
  if (!link.startCommand || CONTROL_CHARS.test(link.startCommand)) return bare;
176
- const line = `${bare} · 시작: ${link.startCommand}`;
214
+ const line = `${bare} · ${linkKorean ? '시작' : 'start'}: ${link.startCommand}`;
177
215
  return cellWidth(line) <= width ? line : bare;
178
216
  })();
179
217
 
180
- return [
181
- input.link ? fitCells(daemonLine, width) : fit(daemonLine),
182
- addressLine.length <= width ? addressLine : fit(address ? 'PWA: address available' : fallback),
183
- fit('Type a message to begin | /help for commands'),
218
+ const hintLine = 'Type a message to begin | /help for commands';
219
+
220
+ const statusRows = [
221
+ fitCells(daemonLine, width),
222
+ cellWidth(addressLine) <= width
223
+ ? addressLine
224
+ : fitCells(address ? (plainKorean ? 'PWA: 주소 확인됨' : 'PWA: address available') : fallback, width),
184
225
  ];
226
+ // The historical band folds its guidance into one mixed-language hint row.
227
+ // An explicit locale renders the same guidance as three available-action rows
228
+ // plus one immediately-typeable example row, in that locale — on every screen
229
+ // state (empty, checking, absent, unhealthy, error) because none of these
230
+ // rows depends on the daemon link or the PWA lookup.
231
+ const guidanceRows = input.locale
232
+ ? [...GUIDANCE[input.locale].actions, GUIDANCE[input.locale].example].map((row) => fitCells(row, width))
233
+ : [fitCells(hintLine, width)];
234
+ return [...statusRows, ...guidanceRows];
185
235
  }
@@ -643,6 +643,7 @@ import { activeCodexAccountView } from '../oauth/codex-account-store.js';
643
643
  import { buildDashboardStatusLines } from './dashboard-status-lines.js';
644
644
  import { readSlashContextNow, seatsNowLine } from '../context-bus/context-now-surfaces.js';
645
645
  import { buildFirstScreenBand, resolveTuiDaemonLink, type TuiDaemonLink } from './first-screen-band.js';
646
+ import { detectLocale } from '../expression/locale.js';
646
647
  import { runNexusShow, type NexusShowResult } from '../cli/nexus-show.js';
647
648
  import {
648
649
  workingDirSegment, sessionCwdSegment, modelSegment, gitSegment,
@@ -1896,18 +1897,20 @@ export function renderDashboardFirstScreenBand(input: {
1896
1897
  readonly history: ReadonlyArray<{ role: string }>;
1897
1898
  readonly lines: string[];
1898
1899
  readonly width: number;
1900
+ /** Renders the localized guidance rows; `undefined` keeps the historical mixed band. */
1901
+ readonly locale?: 'ko' | 'en';
1899
1902
  readonly draw: () => void;
1900
1903
  readonly observe: (data: { daemon: boolean; hasPwa: boolean; hasTailnet: boolean }) => void;
1901
1904
  readonly show?: () => Promise<Pick<NexusShowResult, 'status' | 'urls'>>;
1902
1905
  readonly daemonLink?: () => Promise<TuiDaemonLink>;
1903
1906
  }): Promise<void> {
1904
1907
  if (input.history.some((message) => message.role !== 'system')) return Promise.resolve();
1905
- const band = buildFirstScreenBand({ width: input.width, daemon: false, link: { status: 'checking' } });
1908
+ const band = buildFirstScreenBand({ width: input.width, daemon: false, link: { status: 'checking' }, locale: input.locale });
1906
1909
  input.lines.unshift(...band);
1907
1910
  const daemonLink = input.daemonLink ?? resolveTuiDaemonLink;
1908
1911
  const linkPending = Promise.resolve().then(() => daemonLink()).catch((): TuiDaemonLink => ({ status: 'error', stage: 'health' })).then((link) => {
1909
1912
  if (input.lines[0] !== band[0]) return;
1910
- input.lines[0] = buildFirstScreenBand({ width: input.width, daemon: false, link })[0]!;
1913
+ input.lines[0] = buildFirstScreenBand({ width: input.width, daemon: false, link, locale: input.locale })[0]!;
1911
1914
  try { input.draw(); } catch { /* screen may have closed */ }
1912
1915
  });
1913
1916
  const show = input.show ?? (() => runNexusShow({ format: 'json', out: { log: () => {}, error: () => {} } }));
@@ -1921,7 +1924,7 @@ export function renderDashboardFirstScreenBand(input: {
1921
1924
  const hasTailnet = !!pwa?.tailnet;
1922
1925
  observe({ daemon, hasPwa, hasTailnet });
1923
1926
  if (input.lines[1] !== band[1]) return;
1924
- input.lines[1] = buildFirstScreenBand({ width: input.width, daemon, pwa })[1]!;
1927
+ input.lines[1] = buildFirstScreenBand({ width: input.width, daemon, pwa, locale: input.locale })[1]!;
1925
1928
  try { input.draw(); } catch { /* screen may have closed */ }
1926
1929
  }).catch(() => {
1927
1930
  observe({ daemon: false, hasPwa: false, hasTailnet: false });
@@ -15703,6 +15706,8 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
15703
15706
  history: chat.history,
15704
15707
  lines: chatLines,
15705
15708
  width: Math.max(0, termSize().cols - 4),
15709
+ // The band ships ko/en only; any other detected locale keeps the English rows.
15710
+ locale: detectLocale() === 'ko' ? 'ko' : 'en',
15706
15711
  draw: () => draw(),
15707
15712
  observe: (data) => { try { debug.log('dashboard.first-screen-band', 'resolved', data); } catch { /* observation is best-effort */ } },
15708
15713
  });
@@ -7,16 +7,26 @@ import {
7
7
  } from '../skills/router.js';
8
8
  import type { SkillIndexEntry } from '../skills/index.js';
9
9
  import type { SkillTier } from '../skills/runner.js';
10
- import type { LLMMessage } from '../llm.js';
10
+ import * as llm from '../llm.js';
11
+ import { resolveRoleLlm } from '../user-config.js';
12
+ import { debug } from '../debug/log.js';
11
13
 
12
14
  export interface DashboardSkillDetectionRuntimeDeps {
13
15
  llmFallback: boolean;
14
16
  keywordScoreThreshold: number;
15
17
  llmConfidenceThreshold: number;
16
18
  streamLLM: (
17
- messages: LLMMessage[],
19
+ messages: llm.LLMMessage[],
18
20
  onDelta: (chunk: string, full: string) => void,
19
- opts: { signal?: AbortSignal; maxTokens?: number; temperature?: number },
21
+ opts: {
22
+ signal?: AbortSignal;
23
+ maxTokens?: number;
24
+ temperature?: number;
25
+ /** classify role pin — resolveRoleLlm('classify')가 고른 모델·프로바이더. */
26
+ model?: string;
27
+ provider?: (typeof llm.PROVIDERS)[string];
28
+ usageRole?: 'classify';
29
+ },
20
30
  ) => Promise<string>;
21
31
  /** Active model tier — enables tier-aware full-menu LLM routing in
22
32
  * detectLLM (T1 frontier models route the whole menu when keyword is
@@ -69,10 +79,20 @@ export async function detectDashboardSkillRoute(
69
79
  if (!deps.llmFallback) return keywordDetection;
70
80
 
71
81
  const classify = async (prompt: string, signal?: AbortSignal): Promise<LLMClassifyResult> => {
82
+ const resolved = resolveRoleLlm('classify');
83
+ const provider = llm.PROVIDERS[resolved.provider];
84
+ if (!provider) throw new Error(`Unknown classify provider: ${resolved.provider}`);
85
+ debug.log('llm.role-pin', 'resolved', {
86
+ site: 'skill-detection-runtime',
87
+ role: 'classify',
88
+ model: resolved.model,
89
+ provider: resolved.provider,
90
+ source: resolved.source,
91
+ });
72
92
  const raw = await deps.streamLLM(
73
93
  [{ role: 'user', content: prompt }],
74
94
  () => {},
75
- { signal, maxTokens: 120, temperature: 0 },
95
+ { signal, maxTokens: 120, temperature: 0, model: resolved.model, provider, usageRole: 'classify' },
76
96
  );
77
97
  return parseClassifier(raw);
78
98
  };
@@ -22,6 +22,8 @@ export interface MorningReportOpts {
22
22
  heatmapImageUrl?: string;
23
23
  /** S3 상세 리포트 업로드 여부(기본 true). 테스트는 false 로 부작용 차단. */
24
24
  upload?: boolean;
25
+ /** 시장서사 LLM model 명시(A/B). 미지정이면 지금과 같은 기본 provider 경로(providerFor(cfg)). */
26
+ model?: string;
25
27
  }
26
28
 
27
29
  /** 밤사이 소스를 종합한 아침 에센셜(텔레그램) 조립. 상세는 S3 링크로. */
@@ -38,7 +40,7 @@ export async function composeMorningReport(now: Date = new Date(), opts: Morning
38
40
  catch { uploaded = null; }
39
41
  }
40
42
 
41
- const narrative = opts.narrate ? await synthesizeEssentialBrief(md) : '';
43
+ const narrative = opts.narrate ? await synthesizeEssentialBrief(md, { model: opts.model }) : '';
42
44
 
43
45
  // 결론-우선 상단 블록(매력도 Δ · 섹터순환 · 캡스톤) — PWA 대시보드 데이터 재사용. fail-soft.
44
46
  let heatmap = null, capstone = null;
@@ -3,6 +3,12 @@ export const IDEA_APPROVAL_LIMIT = 10;
3
3
  export const IDEA_STALE_DAYS = 7;
4
4
  export const STALLED_DRAFT_HOURS = 24;
5
5
  export const CLAIM_IDLE_HOURS = 6;
6
+ export const PR_LABEL_NAMES = {
7
+ running: 'elanous:running',
8
+ stalled: 'elanous:stalled',
9
+ superseded: 'elanous:superseded',
10
+ needsRebase: 'elanous:needs-rebase',
11
+ } as const;
6
12
 
7
13
  export type PrLabelAxis = 'state' | 'addon' | 'origin' | 'risk';
8
14
  export type PrLabelApplier = 'harness' | 'launcher' | 'sweeper' | 'session' | 'human';
@@ -24,14 +30,14 @@ export interface PrLabelDefinition {
24
30
  }
25
31
 
26
32
  export const PR_LABELS = [
27
- { name: 'elanous:running', axis: 'state', color: '1D76DB', description: 'Harness run or session is actively handling a draft PR', appliedBy: ['harness', 'session'], sweep: { action: 'mark-stalled', when: 'run-ended' } },
28
- { name: 'elanous:stalled', axis: 'state', color: 'D93F0B', description: 'Incomplete draft requiring human attention', appliedBy: ['harness', 'sweeper'], sweep: { action: 'close', afterHours: STALLED_DRAFT_HOURS } },
33
+ { name: PR_LABEL_NAMES.running, axis: 'state', color: '1D76DB', description: 'Harness run or session is actively handling a draft PR', appliedBy: ['harness', 'session'], sweep: { action: 'mark-stalled', when: 'run-ended' } },
34
+ { name: PR_LABEL_NAMES.stalled, axis: 'state', color: 'D93F0B', description: 'Incomplete draft requiring human attention', appliedBy: ['harness', 'sweeper'], sweep: { action: 'close', afterHours: STALLED_DRAFT_HOURS } },
29
35
  { name: 'elanous:idea-approval', axis: 'state', color: '0E8A16', description: 'Completed, ready PR awaiting human approval', appliedBy: ['launcher'], sweep: { action: 'none' } },
30
- { name: 'elanous:superseded', axis: 'state', color: '6A737D', description: 'Another PR has already merged the same work', appliedBy: ['sweeper', 'session'], sweep: { action: 'close' } },
36
+ { name: PR_LABEL_NAMES.superseded, axis: 'state', color: '6A737D', description: 'Another PR has already merged the same work', appliedBy: ['sweeper', 'session'], sweep: { action: 'close' } },
31
37
  { name: 'elanous:keep', axis: 'addon', color: 'BFDADC', description: 'Human requested that the PR be kept open', appliedBy: ['human'], sweep: { action: 'exclude' } },
32
38
  { name: 'elanous:release-hold', axis: 'addon', color: 'D4C5F9', description: 'Exclude from release notes until verified in production', appliedBy: ['launcher', 'session', 'human'], sweep: { action: 'none' } },
33
39
  { name: 'elanous:idea-stale', axis: 'addon', color: 'FBCA04', description: 'Approval pending beyond the notification threshold', appliedBy: ['sweeper'], sweep: { action: 'notify', afterDays: IDEA_STALE_DAYS } },
34
- { name: 'elanous:needs-rebase', axis: 'addon', color: 'E99695', description: 'Approval blocked by conflicts or failing checks', appliedBy: ['sweeper'], sweep: { action: 'block-approval' } },
40
+ { name: PR_LABEL_NAMES.needsRebase, axis: 'addon', color: 'E99695', description: 'Approval blocked by conflicts or failing checks', appliedBy: ['sweeper', 'session'], sweep: { action: 'block-approval' } },
35
41
  { name: 'elanous:bench', axis: 'addon', color: 'C5DEF5', description: 'Bench arm output: never landed or swept; the owner harvests the winner onto main', appliedBy: ['harness'], sweep: { action: 'none' } },
36
42
  { name: 'elanous:harvestable', axis: 'addon', color: 'FBCA04', description: 'Pod terminal marked this PR for harvest', appliedBy: ['harness'], sweep: { action: 'close', afterHours: STALLED_DRAFT_HOURS } },
37
43
  { name: 'elanous:from-harness', axis: 'origin', color: 'C5DEF5', description: 'Launched from a harness goal', appliedBy: ['harness'], sweep: { action: 'none' } },
@@ -27,6 +27,43 @@ import { releaseResumeAcceptedRegressions, releaseResumeGateExecutor, releaseRes
27
27
 
28
28
  type BashRun = WorkflowDeps['runBash'];
29
29
  const DEFAULT_BASH_TIMEOUT_MS = 5 * 60 * 1_000;
30
+ // Independent of graph YAML and recipe snapshots: an already-wrong saved binding must fail closed.
31
+ const RELEASE_NODE_COMMANDS: Readonly<Record<string, { recipe: string; command?: string }>> = {
32
+ 'version-release': { recipe: 'cmd:version-release', command: 'bun scripts/release-loop/version-node.ts release --json' },
33
+ cutoff: { recipe: 'cmd:cutoff', command: 'bun scripts/release-loop/node-verdict.ts cutoff' },
34
+ 'checklist-gate': { recipe: 'cmd:checklist-gate', command: 'bun scripts/release-loop/checklist-gate-node.ts' },
35
+ prefetch: { recipe: 'cmd:docs', command: 'bun scripts/release-loop/docs-node.ts --json' },
36
+ gate: { recipe: 'cmd:gate', command: 'bun scripts/release-loop/gate-node.ts --json' },
37
+ 'mac-smoke': { recipe: 'cmd:mac-smoke', command: 'bun scripts/release-loop/mac-smoke-node.ts' },
38
+ 'export-check': { recipe: 'cmd:export-check', command: 'bun scripts/release-loop/export-check-node.ts' },
39
+ pwa: { recipe: 'cmd:pwa', command: 'bun scripts/release-loop/node-verdict.ts pwa' },
40
+ prepare: { recipe: 'cmd:prepare', command: 'bun scripts/release-loop/prepare-node.ts' },
41
+ upgrade: { recipe: 'cmd:upgrade', command: 'bun scripts/release-loop/upgrade-node.ts' },
42
+ tui: { recipe: 'cmd:docs', command: 'bun scripts/release-loop/docs-node.ts --json' },
43
+ docs: { recipe: 'cmd:docs', command: 'bun scripts/release-loop/docs-node.ts --json' },
44
+ 'known-issues': { recipe: 'cmd:known-issues', command: 'bun scripts/release-loop/known-issues-node.ts' },
45
+ 'notes-check': { recipe: 'cmd:notes-check', command: 'bun scripts/announce-loop/check-node.ts --json' },
46
+ 'auto-approve': { recipe: 'cmd:auto-approve', command: 'bun scripts/release-loop/auto-approve-node.ts' },
47
+ 'approve-publish': { recipe: 'approval:publish' },
48
+ publish: { recipe: 'cmd:publish-command', command: 'bun scripts/release-loop/publish-node.ts' },
49
+ 'npm-publish': { recipe: 'cmd:npm-publish', command: 'bun scripts/release-loop/npm-publish-node.ts' },
50
+ 'docs-land': { recipe: 'cmd:docs-land', command: 'bun scripts/release-loop/docs-land-node.ts' },
51
+ verify: { recipe: 'cmd:verify', command: 'bun scripts/release-loop/verify-node.ts' },
52
+ 'vault-note': { recipe: 'cmd:vault-note', command: 'bun scripts/release-loop/vault-note-node.ts' },
53
+ 'release-story': { recipe: 'cmd:release-story', command: 'bun scripts/release-story/draft.ts --graph' },
54
+ 'ops-upgrade': { recipe: 'cmd:ops-upgrade', command: 'bun scripts/release-loop/ops-upgrade-node.ts' },
55
+ 'version-dev-bump': { recipe: 'cmd:version-dev-bump', command: 'bun scripts/release-loop/version-node.ts dev-bump --json' },
56
+ done: { recipe: 'none' },
57
+ failed: { recipe: 'none' },
58
+ };
59
+
60
+ function assertReleaseCommandBinding(node: GraphNodeSpec, command: string | undefined, terminal: boolean): void {
61
+ const expected = RELEASE_NODE_COMMANDS[node.nodeId];
62
+ if (!expected || node.recipe !== expected.recipe || command !== expected.command ||
63
+ terminal !== (node.nodeId === 'done' || node.nodeId === 'failed')) {
64
+ throw new Error(`release node dispatch mismatch before execution: ${node.nodeId}`);
65
+ }
66
+ }
30
67
 
31
68
  export interface GraphRunState {
32
69
  graphId: string;
@@ -54,7 +91,9 @@ export interface GraphRunState {
54
91
  graphPath?: string;
55
92
  variant?: { overlayId: string; goal: string; plan: GraphVariantPlan };
56
93
  parentRunId?: string;
57
- unitCalls?: Array<{ node: string; unit: string; version: number; hash: string; childRunId: string; exit: string; steps: number }>;
94
+ /** The entry chosen by a parent call; absent for existing single-entry runs. */
95
+ entryNodeId?: string;
96
+ unitCalls?: Array<{ node: string; unit: string; version: number; hash: string; childRunId: string; exit: string; steps: number; entry?: string }>;
58
97
  growth?: GraphGrowth[];
59
98
  growthRejections?: Array<{ from: string; outcome: string; reason: string }>;
60
99
  growthPark?: { from: string; outcome: string; reason: string; decisionId: string; decisionRef: string; growth: GraphGrowth; command?: GrowthCommand; approval?: string };
@@ -84,7 +123,7 @@ export interface GraphRunOptions {
84
123
  releaseGateExecutor?: { executor: string; host?: string };
85
124
  useCurrentGraph?: boolean;
86
125
  /** Internal unit invocation context, propagated only by runGraph. */
87
- unitContext?: { parentRunId: string; ancestors: readonly string[]; remainingSteps: number };
126
+ unitContext?: { parentRunId: string; ancestors: readonly string[]; remainingSteps: number; entryNodeId?: string };
88
127
  /**
89
128
  * RELEASE-LEDGER-UNIVERSE — stamp every cmd: child with this run's ledger root (`ELANOUS_STATE_DIR`).
90
129
  * Without it a child launched from a non-leader tree (`bun scripts/...` with cwd = that tree) re-resolves
@@ -404,7 +443,14 @@ function nextNode(edges: readonly GraphEdgeSpec[], nodeId: string, exitOutcome:
404
443
 
405
444
  type ResolvedUnit = { path: string; template: GraphTemplateSpec; hash: string };
406
445
 
407
- function resolveUnit(call: string, callerPath: string): ResolvedUnit {
446
+ export class UnitNotFoundError extends Error {
447
+ constructor(call: string) {
448
+ super(`unit not found: ${call}`);
449
+ this.name = 'UnitNotFoundError';
450
+ }
451
+ }
452
+
453
+ export function resolveUnit(call: string, callerPath: string): ResolvedUnit {
408
454
  const [id, requested] = call.split('@');
409
455
  const repo = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
410
456
  const directories = [join(dirname(callerPath), 'units'), dirname(callerPath), join(repo, 'graphs', 'units'), join(getElanousConfigDir(), 'graphs', 'units')];
@@ -424,16 +470,24 @@ function resolveUnit(call: string, callerPath: string): ResolvedUnit {
424
470
  if (matches.length > 1) throw new Error(`ambiguous unit: ${call}`);
425
471
  if (matches.length) return matches[0]!;
426
472
  }
427
- throw new Error(`unit not found: ${call}`);
473
+ throw new UnitNotFoundError(call);
428
474
  }
429
475
 
430
- function validateUnitCalls(graph: GraphTemplateSpec, path: string, ancestors: readonly string[] = []): Map<string, ResolvedUnit> {
476
+ export function validateUnitCalls(graph: GraphTemplateSpec, path: string, ancestors: readonly string[] = [], onMissing?: (call: string) => void): Map<string, ResolvedUnit> {
431
477
  if (ancestors.includes(graph.graphId)) throw new Error(`unit cycle: ${[...ancestors, graph.graphId].join(' -> ')}`);
432
478
  if (ancestors.length > 3) throw new Error('unit nesting exceeds depth 3');
433
479
  const result = new Map<string, ResolvedUnit>();
434
480
  for (const node of graph.nodes) {
435
481
  if (node.kind !== 'subgraph' || node.call === undefined) continue;
436
- const unit = resolveUnit(node.call!, path);
482
+ let unit: ResolvedUnit;
483
+ try { unit = resolveUnit(node.call!, path); } catch (error) {
484
+ if (error instanceof UnitNotFoundError && onMissing) { onMissing(node.call!); continue; }
485
+ throw error;
486
+ }
487
+ const selectedEntry = node.callEntry ?? unit.template.entryNode;
488
+ if (!(unit.template.unit?.entries ?? [unit.template.entryNode]).includes(selectedEntry)) {
489
+ throw new Error(`unit entry mismatch: ${node.nodeId} -> ${selectedEntry}`);
490
+ }
437
491
  const contract = node.contract!;
438
492
  const edge = graph.edges.find(e => e.from === node.nodeId);
439
493
  if (!unit.template.unit || contract.inputs.length !== unit.template.unit.inputs.length ||
@@ -445,7 +499,7 @@ function validateUnitCalls(graph: GraphTemplateSpec, path: string, ancestors: re
445
499
  debug.log('graph.unit', 'contract-mismatch', { graphId: graph.graphId, nodeId: node.nodeId, call: node.call });
446
500
  throw new Error(`unit contract mismatch: ${node.nodeId} -> ${node.call}`);
447
501
  }
448
- validateUnitCalls(unit.template, unit.path, [...ancestors, graph.graphId]);
502
+ validateUnitCalls(unit.template, unit.path, [...ancestors, graph.graphId], onMissing);
449
503
  result.set(node.nodeId, unit);
450
504
  }
451
505
  return result;
@@ -492,6 +546,13 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
492
546
  if (variant && !variant.ok) throw new Error(`graph variant rejected: ${JSON.stringify(variant.rejections)}`);
493
547
  if (variant?.ok) graph = variant.template;
494
548
  const units = validateUnitCalls(graph, path, options.unitContext?.ancestors);
549
+ const unitEntryNode = options.unitContext?.entryNodeId ?? saved?.entryNodeId ?? graph.entryNode;
550
+ if (saved?.entryNodeId !== undefined && options.unitContext?.entryNodeId !== undefined && saved.entryNodeId !== options.unitContext.entryNodeId) {
551
+ throw new Error('unit entry changed on resume');
552
+ }
553
+ if ((options.unitContext || saved?.entryNodeId !== undefined) && !(graph.unit?.entries ?? [graph.entryNode]).includes(unitEntryNode)) {
554
+ throw new Error(`unit entry mismatch: ${unitEntryNode}`);
555
+ }
495
556
  const variantSource = variant?.ok ? stringifyYaml({ ...document,
496
557
  nodes: graph.nodes.map((node, index) => ({
497
558
  ...((document.nodes as Record<string, unknown>[])[index] ?? {}), node_id: node.nodeId, kind: node.kind,
@@ -537,6 +598,14 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
537
598
  if (!resolved.command && !resolved.prompt && !resolved.approval) throw new Error(`unknown command recipe for ${node.nodeId}: ${node.recipe}`);
538
599
  }
539
600
  const graphId = safeSegment(graph.graphId);
601
+ const isReleaseLoop = graphId === 'release-loop';
602
+ // A copied or saved release graph is still a release graph. Its path cannot grant or revoke
603
+ // permission to dispatch a command; only the graph identity and this independent table can.
604
+ const enforceReleaseCommandBindings = isReleaseLoop;
605
+ if (enforceReleaseCommandBindings) {
606
+ for (const node of graph.nodes) assertReleaseCommandBinding(node,
607
+ resolveNodeRecipe(node, recipes, catalogRoles).command?.command, graph.terminalNodes.includes(node.nodeId));
608
+ }
540
609
  if (saved && graphId !== saved.graphId) {
541
610
  if (options.fromNodeId && graphMode === 'current') throw new Error('graph or recipes changed since failed run');
542
611
  throw new Error(`run identity mismatch: ${saved.graphId}/${saved.runId}`);
@@ -562,7 +631,9 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
562
631
  : { graphId, runId, startedAt: new Date().toISOString(), ...runnerOwner, status: 'running', path: [], nodes: [], sourceHash: approvalSourceHash,
563
632
  graphSnapshot: { graphSha: createHash('sha256').update(variantSource).digest('hex'), recipesSha: createHash('sha256').update(recipeSource).digest('hex') },
564
633
  graphPath: resolve(path), ...(variant?.ok ? { variant: { overlayId: variant.overlay.overlayId, ...options.variant! } } : {}),
565
- ...(options.input === undefined ? {} : { input: options.input }), executed: 0, dryRun: options.dryRun === true, statePath };
634
+ ...(options.input === undefined ? {} : { input: options.input }),
635
+ ...(options.unitContext?.entryNodeId === undefined ? {} : { entryNodeId: options.unitContext.entryNodeId }),
636
+ executed: 0, dryRun: options.dryRun === true, statePath };
566
637
  if (options.resumeRunId && graphMode === 'snapshot' && state.sourceHash && state.sourceHash !== approvalSourceHash) {
567
638
  throw new Error('graph snapshot changed since run started');
568
639
  }
@@ -594,7 +665,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
594
665
  const reported = lastJsonObject(prior.output)?.outcome;
595
666
  const next = nextNode(graph.edges, prior.nodeId, prior.ok ? 'ok' : 'fail', typeof reported === 'string' ? reported : undefined);
596
667
  if (next !== recorded.nodeId) throw new Error('saved run path does not follow graph edges');
597
- } else if (recorded.nodeId !== graph.entryNode) throw new Error('saved run path does not start at entry node');
668
+ } else if (recorded.nodeId !== unitEntryNode) throw new Error('saved run path does not start at entry node');
598
669
  if (i < from && !recorded.ok) throw new Error('cannot preserve a failed predecessor when resuming');
599
670
  const approved = resolveNodeRecipe(spec, recipes, catalogRoles).approval;
600
671
  if (approved && i >= from) throw new Error('cannot restart across an approval decision');
@@ -736,7 +807,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
736
807
  if (options.resumeRunId && !options.fromNodeId) {
737
808
  const previousStatus = state.status;
738
809
  Object.assign(state, runnerOwner);
739
- state.resume = { from: state.pending?.nodeId ?? state.path.at(-1) ?? graph.entryNode, at: new Date().toISOString(), previousStatus, graph: graphMode };
810
+ state.resume = { from: state.pending?.nodeId ?? state.path.at(-1) ?? unitEntryNode, at: new Date().toISOString(), previousStatus, graph: graphMode };
740
811
  persistOwnedRun(state, saved!);
741
812
  }
742
813
  if (options.resumeRunId) debug.log('graph.runs', 'resume', { graphId, runId, from: state.resume?.from, graph: graphMode });
@@ -751,7 +822,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
751
822
  : baseLog;
752
823
  const visits = new Map<string, number>();
753
824
  for (const nodeId of state.path) visits.set(nodeId, (visits.get(nodeId) ?? 0) + 1);
754
- let current: string | undefined = options.fromNodeId ?? state.pending?.nodeId ?? state.path.at(-1) ?? graph.entryNode;
825
+ let current: string | undefined = options.fromNodeId ?? state.pending?.nodeId ?? state.path.at(-1) ?? unitEntryNode;
755
826
  if (!options.resumeRunId) {
756
827
  mkdirSync(dirname(statePath), { recursive: true });
757
828
  if (readdirSync(dirname(statePath)).some((file) => file.startsWith(`${runId}.json.`) && file.endsWith('.decision.json'))) {
@@ -788,6 +859,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
788
859
  }
789
860
  const resolved = resolveNodeRecipe(node, recipes, catalogRoles);
790
861
  const command = resolved.command;
862
+ if (enforceReleaseCommandBindings) assertReleaseCommandBinding(node, command?.command, graph.terminalNodes.includes(current));
791
863
  const prompt = resolved.prompt;
792
864
  const approval = resolved.approval ? { approval: approvalMessage(resolved.approval.approval, state.input) } : undefined;
793
865
  if (resumingPending && (!approval || approval.approval !== state.pending?.message)) {
@@ -837,7 +909,13 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
837
909
  const redactCredentials = (text: string) => Object.values(credentials).filter(value => value.length > 0)
838
910
  .reduce((safe, value) => safe.replaceAll(value, '[REDACTED]'), text);
839
911
  const runBash: BashRun = async (body, opts) => {
912
+ // Hash the actual /bin/bash argv after interpolation. Keep the command text (which may contain
913
+ // credentials) out of logs, but leave enough to distinguish a replayed publish from npm/docs.
914
+ const argvSha256 = createHash('sha256').update(JSON.stringify(['/bin/bash', '-c', body])).digest('hex');
915
+ const fingerprint = { graphId, runId, nodeId: current!, recipe: node.recipe, argvSha256 };
916
+ debug.log('graph.run', 'command-dispatch', { ...fingerprint, pid: null });
840
917
  const execute = options.deps?.runBash ?? realRunBash((pid) => {
918
+ debug.log('graph.run', 'command-spawn', { ...fingerprint, pid });
841
919
  // Publish the child before its start time is known, so a concurrent stop refuses instead of failing
842
920
  // the ledger while an untracked node keeps working (REL8b review round 3).
843
921
  state.activeNode = { nodeId: current!, pid, pidStartedAt: UNVERIFIED_NODE_START };
@@ -906,7 +984,10 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
906
984
  const alreadySpent = state.path.length + (state.unitCalls ?? []).reduce((sum, call) => sum + call.steps, 0);
907
985
  const remaining = options.unitContext ? options.unitContext.remainingSteps - alreadySpent : Infinity;
908
986
  const childRunId = randomUUID();
909
- const info = { graphId, runId, nodeId: current, unit: unit.template.graphId, childRunId, parentRunId: runId };
987
+ const selectedEntry = node.callEntry ?? unit.template.entryNode;
988
+ const recordEntry = node.callEntry !== undefined || unit.template.unit?.entries !== undefined;
989
+ const info = { graphId, runId, nodeId: current, unit: unit.template.graphId, childRunId, parentRunId: runId,
990
+ ...(recordEntry ? { entry: selectedEntry } : {}) };
910
991
  let entered = false;
911
992
  try {
912
993
  for (const key of contract.inputs) if (!Object.hasOwn(inputs, key)) throw new Error(`unit input missing: ${current}.${key}`);
@@ -918,7 +999,8 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
918
999
  const child = await runGraph(unit.path, { runId: childRunId, input: inputs, dryRun: false,
919
1000
  pinChildUniverse: options.pinChildUniverse,
920
1001
  unitContext: { parentRunId: runId, ancestors: [...(options.unitContext?.ancestors ?? []), graphId],
921
- remainingSteps: Math.min(node.budget!.innerSteps, remaining) }, deps: options.deps });
1002
+ remainingSteps: Math.min(node.budget!.innerSteps, remaining),
1003
+ ...(recordEntry ? { entryNodeId: selectedEntry } : {}) }, deps: options.deps });
922
1004
  const steps = child.path.length + (child.unitCalls ?? []).reduce((sum, call) => sum + call.steps, 0);
923
1005
  const childExit = child.status === 'done' || child.status === 'failed' ? child.path.at(-1) : undefined;
924
1006
  // The last successful record carrying every contracted output is the source;
@@ -932,7 +1014,8 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
932
1014
  }, undefined) ?? {};
933
1015
  state.unitCalls ??= [];
934
1016
  state.unitCalls.push({ node: current, unit: unit.template.graphId, version: unit.template.version,
935
- hash: unit.hash, childRunId, exit: childExit ?? child.status, steps });
1017
+ hash: unit.hash, childRunId, exit: childExit ?? child.status, steps,
1018
+ ...(recordEntry ? { entry: selectedEntry } : {}) });
936
1019
  if (child.status === 'awaiting-approval') throw new Error(`unit cannot pause for approval: ${current}`);
937
1020
  if (child.status === 'budget-exceeded' || steps > node.budget!.innerSteps || steps > remaining) {
938
1021
  debug.log('graph.unit', 'budget-exhausted', { ...info, steps, innerSteps: node.budget!.innerSteps });
@@ -1005,6 +1088,10 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
1005
1088
  ...(approval && state.pending ? { ...(state.pending.decidedBy === undefined ? {} : { decidedBy: state.pending.decidedBy }), decidedAt: state.pending.decidedAt } : {}),
1006
1089
  startedAt: nodeStartedAt, endedAt: nodeEndedAt, seconds });
1007
1090
  }
1091
+ if (isReleaseLoop && graph.terminalNodes.includes(current) && node.recipe === 'none' &&
1092
+ (exit !== null || output !== undefined || error !== undefined || state.nodes.at(-1)?.executed)) {
1093
+ throw new Error(`release terminal received command output: ${current}`);
1094
+ }
1008
1095
  if (approval) {
1009
1096
  delete state.pending;
1010
1097
  delete state.approvalSourceHash;
@@ -201,6 +201,8 @@ export function planHarnessClean(input: {
201
201
  readWorktreeProvenance?: WorktreeProvenanceReader;
202
202
  /** provenance reader가 쓰는 git runner. planner 단독 테스트에서는 반드시 명시한다. */
203
203
  run?: GitRunner;
204
+ /** Allow reclaiming unowned worktrees only for merged-clean branches; default remains preservation. */
205
+ includeUnownedMergedClean?: boolean;
204
206
  /** 조회별 소요 시간(밀리초). 키는 `queryStatus` 조회 칸과 같다. 못 잰 칸은 생략한다. */
205
207
  queryTimings?: HarnessCleanQueryTimings;
206
208
  }): HarnessCleanPlan {
@@ -353,7 +355,15 @@ export function planHarnessClean(input: {
353
355
  continue;
354
356
  }
355
357
  const ladderReason = `assessment=${assessment.disposition}:${assessment.reason}`;
356
- if (assessment.disposition !== 'reclaim-safe' || !ownership.recorded) {
358
+ const includeUnownedMergedClean = input.includeUnownedMergedClean === true
359
+ && mode === 'merged'
360
+ && mergedPr.has(branch)
361
+ && !openPr.has(branch)
362
+ && assessment.disposition === 'reclaim-safe'
363
+ && assessment.reason === 'merged-clean'
364
+ && !ownership.recorded
365
+ && ownership.status.startsWith('not-recorded:');
366
+ if (assessment.disposition !== 'reclaim-safe' || (!ownership.recorded && !includeUnownedMergedClean)) {
357
367
  preserve.push({ ...base, reason: `${ladderReason}; ownership=${ownership.status}` });
358
368
  continue;
359
369
  }
@@ -992,6 +1002,7 @@ export function execHarnessClean(opts: {
992
1002
  mode: HarnessCleanMode;
993
1003
  dryRun?: boolean;
994
1004
  force?: boolean;
1005
+ includeUnownedMergedClean?: boolean;
995
1006
  branchPrefix?: string;
996
1007
  repo?: string;
997
1008
  window?: { offset: number; size: number };
@@ -1087,6 +1098,7 @@ export function execHarnessClean(opts: {
1087
1098
  readWorktreeProvenance: opts.readWorktreeProvenance,
1088
1099
  run,
1089
1100
  mode: opts.mode,
1101
+ ...(opts.includeUnownedMergedClean ? { includeUnownedMergedClean: true } : {}),
1090
1102
  queryStatus,
1091
1103
  queryTimings: {
1092
1104
  worktrees: worktreesTimed.ms,
@@ -1286,10 +1298,16 @@ export function renderHarnessCleanReport(
1286
1298
  item.reason.includes('reclaim-safe:merged-no-worktree'),
1287
1299
  ).length;
1288
1300
  const branchOnlyReclaimedCount = res.plan.remove.filter((item) => item.reason.includes('branch-only')).length;
1301
+ const isUnownedMergedClean = (item: CleanPlanItem): boolean =>
1302
+ item.reason.includes('assessment=reclaim-safe:merged-clean') && item.reason.includes('ownership=not-recorded:');
1303
+ const plannedUnownedMergedCleanCount = res.plan.remove.filter(isUnownedMergedClean).length;
1304
+ const unownedMergedCleanCount = (res.dryRun ? res.plan.remove : res.removed).filter(isUnownedMergedClean).length;
1305
+ const unownedMergedCleanSummary = plannedUnownedMergedCleanCount > 0
1306
+ ? ` · 미소유 merged-clean ${res.dryRun ? '제거 예정' : '회수'} ${unownedMergedCleanCount}` : '';
1289
1307
  L.push(
1290
1308
  omittedPreserve > 0
1291
- ? `규모 매칭 ${matchedTotal} · 제거 계획 ${res.plan.remove.length} · 보존 ${res.plan.preserve.length} · 화면 ${displayedCount} (보존 ${omittedPreserve}개는 생략) · 브랜치만 회수 ${branchOnlyReclaimedCount} · 워크트리 없이 안전 ${mergedNoWorktreeCount}`
1292
- : `규모 매칭 ${matchedTotal} · 제거 계획 ${res.plan.remove.length} · 보존 ${res.plan.preserve.length} · 화면 ${displayedCount} · 브랜치만 회수 ${branchOnlyReclaimedCount} · 워크트리 없이 안전 ${mergedNoWorktreeCount}`,
1309
+ ? `규모 매칭 ${matchedTotal} · 제거 계획 ${res.plan.remove.length} · 보존 ${res.plan.preserve.length} · 화면 ${displayedCount} (보존 ${omittedPreserve}개는 생략) · 브랜치만 회수 ${branchOnlyReclaimedCount} · 워크트리 없이 안전 ${mergedNoWorktreeCount}${unownedMergedCleanSummary}`
1310
+ : `규모 매칭 ${matchedTotal} · 제거 계획 ${res.plan.remove.length} · 보존 ${res.plan.preserve.length} · 화면 ${displayedCount} · 브랜치만 회수 ${branchOnlyReclaimedCount} · 워크트리 없이 안전 ${mergedNoWorktreeCount}${unownedMergedCleanSummary}`,
1293
1311
  );
1294
1312
  if (res.plan.remove.length === 0) L.push('정리 대상 없음.');
1295
1313
  else {