elanous 0.2.25 → 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 (337) 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-37fd16f479f5554a.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-24453f6a302816b4.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/board/page-6c131d30bb1cf871.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-b12149c51f7d04b1.js → page-fa46d4588e8442c2.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/bots/page-dc0772b11861af51.js +1 -0
  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-305cd294e754906e.js → page-256ffd365d32e100.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-dea4b795741126fd.js → page-e0de78ae7b99f01c.js} +1 -1
  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-45889e7070d11126.js → page-4c741eb760f2f77c.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/field/{page-16a2e893f83ae4d0.js → page-e9ba98f5f1440a21.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/inside/page-dafc901bb1b72b5f.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/intake/{page-56888bc744e08dac.js → page-2f6f303fa114ec24.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/layout-1634081c5272006c.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/live/{page-952c6ceec6d9de38.js → page-ced0bcfc6542b3fc.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live-run/{page-6d0202af568f4c63.js → page-bb1f05cd51c38944.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/loops/page-646b50f63b6fe732.js +1 -0
  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-4fb6fc34e20c07b7.js → page-f736324afa2117c1.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-e9502921f4f01b1f.js → page-6f131b51193a36f9.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/{not-found-b6169ea2f09cf287.js → not-found-435f5e2faf02bc01.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-6a14333d1b1b3967.js → page-497e0eedf20e7890.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-8c91a9a66c9ec90a.js → page-a8d28fdf995e16ea.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8966452228753c5e.js +1 -0
  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-3d3fc73dd187ed52.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-9c04dac0831c2bbb.js → page-c71267e7681da982.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a21d1e1797172692.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-04de9ca6d52a3893.js → page-cb249e81ed339318.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-62527677bbc2fdc4.js → page-781cfc5114e097da.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-9a2e09c4a311764c.js → page-478157214db6a023.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/{page-bfeb35d69e8b24bb.js → page-86141f12cf090b7f.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-89cb33bced824e4a.js → page-7256c73bb1c36751.js} +1 -1
  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-6445ac4cbb8dd869.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-21050928a697ed7a.js +1 -0
  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-20162174b77f96cf.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-05a4b2c9b91034aa.js → page-11bf9e5725a72c22.js} +1 -1
  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 +119 -7
  154. package/scripts/coord-msg-bridge.ts +209 -0
  155. package/scripts/coord-post.sh +40 -7
  156. package/scripts/coord-requests-bridge.ts +10 -5
  157. package/scripts/detect-test-interference.ts +31 -1
  158. package/scripts/expose-rubric-suggest.ts +1 -1
  159. package/scripts/measure-tsc-affected-roots.ts +10 -1
  160. package/scripts/release-loop/docs-node.ts +51 -22
  161. package/scripts/release-loop/export-check-node.ts +8 -5
  162. package/scripts/release-loop/gate-node.ts +37 -4
  163. package/scripts/release-loop/node-verdict.ts +10 -3
  164. package/scripts/release-loop/publish-node.ts +3 -0
  165. package/scripts/release-loop/tui-node-checks.ts +5 -2
  166. package/scripts/skills/public-check.ts +5 -2
  167. package/scripts/test-deterministic.ts +520 -37
  168. package/scripts/ux-sim.ts +7 -1
  169. package/scripts/verify-kpack6-live.ts +230 -0
  170. package/src/acp/codex-plugins.ts +17 -0
  171. package/src/agent/task-notification.ts +32 -2
  172. package/src/agent-mission/in-agent-plugin.ts +8 -3
  173. package/src/agent-substrate/review-observation.ts +9 -2
  174. package/src/agent-substrate/self-review-cli.ts +62 -16
  175. package/src/boot/daemon-tools/self-implement.ts +72 -4
  176. package/src/briefing/brief-items.ts +151 -35
  177. package/src/budget/llm-cost.ts +30 -3
  178. package/src/budget/usage-outcomes.ts +32 -0
  179. package/src/claims/claims-ledger.ts +23 -0
  180. package/src/claims/claims-render.ts +22 -2
  181. package/src/cli/brief-cli.ts +131 -10
  182. package/src/cli/claims-cli.ts +24 -4
  183. package/src/cli/control-cli.ts +29 -0
  184. package/src/cli/doctor-cli.ts +90 -8
  185. package/src/cli/msg-cli.ts +177 -11
  186. package/src/cli/plugin-cli.ts +11 -3
  187. package/src/cli/pr-cli.ts +133 -18
  188. package/src/cli/pr-land-overlap-evidence.ts +18 -10
  189. package/src/cli/pr-land-refusal.ts +20 -0
  190. package/src/cli/release-cli.ts +34 -5
  191. package/src/cli/rfc-cli.ts +24 -0
  192. package/src/cli/seat-cli.ts +121 -0
  193. package/src/cli/self-utterances-cli.ts +140 -0
  194. package/src/cli/tailscale-serve.ts +5 -8
  195. package/src/cli/tasks-cli.ts +528 -31
  196. package/src/cli/usage-cli.ts +9 -4
  197. package/src/context-card/cell.ts +40 -0
  198. package/src/dashboard/boot-timing.ts +35 -0
  199. package/src/dashboard/first-screen-band.ts +63 -13
  200. package/src/dashboard/index.ts +94 -27
  201. package/src/dashboard/input/chat-main-plain-turn-runtime.ts +26 -0
  202. package/src/dashboard/skill-detection-runtime.ts +24 -4
  203. package/src/dashboard/slash-runtime/dashboard-handlers.ts +6 -0
  204. package/src/debug/log.ts +1 -1
  205. package/src/domains/morning-report.ts +3 -1
  206. package/src/domains/release-tool.ts +29 -12
  207. package/src/domains/surface-events.ts +16 -0
  208. package/src/elanous-config-dir.ts +2 -2
  209. package/src/fleet/host-doctor.ts +73 -2
  210. package/src/github/pr-labels.ts +10 -4
  211. package/src/graph-runner/runner.ts +104 -15
  212. package/src/harness/harness-clean.ts +21 -3
  213. package/src/harness/harness-cli-command.ts +68 -10
  214. package/src/harness/harness-pod-dispatch.ts +11 -4
  215. package/src/harness/harness-stop.ts +11 -7
  216. package/src/hq/seats.ts +17 -3
  217. package/src/index.ts +69 -9
  218. package/src/instance/current.ts +2 -6
  219. package/src/instance/resolve.ts +5 -6
  220. package/src/intelligence-map/model-bench-scores.ts +102 -0
  221. package/src/llm/openai-sampling-fields.ts +89 -0
  222. package/src/llm/runtime-model-family.ts +61 -0
  223. package/src/llm/zai-coding-plan.ts +27 -1
  224. package/src/llm.ts +13 -89
  225. package/src/loops/orchestrator/inbox.ts +73 -0
  226. package/src/loops/orchestrator/resync-shadow.ts +100 -12
  227. package/src/loops/orchestrator/tick.ts +73 -2
  228. package/src/machines/machine-ledger.ts +31 -3
  229. package/src/maturity/cli-maturity.ts +13 -3
  230. package/src/maturity/feature-maturity.ts +1 -1
  231. package/src/msg/msg-sse-server.ts +141 -0
  232. package/src/msg/msg-store.ts +149 -17
  233. package/src/ops/overview-collect.ts +3 -2
  234. package/src/ops/overview-owner.ts +29 -0
  235. package/src/patents/ledger.ts +128 -0
  236. package/src/plugin-hooks/shell-hook.ts +17 -2
  237. package/src/plugin-missions/evaluator-runner.ts +18 -2
  238. package/src/plugins/install/market-fetch.ts +32 -1
  239. package/src/plugins/install/plugin-install.ts +11 -1
  240. package/src/pod-lease/admission.ts +116 -41
  241. package/src/prompt-enhance/enhance.ts +11 -0
  242. package/src/release-loop/checklist.ts +52 -4
  243. package/src/release-loop/feature-store.ts +10 -9
  244. package/src/release-loop/merged-pr-checklist.ts +24 -5
  245. package/src/release-loop/rubric.ts +70 -0
  246. package/src/rfc-registry/registry.ts +88 -0
  247. package/src/seat-dispatch/seat-request-ledger.ts +4 -0
  248. package/src/seat-loop/knowhow-lint.ts +233 -0
  249. package/src/seat-loop/seat-loop.ts +5 -1
  250. package/src/seat-loop/seat-pack.ts +16 -4
  251. package/src/seat-loop/traffic-signal.ts +7 -0
  252. package/src/self-dev/ask-launch-flow.ts +48 -8
  253. package/src/self-dev/ask-path-candidates.ts +69 -14
  254. package/src/self-dev/decompose-proposal.ts +43 -1
  255. package/src/self-dev/draft-sweep.ts +23 -14
  256. package/src/self-dev/graph-journey-nodes.ts +2 -0
  257. package/src/self-dev/launch-preflight-text.ts +5 -1
  258. package/src/self-dev/launch-preflight.ts +143 -10
  259. package/src/self-dev/orchestrate.ts +6 -2
  260. package/src/self-dev/run-supervisor.ts +35 -1
  261. package/src/self-implement/gate-cli.ts +6 -2
  262. package/src/self-implement/goal-author.ts +5 -28
  263. package/src/self-implement/goal-command-execution.ts +26 -0
  264. package/src/self-implement/goal-new-paths.ts +4 -5
  265. package/src/self-implement/graph-authority.ts +17 -3
  266. package/src/self-implement/graph-yaml.ts +15 -7
  267. package/src/self-implement/headless-elanous-driver.ts +16 -1
  268. package/src/self-implement/host-regate.ts +44 -9
  269. package/src/self-implement/orchestrator.ts +5 -11
  270. package/src/self-implement/pwa-import-graph.ts +194 -7
  271. package/src/self-implement/rework-policy.ts +80 -2
  272. package/src/self-implement/self-implement-runtime.ts +126 -21
  273. package/src/self-implement/speed-claims.ts +145 -6
  274. package/src/session/index.ts +14 -4
  275. package/src/session-runtime/index.ts +55 -11
  276. package/src/skills/tools/persistent-grounding.ts +59 -4
  277. package/src/skills/tools/shell.ts +79 -1
  278. package/src/steward/rejudge-tick.ts +12 -1
  279. package/src/steward/release-day.ts +158 -0
  280. package/src/steward/resync-tick.ts +123 -0
  281. package/src/task-agent/actions.ts +1 -0
  282. package/src/task-agent/adopt-pr.ts +140 -0
  283. package/src/task-agent/board.ts +11 -1
  284. package/src/task-agent/card-evidence.ts +37 -5
  285. package/src/task-agent/cover.ts +105 -19
  286. package/src/task-agent/failure-notify.ts +61 -0
  287. package/src/task-agent/green-sweep.ts +170 -0
  288. package/src/task-agent/land-worktree.ts +52 -5
  289. package/src/task-agent/live-move-history.ts +1 -0
  290. package/src/task-agent/live-moves.ts +56 -17
  291. package/src/task-agent/parent-host.ts +1 -1
  292. package/src/task-agent/postland-card.ts +139 -0
  293. package/src/task-agent/rejudge-on-head.ts +321 -25
  294. package/src/task-agent/remote-rejudge-bind.ts +62 -0
  295. package/src/task-agent/repair-branch-cas.ts +182 -0
  296. package/src/task-agent/repair-cycle.ts +275 -0
  297. package/src/task-agent/repair-next-move.ts +31 -0
  298. package/src/task-agent/repair-review.ts +24 -0
  299. package/src/task-agent/repair-worker.ts +87 -0
  300. package/src/task-agent/review-result-capture.ts +66 -2
  301. package/src/task-agent/shadow.ts +150 -29
  302. package/src/task-agent/task-hand.ts +133 -8
  303. package/src/task-orchestrator/surfaces/pod-repair-commit.ts +96 -0
  304. package/src/task-orchestrator/surfaces/self-implement-pod.ts +71 -12
  305. package/src/telegram.ts +30 -1
  306. package/src/tool-runtime/tool-search-runtime.ts +25 -0
  307. package/src/tui.ts +20 -1
  308. package/src/user-config.ts +11 -60
  309. package/src/ux-sim/fault-sqlite-busy.ts +50 -0
  310. package/src/version/packed-revision.json +1 -1
  311. package/apps/pwa/out/_next/static/chunks/app/approvals/page-9360b1285b2a346e.js +0 -1
  312. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f08f6529d09ffae1.js +0 -1
  313. package/apps/pwa/out/_next/static/chunks/app/board/page-f000d54d258f9ef6.js +0 -1
  314. package/apps/pwa/out/_next/static/chunks/app/bots/page-22f674e7e4c2c9d5.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/app/ceo/page-8888733e40cea6d1.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/app/chat/page-9717ec6964b92276.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/app/consult/page-44eaad486bc98a45.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/app/control/page-97fa6cf3d34c7eb9.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/app/editor/page-35e05d7450461a67.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/app/inside/page-b2638013c0bf7dfd.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/app/layout-c44f93a41167536f.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/app/loops/page-1b810474507f3702.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/app/market/page-74366ae2693a7d95.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4e634c5b98fd4e93.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-ed3070d637850548.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/outputs/page-b9cc8a10bc6078b4.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/reflection/page-128458c8d86fbb5f.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/sessions/page-cf0c2b0183991d09.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/showroom/page-b008aff96be56654.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/tasks/page-814394dcbae789b1.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/term/page-6a87325c87f09dd2.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/trace/page-852170303c6cd755.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/vault/page-087ceeb975ef272b.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-97c7361cc2a19319.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/workspace/page-8c6901833f897496.js +0 -1
  336. /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → y1ltt7oCZxVpC6zVPhZ2O}/_buildManifest.js +0 -0
  337. /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → y1ltt7oCZxVpC6zVPhZ2O}/_ssgManifest.js +0 -0
@@ -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;
@@ -21,12 +21,15 @@ export const RELEASE_STATUS_SPEC: LLMToolSpec = {
21
21
  + 'topic=cell: id 칸 하나의 상태·근거와 살핀 판.\n'
22
22
  + 'topic=schedule: 발행 이후 판의 컷·착지·발행·동결 일정.\n'
23
23
  + 'topic=ops: 도는 런·발행·늦은 스케줄·자리·열린 결정의 출처 있는 개관.\n'
24
- + 'topic 과 action 을 동시에 쓰지 않는다 — 둘 중 하나만: topic 은 읽을 대상, action 은 짧은 요약.\n'
24
+ + 'topic 과 action 을 동시에 쓰면 topic 을 우선하고 action 은 무시한다. topic 은 읽을 대상, action 은 단독 조회의 짧은 요약.\n'
25
+ + 'topic=features 에서는 owner·status 로 칸을 필터링할 수 있다.\n'
25
26
  + '발행·버전·피처·체크리스트·칸·컷·일정·운영 상태 질문은 Grep·ListDir·파일 검색보다 이 도구를 먼저 부른다 — 그 답은 파일이 아니라 원장에 있다.',
26
27
  parameters: { type: 'object', properties: {
27
28
  version: { type: 'string', description: '조회할 판. 생략하면 개발 판과 다음 판.' },
28
29
  topic: { type: 'string', enum: ['status', 'features', 'cell', 'schedule', 'ops'], description: '읽을 대상(기본 status).' },
29
30
  action: { type: 'string', enum: ['schedule', 'blocked', 'landed'], description: '짧은 읽기 요약: 다음 컷 일정·막힌 칸·최근 24시간 병합 PR.' },
31
+ owner: { type: 'string', description: 'topic=features 때 담당 읽기 필터.' },
32
+ status: { type: 'string', description: 'topic=features 때 상태 읽기 필터.' },
30
33
  id: { type: 'string', description: 'topic=cell 때 찾을 칸 id(필수).' },
31
34
  }, required: [] },
32
35
  };
@@ -122,10 +125,23 @@ function cellView(item: ChecklistItem) {
122
125
  export function dispatchReleaseStatus(args: Record<string, unknown>, deps: ReleaseToolDeps = {}): { text: string; structured: Record<string, unknown> } {
123
126
  const topic = args.topic === undefined ? 'status' : args.topic;
124
127
  if (typeof topic !== 'string' || !['status', 'features', 'cell', 'schedule', 'ops'].includes(topic)) throw new Error('topic 은 status, features, cell, schedule, ops 중 하나');
125
- if (args.topic !== undefined && args.action !== undefined) {
126
- throw new Error(`topic 과 action 을 동시에 보낼 수 없습니다 — 둘 중 하나만: topic=${topic} 는 읽을 대상, action 은 짧은 요약(schedule·blocked·landed)`);
127
- }
128
+ const ignoredAction = args.topic !== undefined && args.action !== undefined ? args.action : undefined;
129
+ const withIgnoredAction = (result: { text: string; structured: Record<string, unknown> }) => ignoredAction === undefined
130
+ ? result
131
+ : { ...result, text: `action=${String(ignoredAction)} 은 무시됨 — topic 과 같이 못 쓴다\n${result.text}` };
128
132
  const requested = args.version;
133
+ if (topic === 'features') {
134
+ for (const key of ['owner', 'status'] as const) {
135
+ if (args[key] !== undefined && (typeof args[key] !== 'string' || !args[key].trim())) throw new Error(`${key} 값이 필요합니다`);
136
+ }
137
+ }
138
+ const filters = topic === 'features' ? {
139
+ ...(typeof args.owner === 'string' ? { owner: args.owner } : {}),
140
+ ...(typeof args.status === 'string' ? { status: args.status } : {}),
141
+ } : {};
142
+ const hasFeatureFilters = Object.keys(filters).length > 0;
143
+ const featureItemMatches = (item: ChecklistItem) =>
144
+ (!('owner' in filters) || item.owner === filters.owner) && (!('status' in filters) || item.status === filters.status);
129
145
  if (requested !== undefined && (typeof requested !== 'string' || !requested.trim())) throw new Error('판 이름이 필요합니다');
130
146
  const checklist = deps.checklist ?? listChecklist;
131
147
  if (topic === 'ops') {
@@ -133,10 +149,10 @@ export function dispatchReleaseStatus(args: Record<string, unknown>, deps: Relea
133
149
  const kinds: ContextFact['kind'][] = ['run', 'release', 'schedule-late', 'seat', 'decision'];
134
150
  const facts = answer.facts.filter(fact => kinds.includes(fact.kind));
135
151
  debug.log('release.tool', 'status', { action: 'status', topic, version: requested ?? null });
136
- return { text: facts.length ? facts.map(fact => JSON.stringify(fact)).join('\n') : '운영 개관: 관측된 사실 없음', structured: { facts } };
152
+ return withIgnoredAction({ text: facts.length ? facts.map(fact => JSON.stringify(fact)).join('\n') : '운영 개관: 관측된 사실 없음', structured: { facts } });
137
153
  }
138
154
  const dev = checklistDevVersion(deps.devVersion?.() ?? devVersion());
139
- if (args.action !== undefined) {
155
+ if (ignoredAction === undefined && args.action !== undefined) {
140
156
  const action = args.action;
141
157
  if (typeof action !== 'string' || !['schedule', 'blocked', 'landed'].includes(action)) {
142
158
  throw new Error('action 은 schedule, blocked, landed 중 하나여야 합니다');
@@ -178,12 +194,13 @@ export function dispatchReleaseStatus(args: Record<string, unknown>, deps: Relea
178
194
  const target = (requested as string | undefined) ?? dev;
179
195
  validateVersion(target);
180
196
  const data = checklist(target);
181
- const counts = summarizeChecklist(data);
182
- const items = data.items.slice(0, 60).map(({ id, title, status, owner, priority }) =>
197
+ const matchingItems = data.items.filter(featureItemMatches);
198
+ const counts = hasFeatureFilters ? summarizeChecklist({ ...data, items: matchingItems }) : summarizeChecklist(data);
199
+ const items = matchingItems.slice(0, 60).map(({ id, title, status, owner, priority }) =>
183
200
  ({ id, title: firstLine(title), status, owner: owner ?? null, priority: priority ?? null }));
184
- const total = data.items.length;
201
+ const total = matchingItems.length;
185
202
  result = { text: `${target} 판 피처 ${total}칸 · 초록 ${counts.green} · 노랑 ${counts.yellow} · 빨강 ${counts.red} · 끝 ${counts.done}\n${items.map(item => `${item.id} ${item.title} · ${item.status} · ${item.owner ?? '미배정'}`).join('\n')}${total > 60 ? `\n앞 60칸만 표시(전체 ${total}칸)` : ''}`,
186
- structured: { version: target, items, counts: { green: counts.green, yellow: counts.yellow, red: counts.red, done: counts.done }, total, truncated: total > 60 } };
203
+ structured: { version: target, items, counts: { green: counts.green, yellow: counts.yellow, red: counts.red, done: counts.done }, total, truncated: total > 60, ...(hasFeatureFilters ? { filters } : {}) } };
187
204
  } else if (topic === 'cell') {
188
205
  const id = requiredString(args, 'id');
189
206
  const searched = requested ? [requested as string] : [...new Set([dev, nextVersion(dev), checklist(dev).released].filter(Boolean))];
@@ -208,7 +225,7 @@ export function dispatchReleaseStatus(args: Record<string, unknown>, deps: Relea
208
225
  publishAt: row.publishAt ?? null, freezeFrom: row.freezeFrom ?? null, freezeUntil: row.freezeUntil ?? null })) } };
209
226
  }
210
227
  debug.log('release.tool', 'status', { action: 'status', topic, version: requested ?? dev });
211
- return result;
228
+ return withIgnoredAction(result);
212
229
  }
213
230
  const version = requested ? null : dev;
214
231
  const versions = requested ? [requested as string] : [version!, nextVersion(version!)];
@@ -253,7 +270,7 @@ export function dispatchReleaseStatus(args: Record<string, unknown>, deps: Relea
253
270
  : published.state === 'unreadable' ? `발행 판 ${published.version} · 발행 기록 못 읽음` : '발행 판 모름';
254
271
  const structured = { releases: entries, latestRun: latest, published };
255
272
  debug.log('release.tool', 'status', { action: 'status', topic, version: versions.join(',') });
256
- return { text: `${lines.join('\n\n')}\n${cards.map(formatElanousCard).join('\n')}\n${publishedLine}`, structured };
273
+ return withIgnoredAction({ text: `${lines.join('\n\n')}\n${cards.map(formatElanousCard).join('\n')}\n${publishedLine}`, structured });
257
274
  }
258
275
 
259
276
  function requiredString(args: Record<string, unknown>, key: string): string {
@@ -169,6 +169,22 @@ export function recordInboundTurn(
169
169
  finally { if (ownDb) db.close(); }
170
170
  }
171
171
 
172
+ export interface SurfaceUtteranceQuery {
173
+ from: string;
174
+ to: string;
175
+ surface?: string;
176
+ }
177
+
178
+ /** Read every event in a half-open UTC interval without touching recall state. */
179
+ export function querySurfaceUtterances(db: Database, opts: SurfaceUtteranceQuery): SurfaceEventRow[] {
180
+ const where = ["julianday(e.ts) >= julianday(?)", "julianday(e.ts) < julianday(?)", "e.direction = 'inbound'", "e.kind IN ('utterance', 'qna')"];
181
+ const params: string[] = [opts.from, opts.to];
182
+ if (opts.surface) { where.push('e.surface = ?'); params.push(opts.surface); }
183
+ return db.prepare(
184
+ `SELECT e.* FROM events e WHERE ${where.join(' AND ')} ORDER BY julianday(e.ts) ASC, e.id ASC`,
185
+ ).all(...params) as SurfaceEventRow[];
186
+ }
187
+
172
188
  export interface QueryEventsOpts {
173
189
  query?: string; // FTS MATCH (없으면 최근순 전체)
174
190
  surface?: string;
@@ -15,8 +15,8 @@
15
15
  // Resolution order:
16
16
  // 1. Programmatic override set via `setElanousConfigDir(dir)` (incl.
17
17
  // from the `--config-dir` CLI flag).
18
- // 2. State-dir via `effectiveInstanceRoot()` (including isolated source trees).
19
- // 3. `~/.elanous` for the installed daily-driver root.
18
+ // 2. State-dir via `effectiveInstanceRoot()` (explicit --test or parent stamp).
19
+ // 3. `~/.elanous` for both installed and source CLIs without an explicit test root.
20
20
 
21
21
  import { homedir } from 'node:os';
22
22
  import { resolve } from 'node:path';
@@ -1,5 +1,7 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { debug } from '../debug/log.js';
3
+ import type { ResourceView } from '../control-plane/ledger.js';
4
+ import type { MachineLedger, MachineFleetIntent } from '../machines/machine-ledger.js';
3
5
 
4
6
  /**
5
7
  * 호스트 닥터 — 첫 조각(FLEET-DOCTOR-1 · 읽기 전용).
@@ -295,6 +297,66 @@ export interface HostDoctorOptions {
295
297
  remote?: HostDoctorRemote;
296
298
  expected?: HostDoctorExpected;
297
299
  now?: () => Date;
300
+ /** HQ observations: null means the source could not be read; [] means a successful empty reading. */
301
+ membership?: {
302
+ intent: MachineLedger | null;
303
+ machines: readonly ResourceView[] | null;
304
+ parentHosts: readonly { host?: string; podPool?: string; approvalCapacity?: number }[] | null;
305
+ intentError?: string;
306
+ machinesError?: string;
307
+ parentHostsError?: string;
308
+ };
309
+ }
310
+
311
+ function membershipCheck(id: string, status: HostDoctorCheckStatus, observed: string, expected: string): HostDoctorCheckResult {
312
+ return { id, status, observed, expected, fixKind: 'human', fix: 'compare Nexus machine registration, machines.yaml intent, and taskAgent.parentHost' };
313
+ }
314
+
315
+ /** Pure HQ comparison. A failed query is null (unmeasured); a successful empty result is a real missing host. */
316
+ export function checkHostMembership(host: string, platform: HostDoctorPlatform | null, membership: NonNullable<HostDoctorOptions['membership']>): HostDoctorCheckResult[] {
317
+ const intent: MachineFleetIntent | undefined = membership.intent?.machines.find((row) => row.fleet?.sshName === host)?.fleet;
318
+ const registryExpected = intent ? `live machine ${host} · role ${intent.role}` : `machines.yaml fleet intent for ${host}`;
319
+ let registry: HostDoctorCheckResult;
320
+ if (membership.intent === null || membership.machines === null) {
321
+ registry = membershipCheck('registry.member', 'unmeasured', membership.intent === null
322
+ ? `intent ledger unavailable (repository checkout required)${membership.intentError ? `: ${membership.intentError}` : ''}`
323
+ : `Nexus machine query unavailable${membership.machinesError ? `: ${membership.machinesError}` : ''}`, registryExpected);
324
+ } else if (!intent) {
325
+ registry = membershipCheck('registry.member', 'fail', 'host absent from fleet intent', registryExpected);
326
+ } else {
327
+ // `machine` is the Nexus identity; `name` is a display label. A missing/expired record
328
+ // is a measured failure even when the remote platform probe did not run.
329
+ const rows = membership.machines.filter((row) => row.kind === 'machine' && row.machine === host && !row.expired);
330
+ const row = rows[0];
331
+ const role = row?.attrs.role;
332
+ if (!row) registry = membershipCheck('registry.member', 'fail', 'no live Nexus machine record', registryExpected);
333
+ else if (rows.length !== 1) registry = membershipCheck('registry.member', 'fail', `${rows.length} live machine records`, registryExpected);
334
+ else if (typeof role !== 'string') registry = membershipCheck('registry.member', 'unmeasured', 'machine attrs.role unavailable', registryExpected);
335
+ else if (role !== intent.role) registry = membershipCheck('registry.member', 'fail', `machine role ${role}`, registryExpected);
336
+ // Existence and role have been measured above; a pass still needs the platform probe.
337
+ else if (platform === null) registry = membershipCheck('registry.member', 'unmeasured', 'host platform unavailable', `intent platform ${intent.platform}`);
338
+ else if (platform !== intent.platform) registry = membershipCheck('registry.member', 'fail', `host platform ${platform}`, `intent platform ${intent.platform}`);
339
+ else registry = membershipCheck('registry.member', 'pass', `live machine ${host} · role ${role}`, registryExpected);
340
+ }
341
+
342
+ const parentExpected = intent ? `taskAgent.parentHost ${host} · ${intent.podPool} · cap ${intent.cap}` : `machines.yaml fleet intent for ${host}`;
343
+ let parent: HostDoctorCheckResult;
344
+ if (membership.intent === null || membership.parentHosts === null) {
345
+ parent = membershipCheck('parentHost.member', 'unmeasured', membership.intent === null
346
+ ? `intent ledger unavailable (repository checkout required)${membership.intentError ? `: ${membership.intentError}` : ''}`
347
+ : `taskAgent.parentHost unavailable${membership.parentHostsError ? `: ${membership.parentHostsError}` : ''}`, parentExpected);
348
+ } else if (!intent) {
349
+ parent = membershipCheck('parentHost.member', 'fail', 'host absent from fleet intent', parentExpected);
350
+ } else {
351
+ const rows = membership.parentHosts.filter((row) => row.host === host);
352
+ const row = rows[0];
353
+ if (intent.cap === 0) parent = membershipCheck('parentHost.member', rows.length === 0 ? 'pass' : 'fail', rows.length === 0 ? 'disabled in both intent and parent config' : 'disabled host still in parent config', 'host absent from taskAgent.parentHost (cap 0)');
354
+ else if (!row) parent = membershipCheck('parentHost.member', 'fail', 'host absent from taskAgent.parentHost', parentExpected);
355
+ else if (rows.length !== 1 || row.podPool !== intent.podPool || row.approvalCapacity !== intent.cap) {
356
+ parent = membershipCheck('parentHost.member', 'fail', `host entries ${rows.length} · pool ${row.podPool ?? '(missing)'} · cap ${row.approvalCapacity ?? '(missing)'}`, parentExpected);
357
+ } else parent = membershipCheck('parentHost.member', 'pass', `host ${host} · pool ${row.podPool} · cap ${row.approvalCapacity}`, parentExpected);
358
+ }
359
+ return [registry, parent];
298
360
  }
299
361
 
300
362
  /** 원격 `uname -s` ⊕ `/proc/version` 의 microsoft 로 darwin|linux|wsl. 못 재면 null. */
@@ -344,9 +406,14 @@ export function runHostDoctor(options: HostDoctorOptions): HostDoctorReport {
344
406
  const platform = reached ? detectHostDoctorPlatform(remote, host) : null;
345
407
  const arch = reached ? detectHostDoctorArch(remote, host) : null;
346
408
  const specs = HOST_DOCTOR_CHECKS.filter((spec) => spec.roles.includes(role));
347
- const checks = reached
409
+ const remoteChecks = reached
348
410
  ? specs.map((spec) => runCheck(spec, { host, remote, role, platform, arch, expected }))
349
411
  : specs.map((spec) => unmeasuredBecauseUnreached(spec, { platform, arch, expected }));
412
+ const checks = [...remoteChecks, ...(role === 'worker' && options.membership ? checkHostMembership(host, platform, options.membership) : [])];
413
+ const mismatch = checks.filter((check) => (check.id === 'registry.member' || check.id === 'parentHost.member') && check.status === 'fail');
414
+ if (mismatch.length) debug.log('fleet.doctor', 'membership-mismatch', { host, checks: mismatch.map((check) => ({ id: check.id, observed: check.observed, expected: check.expected })) });
415
+ const membershipUnmeasured = checks.filter((check) => (check.id === 'registry.member' || check.id === 'parentHost.member') && check.status === 'unmeasured');
416
+ if (membershipUnmeasured.length) debug.log('fleet.doctor', 'membership-unmeasured', { host, checks: membershipUnmeasured.map((check) => ({ id: check.id, observed: check.observed })) });
350
417
  const fail = checks.filter((check) => check.status === 'fail').map((check) => check.id);
351
418
  const unmeasured = checks.filter((check) => check.status === 'unmeasured').map((check) => check.id);
352
419
  const measuredAt = (options.now ?? (() => new Date()))().toISOString();
@@ -365,9 +432,13 @@ export function runHostDoctor(options: HostDoctorOptions): HostDoctorReport {
365
432
  };
366
433
  }
367
434
 
368
- /** 사람 출력 한 줄씩 — «✓|✗|→ <id> <한 줄>» 문법(node-setup.sh --check 와 같다). */
435
+ /** 사람 출력 한 줄씩 — 기본은 «✓|✗|→ <id> <한 줄>»(node-setup.sh --check),
436
+ * membership 미측정은 눈에 보이는 «⚠ <id> unmeasured: <원인>»을 쓴다. */
369
437
  export function formatHostDoctorLines(report: HostDoctorReport): string[] {
370
438
  return report.checks.map((check) => {
439
+ if ((check.id === 'registry.member' || check.id === 'parentHost.member') && check.status === 'unmeasured') {
440
+ return `⚠ ${check.id} unmeasured: ${check.observed}`;
441
+ }
371
442
  const mark = check.status === 'pass' ? '✓' : check.status === 'fail' ? '✗' : '→';
372
443
  const detail = check.status === 'fail' ? `${check.observed} (expected: ${check.expected})` : check.observed;
373
444
  return `${mark} ${check.id} ${detail}`;
@@ -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' } },
@@ -26,6 +26,44 @@ import type { BashNode, NodeExecContext, WorkflowDeps } from '../workflow-runtim
26
26
  import { releaseResumeAcceptedRegressions, releaseResumeGateExecutor, releaseResumeWaiver, type AcceptedRegression, type GateExecutorOverride } from '../../scripts/release-loop/resume.js';
27
27
 
28
28
  type BashRun = WorkflowDeps['runBash'];
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
+ }
29
67
 
30
68
  export interface GraphRunState {
31
69
  graphId: string;
@@ -53,7 +91,9 @@ export interface GraphRunState {
53
91
  graphPath?: string;
54
92
  variant?: { overlayId: string; goal: string; plan: GraphVariantPlan };
55
93
  parentRunId?: string;
56
- 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 }>;
57
97
  growth?: GraphGrowth[];
58
98
  growthRejections?: Array<{ from: string; outcome: string; reason: string }>;
59
99
  growthPark?: { from: string; outcome: string; reason: string; decisionId: string; decisionRef: string; growth: GraphGrowth; command?: GrowthCommand; approval?: string };
@@ -83,7 +123,7 @@ export interface GraphRunOptions {
83
123
  releaseGateExecutor?: { executor: string; host?: string };
84
124
  useCurrentGraph?: boolean;
85
125
  /** Internal unit invocation context, propagated only by runGraph. */
86
- unitContext?: { parentRunId: string; ancestors: readonly string[]; remainingSteps: number };
126
+ unitContext?: { parentRunId: string; ancestors: readonly string[]; remainingSteps: number; entryNodeId?: string };
87
127
  /**
88
128
  * RELEASE-LEDGER-UNIVERSE — stamp every cmd: child with this run's ledger root (`ELANOUS_STATE_DIR`).
89
129
  * Without it a child launched from a non-leader tree (`bun scripts/...` with cwd = that tree) re-resolves
@@ -403,7 +443,14 @@ function nextNode(edges: readonly GraphEdgeSpec[], nodeId: string, exitOutcome:
403
443
 
404
444
  type ResolvedUnit = { path: string; template: GraphTemplateSpec; hash: string };
405
445
 
406
- 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 {
407
454
  const [id, requested] = call.split('@');
408
455
  const repo = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
409
456
  const directories = [join(dirname(callerPath), 'units'), dirname(callerPath), join(repo, 'graphs', 'units'), join(getElanousConfigDir(), 'graphs', 'units')];
@@ -423,16 +470,24 @@ function resolveUnit(call: string, callerPath: string): ResolvedUnit {
423
470
  if (matches.length > 1) throw new Error(`ambiguous unit: ${call}`);
424
471
  if (matches.length) return matches[0]!;
425
472
  }
426
- throw new Error(`unit not found: ${call}`);
473
+ throw new UnitNotFoundError(call);
427
474
  }
428
475
 
429
- 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> {
430
477
  if (ancestors.includes(graph.graphId)) throw new Error(`unit cycle: ${[...ancestors, graph.graphId].join(' -> ')}`);
431
478
  if (ancestors.length > 3) throw new Error('unit nesting exceeds depth 3');
432
479
  const result = new Map<string, ResolvedUnit>();
433
480
  for (const node of graph.nodes) {
434
481
  if (node.kind !== 'subgraph' || node.call === undefined) continue;
435
- 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
+ }
436
491
  const contract = node.contract!;
437
492
  const edge = graph.edges.find(e => e.from === node.nodeId);
438
493
  if (!unit.template.unit || contract.inputs.length !== unit.template.unit.inputs.length ||
@@ -444,7 +499,7 @@ function validateUnitCalls(graph: GraphTemplateSpec, path: string, ancestors: re
444
499
  debug.log('graph.unit', 'contract-mismatch', { graphId: graph.graphId, nodeId: node.nodeId, call: node.call });
445
500
  throw new Error(`unit contract mismatch: ${node.nodeId} -> ${node.call}`);
446
501
  }
447
- validateUnitCalls(unit.template, unit.path, [...ancestors, graph.graphId]);
502
+ validateUnitCalls(unit.template, unit.path, [...ancestors, graph.graphId], onMissing);
448
503
  result.set(node.nodeId, unit);
449
504
  }
450
505
  return result;
@@ -491,6 +546,13 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
491
546
  if (variant && !variant.ok) throw new Error(`graph variant rejected: ${JSON.stringify(variant.rejections)}`);
492
547
  if (variant?.ok) graph = variant.template;
493
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
+ }
494
556
  const variantSource = variant?.ok ? stringifyYaml({ ...document,
495
557
  nodes: graph.nodes.map((node, index) => ({
496
558
  ...((document.nodes as Record<string, unknown>[])[index] ?? {}), node_id: node.nodeId, kind: node.kind,
@@ -536,6 +598,14 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
536
598
  if (!resolved.command && !resolved.prompt && !resolved.approval) throw new Error(`unknown command recipe for ${node.nodeId}: ${node.recipe}`);
537
599
  }
538
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
+ }
539
609
  if (saved && graphId !== saved.graphId) {
540
610
  if (options.fromNodeId && graphMode === 'current') throw new Error('graph or recipes changed since failed run');
541
611
  throw new Error(`run identity mismatch: ${saved.graphId}/${saved.runId}`);
@@ -561,7 +631,9 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
561
631
  : { graphId, runId, startedAt: new Date().toISOString(), ...runnerOwner, status: 'running', path: [], nodes: [], sourceHash: approvalSourceHash,
562
632
  graphSnapshot: { graphSha: createHash('sha256').update(variantSource).digest('hex'), recipesSha: createHash('sha256').update(recipeSource).digest('hex') },
563
633
  graphPath: resolve(path), ...(variant?.ok ? { variant: { overlayId: variant.overlay.overlayId, ...options.variant! } } : {}),
564
- ...(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 };
565
637
  if (options.resumeRunId && graphMode === 'snapshot' && state.sourceHash && state.sourceHash !== approvalSourceHash) {
566
638
  throw new Error('graph snapshot changed since run started');
567
639
  }
@@ -593,7 +665,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
593
665
  const reported = lastJsonObject(prior.output)?.outcome;
594
666
  const next = nextNode(graph.edges, prior.nodeId, prior.ok ? 'ok' : 'fail', typeof reported === 'string' ? reported : undefined);
595
667
  if (next !== recorded.nodeId) throw new Error('saved run path does not follow graph edges');
596
- } 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');
597
669
  if (i < from && !recorded.ok) throw new Error('cannot preserve a failed predecessor when resuming');
598
670
  const approved = resolveNodeRecipe(spec, recipes, catalogRoles).approval;
599
671
  if (approved && i >= from) throw new Error('cannot restart across an approval decision');
@@ -735,7 +807,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
735
807
  if (options.resumeRunId && !options.fromNodeId) {
736
808
  const previousStatus = state.status;
737
809
  Object.assign(state, runnerOwner);
738
- 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 };
739
811
  persistOwnedRun(state, saved!);
740
812
  }
741
813
  if (options.resumeRunId) debug.log('graph.runs', 'resume', { graphId, runId, from: state.resume?.from, graph: graphMode });
@@ -750,7 +822,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
750
822
  : baseLog;
751
823
  const visits = new Map<string, number>();
752
824
  for (const nodeId of state.path) visits.set(nodeId, (visits.get(nodeId) ?? 0) + 1);
753
- 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;
754
826
  if (!options.resumeRunId) {
755
827
  mkdirSync(dirname(statePath), { recursive: true });
756
828
  if (readdirSync(dirname(statePath)).some((file) => file.startsWith(`${runId}.json.`) && file.endsWith('.decision.json'))) {
@@ -787,6 +859,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
787
859
  }
788
860
  const resolved = resolveNodeRecipe(node, recipes, catalogRoles);
789
861
  const command = resolved.command;
862
+ if (enforceReleaseCommandBindings) assertReleaseCommandBinding(node, command?.command, graph.terminalNodes.includes(current));
790
863
  const prompt = resolved.prompt;
791
864
  const approval = resolved.approval ? { approval: approvalMessage(resolved.approval.approval, state.input) } : undefined;
792
865
  if (resumingPending && (!approval || approval.approval !== state.pending?.message)) {
@@ -836,7 +909,13 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
836
909
  const redactCredentials = (text: string) => Object.values(credentials).filter(value => value.length > 0)
837
910
  .reduce((safe, value) => safe.replaceAll(value, '[REDACTED]'), text);
838
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 });
839
917
  const execute = options.deps?.runBash ?? realRunBash((pid) => {
918
+ debug.log('graph.run', 'command-spawn', { ...fingerprint, pid });
840
919
  // Publish the child before its start time is known, so a concurrent stop refuses instead of failing
841
920
  // the ledger while an untracked node keeps working (REL8b review round 3).
842
921
  state.activeNode = { nodeId: current!, pid, pidStartedAt: UNVERIFIED_NODE_START };
@@ -878,7 +957,8 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
878
957
  env.ELANOUS_GRAPH_DRY_RUN = state.dryRun ? '1' : '0';
879
958
  env.ELANOUS_GRAPH_DIR = dirname(resolve(path));
880
959
  const ctx = { arguments: '', artifactsDir: dirname(statePath), outputs: {}, resolvedProvider: undefined, resolvedModel: undefined, toolPolicy: {}, env } as NodeExecContext;
881
- const result = await executeBashNode({ id: current, type: 'bash', bash: state.dryRun ? command.dry_run_command! : command.command, ...(command.timeout_ms ? { idle_timeout: command.timeout_ms } : {}) } as BashNode, ctx, { runBash } as WorkflowDeps);
960
+ const result = await executeBashNode({ id: current, type: 'bash', bash: state.dryRun ? command.dry_run_command! : command.command,
961
+ idle_timeout: command.timeout_ms ?? DEFAULT_BASH_TIMEOUT_MS } as BashNode, ctx, { runBash } as WorkflowDeps);
882
962
  output = result.output;
883
963
  ok = result.ok;
884
964
  exit = code;
@@ -904,7 +984,10 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
904
984
  const alreadySpent = state.path.length + (state.unitCalls ?? []).reduce((sum, call) => sum + call.steps, 0);
905
985
  const remaining = options.unitContext ? options.unitContext.remainingSteps - alreadySpent : Infinity;
906
986
  const childRunId = randomUUID();
907
- 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 } : {}) };
908
991
  let entered = false;
909
992
  try {
910
993
  for (const key of contract.inputs) if (!Object.hasOwn(inputs, key)) throw new Error(`unit input missing: ${current}.${key}`);
@@ -916,7 +999,8 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
916
999
  const child = await runGraph(unit.path, { runId: childRunId, input: inputs, dryRun: false,
917
1000
  pinChildUniverse: options.pinChildUniverse,
918
1001
  unitContext: { parentRunId: runId, ancestors: [...(options.unitContext?.ancestors ?? []), graphId],
919
- 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 });
920
1004
  const steps = child.path.length + (child.unitCalls ?? []).reduce((sum, call) => sum + call.steps, 0);
921
1005
  const childExit = child.status === 'done' || child.status === 'failed' ? child.path.at(-1) : undefined;
922
1006
  // The last successful record carrying every contracted output is the source;
@@ -930,7 +1014,8 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
930
1014
  }, undefined) ?? {};
931
1015
  state.unitCalls ??= [];
932
1016
  state.unitCalls.push({ node: current, unit: unit.template.graphId, version: unit.template.version,
933
- hash: unit.hash, childRunId, exit: childExit ?? child.status, steps });
1017
+ hash: unit.hash, childRunId, exit: childExit ?? child.status, steps,
1018
+ ...(recordEntry ? { entry: selectedEntry } : {}) });
934
1019
  if (child.status === 'awaiting-approval') throw new Error(`unit cannot pause for approval: ${current}`);
935
1020
  if (child.status === 'budget-exceeded' || steps > node.budget!.innerSteps || steps > remaining) {
936
1021
  debug.log('graph.unit', 'budget-exhausted', { ...info, steps, innerSteps: node.budget!.innerSteps });
@@ -1003,6 +1088,10 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
1003
1088
  ...(approval && state.pending ? { ...(state.pending.decidedBy === undefined ? {} : { decidedBy: state.pending.decidedBy }), decidedAt: state.pending.decidedAt } : {}),
1004
1089
  startedAt: nodeStartedAt, endedAt: nodeEndedAt, seconds });
1005
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
+ }
1006
1095
  if (approval) {
1007
1096
  delete state.pending;
1008
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 {