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
@@ -1,13 +1,18 @@
1
1
  import type { Command } from 'commander';
2
- import { canonicalSeatId, openMsgStore, type Message, type MsgStore } from '../msg/msg-store.js';
2
+ import { ensureAuthToken } from '../auth/acp-token.js';
3
+ import { canonicalSeatId, openMsgStore, openMsgStoreReadOnly, type Message, type MsgStore } from '../msg/msg-store.js';
4
+ import { startMsgSseServer } from '../msg/msg-sse-server.js';
3
5
 
4
6
  export interface MsgCliDeps {
5
7
  openStore?: () => MsgStore;
8
+ openReadStore?: () => MsgStore;
6
9
  out?: Pick<Console, 'log' | 'error'>;
7
10
  readStdin?: () => Promise<string>;
8
11
  sleep?: (ms: number) => Promise<void>;
9
12
  setExitCode?: (code: number) => void;
10
13
  signal?: AbortSignal;
14
+ fetch?: typeof fetch;
15
+ authToken?: () => string;
11
16
  }
12
17
 
13
18
  type CommonOptions = { json?: boolean };
@@ -33,6 +38,7 @@ function formatMessage(message: Message): string {
33
38
  /** Register the durable, recipient-addressed CLI. Reading never advances the acknowledgement cursor. */
34
39
  export function registerMsgCommands(program: Command, deps: MsgCliDeps = {}): void {
35
40
  const open = deps.openStore ?? (() => openMsgStore());
41
+ const openRead = deps.openReadStore ?? (() => openMsgStoreReadOnly());
36
42
  const out = deps.out ?? console;
37
43
  const readStdin = deps.readStdin ?? (() => Bun.stdin.text());
38
44
  const sleep = deps.sleep ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)));
@@ -57,13 +63,20 @@ export function registerMsgCommands(program: Command, deps: MsgCliDeps = {}): vo
57
63
  };
58
64
 
59
65
  msg.command('post [body]').description('Post a message; use - or omit body to read stdin')
