elanous 0.2.31 → 0.2.32

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 (282) 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/{1834-50625904996a8fa8.js → 1834-98b64172bc1efe77.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/69-0a2c8209c6d4a158.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/8097-9530c03bcc550f02.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/approvals/page-504204b58c82c253.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3535a4a69f1fafaa.js → page-a9717a67c2eecbaf.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/board/page-5c9ec30aee62dfbf.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/botlab/page-584191533721fa16.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/bots/page-73c4b4bb3ac7ab5f.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/ceo/{page-5a503541353e60a8.js → page-c0f1ca371f710b06.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/chat/{page-6025101384e361a5.js → page-46430d45a9e04eac.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/consult/page-3b2ebf1dc64ff0be.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/control/{page-a645f00a06483e9a.js → page-a27b1e5a0d1b308f.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/decisions/page-25654b82094f7d22.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-e9bab7413b028b28.js → page-8ea49a4c6dd2c6e7.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/editor/page-ce5113aefea0a8d1.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-e547e6ce55f1fb33.js → page-0e5a41fac1c75396.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-d27e1b9850fd54bf.js → page-31aaf1ed37c6df9a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-3dcc1fa411e1f4e0.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-0fec29c7ba01894b.js → page-c69247e7134d329d.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-339a07410cd6f36b.js → layout-66dc5cd3a0999abe.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-26ff7ff651a7217d.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/live-run/page-617b3976981efa96.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/loops/{page-2fd93ed8c9f16580.js → page-d256f5d5d9552823.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/market/{page-9b564dbff0fefc35.js → page-ce8f1907b8a6101c.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-33e182a316263116.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/morning/{page-814ba51385225776.js → page-32866485913d6a04.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/{not-found-397d479626e5a459.js → not-found-7607cca9b84950c6.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-af1bba4e3251b728.js → page-eeab86176b03dc3b.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-3ad3be5e7d3fdce9.js → page-5b93dbae3432b996.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-77a6cb90a9cf2b10.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-4da93ad35d4a8d28.js → page-f47e768b5b242b5a.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/outputs/page-eb41aa3367fc2505.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/page-4d13ab0b8a267e3f.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/reflection/page-84dad32ff0fc0831.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-3a658d5fa6d16a25.js → page-95306f892aa2d0ee.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-0157de6e85f8bb6b.js → page-940fb6c217f6b470.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-0a07363b130182a1.js → page-8c66a3b24a745630.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/settings/{page-841d73d4ac44ed49.js → page-7b675dfd35c36fc9.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-6f43a3d1146e6377.js → page-8bd0d3cf7665bcfc.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/setup/{page-521ee23ac8c0abfa.js → page-c219f2ed171131be.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/share/{page-a9992ee285efaa83.js → page-40f8fa21bdae955b.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-3d48e85fa89ec04f.js → page-468f8313e4f1fbd9.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/tasks/page-7bbb8fa353a246c4.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/term/page-04d00ae9b0890349.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/today/{page-233c71cd5f68f0d7.js → page-4214b855d9233ea8.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/trace/{page-70151d78de2f375e.js → page-936752c7aa58762e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/vault/{page-dcac33a3e2cb72c0.js → page-ec052ad359df1797.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-8305570551091338.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-0448145ccd5c45c1.js → page-fad650e4793adb65.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/workspace/page-d90e64743826e1d8.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-a16bcfd49af86a32.js +1 -0
  54. package/apps/pwa/out/_next/static/css/b5d88eba78a3d7bf.css +1 -0
  55. package/apps/pwa/out/approvals/index.html +1 -1
  56. package/apps/pwa/out/approvals/index.txt +14 -14
  57. package/apps/pwa/out/autopilot/index.html +1 -1
  58. package/apps/pwa/out/autopilot/index.txt +14 -14
  59. package/apps/pwa/out/board/index.html +1 -1
  60. package/apps/pwa/out/board/index.txt +14 -14
  61. package/apps/pwa/out/botlab/index.html +1 -1
  62. package/apps/pwa/out/botlab/index.txt +14 -14
  63. package/apps/pwa/out/bots/index.html +1 -1
  64. package/apps/pwa/out/bots/index.txt +14 -14
  65. package/apps/pwa/out/ceo/index.html +1 -1
  66. package/apps/pwa/out/ceo/index.txt +14 -14
  67. package/apps/pwa/out/chat/index.html +1 -1
  68. package/apps/pwa/out/chat/index.txt +14 -14
  69. package/apps/pwa/out/consult/index.html +1 -1
  70. package/apps/pwa/out/consult/index.txt +14 -14
  71. package/apps/pwa/out/control/index.html +1 -1
  72. package/apps/pwa/out/control/index.txt +14 -14
  73. package/apps/pwa/out/decisions/index.html +1 -1
  74. package/apps/pwa/out/decisions/index.txt +14 -14
  75. package/apps/pwa/out/design-check/index.html +1 -1
  76. package/apps/pwa/out/design-check/index.txt +14 -14
  77. package/apps/pwa/out/editor/index.html +1 -1
  78. package/apps/pwa/out/editor/index.txt +14 -14
  79. package/apps/pwa/out/exec/index.html +1 -1
  80. package/apps/pwa/out/exec/index.txt +14 -14
  81. package/apps/pwa/out/field/index.html +1 -1
  82. package/apps/pwa/out/field/index.txt +14 -14
  83. package/apps/pwa/out/healthz +1 -1
  84. package/apps/pwa/out/index.html +1 -1
  85. package/apps/pwa/out/index.txt +14 -14
  86. package/apps/pwa/out/inside/index.html +1 -1
  87. package/apps/pwa/out/inside/index.txt +14 -14
  88. package/apps/pwa/out/intake/index.html +1 -1
  89. package/apps/pwa/out/intake/index.txt +14 -14
  90. package/apps/pwa/out/live/index.html +1 -1
  91. package/apps/pwa/out/live/index.txt +17 -14
  92. package/apps/pwa/out/live-run/index.html +1 -1
  93. package/apps/pwa/out/live-run/index.txt +14 -14
  94. package/apps/pwa/out/loops/index.html +1 -1
  95. package/apps/pwa/out/loops/index.txt +14 -14
  96. package/apps/pwa/out/market/index.html +1 -1
  97. package/apps/pwa/out/market/index.txt +14 -14
  98. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  99. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  100. package/apps/pwa/out/morning/index.html +1 -1
  101. package/apps/pwa/out/morning/index.txt +14 -14
  102. package/apps/pwa/out/observatory/index.html +1 -1
  103. package/apps/pwa/out/observatory/index.txt +14 -14
  104. package/apps/pwa/out/ops/checklist/index.html +1 -1
  105. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  106. package/apps/pwa/out/ops/release/index.html +1 -1
  107. package/apps/pwa/out/ops/release/index.txt +14 -14
  108. package/apps/pwa/out/ops/seats/index.html +1 -1
  109. package/apps/pwa/out/ops/seats/index.txt +14 -14
  110. package/apps/pwa/out/outputs/index.html +1 -1
  111. package/apps/pwa/out/outputs/index.txt +14 -14
  112. package/apps/pwa/out/reflection/index.html +1 -1
  113. package/apps/pwa/out/reflection/index.txt +15 -15
  114. package/apps/pwa/out/scheduler/index.html +1 -1
  115. package/apps/pwa/out/scheduler/index.txt +14 -14
  116. package/apps/pwa/out/sessions/index.html +1 -1
  117. package/apps/pwa/out/sessions/index.txt +14 -14
  118. package/apps/pwa/out/settings/devices/index.html +1 -1
  119. package/apps/pwa/out/settings/devices/index.txt +14 -14
  120. package/apps/pwa/out/settings/index.html +1 -1
  121. package/apps/pwa/out/settings/index.txt +14 -14
  122. package/apps/pwa/out/setup/done/index.html +1 -1
  123. package/apps/pwa/out/setup/done/index.txt +14 -14
  124. package/apps/pwa/out/setup/index.html +1 -1
  125. package/apps/pwa/out/setup/index.txt +14 -14
  126. package/apps/pwa/out/share/index.html +1 -1
  127. package/apps/pwa/out/share/index.txt +14 -14
  128. package/apps/pwa/out/showroom/index.html +1 -1
  129. package/apps/pwa/out/showroom/index.txt +14 -14
  130. package/apps/pwa/out/tasks/index.html +1 -1
  131. package/apps/pwa/out/tasks/index.txt +14 -14
  132. package/apps/pwa/out/term/index.html +1 -1
  133. package/apps/pwa/out/term/index.txt +14 -14
  134. package/apps/pwa/out/today/index.html +1 -1
  135. package/apps/pwa/out/today/index.txt +14 -14
  136. package/apps/pwa/out/trace/index.html +1 -1
  137. package/apps/pwa/out/trace/index.txt +14 -14
  138. package/apps/pwa/out/vault/index.html +1 -1
  139. package/apps/pwa/out/vault/index.txt +14 -14
  140. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  141. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  142. package/apps/pwa/out/workflows/index.html +1 -1
  143. package/apps/pwa/out/workflows/index.txt +14 -14
  144. package/apps/pwa/out/workspace/index.html +1 -1
  145. package/apps/pwa/out/workspace/index.txt +14 -14
  146. package/apps/pwa/out/worktrees/index.html +1 -1
  147. package/apps/pwa/out/worktrees/index.txt +14 -14
  148. package/graphs/implement-loop.yaml +5 -0
  149. package/package.json +1 -1
  150. package/scripts/ci-typecheck-changed.ts +26 -1
  151. package/scripts/measure-si-task-cpu.ts +143 -0
  152. package/scripts/ratchets/test-interference-patterns.allowlist.txt +0 -2
  153. package/scripts/release-loop/prepare-node.ts +102 -4
  154. package/src/acp/core-turn-bridge.ts +16 -4
  155. package/src/acp/elanous-extensions.ts +3 -0
  156. package/src/acp/server.ts +3 -0
  157. package/src/acp/tool-trace.ts +186 -11
  158. package/src/agent/self-ambient-cache.ts +156 -0
  159. package/src/agent/self-ambient.ts +8 -1
  160. package/src/agent/task-notification.ts +5 -0
  161. package/src/agent-substrate/pr-reviewer.ts +3 -1
  162. package/src/ask-user-question/tool.ts +22 -0
  163. package/src/autopilot/build/llm-conflict-merge.ts +56 -10
  164. package/src/autopilot/pr-manager.ts +23 -5
  165. package/src/capture/frame-state-detect.ts +39 -0
  166. package/src/capture/tui-self-report.ts +134 -6
  167. package/src/chat/index.ts +61 -15
  168. package/src/chat/pickers/modal-runtime.ts +6 -0
  169. package/src/chat/pickers/modals.ts +8 -2
  170. package/src/cli/ask-bare.ts +1 -0
  171. package/src/cli/import-cli.ts +12 -0
  172. package/src/cli/log-event-names.ts +10 -0
  173. package/src/cli/msg-cli.ts +1 -0
  174. package/src/cli/pod-cli.ts +14 -3
  175. package/src/cli/pr-cli.ts +67 -32
  176. package/src/cli/pr-land-base-read.ts +34 -0
  177. package/src/cli/pr-land-heavy-remote.ts +81 -25
  178. package/src/cli/pr-land-overlap-evidence.ts +46 -2
  179. package/src/cli/pr-land-pod-shadow-job.ts +26 -3
  180. package/src/cli/pr-land-refusal.ts +20 -5
  181. package/src/cli/pty-takeover-cli.ts +88 -14
  182. package/src/cli/sim-cli.ts +18 -0
  183. package/src/context-bus/ops-events.ts +6 -2
  184. package/src/core-turn/run-core-turn.ts +1 -0
  185. package/src/core-turn/run-goal-loop.ts +21 -0
  186. package/src/core-turn/types.ts +3 -0
  187. package/src/dashboard/acp-active-model.ts +42 -1
  188. package/src/dashboard/background-jobs-status.ts +66 -0
  189. package/src/dashboard/index.ts +117 -44
  190. package/src/dashboard/input/agent-wake.ts +242 -0
  191. package/src/dashboard/input/chat-main-entry-runtime.ts +5 -0
  192. package/src/dashboard/input/external-input-host.ts +23 -9
  193. package/src/dashboard/input/mouse-wiring.ts +108 -11
  194. package/src/dashboard/log-click-runtime.ts +118 -2
  195. package/src/dashboard/turn-preamble.ts +116 -40
  196. package/src/dashboard/turn-session-layers.ts +4 -2
  197. package/src/dashboard/turn-start-segments.ts +222 -0
  198. package/src/display/prompt-row-owner.ts +146 -0
  199. package/src/display/resize-listener.ts +33 -2
  200. package/src/fold-stack.ts +4 -2
  201. package/src/git-fs/index.ts +263 -36
  202. package/src/harness/gate-reserve.ts +87 -0
  203. package/src/harness/harness-incidents.ts +13 -0
  204. package/src/harness/harness-queue.ts +15 -0
  205. package/src/index.ts +8 -6
  206. package/src/input/external-input-queue.ts +34 -9
  207. package/src/llm/route-decision.ts +36 -0
  208. package/src/llm/tool-loop-steered-rejection.ts +8 -0
  209. package/src/llm-request-shape.ts +209 -0
  210. package/src/llm.ts +166 -267
  211. package/src/loops/orchestrator/tick.ts +24 -2
  212. package/src/maturity/feature-maturity.ts +1 -1
  213. package/src/mss/logging/render-categories.ts +5 -0
  214. package/src/ops/overview.ts +59 -2
  215. package/src/pr-tool/census.ts +19 -4
  216. package/src/pr-tool/inflight-registry.ts +143 -0
  217. package/src/pty-shell/pty-manifest.ts +12 -3
  218. package/src/pty-shell/tui-run-state.ts +34 -3
  219. package/src/seat-loop/traffic-signal.ts +2 -1
  220. package/src/self-dev/ask-launch-flow.ts +13 -0
  221. package/src/self-dev/draft-residue.ts +243 -24
  222. package/src/self-dev/shard-plan.ts +11 -3
  223. package/src/self-dev/shard-run.ts +103 -10
  224. package/src/self-implement/elanous-tui-spawn.ts +14 -0
  225. package/src/self-implement/follow-up-placement.ts +79 -0
  226. package/src/self-implement/gate-artifacts.ts +15 -4
  227. package/src/self-implement/gate-baseline.ts +96 -5
  228. package/src/self-implement/gate-cli.ts +7 -2
  229. package/src/self-implement/gate-remote.ts +26 -1
  230. package/src/self-implement/host-regate.ts +36 -4
  231. package/src/self-implement/orchestrator.ts +101 -11
  232. package/src/self-implement/run-ledger.ts +16 -14
  233. package/src/session-runtime/index.ts +22 -267
  234. package/src/shell-runner/background-surface.ts +15 -5
  235. package/src/skills/tools/agent.ts +39 -6
  236. package/src/skills/tools/list-dir.ts +11 -0
  237. package/src/skills/tools/schedule-wakeup.ts +408 -0
  238. package/src/skills/tools/shell.ts +245 -3
  239. package/src/status/bar.ts +26 -0
  240. package/src/steward/digest-delivery.ts +118 -0
  241. package/src/steward/hold-hygiene.ts +120 -0
  242. package/src/steward/triage.ts +10 -0
  243. package/src/surface/shell-runner-boot.ts +5 -2
  244. package/src/task-agent/task-hand.ts +24 -0
  245. package/src/task-orchestrator/surfaces/pod-command-job.ts +65 -11
  246. package/src/task-orchestrator/surfaces/pod-lease.ts +98 -0
  247. package/src/task-orchestrator/surfaces/pod-ledger-events.ts +1 -1
  248. package/src/task-orchestrator/surfaces/pod-pool.ts +3 -1
  249. package/src/task-orchestrator/surfaces/pod-reuse-cleanup.ts +10 -6
  250. package/src/task-orchestrator/surfaces/pod-warm-worker-pod.ts +525 -0
  251. package/src/task-orchestrator/surfaces/pod-warm-worker-runner.ts +90 -0
  252. package/src/task-orchestrator/surfaces/pod-warm-worker-runtime.ts +156 -2
  253. package/src/task-orchestrator/surfaces/self-implement-pod.ts +7 -5
  254. package/src/tui/chat/execution-badge.ts +5 -1
  255. package/src/tui/input-lag-probe.ts +230 -0
  256. package/src/tui.ts +4 -0
  257. package/src/ux-sim/scenario.ts +255 -0
  258. package/src/version/packed-revision.json +1 -1
  259. package/apps/pwa/out/_next/static/chunks/8097-92f87bd108322f66.js +0 -1
  260. package/apps/pwa/out/_next/static/chunks/app/approvals/page-bf3634a117322df7.js +0 -1
  261. package/apps/pwa/out/_next/static/chunks/app/board/page-b9f51447f757fbf2.js +0 -1
  262. package/apps/pwa/out/_next/static/chunks/app/botlab/page-2ebb8fd59b9461f9.js +0 -1
  263. package/apps/pwa/out/_next/static/chunks/app/bots/page-a2fc4e49a4fa4ba8.js +0 -1
  264. package/apps/pwa/out/_next/static/chunks/app/consult/page-f4b630cc8b5273cd.js +0 -1
  265. package/apps/pwa/out/_next/static/chunks/app/decisions/page-61ac22cbab1ee4ec.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/app/editor/page-c992eafe028b642e.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/app/inside/page-805ac2e137b7a121.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/app/live/page-349f2752d8cd37ad.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/live-run/page-d5c6dcf31c25cecc.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-62f4475d3c38fb9b.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-f219ccdbc0bf27df.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/outputs/page-456666766e5ce6fc.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/page-7404e377e8adb8c5.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/reflection/page-14456240ed2a36ad.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d2ced71132fac9b2.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/term/page-f08358bdf251adb9.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-149dc1caba411bb9.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9cb0c444903da65d.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-17e504c396e0e389.js +0 -1
  280. package/apps/pwa/out/_next/static/css/3f5fc6f96e2cf7b2.css +0 -1
  281. /package/apps/pwa/out/_next/static/{LSXfY0u9QtfDbejY0Ukf_ → IVKr3AlRUVLKsw2fh7eXF}/_buildManifest.js +0 -0
  282. /package/apps/pwa/out/_next/static/{LSXfY0u9QtfDbejY0Ukf_ → IVKr3AlRUVLKsw2fh7eXF}/_ssgManifest.js +0 -0
package/src/cli/pr-cli.ts CHANGED
@@ -58,13 +58,16 @@ import { branchLineageSlug, findSiblingPrs } from './pr-lineage.js';
58
58
  import { parseReleaseNoteSection } from '../release-loop/release-note.js';
59
59
  import { landingFreezeMessage } from '../release-loop/landing-freeze.js';
60
60
  import { admitLandingMerge } from '../self-implement/frozen-merges.js';
61
- import { verifyAllSameHeadReviews, verifyAncestorAutoPass, verifyOverlapEvidence } from './pr-land-overlap-evidence.js';
61
+ import { verifyAllSameHeadReviews, verifyAncestorAutoPass, verifyOverlapEvidence, verifyOverlapEvidenceVirtualRebase } from './pr-land-overlap-evidence.js';
62
+ import type { VerifyOverlapEvidenceInput } from './pr-land-overlap-evidence.js';
62
63
  import { verifyVirtualRebasePatch } from './pr-land-virtual-rebase.js';
63
- import { observePrLandRefusal } from './pr-land-refusal.js';
64
+ import { observeDirectPrLandRefusal, observePrLandRefusal } from './pr-land-refusal.js';
64
65
  import { ensurePrLandHeavyRemote, type HeavyRemoteInput, type HeavyRemoteResult } from './pr-land-heavy-remote.js';
66
+ import { readPrBaseNameWithRetry } from './pr-land-base-read.js';
65
67
  import { runHeavyOnPodShadow } from './pr-land-pod-shadow.js';
66
68
  import type { LandingPodShadowInput, LandingPodShadowVerdict } from './pr-land-pod-shadow-job.js';
67
- import { taskAgentStatePath } from '../task-agent/task-hand.js';
69
+ import { readTaskAgentState, taskAgentStatePath, type TaskCard } from '../task-agent/task-hand.js';
70
+ import { INFLIGHT_REGISTRY_FILE, removeInflightForPr } from '../pr-tool/inflight-registry.js';
68
71
  import { PR_LABEL_NAMES } from '../github/pr-labels.js';
69
72
  import { enqueueL8ShadowQueue, runL8ShadowQueue, shadowPrNumber, statusL8ShadowQueue, type L8ShadowQueueDeps } from '../self-implement/l8-merge-queue.js';
70
73
  import { censusOpenPrs, guardNewPr, parseDiffHunks, readOpenPrs, type Seat } from '../pr-tool/census.js';
@@ -122,6 +125,8 @@ export interface PrLandDeps {
122
125
  readFile?: (path: string) => string;
123
126
  /** 증거 파일 없는 already-ancestor 자동 판정이 리뷰 결과를 찾는 자리(기본 `<TA 상태 디렉터리>/review-results`). */
124
127
  reviewResultsDir?: string;
128
+ /** INFLIGHT-REGISTRY — 착지 뒤 등록부 줄을 찾을 과제 카드 상태(기본 `taskAgentStatePath()` · 등록부는 그 옆). */
129
+ taskStatePath?: string;
125
130
  listUnfinishedRuns?: () => FederatedUnfinishedRunLedgerQuery | readonly FederatedUnfinishedRunLedgerEntry[];
126
131
  queryRunningRuns?: () => RunningRunsResult;
127
132
  listOpenPrs?: () => readonly { number: number; headRefName: string }[] | null;
@@ -392,6 +397,7 @@ export function exportLeakBlock(
392
397
  allow: boolean,
393
398
  out: { log: (message: string) => void; error: (message: string) => void },
394
399
  cwd: string = process.cwd(),
400
+ onRefusal?: (message: string) => void,
395
401
  ): boolean {
396
402
  if (!changedFiles || changedFiles.length === 0) return true;
397
403
  let result: ExportLeakCheck;
@@ -425,7 +431,9 @@ export function exportLeakBlock(
425
431
  }
426
432
  if (blockedHits.length === 0) { out.log('✓ public-export-leak: 바뀐 파일이 공개본에 사적 흔적을 싣지 않는다(prepare 와 같은 변환 뒤).'); return true; }
427
433
  const write = allow ? out.log : out.error;
428
- write(`${allow ? '⚠' : '✗'} public-export-leak: 바뀐 파일 ${blockedHits.length}곳이 공개본에 사적 흔적을 싣는다 — 다음 release prepare 가 여기서 실패한다:`);
434
+ const refusal = `${allow ? '⚠' : '✗'} public-export-leak: 바뀐 파일 ${blockedHits.length}곳이 공개본에 사적 흔적을 싣는다 — 다음 release prepare 가 여기서 실패한다:`;
435
+ write(refusal);
436
+ if (!allow) onRefusal?.(refusal);
429
437
  for (const hit of blockedHits.slice(0, 20)) write(` ${hit.file}:${hit.line} · ${hit.marker}${missingReason.includes(hit) ? ' · 사유 없음' : ''}`);
430
438
  if (blockedHits.length > 20) write(` … 외 ${blockedHits.length - 20}곳`);
431
439
  write(' 확인: bun scripts/public-export.ts --leak-check --all --files <파일…>');
@@ -1060,7 +1068,7 @@ function emitLandingOverlapAdvisory(
1060
1068
  }
1061
1069
 
1062
1070
  /** The pinned overlap evidence decision is shared by landing and its read-only preview. */
1063
- function verifyPinnedOverlapDecision(input: {
1071
+ export function verifyPinnedOverlapDecision(input: {
1064
1072
  opts: PrLandOpts;
1065
1073
  deps: PrLandDeps;
1066
1074
  run: CmdRunner;
@@ -1078,13 +1086,14 @@ function verifyPinnedOverlapDecision(input: {
1078
1086
  let ancestorResyncNeeded = false;
1079
1087
  if (decision === 'unavailable' && opts.overlapEvidence !== undefined) {
1080
1088
  const ref = resolveRemoteBranchRef(run, cwd, base);
1081
- const verdict = verifyOverlapEvidence({
1089
+ const evidenceInput: VerifyOverlapEvidenceInput = {
1082
1090
  evidencePath: opts.overlapEvidence, pr: Number(opts.pr), expectedHead: opts.expectedHead!,
1083
1091
  remote: ref.remote, branch: ref.branch, cwd, run, readFile,
1084
1092
  reviewResultsDir: deps.reviewResultsDir ?? join(dirname(taskAgentStatePath()), 'review-results'),
1085
1093
  listDir: (dir) => readdirSync(dir), reviewMtime: (path) => statSync(path).mtimeMs,
1086
1094
  reviewRealPath: (path) => realpathSync(path), overlappingLandings,
1087
- });
1095
+ };
1096
+ const verdict = verifyOverlapEvidence(evidenceInput);
1088
1097
  accepted = verdict.ok;
1089
1098
  if (verdict.ok) {
1090
1099
  out.log(verdict.mode === 'already-ancestor'
@@ -1092,7 +1101,24 @@ function verifyPinnedOverlapDecision(input: {
1092
1101
  : verdict.mode === 'base-advanced-disjoint'
1093
1102
  ? `✓ 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})`
1094
1103
  : `✓ 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})`);
1095
- } else refuse(`✗ overlap-evidence: ${verdict.reason}`);
1104
+ } else {
1105
+ const pureNotRebased = verdict.baseTip !== undefined && verdict.reason ===
1106
+ `not-rebased: ${ref.remote}/${ref.branch} ${verdict.baseTip.slice(0, 12)} is not an ancestor of ${opts.expectedHead!.slice(0, 12)}`;
1107
+ const measuredOverlap = verdict.baseTip !== undefined && verdict.reason.startsWith('base-stale-overlap:');
1108
+ if (pureNotRebased || measuredOverlap) {
1109
+ const patch = verifyOverlapEvidenceVirtualRebase(evidenceInput, verdict.baseTip!);
1110
+ accepted = patch.ok;
1111
+ record('virtual-rebase', patch.ok, {
1112
+ pr: Number(opts.pr), head: opts.expectedHead, baseTip: verdict.baseTip,
1113
+ reason: patch.ok ? 'same-patch-id' : patch.reason,
1114
+ reviewResult: patch.ok ? patch.reviewResult : null,
1115
+ evidencePath: opts.overlapEvidence,
1116
+ });
1117
+ if (patch.ok) {
1118
+ out.log(`✓ overlap-evidence: virtual-rebase — ${ref.remote}/${ref.branch} ${verdict.baseTip!.slice(0, 12)} 와 head ${opts.expectedHead!.slice(0, 12)} 충돌 없음 · patch-id ${patch.patchId.slice(0, 12)} 동일 · review pass|warn must-fix 0 (${patch.reviewResult})`);
1119
+ } else refuse(`✗ overlap-evidence: ${verdict.reason} · virtual-rebase: ${patch.reason}`);
1120
+ } else refuse(`✗ overlap-evidence: ${verdict.reason}`);
1121
+ }
1096
1122
  } else if (decision === 'unavailable') {
1097
1123
  const verdict = verifyAncestorAutoPass({
1098
1124
  pr: Number(opts.pr), expectedHead: opts.expectedHead!, cwd, run, readFile, overlappingLandings,
@@ -1121,12 +1147,12 @@ function verifyPinnedOverlapDecision(input: {
1121
1147
  ancestorResyncNeeded = !accepted;
1122
1148
  record('virtual-rebase', patch.ok, {
1123
1149
  pr: Number(opts.pr), head: opts.expectedHead, baseTip: baseTip || null,
1124
- reason: patch.ok ? 'same-patch-id' : patch.reason,
1150
+ reason: patch.ok ? 'same-patch-id' : ('reason' in patch ? patch.reason : 'virtual-rebase-unmeasured'),
1125
1151
  reviewResult: reviews.ok ? reviews.reviewResult : null,
1126
1152
  });
1127
- if (patch.ok) {
1153
+ if (patch.ok && reviews.ok) {
1128
1154
  out.log(`✓ overlap-evidence: virtual-rebase — ${ref.remote}/${ref.branch} ${baseTip.slice(0, 12)} 와 head ${opts.expectedHead!.slice(0, 12)} 충돌 없음 · patch-id ${patch.patchId.slice(0, 12)} 동일 · review pass|warn must-fix 0 (${reviews.reviewResult})`);
1129
- } else refuse(`✗ virtual-rebase: ${patch.reason} · ${verdict.reason}`);
1155
+ } else refuse(`✗ virtual-rebase: ${'reason' in patch ? patch.reason : 'virtual-rebase-unmeasured'} · ${verdict.reason}`);
1130
1156
  } else refuse(`✗ ancestor-auto: ${verdict.reason}`);
1131
1157
  }
1132
1158
  }
@@ -1287,12 +1313,16 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1287
1313
  totalElapsedMs: Math.max(0, Date.now() - landStartedAt), ok, ...extra,
1288
1314
  });
1289
1315
  };
1290
- // First slice: observe pinned identity/branch, overlap decision, and GitHub merge refusals.
1291
- // Option syntax and prerequisite gate errors do not describe a merge recovery edge.
1316
+ // Observe actual merge refusals, including the measured remote gate and public export leak.
1317
+ // Option syntax and unmeasured prerequisite errors do not describe a recovery edge.
1292
1318
  const refuse = (message: string, context: { isDraft?: boolean } = {}): void => {
1293
1319
  out.error(message);
1294
1320
  observePrLandRefusal(message, context);
1295
1321
  };
1322
+ const refuseDirect = (message: string): void => {
1323
+ out.error(message);
1324
+ observeDirectPrLandRefusal(message);
1325
+ };
1296
1326
  if (opts.title !== undefined && opts.titleFile !== undefined) {
1297
1327
  out.error('✗ title: --title과 --title-file은 함께 사용할 수 없습니다.');
1298
1328
  return 1;
@@ -1829,7 +1859,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1829
1859
  if (opts.heavyRemote) {
1830
1860
  const main = overlapCheckedBase!;
1831
1861
  const verdict = heavyGate(main, 'initial');
1832
- if (!verdict?.ok) { out.error(`✗ heavy-remote: 원격 게이트 ${verdict?.reason ?? 'unmeasured'} — 로컬 게이트로 폴백하지 않는다.`); return 1; }
1862
+ if (!verdict?.ok) { refuseDirect(`✗ heavy-remote: 원격 게이트 ${verdict?.reason ?? 'unmeasured'} — 로컬 게이트로 폴백하지 않는다.`); return 1; }
1833
1863
  verifiedHeavyBase = main;
1834
1864
  out.log(`✓ heavy-remote: host=${verdict.host ?? 'unknown'} head=${opts.expectedHead!.slice(0, 12)} main=${main.slice(0, 12)} source=${verdict.source} elapsed=${verdict.elapsedMs}ms`);
1835
1865
  out.log(`✓ typecheck-roots: ${JSON.stringify(verdict.rootPlan ?? { mode: 'not-applicable', roots: null, promoted: false, reason: 'no-root-plan-in-gate-output' })}`);
@@ -1850,22 +1880,19 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1850
1880
  // A move during the wait is the same race as a move during the gate: rerun or fail closed.
1851
1881
  // gh merge pins the head only. A retargeted PR would land on a branch the heavy gate never measured.
1852
1882
  // Unreadable baseRefName is not "main". Normal pr land (no --heavy-remote) does not use this check.
1853
- const confirmHeavyPrBase = (): number | undefined => {
1883
+ const confirmHeavyPrBase = async (): Promise<number | undefined> => {
1854
1884
  if (!opts.heavyRemote) return undefined;
1855
- const viewed = run('gh', ['pr', 'view', String(opts.pr), '--json', 'baseRefName,url'], { cwd });
1856
- // JSON only. A bare "main" from another view (tests, --jq) is not this answer; unreadable is not main.
1857
- let baseName = '';
1858
- try {
1859
- if (viewed.ok) {
1860
- const parsed: unknown = JSON.parse(viewed.out);
1861
- const name = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
1862
- ? (parsed as { baseRefName?: unknown }).baseRefName : undefined;
1863
- if (typeof name === 'string') baseName = name.trim();
1864
- }
1865
- } catch { /* unreadable is not main */ }
1885
+ const baseRead = await readPrBaseNameWithRetry({
1886
+ pr: String(opts.pr), cwd, run,
1887
+ sleep: deps.sleep ?? ((ms) => new Promise<void>((resolve) => setTimeout(resolve, ms))),
1888
+ });
1889
+ const baseName = baseRead.baseRefName;
1890
+ record('heavy-remote-pr-base-read', baseName !== null, {
1891
+ pr: Number(opts.pr), baseRefName: baseName, attempts: baseRead.attempts,
1892
+ });
1866
1893
  if (baseName !== 'main') {
1867
- record('heavy-remote-pr-base', false, { pr: Number(opts.pr), baseRefName: baseName || null });
1868
- out.error(`✗ heavy-remote: PR base=${baseName || 'unreadable'} ≠ main — 재타기팅된 PR은 통과한 head/main 쌍이 아니다. 병합하지 않는다.`);
1894
+ record('heavy-remote-pr-base', false, { pr: Number(opts.pr), baseRefName: baseName, attempts: baseRead.attempts });
1895
+ refuseDirect(`✗ heavy-remote: PR base=${baseName ?? 'unreadable'} ≠ main — 재타기팅된 PR은 통과한 head/main 쌍이 아니다. 병합하지 않는다.`);
1869
1896
  return 1;
1870
1897
  }
1871
1898
  return undefined;
@@ -1885,7 +1912,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1885
1912
  };
1886
1913
  const confirmHeavyBaseStable = async (): Promise<number | undefined> => {
1887
1914
  if (!opts.heavyRemote) return undefined;
1888
- const retargeted = confirmHeavyPrBase();
1915
+ const retargeted = await confirmHeavyPrBase();
1889
1916
  if (retargeted !== undefined) return retargeted;
1890
1917
  let stable = false;
1891
1918
  for (let attempt = 0; attempt <= HEAVY_BASE_STABILITY_RETRIES; attempt += 1) {
@@ -1899,7 +1926,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1899
1926
  if (staleOverlap !== undefined) return staleOverlap;
1900
1927
  const verdict = heavyGate(main, 'late-recheck');
1901
1928
  if (!verdict?.ok) {
1902
- out.error(`✗ heavy-remote: 병합 직전 원격 게이트 ${verdict?.reason ?? 'unmeasured'} — 로컬 게이트로 폴백하지 않는다.`);
1929
+ refuseDirect(`✗ heavy-remote: 병합 직전 원격 게이트 ${verdict?.reason ?? 'unmeasured'} — 로컬 게이트로 폴백하지 않는다.`);
1903
1930
  return 1;
1904
1931
  }
1905
1932
  verifiedHeavyBase = main;
@@ -1911,7 +1938,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1911
1938
  }
1912
1939
  // A late regate can take minutes. Re-read baseRefName after that wait and before success:
1913
1940
  // the pre-loop check is stale if the PR was retargeted while origin/main was being remeasured.
1914
- const retargetedAfter = confirmHeavyPrBase();
1941
+ const retargetedAfter = await confirmHeavyPrBase();
1915
1942
  if (retargetedAfter !== undefined) return retargetedAfter;
1916
1943
  return undefined;
1917
1944
  };
@@ -1990,7 +2017,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1990
2017
  nextMdWarning(body ?? '', changedPaths, out);
1991
2018
  if (!conflictMarkerBlock(changedPaths, cwd, out)) return 1;
1992
2019
  publicLeakWarning(changedPaths, deps.runPublicLeakGate ?? ((files, sink) => runPrLandPublicLeakGate(files, sink, cwd, base)), out);
1993
- if (!exportLeakBlock(changedPaths, () => (deps.runExportLeakCheck ?? runPrLandExportLeakCheck)(changedPaths ?? [], cwd), opts.allowPublicLeak === true, out, cwd)) return 1;
2020
+ if (!exportLeakBlock(changedPaths, () => (deps.runExportLeakCheck ?? runPrLandExportLeakCheck)(changedPaths ?? [], cwd), opts.allowPublicLeak === true, out, cwd, observeDirectPrLandRefusal)) return 1;
1994
2021
  if (!await exportImportBlock(changedPaths, () => (deps.runExportImportCheck ?? runPrLandExportImportCheck)(changedPaths ?? [], cwd), out)) return 1;
1995
2022
  if (publicDocPaths(changedPaths, () => true).length > 0) {
1996
2023
  const top = run('git', ['rev-parse', '--show-toplevel'], { cwd });
@@ -2347,6 +2374,14 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
2347
2374
  }
2348
2375
  if (opts.forceFreeze) debug.log('release-loop.freeze', 'force-merge', { pr: prNumber, reason: opts.forceFreeze.trim() });
2349
2376
  record('merge', true, { result: merged.kind, ...(merged.remoteBranchDeletion ? { remoteBranchDeletion: merged.remoteBranchDeletion.detail } : {}) });
2377
+ // INFLIGHT-REGISTRY — 착지하면 비행 중 등록부의 그 런 줄을 지운다(색인 실패는 착지를 막지 않는다).
2378
+ try {
2379
+ const statePath = deps.taskStatePath ?? taskAgentStatePath();
2380
+ const cards = Object.values(readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(statePath).tasks ?? {});
2381
+ removeInflightForPr(prNumber, cards, { path: join(dirname(statePath), INFLIGHT_REGISTRY_FILE) });
2382
+ } catch (error) {
2383
+ try { debug.log('pr-tool.inflight', 'remove-failed', { pr: prNumber, error: String((error as Error)?.message ?? error).slice(0, 200) }, { level: 'warn' }); } catch { /* fail-soft */ }
2384
+ }
2350
2385
  if (merged.remoteBranchDeletion) {
2351
2386
  out.log(`⚠ remote branch deletion: ${merged.remoteBranchDeletion.detail}`);
2352
2387
  }
@@ -0,0 +1,34 @@
1
+ import type { CmdRunner } from '../autopilot/pr-manager.js';
2
+
3
+ export const PR_LAND_BASE_READ_RETRIES = 2;
4
+ export const PR_LAND_BASE_READ_RETRY_DELAY_MS = 100;
5
+
6
+ export interface PrBaseReadResult {
7
+ baseRefName: string | null;
8
+ attempts: number;
9
+ }
10
+
11
+ /** Retry only an unreadable GitHub base response; a readable non-main base is a real retarget. */
12
+ export async function readPrBaseNameWithRetry(input: {
13
+ pr: string;
14
+ cwd: string;
15
+ run: CmdRunner;
16
+ sleep: (ms: number) => Promise<void>;
17
+ }): Promise<PrBaseReadResult> {
18
+ for (let attempt = 1; attempt <= PR_LAND_BASE_READ_RETRIES + 1; attempt += 1) {
19
+ const viewed = input.run('gh', ['pr', 'view', input.pr, '--json', 'baseRefName,url'], { cwd: input.cwd });
20
+ let baseRefName: string | null = null;
21
+ try {
22
+ if (viewed.ok) {
23
+ const parsed: unknown = JSON.parse(viewed.out);
24
+ const name = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
25
+ ? (parsed as { baseRefName?: unknown }).baseRefName : undefined;
26
+ if (typeof name === 'string' && name.trim()) baseRefName = name.trim();
27
+ }
28
+ } catch { /* Retry unreadable responses; do not treat them as main. */ }
29
+
30
+ if (baseRefName !== null) return { baseRefName, attempts: attempt };
31
+ if (attempt <= PR_LAND_BASE_READ_RETRIES) await input.sleep(PR_LAND_BASE_READ_RETRY_DELAY_MS);
32
+ }
33
+ return { baseRefName: null, attempts: PR_LAND_BASE_READ_RETRIES + 1 };
34
+ }
@@ -51,6 +51,8 @@ export interface HeavyRemoteResult {
51
51
  reason?: string;
52
52
  elapsedMs: number;
53
53
  evidencePath: string;
54
+ /** Redacted stdout/stderr from a failed remote gate; never a reusable pass. */
55
+ failureLogPath?: string;
54
56
  rootPlan?: RootPlanSummary | null;
55
57
  interferenceMeasured?: boolean;
56
58
  }
@@ -60,6 +62,8 @@ export interface HeavyRemoteDeps {
60
62
  runRemote?: (input: Parameters<typeof runOnRemote>[0], runner: GateRemoteRunner) => RemoteRunOutcome;
61
63
  settings?: ReturnType<typeof resolveGateRemoteSettings>;
62
64
  now?: () => number;
65
+ /** Test seam — same cadence as self gate (`dispatchHeavyCheck`): sleep at most 15s while a host stays busy. */
66
+ sleep?: (ms: number) => void;
63
67
  observe?: (event: string, data: Record<string, unknown>) => void;
64
68
  }
65
69
 
@@ -157,6 +161,38 @@ function failedGateSummary(stdout: Buffer, stderr: Buffer): string | undefined {
157
161
  return bounded.length ? bounded.join(' | ') : undefined;
158
162
  }
159
163
 
164
+ /** The self-gate baseline prints one failure per line after its count header. Keep only distinct test paths here. */
165
+ function failedGateFiles(stdout: Buffer, stderr: Buffer): string | undefined {
166
+ const files = { introduced: new Set<string>(), preexisting: new Set<string>() };
167
+ for (const line of `${stdout.toString('utf8')}\n${stderr.toString('utf8')}`.split(/\r?\n/)) {
168
+ const match = /^- (introduced|preexisting):\s+(\S+\.(?:test|spec)\.[cm]?[jt]sx?)(?:\s+>|\s|$)/.exec(line.trim());
169
+ if (match) files[match[1] as keyof typeof files].add(match[2]!);
170
+ }
171
+ const parts = (['introduced', 'preexisting'] as const).filter((kind) => files[kind].size > 0)
172
+ .map((kind) => { const names = [...files[kind]].sort(); return `${kind}-files=${names.slice(0, 5).map((name) => redactSecretText(name).slice(0, 160)).join(',')}${names.length > 5 ? `,+${names.length - 5}` : ''}`; });
173
+ return parts.length ? parts.join(' · ') : undefined;
174
+ }
175
+
176
+ /** Remote scratch is deleted on exit. Preserve a bounded, redacted copy before returning a refusal. */
177
+ function writeFailedGateLog(path: string, stdout: Buffer, stderr: Buffer): string | undefined {
178
+ // One latest refusal log per PR; repeated attempts replace it atomically instead of accumulating files.
179
+ const logPath = `${path}.failure.log`;
180
+ const tempPath = `${logPath}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
181
+ const maxBytes = 2 * 1024 * 1024;
182
+ const render = (label: string, bytes: Buffer): string => {
183
+ const safe = Buffer.from(redactSecretText(bytes.toString('utf8')), 'utf8');
184
+ const clipped = safe.length > maxBytes
185
+ ? `${safe.subarray(0, maxBytes / 2).toString('utf8')}\n[remote log middle truncated]\n${safe.subarray(-maxBytes / 2).toString('utf8')}` : safe.toString('utf8');
186
+ return `=== ${label} (${bytes.length} bytes${safe.length > maxBytes ? ', middle truncated' : ''}) ===\n${clipped}\n`;
187
+ };
188
+ try {
189
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
190
+ writeFileSync(tempPath, render('stdout', stdout) + render('stderr', stderr), { mode: 0o600, flag: 'wx' });
191
+ renameSync(tempPath, logPath);
192
+ return logPath;
193
+ } catch { rmSync(tempPath, { force: true }); return undefined; }
194
+ }
195
+
160
196
  /** No local gate fallback: host refusal, an unmeasured run, and a failed gate all block landing. */
161
197
  export function ensurePrLandHeavyRemote(input: HeavyRemoteInput, deps: HeavyRemoteDeps = {}): HeavyRemoteResult {
162
198
  const path = evidencePath(input);
@@ -170,48 +206,66 @@ export function ensurePrLandHeavyRemote(input: HeavyRemoteInput, deps: HeavyRemo
170
206
  }
171
207
  const cached = matchingEvidence(path, input);
172
208
  if (cached) return { ok: true, host: cached.host, source: 'cached', elapsedMs: 0, evidencePath: path, rootPlan: cached.rootPlan, interferenceMeasured: cached.interferenceMeasured };
173
- const started = (deps.now ?? Date.now)();
209
+ const now = deps.now ?? Date.now;
210
+ const sleep = deps.sleep ?? ((ms: number) => { if (ms > 0) Bun.sleepSync(ms); });
211
+ const started = now();
212
+ const deadline = started + settings.slotWaitSeconds * 1000;
174
213
  let outcome: RemoteRunOutcome = { kind: 'busy' };
175
214
  const primaryHost: PrLandHeavyHost = settings.host;
176
215
  let selectedHost: PrLandHeavyHost = primaryHost;
177
- for (const host of [primaryHost, ...PR_LAND_HEAVY_HOSTS.filter((candidate) => candidate !== primaryHost)]) {
178
- selectedHost = host;
179
- try {
180
- outcome = (deps.runRemote ?? runOnRemote)({
181
- repo: input.repo, host, mirror: settings.mirror, commit: input.head,
182
- refs: [input.main], hostCap: gateRemoteHostCap(settings, host), installPwa: true, stableCheckout: true,
183
- // Test the tree GitHub will merge: scratch-checkout the reviewed head, merge the pinned main tip there,
184
- // then run typecheck and self gate (changed tests and PWA reach/build) at that exact pair.
185
- // The detached integration commit stays on the selected host and never changes the PR branch.
186
- argv: (sha) => ['sh', '-c',
216
+ const hosts = [primaryHost, ...PR_LAND_HEAVY_HOSTS.filter((candidate) => candidate !== primaryHost)];
217
+ wait: for (;;) {
218
+ for (const host of hosts) {
219
+ selectedHost = host;
220
+ try {
221
+ outcome = (deps.runRemote ?? runOnRemote)({
222
+ repo: input.repo, host, mirror: settings.mirror, commit: input.head,
223
+ refs: [input.main], hostCap: gateRemoteHostCap(settings, host), installPwa: true, stableCheckout: true,
224
+ // Test the tree GitHub will merge: scratch-checkout the reviewed head, merge the pinned main tip there,
225
+ // then run typecheck and self gate (changed tests and PWA reach/build) at that exact pair.
226
+ // The detached integration commit stays on the selected host and never changes the PR branch.
227
+ argv: (sha) => ['sh', '-c',
187
228
  'git -c user.name=elanous-gate -c user.email=gate@localhost -c commit.gpgsign=false merge --no-ff --no-edit "$1" && TSC_BASE_REF="$1" bun run scripts/ci-typecheck-changed.ts --local && bun bin/elanous.mjs self gate --changed --base "$1" --local --shards 4 && bun scripts/pr-land-extra-remote-gates.ts "$1"',
188
229
  'sh', sha(input.main)],
189
- }, deps.runner ?? realGateRemoteRunner);
190
- } catch (error) {
191
- outcome = { kind: 'infra', reason: `exception: ${error instanceof Error ? error.message : String(error)}` };
230
+ }, deps.runner ?? realGateRemoteRunner);
231
+ } catch (error) {
232
+ outcome = { kind: 'infra', reason: `exception: ${error instanceof Error ? error.message : String(error)}` };
233
+ }
234
+ // A completed run or infrastructure failure is a verdict. Only a full host slot asks for another host.
235
+ if (outcome.kind !== 'busy') break wait;
192
236
  }
193
- // A completed run or infrastructure failure is a verdict. Only a full host slot asks for another host.
194
- if (outcome.kind !== 'busy') break;
237
+ // Both allowlisted hosts were busy. Wait the same slotWaitSeconds window as self gate, then retry.
238
+ debug.log('pr.land', 'host-busy', { host: selectedHost, occupied: outcome.occupied ?? null, slotWaitSeconds: settings.slotWaitSeconds });
239
+ const left = deadline - now();
240
+ if (left <= 0) break;
241
+ sleep(Math.min(15_000, left));
242
+ if (now() >= deadline) break;
195
243
  }
196
- const elapsedMs = Math.max(0, (deps.now ?? Date.now)() - started);
244
+ const elapsedMs = Math.max(0, now() - started);
197
245
  const observe = deps.observe ?? ((event: string, data: Record<string, unknown>) => debug.log('pr.land', event, data));
198
246
  const actualHost = outcome.kind === 'ran' ? outcome.host : undefined;
199
247
  const base = { pr: input.pr, head: input.head, main: input.main, host: actualHost ?? null, attemptedHost: selectedHost, elapsedMs };
200
248
  // The outcome must identify the selected allowlisted host; the configured host alone is not evidence.
201
249
  if (outcome.kind !== 'ran' || outcome.commit !== input.head || outcome.rc !== 0 || outcome.host !== selectedHost) {
202
250
  const summary = outcome.kind === 'ran' && outcome.rc !== 0 ? failedGateSummary(outcome.stdout, outcome.stderr) : undefined;
251
+ // A different host cannot attest to this gate, so do not promote its file list into the refusal.
252
+ const files = outcome.kind === 'ran' && outcome.rc !== 0 && outcome.host === selectedHost && outcome.commit === input.head
253
+ ? failedGateFiles(outcome.stdout, outcome.stderr) : undefined;
254
+ const failureLogPath = outcome.kind === 'ran' ? writeFailedGateLog(path, outcome.stdout, outcome.stderr) : undefined;
203
255
  const reason = outcome.kind === 'ran'
204
256
  ? outcome.host !== selectedHost
205
- ? `gate-host-mismatch (${outcome.host})`
206
- : `gate-rc-${outcome.rc}${outcome.commit !== input.head ? '-head-mismatch' : ''}${summary ? ` · remote: ${summary}` : ''}`
257
+ ? `gate-host-mismatch (${outcome.host})${failureLogPath ? ` · remote-log: ${failureLogPath}` : ''}`
258
+ : `gate-rc-${outcome.rc}${outcome.commit !== input.head ? '-head-mismatch' : ''}${files ? ` · ${files}` : ''}${summary ? ` · remote: ${summary}` : ''}${failureLogPath ? ` · remote-log: ${failureLogPath}` : ' · remote-log: unavailable'}`
207
259
  : outcome.kind === 'busy' ? 'gate-host-busy' : outcome.reason;
208
- observe('heavy-remote', { ...base, outcome: outcome.kind === 'ran' ? 'fail' : 'unmeasured', reason });
209
- return { ok: false, ...(actualHost ? { host: actualHost } : {}), attemptedHost: selectedHost, source: 'unmeasured', reason, elapsedMs, evidencePath: path };
260
+ observe('heavy-remote', { ...base, outcome: outcome.kind === 'ran' ? 'fail' : 'unmeasured', reason, failureLogPath: failureLogPath ?? null });
261
+ return { ok: false, ...(actualHost ? { host: actualHost } : {}), attemptedHost: selectedHost, source: 'unmeasured', reason, elapsedMs, evidencePath: path, ...(failureLogPath ? { failureLogPath } : {}) };
210
262
  }
211
263
  const extraGates = extraGatesFromOutput(outcome.stdout);
212
264
  if (!extraGates) {
213
- observe('heavy-remote', { ...base, outcome: 'unmeasured', reason: 'extra-global-unmeasured' });
214
- return { ok: false, host: selectedHost, attemptedHost: selectedHost, source: 'unmeasured', reason: 'extra-global-unmeasured', elapsedMs, evidencePath: path };
265
+ const failureLogPath = writeFailedGateLog(path, outcome.stdout, outcome.stderr);
266
+ const reason = `extra-global-unmeasured${failureLogPath ? ` · remote-log: ${failureLogPath}` : ' · remote-log: unavailable'}`;
267
+ observe('heavy-remote', { ...base, outcome: 'unmeasured', reason, failureLogPath: failureLogPath ?? null });
268
+ return { ok: false, host: selectedHost, attemptedHost: selectedHost, source: 'unmeasured', reason, elapsedMs, evidencePath: path, ...(failureLogPath ? { failureLogPath } : {}) };
215
269
  }
216
270
  const evidence: HeavyRemoteEvidence = {
217
271
  schema: 2, pr: input.pr, head: input.head, main: input.main, host: selectedHost,
@@ -225,8 +279,10 @@ export function ensurePrLandHeavyRemote(input: HeavyRemoteInput, deps: HeavyRemo
225
279
  renameSync(temp, path);
226
280
  } catch (error) {
227
281
  rmSync(temp, { force: true });
228
- observe('heavy-remote', { ...base, outcome: 'unmeasured', reason: 'evidence-write-failed' });
229
- return { ok: false, host: selectedHost, attemptedHost: selectedHost, source: 'unmeasured', reason: `evidence-write-failed: ${error instanceof Error ? error.message : String(error)}`, elapsedMs, evidencePath: path };
282
+ const failureLogPath = writeFailedGateLog(path, outcome.stdout, outcome.stderr);
283
+ const reason = `evidence-write-failed: ${error instanceof Error ? error.message : String(error)}${failureLogPath ? ` · remote-log: ${failureLogPath}` : ' · remote-log: unavailable'}`;
284
+ observe('heavy-remote', { ...base, outcome: 'unmeasured', reason, failureLogPath: failureLogPath ?? null });
285
+ return { ok: false, host: selectedHost, attemptedHost: selectedHost, source: 'unmeasured', reason, elapsedMs, evidencePath: path, ...(failureLogPath ? { failureLogPath } : {}) };
230
286
  }
231
287
  observe('heavy-remote', { ...base, outcome: 'pass', evidencePath: path, rootPlan: evidence.rootPlan, interferenceMeasured: evidence.interferenceMeasured });
232
288
  return { ok: true, host: selectedHost, source: 'remote', elapsedMs, evidencePath: path, rootPlan: evidence.rootPlan, interferenceMeasured: evidence.interferenceMeasured };
@@ -25,6 +25,7 @@ import { basename, dirname, join, resolve } from 'node:path';
25
25
  import { debug } from '../debug/log.js';
26
26
  import type { CmdRunner } from '../autopilot/pr-manager.js';
27
27
  import { parseReviewResultOutput } from '../task-agent/review-result-capture.js';
28
+ import { verifyVirtualRebasePatch } from './pr-land-virtual-rebase.js';
28
29
 
29
30
  const SHA40 = /^[0-9a-f]{40}$/i;
30
31
 
@@ -45,7 +46,7 @@ export type OverlapEvidenceVerdict =
45
46
  | { ok: true; mode: 'already-ancestor'; reviewResult: string }
46
47
  | { ok: true; mode: 'rebased'; baseTip: string; reviewResult: string }
47
48
  | { ok: true; mode: 'base-advanced-disjoint'; baseTip: string; gateBase: string; windowCommits: number; reviewResult: string }
48
- | { ok: false; reason: string };
49
+ | { ok: false; reason: string; baseTip?: string };
49
50
 
50
51
  export interface VerifyOverlapEvidenceInput {
51
52
  evidencePath: string;
@@ -222,6 +223,47 @@ function checkReview(input: Pick<VerifyOverlapEvidenceInput, 'pr' | 'readFile'>,
222
223
  return { ok: true, reviewResult: e.reviewResult };
223
224
  }
224
225
 
226
+ /** Reuse the evidence-selected review when a pinned head needs a read-only virtual rebase. */
227
+ export function verifyOverlapEvidenceVirtualRebase(input: VerifyOverlapEvidenceInput, baseTip: string):
228
+ { ok: true; reviewResult: string; patchId: string } | { ok: false; reason: string; baseTip?: string } {
229
+ const head = input.expectedHead.toLowerCase();
230
+ if (!SHA40.test(head) || !SHA40.test(baseTip)) return { ok: false, reason: 'virtual-rebase-invalid-sha' };
231
+ let raw: unknown;
232
+ try { raw = JSON.parse(input.readFile(input.evidencePath)); }
233
+ catch { return { ok: false, reason: 'evidence-unreadable' }; }
234
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, reason: 'evidence-not-object' };
235
+ const evidence = raw as Partial<OverlapEvidenceFile>;
236
+ const evidencePr = typeof evidence.pr === 'number' ? evidence.pr
237
+ : typeof evidence.pr === 'string' && /^[0-9]+$/.test(evidence.pr) ? Number(evidence.pr) : NaN;
238
+ if (!Number.isSafeInteger(evidencePr) || evidencePr !== input.pr) return { ok: false, reason: 'evidence-pr-mismatch' };
239
+ if (typeof evidence.head !== 'string' || evidence.head.toLowerCase() !== head) return { ok: false, reason: 'evidence-head-mismatch' };
240
+ const gate = evidence.gate;
241
+ if (!gate || typeof gate !== 'object') return { ok: false, reason: 'gate-missing' };
242
+ if (gate.passed !== true) return { ok: false, reason: 'gate-not-passed' };
243
+ if (typeof gate.head !== 'string' || gate.head.toLowerCase() !== head) return { ok: false, reason: 'gate-head-mismatch' };
244
+ if (typeof gate.baseCommit !== 'string' || !SHA40.test(gate.baseCommit)) return { ok: false, reason: 'gate-base-unmeasured' };
245
+ const review = checkReview(input, evidence, head, baseTip);
246
+ if (!review.ok) return review;
247
+ const attempts = checkSelectedReviewAttempts(input, evidence, head);
248
+ if (!attempts.ok) return attempts;
249
+ const remote = input.run('git', ['ls-remote', input.remote, `refs/heads/${input.branch}`], { cwd: input.cwd });
250
+ if (!remote.ok || remote.out.trim().split(/\s+/)[0]?.toLowerCase() !== baseTip.toLowerCase()) {
251
+ return { ok: false, reason: 'virtual-rebase-base-advanced' };
252
+ }
253
+ const local = input.run('git', ['rev-parse', '--verify', `refs/remotes/${input.remote}/${input.branch}`], { cwd: input.cwd });
254
+ if (!local.ok || local.out.trim().toLowerCase() !== baseTip.toLowerCase()) {
255
+ return { ok: false, reason: 'virtual-rebase-base-stale-or-unmeasured' };
256
+ }
257
+ const patch = verifyVirtualRebasePatch({ cwd: input.cwd, baseTip, head });
258
+ if (!patch.ok) return patch;
259
+ const oldBase = patch.oldBase.toLowerCase();
260
+ const gateBase = gate.baseCommit.toLowerCase();
261
+ if (!input.run('git', ['merge-base', '--is-ancestor', gateBase, oldBase], { cwd: input.cwd }).ok) {
262
+ return { ok: false, reason: 'gate-base-not-ancestor-of-old-base' };
263
+ }
264
+ return { ok: true, reviewResult: review.reviewResult, patchId: patch.patchId };
265
+ }
266
+
225
267
  type WindowResult = { ok: true; windowCommits: number } | { ok: false; reason: string; windowCommits?: number };
226
268
 
227
269
  /**
@@ -431,7 +473,9 @@ export function verifyOverlapEvidence(input: VerifyOverlapEvidenceInput): Overla
431
473
  gateBase: (verdict.ok ? (verdict.mode === 'base-advanced-disjoint' ? verdict.gateBase : undefined) : verdict.gateBase) ?? null,
432
474
  windowCommits: (verdict.ok ? (verdict.mode === 'base-advanced-disjoint' ? verdict.windowCommits : undefined) : verdict.windowCommits) ?? null,
433
475
  });
434
- if (!verdict.ok) return { ok: false, reason: verdict.reason };
476
+ if (!verdict.ok) return (verdict.reason.startsWith('not-rebased:') || verdict.reason.startsWith('base-stale-overlap:')) && verdict.baseTip
477
+ ? { ok: false, reason: verdict.reason, baseTip: verdict.baseTip }
478
+ : { ok: false, reason: verdict.reason };
435
479
  if (verdict.mode === 'already-ancestor') return { ok: true, mode: 'already-ancestor', reviewResult: verdict.reviewResult };
436
480
  if (verdict.mode === 'base-advanced-disjoint') {
437
481
  return { ok: true, mode: 'base-advanced-disjoint', baseTip: verdict.baseTip!, gateBase: verdict.gateBase, windowCommits: verdict.windowCommits, reviewResult: verdict.reviewResult };
@@ -7,6 +7,9 @@ import type { RootPlanSummary } from './pr-land-heavy-remote.js';
7
7
 
8
8
  const SHA = /^[0-9a-f]{40}$/i;
9
9
  const MARKER = 'ELANOUS_PRLAND_SHADOW';
10
+ /** Shadow evidence is optional; it must not hold a ready landing for the Pod's 45-minute deadline. */
11
+ export const LANDING_POD_SHADOW_HOST_TIMEOUT_MS = 10 * 60_000;
12
+ export const LANDING_POD_SHADOW_STATUS_UNREADABLE_MS = 60_000;
10
13
 
11
14
  export interface LandingPodShadowInput { head: string; main: string }
12
15
  export interface LandingPodShadowVerdict {
@@ -42,12 +45,32 @@ export function landingPodShadowScript(head: string, main: string): string {
42
45
  export async function runLandingPodShadowJob(input: LandingPodShadowInput, deps: {
43
46
  runPod?: (options: RunPodCommandOptions) => Promise<PodCommandResult>;
44
47
  readLog?: (path: string) => string;
48
+ /** Deterministic timer seam for the host deadline. */
49
+ armTimeout?: (onTimeout: () => void, ms: number) => () => void;
45
50
  } = {}): Promise<LandingPodShadowVerdict> {
46
51
  const script = landingPodShadowScript(input.head, input.main);
47
- const job = await (deps.runPod ?? runPodCommand)({
48
- command: ['bash', '-lc', script], clone: true, source: { kind: 'commit', sha: input.head },
49
- returnLogs: true, deadlineSeconds: 45 * 60,
52
+ const controller = new AbortController();
53
+ const armTimeout = deps.armTimeout ?? ((onTimeout: () => void, ms: number) => {
54
+ const timer = setTimeout(onTimeout, ms);
55
+ return () => clearTimeout(timer);
50
56
  });
57
+ const disarm = armTimeout(() => {
58
+ const reason = new Error(`pod shadow: host timeout after ${LANDING_POD_SHADOW_HOST_TIMEOUT_MS}ms`);
59
+ controller.abort(reason);
60
+ }, LANDING_POD_SHADOW_HOST_TIMEOUT_MS);
61
+ let job: PodCommandResult;
62
+ try {
63
+ // Wait for the runner to observe abort and finish Job/Secret/lease cleanup before landing continues.
64
+ job = await (deps.runPod ?? runPodCommand)({
65
+ command: ['bash', '-lc', script], clone: true, source: { kind: 'commit', sha: input.head },
66
+ returnLogs: true, deadlineSeconds: 45 * 60,
67
+ statusUnreadableMs: LANDING_POD_SHADOW_STATUS_UNREADABLE_MS,
68
+ hostDeadlineAtMs: Date.now() + LANDING_POD_SHADOW_HOST_TIMEOUT_MS,
69
+ signal: controller.signal,
70
+ });
71
+ } finally {
72
+ disarm();
73
+ }
51
74
  let log: string;
52
75
  try { log = (deps.readLog ?? ((path: string) => readFileSync(path, 'utf8')))(join(job.artifactsDir, 'child.log')); }
53
76
  catch (error) {
@@ -1,20 +1,35 @@
1
1
  import { readJourneyEdge, recordJourneyEdge } from '../self-dev/graph-journey-route.js';
2
2
  import { debug } from '../debug/log.js';
3
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';
4
+ /** Observed `pr land` refusal reasons. Classification never retries a landing. */
5
+ export type PrLandRefusal = 'draft' | 'head-branch' | 'not-ancestor' | 'review-missing'
6
+ | 'gate-host-busy' | 'gate-merge-conflict' | 'gate-touch-clean'
7
+ | 'pr-base-unreadable' | 'public-export-leak' | 'other';
6
8
 
7
9
  export function classifyPrLandRefusal(message: string, context: { isDraft?: boolean } = {}): PrLandRefusal {
8
10
  if (context.isDraft === true && message.startsWith('✗ expected-head: PR OPEN non-draft')) return 'draft';
9
11
  if (message.startsWith('✗ branch: PR head branch와 현재 브랜치가 다릅니다.')) return 'head-branch';
10
12
  if (message.startsWith('✗ ancestor-auto: ancestor-auto-not-ancestor:')) return 'not-ancestor';
11
13
  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';
14
+ if (message.startsWith('✗ heavy-remote: PR base=unreadable ≠ main')) return 'pr-base-unreadable';
15
+ if (/^✗ heavy-remote: (?:원격|병합 직전 원격|node-b) 게이트 gate-host-busy(?:\b|\s|$)/.test(message)) return 'gate-host-busy';
16
+ if (message.startsWith('✗ heavy-remote: 원격 게이트 gate-rc-1 · remote: stdout: CONFLICT (content): Merge conflict')) return 'gate-merge-conflict';
17
+ if (message.startsWith('✗ heavy-remote: 원격 게이트 gate-rc-1 · remote: stderr: 건드린 src/scripts 파일과 baseline 밖 test 파일은 tsc 클린이어야 합니다(touch-clean).')) return 'gate-touch-clean';
18
+ if (message.startsWith('✗ public-export-leak: 바뀐 파일')) return 'public-export-leak';
12
19
  return 'other';
13
20
  }
14
21
 
22
+ /** Direct landing failures are observed only when today's refusal shape is recognized. */
23
+ export function observeDirectPrLandRefusal(message: string): void {
24
+ if (classifyPrLandRefusal(message) === 'other') return;
25
+ observePrLandRefusal(message);
26
+ }
27
+
15
28
  /** Read the declared recovery edge and observe the refusal. No recovery command runs here. */
16
29
  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' });
30
+ try {
31
+ const route = readJourneyEdge('merge', classifyPrLandRefusal(message, context));
32
+ if (route.to !== null) recordJourneyEdge(route, route.to, 'pr-land-refused; recovery-execution=none');
33
+ else debug.log('self-implement', 'graph-edge-unresolved', { from: 'merge', label: route.label, reason: 'pr-land-refused; recovery-execution=none' });
34
+ } catch { /* An observation failure must never change the landing refusal. */ }
20
35
  }