elanous 0.2.19 → 0.2.20

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 (388) 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/{J5H7BvqgSI8DUjtRRcA_S → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 1268.0840ff91e083fcbd.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/1307-77fad80cf54477c6.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +5 -0
  11. package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{2977-e38442e86289f1d3.js → 211-9c9090c59e2c5907.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/{4847.56f5bc454fbcc827.js → 6110.1297d4093fdfbb0d.js} +2 -2
  35. package/apps/pwa/out/_next/static/chunks/6158.af2e93b7adff709b.js +2 -0
  36. package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/{1470.98a6ab61ccb4131c.js → 726.83532d69452ee24b.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 7327-2d69fb321ba73389.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/{8870-9614a213b990b10b.js → 7748-6e51bfe927019f5c.js} +3 -3
  41. package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/{7647.2fcd30d894d5ee30.js → 935.4ddaa99fc1d1126c.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/{ae787e4c-e5b1ef00b2dbceca.js → a9f2b54d-33c0689b70ed8cf3.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/approvals/page-c855c24de7db74fe.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-85b316bb4235980b.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/board/page-9b2b26a33cb2cd42.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/botlab/page-c9955fde67b1bcb5.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/bots/page-7a852956c74015a3.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/chat/page-4ba4abe096b6c2a8.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/control/page-f5d56ff5f45ad164.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/design-check/page-7c058069c5e7fa63.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/exec/page-466bf0ee16ad1432.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/field/page-a23bc71713bcc08f.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/inside/{page-5b29d9b06ceadade.js → page-ed4a6f799f005d09.js} +1 -1
  69. package/apps/pwa/out/_next/static/chunks/app/intake/page-9cd565005b93aca6.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/live/page-474cb5888ea0e8f3.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/market/page-9ca21f9ad3208a61.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-f9ccb1b1adbea14d.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/morning/page-ec102c9b8b691e37.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/not-found-a9c2cfa8dda922a5.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/reflection/page-db03ae3589a81c9e.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/sessions/page-75bffecc9aa1004e.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/settings/page-31d5b875760bb62d.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/setup/page-68793c6e10331d66.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/share/page-46d5201d2207a511.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/showroom/page-d81c07a7b4f618ae.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/tasks/page-be1c4e8697f22360.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/app/trace/page-0fd895fab9b62c1b.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/app/workspace/page-34882c84f84021c5.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → f6231284-45726550aba08fca.js} +1 -1
  103. package/apps/pwa/out/_next/static/chunks/{2ea96958.e8c7e112de50b067.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
  104. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
  105. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
  106. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
  107. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
  108. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
  109. package/apps/pwa/out/_next/static/chunks/webpack-c260571513bf43c2.js +1 -0
  110. package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
  111. package/apps/pwa/out/approvals/index.html +1 -1
  112. package/apps/pwa/out/approvals/index.txt +23 -23
  113. package/apps/pwa/out/autopilot/index.html +1 -1
  114. package/apps/pwa/out/autopilot/index.txt +23 -23
  115. package/apps/pwa/out/board/index.html +1 -1
  116. package/apps/pwa/out/board/index.txt +23 -23
  117. package/apps/pwa/out/botlab/index.html +1 -1
  118. package/apps/pwa/out/botlab/index.txt +22 -22
  119. package/apps/pwa/out/bots/index.html +1 -1
  120. package/apps/pwa/out/bots/index.txt +22 -22
  121. package/apps/pwa/out/ceo/index.html +1 -1
  122. package/apps/pwa/out/ceo/index.txt +23 -23
  123. package/apps/pwa/out/chat/index.html +1 -1
  124. package/apps/pwa/out/chat/index.txt +22 -22
  125. package/apps/pwa/out/consult/index.html +1 -1
  126. package/apps/pwa/out/consult/index.txt +23 -23
  127. package/apps/pwa/out/control/index.html +1 -1
  128. package/apps/pwa/out/control/index.txt +23 -23
  129. package/apps/pwa/out/decisions/index.html +1 -1
  130. package/apps/pwa/out/decisions/index.txt +22 -22
  131. package/apps/pwa/out/design-check/index.html +1 -1
  132. package/apps/pwa/out/design-check/index.txt +23 -23
  133. package/apps/pwa/out/editor/index.html +1 -1
  134. package/apps/pwa/out/editor/index.txt +23 -23
  135. package/apps/pwa/out/exec/index.html +1 -1
  136. package/apps/pwa/out/exec/index.txt +23 -23
  137. package/apps/pwa/out/field/index.html +1 -1
  138. package/apps/pwa/out/field/index.txt +23 -23
  139. package/apps/pwa/out/healthz +1 -1
  140. package/apps/pwa/out/index.html +1 -1
  141. package/apps/pwa/out/index.txt +22 -22
  142. package/apps/pwa/out/inside/index.html +1 -1
  143. package/apps/pwa/out/inside/index.txt +22 -22
  144. package/apps/pwa/out/intake/index.html +1 -1
  145. package/apps/pwa/out/intake/index.txt +23 -23
  146. package/apps/pwa/out/live/index.html +1 -1
  147. package/apps/pwa/out/live/index.txt +23 -23
  148. package/apps/pwa/out/loops/index.html +1 -1
  149. package/apps/pwa/out/loops/index.txt +22 -22
  150. package/apps/pwa/out/market/index.html +1 -1
  151. package/apps/pwa/out/market/index.txt +22 -22
  152. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  153. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  154. package/apps/pwa/out/morning/index.html +1 -1
  155. package/apps/pwa/out/morning/index.txt +23 -23
  156. package/apps/pwa/out/observatory/index.html +1 -1
  157. package/apps/pwa/out/observatory/index.txt +23 -23
  158. package/apps/pwa/out/ops/checklist/index.html +1 -1
  159. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  160. package/apps/pwa/out/ops/release/index.html +1 -1
  161. package/apps/pwa/out/ops/release/index.txt +22 -22
  162. package/apps/pwa/out/ops/seats/index.html +1 -1
  163. package/apps/pwa/out/ops/seats/index.txt +22 -22
  164. package/apps/pwa/out/outputs/index.html +1 -1
  165. package/apps/pwa/out/outputs/index.txt +22 -22
  166. package/apps/pwa/out/reflection/index.html +1 -1
  167. package/apps/pwa/out/reflection/index.txt +23 -23
  168. package/apps/pwa/out/scheduler/index.html +1 -1
  169. package/apps/pwa/out/scheduler/index.txt +23 -23
  170. package/apps/pwa/out/sessions/index.html +1 -1
  171. package/apps/pwa/out/sessions/index.txt +22 -22
  172. package/apps/pwa/out/settings/devices/index.html +1 -1
  173. package/apps/pwa/out/settings/devices/index.txt +23 -23
  174. package/apps/pwa/out/settings/index.html +1 -1
  175. package/apps/pwa/out/settings/index.txt +23 -23
  176. package/apps/pwa/out/setup/done/index.html +1 -1
  177. package/apps/pwa/out/setup/done/index.txt +23 -23
  178. package/apps/pwa/out/setup/index.html +1 -1
  179. package/apps/pwa/out/setup/index.txt +23 -23
  180. package/apps/pwa/out/share/index.html +1 -1
  181. package/apps/pwa/out/share/index.txt +23 -23
  182. package/apps/pwa/out/showroom/index.html +1 -1
  183. package/apps/pwa/out/showroom/index.txt +22 -22
  184. package/apps/pwa/out/tasks/index.html +1 -1
  185. package/apps/pwa/out/tasks/index.txt +23 -23
  186. package/apps/pwa/out/term/index.html +1 -1
  187. package/apps/pwa/out/term/index.txt +23 -23
  188. package/apps/pwa/out/today/index.html +1 -1
  189. package/apps/pwa/out/today/index.txt +23 -23
  190. package/apps/pwa/out/trace/index.html +1 -1
  191. package/apps/pwa/out/trace/index.txt +23 -23
  192. package/apps/pwa/out/vault/index.html +1 -1
  193. package/apps/pwa/out/vault/index.txt +23 -23
  194. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  195. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  196. package/apps/pwa/out/workflows/index.html +1 -1
  197. package/apps/pwa/out/workflows/index.txt +23 -23
  198. package/apps/pwa/out/workspace/index.html +1 -1
  199. package/apps/pwa/out/workspace/index.txt +23 -23
  200. package/apps/pwa/out/worktrees/index.html +1 -1
  201. package/apps/pwa/out/worktrees/index.txt +23 -23
  202. package/graphs/demo/editor-recipes.yaml +22 -0
  203. package/graphs/retro/daily.yaml +25 -0
  204. package/graphs/retro/recipes.yaml +24 -0
  205. package/package.json +1 -1
  206. package/scripts/ask-marker-check.ts +3 -1
  207. package/scripts/land-global-checks.ts +89 -0
  208. package/scripts/lib/pch-regress-map.ts +2 -1
  209. package/scripts/release-loop/gate-node.ts +296 -25
  210. package/scripts/release-loop/gate-timing.ts +116 -7
  211. package/scripts/release-loop/preflight.ts +6 -4
  212. package/scripts/release-loop/publish-window.ts +141 -0
  213. package/scripts/release-loop/release-docs.ts +1 -4
  214. package/scripts/release-loop/release-universe.ts +38 -0
  215. package/scripts/release-loop/resume-release.ts +4 -1
  216. package/scripts/release-loop/unattended-release.ts +10 -2
  217. package/scripts/release-story/draft.ts +5 -1
  218. package/scripts/retro/shadow-compare.ts +71 -0
  219. package/scripts/rhythm/daily.ts +6 -1
  220. package/src/agent-mission/driver.ts +34 -6
  221. package/src/autopilot/build/llm-conflict-merge.ts +132 -7
  222. package/src/autopilot/build/merge-intent.ts +135 -7
  223. package/src/away/release-watch.ts +20 -3
  224. package/src/cli/config-drift.ts +513 -0
  225. package/src/cli/log-event-names.ts +4 -0
  226. package/src/cli/pr-cli.ts +20 -0
  227. package/src/cli/release-cli.ts +20 -5
  228. package/src/cli/retro-cli.ts +78 -0
  229. package/src/cli/tasks-cli.ts +94 -8
  230. package/src/dashboard/slash-runtime/dashboard-handlers.ts +5 -4
  231. package/src/dashboard/slash-runtime/help-from-registry.ts +57 -31
  232. package/src/domains/outbound-log-probe.ts +6 -1
  233. package/src/graph-runner/graph-run-api.ts +205 -0
  234. package/src/graph-runner/runner.ts +11 -1
  235. package/src/harness/harness-cli-command.ts +419 -52
  236. package/src/harness/harness-queue.ts +315 -25
  237. package/src/harness/harness-salvage-cli.ts +62 -0
  238. package/src/hq/fence-outcomes.ts +63 -0
  239. package/src/hq/fence-wrapper.ts +33 -0
  240. package/src/hq/hq.ts +11 -1
  241. package/src/index.ts +69 -6
  242. package/src/maturity/feature-maturity.ts +2 -2
  243. package/src/nexus/api/graph-kinds.ts +10 -4
  244. package/src/nexus/api/graphs-api.ts +178 -6
  245. package/src/nexus/api/http-server.ts +38 -3
  246. package/src/nexus/api/loop-edges.ts +4 -2
  247. package/src/nexus/api/ops-api.ts +22 -7
  248. package/src/nexus/api/ops-board-route.ts +48 -0
  249. package/src/onboarding/entry-hints.ts +5 -0
  250. package/src/onboarding.ts +24 -6
  251. package/src/release-loop/feature-store.ts +14 -11
  252. package/src/release-loop/gate-shards.ts +158 -0
  253. package/src/release-loop/release-schedule.ts +6 -2
  254. package/src/retro/daily.ts +372 -0
  255. package/src/seat-loop/op-decide.ts +173 -0
  256. package/src/seat-loop/seat-loop.ts +60 -3
  257. package/src/seat-loop/seat-report.ts +58 -3
  258. package/src/self-dev/ask-launch-flow.ts +5 -4
  259. package/src/self-dev/ask-launch-io.ts +49 -1
  260. package/src/self-dev/ask-path-candidates.ts +40 -2
  261. package/src/self-dev/draft-sweep.ts +371 -16
  262. package/src/self-dev/draft-triage-rules.ts +19 -4
  263. package/src/self-dev/pr-preview.ts +84 -16
  264. package/src/self-implement/blocked-draft-policy.ts +101 -0
  265. package/src/self-implement/execution-clarification.ts +11 -0
  266. package/src/self-implement/gate-env-retry.ts +83 -0
  267. package/src/self-implement/orchestrator.ts +218 -5
  268. package/src/self-implement/run-stop.ts +123 -0
  269. package/src/self-implement/run-ttl.ts +258 -0
  270. package/src/self-implement/salvage-retention.ts +123 -0
  271. package/src/self-implement/seams.ts +4 -0
  272. package/src/self-implement/self-implement-cli.ts +2 -0
  273. package/src/self-implement/sibling-resync.ts +223 -0
  274. package/src/self-implement/stop-autoheal.ts +204 -0
  275. package/src/task-agent/board.ts +1 -1
  276. package/src/task-agent/card-evidence.ts +99 -10
  277. package/src/task-agent/judge.ts +7 -0
  278. package/src/task-agent/live-moves.ts +120 -20
  279. package/src/task-agent/mission.ts +1 -1
  280. package/src/task-agent/shadow.ts +10 -1
  281. package/src/task-agent/task-hand.ts +7 -2
  282. package/src/task-orchestrator/surfaces/pod-command-job.ts +19 -5
  283. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
  284. package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
  285. package/src/task-orchestrator/surfaces/pod-lease.ts +93 -10
  286. package/src/task-orchestrator/surfaces/pod-pool.ts +147 -34
  287. package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -3
  288. package/src/telegram-commands.ts +41 -0
  289. package/src/testing/real-cli-spawn.ts +106 -0
  290. package/src/user-config.ts +56 -3
  291. package/src/version/packed-revision.json +1 -1
  292. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/2375-e061bb24cd2b5cde.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
  300. package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +0 -2
  301. package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +0 -1
  302. package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +0 -1
  303. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
  304. package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +0 -1
  305. package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +0 -1
  306. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  307. package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +0 -1
  308. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  309. package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +0 -1
  310. package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +0 -1
  311. package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +0 -1
  312. package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +0 -1
  313. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  314. package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +0 -1
  387. package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +0 -1
  388. /package/apps/pwa/out/_next/static/{J5H7BvqgSI8DUjtRRcA_S → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
@@ -0,0 +1,204 @@
1
+ /**
2
+ * STOP-AUTOHEAL (0.2.20 P0 · 첫 조각) — 멈춘 런의 stop 레코드(STOP-RECORD #24907)를 읽고
3
+ * 갈래마다 «한 번»만 스스로 고쳐 본다. 안 되면 `needs-owner` 로 사람에게 넘긴다.
4
+ *
5
+ * S: 런이 멈추면 원장에 `event='stop'` 한 줄(class·cause·evidenceRef·nextMove)이 남는다.
6
+ * C: 그 줄을 읽는 쪽이 없어서 환경 결손·main 동기화·Pod 사망·증거 누락처럼 «기계가 고칠 수 있는» 멈춤도 사람을 기다렸다.
7
+ * Q: 갈래별로 한 수를 정해 두고 «한 번만» 시도할 수 있나?
8
+ * A: `autohealFromStop` 하나가 class → action 을 고르고, 원장 `event='autoheal'` 줄로 시도를 남긴다.
9
+ * 같은 런·같은 갈래·같은 모드에 이미 줄이 있으면 다시 하지 않는다(원장 가드).
10
+ *
11
+ * 사다리(class → action):
12
+ * env-unrelated → clean-workspace-regate 깨끗한 자리에서 gate 만 다시 1회(GATE-ENV-RETRY 재사용 자리)
13
+ * main-sync → rebase-regate 리베이스(MERGE-INTENT-RESOLVE 의도 해석) 뒤 재게이트
14
+ * pod-died → resume-from-salvage `salvage/run-<6hex>/<leaf>` 가지에서 resume(DRAFT-NOT-ARCHIVE)
15
+ * evidence-uncovered → derive-evidence-rejudge 런 출력·시험·배선에서 증거를 끌어내 1회 재판정
16
+ * review-repeat · review-out-of-scope · fabric · launch-failed · unclassified → needs-owner(새 라운드 없음)
17
+ *
18
+ * 모드 `tools.selfImplement.autoheal.mode` = `shadow`(기본: 할 일을 원장에만 적고 손대지 않는다) | `live`.
19
+ * ⛔ live 에서도 행동은 주입 seam 으로만 한다 — seam 이 없는 행동은 `needs-owner`(action-unwired)로 떨어진다.
20
+ * «안 이어진 손»을 「고쳤다」로 읽지 않게.
21
+ * 관측 = `debug.log('self-implement.autoheal', …)` ⊕ 원장 `event='autoheal'`.
22
+ */
23
+ import { debug } from '../debug/log.js';
24
+ import { appendRunLedgerEntry, loadRunLedger, type RunLedgerEntry, type RunLedgerWriter } from './run-ledger.js';
25
+ import type { RunStopClass, RunStopRecord } from './run-stop.js';
26
+
27
+ export const AUTOHEAL_EVENT = 'autoheal';
28
+ export type AutohealMode = 'shadow' | 'live';
29
+ export const DEFAULT_AUTOHEAL_MODE: AutohealMode = 'shadow';
30
+
31
+ export type AutohealAction =
32
+ | 'clean-workspace-regate'
33
+ | 'rebase-regate'
34
+ | 'resume-from-salvage'
35
+ | 'derive-evidence-rejudge'
36
+ | 'needs-owner';
37
+
38
+ /** 닫힌 목록 전체를 덮는다 — 새 class 가 생기면 여기서 타입 오류가 난다. */
39
+ export const AUTOHEAL_ACTION_BY_CLASS: Readonly<Record<RunStopClass, AutohealAction>> = {
40
+ 'env-unrelated': 'clean-workspace-regate',
41
+ 'main-sync': 'rebase-regate',
42
+ 'pod-died': 'resume-from-salvage',
43
+ 'evidence-uncovered': 'derive-evidence-rejudge',
44
+ 'review-repeat': 'needs-owner',
45
+ 'review-out-of-scope': 'needs-owner',
46
+ fabric: 'needs-owner',
47
+ 'launch-failed': 'needs-owner',
48
+ unclassified: 'needs-owner',
49
+ };
50
+
51
+ /** 설정 원값 → 모드. 모르는 값은 «좁은 쪽»(shadow). */
52
+ export function parseAutohealMode(raw: unknown): AutohealMode {
53
+ return raw === 'live' ? 'live' : 'shadow';
54
+ }
55
+
56
+ export interface AutohealContext {
57
+ runId: string;
58
+ goalId?: string;
59
+ record: RunStopRecord;
60
+ }
61
+
62
+ export interface AutohealActionResult {
63
+ ok: boolean;
64
+ /** 한 줄 — 무엇을 했나/왜 못 했나. */
65
+ detail?: string;
66
+ }
67
+
68
+ export type AutohealActionFn = (ctx: AutohealContext) => Promise<AutohealActionResult> | AutohealActionResult;
69
+
70
+ /** 행동 seam — needs-owner 를 뺀 넷. 없는 seam 은 live 에서도 실행하지 않는다. */
71
+ export type AutohealActions = Partial<Record<Exclude<AutohealAction, 'needs-owner'>, AutohealActionFn>>;
72
+
73
+ export type AutohealOutcome =
74
+ /** shadow — 할 일을 적기만 했다. */
75
+ | 'would-act'
76
+ /** live — 행동을 했고 성공했다. */
77
+ | 'healed'
78
+ /** 사람에게 넘긴다(갈래가 needs-owner 이거나 행동이 실패·미배선). */
79
+ | 'needs-owner'
80
+ /** 같은 런·갈래·모드에 이미 시도가 있다 — 아무것도 안 했다(원장에 새 줄도 안 쓴다). */
81
+ | 'already-attempted';
82
+
83
+ export interface AutohealResult {
84
+ runId: string;
85
+ class: RunStopClass;
86
+ action: AutohealAction;
87
+ mode: AutohealMode;
88
+ outcome: AutohealOutcome;
89
+ /** needs-owner 일 때 사람이 볼 이유(멈춘 원인 그대로 또는 행동 실패). */
90
+ reason?: string;
91
+ detail?: string;
92
+ entry?: RunLedgerEntry;
93
+ }
94
+
95
+ export interface AutohealDeps {
96
+ mode?: AutohealMode;
97
+ actions?: AutohealActions;
98
+ /** 가드용 원장 읽기 — 기본 `loadRunLedger(runId)`. null = 원장 없음(첫 시도). */
99
+ loadLedger?: (runId: string) => readonly RunLedgerEntry[] | null;
100
+ writeLedger?: RunLedgerWriter;
101
+ }
102
+
103
+ /**
104
+ * 이 프로세스가 쓴 autoheal 줄 — 원장 읽기가 주입 writer 와 다른 곳을 보거나(시험 seam) 비어 있어도
105
+ * 같은 프로세스 안에서는 «한 번»이 지켜지게 한다(리뷰 must-fix #24912).
106
+ */
107
+ const writtenThisProcess = new Map<string, RunLedgerEntry[]>();
108
+ const WRITTEN_MAX_RUNS = 4096;
109
+
110
+ /** 시험용 — 프로세스 기억을 비운다. */
111
+ export function resetAutohealProcessMemory(): void {
112
+ writtenThisProcess.clear();
113
+ }
114
+
115
+ /** 원장 가드 — 같은 런·같은 갈래·같은 모드의 autoheal 줄이 있으면 참. */
116
+ export function autohealAlreadyAttempted(ledger: readonly RunLedgerEntry[] | null, cls: RunStopClass, mode: AutohealMode): boolean {
117
+ if (!ledger) return false;
118
+ return ledger.some((e) => e.event === AUTOHEAL_EVENT && e.data?.class === cls && e.data?.mode === mode);
119
+ }
120
+
121
+ function observe(event: string, data: Record<string, unknown>): void {
122
+ try { debug.log('self-implement.autoheal', event, data); } catch { /* observation is fail-soft */ }
123
+ }
124
+
125
+ /**
126
+ * stop 레코드 하나 → 최대 한 번의 힐 시도. 예외를 던지지 않는다(행동 seam 의 throw 도 needs-owner 로 접는다).
127
+ */
128
+ export async function autohealFromStop(ctx: AutohealContext, deps: AutohealDeps = {}): Promise<AutohealResult> {
129
+ const mode = deps.mode ?? DEFAULT_AUTOHEAL_MODE;
130
+ const cls = ctx.record.class;
131
+ const action = AUTOHEAL_ACTION_BY_CLASS[cls] ?? 'needs-owner';
132
+ const base = { runId: ctx.runId, class: cls, action, mode };
133
+
134
+ let ledger: readonly RunLedgerEntry[] | null = null;
135
+ try {
136
+ ledger = (deps.loadLedger ?? ((id) => loadRunLedger(id)))(ctx.runId);
137
+ } catch (error) {
138
+ // ⛔ 가드를 못 읽으면 «한 번»을 보장할 수 없다 — live 행동을 하지 않고 사람에게 넘긴다.
139
+ const reason = `ledger-unreadable: ${error instanceof Error ? error.message : String(error)}`;
140
+ observe('guard-unreadable', { ...base, reason });
141
+ return { ...base, outcome: 'needs-owner', reason };
142
+ }
143
+ if (autohealAlreadyAttempted(ledger, cls, mode) || autohealAlreadyAttempted(writtenThisProcess.get(ctx.runId) ?? null, cls, mode)) {
144
+ observe('already-attempted', base);
145
+ return { ...base, outcome: 'already-attempted' };
146
+ }
147
+
148
+ let outcome: AutohealOutcome;
149
+ let reason: string | undefined;
150
+ let detail: string | undefined;
151
+ if (action === 'needs-owner') {
152
+ outcome = 'needs-owner';
153
+ reason = `stop ${cls}: ${ctx.record.cause}`;
154
+ } else if (mode === 'shadow') {
155
+ outcome = 'would-act';
156
+ } else {
157
+ const fn = deps.actions?.[action];
158
+ if (!fn) {
159
+ outcome = 'needs-owner';
160
+ reason = `action-unwired: ${action} (stop ${cls}: ${ctx.record.cause})`;
161
+ } else {
162
+ // ⭐ 시도 «전»에 원장 줄을 쓴다 — 행동 중 프로세스가 죽어도 두 번째 시도가 막힌다.
163
+ // ⛔ 그 줄을 못 쓰면 «한 번»을 보장할 수 없으니 행동하지 않는다.
164
+ if (!writeEntry(ctx, deps, { ...base, phase: 'attempt' })) {
165
+ const unguarded = `guard-write-failed: ${action} not attempted (stop ${cls}: ${ctx.record.cause})`;
166
+ observe('needs-owner', { ...base, reason: unguarded });
167
+ return { ...base, outcome: 'needs-owner', reason: unguarded };
168
+ }
169
+ try {
170
+ const res = await fn(ctx);
171
+ detail = res.detail;
172
+ outcome = res.ok ? 'healed' : 'needs-owner';
173
+ if (!res.ok) reason = `${action} failed${res.detail ? `: ${res.detail}` : ''}`;
174
+ } catch (error) {
175
+ outcome = 'needs-owner';
176
+ reason = `${action} threw: ${error instanceof Error ? error.message : String(error)}`;
177
+ }
178
+ }
179
+ }
180
+
181
+ const entry = writeEntry(ctx, deps, {
182
+ ...base, phase: 'result', outcome,
183
+ ...(reason ? { reason } : {}), ...(detail ? { detail } : {}),
184
+ evidenceRef: ctx.record.evidenceRef, nextMove: ctx.record.nextMove,
185
+ });
186
+ observe(outcome, { ...base, ...(reason ? { reason } : {}), ...(detail ? { detail } : {}) });
187
+ return { ...base, outcome, ...(reason ? { reason } : {}), ...(detail ? { detail } : {}), ...(entry ? { entry } : {}) };
188
+ }
189
+
190
+ function writeEntry(ctx: AutohealContext, deps: AutohealDeps, data: Record<string, unknown>): RunLedgerEntry | undefined {
191
+ const entry: RunLedgerEntry = {
192
+ timestamp: new Date().toISOString(), runId: ctx.runId, event: AUTOHEAL_EVENT, data,
193
+ ...(ctx.goalId ? { goalId: ctx.goalId } : {}),
194
+ };
195
+ try {
196
+ (deps.writeLedger ?? appendRunLedgerEntry)(entry);
197
+ if (!writtenThisProcess.has(ctx.runId) && writtenThisProcess.size >= WRITTEN_MAX_RUNS) writtenThisProcess.clear();
198
+ writtenThisProcess.set(ctx.runId, [...(writtenThisProcess.get(ctx.runId) ?? []), entry]);
199
+ return entry;
200
+ } catch (error) {
201
+ observe('ledger-write-failed', { runId: ctx.runId, error: error instanceof Error ? error.message : String(error) });
202
+ return undefined;
203
+ }
204
+ }
@@ -56,7 +56,7 @@ function aggregate(node: BoardNode): void {
56
56
  }
57
57
 
58
58
  function cardStatus(card: TaskCard): BoardStatus {
59
- if (card.status === 'launch-failed') return 'blocked';
59
+ if (card.status === 'launch-failed' || card.status === 'failed') return 'blocked';
60
60
  return card.greenProposal ? 'done' : 'open';
61
61
  }
62
62
 
@@ -7,9 +7,15 @@
7
7
  * - 관측: `debug.log('task-agent', 'card-pr-bound', {card, runId, pr})`.
8
8
  */
9
9
  import { existsSync } from 'node:fs';
10
+ import { STOP_CLASS_POD_FAILURE, STOP_CLASS_NO_LAUNCH } from '../task-orchestrator/surfaces/pod-failure-reason.js';
11
+ import { readLaunchBinding } from '../harness/launch-registry.js';
12
+ import { readRunExits, type RunExit } from '../harness/harness-incidents.js';
13
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
10
14
  import { debug } from '../debug/log.js';
15
+ import { LogStore, logsDbPath } from '../mss/logging/log-store.js';
11
16
  import type { RunLedgerEntry } from '../self-implement/run-ledger.js';
12
17
  import { cardPrNumber, taskAgentStatePath, updateTaskCard, readTaskCard, type TaskCard } from './task-hand.js';
18
+ import { judgeNextMove } from './judge.js';
13
19
 
14
20
  const GOAL_ID = /^[a-f0-9]{16}$/;
15
21
  const PR_URL = /^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/(\d+)$/;
@@ -19,7 +25,7 @@ const RUN_ID = /^run-[A-Za-z0-9_-]{4,64}$/;
19
25
  export interface CardRunSnapshot {
20
26
  host: readonly RunLedgerEntry[] | null;
21
27
  children: Readonly<Record<string, readonly RunLedgerEntry[] | null>>;
22
- podFinishes?: ReadonlyArray<{ childRunId: string; prUrl: string }>;
28
+ podFinishes?: ReadonlyArray<{ childRunId: string; prUrl: string; state?: string; childError?: string }>;
23
29
  /** 읽기 «실패»(원장·logs) — 부재(null)와 갈린다. 있으면 refresh 가 card-bind-failed 를 남긴다. */
24
30
  readErrors?: readonly string[];
25
31
  }
@@ -92,11 +98,39 @@ export function bindCardEvidence(card: Pick<TaskCard, 'runId'>, snapshot: CardRu
92
98
  return { ledgerFound: true, ...(goalId ? { goalId } : {}), ...(pr ? { pr } : {}), ...(childRunId ? { childRunId } : {}) };
93
99
  }
94
100
 
101
+ /** DRAFT-NOT-ARCHIVE — 원장의 마지막 `salvaged`(self-implement.draft-not-archive) 줄이 남긴 수확 가지. 없으면 undefined. */
102
+ export function salvageBranchOf(entries: readonly RunLedgerEntry[] | null | undefined): string | undefined {
103
+ return lastSalvaged(entries)?.branch;
104
+ }
105
+
106
+ function lastSalvaged(entries: readonly RunLedgerEntry[] | null | undefined): { branch: string; at: number; order: number } | undefined {
107
+ for (let at = (entries?.length ?? 0) - 1; at >= 0; at--) {
108
+ const entry = entries![at]!;
109
+ const branch = entry.event === 'salvaged' ? entry.data?.salvageBranch : undefined;
110
+ if (typeof branch === 'string' && branch.startsWith('salvage/')) return { branch, at: Date.parse(entry.timestamp ?? ''), order: at };
111
+ }
112
+ return undefined;
113
+ }
114
+
115
+ /** 카드 런(호스트 → 자식 나중 것부터)의 수확 가지. 원장을 못 읽었으면 `readErrors` 로 갈린다(⛔ «없음» 이 아니다). */
116
+ export async function readCardSalvageBranch(card: Pick<TaskCard, 'runId' | 'createdAt'>, deps: CardRunEvidenceDeps = {}): Promise<{ salvageBranch: string | null; readErrors?: readonly string[] }> {
117
+ if (!card.runId) return { salvageBranch: null };
118
+ const snapshot = await readCardRunSnapshot(card, { ...deps, podFinishes: () => [] });
119
+ // 호스트 ⊕ Pod 자식 원장 전체에서 «가장 나중»(타임스탬프) 수확 가지. 시각이 같거나 못 읽으면 호스트가 이긴다.
120
+ const candidates = [lastSalvaged(snapshot.host), ...podChildRunIds(snapshot.host, card.runId).map((id) => lastSalvaged(snapshot.children[id]))]
121
+ .filter((row): row is NonNullable<typeof row> => row !== undefined);
122
+ const latest = candidates.reduce<typeof candidates[number] | undefined>((best, row) => !best || (Number.isFinite(row.at) && (!Number.isFinite(best.at) || row.at > best.at)) ? row : best, undefined);
123
+ return { salvageBranch: latest?.branch ?? null, ...(snapshot.readErrors?.length ? { readErrors: snapshot.readErrors } : {}) };
124
+ }
125
+
95
126
  /** 원장·logs 읽기(시험 주입). */
96
127
  export interface CardRunEvidenceDeps {
97
128
  loadLedger?: (runId: string) => RunLedgerEntry[] | null;
98
- podFinishes?: (childRunIds: readonly string[], sinceIso: string) => Array<{ childRunId: string; prUrl: string }>;
129
+ podFinishes?: (childRunIds: readonly string[], sinceIso: string) => Array<{ childRunId: string; prUrl: string; state?: string; childError?: string }>;
99
130
  now?: () => Date;
131
+ launchBound?: (runId: string) => boolean;
132
+ dispatchPod?: (runId: string, sinceIso: string) => boolean;
133
+ runExit?: (runId: string) => RunExit | undefined;
100
134
  }
101
135
 
102
136
  async function defaultLoadLedger(): Promise<(runId: string) => RunLedgerEntry[] | null> {
@@ -120,20 +154,30 @@ export function ledgerLoader(dirs: readonly string[], load: (runId: string, dir:
120
154
  };
121
155
  }
122
156
 
157
+ function defaultDispatchPod(runId: string, sinceIso: string): boolean {
158
+ const path = logsDbPath();
159
+ if (!existsSync(path)) return false;
160
+ const store = LogStore.openReadOnly(path);
161
+ try {
162
+ const sinceMs = Date.parse(sinceIso);
163
+ return store.queryAll({ exactCategories: ['harness.substrate'], events: ['dispatch-pod'], ...(Number.isFinite(sinceMs) ? { sinceMs } : {}) })
164
+ .some((row) => { try { return (JSON.parse(row.data ?? '{}') as { runId?: string }).runId === runId; } catch { return false; } });
165
+ } finally { store.close(); }
166
+ }
167
+
123
168
  async function defaultPodFinishes(): Promise<NonNullable<CardRunEvidenceDeps['podFinishes']>> {
124
- const { LogStore, logsDbPath } = await import('../mss/logging/log-store.js');
125
169
  return (childRunIds, sinceIso) => {
126
170
  const path = logsDbPath();
127
171
  if (!childRunIds.length || !existsSync(path)) return [];
128
172
  const store = LogStore.openReadOnly(path);
129
173
  try {
130
174
  const wanted = new Set(childRunIds);
131
- const rows: Array<{ childRunId: string; prUrl: string }> = [];
175
+ const rows: Array<{ childRunId: string; prUrl: string; state?: string; childError?: string }> = [];
132
176
  const sinceMs = Date.parse(sinceIso);
133
177
  for (const row of store.queryAll({ exactCategories: ['self-implement.pod'], events: ['job-finished'], ...(Number.isFinite(sinceMs) ? { sinceMs } : {}) })) {
134
- let data: { childRunId?: unknown; prUrl?: unknown } | null = null;
178
+ let data: { childRunId?: unknown; prUrl?: unknown; state?: unknown; childError?: unknown } | null = null;
135
179
  try { data = row.data ? JSON.parse(row.data) : null; } catch { continue; }
136
- if (typeof data?.childRunId === 'string' && wanted.has(data.childRunId) && typeof data.prUrl === 'string') rows.push({ childRunId: data.childRunId, prUrl: data.prUrl });
180
+ if (typeof data?.childRunId === 'string' && wanted.has(data.childRunId)) rows.push({ childRunId: data.childRunId, prUrl: typeof data.prUrl === 'string' ? data.prUrl : '', ...(typeof data.state === 'string' ? { state: data.state } : {}), ...(typeof data.childError === 'string' ? { childError: data.childError } : {}) });
137
181
  }
138
182
  return rows;
139
183
  } finally { store.close(); }
@@ -150,10 +194,9 @@ export async function readCardRunSnapshot(card: Pick<TaskCard, 'runId' | 'create
150
194
  const host = safe(card.runId);
151
195
  const childIds = podChildRunIds(host, card.runId);
152
196
  const children = Object.fromEntries(childIds.map((id) => [id, safe(id)]));
153
- let podFinishes: Array<{ childRunId: string; prUrl: string }> = [];
154
- const missing = childIds.filter((id) => !children[id] || !lastPr(children[id]!));
155
- if (missing.length && (!host || !lastPr(host))) {
156
- try { podFinishes = (deps.podFinishes ?? await defaultPodFinishes())(missing, card.createdAt); } catch (error) { readErrors.push(`logs: ${reason(error)}`); }
197
+ let podFinishes: NonNullable<CardRunSnapshot['podFinishes']> = [];
198
+ if (childIds.length && (!host || !lastPr(host))) {
199
+ try { podFinishes = (deps.podFinishes ?? await defaultPodFinishes())(childIds, card.createdAt); } catch (error) { readErrors.push(`logs: ${reason(error)}`); }
157
200
  }
158
201
  return { host, children, podFinishes, ...(readErrors.length ? { readErrors } : {}) };
159
202
  }
@@ -171,6 +214,52 @@ export async function refreshCardRunBinding(cardId: string, statePath = taskAgen
171
214
  try { debug.log('task-agent', 'card-bind-failed', { card: cardId, runId: card.runId, errors: snapshot.readErrors }); } catch { /* fail-soft */ }
172
215
  }
173
216
  const evidence = bindCardEvidence(card, snapshot);
217
+ if (card.status === 'launched' && !card.pr && !evidence.pr && !snapshot.readErrors?.length) {
218
+ const rows = [...(snapshot.host ?? []), ...Object.values(snapshot.children).flatMap((entries) => entries ?? [])];
219
+ let exit: RunExit | undefined;
220
+ let exitReadFailed = false;
221
+ try { exit = (deps.runExit ?? ((id) => readRunExits(effectiveInstanceRoot()).find((row) => row.runId === id)))(card.runId); }
222
+ catch { exitReadFailed = true; }
223
+ const finished = [...(snapshot.podFinishes ?? [])].reverse().find((item) => item.state === 'failed');
224
+ const parentTerminal = [...(snapshot.host ?? [])].reverse().find((item) => item.event === 'run-status');
225
+ const parentFailed = parentTerminal?.data?.runStatus === 'failed';
226
+ const boundAt = card.history.find((item) => item.event === 'run-bound')?.at;
227
+ const elapsed = boundAt ? (deps.now ?? (() => new Date()))().getTime() - Date.parse(boundAt) : NaN;
228
+ const launched = rows.some((entry) => entry.event === 'pod-child-run');
229
+ let dispatched = false;
230
+ let dispatchReadFailed = false;
231
+ try { dispatched = (deps.dispatchPod ?? defaultDispatchPod)(card.runId, boundAt ?? card.createdAt); }
232
+ catch { dispatchReadFailed = true; }
233
+ let launchBound = false;
234
+ let launchReadFailed = false;
235
+ try { launchBound = (deps.launchBound ?? ((id) => !!readLaunchBinding(id)))(card.runId); }
236
+ catch { launchReadFailed = true; }
237
+ const timedOut = !finished && !launched && !dispatched && !dispatchReadFailed && !exit && !exitReadFailed && !launchBound && !launchReadFailed && Number.isFinite(elapsed) && elapsed >= 600_000;
238
+ const hasPr = !!card.pr || !!evidence.pr;
239
+ const exitHarvestBranch = exit?.lastLines?.some((line) => /(?:수확할 브랜치: |ELANOUS_POD_SALVAGE )(?:salvage\/|self-impl\/)[^\s]+/.test(line)) ?? false;
240
+ const ledgerFailure = rows.find((entry) => entry.event === 'run-status' && entry.data?.runStatus === 'failed'
241
+ && (entry.data?.failureKind === STOP_CLASS_POD_FAILURE || entry.data?.stop_class === STOP_CLASS_POD_FAILURE));
242
+ const resultFailure = rows.find((entry) => entry.event === 'result-without-error' && entry.data?.ok === false);
243
+ const noLaunchExit = exit?.reason === STOP_CLASS_NO_LAUNCH && exit.status !== null && exit.status !== 0 && !launched && !dispatched && !dispatchReadFailed;
244
+ // 발사 근거가 있는 런이 nonzero 로 끝났고 분류기가 pod-failure 나 unknown(원인 미상)을 냈다 = 죽은 런(TA-JUDGE-DEAD-RUN ③ «exit 1 · unknown/pod-failure»).
245
+ const deadExit = (exit?.reason === STOP_CLASS_POD_FAILURE || exit?.reason === 'unknown') && exit.status !== null && exit.status !== 0;
246
+ const podFailure = !noLaunchExit && (launched || dispatched) && (parentFailed && (finished || !!ledgerFailure || !!resultFailure) || deadExit)
247
+ && (finished || ledgerFailure || resultFailure || deadExit);
248
+ const failure = podFailure ? judgeNextMove({ terminalRun: {
249
+ kind: STOP_CLASS_POD_FAILURE,
250
+ reason: finished?.childError || (exit?.status != null ? `exit ${exit.status} · ${exit.reason || STOP_CLASS_POD_FAILURE}` : resultFailure ? 'result-without-error · failed' : finished ? `job-finished failed · ${STOP_CLASS_POD_FAILURE}` : `run-status failed · ${STOP_CLASS_POD_FAILURE}`),
251
+ hasPr,
252
+ hasHarvestBranch: exitHarvestBranch || rows.some((entry) => typeof entry.data?.harvestBranch === 'string' && !!entry.data.harvestBranch) || !!salvageBranchOf(rows),
253
+ } }) : noLaunchExit ? judgeNextMove({ terminalRun: { kind: STOP_CLASS_NO_LAUNCH, reason: `exit ${exit!.status} · ${exit!.reason}`, hasPr, hasHarvestBranch: false } })
254
+ : timedOut ? judgeNextMove({ terminalRun: { kind: STOP_CLASS_NO_LAUNCH, reason: 'card-run-bound 뒤 10분 동안 Pod 발사 근거 없음', hasPr: false, hasHarvestBranch: false } }) : undefined;
255
+ if (failure?.reason.startsWith('failed/')) {
256
+ const at = (deps.now ?? (() => new Date()))().toISOString();
257
+ const next = updateTaskCard(statePath, cardId, (current) => current?.status === 'launched' && current.runId === card.runId && !current.pr && current.refSource !== 'manual' ? {
258
+ ...current, status: 'failed', history: [...current.history, { at, event: 'failed', detail: failure.reason, runId: card.runId }],
259
+ } : undefined);
260
+ return { card: next, evidence };
261
+ }
262
+ }
174
263
  // 이미 묶인 PR 이 있으면 «같은» PR(번호 ⊕ 낸 자식)을 다시 찾았을 때만 뒤늦은 골을 채운다.
175
264
  const samePr = (current: TaskCard) => current.pr === undefined
176
265
  || (evidence.pr !== undefined && cardPrNumber(current) === evidence.pr.number && current.runChildId === evidence.childRunId);
@@ -4,6 +4,7 @@
4
4
  * RFC-task-agent-any-task-to-completion-2026-10-06 §A2 표를 순수 함수로 옮긴다.
5
5
  * 부작용이 없다(발사·리뷰·착지는 실행부 `actions.ts` 의 몫). 수확 원천 = run-c9ad6d6d `chooseTaskAction`.
6
6
  */
7
+ import { STOP_CLASS_POD_FAILURE, STOP_CLASS_NO_LAUNCH } from '../task-orchestrator/surfaces/pod-failure-reason.js';
7
8
  import type { SupervisorStopReason } from '../self-dev/run-supervisor.js';
8
9
  import type { NextActionKind, RetryVariant } from './actions.js';
9
10
  import type { CompletionKind } from './task-hand.js';
@@ -33,6 +34,8 @@ export interface TaskJudgeInput {
33
34
  completion?: CompletionKind;
34
35
  /** code-pr 밖 종류의 확인 증거(`completion-evidence.ts` 판독). 비면 «아직 못 받았다» — 통과로 읽지 않는다. */
35
36
  evidence?: { ok: boolean; ref: string };
37
+ /** Only classified terminal evidence; an absent input preserves the existing stop-reason table. */
38
+ terminalRun?: { kind: typeof STOP_CLASS_POD_FAILURE | typeof STOP_CLASS_NO_LAUNCH; reason: string; hasPr: boolean; hasHarvestBranch: boolean };
36
39
  }
37
40
 
38
41
  export interface TaskJudgement {
@@ -73,6 +76,10 @@ function base(input: TaskJudgeInput): Omit<TaskJudgement, 'executorKind'> {
73
76
  if (input.deliveryEvidence) return input.deliveryEvidence.ok
74
77
  ? { move: 'propose-green', stage: 'closing', reason: `전달 근거 확인: ${input.deliveryEvidence.files.join(', ')}` }
75
78
  : { move: 'wait', stage: 'waiting', reason: input.deliveryEvidence.reason };
79
+ if (input.terminalRun && !input.terminalRun.hasPr && !input.terminalRun.hasHarvestBranch) {
80
+ const disposition = input.terminalRun.kind === STOP_CLASS_POD_FAILURE ? 'needs-owner' : 'needs-relaunch';
81
+ return { move: 'wait', stage: 'escalated', reason: `failed/${disposition} — ${input.terminalRun.reason.replace(/[\r\n]+/gu, ' ').trim() || input.terminalRun.kind}` };
82
+ }
76
83
  const stop = input.stopReason;
77
84
  const kind = input.completion;
78
85
  if (kind !== undefined && kind !== 'code-pr') {
@@ -8,8 +8,11 @@
8
8
  * - 관측: 실행한 수마다 `debug.log('task-agent', 'live-move', {kind, card, ok, detail, live:true})`.
9
9
  */
10
10
  import { spawn, spawnSync } from 'node:child_process';
11
- import { resolve } from 'node:path';
11
+ import { statSync } from 'node:fs';
12
+ import { isAbsolute, resolve } from 'node:path';
12
13
  import { debug } from '../debug/log.js';
14
+ import { findGitDir } from '../git-fs/locate.js';
15
+ import { getUserConfig } from '../user-config.js';
13
16
  import type { DetachedSpawn } from '../cli/tasks-cli.js';
14
17
  import { taskAgentStatePath, updateTaskCard, type TaskCard } from './task-hand.js';
15
18
 
@@ -53,22 +56,98 @@ export async function configuredTaskAgentLiveMoves(): Promise<ReadonlySet<TaskAg
53
56
  export interface LiveMoveDeps {
54
57
  statePath?: string;
55
58
  now?: () => Date;
56
- /** PR 머리 sha·상태 — 시험은 주입한다. 기본은 `gh pr view`(작업 트리 cwd). */
57
- prHead?: (pr: number, cwd?: string) => Promise<{ head: string; state: string } | null>;
59
+ /** PR 머리 sha·상태(·브랜치·URL) — 시험은 주입한다. 기본은 `gh pr view`(작업 트리 cwd). */
60
+ prHead?: (pr: number, cwd?: string) => Promise<PrHeadView | null>;
61
+ /** TA-LIVE-REVIEW-POD — 작업 트리가 없는 런(Pod)의 리뷰 저장소 후보. 기본은 카드 project.target → harness.defaultRepo → 호스트 checkout. */
62
+ repoCandidates?: (card: TaskCard) => readonly ReviewRepoCandidate[];
58
63
  /** 리뷰 요청(떼어 띄운다) — 시험은 반드시 주입한다. */
59
64
  requestReview?: (pr: number, intent: string, cwd?: string) => Promise<void>;
60
65
  log?: (category: string, event: string, data: Record<string, unknown>) => void;
61
66
  }
62
67
 
63
68
  /** `executed` = 부작용을 실제로 냈다(리뷰를 띄웠다 · 제안을 적었다). ok 이지만 이미 한 수(같은 머리·이미 제안)면 false. */
64
- export interface LiveMoveResult { kind: TaskAgentLiveMove; card: string | null; ok: boolean; executed: boolean; detail: string }
69
+ export interface LiveMoveResult {
70
+ kind: TaskAgentLiveMove; card: string | null; ok: boolean; executed: boolean; detail: string;
71
+ /** TA-LIVE-REVIEW-POD — 작업 트리 없이 리뷰 저장소를 찾았을 때(또는 못 찾았을 때)의 근거 한 줄. */
72
+ reviewRepo?: { resolved: boolean; source?: ReviewRepoCandidate['source']; cwd?: string; proof?: 'head-sha' | 'branch'; reason: string };
73
+ }
74
+
75
+ export interface PrHeadView { head: string; state: string; branch?: string; url?: string }
76
+
77
+ /** 리뷰 저장소 후보 — 과제를 넘길 때 카드가 안 대상 · 하니스 기본 저장소(harness.defaultRepo · harness.repo 의 로컬 뿌리) · 호스트 checkout. */
78
+ export interface ReviewRepoCandidate { source: 'card.project' | 'harness.defaultRepo' | 'host'; cwd: string }
79
+
80
+ /** 기본 후보 — 존재하는 디렉터리만 · 같은 경로는 한 번. 읽기 실패는 그 후보를 뺀다(없음 ≠ 통과). */
81
+ export function defaultReviewRepoCandidates(card: TaskCard, opts: { defaultRepo?: () => string | undefined; hostRoot?: () => string | undefined } = {}): ReviewRepoCandidate[] {
82
+ const raw: ReviewRepoCandidate[] = [];
83
+ if (card.project?.target && isAbsolute(card.project.target)) raw.push({ source: 'card.project', cwd: card.project.target });
84
+ try {
85
+ const configured = opts.defaultRepo ? opts.defaultRepo() : getUserConfig().harness?.defaultRepo;
86
+ if (configured && isAbsolute(configured)) raw.push({ source: 'harness.defaultRepo', cwd: configured });
87
+ } catch { /* 후보에서 뺀다 */ }
88
+ try {
89
+ const host = opts.hostRoot ? opts.hostRoot() : findGitDir(process.cwd())?.root;
90
+ if (host) raw.push({ source: 'host', cwd: host });
91
+ } catch { /* 후보에서 뺀다 */ }
92
+ const seen = new Set<string>();
93
+ const out: ReviewRepoCandidate[] = [];
94
+ for (const candidate of raw) {
95
+ if (!isAbsolute(candidate.cwd)) continue;
96
+ const cwd = resolve(candidate.cwd);
97
+ if (seen.has(cwd)) continue;
98
+ seen.add(cwd);
99
+ try { if (statSync(cwd).isDirectory()) out.push({ source: candidate.source, cwd }); } catch { /* 없는 디렉터리는 뺀다 */ }
100
+ }
101
+ return out;
102
+ }
103
+
104
+ /**
105
+ * TA-LIVE-REVIEW-POD — 작업 트리가 없을 때(Pod 런) 리뷰 cwd 를 정한다.
106
+ * 후보 저장소에서 `gh pr view <pr>` 가 «런이 낸 머리»(기록된 sha · 없으면 런의 브랜치)를 돌려줄 때만 그 저장소를 쓴다 —
107
+ * «다른 저장소에서 gh·self review 를 돌리지 않는다»(TA-JUDGE-LIVE-SAFE)는 계약을 지킨다. 못 정하면 이유 한 줄.
108
+ */
109
+ export async function resolveReviewRepo(
110
+ card: TaskCard, pr: number,
111
+ produced: { headCommit?: string; branch?: string; prUrl?: string } | undefined,
112
+ deps: LiveMoveDeps = {},
113
+ ): Promise<{ resolved: true; cwd: string; source: ReviewRepoCandidate['source']; proof: 'head-sha' | 'branch'; view: PrHeadView; reason: string } | { resolved: false; reason: string }> {
114
+ // 호출자가 런 근거를 «안 넘긴» 것과 런이 «기록하지 않은» 것은 다른 사실이다 — 섞어 적지 않는다.
115
+ if (produced === undefined) return { resolved: false, reason: 'caller passed no run evidence (produced) — head/branch not checked' };
116
+ // 기록된 머리가 «있으면» 그것만 근거다 — 모양이 틀린 sha 를 브랜치로 대체하지 않는다(확인 못 한 머리로 리뷰하지 않게).
117
+ if (produced?.headCommit !== undefined && !/^[0-9a-f]{40}$/i.test(produced.headCommit)) {
118
+ return { resolved: false, reason: `run head ${JSON.stringify(produced.headCommit.slice(0, 40))} is not a full commit sha` };
119
+ }
120
+ const expectedHead = produced?.headCommit?.toLowerCase();
121
+ const expectedBranch = produced?.branch?.trim() || undefined;
122
+ if (!expectedHead && !expectedBranch) return { resolved: false, reason: 'run result carries no PR head or branch to confirm against' };
123
+ let candidates: readonly ReviewRepoCandidate[];
124
+ try { candidates = (deps.repoCandidates ?? defaultReviewRepoCandidates)(card); } catch { candidates = []; }
125
+ if (!candidates.length) return { resolved: false, reason: 'no target repository (card project · harness.defaultRepo · host checkout)' };
126
+ const sameUrl = (a: string, b: string) => a.trim().replace(/\/+$/, '').toLowerCase() === b.trim().replace(/\/+$/, '').toLowerCase();
127
+ const misses: string[] = [];
128
+ for (const candidate of candidates) {
129
+ let view: PrHeadView | null;
130
+ try { view = await (deps.prHead ?? defaultPrHead)(pr, candidate.cwd); } catch { view = null; }
131
+ if (!view) { misses.push(`${candidate.source}: PR #${pr} unreadable`); continue; }
132
+ if (produced?.prUrl && view.url && !sameUrl(view.url, produced.prUrl)) { misses.push(`${candidate.source}: PR url ${view.url} ≠ run ${produced.prUrl}`); continue; }
133
+ if (expectedHead) {
134
+ if (view.head.toLowerCase() !== expectedHead) { misses.push(`${candidate.source}: head ${view.head.slice(0, 12)} ≠ run ${expectedHead.slice(0, 12)}`); continue; }
135
+ return { resolved: true, cwd: candidate.cwd, source: candidate.source, proof: 'head-sha', view, reason: `${candidate.source} PR #${pr} head matches run` };
136
+ }
137
+ if (view.branch !== expectedBranch) { misses.push(`${candidate.source}: branch ${view.branch ?? '?'} ≠ run ${expectedBranch}`); continue; }
138
+ // 브랜치 근거는 sha 보다 약하다 — 같은 PR(저장소 · 번호)임을 URL 로 «양쪽 다» 확인할 때만 쓴다.
139
+ if (!produced?.prUrl || !view.url) { misses.push(`${candidate.source}: branch matches but PR url unconfirmed (run ${produced?.prUrl ?? 'none'} · repo ${view.url ?? 'none'})`); continue; }
140
+ return { resolved: true, cwd: candidate.cwd, source: candidate.source, proof: 'branch', view, reason: `${candidate.source} PR #${pr} head branch matches run` };
141
+ }
142
+ return { resolved: false, reason: misses.join(' · ') };
143
+ }
65
144
 
66
145
  /** 받은 환경 그대로 먼저 묻고, 실패하면 프록시만 뺀 환경으로 한 번 더(이 저장소의 gh 운영 관행 — 프록시 경유가 gh 를 깨는 호스트). */
67
146
  export type GhRun = (args: string[], options: { env: NodeJS.ProcessEnv; cwd?: string }) => { status: number | null; stdout: string };
68
147
  const runGh: GhRun = (args, options) => spawnSync('gh', args, { encoding: 'utf8', timeout: 30_000, env: options.env, ...(options.cwd ? { cwd: options.cwd } : {}) });
69
148
 
70
- export async function defaultPrHead(pr: number, cwd?: string, gh: GhRun = runGh): Promise<{ head: string; state: string } | null> {
71
- const view = (env: NodeJS.ProcessEnv) => gh(['pr', 'view', String(pr), '--json', 'headRefOid,state'], { env, ...(cwd ? { cwd } : {}) });
149
+ export async function defaultPrHead(pr: number, cwd?: string, gh: GhRun = runGh): Promise<PrHeadView | null> {
150
+ const view = (env: NodeJS.ProcessEnv) => gh(['pr', 'view', String(pr), '--json', 'headRefOid,headRefName,state,url'], { env, ...(cwd ? { cwd } : {}) });
72
151
  let out = view(process.env);
73
152
  if (out.status !== 0) {
74
153
  const noProxy = { ...process.env };
@@ -77,9 +156,13 @@ export async function defaultPrHead(pr: number, cwd?: string, gh: GhRun = runGh)
77
156
  }
78
157
  if (out.status !== 0) return null;
79
158
  try {
80
- const value = JSON.parse(out.stdout) as { headRefOid?: unknown; state?: unknown };
159
+ const value = JSON.parse(out.stdout) as { headRefOid?: unknown; headRefName?: unknown; state?: unknown; url?: unknown };
81
160
  return typeof value.headRefOid === 'string' && /^[a-f0-9]{40}$/i.test(value.headRefOid) && typeof value.state === 'string'
82
- ? { head: value.headRefOid, state: value.state } : null;
161
+ ? {
162
+ head: value.headRefOid, state: value.state,
163
+ ...(typeof value.headRefName === 'string' && value.headRefName ? { branch: value.headRefName } : {}),
164
+ ...(typeof value.url === 'string' && value.url ? { url: value.url } : {}),
165
+ } : null;
83
166
  } catch { return null; }
84
167
  }
85
168
 
@@ -121,16 +204,33 @@ function observe(deps: LiveMoveDeps, result: LiveMoveResult): LiveMoveResult {
121
204
  * 다음 틱에 한 번 더 요청될 수 있다(초과는 많아야 한 번 · 리뷰는 읽기 전용이라 착지·재발사로 번지지 않는다).
122
205
  */
123
206
  /** `review` — PR 머리마다 한 번. 카드에 먼저 적고(잠금 안) 그다음 띄운다 · 띄우기 실패는 카드에 오류로 남기고 같은 머리는 다시 안 띄운다. */
124
- export async function executeLiveReview(card: TaskCard | undefined, pr: number | undefined, ctx: { runId: string | null; stopReason: string; cwd?: string }, deps: LiveMoveDeps = {}): Promise<LiveMoveResult> {
207
+ export async function executeLiveReview(
208
+ card: TaskCard | undefined, pr: number | undefined,
209
+ ctx: { runId: string | null; stopReason: string; cwd?: string; produced?: { headCommit?: string; branch?: string; prUrl?: string } },
210
+ deps: LiveMoveDeps = {},
211
+ ): Promise<LiveMoveResult> {
125
212
  const kind = 'review' as const;
126
213
  if (!card) return observe(deps, { kind, executed: false, card: null, ok: false, detail: 'no task card — stays shadow' });
127
214
  if (!pr || !Number.isSafeInteger(pr) || pr < 1) return observe(deps, { kind, executed: false, card: card.id, ok: false, detail: 'no PR — stays shadow' });
128
- // PR 의 저장소(그 PR 을 낸 결과의 작업 트리)를 모르면 보류한다 — 다른 저장소에서 gh·self review 를 돌리지 않게.
129
- if (!ctx.cwd) return observe(deps, { kind, executed: false, card: card.id, ok: false, detail: `PR #${pr} worktree unknown — stays shadow` });
130
- let head: { head: string; state: string } | null;
131
- try { head = await (deps.prHead ?? defaultPrHead)(pr, ctx.cwd); } catch { head = null; }
132
- if (!head) return observe(deps, { kind, executed: false, card: card.id, ok: false, detail: `PR #${pr} head unknown` });
133
- if (head.state !== 'OPEN') return observe(deps, { kind, executed: false, card: card.id, ok: false, detail: `PR #${pr} is ${head.state}` });
215
+ // PR 의 저장소를 모르면 보류한다 — 다른 저장소에서 gh·self review 를 돌리지 않게. 작업 트리가 없으면(Pod 런 ·
216
+ // TA-LIVE-REVIEW-POD) 후보 저장소 중 «런이 낸 PR 머리»가 확인되는 곳만 쓰고, 못 정하면 이유 한 줄을 관측에 싣는다.
217
+ let cwd = ctx.cwd;
218
+ let head: PrHeadView | null = null;
219
+ let reviewRepo: LiveMoveResult['reviewRepo'];
220
+ if (!cwd) {
221
+ const found = await resolveReviewRepo(card, pr, ctx.produced, deps);
222
+ if (!found.resolved) {
223
+ return observe(deps, { kind, executed: false, card: card.id, ok: false, detail: `PR #${pr} worktree unknown — stays shadow`, reviewRepo: { resolved: false, reason: found.reason } });
224
+ }
225
+ cwd = found.cwd;
226
+ head = found.view;
227
+ reviewRepo = { resolved: true, source: found.source, cwd: found.cwd, proof: found.proof, reason: found.reason };
228
+ } else {
229
+ try { head = await (deps.prHead ?? defaultPrHead)(pr, cwd); } catch { head = null; }
230
+ }
231
+ const withRepo = (result: LiveMoveResult): LiveMoveResult => reviewRepo ? { ...result, reviewRepo } : result;
232
+ if (!head) return observe(deps, withRepo({ kind, executed: false, card: card.id, ok: false, detail: `PR #${pr} head unknown` }));
233
+ if (head.state !== 'OPEN') return observe(deps, withRepo({ kind, executed: false, card: card.id, ok: false, detail: `PR #${pr} is ${head.state}` }));
134
234
  const path = deps.statePath ?? taskAgentStatePath();
135
235
  const at = (deps.now ?? (() => new Date()))().toISOString();
136
236
  const claim = { claimed: false, missing: false };
@@ -141,18 +241,18 @@ export async function executeLiveReview(card: TaskCard | undefined, pr: number |
141
241
  claim.claimed = true;
142
242
  return { ...current, reviewRequests: [...(current.reviewRequests ?? []), { pr, head: head!.head, at }] };
143
243
  });
144
- if (claim.missing) return observe(deps, { kind, executed: false, card: card.id, ok: false, detail: 'task card not in state file' });
145
- if (!claim.claimed) return observe(deps, { kind, executed: false, card: card.id, ok: true, detail: `already requested for #${pr} head ${head.head.slice(0, 12)}` });
244
+ if (claim.missing) return observe(deps, withRepo({ kind, executed: false, card: card.id, ok: false, detail: 'task card not in state file' }));
245
+ if (!claim.claimed) return observe(deps, withRepo({ kind, executed: false, card: card.id, ok: true, detail: `already requested for #${pr} head ${head.head.slice(0, 12)}` }));
146
246
  try {
147
- await (deps.requestReview ?? defaultRequestReview)(pr, liveReviewIntent(card, ctx.runId, ctx.stopReason), ctx.cwd);
247
+ await (deps.requestReview ?? defaultRequestReview)(pr, liveReviewIntent(card, ctx.runId, ctx.stopReason), cwd);
148
248
  } catch (error) {
149
249
  const reason = error instanceof Error ? error.message : String(error);
150
250
  updateTaskCard(path, card.id, (current) => current?.reviewRequests?.some((entry) => entry.pr === pr && entry.head === head!.head && entry.at === at)
151
251
  ? { ...current, reviewRequests: current.reviewRequests.map((entry) => entry.pr === pr && entry.head === head!.head && entry.at === at ? { ...entry, error: reason } : entry) }
152
252
  : undefined);
153
- return observe(deps, { kind, executed: false, card: card.id, ok: false, detail: `review request failed for #${pr} head ${head.head.slice(0, 12)}: ${reason}` });
253
+ return observe(deps, withRepo({ kind, executed: false, card: card.id, ok: false, detail: `review request failed for #${pr} head ${head.head.slice(0, 12)}: ${reason}` }));
154
254
  }
155
- return observe(deps, { kind, executed: true, card: card.id, ok: true, detail: `review requested for #${pr} head ${head.head.slice(0, 12)}` });
255
+ return observe(deps, withRepo({ kind, executed: true, card: card.id, ok: true, detail: `review requested for #${pr} head ${head.head.slice(0, 12)}` }));
156
256
  }
157
257
 
158
258
  /** `propose-green` — 비지 않은 근거가 있을 때만 카드에 제안을 적는다(한 번). 체크리스트는 쓰지 않는다. */
@@ -296,7 +296,7 @@ export function recordPieceRef(pieceId: string, ref: { pr?: number; goalId?: str
296
296
 
297
297
  /** 이미 넘긴 조각인가 — live 는 live 넘김·발사·발사 실패면 다시 안 넘긴다 · shadow 는 어떤 넘김이든 있으면 안 넘긴다. */
298
298
  function alreadyHanded(card: TaskCard, mode: 'shadow' | 'live'): boolean {
299
- if (card.status === 'launched' || card.status === 'launch-failed') return true;
299
+ if (card.status === 'launched' || card.status === 'launch-failed' || card.status === 'failed') return true;
300
300
  if (!card.handed) return false;
301
301
  return mode === 'shadow' || card.handed.mode === 'live';
302
302
  }
@@ -227,7 +227,16 @@ export async function recordTaskAgentShadowMove(input: TaskAgentShadowInput, dep
227
227
  if (live) {
228
228
  if (liveKind === 'review') {
229
229
  liveMove = await executeLiveReview(boundCard, prResult?.prNumber,
230
- { runId: input.runId, stopReason: input.stopReason, ...(prResult?.worktreePath ? { cwd: prResult.worktreePath } : {}) }, { ...(deps.log ? { log: deps.log } : {}), ...deps.live });
230
+ {
231
+ runId: input.runId, stopReason: input.stopReason,
232
+ ...(prResult?.worktreePath ? { cwd: prResult.worktreePath } : {}),
233
+ // TA-LIVE-REVIEW-POD — 작업 트리가 없는 런(Pod)은 «런이 낸 머리»로 리뷰 저장소를 확인한다.
234
+ ...(prResult && !prResult.worktreePath ? { produced: {
235
+ ...(prResult.checkedHeadCommit !== undefined ? { headCommit: prResult.checkedHeadCommit } : {}),
236
+ ...(prResult.branch ? { branch: prResult.branch } : {}),
237
+ ...(prResult.prUrl ? { prUrl: prResult.prUrl } : {}),
238
+ } } : {}),
239
+ }, { ...(deps.log ? { log: deps.log } : {}), ...deps.live });
231
240
  } else {
232
241
  const evidence: Record<string, string> = {};
233
242
  if (boundCard && !delivery && judgeInput.prState === 'MERGED' && prResult) evidence[boundCard.id] = `#${prResult.prNumber}`;