elanous 0.2.18 → 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 (350) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1307-30dff22c9e1f3073.js → 1307-77fad80cf54477c6.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/7748-6e51bfe927019f5c.js +25 -0
  14. package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/a9f2b54d-33c0689b70ed8cf3.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d92f7ee3994e84c7.js → page-c855c24de7db74fe.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-4733c967b56a4277.js → page-85b316bb4235980b.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/board/{page-d22fd849cc22f104.js → page-9b2b26a33cb2cd42.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-bb742f995c6769e7.js → page-c9955fde67b1bcb5.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/bots/{page-02b073a2ac793b83.js → page-7a852956c74015a3.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/chat/{page-8a9b3594bc23289b.js → page-4ba4abe096b6c2a8.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/control/{page-308111abadf35670.js → page-f5d56ff5f45ad164.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-5d97b2901e0d1ea6.js → page-7c058069c5e7fa63.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/exec/{page-8c3d8e63ae2e1c98.js → page-466bf0ee16ad1432.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/field/{page-9b72a07d424701c6.js → page-a23bc71713bcc08f.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/inside/page-ed4a6f799f005d09.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/intake/{page-a572f58cd3ca5864.js → page-9cd565005b93aca6.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/live/{page-be5ea965a07fbc41.js → page-474cb5888ea0e8f3.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/market/{page-fa244bd146a00ae8.js → page-9ca21f9ad3208a61.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-44d56d6391965a6a.js → page-f9ccb1b1adbea14d.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/morning/{page-83a700d4c754d802.js → page-ec102c9b8b691e37.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/{not-found-7b3170f374f77b9d.js → not-found-a9c2cfa8dda922a5.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1467047104141297.js → page-db03ae3589a81c9e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-102310930791197a.js → page-75bffecc9aa1004e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/settings/{page-6ed0b73bae8351d8.js → page-31d5b875760bb62d.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/setup/{page-43336055451f6e37.js → page-68793c6e10331d66.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/share/{page-3cc47dca0922ab9a.js → page-46d5201d2207a511.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-a77ee355a2304ff0.js → page-d81c07a7b4f618ae.js} +1 -1
  55. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-b17cd46cf0e9b8cf.js → page-be1c4e8697f22360.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/trace/{page-7bf54c9bb9274af8.js → page-0fd895fab9b62c1b.js} +1 -1
  59. package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-134da468538baf48.js → page-34882c84f84021c5.js} +1 -1
  63. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/{webpack-c2782b330447ffb6.js → webpack-c260571513bf43c2.js} +1 -1
  65. package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
  66. package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
  67. package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
  68. package/apps/pwa/out/approvals/index.html +1 -1
  69. package/apps/pwa/out/approvals/index.txt +14 -14
  70. package/apps/pwa/out/autopilot/index.html +1 -1
  71. package/apps/pwa/out/autopilot/index.txt +14 -14
  72. package/apps/pwa/out/board/index.html +1 -1
  73. package/apps/pwa/out/board/index.txt +14 -14
  74. package/apps/pwa/out/botlab/index.html +1 -1
  75. package/apps/pwa/out/botlab/index.txt +14 -14
  76. package/apps/pwa/out/bots/index.html +1 -1
  77. package/apps/pwa/out/bots/index.txt +14 -14
  78. package/apps/pwa/out/ceo/index.html +1 -1
  79. package/apps/pwa/out/ceo/index.txt +14 -14
  80. package/apps/pwa/out/chat/index.html +1 -1
  81. package/apps/pwa/out/chat/index.txt +14 -14
  82. package/apps/pwa/out/consult/index.html +1 -1
  83. package/apps/pwa/out/consult/index.txt +14 -14
  84. package/apps/pwa/out/control/index.html +1 -1
  85. package/apps/pwa/out/control/index.txt +14 -14
  86. package/apps/pwa/out/decisions/index.html +1 -1
  87. package/apps/pwa/out/decisions/index.txt +14 -14
  88. package/apps/pwa/out/design-check/index.html +1 -1
  89. package/apps/pwa/out/design-check/index.txt +14 -14
  90. package/apps/pwa/out/editor/index.html +1 -1
  91. package/apps/pwa/out/editor/index.txt +16 -15
  92. package/apps/pwa/out/exec/index.html +1 -1
  93. package/apps/pwa/out/exec/index.txt +14 -14
  94. package/apps/pwa/out/field/index.html +1 -1
  95. package/apps/pwa/out/field/index.txt +14 -14
  96. package/apps/pwa/out/healthz +1 -1
  97. package/apps/pwa/out/index.html +1 -1
  98. package/apps/pwa/out/index.txt +14 -14
  99. package/apps/pwa/out/inside/index.html +1 -1
  100. package/apps/pwa/out/inside/index.txt +16 -15
  101. package/apps/pwa/out/intake/index.html +1 -1
  102. package/apps/pwa/out/intake/index.txt +14 -14
  103. package/apps/pwa/out/live/index.html +1 -1
  104. package/apps/pwa/out/live/index.txt +14 -14
  105. package/apps/pwa/out/loops/index.html +1 -1
  106. package/apps/pwa/out/loops/index.txt +24 -23
  107. package/apps/pwa/out/market/index.html +1 -1
  108. package/apps/pwa/out/market/index.txt +14 -14
  109. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  110. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  111. package/apps/pwa/out/morning/index.html +1 -1
  112. package/apps/pwa/out/morning/index.txt +14 -14
  113. package/apps/pwa/out/observatory/index.html +1 -1
  114. package/apps/pwa/out/observatory/index.txt +14 -14
  115. package/apps/pwa/out/ops/checklist/index.html +1 -1
  116. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  117. package/apps/pwa/out/ops/release/index.html +1 -1
  118. package/apps/pwa/out/ops/release/index.txt +14 -14
  119. package/apps/pwa/out/ops/seats/index.html +1 -1
  120. package/apps/pwa/out/ops/seats/index.txt +14 -14
  121. package/apps/pwa/out/outputs/index.html +1 -1
  122. package/apps/pwa/out/outputs/index.txt +14 -14
  123. package/apps/pwa/out/reflection/index.html +1 -1
  124. package/apps/pwa/out/reflection/index.txt +15 -15
  125. package/apps/pwa/out/scheduler/index.html +1 -1
  126. package/apps/pwa/out/scheduler/index.txt +14 -14
  127. package/apps/pwa/out/sessions/index.html +1 -1
  128. package/apps/pwa/out/sessions/index.txt +14 -14
  129. package/apps/pwa/out/settings/devices/index.html +1 -1
  130. package/apps/pwa/out/settings/devices/index.txt +14 -14
  131. package/apps/pwa/out/settings/index.html +1 -1
  132. package/apps/pwa/out/settings/index.txt +14 -14
  133. package/apps/pwa/out/setup/done/index.html +1 -1
  134. package/apps/pwa/out/setup/done/index.txt +14 -14
  135. package/apps/pwa/out/setup/index.html +1 -1
  136. package/apps/pwa/out/setup/index.txt +14 -14
  137. package/apps/pwa/out/share/index.html +1 -1
  138. package/apps/pwa/out/share/index.txt +14 -14
  139. package/apps/pwa/out/showroom/index.html +1 -1
  140. package/apps/pwa/out/showroom/index.txt +14 -14
  141. package/apps/pwa/out/tasks/index.html +1 -1
  142. package/apps/pwa/out/tasks/index.txt +14 -14
  143. package/apps/pwa/out/term/index.html +1 -1
  144. package/apps/pwa/out/term/index.txt +14 -14
  145. package/apps/pwa/out/today/index.html +1 -1
  146. package/apps/pwa/out/today/index.txt +14 -14
  147. package/apps/pwa/out/trace/index.html +1 -1
  148. package/apps/pwa/out/trace/index.txt +14 -14
  149. package/apps/pwa/out/vault/index.html +1 -1
  150. package/apps/pwa/out/vault/index.txt +14 -14
  151. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  152. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  153. package/apps/pwa/out/workflows/index.html +1 -1
  154. package/apps/pwa/out/workflows/index.txt +14 -14
  155. package/apps/pwa/out/workspace/index.html +1 -1
  156. package/apps/pwa/out/workspace/index.txt +14 -14
  157. package/apps/pwa/out/worktrees/index.html +1 -1
  158. package/apps/pwa/out/worktrees/index.txt +14 -14
  159. package/graphs/demo/editor-recipes.yaml +22 -0
  160. package/graphs/ops/guardian.yaml +26 -0
  161. package/graphs/ops/recipes.yaml +3 -0
  162. package/graphs/retro/daily.yaml +25 -0
  163. package/graphs/retro/recipes.yaml +24 -0
  164. package/package.json +1 -1
  165. package/scripts/ask-marker-check.ts +3 -1
  166. package/scripts/coord-channel-watch.sh +2 -2
  167. package/scripts/discord-seat-probe.ts +3 -1
  168. package/scripts/land-global-checks.ts +89 -0
  169. package/scripts/lib/pch-regress-map.ts +24 -0
  170. package/scripts/morning-report.ts +18 -4
  171. package/scripts/pch-regress.ts +88 -0
  172. package/scripts/release-loop/gate-diff.ts +18 -0
  173. package/scripts/release-loop/gate-node.ts +628 -65
  174. package/scripts/release-loop/gate-partial.ts +106 -0
  175. package/scripts/release-loop/gate-timing.ts +229 -0
  176. package/scripts/release-loop/preflight.ts +6 -4
  177. package/scripts/release-loop/publish-window.ts +141 -0
  178. package/scripts/release-loop/release-docs.ts +1 -4
  179. package/scripts/release-loop/release-universe.ts +38 -0
  180. package/scripts/release-loop/resume-release.ts +67 -5
  181. package/scripts/release-loop/shard-plan.ts +65 -0
  182. package/scripts/release-loop/unattended-release.ts +12 -2
  183. package/scripts/release-story/draft.ts +5 -1
  184. package/scripts/retro/shadow-compare.ts +71 -0
  185. package/scripts/rhythm/daily.ts +6 -1
  186. package/scripts/video/scene-table.ts +159 -0
  187. package/src/acp/core-turn-bridge.ts +39 -2
  188. package/src/agent-mission/driver.ts +34 -6
  189. package/src/autopilot/build/llm-conflict-merge.ts +132 -7
  190. package/src/autopilot/build/merge-intent.ts +135 -7
  191. package/src/away/presence.ts +40 -0
  192. package/src/away/release-watch.ts +272 -0
  193. package/src/chat/index.ts +2 -1
  194. package/src/chat/short-question.ts +3 -40
  195. package/src/cli/away-cli.ts +68 -0
  196. package/src/cli/config-drift.ts +513 -0
  197. package/src/cli/guardian-cli.ts +49 -0
  198. package/src/cli/intake-cli.ts +13 -7
  199. package/src/cli/log-event-names.ts +5 -0
  200. package/src/cli/pod-cli.ts +5 -1
  201. package/src/cli/pr-cli.ts +20 -0
  202. package/src/cli/pty-drive-cli.ts +14 -3
  203. package/src/cli/release-cli.ts +52 -12
  204. package/src/cli/retro-cli.ts +78 -0
  205. package/src/cli/setup-cli.ts +31 -1
  206. package/src/cli/tasks-cli.ts +193 -14
  207. package/src/context-bus/context-now-public.ts +1 -1
  208. package/src/context-bus/context-now-surfaces.ts +16 -6
  209. package/src/context-bus/context-now.ts +97 -3
  210. package/src/core-turn/short-question.ts +100 -0
  211. package/src/dashboard/dashboard-status-lines.ts +24 -4
  212. package/src/dashboard/index.ts +50 -5
  213. package/src/dashboard/input/slash-executor.ts +6 -2
  214. package/src/dashboard/slash-runtime/dashboard-handlers.ts +68 -40
  215. package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
  216. package/src/dashboard/slash-runtime/help-from-registry.ts +62 -23
  217. package/src/dashboard/slash-runtime/help-groups.ts +44 -0
  218. package/src/dashboard/slash-runtime/model-choices.ts +125 -0
  219. package/src/domains/guardian-tick.ts +138 -0
  220. package/src/domains/outbound-alert.ts +44 -13
  221. package/src/domains/outbound-log-probe.ts +6 -1
  222. package/src/domains/schedule-registry.ts +23 -0
  223. package/src/domains/telegram-kind-route.ts +39 -0
  224. package/src/domains/watch-default-scheduler.ts +95 -0
  225. package/src/esc-abort-gate.ts +44 -0
  226. package/src/graph-runner/graph-run-api.ts +205 -0
  227. package/src/graph-runner/runner.ts +36 -6
  228. package/src/harness/harness-cli-command.ts +420 -51
  229. package/src/harness/harness-queue.ts +315 -25
  230. package/src/harness/harness-salvage-cli.ts +140 -0
  231. package/src/harness/harness-worktree-add.ts +68 -0
  232. package/src/harness/ledger-write-guard.ts +6 -16
  233. package/src/hq/fence-outcomes.ts +63 -0
  234. package/src/hq/fence-wrapper.ts +33 -0
  235. package/src/hq/hq.ts +17 -2
  236. package/src/index.ts +118 -6
  237. package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
  238. package/src/instance/test-write-guard.ts +200 -0
  239. package/src/intake-plane/digest.ts +39 -15
  240. package/src/intake-plane/harness-target.ts +4 -3
  241. package/src/intake-plane/intake-sources.ts +1 -0
  242. package/src/intake-plane/items.ts +1 -0
  243. package/src/loops/orchestrator/degradation.ts +57 -0
  244. package/src/loops/orchestrator/finish-rate.ts +33 -3
  245. package/src/loops/orchestrator/tick.ts +166 -45
  246. package/src/maturity/feature-maturity.ts +6 -4
  247. package/src/maturity/tui-slash-maturity.ts +1 -0
  248. package/src/mss/logging/log-store.ts +3 -0
  249. package/src/nexus/api/graph-kinds.ts +10 -4
  250. package/src/nexus/api/graphs-api.ts +178 -6
  251. package/src/nexus/api/http-server.ts +54 -8
  252. package/src/nexus/api/loop-edges.ts +292 -17
  253. package/src/nexus/api/ops-api.ts +112 -29
  254. package/src/nexus/api/ops-board-route.ts +48 -0
  255. package/src/nexus/index.ts +7 -0
  256. package/src/onboarding/entry-hints.ts +5 -0
  257. package/src/onboarding.ts +24 -6
  258. package/src/release-loop/checklist.ts +2 -0
  259. package/src/release-loop/feature-store.ts +68 -18
  260. package/src/release-loop/gate-shards.ts +158 -0
  261. package/src/release-loop/light-rc.ts +173 -0
  262. package/src/release-loop/placement.ts +51 -17
  263. package/src/release-loop/release-schedule.ts +6 -2
  264. package/src/release-loop/rubric.ts +57 -1
  265. package/src/retro/daily.ts +372 -0
  266. package/src/seat-loop/op-decide.ts +173 -0
  267. package/src/seat-loop/seat-loop.ts +60 -3
  268. package/src/seat-loop/seat-report.ts +58 -3
  269. package/src/self-dev/ask-launch-flow.ts +5 -4
  270. package/src/self-dev/ask-launch-io.ts +49 -1
  271. package/src/self-dev/ask-path-candidates.ts +40 -2
  272. package/src/self-dev/dev-cli.ts +36 -0
  273. package/src/self-dev/draft-sweep.ts +371 -16
  274. package/src/self-dev/draft-triage-rules.ts +19 -4
  275. package/src/self-dev/orchestrate.ts +1 -0
  276. package/src/self-dev/pr-preview.ts +247 -0
  277. package/src/self-dev/shard-run.ts +23 -7
  278. package/src/self-implement/blocked-draft-policy.ts +101 -0
  279. package/src/self-implement/execution-clarification.ts +11 -0
  280. package/src/self-implement/gate-cli.ts +29 -8
  281. package/src/self-implement/gate-env-retry.ts +83 -0
  282. package/src/self-implement/goal-author-clarification.ts +88 -13
  283. package/src/self-implement/goal-clarification-escalation.ts +2 -0
  284. package/src/self-implement/orchestrator.ts +220 -6
  285. package/src/self-implement/run-stop.ts +123 -0
  286. package/src/self-implement/run-ttl.ts +258 -0
  287. package/src/self-implement/salvage-branches.ts +254 -0
  288. package/src/self-implement/salvage-retention.ts +123 -0
  289. package/src/self-implement/seams.ts +4 -0
  290. package/src/self-implement/self-implement-cli.ts +2 -0
  291. package/src/self-implement/sibling-resync.ts +223 -0
  292. package/src/self-implement/stop-autoheal.ts +204 -0
  293. package/src/task-agent/actions.ts +63 -2
  294. package/src/task-agent/board.ts +132 -0
  295. package/src/task-agent/card-evidence.ts +292 -0
  296. package/src/task-agent/completion-evidence.ts +126 -0
  297. package/src/task-agent/judge.ts +25 -0
  298. package/src/task-agent/live-moves.ts +276 -0
  299. package/src/task-agent/mission.ts +33 -7
  300. package/src/task-agent/shadow.ts +192 -15
  301. package/src/task-agent/task-hand.ts +124 -17
  302. package/src/task-orchestrator/surfaces/pod-command-job.ts +82 -8
  303. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
  304. package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
  305. package/src/task-orchestrator/surfaces/pod-lease.ts +233 -21
  306. package/src/task-orchestrator/surfaces/pod-pool.ts +155 -34
  307. package/src/task-orchestrator/surfaces/self-implement-pod.ts +21 -5
  308. package/src/telegram-away-command.ts +38 -0
  309. package/src/telegram-commands.ts +52 -0
  310. package/src/telegram-report-log-probe.ts +21 -0
  311. package/src/telegram-report.ts +27 -3
  312. package/src/testing/real-cli-spawn.ts +106 -0
  313. package/src/tui-client/dashboard-session.ts +1 -0
  314. package/src/user-config.ts +190 -5
  315. package/src/version/packed-revision.json +1 -1
  316. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
  324. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
  348. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
  349. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +0 -0
  350. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
@@ -0,0 +1,276 @@
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 { statSync } from 'node:fs';
12
+ import { isAbsolute, resolve } from 'node:path';
13
+ import { debug } from '../debug/log.js';
14
+ import { findGitDir } from '../git-fs/locate.js';
15
+ import { getUserConfig } from '../user-config.js';
16
+ import type { DetachedSpawn } from '../cli/tasks-cli.js';
17
+ import { taskAgentStatePath, updateTaskCard, type TaskCard } from './task-hand.js';
18
+
19
+ export const TASK_AGENT_LIVE_MOVES = ['review', 'propose-green'] as const;
20
+ export type TaskAgentLiveMove = typeof TASK_AGENT_LIVE_MOVES[number];
21
+ export const TASK_AGENT_LIVE_MOVES_ENV = 'ELANOUS_TASK_AGENT_LIVE_MOVES';
22
+
23
+ export interface LiveMovesResolution { moves: ReadonlySet<TaskAgentLiveMove>; source: 'config' | 'env' | 'default'; ignored: string[] }
24
+
25
+ /** 설정 > 환경 > 없음. 모르는 항목은 허용으로 읽지 않는다 — 경고 관측만 남기고 버린다. */
26
+ export function resolveTaskAgentLiveMoves(
27
+ configured: unknown,
28
+ env: Record<string, string | undefined> = process.env,
29
+ log: (category: string, event: string, data: Record<string, unknown>) => void = (c, e, d) => debug.log(c, e, d, { level: 'warn' }),
30
+ ): LiveMovesResolution {
31
+ let raw: unknown[];
32
+ let source: LiveMovesResolution['source'];
33
+ if (configured !== undefined) { raw = Array.isArray(configured) ? configured : [configured]; source = 'config'; }
34
+ 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'; }
35
+ else return { moves: new Set(), source: 'default', ignored: [] };
36
+ const moves = new Set<TaskAgentLiveMove>();
37
+ const ignored: string[] = [];
38
+ for (const entry of raw) {
39
+ if (typeof entry === 'string' && (TASK_AGENT_LIVE_MOVES as readonly string[]).includes(entry)) moves.add(entry as TaskAgentLiveMove);
40
+ else ignored.push(typeof entry === 'string' ? entry : JSON.stringify(entry));
41
+ }
42
+ if (ignored.length) {
43
+ try { log('task-agent', 'live-moves-ignored', { ignored, source, allowed: [...TASK_AGENT_LIVE_MOVES] }); } catch { /* fail-soft */ }
44
+ }
45
+ return { moves, source, ignored };
46
+ }
47
+
48
+ /** 운영 설정에서 읽는다 — 읽기 실패는 «없음»(순수 그림자)이다. */
49
+ export async function configuredTaskAgentLiveMoves(): Promise<ReadonlySet<TaskAgentLiveMove>> {
50
+ try {
51
+ const { getUserConfig } = await import('../user-config.js');
52
+ return resolveTaskAgentLiveMoves(getUserConfig().taskAgent?.liveMoves).moves;
53
+ } catch { return new Set(); }
54
+ }
55
+
56
+ export interface LiveMoveDeps {
57
+ statePath?: string;
58
+ now?: () => Date;
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[];
63
+ /** 리뷰 요청(떼어 띄운다) — 시험은 반드시 주입한다. */
64
+ requestReview?: (pr: number, intent: string, cwd?: string) => Promise<void>;
65
+ log?: (category: string, event: string, data: Record<string, unknown>) => void;
66
+ }
67
+
68
+ /** `executed` = 부작용을 실제로 냈다(리뷰를 띄웠다 · 제안을 적었다). ok 이지만 이미 한 수(같은 머리·이미 제안)면 false. */
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
+ }
144
+
145
+ /** 받은 환경 그대로 먼저 묻고, 실패하면 프록시만 뺀 환경으로 한 번 더(이 저장소의 gh 운영 관행 — 프록시 경유가 gh 를 깨는 호스트). */
146
+ export type GhRun = (args: string[], options: { env: NodeJS.ProcessEnv; cwd?: string }) => { status: number | null; stdout: string };
147
+ const runGh: GhRun = (args, options) => spawnSync('gh', args, { encoding: 'utf8', timeout: 30_000, env: options.env, ...(options.cwd ? { cwd: options.cwd } : {}) });
148
+
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 } : {}) });
151
+ let out = view(process.env);
152
+ if (out.status !== 0) {
153
+ const noProxy = { ...process.env };
154
+ for (const key of ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy']) delete noProxy[key];
155
+ out = view(noProxy);
156
+ }
157
+ if (out.status !== 0) return null;
158
+ try {
159
+ const value = JSON.parse(out.stdout) as { headRefOid?: unknown; headRefName?: unknown; state?: unknown; url?: unknown };
160
+ return typeof value.headRefOid === 'string' && /^[a-f0-9]{40}$/i.test(value.headRefOid) && typeof value.state === 'string'
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;
166
+ } catch { return null; }
167
+ }
168
+
169
+ /** 리뷰 요청 argv — 진입점 · 같은 우주(`--config-dir`) · `self review <PR> --json --intent`. */
170
+ export function liveReviewArgs(entry: string, configDir: string, pr: number, intent: string): string[] {
171
+ return [resolve(entry), '--config-dir', configDir, 'self', 'review', String(pr), '--json', '--intent', intent];
172
+ }
173
+
174
+ /**
175
+ * 지금 도는 진입점으로 같은 우주의 리뷰를 떼어 띄운다 — 기다리지 않는다(결과는 self review 자기 관측에 남는다).
176
+ * `opts` 는 시험 몫(spawn·진입점·우주) — 기본은 실물.
177
+ */
178
+ export async function defaultRequestReview(pr: number, intent: string, cwd?: string, opts: { spawn?: DetachedSpawn; entry?: string; configDir?: string } = {}): Promise<void> {
179
+ const { spawnDetachedConfirmed, ELANOUS_CLI_ENTRY } = await import('../cli/tasks-cli.js');
180
+ const { getElanousConfigDirOverride } = await import('../elanous-config-dir.js');
181
+ const { effectiveInstanceRoot } = await import('../instance/resolve.js');
182
+ const configDir = opts.configDir ?? getElanousConfigDirOverride() ?? effectiveInstanceRoot();
183
+ await spawnDetachedConfirmed(opts.spawn ?? (spawn as unknown as DetachedSpawn), process.execPath, liveReviewArgs(opts.entry ?? ELANOUS_CLI_ENTRY, configDir, pr, intent), undefined, cwd);
184
+ }
185
+
186
+ export function liveReviewIntent(card: TaskCard, runId: string | null, stopReason: string): string {
187
+ const goal = card.text.split('\n')[0]!.trim().slice(0, 300);
188
+ return [
189
+ `목표: ${goal}`,
190
+ `수용기준: 과제 카드 ${card.id} 원문 —\n${card.text.trim().slice(0, 4000)}`,
191
+ `직전 반영분: 런 ${runId ?? '-'} 이 ${stopReason} 로 멈춤 — 수확 가능`,
192
+ '의도적 범위 경계: task-agent live review 요청(TA-JUDGE-LIVE-SAFE) — 리뷰만 한다 · 착지·재발사는 이 요청의 결정이 아니다',
193
+ ].join('\n');
194
+ }
195
+
196
+ /** 실행한 수를 관측에 남긴다(`live-move`). */
197
+ function observe(deps: LiveMoveDeps, result: LiveMoveResult): LiveMoveResult {
198
+ try { (deps.log ?? ((c, e, d) => debug.log(c, e, d)))('task-agent', 'live-move', { ...result, live: true }); } catch { /* fail-soft */ }
199
+ return result;
200
+ }
201
+
202
+ /**
203
+ * 단위는 «관측한 머리»다 — `self review` 에 머리 고정 인자가 없어, 관측~리뷰 시작 사이에 머리가 바뀌면 새 머리가
204
+ * 다음 틱에 한 번 더 요청될 수 있다(초과는 많아야 한 번 · 리뷰는 읽기 전용이라 착지·재발사로 번지지 않는다).
205
+ */
206
+ /** `review` — PR 머리마다 한 번. 카드에 먼저 적고(잠금 안) 그다음 띄운다 · 띄우기 실패는 카드에 오류로 남기고 같은 머리는 다시 안 띄운다. */
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> {
212
+ const kind = 'review' as const;
213
+ if (!card) return observe(deps, { kind, executed: false, card: null, ok: false, detail: 'no task card — stays shadow' });
214
+ if (!pr || !Number.isSafeInteger(pr) || pr < 1) return observe(deps, { kind, executed: false, card: card.id, ok: false, detail: 'no PR — stays shadow' });
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}` }));
234
+ const path = deps.statePath ?? taskAgentStatePath();
235
+ const at = (deps.now ?? (() => new Date()))().toISOString();
236
+ const claim = { claimed: false, missing: false };
237
+ updateTaskCard(path, card.id, (current) => {
238
+ if (!current) { claim.missing = true; return undefined; }
239
+ // 머리 «이력»으로 본다 — A→B→A 로 돌아와도 A 는 다시 요청하지 않는다.
240
+ if ((current.reviewRequests ?? []).some((entry) => entry.pr === pr && entry.head === head!.head)) return undefined;
241
+ claim.claimed = true;
242
+ return { ...current, reviewRequests: [...(current.reviewRequests ?? []), { pr, head: head!.head, at }] };
243
+ });
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)}` }));
246
+ try {
247
+ await (deps.requestReview ?? defaultRequestReview)(pr, liveReviewIntent(card, ctx.runId, ctx.stopReason), cwd);
248
+ } catch (error) {
249
+ const reason = error instanceof Error ? error.message : String(error);
250
+ updateTaskCard(path, card.id, (current) => current?.reviewRequests?.some((entry) => entry.pr === pr && entry.head === head!.head && entry.at === at)
251
+ ? { ...current, reviewRequests: current.reviewRequests.map((entry) => entry.pr === pr && entry.head === head!.head && entry.at === at ? { ...entry, error: reason } : entry) }
252
+ : undefined);
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}` }));
254
+ }
255
+ return observe(deps, withRepo({ kind, executed: true, card: card.id, ok: true, detail: `review requested for #${pr} head ${head.head.slice(0, 12)}` }));
256
+ }
257
+
258
+ /** `propose-green` — 비지 않은 근거가 있을 때만 카드에 제안을 적는다(한 번). 체크리스트는 쓰지 않는다. */
259
+ export function executeLiveGreenProposal(card: TaskCard | undefined, evidence: Record<string, string>, deps: LiveMoveDeps = {}): LiveMoveResult {
260
+ const kind = 'propose-green' as const;
261
+ if (!card) return observe(deps, { kind, executed: false, card: null, ok: false, detail: 'no task card — stays shadow' });
262
+ const refs = Object.fromEntries(Object.entries(evidence).filter(([, ref]) => typeof ref === 'string' && ref.trim()));
263
+ if (!Object.keys(refs).length) return observe(deps, { kind, executed: false, card: card.id, ok: false, detail: 'no evidence ref — not proposed' });
264
+ const path = deps.statePath ?? taskAgentStatePath();
265
+ const proposal = { at: (deps.now ?? (() => new Date()))().toISOString(), checklistId: card.checklistId ?? null, evidence: refs };
266
+ const wrote = { proposed: false, missing: false };
267
+ updateTaskCard(path, card.id, (current) => {
268
+ if (!current) { wrote.missing = true; return undefined; }
269
+ if (current.greenProposal) return undefined;
270
+ wrote.proposed = true;
271
+ return { ...current, greenProposal: proposal };
272
+ });
273
+ if (wrote.missing) return observe(deps, { kind, executed: false, card: card.id, ok: false, detail: 'task card not in state file' });
274
+ if (!wrote.proposed) return observe(deps, { kind, executed: false, card: card.id, ok: true, detail: 'already proposed' });
275
+ return observe(deps, { kind, executed: true, card: card.id, ok: true, detail: `green proposed (${Object.values(refs).join(', ')}) · checklist untouched` });
276
+ }
@@ -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 */ }
@@ -285,7 +296,7 @@ export function recordPieceRef(pieceId: string, ref: { pr?: number; goalId?: str
285
296
 
286
297
  /** 이미 넘긴 조각인가 — live 는 live 넘김·발사·발사 실패면 다시 안 넘긴다 · shadow 는 어떤 넘김이든 있으면 안 넘긴다. */
287
298
  function alreadyHanded(card: TaskCard, mode: 'shadow' | 'live'): boolean {
288
- if (card.status === 'launched' || card.status === 'launch-failed') return true;
299
+ if (card.status === 'launched' || card.status === 'launch-failed' || card.status === 'failed') return true;
289
300
  if (!card.handed) return false;
290
301
  return mode === 'shadow' || card.handed.mode === 'live';
291
302
  }
@@ -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
  });
@@ -5,14 +5,18 @@
5
5
  * - 입력은 슈퍼바이저 결과에 «이미 실려 있는» 값만 쓴다(네트워크 호출 0): 종료 어휘 ·
6
6
  * PR 번호/병합 여부(`prNumber`·`merged`). 리뷰 판정·must-fix 는 결과에 없으므로 비운다
7
7
  * («통과»로 읽지 않는다 — 판단부가 review 수를 고른다).
8
- * - 실행부는 언제나 shadow 로 부른다: «했을 수»만 기록하고 명령은 하나도 돌리지 않는다.
9
- * 설정 `taskAgent.mode=live` 도 이 경로에는 먹지 않는다(실행은 `autopilot task-agent-action` 몫).
8
+ * - 슈퍼바이저의 기본 호출은 shadow: «했을 수»만 기록하고 명령·복사는 하나도 돌리지 않는다.
9
+ * 전달 과제의 명시적 live 호출만 파일을 복사한다. 설정 `taskAgent.mode=live` 는 기본 호출에 먹지 않는다.
10
10
  * - 관측: `debug.log('task-agent', 'shadow-move', …)` → `elanous logs --category task-agent`.
11
11
  */
12
+ import { lstatSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
13
+ import { extname, join, relative, resolve } from 'node:path';
12
14
  import { debug } from '../debug/log.js';
15
+ import { readTaskAgentState, readTaskCard, taskAgentStatePath, type CompletionKind, type TaskCard } from './task-hand.js';
13
16
  import type { SupervisorJobResult, SupervisorStopReason } from '../self-dev/run-supervisor.js';
14
- import { executeNextAction, type NextAction } from './actions.js';
17
+ import { executeNextAction, type DeliveryKind, type NextAction } from './actions.js';
15
18
  import { judgeNextMove, type TaskJudgeInput, type TaskJudgement } from './judge.js';
19
+ import { configuredTaskAgentLiveMoves, executeLiveGreenProposal, executeLiveReview, type LiveMoveDeps, type LiveMoveResult, type TaskAgentLiveMove } from './live-moves.js';
16
20
 
17
21
  export interface TaskAgentShadowInput {
18
22
  runId: string | null;
@@ -27,23 +31,42 @@ export interface TaskAgentShadowMove {
27
31
  executorKind: TaskJudgement['executorKind'] | null;
28
32
  variant: TaskJudgement['executorVariant'] | null;
29
33
  reason: string;
30
- /** 실행부 결과 — shadow 경로에서는 늘 'shadow'(실행부 밖의 수면 null). */
34
+ /** 실행부 결과 — 기본 shadow 경로에서는 'shadow'(실행부 밖의 수면 null). */
31
35
  executorResult: string | null;
32
36
  /** 실행부가 남긴 «했을 수» 문면. */
33
37
  wouldDo: string | null;
34
38
  pr: number | null;
39
+ /** TA-JUDGE-LIVE-SAFE — `taskAgent.liveMoves` 가 이 수를 실제로 했을 때만 실린다(없으면 종전 그대로 그림자). */
40
+ liveMove?: LiveMoveResult;
35
41
  }
36
42
 
37
43
  export interface TaskAgentShadowDeps {
38
44
  log?: (category: string, event: string, data: Record<string, unknown>) => void;
39
45
  /** 시험용 — shadow 실행부가 명령을 부르면 여기로 온다(정상이면 0회). */
40
46
  command?: (args: string[]) => Promise<{ status: number; stdout: string; stderr?: string }>;
47
+ readCard?: (taskId: string) => TaskCard | undefined;
48
+ /** Explicit live invocation only; supervisor's default hook always uses shadow. */
49
+ mode?: 'shadow' | 'live';
50
+ /** TA-JUDGE-LIVE-SAFE — 실제로 할 안전한 수(`review`·`propose-green`). 비면 설정 `taskAgent.liveMoves` 를 읽는다. */
51
+ liveMoves?: ReadonlySet<TaskAgentLiveMove>;
52
+ /** live 수의 부작용(리뷰 띄우기·PR 머리 조회·상태 파일) — 시험은 주입한다. */
53
+ live?: LiveMoveDeps;
41
54
  }
42
55
 
43
- /** 슈퍼바이저 결과에서 판단부 입력을 만든다 — 추가 조회 없이 이미 실린 값만. */
44
- export function shadowJudgeInput(stopReason: SupervisorStopReason, results: readonly SupervisorJobResult[]): TaskJudgeInput {
56
+ /** 파일 산출물로 전달(elanous-out)하는 종결 종류 — 그 밖의 code-pr 밖 종류는 확인 증거를 판단부에서 기다린다. */
57
+ const DELIVERY_KINDS: ReadonlySet<CompletionKind> = new Set<CompletionKind>(['research-report', 'artifact', 'content']);
58
+ function isDeliveryKind(kind: CompletionKind | undefined): kind is DeliveryKind {
59
+ return kind !== undefined && DELIVERY_KINDS.has(kind);
60
+ }
61
+
62
+ /**
63
+ * 슈퍼바이저 결과에서 판단부 입력을 만든다 — 추가 조회 없이 이미 실린 값만.
64
+ * `completion` 은 카드의 종결 종류(§A4b③) — 있으면 판단부 입력에 싣는다(비면 종전 입력 그대로).
65
+ */
66
+ export function shadowJudgeInput(stopReason: SupervisorStopReason, results: readonly SupervisorJobResult[], completion?: CompletionKind): TaskJudgeInput {
45
67
  const withPr = results.filter((result) => typeof result.prNumber === 'number' && result.prNumber > 0);
46
68
  const input: TaskJudgeInput = { stopReason };
69
+ if (completion !== undefined) input.completion = completion;
47
70
  if (withPr.length > 0) {
48
71
  input.pr = withPr[0]!.prNumber!;
49
72
  // 병합 여부만 결과에 실린다 — 닫힘(CLOSED)은 여기서 모른다. 병합이 아니면 OPEN 으로 둔다(판단부는 MERGED 만 본다).
@@ -52,24 +75,117 @@ export function shadowJudgeInput(stopReason: SupervisorStopReason, results: read
52
75
  return input;
53
76
  }
54
77
 
78
+ export function selectDeliveryCandidates(worktreePath: string, kind: DeliveryKind): string[] {
79
+ const allowed = kind === 'research-report' ? new Set(['.md']) : new Set(['.md', '.html', '.pdf', '.png', '.pptx']);
80
+ try {
81
+ const root = realpathSync(worktreePath);
82
+ const gitMarker = join(root, '.git');
83
+ let gitPointer = '';
84
+ try { if (lstatSync(gitMarker).isFile()) gitPointer = readFileSync(gitMarker, 'utf8').trim(); }
85
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
86
+ const gitDir = gitPointer.startsWith('gitdir: ') ? resolve(root, gitPointer.slice(8)) : gitMarker;
87
+ const commonPointer = join(gitDir, 'commondir');
88
+ const sharedDir = (() => { try { return resolve(gitDir, readFileSync(commonPointer, 'utf8').trim()); } catch { return gitDir; } })();
89
+ let index: Buffer;
90
+ try { index = readFileSync(join(gitDir, 'index')); }
91
+ catch (error) {
92
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
93
+ index = Buffer.alloc(12);
94
+ index.write('DIRC');
95
+ index.writeUInt32BE(2, 4);
96
+ }
97
+ const tracked = new Set<string>();
98
+ if (index.toString('ascii', 0, 4) !== 'DIRC' || ![2, 3].includes(index.readUInt32BE(4))) return [];
99
+ let offset = 12;
100
+ const count = index.readUInt32BE(8);
101
+ for (let i = 0; i < count; i++) {
102
+ const start = offset;
103
+ const flags = index.readUInt16BE(start + 60);
104
+ offset = start + 62;
105
+ if (flags & 0x4000) offset += 2;
106
+ const end = index.indexOf(0, offset);
107
+ if (end < 0) return [];
108
+ tracked.add(index.toString('utf8', offset, end));
109
+ offset = start + Math.ceil((end + 1 - start) / 8) * 8;
110
+ }
111
+ const ignored = (() => {
112
+ try { return readFileSync(join(root, '.gitignore'), 'utf8').split(/\r?\n/).map(line => line.trim()).filter(line => line && !line.startsWith('#')); }
113
+ catch { return [] as string[]; }
114
+ })();
115
+ const excluded = (() => {
116
+ try { return readFileSync(join(sharedDir, 'info', 'exclude'), 'utf8').split(/\r?\n/).map(line => line.trim()).filter(line => line && !line.startsWith('#')); }
117
+ catch { return [] as string[]; }
118
+ })();
119
+ const isIgnored = (name: string): boolean => [...ignored, ...excluded].some(pattern => {
120
+ const rule = pattern.replace(/^\//, '').replace(/\/$/, '');
121
+ if (rule.includes('*')) return new RegExp(`^${rule.split('*').map(part => part.replace(/[|\\{}()[\]^$+?.]/g, '\\$&')).join('.*')}$`).test(name);
122
+ return name === rule || name.startsWith(rule + '/') || name.split('/').includes(rule);
123
+ });
124
+ const candidates: string[] = [];
125
+ const walk = (dir: string): void => {
126
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
127
+ if (entry.name === '.git' || entry.name === 'node_modules') continue;
128
+ const file = join(dir, entry.name);
129
+ const name = relative(root, file);
130
+ if (isIgnored(name)) continue;
131
+ if (entry.isDirectory()) { walk(file); continue; }
132
+ if (!entry.isFile()) continue;
133
+ if (allowed.has(extname(name).toLowerCase()) && !tracked.has(name) && lstatSync(file).isFile() && realpathSync(file) === file) candidates.push(name);
134
+ }
135
+ };
136
+ walk(root);
137
+ return candidates.sort();
138
+ } catch { return []; }
139
+ }
140
+
141
+ /** shadow-move 관측의 종결 종류 — 읽은 카드만 «비면 code-pr» 로 풀고, 조회 실패는 'unmeasured', 카드를 안 봤으면 null. */
142
+ function shadowCompletion(card: TaskCard | undefined, lookupFailed: boolean): CompletionKind | 'unmeasured' | null {
143
+ if (card) return card.completion ?? 'code-pr';
144
+ return lookupFailed ? 'unmeasured' : null;
145
+ }
146
+
55
147
  export async function recordTaskAgentShadowMove(input: TaskAgentShadowInput, deps: TaskAgentShadowDeps = {}): Promise<TaskAgentShadowMove> {
56
- const judgeInput = shadowJudgeInput(input.stopReason, input.results);
57
- const judgement = judgeNextMove(judgeInput);
148
+ const successful = input.results.find(result => result.ok === true && result.worktreePath);
149
+ let card: TaskCard | undefined;
150
+ let cardLookupFailed = false;
151
+ try {
152
+ card = successful ? (deps.readCard ?? readTaskCard)(successful.taskId) : undefined;
153
+ if (!card && successful && !deps.readCard) {
154
+ const tasks = readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(taskAgentStatePath()).tasks ?? {};
155
+ const matches = Object.values(tasks).filter(candidate => candidate.status === 'launched' && candidate.text === successful.feature);
156
+ if (matches.length === 1) card = matches[0];
157
+ }
158
+ } catch {
159
+ // Card lookup is optional; preserve the original PR judgement when unavailable — but mark the completion kind as unmeasured.
160
+ cardLookupFailed = true;
161
+ }
162
+ const kind = card?.completion;
163
+ const judgeInput = shadowJudgeInput(input.stopReason, input.results, kind);
164
+ const delivery = successful?.worktreePath && card?.project?.target && isDeliveryKind(kind)
165
+ ? { kind, worktreePath: successful.worktreePath, projectTarget: card.project.target, files: selectDeliveryCandidates(successful.worktreePath, kind) }
166
+ : undefined;
167
+ const judgement: TaskJudgement = delivery
168
+ ? delivery.files.length
169
+ ? { move: 'wait', stage: 'observing', reason: '전달 후보 확인 — 산출물 판독 대기', executorKind: 'deliver' }
170
+ : { move: 'wait', stage: 'waiting', reason: '전달 후보 없음' }
171
+ : judgeNextMove(judgeInput);
172
+ if (cardLookupFailed) judgement.reason = `${judgement.reason} · 종결 종류 미확인(카드 조회 실패)`;
58
173
  let executorResult: string | null = null;
59
174
  let wouldDo: string | null = null;
60
175
  if (judgement.executorKind) {
61
176
  const action: NextAction = {
62
177
  kind: judgement.executorKind,
63
178
  ...(judgement.executorVariant ? { variant: judgement.executorVariant } : {}),
64
- taskId: input.runId ?? 'run-unknown',
179
+ taskId: delivery ? card!.id : input.runId ?? 'run-unknown',
65
180
  rationale: judgement.reason,
66
- pr: typeof judgeInput.pr === 'number' ? judgeInput.pr : 0,
181
+ pr: delivery ? 0 : typeof judgeInput.pr === 'number' ? judgeInput.pr : 0,
67
182
  runId: input.runId ?? '',
68
183
  original: [...new Set(input.results.map((result) => result.feature))].join(' · '),
69
- checklistId: '',
184
+ checklistId: delivery ? card?.checklistId ?? '' : '',
185
+ ...(delivery ? { delivery } : {}),
70
186
  };
71
187
  executorResult = await executeNextAction(action, {
72
- mode: 'shadow',
188
+ mode: delivery ? (deps.mode ?? 'shadow') : 'shadow',
73
189
  command: deps.command ?? (async () => { throw new Error('task-agent shadow path must not run commands'); }),
74
190
  observe: (_event, data) => { wouldDo = data.reason; },
75
191
  });
@@ -83,8 +199,69 @@ export async function recordTaskAgentShadowMove(input: TaskAgentShadowInput, dep
83
199
  reason: judgement.reason,
84
200
  executorResult,
85
201
  wouldDo,
86
- pr: typeof judgeInput.pr === 'number' ? judgeInput.pr : null,
202
+ pr: delivery ? null : typeof judgeInput.pr === 'number' ? judgeInput.pr : null,
87
203
  };
88
- (deps.log ?? ((category, event, data) => debug.log(category, event, data)))('task-agent', 'shadow-move', { ...move });
89
- return move;
204
+ if (delivery && deps.mode !== 'live') {
205
+ move.wouldDo = `${move.wouldDo ?? 'would wait'} → ${join(delivery.projectTarget, 'elanous-out', card!.id)} (${delivery.files.join(', ')})`;
206
+ }
207
+ if (delivery && deps.mode === 'live') {
208
+ move.move = executorResult === 'done' ? 'propose-green' : 'wait';
209
+ move.reason = wouldDo ?? judgement.reason;
210
+ move.wouldDo = null;
211
+ }
212
+ const liveKind: TaskAgentLiveMove | null = move.move === 'review' || move.move === 'propose-green' ? move.move : null;
213
+ // 판단마다 해석한다 — 안전하지 않은 수를 판단할 때도 모르는 항목 경고가 남게(실행은 아래 두 수뿐).
214
+ const liveMoves = deps.liveMoves ?? await configuredTaskAgentLiveMoves();
215
+ const live = liveKind !== null && liveMoves.has(liveKind);
216
+ // 카드 id 는 모든 판단 관측에 싣는다(그림자 일치율을 카드로 잇는다) — 읽기 전용 조회.
217
+ // live 수는 «근거 결과»에 묶는다 — PR 수는 그 PR 을 낸 결과의 카드·작업 트리, 전달은 전달한 카드(다중 결과에서 남의 카드에 쓰지 않게).
218
+ const prResult = typeof judgeInput.pr === 'number' ? input.results.find((result) => result.prNumber === judgeInput.pr) : undefined;
219
+ const boundCard = delivery
220
+ ? card
221
+ : prResult
222
+ ? (card && successful?.taskId === prResult.taskId ? card : liveCardFor([prResult], deps))
223
+ : undefined;
224
+ // PR 에 묶인 수는 그 PR 의 카드만 관측에 싣는다(못 찾으면 null — 다른 결과의 카드로 대체하지 않는다).
225
+ const liveCard = prResult || delivery ? boundCard : card ?? (cardLookupFailed ? undefined : liveCardFor(input.results, deps));
226
+ let liveMove: LiveMoveResult | undefined;
227
+ if (live) {
228
+ if (liveKind === 'review') {
229
+ liveMove = await executeLiveReview(boundCard, prResult?.prNumber,
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 });
240
+ } else {
241
+ const evidence: Record<string, string> = {};
242
+ if (boundCard && !delivery && judgeInput.prState === 'MERGED' && prResult) evidence[boundCard.id] = `#${prResult.prNumber}`;
243
+ else if (boundCard && delivery && deps.mode === 'live' && executorResult === 'done') evidence[boundCard.id] = join(delivery.projectTarget, 'elanous-out', boundCard.id);
244
+ liveMove = executeLiveGreenProposal(boundCard, evidence, { ...(deps.log ? { log: deps.log } : {}), ...deps.live });
245
+ }
246
+ }
247
+ // 판단한 수마다 live/shadow 를 남긴다 — OP 가 «오판 건수»·«그림자 일치율»을 카드·수 종류로 잰다.
248
+ (deps.log ?? ((category, event, data) => debug.log(category, event, data)))('task-agent', 'shadow-move', {
249
+ ...move, completion: shadowCompletion(card, cardLookupFailed), card: liveCard?.id ?? null, live, liveExecuted: liveMove?.executed ?? false, ...(liveMove ? { liveOk: liveMove.ok } : {}),
250
+ });
251
+ return liveMove ? { ...move, liveMove } : move;
252
+ }
253
+
254
+ /** live 수의 카드 — 성공한 결과가 없어도(수확 가능 런은 ok 가 아닐 수 있다) 결과의 taskId · 유일한 발사 문장으로 찾는다. */
255
+ function liveCardFor(results: readonly SupervisorJobResult[], deps: TaskAgentShadowDeps): TaskCard | undefined {
256
+ try {
257
+ for (const result of results) {
258
+ const found = deps.readCard ? deps.readCard(result.taskId) : readTaskCard(result.taskId, deps.live?.statePath);
259
+ if (found) return found;
260
+ }
261
+ if (deps.readCard) return undefined;
262
+ const tasks = readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(deps.live?.statePath ?? taskAgentStatePath()).tasks ?? {};
263
+ const features = new Set(results.map((result) => result.feature));
264
+ const matches = Object.values(tasks).filter(candidate => candidate.status === 'launched' && features.has(candidate.text));
265
+ return matches.length === 1 ? matches[0] : undefined;
266
+ } catch { return undefined; }
90
267
  }