elanous 0.2.25 → 0.2.27

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 (244) 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-a9c338f5bb556f42.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-9360b1285b2a346e.js → page-a1ce306364f08da9.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-07110da72c45e68f.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/board/page-d2d76d4a7eae91bd.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/botlab/page-30007380f2e2a3c1.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/bots/page-e5a9302c105725a3.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/ceo/page-b93c04b5311c97ec.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/chat/page-718a49e86bf25f9c.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/consult/{page-44eaad486bc98a45.js → page-20662900d6b4b455.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/control/{page-97fa6cf3d34c7eb9.js → page-f707a509515ac50c.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/decisions/{page-305cd294e754906e.js → page-6a16a48d1aa6e91f.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/design-check/page-fcb06e305f515527.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/editor/page-6acf5ec4e6c6d1af.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/exec/{page-45889e7070d11126.js → page-39f82acf619cca23.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/field/{page-16a2e893f83ae4d0.js → page-7c2c8f81b185f9c0.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/inside/page-ae7aff07e32676bb.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/intake/{page-56888bc744e08dac.js → page-482c60c1bc540d0a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/layout-7cb996c1aa08b6f5.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/live/{page-952c6ceec6d9de38.js → page-623e08fe4477d606.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/live-run/{page-6d0202af568f4c63.js → page-db460cd8aecb8490.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/loops/page-ff27902d0b11ef4f.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/market/{page-74366ae2693a7d95.js → page-8397a5c6ca569bf4.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-4fb6fc34e20c07b7.js → page-1917ea87ea71ab6b.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/morning/{page-e9502921f4f01b1f.js → page-25a302273b054fd7.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/{not-found-b6169ea2f09cf287.js → not-found-670510eda8b9443f.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/observatory/page-d73c3f1e00d93986.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-65caacf54389081e.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4bb7556e2e97a750.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-77e04b4f8762c666.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-b9cc8a10bc6078b4.js → page-15c0a4095baddfba.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/{page-3d3fc73dd187ed52.js → page-4eb96fc7241c2456.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/reflection/page-63e272a4cd3b6634.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-9c04dac0831c2bbb.js → page-0d64ff212dc55d03.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/sessions/page-7f096248993fc2d4.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-04de9ca6d52a3893.js → page-e47fc72049149a38.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/{page-62527677bbc2fdc4.js → page-f7d808e31e005895.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-9a2e09c4a311764c.js → page-5c2b24b5496dad73.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/{page-bfeb35d69e8b24bb.js → page-76cdb531668b5191.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/share/page-e2b63345bba1a11f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-b008aff96be56654.js → page-d376e0c83edc041a.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d6db55fe2c04d128.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/term/page-87b88f551f421d8b.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/today/{page-6445ac4cbb8dd869.js → page-dab163b338e3b966.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/trace/{page-852170303c6cd755.js → page-08019efef111ed2e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/vault/page-0260ffc7eda50db1.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-97c7361cc2a19319.js → page-980ce6fe36ad95eb.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-20162174b77f96cf.js → page-99c4e6c7345a9156.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-8c6901833f897496.js → page-0ab42e0f05823013.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ad9220422d20ba33.js +1 -0
  52. package/apps/pwa/out/approvals/index.html +1 -1
  53. package/apps/pwa/out/approvals/index.txt +13 -13
  54. package/apps/pwa/out/autopilot/index.html +1 -1
  55. package/apps/pwa/out/autopilot/index.txt +13 -13
  56. package/apps/pwa/out/board/index.html +1 -1
  57. package/apps/pwa/out/board/index.txt +13 -13
  58. package/apps/pwa/out/botlab/index.html +1 -1
  59. package/apps/pwa/out/botlab/index.txt +13 -13
  60. package/apps/pwa/out/bots/index.html +1 -1
  61. package/apps/pwa/out/bots/index.txt +13 -13
  62. package/apps/pwa/out/ceo/index.html +1 -1
  63. package/apps/pwa/out/ceo/index.txt +13 -13
  64. package/apps/pwa/out/chat/index.html +1 -1
  65. package/apps/pwa/out/chat/index.txt +13 -13
  66. package/apps/pwa/out/consult/index.html +1 -1
  67. package/apps/pwa/out/consult/index.txt +13 -13
  68. package/apps/pwa/out/control/index.html +1 -1
  69. package/apps/pwa/out/control/index.txt +13 -13
  70. package/apps/pwa/out/decisions/index.html +1 -1
  71. package/apps/pwa/out/decisions/index.txt +13 -13
  72. package/apps/pwa/out/design-check/index.html +1 -1
  73. package/apps/pwa/out/design-check/index.txt +13 -13
  74. package/apps/pwa/out/editor/index.html +1 -1
  75. package/apps/pwa/out/editor/index.txt +13 -13
  76. package/apps/pwa/out/exec/index.html +1 -1
  77. package/apps/pwa/out/exec/index.txt +13 -13
  78. package/apps/pwa/out/field/index.html +1 -1
  79. package/apps/pwa/out/field/index.txt +13 -13
  80. package/apps/pwa/out/healthz +1 -1
  81. package/apps/pwa/out/index.html +1 -1
  82. package/apps/pwa/out/index.txt +13 -13
  83. package/apps/pwa/out/inside/index.html +1 -1
  84. package/apps/pwa/out/inside/index.txt +13 -13
  85. package/apps/pwa/out/intake/index.html +1 -1
  86. package/apps/pwa/out/intake/index.txt +13 -13
  87. package/apps/pwa/out/live/index.html +1 -1
  88. package/apps/pwa/out/live/index.txt +13 -13
  89. package/apps/pwa/out/live-run/index.html +1 -1
  90. package/apps/pwa/out/live-run/index.txt +13 -13
  91. package/apps/pwa/out/loops/index.html +1 -1
  92. package/apps/pwa/out/loops/index.txt +13 -13
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +13 -13
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +13 -13
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +13 -13
  101. package/apps/pwa/out/ops/checklist/index.html +1 -1
  102. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  103. package/apps/pwa/out/ops/release/index.html +1 -1
  104. package/apps/pwa/out/ops/release/index.txt +13 -13
  105. package/apps/pwa/out/ops/seats/index.html +1 -1
  106. package/apps/pwa/out/ops/seats/index.txt +13 -13
  107. package/apps/pwa/out/outputs/index.html +1 -1
  108. package/apps/pwa/out/outputs/index.txt +13 -13
  109. package/apps/pwa/out/reflection/index.html +1 -1
  110. package/apps/pwa/out/reflection/index.txt +14 -14
  111. package/apps/pwa/out/scheduler/index.html +1 -1
  112. package/apps/pwa/out/scheduler/index.txt +13 -13
  113. package/apps/pwa/out/sessions/index.html +1 -1
  114. package/apps/pwa/out/sessions/index.txt +13 -13
  115. package/apps/pwa/out/settings/devices/index.html +1 -1
  116. package/apps/pwa/out/settings/devices/index.txt +13 -13
  117. package/apps/pwa/out/settings/index.html +1 -1
  118. package/apps/pwa/out/settings/index.txt +13 -13
  119. package/apps/pwa/out/setup/done/index.html +1 -1
  120. package/apps/pwa/out/setup/done/index.txt +13 -13
  121. package/apps/pwa/out/setup/index.html +1 -1
  122. package/apps/pwa/out/setup/index.txt +13 -13
  123. package/apps/pwa/out/share/index.html +1 -1
  124. package/apps/pwa/out/share/index.txt +13 -13
  125. package/apps/pwa/out/showroom/index.html +1 -1
  126. package/apps/pwa/out/showroom/index.txt +13 -13
  127. package/apps/pwa/out/tasks/index.html +1 -1
  128. package/apps/pwa/out/tasks/index.txt +13 -13
  129. package/apps/pwa/out/term/index.html +1 -1
  130. package/apps/pwa/out/term/index.txt +13 -13
  131. package/apps/pwa/out/today/index.html +1 -1
  132. package/apps/pwa/out/today/index.txt +13 -13
  133. package/apps/pwa/out/trace/index.html +1 -1
  134. package/apps/pwa/out/trace/index.txt +13 -13
  135. package/apps/pwa/out/vault/index.html +1 -1
  136. package/apps/pwa/out/vault/index.txt +13 -13
  137. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  138. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  139. package/apps/pwa/out/workflows/index.html +1 -1
  140. package/apps/pwa/out/workflows/index.txt +13 -13
  141. package/apps/pwa/out/workspace/index.html +1 -1
  142. package/apps/pwa/out/workspace/index.txt +13 -13
  143. package/apps/pwa/out/worktrees/index.html +1 -1
  144. package/apps/pwa/out/worktrees/index.txt +13 -13
  145. package/package.json +1 -1
  146. package/scripts/ci-typecheck-changed.ts +81 -2
  147. package/scripts/coord-msg-bridge.ts +209 -0
  148. package/scripts/coord-post.sh +40 -7
  149. package/scripts/coord-requests-bridge.ts +10 -5
  150. package/scripts/expose-rubric-suggest.ts +1 -1
  151. package/scripts/measure-tsc-affected-roots.ts +10 -1
  152. package/scripts/release-loop/export-check-node.ts +8 -5
  153. package/scripts/release-loop/node-verdict.ts +7 -2
  154. package/src/agent/task-notification.ts +30 -1
  155. package/src/agent-substrate/review-observation.ts +9 -2
  156. package/src/agent-substrate/self-review-cli.ts +28 -6
  157. package/src/boot/daemon-tools/self-implement.ts +72 -4
  158. package/src/budget/llm-cost.ts +5 -1
  159. package/src/cli/control-cli.ts +29 -0
  160. package/src/cli/doctor-cli.ts +90 -8
  161. package/src/cli/msg-cli.ts +15 -5
  162. package/src/cli/pr-cli.ts +53 -0
  163. package/src/cli/pr-land-overlap-evidence.ts +18 -10
  164. package/src/cli/release-cli.ts +32 -5
  165. package/src/cli/seat-cli.ts +121 -0
  166. package/src/cli/self-utterances-cli.ts +140 -0
  167. package/src/cli/tasks-cli.ts +139 -9
  168. package/src/context-card/cell.ts +40 -0
  169. package/src/dashboard/boot-timing.ts +35 -0
  170. package/src/dashboard/index.ts +86 -24
  171. package/src/dashboard/input/chat-main-plain-turn-runtime.ts +26 -0
  172. package/src/dashboard/slash-runtime/dashboard-handlers.ts +6 -0
  173. package/src/debug/log.ts +1 -1
  174. package/src/domains/release-tool.ts +29 -12
  175. package/src/domains/surface-events.ts +16 -0
  176. package/src/elanous-config-dir.ts +2 -2
  177. package/src/fleet/host-doctor.ts +73 -2
  178. package/src/graph-runner/runner.ts +3 -1
  179. package/src/harness/harness-cli-command.ts +46 -4
  180. package/src/harness/harness-stop.ts +11 -7
  181. package/src/index.ts +35 -4
  182. package/src/instance/current.ts +2 -6
  183. package/src/instance/resolve.ts +5 -6
  184. package/src/llm/openai-sampling-fields.ts +89 -0
  185. package/src/llm/runtime-model-family.ts +61 -0
  186. package/src/llm/zai-coding-plan.ts +27 -1
  187. package/src/llm.ts +13 -89
  188. package/src/machines/machine-ledger.ts +31 -3
  189. package/src/maturity/cli-maturity.ts +13 -3
  190. package/src/msg/msg-store.ts +69 -7
  191. package/src/ops/overview-collect.ts +3 -2
  192. package/src/ops/overview-owner.ts +29 -0
  193. package/src/plugin-hooks/shell-hook.ts +17 -2
  194. package/src/plugin-missions/evaluator-runner.ts +18 -2
  195. package/src/plugins/install/plugin-install.ts +11 -1
  196. package/src/prompt-enhance/enhance.ts +11 -0
  197. package/src/release-loop/checklist.ts +41 -4
  198. package/src/release-loop/feature-store.ts +10 -9
  199. package/src/seat-loop/seat-loop.ts +5 -1
  200. package/src/self-dev/draft-sweep.ts +23 -14
  201. package/src/self-dev/orchestrate.ts +6 -2
  202. package/src/self-dev/run-supervisor.ts +35 -1
  203. package/src/self-implement/goal-author.ts +3 -27
  204. package/src/self-implement/goal-command-execution.ts +26 -0
  205. package/src/self-implement/orchestrator.ts +2 -1
  206. package/src/self-implement/pwa-import-graph.ts +194 -7
  207. package/src/self-implement/self-implement-runtime.ts +126 -21
  208. package/src/session/index.ts +14 -4
  209. package/src/session-runtime/index.ts +55 -11
  210. package/src/task-agent/cover.ts +28 -9
  211. package/src/task-agent/live-moves.ts +14 -6
  212. package/src/task-agent/rejudge-on-head.ts +220 -23
  213. package/src/task-agent/review-result-capture.ts +64 -1
  214. package/src/task-agent/shadow.ts +137 -22
  215. package/src/task-agent/task-hand.ts +18 -2
  216. package/src/task-orchestrator/surfaces/self-implement-pod.ts +1 -0
  217. package/src/tool-runtime/tool-search-runtime.ts +25 -0
  218. package/src/tui.ts +20 -1
  219. package/src/user-config.ts +3 -59
  220. package/src/version/packed-revision.json +1 -1
  221. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f08f6529d09ffae1.js +0 -1
  222. package/apps/pwa/out/_next/static/chunks/app/board/page-f000d54d258f9ef6.js +0 -1
  223. package/apps/pwa/out/_next/static/chunks/app/botlab/page-b12149c51f7d04b1.js +0 -1
  224. package/apps/pwa/out/_next/static/chunks/app/bots/page-22f674e7e4c2c9d5.js +0 -1
  225. package/apps/pwa/out/_next/static/chunks/app/ceo/page-8888733e40cea6d1.js +0 -1
  226. package/apps/pwa/out/_next/static/chunks/app/chat/page-9717ec6964b92276.js +0 -1
  227. package/apps/pwa/out/_next/static/chunks/app/design-check/page-dea4b795741126fd.js +0 -1
  228. package/apps/pwa/out/_next/static/chunks/app/editor/page-35e05d7450461a67.js +0 -1
  229. package/apps/pwa/out/_next/static/chunks/app/inside/page-b2638013c0bf7dfd.js +0 -1
  230. package/apps/pwa/out/_next/static/chunks/app/layout-c44f93a41167536f.js +0 -1
  231. package/apps/pwa/out/_next/static/chunks/app/loops/page-1b810474507f3702.js +0 -1
  232. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6a14333d1b1b3967.js +0 -1
  233. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-8c91a9a66c9ec90a.js +0 -1
  234. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4e634c5b98fd4e93.js +0 -1
  235. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-ed3070d637850548.js +0 -1
  236. package/apps/pwa/out/_next/static/chunks/app/reflection/page-128458c8d86fbb5f.js +0 -1
  237. package/apps/pwa/out/_next/static/chunks/app/sessions/page-cf0c2b0183991d09.js +0 -1
  238. package/apps/pwa/out/_next/static/chunks/app/share/page-89cb33bced824e4a.js +0 -1
  239. package/apps/pwa/out/_next/static/chunks/app/tasks/page-814394dcbae789b1.js +0 -1
  240. package/apps/pwa/out/_next/static/chunks/app/term/page-6a87325c87f09dd2.js +0 -1
  241. package/apps/pwa/out/_next/static/chunks/app/vault/page-087ceeb975ef272b.js +0 -1
  242. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-05a4b2c9b91034aa.js +0 -1
  243. /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → Ev2jivDMTSoEc-5rIXdcy}/_buildManifest.js +0 -0
  244. /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → Ev2jivDMTSoEc-5rIXdcy}/_ssgManifest.js +0 -0
@@ -23,7 +23,7 @@
23
23
  * fetch 한 뒤 PR 가지에 3-way merge(rerere 켬 · ⛔ rebase/force-push/reset/파일째 checkout) 하고 일반 push 로 새 머리를 만든 다음, 그 머리는 기존
24
24
  * ①②③ 사슬을 탄다. 머리마다 1회 · 카드당 `CATCH_UP_CAP`(기본 2)회. 충돌이면 `merge --abort` ⊕ needs-owner(`main 따라잡기 충돌 · 파일 …`).
25
25
  * 관측 `rejudge-on-head {step:'catch-up', decision: pushed|conflict|cap-reached, fromBase, toBase, newHead}`.
26
- * - 대상: 카드에 묶인 PR(URL 로 저장소 확인) ⊕ 그 PR 로 TA 가 이미 리뷰를 요청한 카드(= 런이 멈춰 TA 판단이 시작됐다).
26
+ * - 대상: 카드에 묶인 PR(URL 로 저장소 확인) ⊕ 기존 TA 리뷰 또는 원격 부모→자식 간선·같은 PR·자식 run-status 종결 증거.
27
27
  * - 사람의 hold(draft PR)는 뒤집지 않는다 — 재게이트(`noMerge`)와 land 관문이 draft 를 거부한다.
28
28
  * - 허용: 설정 `taskAgent.liveMoves` 그대로 — 재게이트·land 는 `propose-land`, 리뷰 재요청은 `review` 가 있을 때만(없으면 «했을 수»).
29
29
  * - 관측: `task-agent` · `rejudge-on-head {card, pr, fromHead, toHead, step, decision, reason}` — 수가 난 판단은 카드 history 에도
@@ -34,10 +34,13 @@ import { spawn, spawnSync } from 'node:child_process';
34
34
  import { tmpdir } from 'node:os';
35
35
  import { dirname, join } from 'node:path';
36
36
  import { debug } from '../debug/log.js';
37
+ import { prodInstanceRoot } from '../instance/resolve.js';
38
+ import { loadRunLedger, type RunLedgerEntry } from '../self-implement/run-ledger.js';
37
39
  import type { DetachedSpawn } from '../cli/tasks-cli.js';
40
+ import { remoteLedgerMirrorDir } from './parent-host.js';
38
41
  import { appendLandOrderRow, joinLandOrderRow } from './land-order-ledger.js';
39
42
  import { recordLiveMoveHistory } from './live-move-history.js';
40
- import { defaultPrHead, defaultReviewRepoCandidates, executeLiveLand, executeLiveReview, landRetryPending, type LiveMoveDeps, type LiveMoveResult, type PrHeadView, type ReviewRepoCandidate, type TaskAgentLiveMove } from './live-moves.js';
43
+ import { defaultLandPrHead, defaultPrHead, defaultReviewRepoCandidates, executeLiveLand, executeLiveReview, landRetryPending, type LiveMoveDeps, type LiveMoveResult, type PrHeadView, type ReviewRepoCandidate, type TaskAgentLiveMove } from './live-moves.js';
41
44
  import { capturedLandReview, captureReviewResults, parseReviewResultOutput, pendingReviewResults, type ParsedReviewResult } from './review-result-capture.js';
42
45
  import { cardPrNumber, readTaskCard, taskAgentStatePath, updateTaskCard, type TaskCard } from './task-hand.js';
43
46
 
@@ -49,7 +52,7 @@ export const CATCH_UP_CAP = 2;
49
52
  /** 떼어 띄운 재게이트 결과를 기다리는 상한 — 지나도 파일이 없으면 «결과 없음»으로 적는다(같은 머리는 다시 안 돌린다). */
50
53
  export const REGATE_RESULT_WAIT_MS = 90 * 60_000;
51
54
 
52
- export type RejudgeStep = 'regate' | 'review' | 'land' | 'target' | 'catch-up';
55
+ export type RejudgeStep = 'regate' | 'review' | 'ready' | 'land' | 'target' | 'catch-up';
53
56
  export type CatchUpDecision = 'pushed' | 'conflict' | 'cap-reached' | 'failed';
54
57
  export interface RejudgeOutcome {
55
58
  card: string; pr: number | null; fromHead: string | null; toHead: string | null; step: RejudgeStep; decision: string; reason: string;
@@ -71,6 +74,12 @@ export type CatchUpGit = (cwd: string, args: string[]) => { status: number | nul
71
74
  export interface RejudgeDeps {
72
75
  live?: LiveMoveDeps;
73
76
  liveMoves: ReadonlySet<TaskAgentLiveMove>;
77
+ /** Only phase ① shadow callbacks may inspect a previously judged head without a regate. */
78
+ shadowSameHead?: boolean;
79
+ /** HQ mirror proof that the bound remote child has stopped. Test seam; absence fails closed. */
80
+ remoteRunTerminal?: (card: TaskCard) => boolean;
81
+ /** Make a reviewed draft PR ready; tests inject this instead of calling gh. */
82
+ readyPr?: (pr: number, cwd: string) => Promise<{ ok: boolean; reason: string }>;
74
83
  /** 재게이트를 떼어 띄운다 — 시험은 주입한다. 기본은 `tasks regate-run` 자식(stdio ignore · 결과는 파일). */
75
84
  requestRegate?: (request: RegateRequest) => Promise<void>;
76
85
  /** TA-REJUDGE-REBASE-LOOP — origin/main 을 PR 가지에 3-way merge 하고 일반 push. 시험은 주입한다. */
@@ -80,12 +89,149 @@ export interface RejudgeDeps {
80
89
  readFile?: (path: string) => string;
81
90
  }
82
91
 
92
+ /** PR binding alone does not mean a remote parent has stopped. Require its child edge and terminal ledger row. */
93
+ export function remoteBoundRunTerminal(card: TaskCard, load: (runId: string, dir: string) => RunLedgerEntry[] | null = loadRunLedger): boolean {
94
+ const pr = cardPrNumber(card);
95
+ const url = typeof card.pr === 'object' ? card.pr.url : undefined;
96
+ if (card.status !== 'launched' || card.taLand !== true || !card.parentHost || !card.runId || !card.runChildId || !pr || !url) return false;
97
+ try {
98
+ const dir = remoteLedgerMirrorDir(prodInstanceRoot(), card.parentHost.host);
99
+ const parent = load(card.runId, dir);
100
+ if (!parent?.some((entry) => entry.event === 'pod-child-run' && entry.data?.childRunId === card.runChildId)) return false;
101
+ const child = load(card.runChildId, dir);
102
+ return !!child?.some((entry) => entry.event === 'pr-opened' && entry.data?.number === pr && entry.data.url === url)
103
+ && child.some((entry) => entry.event === 'run-status' && ['completed', 'failed', 'cancelled'].includes(String(entry.data?.runStatus)));
104
+ } catch { return false; }
105
+ }
106
+
107
+ /** Shared completion callback for detached review and regate workers. */
108
+ type CompletedWorker = { pr: number; head: string; kind?: 'review' | 'regate'; resultPath?: string };
109
+
110
+ export async function rejudgeAsyncResultCard(
111
+ cardId: string, statePath: string,
112
+ external: Pick<LiveMoveDeps, 'repoCandidates' | 'prHead' | 'landPrHead' | 'land' | 'landWorktree' | 'requestReview'>
113
+ & Pick<RejudgeDeps, 'requestRegate'> = {},
114
+ completed?: CompletedWorker,
115
+ ): Promise<RejudgeOutcome[]> {
116
+ const { requestRegate, ...live } = external;
117
+ return rejudgeShadowCard(cardId, statePath, { live, requestRegate }, completed);
118
+ }
119
+
120
+ export async function rejudgeShadowCard(cardId: string, statePath: string, deps: Partial<RejudgeDeps> = {}, completed?: CompletedWorker): Promise<RejudgeOutcome[]> {
121
+ // TA-LAND-AFTER-REVIEW phase ①: completion paths observe the decision but execute no
122
+ // regate/review/land move. A post-landing canary will gate the separate live switch.
123
+ const outcomes = await rejudgeCardOnHead(cardId, { ...deps, liveMoves: new Set(), shadowSameHead: true, live: { ...deps.live, statePath } });
124
+ requireSuccessfulRejudge(outcomes);
125
+ if (completed) requireCompletedHeadRejudge(outcomes, completed.pr, completed.head);
126
+ const last = outcomes.at(-1);
127
+ if (last) {
128
+ const decision = last.decision === 'would-land' ? 'land' : last.decision;
129
+ let claimed = false;
130
+ try {
131
+ updateTaskCard(statePath, cardId, (current) => {
132
+ if (!current || current.status !== 'launched' || current.taLand !== true || last.pr === null
133
+ || cardPrNumber(current) !== last.pr || !last.toHead || !/^[0-9a-f]{40}$/i.test(last.toHead)
134
+ || (completed?.kind && (!completed.resultPath || !(completed.kind === 'review' ? current.reviewRequests : current.regates)
135
+ ?.some((entry) => entry.pr === completed.pr && sameHead(entry.head, completed.head) && entry.resultPath === completed.resultPath)))
136
+ || (decision === 'land' && (!capturedLandReview(current, last.pr, last.toHead)
137
+ || !(current.regates ?? []).some((entry) => entry.pr === last.pr && sameHead(entry.head, last.toHead!) && entry.passed === true && entry.resultPath)))
138
+ || (current.history ?? []).some((item) => item.event === 'rejudge-shadow' && item.pr === last.pr && item.head === last.toHead && item.effect === decision)) return undefined;
139
+ claimed = true;
140
+ return { ...current, history: [...(current.history ?? []), { at: new Date().toISOString(), event: 'rejudge-shadow', pr: last.pr, head: last.toHead ?? undefined, effect: decision, detail: last.reason }] };
141
+ });
142
+ if (claimed) debug.log('task-agent', 'rejudge-shadow', {
143
+ card: cardId, head12: last.toHead?.slice(0, 12) ?? null, decision, reason: last.reason,
144
+ });
145
+ } catch { /* observation must not change completion */ }
146
+ }
147
+ return outcomes;
148
+ }
149
+
150
+ /** A completed callback must not claim success when the rejudge returned a failed move. */
151
+ export function requireSuccessfulRejudge(result: unknown): void {
152
+ if (result == null || (Array.isArray(result) && result.length === 0)) throw new Error('task-agent rejudge returned no decision');
153
+ if (result === false || (result && typeof result === 'object' && !Array.isArray(result)
154
+ && ('ok' in result && result.ok === false || 'success' in result && result.success === false))) {
155
+ throw new Error('task-agent rejudge returned failure');
156
+ }
157
+ if (!Array.isArray(result)) throw new Error('task-agent rejudge returned no decision array');
158
+ const failed = result.find((item) => {
159
+ if (item && typeof item === 'object' && 'ok' in item && item.ok === false) return true;
160
+ const decision = item && typeof item === 'object' && 'decision' in item ? item.decision : undefined;
161
+ return typeof decision === 'string' && (decision.endsWith('-failed') || decision.endsWith('-rejected')
162
+ || ['no-pr', 'card-unreadable', 'conflict', 'cap-reached'].includes(decision));
163
+ });
164
+ if (failed) throw new Error(`task-agent rejudge returned failure: ${String(failed.decision ?? 'ok:false')}`);
165
+ }
166
+
167
+ /** A completion marker belongs to the exact PR head whose worker result triggered it. */
168
+ export function requireCompletedHeadRejudge(result: unknown, pr: number, head: string): void {
169
+ requireSuccessfulRejudge(result);
170
+ const last = Array.isArray(result) ? result.at(-1) : undefined;
171
+ if (!last || typeof last !== 'object' || last.pr !== pr || typeof last.toHead !== 'string'
172
+ || !/^[0-9a-f]{40}$/i.test(last.toHead) || !sameHead(last.toHead, head)) {
173
+ throw new Error('task-agent rejudge result head does not match completed worker head');
174
+ }
175
+ }
176
+
177
+ const regateRejudgeInFlight = new Set<string>();
178
+
179
+ /** Regate worker has written the exact result file; claim one callback for its card/head/result. */
180
+ export async function rejudgeCompletedTaskRegate(
181
+ cardId: string, pr: number, head: string, resultPath: string, statePath: string,
182
+ rejudge: (cardId: string, statePath: string) => Promise<unknown> = rejudgeAsyncResultCard,
183
+ rejudgeDeps?: Partial<RejudgeDeps>,
184
+ ): Promise<boolean> {
185
+ if (!/^[0-9a-f]{40}$/i.test(head)) return false;
186
+ const key = `${statePath}\0${cardId}\0${pr}\0${head}\0${resultPath}`;
187
+ if (regateRejudgeInFlight.has(key)) return false;
188
+ const card = readTaskCard(cardId, statePath);
189
+ if (card?.status !== 'launched' || card.taLand !== true || cardPrNumber(card) !== pr
190
+ || !(card.regates ?? []).some((entry) => entry.pr === pr && sameHead(entry.head, head) && entry.resultPath === resultPath && entry.passed === undefined)
191
+ || (card.history ?? []).some((entry) => entry.event === 'async-result-rejudge' && entry.pr === pr && sameHead(entry.head ?? '', head) && entry.resultPath === resultPath)) return false;
192
+ let text: string;
193
+ try { text = readFileSync(resultPath, 'utf8'); } catch { return false; }
194
+ const parsed = parseRegateResult(text, pr, head);
195
+ if (!parsed) return false;
196
+ regateRejudgeInFlight.add(key);
197
+ try {
198
+ const run = () => rejudgeDeps
199
+ ? rejudgeShadowCard(cardId, statePath, rejudgeDeps, { pr, head, kind: 'regate', resultPath })
200
+ : rejudge === rejudgeAsyncResultCard ? rejudgeAsyncResultCard(cardId, statePath, {}, { pr, head, kind: 'regate', resultPath }) : rejudge(cardId, statePath);
201
+ try { requireCompletedHeadRejudge(await run(), pr, head); } catch { requireCompletedHeadRejudge(await run(), pr, head); } // Bounded retry; no marker on failed outcomes.
202
+ updateTaskCard(statePath, cardId, (current) => parsed.passed === true && current?.status === 'launched' && current.taLand === true && cardPrNumber(current) === pr
203
+ && (current.regates ?? []).some((entry) => entry.pr === pr && sameHead(entry.head, head) && entry.resultPath === resultPath
204
+ && (entry.passed === undefined || entry.passed === parsed.passed))
205
+ && !(current.history ?? []).some((entry) => entry.event === 'async-result-rejudge' && entry.pr === pr && sameHead(entry.head ?? '', head) && entry.resultPath === resultPath)
206
+ ? { ...current, history: [...(current.history ?? []), { at: new Date().toISOString(), event: 'async-result-rejudge', kind: 'regate', pr, head, resultPath, effect: 'completion callback succeeded' }] }
207
+ : undefined);
208
+ } finally {
209
+ regateRejudgeInFlight.delete(key);
210
+ }
211
+ return true;
212
+ }
213
+
83
214
  const safe = (value: string) => value.replace(/[^A-Za-z0-9._-]/g, '_');
84
215
  export function regateResultPath(statePath: string, cardId: string, pr: number, head: string, staleBase?: string): string {
85
216
  const suffix = staleBase ? `-from-${safe(staleBase.slice(0, 12))}` : '';
86
217
  return join(dirname(statePath), 'regate-results', `${safe(cardId)}-${pr}-${safe(head.slice(0, 12))}${suffix}.json`);
87
218
  }
88
219
 
220
+ function readyRegateResultPath(statePath: string, cardId: string, pr: number, head: string): string {
221
+ return regateResultPath(statePath, cardId, pr, head).replace(/\.json$/, '-after-ready.json');
222
+ }
223
+
224
+ export async function defaultReadyPr(pr: number, cwd: string): Promise<{ ok: boolean; reason: string }> {
225
+ const run = (env: NodeJS.ProcessEnv) => spawnSync('gh', ['pr', 'ready', String(pr)], { cwd, env, encoding: 'utf8', timeout: 30_000 });
226
+ let result = run(process.env);
227
+ if (result.status !== 0) {
228
+ const noProxy = { ...process.env };
229
+ for (const key of ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy']) delete noProxy[key];
230
+ result = run(noProxy);
231
+ }
232
+ return { ok: result.status === 0, reason: (result.stderr || result.stdout || `gh pr ready exit ${result.status ?? 'unknown'}`).trim().slice(-200) };
233
+ }
234
+
89
235
  /** REJUDGE base-stale — 이 재게이트(요청 시각 `since`) «뒤»에 같은 머리 land 가 base-stale 로 거절된 마지막 시도. */
90
236
  export function baseStaleAttemptAfter(card: Pick<TaskCard, 'landAttempts'>, pr: number, head: string, since: string): NonNullable<TaskCard['landAttempts']>[number] | undefined {
91
237
  const from = Date.parse(since);
@@ -258,8 +404,9 @@ export async function rejudgeCardOnHead(cardId: string, deps: RejudgeDeps): Prom
258
404
  const pr = card ? cardPrNumber(card) : undefined;
259
405
  if (!card || pr === undefined) return target('no-pr', 'card has no bound PR');
260
406
  const reviews = (card.reviewRequests ?? []).filter((entry) => entry.pr === pr);
261
- // 런이 멈춰 TA 가 이 PR 을 리뷰한 적이 있어야 재판정한다 — 도는 런의 PR 에 끼어들지 않는다.
262
- if (!reviews.length) return target('not-started', 'no task-agent review on this PR yet (run not stopped/judged)', pr);
407
+ // 원격 부모는 node-b 의 liveMoves 가 꺼져 리뷰 이력이 0일 수 있다. HQ 거울의 종결 증거가 있을 때만 시작한다.
408
+ if (!reviews.length && (!card.parentHost || !(deps.remoteRunTerminal ?? remoteBoundRunTerminal)(card)))
409
+ return target('not-started', 'no task-agent review on this PR yet (remote terminal unproven or run not stopped/judged)', pr);
263
410
  // 떼어 띄운 재게이트 결과를 «머리와 무관하게» 먼저 회수한다 — 결과가 나오기 전에 머리가 또 바뀌어도 그 머리의 결과는 카드에 남는다.
264
411
  for (const pending of (card.regates ?? []).filter((entry) => entry.pr === pr && entry.passed === undefined && entry.resultPath)) {
265
412
  let text: string | null = null;
@@ -299,6 +446,44 @@ export async function rejudgeCardOnHead(cardId: string, deps: RejudgeDeps): Prom
299
446
  const step = (s: RejudgeStep, decision: string, reason: string, record: boolean, extra: Partial<RejudgeOutcome> = {}) =>
300
447
  emit({ card: card!.id, pr, fromHead, toHead, step: s, decision, reason, ...extra }, record);
301
448
 
449
+ // A remote --ta-land parent leaves its PR draft. Only a captured review for this exact
450
+ // head may let HQ make it ready; the regate runner deliberately rejects draft PRs.
451
+ let readyConfirmed = false;
452
+ const readyReview = card.taLand ? capturedLandReview(card, pr, toHead) : undefined;
453
+ if (readyReview?.mustFixCount === 0 && deps.liveMoves.has('propose-land')) {
454
+ let fresh: Awaited<ReturnType<NonNullable<LiveMoveDeps['landPrHead']>>>;
455
+ try { fresh = await (live.landPrHead ?? defaultLandPrHead)(pr, cwd); } catch { fresh = null; }
456
+ if (!fresh || fresh.state !== 'OPEN' || !sameHead(fresh.head, toHead) || !fresh.url || !sameUrl(fresh.url, url)) {
457
+ step('ready', 'ready-unconfirmed', 'fresh PR head, URL or draft state unavailable — no regate', false);
458
+ return outcomes;
459
+ }
460
+ if (fresh.isDraft) {
461
+ const result = await (deps.readyPr ?? defaultReadyPr)(pr, cwd);
462
+ if (!result.ok) {
463
+ step('ready', 'ready-failed', result.reason, true);
464
+ return outcomes;
465
+ }
466
+ try { fresh = await (live.landPrHead ?? defaultLandPrHead)(pr, cwd); } catch { fresh = null; }
467
+ if (!fresh || fresh.isDraft || fresh.state !== 'OPEN' || !sameHead(fresh.head, toHead) || !fresh.url || !sameUrl(fresh.url, url)) {
468
+ step('ready', 'ready-unconfirmed', 'gh pr ready returned success but the same PR/head is not confirmed ready', true);
469
+ return outcomes;
470
+ }
471
+ step('ready', 'ready-confirmed', `PR #${pr} head ${toHead.slice(0, 12)} is ready after captured review`, true);
472
+ }
473
+ readyConfirmed = true;
474
+ }
475
+
476
+ if (!reviews.length) {
477
+ if (!deps.liveMoves.has('review')) step('review', 'would-review', 'remote run terminal proven · review not enabled in HQ taskAgent.liveMoves — shadow', false);
478
+ else {
479
+ const before = { reviewRequests: card.reviewRequests, landAttempts: card.landAttempts };
480
+ const result = await executeLiveReview(card, pr, { runId: card.runId ?? null, stopReason: 'remote-run-terminal', cwd }, { ...live, ...(deps.log ? { log: deps.log } : {}) });
481
+ recordMove(statePath, card.id, card.runId, 'review', result, pr, before, now, deps.log);
482
+ step('review', result.executed ? 'review-requested' : 'review-request-failed', result.detail, true);
483
+ }
484
+ return outcomes;
485
+ }
486
+
302
487
  const tryCatchUp = async (fromBase: string | undefined, why: string): Promise<boolean> => {
303
488
  const prior = catchUpHistory(card!, pr);
304
489
  // 상한은 «실제 push» 만 센다 — 충돌·실패 뒤 사람이 새 머리를 올리면 다시 시도할 수 있어야 한다.
@@ -341,13 +526,22 @@ export async function rejudgeCardOnHead(cardId: string, deps: RejudgeDeps): Prom
341
526
  return true;
342
527
  };
343
528
 
344
- // 머리가 런 멈춤 때 그대로면 재판정하지 않는다 — 그 머리의 재게이트·리뷰·land 판단은 멈춤 때의 판단(슈퍼바이저 틱)이 이미 했다.
345
- // TA-LAND-RETRY-ON-INFRA — 단, 그 머리의 land 가 TA 쪽 인프라 결손으로 실패해 재시도가 남았으면 아래 ①②③ 을 그대로 탄다
346
- // (재게이트·회수한 리뷰·겹침 증거를 다시 갖춘 뒤에만 land — 관문은 그대로).
347
- if (!regates.length && judgedHead && sameHead(toHead, judgedHead) && !landRetryPending(card, pr, toHead)) return target('head-unchanged', `PR #${pr} head is still the judged head`, pr, fromHead, toHead);
529
+ // 기존 live 경로의 같은 머리 조기 반환은 유지한다. 완료 훅의 그림자 판단만 통과 리뷰를 다시 본다.
530
+ if (!regates.length && judgedHead && sameHead(toHead, judgedHead) && !landRetryPending(card, pr, toHead)) {
531
+ const unchangedReview = capturedLandReview(card, pr, toHead);
532
+ if (!deps.shadowSameHead || deps.liveMoves.size !== 0 || !unchangedReview || !['pass', 'warn'].includes(unchangedReview.verdict) || unchangedReview.mustFixCount !== 0) {
533
+ return target('head-unchanged', `PR #${pr} head is still the judged head`, pr, fromHead, toHead);
534
+ }
535
+ }
536
+ // `regates` 는 PR 전체 기록이다. 현재 머리의 결과만 재사용하고, 없으면 재게이트한다.
537
+ const currentHeadRegate = regates.filter((entry) => sameHead(entry.head, toHead)).at(-1);
348
538
 
349
539
  // ① 재게이트 — 지금 머리의 «마지막» 기록(REJUDGE base-stale 로 같은 머리를 다시 잰 기록이 뒤에 붙는다).
350
- const regate = regates.filter((entry) => sameHead(entry.head, toHead)).at(-1);
540
+ const regate = currentHeadRegate;
541
+ // A draft rejection is a stale input after the same PR becomes ready, not a permanent
542
+ // judgement about the code. Retry this exact head once with a separate result path.
543
+ const retryDraft = readyConfirmed && regate?.passed === false && /^pr-head:.*\bisDraft=true\b/.test(regate.detail ?? '')
544
+ && !regates.some((entry) => sameHead(entry.head, toHead) && entry.readyRetry === true);
351
545
  // REJUDGE base-stale — 이 머리의 통과한 재게이트로 land 했더니 base-stale-overlap 으로 거절됐다 → 새 base 위에서 한 번 더 잰다.
352
546
  const stale = regate?.passed === true ? baseStaleAttemptAfter(card, pr, toHead, regate.at) : undefined;
353
547
  const staleBase = stale ? regate?.baseCommit : undefined;
@@ -355,8 +549,9 @@ export async function rejudgeCardOnHead(cardId: string, deps: RejudgeDeps): Prom
355
549
  await tryCatchUp(undefined, `base-stale-overlap on head ${toHead.slice(0, 12)} but the stale regate has no baseCommit`);
356
550
  return outcomes;
357
551
  }
358
- if (!regate || staleBase) {
359
- const why = staleBase ? `base-stale-overlap — gate base ${staleBase.slice(0, 12)} went stale (${(stale!.reason ?? '').replace(/^base-stale-overlap:\s*/, '').slice(0, 160)})` : '';
552
+ if (!regate || staleBase || retryDraft) {
553
+ const why = staleBase ? `base-stale-overlap — gate base ${staleBase.slice(0, 12)} went stale (${(stale!.reason ?? '').replace(/^base-stale-overlap:\s*/, '').slice(0, 160)})`
554
+ : retryDraft ? 'draft rejection became stale after PR ready' : '';
360
555
  if (regates.length >= REJUDGE_HEAD_CAP) {
361
556
  step('regate', 'cap-reached', `${why ? `${why} · ` : ''}re-gated ${regates.length} times already (cap ${REJUDGE_HEAD_CAP}) — no more rejudge; needs owner`, true);
362
557
  return outcomes;
@@ -364,17 +559,19 @@ export async function rejudgeCardOnHead(cardId: string, deps: RejudgeDeps): Prom
364
559
  if (!deps.liveMoves.has('propose-land')) step('regate', 'would-regate', `${why ? `${why} · ` : ''}propose-land not enabled in taskAgent.liveMoves — shadow`, false);
365
560
  else {
366
561
  const at = now().toISOString();
367
- const resultPath = regateResultPath(statePath, card.id, pr, toHead, staleBase);
562
+ const resultPath = retryDraft ? readyRegateResultPath(statePath, card.id, pr, toHead)
563
+ : regateResultPath(statePath, card.id, pr, toHead, staleBase);
368
564
  let claimed = false;
369
565
  let capped = false;
370
566
  updateTaskCard(statePath, card.id, (current) => {
371
567
  const held = (current?.regates ?? []).filter((entry) => entry.pr === pr);
372
568
  // 멱등 — 처음 재게이트는 «머리», base-stale 재게이트는 «머리 ⊕ 낡은 base» 가 키다(같은 머리·같은 base 로 두 번 안 잰다).
373
- if (!current || held.some((entry) => sameHead(entry.head, toHead) && (staleBase ? entry.staleBase !== undefined && sameHead(entry.staleBase, staleBase) : true))) return undefined;
569
+ if (!current || held.some((entry) => sameHead(entry.head, toHead) && (staleBase ? entry.staleBase !== undefined && sameHead(entry.staleBase, staleBase)
570
+ : retryDraft ? entry.readyRetry === true : true))) return undefined;
374
571
  // 상한은 잠금 안의 «지금» 카드로 다시 잰다 — 다른 머리를 본 두 틱이 겹쳐도 상한을 넘지 않게.
375
572
  if (held.length >= REJUDGE_HEAD_CAP) { capped = true; return undefined; }
376
573
  claimed = true;
377
- return { ...current, regates: [...(current.regates ?? []), { pr, head: toHead, at, resultPath, ...(staleBase ? { staleBase } : {}) }] };
574
+ return { ...current, regates: [...(current.regates ?? []), { pr, head: toHead, at, resultPath, ...(staleBase ? { staleBase } : {}), ...(retryDraft ? { readyRetry: true as const } : {}) }] };
378
575
  });
379
576
  if (capped) {
380
577
  step('regate', 'cap-reached', `${why ? `${why} · ` : ''}re-gated ${REJUDGE_HEAD_CAP} times already (cap ${REJUDGE_HEAD_CAP} · claim-time recheck) — no more rejudge; needs owner`, true);
@@ -395,7 +592,7 @@ export async function rejudgeCardOnHead(cardId: string, deps: RejudgeDeps): Prom
395
592
  }
396
593
  }
397
594
  // base-stale 재게이트는 그 결과가 나와야 land 한다 — 낡은 base 의 증거로 이번 틱에 다시 land 하지 않는다.
398
- if (staleBase) return outcomes;
595
+ if (staleBase || retryDraft) return outcomes;
399
596
  } else if (regate.passed === undefined) {
400
597
  step('regate', 'awaiting-regate-result', 'host regate still running', false);
401
598
  }
@@ -463,10 +660,7 @@ export async function rejudgeCardOnHead(cardId: string, deps: RejudgeDeps): Prom
463
660
  // review-results 파일을 못 읽으면 reviewVerdict 'missing' — 0 으로 쓰지 않는다(land 판정의 회수 결과와는 별개 파일).
464
661
  let reviewForLedger: ParsedReviewResult | null = null;
465
662
  try { reviewForLedger = parseReviewResultOutput(read(reviewResult)); } catch { reviewForLedger = null; }
466
- appendLandOrderRow(statePath, joinLandOrderRow({
467
- overlapEvidence: { ...overlapEvidenceContent, cardId: card.id },
468
- reviewResult: reviewForLedger, landed, at: now().toISOString(),
469
- }));
663
+ appendLandOrderRow(statePath, joinLandOrderRow({ overlapEvidence: { ...overlapEvidenceContent, cardId: card.id }, reviewResult: reviewForLedger, landed, at: now().toISOString() }));
470
664
  landOrderLog('appended');
471
665
  } catch {
472
666
  landOrderLog('append-failed');
@@ -476,7 +670,9 @@ export async function rejudgeCardOnHead(cardId: string, deps: RejudgeDeps): Prom
476
670
  const attempt = landed.landAttempt?.class
477
671
  ? ` · land-class ${landed.landAttempt.class} (${landed.landAttempt.reason}) · attempt ${landed.landAttempt.attempt} · ${landed.landAttempt.class === 'base-stale' ? 'base-stale → re-gate on a new base next tick (cap counts)' : landed.landAttempt.retryAllowed ? 'retry allowed on this head' : 'no retry on this head'}`
478
672
  : '';
479
- step('land', landed.executed && landed.ok ? 'landed' : 'land-rejected', `${landed.detail}${attempt} · overlap-evidence ${overlapEvidence}`, true);
673
+ step('land', landed.executed && landed.ok ? 'landed'
674
+ : !landed.executed && landed.ok ? 'land-skipped' : 'land-rejected',
675
+ `${landed.detail}${attempt} · overlap-evidence ${overlapEvidence}`, true);
480
676
  return outcomes;
481
677
  }
482
678
 
@@ -487,10 +683,11 @@ function recordMove(statePath: string, cardId: string, runId: string | undefined
487
683
  } catch { /* recordLiveMoveHistory 는 스스로 관측한다 */ }
488
684
  }
489
685
 
490
- /** 재판정 대상 카드 — 묶인 PR ⊕ 그 PR 로 TA 리뷰를 요청한 적이 있는 카드. */
686
+ /** 재판정 대상 카드 — 기존 리뷰 또는 HQ 가 종결 원장을 검증할 원격 부모 카드. */
491
687
  export function rejudgeCandidates(tasks: Readonly<Record<string, TaskCard>>): string[] {
492
688
  return Object.values(tasks).filter((card) => {
493
689
  const pr = cardPrNumber(card);
494
- return pr !== undefined && (card.reviewRequests ?? []).some((entry) => entry.pr === pr);
690
+ return pr !== undefined && ((card.reviewRequests ?? []).some((entry) => entry.pr === pr)
691
+ || (card.status === 'launched' && card.taLand === true && !!card.parentHost && !!card.runId && !!card.runChildId));
495
692
  }).map((card) => card.id);
496
693
  }
@@ -14,14 +14,21 @@ import { readFileSync } from 'node:fs';
14
14
  import { dirname, join } from 'node:path';
15
15
  import { isNoneSentinelFinding } from '../agent-substrate/pr-reviewer.js';
16
16
  import { debug } from '../debug/log.js';
17
- import { LIVE_MOVE_RESULT_EVENT, readTaskAgentState, updateTaskCard, type TaskCard, type TaskCardEvent } from './task-hand.js';
17
+ import { LIVE_MOVE_RESULT_EVENT, cardPrNumber, readTaskAgentState, readTaskCard, updateTaskCard, type TaskCard, type TaskCardEvent } from './task-hand.js';
18
+ import type { RejudgeDeps } from './rejudge-on-head.js';
18
19
 
19
20
  export const REVIEW_RESULT_MISSING_EFFECT = 'review process produced no result';
20
21
  /** 결과 파일은 있는데 읽지 못했다(EACCES 등) — «결과 없음»과 가른다(결과가 있었을 수 있다). */
21
22
  export const REVIEW_RESULT_UNREADABLE_EFFECT = 'review result file unreadable';
23
+ /** Detached self review child: only this card and exact result file may trigger a completion tick. */
24
+ export const TA_REVIEW_COMPLETION_CARD_ENV = 'ELANOUS_TA_REVIEW_COMPLETION_CARD';
25
+ export const TA_REVIEW_COMPLETION_STATE_ENV = 'ELANOUS_TA_REVIEW_COMPLETION_STATE';
26
+ export const TA_REVIEW_COMPLETION_RESULT_ENV = 'ELANOUS_TA_REVIEW_COMPLETION_RESULT';
22
27
  /** 리뷰 한 판(1차 ⊕ 폴백 · ACP 300s 상한 여러 번)을 넉넉히 덮는 대기 상한 — 이 안에는 «아직»으로 본다. */
23
28
  export const REVIEW_RESULT_WAIT_MS = 90 * 60_000;
24
29
 
30
+ const reviewRejudgeInFlight = new Set<string>();
31
+
25
32
  const SHA40 = /^[0-9a-f]{40}$/i;
26
33
 
27
34
  /** 결과 파일 경로 — 카드 상태 파일 옆 `review-results/` · 카드 id 는 파일 이름에 안전한 글자만. */
@@ -88,6 +95,8 @@ export function pendingReviewResults(card: TaskCard): ReviewRequest[] {
88
95
  export interface CaptureReviewResultsOptions {
89
96
  /** 이 카드들만 본다 — 없으면 상태 파일의 모든 카드. */
90
97
  cardIds?: readonly string[];
98
+ /** A detached completion callback may only harvest its own result file. */
99
+ resultPaths?: readonly string[];
91
100
  now?: () => Date;
92
101
  /** 시험 seam — 결과 파일 읽기(없으면 ENOENT 를 던진다). */
93
102
  readFile?: (path: string) => string;
@@ -123,6 +132,7 @@ export function captureReviewResults(statePath: string, opts: CaptureReviewResul
123
132
  if (!card) continue;
124
133
  for (const request of pendingReviewResults(card)) {
125
134
  const resultPath = request.resultPath!;
135
+ if (opts.resultPaths && !opts.resultPaths.includes(resultPath)) continue;
126
136
  let text: string | null = null;
127
137
  let readError: string | null = null;
128
138
  let unreadable = false;
@@ -184,6 +194,59 @@ export function captureReviewResults(statePath: string, opts: CaptureReviewResul
184
194
  return out;
185
195
  }
186
196
 
197
+ /** Run after the detached review child has fully written its JSON stdout. Never infer a card from PR alone. */
198
+ export async function rejudgeCompletedTaskReview(
199
+ env: Record<string, string | undefined> = process.env,
200
+ rejudge?: (cardId: string, statePath: string) => Promise<unknown>,
201
+ rejudgeDeps?: RejudgeDeps,
202
+ ): Promise<boolean> {
203
+ const cardId = env[TA_REVIEW_COMPLETION_CARD_ENV];
204
+ const statePath = env[TA_REVIEW_COMPLETION_STATE_ENV];
205
+ const resultPath = env[TA_REVIEW_COMPLETION_RESULT_ENV];
206
+ if (!cardId || !statePath || !resultPath) return false;
207
+ const before = readTaskCard(cardId, statePath);
208
+ const bound = before?.reviewRequests?.find((item) => item.resultPath === resultPath);
209
+ if (before?.status !== 'launched' || before.taLand !== true || !bound || cardPrNumber(before) !== bound.pr
210
+ || !SHA40.test(bound.head) || (before.history ?? []).some((item) => item.event === 'async-result-rejudge' && item.resultPath === resultPath)) return false;
211
+ captureReviewResults(statePath, { cardIds: [cardId], resultPaths: [resultPath] });
212
+ const key = `${statePath}\0${cardId}\0${resultPath}`;
213
+ if (reviewRejudgeInFlight.has(key)) return false;
214
+ const card = readTaskCard(cardId, statePath);
215
+ const request = card?.reviewRequests?.find((item) => item.resultPath === resultPath);
216
+ if (card?.status !== 'launched' || card.taLand !== true || !request || cardPrNumber(card) !== request.pr
217
+ || !(card.history ?? []).some((item) => item.event === LIVE_MOVE_RESULT_EVENT && item.resultPath === resultPath && item.kind === 'review'
218
+ && item.pr === request.pr && SHA40.test(request.head) && SHA40.test(item.head ?? '')
219
+ && item.effect?.startsWith('review result captured') && item.reviewed === true && item.head?.toLowerCase() === request.head.toLowerCase())
220
+ || (card.history ?? []).some((item) => item.event === 'async-result-rejudge' && item.resultPath === resultPath)) return false;
221
+ reviewRejudgeInFlight.add(key);
222
+ try {
223
+ const run = async (): Promise<unknown> => {
224
+ if (rejudgeDeps) {
225
+ const { rejudgeShadowCard } = await import('./rejudge-on-head.js');
226
+ return rejudgeShadowCard(cardId, statePath, rejudgeDeps, { pr: request.pr, head: request.head, kind: 'review', resultPath });
227
+ } else if (rejudge) return rejudge(cardId, statePath);
228
+ else {
229
+ const { rejudgeAsyncResultCard } = await import('./rejudge-on-head.js');
230
+ return rejudgeAsyncResultCard(cardId, statePath, {}, { pr: request.pr, head: request.head, kind: 'review', resultPath });
231
+ }
232
+ };
233
+ const { requireCompletedHeadRejudge } = await import('./rejudge-on-head.js');
234
+ try { requireCompletedHeadRejudge(await run(), request.pr, request.head); }
235
+ catch { requireCompletedHeadRejudge(await run(), request.pr, request.head); } // Bounded retry; no marker on failed outcomes.
236
+ updateTaskCard(statePath, cardId, (current) => current?.status === 'launched' && current.taLand === true && cardPrNumber(current) === request.pr
237
+ && (current.reviewRequests ?? []).some((item) => item.pr === request.pr && SHA40.test(item.head) && item.head.toLowerCase() === request.head.toLowerCase() && item.resultPath === resultPath)
238
+ && (current.history ?? []).some((item) => item.event === LIVE_MOVE_RESULT_EVENT && item.resultPath === resultPath && item.kind === 'review'
239
+ && item.pr === request.pr && SHA40.test(item.head ?? '') && item.head?.toLowerCase() === request.head.toLowerCase()
240
+ && item.reviewed === true && item.effect?.startsWith('review result captured'))
241
+ && !(current.history ?? []).some((item) => item.event === 'async-result-rejudge' && item.resultPath === resultPath)
242
+ ? { ...current, history: [...(current.history ?? []), { at: new Date().toISOString(), event: 'async-result-rejudge', kind: 'review', pr: request.pr, head: request.head, resultPath, effect: 'completion callback succeeded' }] }
243
+ : undefined);
244
+ } finally {
245
+ reviewRejudgeInFlight.delete(key);
246
+ }
247
+ return true;
248
+ }
249
+
187
250
  /**
188
251
  * 다음 수 판단의 입력 — 카드에 회수된 리뷰 결과 중 «그 머리(`head`)»의 «가장 최근» 결과가 실제로 돈(reviewed) pass 이고
189
252
  * 리뷰한 머리가 40자 sha 일 때만 `selfReview` 모양으로 돌려준다(슈퍼바이저가 나르는 `self-implement-cli` 의 규칙과 같다 ·