elanous 0.2.18 → 0.2.19

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 (404) 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/{P6YTVgzuZz5oTZhY4Din_ → J5H7BvqgSI8DUjtRRcA_S}/_buildManifest.js +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{726.83532d69452ee24b.js → 1470.98a6ab61ccb4131c.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/{9247-269e4464cc4b491c.js → 2375-e061bb24cd2b5cde.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{211-9c9090c59e2c5907.js → 2977-e38442e86289f1d3.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/{f6b7c9ba.64a895b8b2fedbdc.js → 2ea96958.e8c7e112de50b067.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +2 -0
  16. package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +5 -0
  19. package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/{1268.0840ff91e083fcbd.js → 4323.c5dfb6f503304775.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/{6110.1297d4093fdfbb0d.js → 4847.56f5bc454fbcc827.js} +2 -2
  28. package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/{7327-2d69fb321ba73389.js → 6062-73e68343a9814f6a.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/{935.4ddaa99fc1d1126c.js → 7647.2fcd30d894d5ee30.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/{f6231284-45726550aba08fca.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/8870-9614a213b990b10b.js +25 -0
  49. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/ae787e4c-e5b1ef00b2dbceca.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/inside/page-5b29d9b06ceadade.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +1 -0
  104. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
  105. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
  106. package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +1 -0
  107. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
  108. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
  109. package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +1 -0
  110. package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
  111. package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +1 -0
  112. package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
  113. package/apps/pwa/out/approvals/index.html +1 -1
  114. package/apps/pwa/out/approvals/index.txt +23 -23
  115. package/apps/pwa/out/autopilot/index.html +1 -1
  116. package/apps/pwa/out/autopilot/index.txt +23 -23
  117. package/apps/pwa/out/board/index.html +1 -1
  118. package/apps/pwa/out/board/index.txt +23 -23
  119. package/apps/pwa/out/botlab/index.html +1 -1
  120. package/apps/pwa/out/botlab/index.txt +22 -22
  121. package/apps/pwa/out/bots/index.html +1 -1
  122. package/apps/pwa/out/bots/index.txt +22 -22
  123. package/apps/pwa/out/ceo/index.html +1 -1
  124. package/apps/pwa/out/ceo/index.txt +23 -23
  125. package/apps/pwa/out/chat/index.html +1 -1
  126. package/apps/pwa/out/chat/index.txt +22 -22
  127. package/apps/pwa/out/consult/index.html +1 -1
  128. package/apps/pwa/out/consult/index.txt +23 -23
  129. package/apps/pwa/out/control/index.html +1 -1
  130. package/apps/pwa/out/control/index.txt +23 -23
  131. package/apps/pwa/out/decisions/index.html +1 -1
  132. package/apps/pwa/out/decisions/index.txt +22 -22
  133. package/apps/pwa/out/design-check/index.html +1 -1
  134. package/apps/pwa/out/design-check/index.txt +23 -23
  135. package/apps/pwa/out/editor/index.html +1 -1
  136. package/apps/pwa/out/editor/index.txt +25 -24
  137. package/apps/pwa/out/exec/index.html +1 -1
  138. package/apps/pwa/out/exec/index.txt +23 -23
  139. package/apps/pwa/out/field/index.html +1 -1
  140. package/apps/pwa/out/field/index.txt +23 -23
  141. package/apps/pwa/out/healthz +1 -1
  142. package/apps/pwa/out/index.html +1 -1
  143. package/apps/pwa/out/index.txt +22 -22
  144. package/apps/pwa/out/inside/index.html +1 -1
  145. package/apps/pwa/out/inside/index.txt +24 -23
  146. package/apps/pwa/out/intake/index.html +1 -1
  147. package/apps/pwa/out/intake/index.txt +23 -23
  148. package/apps/pwa/out/live/index.html +1 -1
  149. package/apps/pwa/out/live/index.txt +23 -23
  150. package/apps/pwa/out/loops/index.html +1 -1
  151. package/apps/pwa/out/loops/index.txt +28 -27
  152. package/apps/pwa/out/market/index.html +1 -1
  153. package/apps/pwa/out/market/index.txt +22 -22
  154. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  155. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  156. package/apps/pwa/out/morning/index.html +1 -1
  157. package/apps/pwa/out/morning/index.txt +23 -23
  158. package/apps/pwa/out/observatory/index.html +1 -1
  159. package/apps/pwa/out/observatory/index.txt +23 -23
  160. package/apps/pwa/out/ops/checklist/index.html +1 -1
  161. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  162. package/apps/pwa/out/ops/release/index.html +1 -1
  163. package/apps/pwa/out/ops/release/index.txt +22 -22
  164. package/apps/pwa/out/ops/seats/index.html +1 -1
  165. package/apps/pwa/out/ops/seats/index.txt +22 -22
  166. package/apps/pwa/out/outputs/index.html +1 -1
  167. package/apps/pwa/out/outputs/index.txt +22 -22
  168. package/apps/pwa/out/reflection/index.html +1 -1
  169. package/apps/pwa/out/reflection/index.txt +23 -23
  170. package/apps/pwa/out/scheduler/index.html +1 -1
  171. package/apps/pwa/out/scheduler/index.txt +23 -23
  172. package/apps/pwa/out/sessions/index.html +1 -1
  173. package/apps/pwa/out/sessions/index.txt +22 -22
  174. package/apps/pwa/out/settings/devices/index.html +1 -1
  175. package/apps/pwa/out/settings/devices/index.txt +23 -23
  176. package/apps/pwa/out/settings/index.html +1 -1
  177. package/apps/pwa/out/settings/index.txt +23 -23
  178. package/apps/pwa/out/setup/done/index.html +1 -1
  179. package/apps/pwa/out/setup/done/index.txt +23 -23
  180. package/apps/pwa/out/setup/index.html +1 -1
  181. package/apps/pwa/out/setup/index.txt +23 -23
  182. package/apps/pwa/out/share/index.html +1 -1
  183. package/apps/pwa/out/share/index.txt +23 -23
  184. package/apps/pwa/out/showroom/index.html +1 -1
  185. package/apps/pwa/out/showroom/index.txt +22 -22
  186. package/apps/pwa/out/tasks/index.html +1 -1
  187. package/apps/pwa/out/tasks/index.txt +23 -23
  188. package/apps/pwa/out/term/index.html +1 -1
  189. package/apps/pwa/out/term/index.txt +23 -23
  190. package/apps/pwa/out/today/index.html +1 -1
  191. package/apps/pwa/out/today/index.txt +23 -23
  192. package/apps/pwa/out/trace/index.html +1 -1
  193. package/apps/pwa/out/trace/index.txt +23 -23
  194. package/apps/pwa/out/vault/index.html +1 -1
  195. package/apps/pwa/out/vault/index.txt +23 -23
  196. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  197. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  198. package/apps/pwa/out/workflows/index.html +1 -1
  199. package/apps/pwa/out/workflows/index.txt +23 -23
  200. package/apps/pwa/out/workspace/index.html +1 -1
  201. package/apps/pwa/out/workspace/index.txt +23 -23
  202. package/apps/pwa/out/worktrees/index.html +1 -1
  203. package/apps/pwa/out/worktrees/index.txt +23 -23
  204. package/graphs/ops/guardian.yaml +26 -0
  205. package/graphs/ops/recipes.yaml +3 -0
  206. package/package.json +1 -1
  207. package/scripts/coord-channel-watch.sh +2 -2
  208. package/scripts/discord-seat-probe.ts +3 -1
  209. package/scripts/lib/pch-regress-map.ts +23 -0
  210. package/scripts/morning-report.ts +18 -4
  211. package/scripts/pch-regress.ts +88 -0
  212. package/scripts/release-loop/gate-diff.ts +18 -0
  213. package/scripts/release-loop/gate-node.ts +338 -46
  214. package/scripts/release-loop/gate-partial.ts +106 -0
  215. package/scripts/release-loop/gate-timing.ts +120 -0
  216. package/scripts/release-loop/resume-release.ts +64 -5
  217. package/scripts/release-loop/shard-plan.ts +65 -0
  218. package/scripts/release-loop/unattended-release.ts +2 -0
  219. package/scripts/video/scene-table.ts +159 -0
  220. package/src/acp/core-turn-bridge.ts +39 -2
  221. package/src/away/presence.ts +40 -0
  222. package/src/away/release-watch.ts +255 -0
  223. package/src/chat/index.ts +2 -1
  224. package/src/chat/short-question.ts +3 -40
  225. package/src/cli/away-cli.ts +68 -0
  226. package/src/cli/guardian-cli.ts +49 -0
  227. package/src/cli/intake-cli.ts +13 -7
  228. package/src/cli/log-event-names.ts +1 -0
  229. package/src/cli/pod-cli.ts +5 -1
  230. package/src/cli/pty-drive-cli.ts +14 -3
  231. package/src/cli/release-cli.ts +32 -7
  232. package/src/cli/setup-cli.ts +31 -1
  233. package/src/cli/tasks-cli.ts +104 -11
  234. package/src/context-bus/context-now-public.ts +1 -1
  235. package/src/context-bus/context-now-surfaces.ts +16 -6
  236. package/src/context-bus/context-now.ts +97 -3
  237. package/src/core-turn/short-question.ts +100 -0
  238. package/src/dashboard/dashboard-status-lines.ts +24 -4
  239. package/src/dashboard/index.ts +50 -5
  240. package/src/dashboard/input/slash-executor.ts +6 -2
  241. package/src/dashboard/slash-runtime/dashboard-handlers.ts +63 -36
  242. package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
  243. package/src/dashboard/slash-runtime/help-from-registry.ts +21 -8
  244. package/src/dashboard/slash-runtime/help-groups.ts +44 -0
  245. package/src/dashboard/slash-runtime/model-choices.ts +125 -0
  246. package/src/domains/guardian-tick.ts +138 -0
  247. package/src/domains/outbound-alert.ts +44 -13
  248. package/src/domains/schedule-registry.ts +23 -0
  249. package/src/domains/telegram-kind-route.ts +39 -0
  250. package/src/domains/watch-default-scheduler.ts +95 -0
  251. package/src/esc-abort-gate.ts +44 -0
  252. package/src/graph-runner/runner.ts +25 -5
  253. package/src/harness/harness-cli-command.ts +2 -0
  254. package/src/harness/harness-salvage-cli.ts +78 -0
  255. package/src/harness/harness-worktree-add.ts +68 -0
  256. package/src/harness/ledger-write-guard.ts +6 -16
  257. package/src/hq/hq.ts +6 -1
  258. package/src/index.ts +52 -3
  259. package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
  260. package/src/instance/test-write-guard.ts +200 -0
  261. package/src/intake-plane/digest.ts +39 -15
  262. package/src/intake-plane/harness-target.ts +4 -3
  263. package/src/intake-plane/intake-sources.ts +1 -0
  264. package/src/intake-plane/items.ts +1 -0
  265. package/src/loops/orchestrator/degradation.ts +57 -0
  266. package/src/loops/orchestrator/finish-rate.ts +33 -3
  267. package/src/loops/orchestrator/tick.ts +166 -45
  268. package/src/maturity/feature-maturity.ts +6 -4
  269. package/src/maturity/tui-slash-maturity.ts +1 -0
  270. package/src/mss/logging/log-store.ts +3 -0
  271. package/src/nexus/api/http-server.ts +16 -5
  272. package/src/nexus/api/loop-edges.ts +290 -17
  273. package/src/nexus/api/ops-api.ts +96 -28
  274. package/src/nexus/index.ts +7 -0
  275. package/src/release-loop/checklist.ts +2 -0
  276. package/src/release-loop/feature-store.ts +54 -7
  277. package/src/release-loop/light-rc.ts +173 -0
  278. package/src/release-loop/placement.ts +51 -17
  279. package/src/release-loop/rubric.ts +57 -1
  280. package/src/self-dev/dev-cli.ts +36 -0
  281. package/src/self-dev/orchestrate.ts +1 -0
  282. package/src/self-dev/pr-preview.ts +179 -0
  283. package/src/self-dev/shard-run.ts +23 -7
  284. package/src/self-implement/gate-cli.ts +29 -8
  285. package/src/self-implement/goal-author-clarification.ts +88 -13
  286. package/src/self-implement/goal-clarification-escalation.ts +2 -0
  287. package/src/self-implement/orchestrator.ts +2 -1
  288. package/src/self-implement/salvage-branches.ts +254 -0
  289. package/src/task-agent/actions.ts +63 -2
  290. package/src/task-agent/board.ts +132 -0
  291. package/src/task-agent/card-evidence.ts +203 -0
  292. package/src/task-agent/completion-evidence.ts +126 -0
  293. package/src/task-agent/judge.ts +18 -0
  294. package/src/task-agent/live-moves.ts +176 -0
  295. package/src/task-agent/mission.ts +32 -6
  296. package/src/task-agent/shadow.ts +183 -15
  297. package/src/task-agent/task-hand.ts +117 -15
  298. package/src/task-orchestrator/surfaces/pod-command-job.ts +64 -4
  299. package/src/task-orchestrator/surfaces/pod-lease.ts +142 -13
  300. package/src/task-orchestrator/surfaces/pod-pool.ts +13 -5
  301. package/src/task-orchestrator/surfaces/self-implement-pod.ts +2 -2
  302. package/src/telegram-away-command.ts +38 -0
  303. package/src/telegram-commands.ts +11 -0
  304. package/src/telegram-report-log-probe.ts +21 -0
  305. package/src/telegram-report.ts +27 -3
  306. package/src/tui-client/dashboard-session.ts +1 -0
  307. package/src/user-config.ts +134 -2
  308. package/src/version/packed-revision.json +1 -1
  309. package/apps/pwa/out/_next/static/chunks/1307-30dff22c9e1f3073.js +0 -1
  310. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +0 -1
  311. package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +0 -1
  312. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +0 -1
  313. package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +0 -5
  314. package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/6158.af2e93b7adff709b.js +0 -2
  335. package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
  344. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d92f7ee3994e84c7.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-4733c967b56a4277.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/board/page-d22fd849cc22f104.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/botlab/page-bb742f995c6769e7.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/bots/page-02b073a2ac793b83.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/control/page-308111abadf35670.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/app/design-check/page-5d97b2901e0d1ea6.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/app/exec/page-8c3d8e63ae2e1c98.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/app/field/page-9b72a07d424701c6.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/app/intake/page-a572f58cd3ca5864.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/app/market/page-fa244bd146a00ae8.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-44d56d6391965a6a.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/app/morning/page-83a700d4c754d802.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/not-found-7b3170f374f77b9d.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/reflection/page-1467047104141297.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/app/share/page-3cc47dca0922ab9a.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +0 -1
  388. package/apps/pwa/out/_next/static/chunks/app/tasks/page-b17cd46cf0e9b8cf.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/app/trace/page-7bf54c9bb9274af8.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/app/workspace/page-134da468538baf48.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/webpack-c2782b330447ffb6.js +0 -1
  403. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
  404. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → J5H7BvqgSI8DUjtRRcA_S}/_ssgManifest.js +0 -0
@@ -0,0 +1,203 @@
1
+ /**
2
+ * TA-CARD-RUN-LINK — 과제 카드의 런 id 로 그 런이 연 골 id · PR 을 런 원장에서 읽어 카드에 «한 번» 적는다.
3
+ * - 순수 판정 `bindCardEvidence(card, snapshot)` — 호스트 원장 → Pod 자식 원장(`pod-child-run`) → 호스트 logs 의
4
+ * `self-implement.pod job-finished {childRunId, prUrl}` 순. 모르면 비운다(추측하지 않는다).
5
+ * - `refreshCardRunBinding` 은 `tasks show`·`tasks advance` 가 부른다 — 읽기 조회이고, 찾았을 때만 잠금 안에서 카드의
6
+ * `goalId`·`pr {number,url}`(UX LOOP-INTERACT 계약 칸)을 비어 있을 때만 적는다 · 수동 근거(refSource manual)는 덮지 않는다.
7
+ * - 관측: `debug.log('task-agent', 'card-pr-bound', {card, runId, pr})`.
8
+ */
9
+ import { existsSync } from 'node:fs';
10
+ import { debug } from '../debug/log.js';
11
+ import type { RunLedgerEntry } from '../self-implement/run-ledger.js';
12
+ import { cardPrNumber, taskAgentStatePath, updateTaskCard, readTaskCard, type TaskCard } from './task-hand.js';
13
+
14
+ const GOAL_ID = /^[a-f0-9]{16}$/;
15
+ const PR_URL = /^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/(\d+)$/;
16
+ const RUN_ID = /^run-[A-Za-z0-9_-]{4,64}$/;
17
+
18
+ /** 카드 런 하나의 원장 사진 — host = 카드 런 원장(없으면 null) · children = `pod-child-run` 자식 원장 · podFinishes = 호스트 logs. */
19
+ export interface CardRunSnapshot {
20
+ host: readonly RunLedgerEntry[] | null;
21
+ children: Readonly<Record<string, readonly RunLedgerEntry[] | null>>;
22
+ podFinishes?: ReadonlyArray<{ childRunId: string; prUrl: string }>;
23
+ /** 읽기 «실패»(원장·logs) — 부재(null)와 갈린다. 있으면 refresh 가 card-bind-failed 를 남긴다. */
24
+ readErrors?: readonly string[];
25
+ }
26
+
27
+ export interface CardRunEvidence {
28
+ goalId?: string;
29
+ pr?: { number: number; url?: string };
30
+ childRunId?: string;
31
+ /** 호스트 원장이 있었나 — 없으면 런이 아직 시작 전이거나 대기열로 갔다. */
32
+ ledgerFound: boolean;
33
+ }
34
+
35
+ function entryGoal(entry: RunLedgerEntry): string | undefined {
36
+ const value = entry.goalId ?? (typeof entry.data?.goalId === 'string' ? entry.data.goalId : undefined);
37
+ return typeof value === 'string' && GOAL_ID.test(value) ? value : undefined;
38
+ }
39
+
40
+ /**
41
+ * 그 PR 의 골 id — PR 의 `pr-opened` 줄 골이 먼저. 없으면 원장 전체의 골이 «하나뿐»이고 다른 PR 줄이 골을 싣지 않았을 때만
42
+ * 그 골(대응이 모호하면 비운다 — set-once 라 틀린 짝은 못 고친다).
43
+ */
44
+ function goalIdOf(entries: readonly RunLedgerEntry[], prNumber: number): string | undefined {
45
+ const own = [...entries].reverse().find((entry) => entry.event === 'pr-opened' && entry.data?.number === prNumber && entryGoal(entry));
46
+ if (own) return entryGoal(own);
47
+ if (entries.some((entry) => entry.event === 'pr-opened' && entry.data?.number !== prNumber && entryGoal(entry))) return undefined;
48
+ const goals = new Set(entries.map(entryGoal).filter((value): value is string => value !== undefined));
49
+ return goals.size === 1 ? [...goals][0] : undefined;
50
+ }
51
+
52
+ function lastPr(entries: readonly RunLedgerEntry[]): { number: number; url?: string } | undefined {
53
+ for (let at = entries.length - 1; at >= 0; at--) {
54
+ const entry = entries[at]!;
55
+ if (entry.event !== 'pr-opened') continue;
56
+ const number = entry.data?.number;
57
+ if (!Number.isSafeInteger(number) || (number as number) <= 0) continue;
58
+ const url = typeof entry.data.url === 'string' && PR_URL.exec(entry.data.url)?.[1] === String(number) ? entry.data.url : undefined;
59
+ return { number: number as number, ...(url ? { url } : {}) };
60
+ }
61
+ return undefined;
62
+ }
63
+
64
+ /** 자식 런 id — 호스트 원장의 `pod-child-run` 만(호스트 런 자신은 빼고 · 순서 유지). */
65
+ export function podChildRunIds(host: readonly RunLedgerEntry[] | null, runId?: string): string[] {
66
+ return [...new Set((host ?? []).filter((entry) => entry.event === 'pod-child-run' && typeof entry.data?.childRunId === 'string'
67
+ && RUN_ID.test(entry.data.childRunId) && entry.data.childRunId !== runId).map((entry) => entry.data.childRunId as string))];
68
+ }
69
+
70
+ /** 순수 — 카드 런의 골 id · PR. PR 은 호스트 → 자식(나중 자식 먼저) → job-finished 순으로 처음 찾은 것. */
71
+ export function bindCardEvidence(card: Pick<TaskCard, 'runId'>, snapshot: CardRunSnapshot): CardRunEvidence {
72
+ if (!card.runId || !snapshot.host) return { ledgerFound: false };
73
+ const children = podChildRunIds(snapshot.host, card.runId).reverse();
74
+ const childLedgers = children.map((id) => [id, snapshot.children[id] ?? null] as const);
75
+ let pr = lastPr(snapshot.host);
76
+ let childRunId: string | undefined;
77
+ for (const [id, entries] of childLedgers) {
78
+ if (pr) break;
79
+ const found = entries ? lastPr(entries) : undefined;
80
+ if (found) { pr = found; childRunId = id; }
81
+ }
82
+ for (const id of children) {
83
+ if (pr) break;
84
+ const finish = [...(snapshot.podFinishes ?? [])].reverse().find((row) => row.childRunId === id && PR_URL.test(row.prUrl));
85
+ const number = finish ? Number(PR_URL.exec(finish.prUrl)![1]) : NaN;
86
+ if (finish && Number.isSafeInteger(number) && number > 0) { pr = { number, url: finish.prUrl }; childRunId = id; }
87
+ }
88
+ // 골 id 는 PR 을 낸 런의 것뿐이고 PR 과 함께만 — 자식이 냈으면 그 자식 원장의 골(아직 없으면 비움 · 나중 조회가 채운다),
89
+ // 호스트가 냈으면 호스트 골. PR 전에는 누가 낼지 모르니 골도 묶지 않는다(set-once 라 섞이면 못 고친다).
90
+ const childGoal = (id: string) => { const entries = snapshot.children[id]; return entries && pr ? goalIdOf(entries, pr.number) : undefined; };
91
+ const goalId = pr ? (childRunId ? childGoal(childRunId) : goalIdOf(snapshot.host, pr.number)) : undefined;
92
+ return { ledgerFound: true, ...(goalId ? { goalId } : {}), ...(pr ? { pr } : {}), ...(childRunId ? { childRunId } : {}) };
93
+ }
94
+
95
+ /** 원장·logs 읽기(시험 주입). */
96
+ export interface CardRunEvidenceDeps {
97
+ loadLedger?: (runId: string) => RunLedgerEntry[] | null;
98
+ podFinishes?: (childRunIds: readonly string[], sinceIso: string) => Array<{ childRunId: string; prUrl: string }>;
99
+ now?: () => Date;
100
+ }
101
+
102
+ async function defaultLoadLedger(): Promise<(runId: string) => RunLedgerEntry[] | null> {
103
+ const { loadRunLedger, runLedgerDir, resolveFederatedRunLedgerDirectories } = await import('../self-implement/run-ledger.js');
104
+ let dirs: string[] = [runLedgerDir()];
105
+ // 연합 목록을 못 읽으면 이 우주만 읽되, 여기서 못 찾으면 «없음»이 아니라 실패로 올린다(card-bind-failed).
106
+ let federationFailure: unknown;
107
+ try { dirs = [...new Set([...dirs, ...resolveFederatedRunLedgerDirectories({})])]; } catch (error) { federationFailure = new Error(`federated run-ledger lookup failed: ${error instanceof Error ? error.message : String(error)}`); }
108
+ return ledgerLoader(dirs, (runId, dir) => loadRunLedger(runId, dir), federationFailure);
109
+ }
110
+
111
+ /** 디렉터리들에서 첫 원장 — 어디서도 못 찾았고 읽기(또는 연합 목록) 실패가 있었으면 부재(null)가 아니라 던진다. */
112
+ export function ledgerLoader(dirs: readonly string[], load: (runId: string, dir: string) => RunLedgerEntry[] | null, priorFailure?: unknown): (runId: string) => RunLedgerEntry[] | null {
113
+ return (runId) => {
114
+ let failure: unknown = priorFailure;
115
+ for (const dir of dirs) {
116
+ try { const entries = load(runId, dir); if (entries) return entries; } catch (error) { failure ??= error; }
117
+ }
118
+ if (failure !== undefined) throw failure;
119
+ return null;
120
+ };
121
+ }
122
+
123
+ async function defaultPodFinishes(): Promise<NonNullable<CardRunEvidenceDeps['podFinishes']>> {
124
+ const { LogStore, logsDbPath } = await import('../mss/logging/log-store.js');
125
+ return (childRunIds, sinceIso) => {
126
+ const path = logsDbPath();
127
+ if (!childRunIds.length || !existsSync(path)) return [];
128
+ const store = LogStore.openReadOnly(path);
129
+ try {
130
+ const wanted = new Set(childRunIds);
131
+ const rows: Array<{ childRunId: string; prUrl: string }> = [];
132
+ const sinceMs = Date.parse(sinceIso);
133
+ 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;
135
+ 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 });
137
+ }
138
+ return rows;
139
+ } finally { store.close(); }
140
+ };
141
+ }
142
+
143
+ /** 카드 런의 원장 사진을 읽는다 — 읽기 실패는 «없음»(null)이다. */
144
+ export async function readCardRunSnapshot(card: Pick<TaskCard, 'runId' | 'createdAt'>, deps: CardRunEvidenceDeps = {}): Promise<CardRunSnapshot> {
145
+ if (!card.runId) return { host: null, children: {} };
146
+ const load = deps.loadLedger ?? await defaultLoadLedger();
147
+ const readErrors: string[] = [];
148
+ const reason = (error: unknown) => (error instanceof Error ? error.message : String(error)).slice(0, 300);
149
+ const safe = (runId: string) => { try { return load(runId); } catch (error) { readErrors.push(`ledger ${runId}: ${reason(error)}`); return null; } };
150
+ const host = safe(card.runId);
151
+ const childIds = podChildRunIds(host, card.runId);
152
+ 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)}`); }
157
+ }
158
+ return { host, children, podFinishes, ...(readErrors.length ? { readErrors } : {}) };
159
+ }
160
+
161
+ /**
162
+ * 카드에 런 근거를 채운다 — 런 id 가 있고 아직 비어 있는 칸(goalId·pr)만, 잠금 안에서 «한 번».
163
+ * 찾지 못했으면 쓰지 않는다. 쓴(또는 그대로인) 카드를 돌려준다.
164
+ */
165
+ export async function refreshCardRunBinding(cardId: string, statePath = taskAgentStatePath(), deps: CardRunEvidenceDeps = {}): Promise<{ card: TaskCard | undefined; evidence?: CardRunEvidence }> {
166
+ const card = readTaskCard(cardId, statePath);
167
+ // 수동 근거(refSource manual)는 덮지 않는다 · 둘 다 차 있으면 다시 읽지 않는다(한 번만).
168
+ if (!card?.runId || card.refSource === 'manual' || (card.pr !== undefined && card.goalId !== undefined)) return { card };
169
+ const snapshot = await readCardRunSnapshot(card, deps);
170
+ if (snapshot.readErrors?.length) {
171
+ try { debug.log('task-agent', 'card-bind-failed', { card: cardId, runId: card.runId, errors: snapshot.readErrors }); } catch { /* fail-soft */ }
172
+ }
173
+ const evidence = bindCardEvidence(card, snapshot);
174
+ // 이미 묶인 PR 이 있으면 «같은» PR(번호 ⊕ 낸 자식)을 다시 찾았을 때만 뒤늦은 골을 채운다.
175
+ const samePr = (current: TaskCard) => current.pr === undefined
176
+ || (evidence.pr !== undefined && cardPrNumber(current) === evidence.pr.number && current.runChildId === evidence.childRunId);
177
+ const wantsGoal = card.goalId === undefined && evidence.goalId && samePr(card);
178
+ const wantsPr = card.pr === undefined && evidence.pr;
179
+ if (!wantsGoal && !wantsPr) return { card, evidence };
180
+ const at = (deps.now ?? (() => new Date()))().toISOString();
181
+ const wrote = { pr: false };
182
+ const next = updateTaskCard(statePath, cardId, (current) => {
183
+ if (!current || current.runId !== card.runId || current.refSource === 'manual') return undefined;
184
+ const goal = current.goalId === undefined && evidence.goalId && samePr(current) ? evidence.goalId : undefined;
185
+ const pr = current.pr === undefined && evidence.pr ? evidence.pr : undefined;
186
+ if (!goal && !pr) return undefined;
187
+ wrote.pr = pr !== undefined;
188
+ return {
189
+ ...current,
190
+ ...(goal ? { goalId: goal } : {}),
191
+ ...(pr ? { pr, ...(evidence.childRunId && !current.runChildId ? { runChildId: evidence.childRunId } : {}) } : {}),
192
+ history: [
193
+ ...(current.history ?? []),
194
+ ...(goal ? [{ at, event: 'goal-bound', detail: goal, runId: current.runId! }] : []),
195
+ ...(pr ? [{ at, event: 'pr-bound', detail: pr.url ?? `#${pr.number}`, runId: current.runId!, pr: pr.number }] : []),
196
+ ],
197
+ };
198
+ });
199
+ if (wrote.pr && typeof next?.pr === 'object') {
200
+ try { debug.log('task-agent', 'card-pr-bound', { card: cardId, runId: next.runId ?? null, pr: next.pr.number, url: next.pr.url ?? null, childRunId: next.runChildId ?? null }); } catch { /* fail-soft */ }
201
+ }
202
+ return { card: next, evidence };
203
+ }
@@ -0,0 +1,126 @@
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ import { extname, isAbsolute, relative, resolve, sep } from 'node:path';
3
+ import type { Database } from 'bun:sqlite';
4
+ import type { DecisionLedger } from '../decisions/decision-ledger.js';
5
+ import { getLastStatus } from '../domains/schedule-registry.js';
6
+
7
+ export type CompletionEvidenceKind = 'research-report' | 'artifact' | 'content' | 'ops-action' | 'watch-brief' | 'decision-support';
8
+ export interface CompletionEvidenceDeps {
9
+ ledger?: Pick<DecisionLedger, 'list'>;
10
+ schedulesDb?: Database;
11
+ }
12
+
13
+ /** A missing record is negative evidence; a failed read is explicitly unmeasured. No store is opened or mutated here. */
14
+ export function checkCompletionEvidence(
15
+ kind: CompletionEvidenceKind,
16
+ input: { dir: string; ref: string },
17
+ deps: CompletionEvidenceDeps = {},
18
+ ): { ok: boolean; ref: string; missing: string[] } {
19
+ const missing: string[] = [];
20
+ const add = (item: string) => { if (!missing.includes(item)) missing.push(item); };
21
+ const pathFor = (ref: string): string | null => {
22
+ const root = resolve(input.dir);
23
+ const path = resolve(root, ref);
24
+ const rel = relative(root, path);
25
+ return !isAbsolute(ref) && rel !== '..' && !rel.startsWith(`..${sep}`) ? path : null;
26
+ };
27
+ const read = (path: string): string | null => {
28
+ try { return readFileSync(path, 'utf8'); }
29
+ catch (error) {
30
+ add((error as NodeJS.ErrnoException).code === 'ENOENT' ? '파일' : '못 쟀다: 파일 읽기');
31
+ return null;
32
+ }
33
+ };
34
+ const file = () => {
35
+ const path = pathFor(input.ref);
36
+ if (!path) add('못 쟀다: 경로');
37
+ return path;
38
+ };
39
+
40
+ switch (kind) {
41
+ case 'research-report': {
42
+ const path = file();
43
+ if (!path) break;
44
+ if (extname(path).toLowerCase() !== '.md') { add('md 파일'); break; }
45
+ const text = read(path);
46
+ if (text === null) break;
47
+ const sources = new Set([...text.matchAll(/https?:\/\/[^\s<>"')\]]+/g)].map((match) => match[0]!.replace(/[.,;]+$/, '')));
48
+ if (sources.size < 2) add('출처');
49
+ if (!/^#{1,6}\s*반대 근거\s*$/m.test(text)) add('반대 근거');
50
+ break;
51
+ }
52
+ case 'artifact':
53
+ case 'content': {
54
+ const path = file();
55
+ if (!path) break;
56
+ try {
57
+ const stat = statSync(path);
58
+ if (!stat.isFile() || stat.size === 0) add('파일 크기');
59
+ } catch (error) { add((error as NodeJS.ErrnoException).code === 'ENOENT' ? '파일' : '못 쟀다: 파일 크기'); }
60
+ const stem = path.slice(0, path.length - extname(path).length);
61
+ let preview = false;
62
+ for (const ext of ['.png', '.pdf', '.html']) {
63
+ const candidate = `${stem}${ext}`;
64
+ if (candidate === path) continue;
65
+ try {
66
+ const stat = statSync(candidate);
67
+ if (stat.isFile() && stat.size > 0) preview = true;
68
+ } catch (error) {
69
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') add('못 쟀다: 미리보기 읽기');
70
+ }
71
+ }
72
+ if (extname(path).toLowerCase() === '.md' && !missing.includes('파일')) {
73
+ const text = read(path);
74
+ if (text !== null && /^#{1,6}\s+\S.*$/m.test(text)) preview = true;
75
+ }
76
+ if (!preview && !missing.some((item) => item.startsWith('못 쟀다: 미리보기') || item === '못 쟀다: 파일 읽기')) add('미리보기');
77
+ break;
78
+ }
79
+ case 'ops-action': {
80
+ const path = pathFor('execution.jsonl');
81
+ if (!path) { add('못 쟀다: 경로'); break; }
82
+ let text: string;
83
+ try { text = readFileSync(path, 'utf8'); }
84
+ catch (error) {
85
+ add((error as NodeJS.ErrnoException).code === 'ENOENT' ? '실행 기록' : '못 쟀다: 실행 기록 읽기');
86
+ break;
87
+ }
88
+ let execution = false;
89
+ let readback = false;
90
+ for (const line of text.split('\n').filter((line) => line.trim())) {
91
+ let record: unknown;
92
+ try { record = JSON.parse(line); }
93
+ catch { add('못 쟀다: 실행 기록 해석'); continue; }
94
+ if (!record || typeof record !== 'object') { add('못 쟀다: 실행 기록 해석'); continue; }
95
+ const entry = record as { id?: unknown; type?: unknown };
96
+ if (entry.id !== input.ref) continue;
97
+ if (entry.type === 'execution') execution = true;
98
+ if (entry.type === 'readback') readback = true;
99
+ }
100
+ if (!missing.includes('못 쟀다: 실행 기록 해석')) {
101
+ if (!execution) add('실행 기록');
102
+ if (!readback) add('되읽기');
103
+ }
104
+ break;
105
+ }
106
+ case 'watch-brief': {
107
+ if (!deps.schedulesDb) { add('못 쟀다: 스케줄 DB'); break; }
108
+ try {
109
+ const row = deps.schedulesDb.query('SELECT id FROM schedule_registry WHERE id = ?').get(input.ref);
110
+ if (!row) add('일정');
111
+ else if (getLastStatus(deps.schedulesDb, input.ref) !== 'ok') add('도착');
112
+ } catch { add('못 쟀다: 스케줄 DB 읽기'); }
113
+ break;
114
+ }
115
+ case 'decision-support': {
116
+ if (!deps.ledger) { add('못 쟀다: 결정 원장'); break; }
117
+ try {
118
+ if (!deps.ledger.list({ status: 'all' }).some((entry) => entry.id === input.ref)) add('결정 기록');
119
+ } catch { add('못 쟀다: 결정 원장 읽기'); }
120
+ break;
121
+ }
122
+ default:
123
+ add('못 쟀다: 증거 종류');
124
+ }
125
+ return { ok: missing.length === 0, ref: input.ref, missing };
126
+ }
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { SupervisorStopReason } from '../self-dev/run-supervisor.js';
8
8
  import type { NextActionKind, RetryVariant } from './actions.js';
9
+ import type { CompletionKind } from './task-hand.js';
9
10
 
10
11
  export type TaskMove =
11
12
  | 'propose-green'
@@ -27,6 +28,11 @@ export interface TaskJudgeInput {
27
28
  mustFix?: readonly string[];
28
29
  /** 수마다 누적 실패 수. */
29
30
  failures?: Partial<Record<TaskMove, number>>;
31
+ deliveryEvidence?: { ok: boolean; files: readonly string[]; reason: string };
32
+ /** 과제 종결 종류(§A4b③) — 비거나 code-pr 이면 종전처럼 PR 병합이 종결이다. */
33
+ completion?: CompletionKind;
34
+ /** code-pr 밖 종류의 확인 증거(`completion-evidence.ts` 판독). 비면 «아직 못 받았다» — 통과로 읽지 않는다. */
35
+ evidence?: { ok: boolean; ref: string };
30
36
  }
31
37
 
32
38
  export interface TaskJudgement {
@@ -64,7 +70,19 @@ const EXECUTOR_VARIANT: Partial<Record<TaskMove, RetryVariant>> = {
64
70
  };
65
71
 
66
72
  function base(input: TaskJudgeInput): Omit<TaskJudgement, 'executorKind'> {
73
+ if (input.deliveryEvidence) return input.deliveryEvidence.ok
74
+ ? { move: 'propose-green', stage: 'closing', reason: `전달 근거 확인: ${input.deliveryEvidence.files.join(', ')}` }
75
+ : { move: 'wait', stage: 'waiting', reason: input.deliveryEvidence.reason };
67
76
  const stop = input.stopReason;
77
+ const kind = input.completion;
78
+ if (kind !== undefined && kind !== 'code-pr') {
79
+ const ref = input.evidence?.ref?.trim() ?? '';
80
+ // green 은 추적 가능한 근거(비지 않은 ref)가 있을 때만 — ok 인데 ref 가 비면 «증거 없음»으로 읽는다.
81
+ if (input.evidence?.ok === true && ref) return { move: 'propose-green', stage: 'closing', reason: `확인 증거 ${ref} 근거로 칸 green 제안 — ${kind}` };
82
+ if (input.evidence?.ok === false) return { move: 'narrow-relaunch', stage: 'repairing', reason: `확인 증거 미충족(${ref || '근거 없음'}) — ${kind} · 범위를 좁혀 재발사` };
83
+ // 증거가 아직 없다 — 완주·종료 미관측이면 증거를 기다린다(PR 병합은 이 종류의 종결이 아니다). 그 밖의 종료 어휘는 아래 표 그대로.
84
+ if (stop === 'converged' || stop === undefined) return { move: 'wait', stage: 'observing', reason: `확인 증거 대기 — ${kind}${input.evidence?.ok === true ? ' (ref 비어 있음)' : ''}` };
85
+ }
68
86
  const merged = input.prState === 'MERGED' && input.pr !== undefined && input.pr !== '';
69
87
  if (merged) {
70
88
  return { move: 'propose-green', stage: 'closing', reason: `병합된 PR ${input.pr} 근거로 칸 green 제안` };
@@ -0,0 +1,176 @@
1
+ /**
2
+ * TA-JUDGE-LIVE-SAFE — 판단부의 «안전한 수» 둘만 실제로 한다(10-07 승인 · 그 밖의 수는 그림자 그대로).
3
+ *
4
+ * - `review`: 수확 가능 런의 PR 에 elanous 리뷰를 «한 번» 요청한다 — PR 머리(sha)마다 한 번(카드 `reviewRequests` 이력).
5
+ * 요청만 한다: 결과로 착지·재발사로 잇지 않는다(실행부 `executeNextAction` 의 review→land 사슬을 타지 않는다).
6
+ * - `propose-green`: 카드에 green «제안»(`greenProposal`)만 적는다 — 체크리스트는 손대지 않는다.
7
+ * - 설정 `taskAgent.liveMoves`(config > env `ELANOUS_TASK_AGENT_LIVE_MOVES` > 없음). 모르는 항목은 경고하고 버린다.
8
+ * - 관측: 실행한 수마다 `debug.log('task-agent', 'live-move', {kind, card, ok, detail, live:true})`.
9
+ */
10
+ import { spawn, spawnSync } from 'node:child_process';
11
+ import { resolve } from 'node:path';
12
+ import { debug } from '../debug/log.js';
13
+ import type { DetachedSpawn } from '../cli/tasks-cli.js';
14
+ import { taskAgentStatePath, updateTaskCard, type TaskCard } from './task-hand.js';
15
+
16
+ export const TASK_AGENT_LIVE_MOVES = ['review', 'propose-green'] as const;
17
+ export type TaskAgentLiveMove = typeof TASK_AGENT_LIVE_MOVES[number];
18
+ export const TASK_AGENT_LIVE_MOVES_ENV = 'ELANOUS_TASK_AGENT_LIVE_MOVES';
19
+
20
+ export interface LiveMovesResolution { moves: ReadonlySet<TaskAgentLiveMove>; source: 'config' | 'env' | 'default'; ignored: string[] }
21
+
22
+ /** 설정 > 환경 > 없음. 모르는 항목은 허용으로 읽지 않는다 — 경고 관측만 남기고 버린다. */
23
+ export function resolveTaskAgentLiveMoves(
24
+ configured: unknown,
25
+ env: Record<string, string | undefined> = process.env,
26
+ log: (category: string, event: string, data: Record<string, unknown>) => void = (c, e, d) => debug.log(c, e, d, { level: 'warn' }),
27
+ ): LiveMovesResolution {
28
+ let raw: unknown[];
29
+ let source: LiveMovesResolution['source'];
30
+ if (configured !== undefined) { raw = Array.isArray(configured) ? configured : [configured]; source = 'config'; }
31
+ else if (env[TASK_AGENT_LIVE_MOVES_ENV]?.trim()) { raw = env[TASK_AGENT_LIVE_MOVES_ENV]!.split(',').map((entry) => entry.trim()).filter(Boolean); source = 'env'; }
32
+ else return { moves: new Set(), source: 'default', ignored: [] };
33
+ const moves = new Set<TaskAgentLiveMove>();
34
+ const ignored: string[] = [];
35
+ for (const entry of raw) {
36
+ if (typeof entry === 'string' && (TASK_AGENT_LIVE_MOVES as readonly string[]).includes(entry)) moves.add(entry as TaskAgentLiveMove);
37
+ else ignored.push(typeof entry === 'string' ? entry : JSON.stringify(entry));
38
+ }
39
+ if (ignored.length) {
40
+ try { log('task-agent', 'live-moves-ignored', { ignored, source, allowed: [...TASK_AGENT_LIVE_MOVES] }); } catch { /* fail-soft */ }
41
+ }
42
+ return { moves, source, ignored };
43
+ }
44
+
45
+ /** 운영 설정에서 읽는다 — 읽기 실패는 «없음»(순수 그림자)이다. */
46
+ export async function configuredTaskAgentLiveMoves(): Promise<ReadonlySet<TaskAgentLiveMove>> {
47
+ try {
48
+ const { getUserConfig } = await import('../user-config.js');
49
+ return resolveTaskAgentLiveMoves(getUserConfig().taskAgent?.liveMoves).moves;
50
+ } catch { return new Set(); }
51
+ }
52
+
53
+ export interface LiveMoveDeps {
54
+ statePath?: string;
55
+ now?: () => Date;
56
+ /** PR 머리 sha·상태 — 시험은 주입한다. 기본은 `gh pr view`(작업 트리 cwd). */
57
+ prHead?: (pr: number, cwd?: string) => Promise<{ head: string; state: string } | null>;
58
+ /** 리뷰 요청(떼어 띄운다) — 시험은 반드시 주입한다. */
59
+ requestReview?: (pr: number, intent: string, cwd?: string) => Promise<void>;
60
+ log?: (category: string, event: string, data: Record<string, unknown>) => void;
61
+ }
62
+
63
+ /** `executed` = 부작용을 실제로 냈다(리뷰를 띄웠다 · 제안을 적었다). ok 이지만 이미 한 수(같은 머리·이미 제안)면 false. */
64
+ export interface LiveMoveResult { kind: TaskAgentLiveMove; card: string | null; ok: boolean; executed: boolean; detail: string }
65
+
66
+ /** 받은 환경 그대로 먼저 묻고, 실패하면 프록시만 뺀 환경으로 한 번 더(이 저장소의 gh 운영 관행 — 프록시 경유가 gh 를 깨는 호스트). */
67
+ export type GhRun = (args: string[], options: { env: NodeJS.ProcessEnv; cwd?: string }) => { status: number | null; stdout: string };
68
+ const runGh: GhRun = (args, options) => spawnSync('gh', args, { encoding: 'utf8', timeout: 30_000, env: options.env, ...(options.cwd ? { cwd: options.cwd } : {}) });
69
+
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 } : {}) });
72
+ let out = view(process.env);
73
+ if (out.status !== 0) {
74
+ const noProxy = { ...process.env };
75
+ for (const key of ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy']) delete noProxy[key];
76
+ out = view(noProxy);
77
+ }
78
+ if (out.status !== 0) return null;
79
+ try {
80
+ const value = JSON.parse(out.stdout) as { headRefOid?: unknown; state?: unknown };
81
+ 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;
83
+ } catch { return null; }
84
+ }
85
+
86
+ /** 리뷰 요청 argv — 진입점 · 같은 우주(`--config-dir`) · `self review <PR> --json --intent`. */
87
+ export function liveReviewArgs(entry: string, configDir: string, pr: number, intent: string): string[] {
88
+ return [resolve(entry), '--config-dir', configDir, 'self', 'review', String(pr), '--json', '--intent', intent];
89
+ }
90
+
91
+ /**
92
+ * 지금 도는 진입점으로 같은 우주의 리뷰를 떼어 띄운다 — 기다리지 않는다(결과는 self review 자기 관측에 남는다).
93
+ * `opts` 는 시험 몫(spawn·진입점·우주) — 기본은 실물.
94
+ */
95
+ export async function defaultRequestReview(pr: number, intent: string, cwd?: string, opts: { spawn?: DetachedSpawn; entry?: string; configDir?: string } = {}): Promise<void> {
96
+ const { spawnDetachedConfirmed, ELANOUS_CLI_ENTRY } = await import('../cli/tasks-cli.js');
97
+ const { getElanousConfigDirOverride } = await import('../elanous-config-dir.js');
98
+ const { effectiveInstanceRoot } = await import('../instance/resolve.js');
99
+ const configDir = opts.configDir ?? getElanousConfigDirOverride() ?? effectiveInstanceRoot();
100
+ await spawnDetachedConfirmed(opts.spawn ?? (spawn as unknown as DetachedSpawn), process.execPath, liveReviewArgs(opts.entry ?? ELANOUS_CLI_ENTRY, configDir, pr, intent), undefined, cwd);
101
+ }
102
+
103
+ export function liveReviewIntent(card: TaskCard, runId: string | null, stopReason: string): string {
104
+ const goal = card.text.split('\n')[0]!.trim().slice(0, 300);
105
+ return [
106
+ `목표: ${goal}`,
107
+ `수용기준: 과제 카드 ${card.id} 원문 —\n${card.text.trim().slice(0, 4000)}`,
108
+ `직전 반영분: 런 ${runId ?? '-'} 이 ${stopReason} 로 멈춤 — 수확 가능`,
109
+ '의도적 범위 경계: task-agent live review 요청(TA-JUDGE-LIVE-SAFE) — 리뷰만 한다 · 착지·재발사는 이 요청의 결정이 아니다',
110
+ ].join('\n');
111
+ }
112
+
113
+ /** 실행한 수를 관측에 남긴다(`live-move`). */
114
+ function observe(deps: LiveMoveDeps, result: LiveMoveResult): LiveMoveResult {
115
+ try { (deps.log ?? ((c, e, d) => debug.log(c, e, d)))('task-agent', 'live-move', { ...result, live: true }); } catch { /* fail-soft */ }
116
+ return result;
117
+ }
118
+
119
+ /**
120
+ * 단위는 «관측한 머리»다 — `self review` 에 머리 고정 인자가 없어, 관측~리뷰 시작 사이에 머리가 바뀌면 새 머리가
121
+ * 다음 틱에 한 번 더 요청될 수 있다(초과는 많아야 한 번 · 리뷰는 읽기 전용이라 착지·재발사로 번지지 않는다).
122
+ */
123
+ /** `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> {
125
+ const kind = 'review' as const;
126
+ if (!card) return observe(deps, { kind, executed: false, card: null, ok: false, detail: 'no task card — stays shadow' });
127
+ 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}` });
134
+ const path = deps.statePath ?? taskAgentStatePath();
135
+ const at = (deps.now ?? (() => new Date()))().toISOString();
136
+ const claim = { claimed: false, missing: false };
137
+ updateTaskCard(path, card.id, (current) => {
138
+ if (!current) { claim.missing = true; return undefined; }
139
+ // 머리 «이력»으로 본다 — A→B→A 로 돌아와도 A 는 다시 요청하지 않는다.
140
+ if ((current.reviewRequests ?? []).some((entry) => entry.pr === pr && entry.head === head!.head)) return undefined;
141
+ claim.claimed = true;
142
+ return { ...current, reviewRequests: [...(current.reviewRequests ?? []), { pr, head: head!.head, at }] };
143
+ });
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)}` });
146
+ try {
147
+ await (deps.requestReview ?? defaultRequestReview)(pr, liveReviewIntent(card, ctx.runId, ctx.stopReason), ctx.cwd);
148
+ } catch (error) {
149
+ const reason = error instanceof Error ? error.message : String(error);
150
+ updateTaskCard(path, card.id, (current) => current?.reviewRequests?.some((entry) => entry.pr === pr && entry.head === head!.head && entry.at === at)
151
+ ? { ...current, reviewRequests: current.reviewRequests.map((entry) => entry.pr === pr && entry.head === head!.head && entry.at === at ? { ...entry, error: reason } : entry) }
152
+ : 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}` });
154
+ }
155
+ return observe(deps, { kind, executed: true, card: card.id, ok: true, detail: `review requested for #${pr} head ${head.head.slice(0, 12)}` });
156
+ }
157
+
158
+ /** `propose-green` — 비지 않은 근거가 있을 때만 카드에 제안을 적는다(한 번). 체크리스트는 쓰지 않는다. */
159
+ export function executeLiveGreenProposal(card: TaskCard | undefined, evidence: Record<string, string>, deps: LiveMoveDeps = {}): LiveMoveResult {
160
+ const kind = 'propose-green' as const;
161
+ if (!card) return observe(deps, { kind, executed: false, card: null, ok: false, detail: 'no task card — stays shadow' });
162
+ const refs = Object.fromEntries(Object.entries(evidence).filter(([, ref]) => typeof ref === 'string' && ref.trim()));
163
+ if (!Object.keys(refs).length) return observe(deps, { kind, executed: false, card: card.id, ok: false, detail: 'no evidence ref — not proposed' });
164
+ const path = deps.statePath ?? taskAgentStatePath();
165
+ const proposal = { at: (deps.now ?? (() => new Date()))().toISOString(), checklistId: card.checklistId ?? null, evidence: refs };
166
+ const wrote = { proposed: false, missing: false };
167
+ updateTaskCard(path, card.id, (current) => {
168
+ if (!current) { wrote.missing = true; return undefined; }
169
+ if (current.greenProposal) return undefined;
170
+ wrote.proposed = true;
171
+ return { ...current, greenProposal: proposal };
172
+ });
173
+ if (wrote.missing) return observe(deps, { kind, executed: false, card: card.id, ok: false, detail: 'task card not in state file' });
174
+ if (!wrote.proposed) return observe(deps, { kind, executed: false, card: card.id, ok: true, detail: 'already proposed' });
175
+ return observe(deps, { kind, executed: true, card: card.id, ok: true, detail: `green proposed (${Object.values(refs).join(', ')}) · checklist untouched` });
176
+ }
@@ -12,7 +12,9 @@
12
12
  import { randomBytes } from 'node:crypto';
13
13
  import { debug } from '../debug/log.js';
14
14
  import type { SelfDevDecomposition } from '../self-dev/decompose.js';
15
+ import { refreshCardRunBinding, type CardRunEvidenceDeps } from './card-evidence.js';
15
16
  import {
17
+ cardPrNumber,
16
18
  handTask,
17
19
  nextMoveFor,
18
20
  readTaskAgentState,
@@ -245,11 +247,19 @@ async function defaultEvidenceReader(ref: number | string): Promise<'merged' | '
245
247
  return predecessorState(ref);
246
248
  }
247
249
 
248
- /** 조각 카드마다 근거를 모은다 — PR 번호가 있으면 그것, 없으면 골 id. 읽기 실패는 pending(미착지). */
250
+ /**
251
+ * 조각의 착지 근거 하나 — PR 번호 › 골 id. 수동(`--pr`/`--goal`)이든 런 원장 묶기든 같은 칸이다(TA-CARD-RUN-LINK) —
252
+ * 수동이 적히면 `refSource: 'manual'` 이라 묶기가 다시 채우지 않는다(수동이 덮는다).
253
+ */
254
+ export function pieceRef(piece: Pick<TaskCard, 'pr' | 'goalId'>): number | string | undefined {
255
+ return cardPrNumber(piece) ?? piece.goalId;
256
+ }
257
+
258
+ /** 조각 카드마다 근거를 모은다 — `pieceRef` 순. 읽기 실패는 pending(미착지). */
249
259
  export async function collectPieceEvidence(pieces: readonly TaskCard[], read: PieceEvidenceReader = defaultEvidenceReader): Promise<Record<string, PieceEvidence>> {
250
260
  const out: Record<string, PieceEvidence> = {};
251
261
  for (const piece of pieces) {
252
- const ref = piece.pr ?? piece.goalId;
262
+ const ref = pieceRef(piece);
253
263
  if (ref === undefined) { out[piece.id] = 'no-ref'; continue; }
254
264
  let state: 'merged' | 'waiting' | 'blocked';
255
265
  try { state = await read(ref); } catch { state = 'waiting'; }
@@ -275,8 +285,9 @@ export function recordPieceRef(pieceId: string, ref: { pr?: number; goalId?: str
275
285
  // green 제안 뒤 근거를 바꾸면 제안이 바뀐 근거와 어긋난다 — 제안된 미션의 조각 근거는 잠근다.
276
286
  if (tasks[current.mission]?.greenProposal) { refused.reason = `미션 ${current.mission} 은 이미 green 제안됐다 — 근거를 바꾸지 않는다`; return undefined; }
277
287
  // 근거는 하나만 — 새 근거가 옛 근거를 «교체»한다(정정한 골 id 뒤에 옛 PR 이 남아 우선되지 않게).
278
- const { pr: _oldPr, goalId: _oldGoal, ...rest } = current;
279
- return { ...rest, ...(ref.pr !== undefined ? { pr: ref.pr } : { goalId: ref.goalId! }) };
288
+ // 런 묶기가 적은 자식 런도 함께 지운다 — 수동 근거를 옛 Pod 자식 런의 결과처럼 보이지 않게.
289
+ const { pr: _oldPr, goalId: _oldGoal, runChildId: _oldChild, ...rest } = current;
290
+ return { ...rest, ...(ref.pr !== undefined ? { pr: ref.pr } : { goalId: ref.goalId! }), refSource: 'manual' as const };
280
291
  });
281
292
  if (refused.reason || !card) throw new Error(refused.reason ?? `미션 조각 카드가 아니다: ${pieceId}`);
282
293
  try { debug.log('task-agent', 'mission-piece-ref', { pieceId, missionId: card.mission ?? null, pr: ref.pr ?? null, goalId: ref.goalId ?? null }); } catch { /* fail-soft */ }
@@ -295,6 +306,8 @@ export interface AdvanceMissionOptions {
295
306
  statePath?: string;
296
307
  launcher?: TaskLauncher;
297
308
  readEvidence?: PieceEvidenceReader;
309
+ /** 런 id 가 있는 조각의 런 근거(PR·골 id) 읽기(시험 주입) — `false` 면 묶지 않는다. */
310
+ runEvidence?: CardRunEvidenceDeps | false;
298
311
  now?: () => Date;
299
312
  }
300
313
 
@@ -344,6 +357,19 @@ async function advanceMissionOnce(missionId: string, opts: AdvanceMissionOptions
344
357
  const clock = opts.now ?? (() => new Date());
345
358
  // live 인데 발사기가 없으면 claim 을 적기 «전»에 멈춘다 — live 표지가 발사 없이 남아 영영 안 넘겨지지 않게.
346
359
  if (mode === 'live' && !opts.launcher) throw new Error('--live 인데 launcher 가 없다');
360
+ const before = readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(path).tasks ?? {};
361
+ if (!before[missionId]?.pieces) throw new Error(`미션 카드가 아니다: ${missionId}`);
362
+ // 런 id 가 있고 PR·골 id 중 빈 칸이 있는 조각 — 원장에서 묶어 둔다(제안 끝난 미션은 건드리지 않는다 · 실패는 근거 없음).
363
+ if (opts.runEvidence !== false && !before[missionId]!.greenProposal) {
364
+ for (const id of before[missionId]!.pieces!) {
365
+ const piece = before[id];
366
+ if (!piece?.runId || piece.refSource === 'manual' || (piece.pr !== undefined && piece.goalId !== undefined)) continue;
367
+ try { await refreshCardRunBinding(id, path, opts.runEvidence ?? {}); } catch (error) {
368
+ // 근거 없음으로 본다(미착지) — 원장 부재와 갈리게 실패는 남긴다.
369
+ try { debug.log('task-agent', 'card-bind-failed', { card: id, runId: piece.runId, via: 'advance', error: error instanceof Error ? error.message : String(error) }); } catch { /* fail-soft */ }
370
+ }
371
+ }
372
+ }
347
373
  const all = readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(path).tasks ?? {};
348
374
  const mission = all[missionId];
349
375
  if (!mission?.pieces) throw new Error(`미션 카드가 아니다: ${missionId}`);
@@ -398,13 +424,13 @@ async function advanceMissionOnce(missionId: string, opts: AdvanceMissionOptions
398
424
  const proposal = {
399
425
  at: clock().toISOString(),
400
426
  checklistId: mission.checklistId ?? null,
401
- evidence: Object.fromEntries(pieces.map((piece) => [piece.id, piece.pr !== undefined ? `#${piece.pr}` : `goal:${piece.goalId}`])),
427
+ evidence: Object.fromEntries(pieces.map((piece) => { const ref = pieceRef(piece); return [piece.id, typeof ref === 'number' ? `#${ref}` : `goal:${ref}`]; })),
402
428
  };
403
429
  const wrote = { proposed: false, stale: false };
404
430
  updateTaskCard(path, missionId, (current, tasks) => {
405
431
  if (!current || current.greenProposal) return undefined;
406
432
  // 조회에 쓴 근거가 잠금 안에서도 그대로일 때만 — 조회 도중 근거가 바뀌었으면 다음 호출이 다시 본다.
407
- if (pieces.some((piece) => tasks[piece.id]?.pr !== piece.pr || tasks[piece.id]?.goalId !== piece.goalId)) { wrote.stale = true; return undefined; }
433
+ if (pieces.some((piece) => { const now = tasks[piece.id]; return (now ? cardPrNumber(now) : undefined) !== cardPrNumber(piece) || now?.goalId !== piece.goalId; })) { wrote.stale = true; return undefined; }
408
434
  wrote.proposed = true;
409
435
  return { ...current, greenProposal: proposal };
410
436
  });