60
- .option('--from <seat>').option('--to <seat>').option('--kind <kind>').option('--body <text>').option('--json')
61
- .action((body: string | undefined, opts: CommonOptions & { from?: string; to?: string; kind?: string; body?: string }) => run(async () => {
66
+ .option('--from <seat>').option('--to <seat>').option('--kind <kind>').option('--body <text>')
67
+ .option('--reply-to <id>', 'point this reply at a question message ID')
68
+ .option('--origin-key <key>', 'deduplicate a channel mirror by coord:<commentId>:<recipient>').option('--json')
69
+ .action((body: string | undefined, opts: CommonOptions & { from?: string; to?: string; kind?: string; body?: string; replyTo?: string; originKey?: string }) => run(async () => {
62
70
  const from = seat(opts.from, '--from');
63
71
  const to = seat(opts.to, '--to');
64
72
  if (body !== undefined && body !== '-' && opts.body !== undefined) throw new Error('provide body once (argument or --body)');
65
73
  const text = opts.body ?? (body !== undefined && body !== '-' ? body : await readStdin());
66
- const posted = withStore(store => store.post({ from, to, body: text, ...(opts.kind === undefined ? {} : { kind: opts.kind }) }));
74
+ const envelope = { from, to, body: text,
75
+ ...(opts.kind === undefined ? {} : { kind: opts.kind }),
76
+ ...(opts.replyTo === undefined ? {} : { replyTo: integer(opts.replyTo, '--reply-to', 0, 1, Number.MAX_SAFE_INTEGER) }) };
77
+ const posted = withStore(store => opts.originKey === undefined
78
+ ? store.post(envelope)
79
+ : store.appendWithProvenance(envelope, opts.originKey));
67
80
  emit(posted, opts.json, () => out.log(formatMessage(posted)));
68
81
  })());
69
82
 
@@ -95,25 +108,32 @@ export function registerMsgCommands(program: Command, deps: MsgCliDeps = {}): vo
95
108
  .option('--to <seat>').option('--json')
96
109
  .action((opts: CommonOptions & { to?: string }) => run(() => {
97
110
  const to = opts.to === undefined ? undefined : seat(opts.to, '--to');
98
- const rows = withStore(store => store.unread().filter(row => to === undefined || row.recipient === to));
111
+ const rows = withStore(store => {
112
+ if (to === undefined) return store.unread();
113
+ const count = store.unreadCount(to);
114
+ return count ? [{ recipient: to, count }] : [];
115
+ });
99
116
  emit(rows, opts.json, () => {
100
117
  if (!rows.length) out.log('No unread messages.');
101
118
  for (const row of rows) out.log(`${row.recipient}: ${row.count} unread`);
102
119
  });
103
120
  })());
104
121
 
105
- msg.command('watch').description('Poll for new messages without acknowledging them (Ctrl-C to stop)')
122
+ msg.command('watch').description('Watch durable messages; SSE accelerates delivery and polling covers outages')
106
123
  .option('--to <seat>').option('--after <id>', 'exclusive message ID (default: acknowledged cursor)')
124
+ .option('--since-ack', 'start from the recipient seat’s acknowledged cursor')
107
125
  .option('--interval <ms>', 'poll interval in milliseconds', '1000')
126
+ .option('--sse <url>', 'post office base URL (fall back to ledger polling on disconnect)')
108
127
  .option('--once', 'poll once and exit').option('--json')
109
- .action((opts: RecipientOptions & { interval?: string; once?: boolean }) => run(async () => {
128
+ .action((opts: RecipientOptions & { interval?: string; once?: boolean; sinceAck?: boolean; sse?: string }) => run(async () => {
129
+ if (opts.sinceAck && opts.after !== undefined) throw new Error('--since-ack cannot be used with --after');
110
130
  const to = seat(opts.to, '--to');
111
131
  const interval = integer(opts.interval, '--interval', 1000, 1, 60000);
112
132
  const after = opts.after === undefined ? undefined : integer(opts.after, '--after', 0, 0, Number.MAX_SAFE_INTEGER);
113
133
  const store = open();
114
134
  try {
115
135
  let cursor = after ?? store.getCursor(to);
116
- do {
136
+ const poll = () => {
117
137
  let messages: Message[];
118
138
  do {
119
139
  messages = store.list(to, cursor, 1000);
@@ -122,12 +142,158 @@ export function registerMsgCommands(program: Command, deps: MsgCliDeps = {}): vo
122
142
  cursor = message.id;
123
143
  }
124
144
  } while (messages.length === 1000 && !deps.signal?.aborted);
125
- if (opts.once || deps.signal?.aborted) break;
126
- await sleep(interval);
127
- } while (!deps.signal?.aborted);
145
+ };
146
+ if (!opts.sse || opts.once) {
147
+ do {
148
+ poll();
149
+ if (opts.once || deps.signal?.aborted) break;
150
+ await sleep(interval);
151
+ } while (!deps.signal?.aborted);
152
+ } else {
153
+ const url = new URL('/stream', opts.sse);
154
+ if (!['http:', 'https:'].includes(url.protocol)) throw new Error('--sse requires an HTTP URL');
155
+ if (url.protocol === 'http:' && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))
156
+ throw new Error('--sse requires HTTPS outside localhost');
157
+ url.searchParams.set('to', to);
158
+ const token = (deps.authToken ?? (() => ensureAuthToken().token))();
159
+ let outage = false;
160
+ while (!deps.signal?.aborted) {
161
+ poll();
162
+ url.searchParams.set('since', String(cursor));
163
+ const controller = new AbortController();
164
+ const onAbort = () => controller.abort();
165
+ deps.signal?.addEventListener('abort', onAbort, { once: true });
166
+ try {
167
+ let connectTimer: ReturnType<typeof setTimeout> | undefined;
168
+ const response = await Promise.race([
169
+ (deps.fetch ?? fetch)(url, { headers: {
170
+ Authorization: `Bearer ${token}`, 'Last-Event-ID': String(cursor),
171
+ }, signal: controller.signal }),
172
+ new Promise<never>((_, reject) => { connectTimer = setTimeout(() => {
173
+ controller.abort();
174
+ reject(new Error('SSE connect timeout'));
175
+ }, Math.max(1000, Math.min(5000, interval * 2))); }),
176
+ ]).finally(() => clearTimeout(connectTimer));
177
+ if (response.status === 401) throw new Error('SSE unauthorized (401)');
178
+ if (!response.ok || !response.body) throw new Error(`SSE unavailable (${response.status})`);
179
+ outage = false;
180
+ const reader = response.body.getReader();
181
+ const decoder = new TextDecoder();
182
+ let pending = '';
183
+ while (!deps.signal?.aborted) {
184
+ let idleTimer: ReturnType<typeof setTimeout> | undefined;
185
+ const chunk = await Promise.race([
186
+ reader.read(),
187
+ new Promise<never>((_, reject) => { idleTimer = setTimeout(() => {
188
+ controller.abort();
189
+ reject(new Error('SSE idle timeout'));
190
+ }, Math.max(1000, Math.min(5000, interval * 2))); }),
191
+ ]).finally(() => clearTimeout(idleTimer));
192
+ if (chunk.done) break;
193
+ pending += decoder.decode(chunk.value, { stream: true });
194
+ pending = pending.replace(/\r\n/g, '\n');
195
+ if (pending.length > 1_000_000) throw new Error('SSE event exceeds limit');
196
+ let boundary: number;
197
+ while ((boundary = pending.indexOf('\n\n')) >= 0) {
198
+ const event = pending.slice(0, boundary);
199
+ pending = pending.slice(boundary + 2);
200
+ if (event.split('\n').some((line) => line.startsWith('id: '))) poll();
201
+ }
202
+ }
203
+ } catch (error) {
204
+ if (error instanceof Error && error.message === 'SSE unauthorized (401)') throw error;
205
+ if (!outage && !deps.signal?.aborted) out.error(`msg: SSE disconnected; polling ledger (${error instanceof Error ? error.message : String(error)})`);
206
+ outage = true;
207
+ } finally { deps.signal?.removeEventListener('abort', onAbort); controller.abort(); }
208
+ if (deps.signal?.aborted) break;
209
+ poll();
210
+ await sleep(interval);
211
+ }
212
+ }
128
213
  } finally { store.close(); }
129
214
  })());
130
215
 
216
+ msg.command('ask <recipient> <question>').description('Post a question and wait for its first reply')
217
+ .option('--from <seat>', 'asking seat (required)').option('--wait <seconds>', 'seconds to wait for a reply (default 60)', '60')
218
+ .option('--json')
219
+ .action((recipient: string, question: string, opts: CommonOptions & { from?: string; wait?: string }) => run(async () => {
220
+ const from = seat(opts.from, '--from');
221
+ const to = canonicalSeatId(recipient);
222
+ const waitSeconds = integer(opts.wait, '--wait', 60, 0, 86400);
223
+ const interrupt = deps.signal === undefined ? new AbortController() : undefined;
224
+ const signal = deps.signal ?? interrupt!.signal;
225
+ const onInterrupt = () => interrupt?.abort();
226
+ if (interrupt) {
227
+ process.once('SIGINT', onInterrupt);
228
+ process.once('SIGTERM', onInterrupt);
229
+ }
230
+ let store: MsgStore | undefined;
231
+ try {
232
+ const asked = withStore(ledger => ledger.post({ from, to, body: question }));
233
+ const deadline = Date.now() + waitSeconds * 1000;
234
+ let reply: Message | undefined;
235
+ store = open();
236
+ do {
237
+ reply = store.listReplies(asked.id, 1)[0];
238
+ if (reply !== undefined || signal.aborted || Date.now() >= deadline) break;
239
+ await sleep(1000);
240
+ } while (true);
241
+ if (reply === undefined && signal.aborted) throw new Error(`ask interrupted after posting #${asked.id}`);
242
+ if (reply === undefined) throw new Error(`no reply to #${asked.id} within ${waitSeconds}s`);
243
+ emit(reply, opts.json, () => out.log(formatMessage(reply)));
244
+ } finally {
245
+ store?.close();
246
+ if (interrupt) {
247
+ process.removeListener('SIGINT', onInterrupt);
248
+ process.removeListener('SIGTERM', onInterrupt);
249
+ }
250
+ }
251
+ })());
252
+
253
+ msg.command('thread <id>').description('Show one question and its replies in time order')
254
+ .option('--json')
255
+ .action((id: string, opts: CommonOptions) => run(() => {
256
+ const target = integer(id, 'message ID', 0, 1, Number.MAX_SAFE_INTEGER);
257
+ const thread = withStore(store => {
258
+ const question = store.getMessage(target);
259
+ if (question === null) throw new Error(`message not found: #${target}`);
260
+ // Keep paging until a short page so a thread longer than one store page is never truncated.
261
+ const replies: Message[] = [];
262
+ let after = 0;
263
+ let page: Message[];
264
+ do {
265
+ page = store.listReplies(target, 100, after);
266
+ replies.push(...page);
267
+ after = page.at(-1)?.id ?? after;
268
+ } while (page.length === 100);
269
+ return { question, replies };
270
+ });
271
+ emit(thread, opts.json, () => {
272
+ out.log(formatMessage(thread.question));
273
+ for (const reply of thread.replies) out.log(formatMessage(reply));
274
+ });
275
+ })());
276
+
277
+ msg.command('serve').description('Run the independent, read-only post office SSE service')
278
+ .option('--sse', 'serve GET /stream')
279
+ .option('--port <port>', 'port (or ELANOUS_MSG_SSE_PORT)')
280
+ .option('--host <host>', 'bind address', '127.0.0.1')
281
+ .action((opts: { sse?: boolean; port?: string; host?: string }) => run(() => {
282
+ if (!opts.sse) throw new Error('serve requires --sse');
283
+ const rawPort = opts.port ?? process.env.ELANOUS_MSG_SSE_PORT;
284
+ if (rawPort === undefined) throw new Error('--port or ELANOUS_MSG_SSE_PORT is required');
285
+ const port = integer(rawPort, '--port', 0, 0, 65535);
286
+ const store = openRead();
287
+ try {
288
+ const service = startMsgSseServer({ store, token: (deps.authToken ?? (() => ensureAuthToken().token))(),
289
+ port, hostname: opts.host });
290
+ out.log(`msg SSE listening on ${opts.host}:${service.port}`);
291
+ const stop = () => { service.stop(); store.close(); };
292
+ process.once('SIGINT', stop);
293
+ process.once('SIGTERM', stop);
294
+ } catch (error) { store.close(); throw error; }
295
+ })());
296
+
131
297
  // Commander reports option/argument errors before action handlers run. Scope their exit code
132
298
  // to msg so unrelated commands keep their own error and help behavior.
133
299
  for (const command of [msg, ...msg.commands]) {
@@ -6,7 +6,8 @@ import { existsSync, readFileSync } from 'node:fs';
6
6
  import { elanousStateRoot } from '../autopilot/state-paths.js';
7
7
  import { userConfigPath } from '../user-config.js';
8
8
  import { debug, redactSecretText } from '../debug/log.js';
9
- import { addMarket, ensureInternalMarketIndex, listMarkets, MarketFetchError, updateMarket } from '../plugins/install/market-fetch.js';
9
+ import { addMarket, codexPluginPackagePath, ensureInternalMarketIndex, listMarkets, MarketFetchError, updateMarket } from '../plugins/install/market-fetch.js';
10
+ import { readCodexPluginList } from '../acp/codex-plugins.js';
10
11
  import { emitDecision } from '../live/detail-switch.js';
11
12
  import { OFFICIAL_INDEX_KEYS } from '../market/official-keys.js';
12
13
  import { installPlugin, listInstalledPlugins, PluginInstallError, removePlugin, type InstallEvent } from '../plugins/install/plugin-install.js';
@@ -118,15 +119,22 @@ export function registerPluginCommands(program: Command): void {
118
119
  }
119
120
  });
120
121
  plugin.command('add <spec>').description('Install a local path, pinned git plugin or signed market plugin')
122
+ .option('--from-codex', 'Import an installed Codex plugin by name@marketplace')
121
123
  .option('--yes', 'Accept requested capabilities')
122
124
  .option('--json', 'NDJSON progress events')
123
125
  .option('--allow-unsigned', 'Allow unsigned marketplace indices')
124
126
  .option('--refresh', 'Refresh the signed marketplace index before installing')
125
- .action(async (spec: string, opts: { yes?: boolean; json?: boolean; allowUnsigned?: boolean; refresh?: boolean }) => {
127
+ .action(async (spec: string, opts: { yes?: boolean; json?: boolean; allowUnsigned?: boolean; refresh?: boolean; fromCodex?: boolean }) => {
126
128
  try {
127
129
  const root = elanousStateRoot();
128
130
  const nodeCount = { value: 0 };
129
- await installPlugin(spec, {
131
+ let source = spec;
132
+ if (opts.fromCodex) {
133
+ const match = /^([a-z0-9][a-z0-9-]{1,39})@([a-z0-9][a-z0-9-]{1,39})$/.exec(spec);
134
+ if (!match) throw new PluginInstallError('io', 'Codex import requires name@marketplace');
135
+ source = codexPluginPackagePath(await readCodexPluginList(), match[1]!, match[2]!);
136
+ }
137
+ await installPlugin(source, {
130
138
  root, marketDir: join(root, 'plugins', 'markets'), trustedKeys: keys(),
131
139
  yes: opts.yes, allowUnsigned: opts.allowUnsigned, refresh: opts.refresh,
132
140
  onEvent: event => outputEvent(event, !!opts.json, nodeCount),
package/src/cli/pr-cli.ts CHANGED
@@ -58,7 +58,9 @@ import { parseReleaseNoteSection } from '../release-loop/release-note.js';
58
58
  import { landingFreezeMessage } from '../release-loop/landing-freeze.js';
59
59
  import { admitLandingMerge } from '../self-implement/frozen-merges.js';
60
60
  import { verifyAncestorAutoPass, verifyOverlapEvidence } from './pr-land-overlap-evidence.js';
61
+ import { observePrLandRefusal } from './pr-land-refusal.js';
61
62
  import { taskAgentStatePath } from '../task-agent/task-hand.js';
63
+ import { PR_LABEL_NAMES } from '../github/pr-labels.js';
62
64
  import { enqueueL8ShadowQueue, runL8ShadowQueue, shadowPrNumber, statusL8ShadowQueue, type L8ShadowQueueDeps } from '../self-implement/l8-merge-queue.js';
63
65
 
64
66
  export { branchLineageSlug, findSiblingPrs };
@@ -1143,8 +1145,36 @@ export function runPrGranularity(opts: PrGranularityOpts = {}, deps: PrLandDeps
1143
1145
  const GITHUB_MERGEABLE = new Set(['MERGEABLE', 'CONFLICTING', 'UNKNOWN']);
1144
1146
  const GITHUB_MERGE_STATE = new Set(['BEHIND', 'BLOCKED', 'CLEAN', 'DIRTY', 'DRAFT', 'HAS_HOOKS', 'UNKNOWN', 'UNSTABLE']);
1145
1147
 
1148
+ /** Read-only identity check shared by pinned landing and its dry-run. Later landing gates still run only on a real landing. */
1149
+ function inspectPinnedPr(run: CmdRunner, cwd: string, pr: number, expectedHead: string) {
1150
+ const viewed = run('gh', ['pr', 'view', String(pr), '--json', 'headRefOid,headRefName,state,isDraft,url'], { cwd });
1151
+ let value: { headRefOid?: string; headRefName?: string; state?: string; isDraft?: boolean; url?: string } = {};
1152
+ try {
1153
+ if (viewed.ok) {
1154
+ const parsed: unknown = JSON.parse(viewed.out);
1155
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) value = parsed as typeof value;
1156
+ }
1157
+ } catch { /* unreadable is not approval */ }
1158
+ const local = run('git', ['rev-parse', 'HEAD'], { cwd });
1159
+ const dirty = run('git', ['status', '--porcelain'], { cwd });
1160
+ const reasons: string[] = [];
1161
+ if (value.state !== 'OPEN') reasons.push('PR is not OPEN or its state is unavailable');
1162
+ if (value.isDraft !== false) reasons.push(value.isDraft === true ? 'PR is draft' : 'PR ready status is unavailable');
1163
+ if (value.headRefOid?.toLowerCase() !== expectedHead.toLowerCase()) reasons.push('PR head differs from --expected-head or is unavailable');
1164
+ if (!value.url?.endsWith(`/pull/${pr}`) || !value.headRefName) reasons.push('PR identity or head branch is unavailable');
1165
+ if (!local.ok || local.out.trim().toLowerCase() !== expectedHead.toLowerCase()) reasons.push('local HEAD differs from --expected-head or is unavailable');
1166
+ if (!dirty.ok || dirty.out.trim()) reasons.push(dirty.ok ? 'local worktree is dirty' : 'local worktree status is unavailable');
1167
+ return { ok: reasons.length === 0, reasons, branch: value.headRefName, url: value.url };
1168
+ }
1169
+
1146
1170
  export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): Promise<number> {
1147
1171
  const out = deps.out ?? liveOut;
1172
+ // First slice: observe pinned identity/branch, overlap decision, and GitHub merge refusals.
1173
+ // Option syntax and prerequisite gate errors do not describe a merge recovery edge.
1174
+ const refuse = (message: string, context: { isDraft?: boolean } = {}): void => {
1175
+ out.error(message);
1176
+ observePrLandRefusal(message, context);
1177
+ };
1148
1178
  if (opts.title !== undefined && opts.titleFile !== undefined) {
1149
1179
  out.error('✗ title: --title과 --title-file은 함께 사용할 수 없습니다.');
1150
1180
  return 1;
@@ -1195,6 +1225,84 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1195
1225
  const cwd = opts.cwd ?? process.cwd();
1196
1226
  const run = deps.run ?? defaultCmdRunner;
1197
1227
  const pinned = opts.pr !== undefined || opts.expectedHead !== undefined;
1228
+ if (opts.dryRun && opts.pr !== undefined && opts.expectedHead === undefined) {
1229
+ if (opts.overlapEvidence !== undefined || opts.hold) {
1230
+ out.error('✗ dry-run: --overlap-evidence 와 --hold 는 번호 지정 읽기 전용 미리보기에서 사용할 수 없습니다.');
1231
+ return 1;
1232
+ }
1233
+ const pr = Number(opts.pr);
1234
+ if (!/^[1-9]\d*$/.test(opts.pr) || !Number.isSafeInteger(pr)) {
1235
+ out.error('✗ dry-run: --pr 에는 양의 정수를 지정해야 합니다.');
1236
+ return 1;
1237
+ }
1238
+ const viewed = run('gh', ['pr', 'view', String(pr), '--json', 'headRefOid,state,headRefName,isDraft,mergeable,mergeStateStatus,url'], { cwd });
1239
+ if (!viewed.ok) {
1240
+ out.error(`✗ dry-run: PR #${pr} 를 조회하지 못했습니다 — 존재하지 않거나 GitHub 조회가 실패했습니다.`);
1241
+ return 1;
1242
+ }
1243
+ let value: { headRefOid?: string; state?: string; headRefName?: string; isDraft?: boolean; mergeable?: string; mergeStateStatus?: string; url?: string };
1244
+ try {
1245
+ const parsed: unknown = JSON.parse(viewed.out);
1246
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('not an object');
1247
+ value = parsed as typeof value;
1248
+ }
1249
+ catch {
1250
+ out.error(`✗ dry-run: PR #${pr} 조회 결과를 읽을 수 없습니다.`);
1251
+ return 1;
1252
+ }
1253
+ if (typeof value.headRefOid !== 'string' || !/^[a-f0-9]{40}$/i.test(value.headRefOid) || value.state !== 'OPEN') {
1254
+ out.error(`✗ dry-run: PR #${pr} 가 열려 있지 않거나 head 정보가 없습니다 (state=${value.state ?? 'unmeasured'}).`);
1255
+ return 1;
1256
+ }
1257
+ out.log(`PR #${pr} · head ${value.headRefOid} · state ${value.state}${value.headRefName ? ` · branch ${value.headRefName}` : ''}`);
1258
+ out.log('Landing plan (read-only preview):');
1259
+ out.log(`- GitHub PR state/head: PASS (${value.state}, ${value.headRefOid})`);
1260
+ if (value.isDraft === true) out.log('- Draft status: GitHub reports DRAFT; landing eligibility UNMEASURED.');
1261
+ else if (value.isDraft === false) out.log('- Draft status: observed — ready for review.');
1262
+ else if (value.isDraft !== false) out.log('- Draft status: UNMEASURED');
1263
+ if (value.mergeable === 'CONFLICTING') out.log('- Warning: GitHub reports merge conflicts; landing eligibility UNMEASURED.');
1264
+ else if (value.mergeable === 'MERGEABLE') out.log('- Mergeability: GitHub reports MERGEABLE; repository landing gates remain UNMEASURED.');
1265
+ else out.log(`- Mergeability: UNMEASURED${value.mergeable ? ` (${value.mergeable})` : ''}`);
1266
+ if (value.mergeStateStatus === 'BLOCKED' || value.mergeStateStatus === 'DIRTY' || value.mergeStateStatus === 'DRAFT') {
1267
+ out.log(`- Warning: GitHub merge state is ${value.mergeStateStatus}; landing eligibility UNMEASURED.`);
1268
+ } else if (value.mergeStateStatus && value.mergeStateStatus !== 'CLEAN') {
1269
+ out.log(`- GitHub merge state: ${value.mergeStateStatus} (additional requirements may apply; landing eligibility UNMEASURED).`);
1270
+ }
1271
+ out.log('- Local HEAD/worktree, branch/base, and same-branch guard: UNMEASURED — local git is not run');
1272
+ out.log('- Changed-file scope, active-run files, untracked files, plan artifacts, and deletion advisory: UNMEASURED — local files are not read');
1273
+ out.log('- Typecheck, isolation, mock-module restore, model-hardcode, daemon-port, test-interference, and global checks: UNMEASURED — gates are not run');
1274
+ out.log('- Public export/import, public leak, and docs CLI checks: UNMEASURED — gates are not run');
1275
+ out.log('- Android, iOS, and PWA gates: CONDITIONAL / UNMEASURED — changed-file applicability and builds are not checked');
1276
+ out.log('- Freeze, PR base/bench guards, sibling PRs, recent landings, overlap, and approval: UNMEASURED — not queried or approved');
1277
+ out.log('- GitHub draft/mergeability status: observed above; this preview does not approve or merge the PR.');
1278
+ out.log('- Mutations (ready/edit/create/merge, git commit/push, worktree): NOT RUN');
1279
+ return 0;
1280
+ }
1281
+ if (opts.dryRun && pinned) {
1282
+ const pr = Number(opts.pr);
1283
+ if (!/^[1-9]\d*$/.test(opts.pr ?? '') || !Number.isSafeInteger(pr) || !/^[a-f0-9]{40}$/i.test(opts.expectedHead ?? '')) {
1284
+ out.error('✗ expected-head: --pr and --expected-head (full SHA) are required together without --hold');
1285
+ return 1;
1286
+ }
1287
+ if (opts.hold) {
1288
+ out.error('✗ dry-run: --hold 는 머리 고정 읽기 전용 예행과 함께 사용할 수 없습니다.');
1289
+ return 1;
1290
+ }
1291
+ if (opts.overlapEvidence !== undefined) {
1292
+ out.error('✗ dry-run: --overlap-evidence 는 머리 고정 읽기 전용 예행에서 사용할 수 없습니다.');
1293
+ return 1;
1294
+ }
1295
+ const inspected = inspectPinnedPr(run, cwd, pr, opts.expectedHead!);
1296
+ const branch = (deps.currentBranch ?? liveCurrentBranch)(cwd);
1297
+ const reasons = [...inspected.reasons];
1298
+ if (!branch || branch !== inspected.branch) reasons.push('current branch differs from the PR head branch or is unavailable');
1299
+ out.log(`[dry-run] pinned PR #${pr} · expected head ${opts.expectedHead}`);
1300
+ for (const reason of reasons) out.log(`[dry-run] ✗ preflight: ${reason}`);
1301
+ if (reasons.includes('PR is draft')) out.log(`[dry-run] 선행: eln gh pr ready ${pr}`);
1302
+ if (reasons.length === 0) out.log('[dry-run] ✓ preflight: PR OPEN non-draft, matching PR/local HEAD and branch, clean worktree');
1303
+ out.log('[dry-run] Remaining landing gates, freeze, overlap and merge: UNMEASURED; no PR or git state changed.');
1304
+ return reasons.length === 0 ? 0 : 1;
1305
+ }
1198
1306
  if (opts.overlapEvidence !== undefined && (!pinned || !opts.overlapEvidence.trim())) {
1199
1307
  out.error('✗ overlap-evidence: --overlap-evidence <file> 는 --pr ⊕ --expected-head 머리 고정 착지에서만 쓴다.');
1200
1308
  return 1;
@@ -1203,30 +1311,26 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1203
1311
  let pinnedUrl: string | undefined;
1204
1312
  if (pinned) {
1205
1313
  const pr = Number(opts.pr);
1206
- if (!/^[1-9]\d*$/.test(opts.pr ?? '') || !Number.isSafeInteger(pr) || !/^[a-f0-9]{40}$/i.test(opts.expectedHead ?? '') || opts.hold || opts.dryRun) {
1207
- out.error('✗ expected-head: --pr and --expected-head (full SHA) are required together without --hold/--dry-run');
1314
+ if (!/^[1-9]\d*$/.test(opts.pr ?? '') || !Number.isSafeInteger(pr) || !/^[a-f0-9]{40}$/i.test(opts.expectedHead ?? '') || opts.hold) {
1315
+ out.error('✗ expected-head: --pr and --expected-head (full SHA) are required together without --hold');
1208
1316
  return 1;
1209
1317
  }
1210
- const viewed = run('gh', ['pr', 'view', String(pr), '--json', 'headRefOid,headRefName,state,isDraft,url'], { cwd });
1211
- let value: { headRefOid?: string; headRefName?: string; state?: string; isDraft?: boolean; url?: string } = {};
1212
- try { if (viewed.ok) value = JSON.parse(viewed.out) as typeof value; } catch { /* unreadable is not approval */ }
1213
- const local = run('git', ['rev-parse', 'HEAD'], { cwd });
1214
- const dirty = run('git', ['status', '--porcelain'], { cwd });
1215
- if (value.state !== 'OPEN' || value.isDraft !== false || value.headRefOid?.toLowerCase() !== opts.expectedHead!.toLowerCase()
1216
- || !value.url?.endsWith(`/pull/${pr}`) || !value.headRefName || !local.ok || local.out.trim().toLowerCase() !== opts.expectedHead!.toLowerCase()
1217
- || !dirty.ok || dirty.out.trim()) {
1218
- out.error('✗ expected-head: PR OPEN non-draft, matching local HEAD and clean worktree required');
1318
+ const inspected = inspectPinnedPr(run, cwd, pr, opts.expectedHead!);
1319
+ if (!inspected.ok) {
1320
+ refuse('✗ expected-head: PR OPEN non-draft, matching local HEAD and clean worktree required', {
1321
+ isDraft: inspected.reasons.includes('PR is draft'),
1322
+ });
1219
1323
  return 1;
1220
1324
  }
1221
- pinnedBranch = value.headRefName;
1222
- pinnedUrl = value.url;
1325
+ pinnedBranch = inspected.branch;
1326
+ pinnedUrl = inspected.url;
1223
1327
  }
1224
1328
  const resolveBase = deps.resolveBase ?? resolveDeliverableBase;
1225
1329
  const branch = (deps.currentBranch ?? liveCurrentBranch)(cwd);
1226
1330
  if (!branch || (pinned && branch !== pinnedBranch)) {
1227
1331
  const detail = pinned ? 'PR head branch와 현재 브랜치가 다릅니다.' : '현재 브랜치를 해석하지 못했습니다.';
1228
1332
  record('branch', false);
1229
- out.error(`✗ branch: ${detail}`);
1333
+ refuse(`✗ branch: ${detail}`);
1230
1334
  return 1;
1231
1335
  }
1232
1336
  record('branch', true);
@@ -1611,6 +1715,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1611
1715
  const hold = !!opts.hold || overlapDecision?.outcome === 'user-hold';
1612
1716
  // TA-LAND-OVERLAP — 비대화형 «승인 불가»만 증거로 대신한다(사람의 hold 는 증거로 뒤집지 않는다) · 증거가 없거나 어긋나면 종전 거부 그대로.
1613
1717
  let overlapEvidenceAccepted = false;
1718
+ let ancestorResyncNeeded = false;
1614
1719
  if (pinned && overlapDecision?.outcome === 'unavailable' && opts.overlapEvidence !== undefined) {
1615
1720
  const ref = resolveRemoteBranchRef(run, cwd, base);
1616
1721
  const verdict = verifyOverlapEvidence({
@@ -1625,7 +1730,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1625
1730
  ? `✓ overlap-evidence: base-advanced-disjoint — gate base ${verdict.gateBase.slice(0, 12)} ⊂ head ${opts.expectedHead!.slice(0, 12)} · ${ref.remote}/${ref.branch} ${verdict.baseTip.slice(0, 12)} 까지 ${verdict.windowCommits}개 커밋이 PR 파일을 안 건드린다 · review pass|warn must-fix 0 (${verdict.reviewResult})`
1626
1731
  : `✓ overlap-evidence: ${ref.remote}/${ref.branch} ${verdict.baseTip.slice(0, 12)} ⊂ head ${opts.expectedHead!.slice(0, 12)} · gate passed on that base · review pass must-fix 0 (${verdict.reviewResult})`);
1627
1732
  }
1628
- else out.error(`✗ overlap-evidence: ${verdict.reason}`);
1733
+ else refuse(`✗ overlap-evidence: ${verdict.reason}`);
1629
1734
  } else if (pinned && overlapDecision?.outcome === 'unavailable') {
1630
1735
  // 증거 파일이 없을 때 — 겹친 착지가 전부 머리의 조상이고 표준 자리의 리뷰가 pass|warn ⊕ must-fix 0 이면 통과(아니면 종전 거부).
1631
1736
  const verdict = verifyAncestorAutoPass({
@@ -1637,9 +1742,19 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1637
1742
  if (verdict.ok) {
1638
1743
  out.log(`✓ overlap-evidence: already-ancestor-auto — 겹친 최근 착지 ${overlappingLandings.length}개가 전부 head ${opts.expectedHead!.slice(0, 12)} 의 조상 · review pass|warn must-fix 0 (${verdict.reviewResult})`);
1639
1744
  }
1640
- else out.error(`✗ ancestor-auto: ${verdict.reason}`);
1745
+ else {
1746
+ refuse(`✗ ancestor-auto: ${verdict.reason}`);
1747
+ ancestorResyncNeeded = verdict.reason.startsWith('ancestor-auto-not-ancestor:');
1748
+ }
1641
1749
  }
1642
1750
  if (pinned && overlapDecision && overlapDecision.outcome !== 'user-merge' && !overlapEvidenceAccepted) {
1751
+ // A clean GitHub PR can still be behind a recently landed overlapping head. Preserve the cue
1752
+ // across hosts; the current steward tick acts only if it can also prove local run ownership.
1753
+ if (ancestorResyncNeeded && !opts.dryRun) {
1754
+ const marked = run('gh', ['pr', 'edit', String(opts.pr), '--add-label', PR_LABEL_NAMES.needsRebase], { cwd });
1755
+ record('resync-marker', marked.ok, { pr: Number(opts.pr), reason: 'ancestor-auto-not-ancestor' });
1756
+ if (!marked.ok) out.error('⚠ resync-marker: needs-rebase 라벨을 남기지 못했습니다');
1757
+ }
1643
1758
  out.error('✗ expected-head: overlap merge approval unavailable for the reviewed head');
1644
1759
  return 1;
1645
1760
  }
@@ -1700,7 +1815,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1700
1815
  if (opts.hold) {
1701
1816
  out.log(`[dry-run] ${existing.url ? '기존 PR을 갱신' : 'PR을 생성'}하고 --hold 로 병합하지 않고 멈춥니다.`);
1702
1817
  } else {
1703
- out.log(`[dry-run] ${existing.url ? '기존 PR을 갱신하고 ready 상태를 보장' : 'PR을 생성하고 ready 상태를 보장'}한 뒤 squash merge 합니다.`);
1818
+ out.log(`[dry-run] 머리 고정 없는 일반 착지: ${existing.url ? '기존 PR을 갱신하고 ready 상태를 보장' : 'PR을 생성하고 ready 상태를 보장'}한 뒤 squash merge 합니다.`);
1704
1819
  }
1705
1820
  return 0;
1706
1821
  }
@@ -1874,7 +1989,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1874
1989
  ...(mergeable ? { mergeable } : {}),
1875
1990
  ...(mergeStateStatus ? { mergeStateStatus } : {}),
1876
1991
  });
1877
- out.error(`✗ merge: ${detail}`);
1992
+ refuse(`✗ merge: ${detail}`);
1878
1993
  if (mergeable === 'CONFLICTING') {
1879
1994
  out.error(' ↳ 충돌 — 이 브랜치가 기준 브랜치의 최근 착지와 같은 줄을 고쳤다. `bun bin/elanous.mjs git fetch origin` → `bun bin/elanous.mjs git rebase origin/<기준>` 로 풀고 다시 pr land.');
1880
1995
  }
@@ -11,12 +11,12 @@
11
11
  * ⓐ `gate.baseCommit` 이 고정 머리의 조상 ⊕ 지금 끝의 조상이고 ⓑ 창 `gate.baseCommit..지금 끝`의 커밋이 «하나도»
12
12
  * PR 의 바뀐 파일(머리 vs merge-base)을 안 건드리면 통과(관측 reason `base-advanced-disjoint`).
13
13
  * 겹치면 `base-stale-overlap`(REJUDGE 가 재게이트를 청한다) · 못 재면 `base-window-unmeasured` · ⓐ 가 아니면 종전 사유.
14
- * ③ 리뷰 — 증거가 가리키는 `review-results/<card>-<pr>-<head12>.json`(TA 가 받는 `self review --json` stdout)이
15
- * 같은 PR·같은 머리에 대해 `reviewed:true` · `verdict ∈ {pass, warn}` · must-fix 가 «잰 수» 0 인가
14
+ * ③ 리뷰 — 증거가 가리키는 `review-results/<card>-<pr>-<head접두 10~40>.json`(TA 가 받는 `self review --json` stdout)이
15
+ * 파일명에서 같은 PR·머리 접두이고 내용에서 전체 PR·머리가 정확히 같으며 `reviewed:true` · `verdict ∈ {pass, warn}` · must-fix 가 «잰 수» 0 인가
16
16
  * (자동 병합 canAuto · TA-LAND-WARN-MUSTFIX0 과 같은 규칙 — must-fix 를 못 셌으면 거부).
17
17
  * ⓐ 이미 조상(TC #26019 실측) — 겹친 최근 착지 커밋이 «전부» 고정 머리의 조상이면 그 겹침은 이미 그 머리의 게이트·리뷰에
18
18
  * 반영됐다 ⇒ ①② 없이 ③ 만으로 통과(관측 reason `already-ancestor`). 하나라도 조상이 아니면 ①②③ 전부.
19
- * ⓐ' 증거 파일이 없을 때(`verifyAncestorAutoPass`) — 같은 ⓐ 를 표준 자리 `review-results/*-<pr>-<head12>.json` 만으로 잰다
19
+ * ⓐ' 증거 파일이 없을 때(`verifyAncestorAutoPass`) — 같은 ⓐ 를 표준 자리 `review-results/*-<pr>-<head접두 10~40>.json` 만으로 잰다
20
20
  * (관측 reason `already-ancestor-auto`). 조상이 아닌 착지가 하나라도 있거나 리뷰 결과가 없으면 거부.
21
21
  * 하나라도 못 재거나 어긋나면 거부 — 호출부는 종전 거부를 그대로 낸다(fail-closed).
22
22
  * - 관측: `pr.land` · `overlap-evidence` (decision · reason · 잰 값).
@@ -33,7 +33,7 @@ interface OverlapEvidenceFile {
33
33
  head: string;
34
34
  /** ①② 경로에서만 필수 — 겹친 착지가 전부 조상(already-ancestor)이면 생략해도 된다. */
35
35
  gate?: { head: string; baseCommit: string; passed: boolean };
36
- /** `self review --json` stdout 을 받은 파일(TA: `<state>/review-results/<card>-<pr>-<head12>.json`). */
36
+ /** `self review --json` stdout 을 받은 파일(TA: `<state>/review-results/<card>-<pr>-<head접두 10~40>.json`); 내용의 전체 head도 확인한다. */
37
37
  reviewResult: string;
38
38
  }
39
39
 
@@ -65,9 +65,14 @@ const COMMIT_MARK = '\u0001';
65
65
 
66
66
  function checkReview(input: Pick<VerifyOverlapEvidenceInput, 'pr' | 'readFile'>, e: Partial<OverlapEvidenceFile>, head: string, baseTip?: string): { ok: false; reason: string; baseTip?: string } | { ok: true; reviewResult: string } {
67
67
  if (typeof e.reviewResult !== 'string' || !e.reviewResult) return { ok: false, reason: 'review-missing', baseTip };
68
- // TA 가 받는 자리(`<state>/review-results/<card>-<pr>-<head12>.json` · reviewResultPath)만 — 아무 디렉터리의 같은 이름은 안 받는다.
68
+ // TA 가 받는 자리(`<state>/review-results/<card>-<pr>-<head접두 10~40>.json` · reviewResultPath)만; 내용의 전체 head도 확인한다.
69
+ const resultName = basename(e.reviewResult);
70
+ const resultMatch = /^([A-Za-z0-9][A-Za-z0-9._-]*)-(\d+)-([a-f0-9]{10,40})\.json$/i.exec(resultName);
69
71
  if (basename(dirname(e.reviewResult)) !== 'review-results'
70
- || !new RegExp(`^[A-Za-z0-9][A-Za-z0-9._-]*-${input.pr}-${head.slice(0, 12)}\\.json$`, 'i').test(basename(e.reviewResult))) {
72
+ || !resultMatch
73
+ || Number(resultMatch[2]) !== input.pr
74
+ || resultMatch[3]!.length < 10
75
+ || !head.startsWith(resultMatch[3]!.toLowerCase())) {
71
76
  return { ok: false, reason: 'review-path-not-for-this-pr-head', baseTip };
72
77
  }
73
78
  let text: string;
@@ -206,7 +211,7 @@ export interface VerifyAncestorAutoInput {
206
211
  cwd: string;
207
212
  run: CmdRunner;
208
213
  readFile: (path: string) => string;
209
- /** 표준 자리 `<state>/review-results` — 이 안의 `*-<pr>-<head12>.json` 만 본다. */
214
+ /** 표준 자리 `<state>/review-results` — 이 안의 `*-<pr>-<head접두 10~40>.json`만 후보로 보고 내용의 전체 head를 확인한다. */
210
215
  reviewResultsDir: string;
211
216
  listDir: (dir: string) => readonly string[];
212
217
  /** 겹침 관문이 찾은 «겹친» 최근 착지 커밋들. */
@@ -222,10 +227,13 @@ function checkAncestorAuto(input: VerifyAncestorAutoInput): AncestorAutoVerdict
222
227
  if (notAncestor !== undefined) return { ok: false, reason: `ancestor-auto-not-ancestor: ${notAncestor.slice(0, 12)} is not an ancestor of ${head.slice(0, 12)}` };
223
228
  let names: readonly string[];
224
229
  try { names = input.listDir(input.reviewResultsDir); } catch { return { ok: false, reason: 'review-missing' }; }
225
- const suffix = new RegExp(`-${input.pr}-${head.slice(0, 12)}\\.json$`, 'i');
226
- // 같은 <pr>-<head12> 리뷰가 여럿이면 «전부» 통과해야 한다 — 하나라도 거부면 거부(fail-closed · 처음 통과한 하나로 승인하지 않는다).
230
+ const filename = new RegExp(`^[A-Za-z0-9][A-Za-z0-9._-]*-${input.pr}-([a-f0-9]{10,40})\\.json$`, 'i');
231
+ // 같은 PR·머리 접두의 리뷰가 여럿이면 «전부» 통과해야 한다 — 내용 headCommit 이 정확히 맞아야 승인한다.
227
232
  let accepted: string | undefined;
228
- for (const name of [...names].filter((n) => suffix.test(n)).sort()) {
233
+ for (const name of [...names].filter((n) => {
234
+ const match = filename.exec(n);
235
+ return Boolean(match && match[1]!.length >= 10 && head.startsWith(match[1]!.toLowerCase()));
236
+ }).sort()) {
229
237
  const review = checkReview(input, { reviewResult: join(input.reviewResultsDir, name) }, head);
230
238
  if (!review.ok) return { ok: false, reason: review.reason };
231
239
  accepted = review.reviewResult;
@@ -0,0 +1,20 @@
1
+ import { readJourneyEdge, recordJourneyEdge } from '../self-dev/graph-journey-route.js';
2
+ import { debug } from '../debug/log.js';
3
+
4
+ /** The five outcomes of an observed `pr land` refusal. This function never retries a landing. */
5
+ export type PrLandRefusal = 'draft' | 'head-branch' | 'not-ancestor' | 'review-missing' | 'other';
6
+
7
+ export function classifyPrLandRefusal(message: string, context: { isDraft?: boolean } = {}): PrLandRefusal {
8
+ if (context.isDraft === true && message.startsWith('✗ expected-head: PR OPEN non-draft')) return 'draft';
9
+ if (message.startsWith('✗ branch: PR head branch와 현재 브랜치가 다릅니다.')) return 'head-branch';
10
+ if (message.startsWith('✗ ancestor-auto: ancestor-auto-not-ancestor:')) return 'not-ancestor';
11
+ if (/^✗ (?:ancestor-auto|overlap-evidence): (?:review-missing|review-not-reviewed|review-error|review-no-result|review-unreadable|review-pr-mismatch|review-head-mismatch|review-path-not-for-this-pr-head|review-verdict-null|review-must-fix-unknown)(?:\b|$)/.test(message)) return 'review-missing';
12
+ return 'other';
13
+ }
14
+
15
+ /** Read the declared recovery edge and observe the refusal. No recovery command runs here. */
16
+ export function observePrLandRefusal(message: string, context: { isDraft?: boolean } = {}): void {
17
+ const route = readJourneyEdge('merge', classifyPrLandRefusal(message, context));
18
+ if (route.to !== null) recordJourneyEdge(route, route.to, 'pr-land-refused; recovery-execution=none');
19
+ else debug.log('self-implement', 'graph-edge-unresolved', { from: 'merge', label: route.label, reason: 'pr-land-refused; recovery-execution=none' });
20
+ }