elanous 0.2.27 → 0.2.28

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 (285) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1454-a9c338f5bb556f42.js → 1454-29d1d773d5996a49.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{599-a914cc0468f56143.js → 599-56f2c0a5cc4a7295.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/page-5ab1e47978577c5e.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-07110da72c45e68f.js → page-24453f6a302816b4.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/board/{page-d2d76d4a7eae91bd.js → page-6c131d30bb1cf871.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/page-fa46d4588e8442c2.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/bots/{page-e5a9302c105725a3.js → page-dc0772b11861af51.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/page-66ad46ed0279feb9.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/chat/page-5159dc8752c6cd29.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/consult/page-c5a2478861c08b87.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-f722a9dec084e08b.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/decisions/{page-6a16a48d1aa6e91f.js → page-256ffd365d32e100.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e0de78ae7b99f01c.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/editor/page-cfbdc44b708d9801.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/exec/{page-39f82acf619cca23.js → page-4c741eb760f2f77c.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/field/{page-7c2c8f81b185f9c0.js → page-e9ba98f5f1440a21.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/inside/{page-ae7aff07e32676bb.js → page-dafc901bb1b72b5f.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/intake/{page-482c60c1bc540d0a.js → page-2f6f303fa114ec24.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/{layout-7cb996c1aa08b6f5.js → layout-1634081c5272006c.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/live/{page-623e08fe4477d606.js → page-ced0bcfc6542b3fc.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live-run/{page-db460cd8aecb8490.js → page-bb1f05cd51c38944.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/loops/{page-ff27902d0b11ef4f.js → page-646b50f63b6fe732.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/market/page-db069b48a59c8c00.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-1917ea87ea71ab6b.js → page-f736324afa2117c1.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-25a302273b054fd7.js → page-6f131b51193a36f9.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/{not-found-670510eda8b9443f.js → not-found-435f5e2faf02bc01.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-497e0eedf20e7890.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a8d28fdf995e16ea.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4bb7556e2e97a750.js → page-8966452228753c5e.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fd5d18656cb432b6.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/page-5663779cb1a37609.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/{page-4eb96fc7241c2456.js → page-66a67eb19ebd976b.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/page-3b492aa78cf3c4a6.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-0d64ff212dc55d03.js → page-c71267e7681da982.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-7f096248993fc2d4.js → page-a21d1e1797172692.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-e47fc72049149a38.js → page-cb249e81ed339318.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-f7d808e31e005895.js → page-781cfc5114e097da.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-5c2b24b5496dad73.js → page-478157214db6a023.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/{page-76cdb531668b5191.js → page-86141f12cf090b7f.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/share/page-7256c73bb1c36751.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/page-5c738ca7adef3de5.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/page-0138c8091ec17224.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/term/page-cc2f24f16d85e735.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/today/{page-dab163b338e3b966.js → page-979d318a534e92da.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-182d394f7e6e096b.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-0260ffc7eda50db1.js → page-21050928a697ed7a.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-44b9d807005a9030.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-99c4e6c7345a9156.js → page-601fb77340e63c9a.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/page-75d3fb5d47af0edf.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-11bf9e5725a72c22.js +1 -0
  53. package/apps/pwa/out/approvals/index.html +1 -1
  54. package/apps/pwa/out/approvals/index.txt +13 -13
  55. package/apps/pwa/out/autopilot/index.html +1 -1
  56. package/apps/pwa/out/autopilot/index.txt +13 -13
  57. package/apps/pwa/out/board/index.html +1 -1
  58. package/apps/pwa/out/board/index.txt +13 -13
  59. package/apps/pwa/out/botlab/index.html +1 -1
  60. package/apps/pwa/out/botlab/index.txt +13 -13
  61. package/apps/pwa/out/bots/index.html +1 -1
  62. package/apps/pwa/out/bots/index.txt +13 -13
  63. package/apps/pwa/out/ceo/index.html +1 -1
  64. package/apps/pwa/out/ceo/index.txt +13 -13
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +13 -13
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +13 -13
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +13 -13
  71. package/apps/pwa/out/decisions/index.html +1 -1
  72. package/apps/pwa/out/decisions/index.txt +13 -13
  73. package/apps/pwa/out/design-check/index.html +1 -1
  74. package/apps/pwa/out/design-check/index.txt +13 -13
  75. package/apps/pwa/out/editor/index.html +1 -1
  76. package/apps/pwa/out/editor/index.txt +13 -13
  77. package/apps/pwa/out/exec/index.html +1 -1
  78. package/apps/pwa/out/exec/index.txt +13 -13
  79. package/apps/pwa/out/field/index.html +1 -1
  80. package/apps/pwa/out/field/index.txt +13 -13
  81. package/apps/pwa/out/healthz +1 -1
  82. package/apps/pwa/out/index.html +1 -1
  83. package/apps/pwa/out/index.txt +13 -13
  84. package/apps/pwa/out/inside/index.html +1 -1
  85. package/apps/pwa/out/inside/index.txt +13 -13
  86. package/apps/pwa/out/intake/index.html +1 -1
  87. package/apps/pwa/out/intake/index.txt +13 -13
  88. package/apps/pwa/out/live/index.html +1 -1
  89. package/apps/pwa/out/live/index.txt +13 -13
  90. package/apps/pwa/out/live-run/index.html +1 -1
  91. package/apps/pwa/out/live-run/index.txt +13 -13
  92. package/apps/pwa/out/loops/index.html +1 -1
  93. package/apps/pwa/out/loops/index.txt +13 -13
  94. package/apps/pwa/out/market/index.html +1 -1
  95. package/apps/pwa/out/market/index.txt +13 -13
  96. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  97. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  98. package/apps/pwa/out/morning/index.html +1 -1
  99. package/apps/pwa/out/morning/index.txt +13 -13
  100. package/apps/pwa/out/observatory/index.html +1 -1
  101. package/apps/pwa/out/observatory/index.txt +13 -13
  102. package/apps/pwa/out/ops/checklist/index.html +1 -1
  103. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  104. package/apps/pwa/out/ops/release/index.html +1 -1
  105. package/apps/pwa/out/ops/release/index.txt +13 -13
  106. package/apps/pwa/out/ops/seats/index.html +1 -1
  107. package/apps/pwa/out/ops/seats/index.txt +13 -13
  108. package/apps/pwa/out/outputs/index.html +1 -1
  109. package/apps/pwa/out/outputs/index.txt +13 -13
  110. package/apps/pwa/out/reflection/index.html +1 -1
  111. package/apps/pwa/out/reflection/index.txt +14 -14
  112. package/apps/pwa/out/scheduler/index.html +1 -1
  113. package/apps/pwa/out/scheduler/index.txt +13 -13
  114. package/apps/pwa/out/sessions/index.html +1 -1
  115. package/apps/pwa/out/sessions/index.txt +13 -13
  116. package/apps/pwa/out/settings/devices/index.html +1 -1
  117. package/apps/pwa/out/settings/devices/index.txt +13 -13
  118. package/apps/pwa/out/settings/index.html +1 -1
  119. package/apps/pwa/out/settings/index.txt +13 -13
  120. package/apps/pwa/out/setup/done/index.html +1 -1
  121. package/apps/pwa/out/setup/done/index.txt +13 -13
  122. package/apps/pwa/out/setup/index.html +1 -1
  123. package/apps/pwa/out/setup/index.txt +13 -13
  124. package/apps/pwa/out/share/index.html +1 -1
  125. package/apps/pwa/out/share/index.txt +13 -13
  126. package/apps/pwa/out/showroom/index.html +1 -1
  127. package/apps/pwa/out/showroom/index.txt +13 -13
  128. package/apps/pwa/out/tasks/index.html +1 -1
  129. package/apps/pwa/out/tasks/index.txt +13 -13
  130. package/apps/pwa/out/term/index.html +1 -1
  131. package/apps/pwa/out/term/index.txt +13 -13
  132. package/apps/pwa/out/today/index.html +1 -1
  133. package/apps/pwa/out/today/index.txt +13 -13
  134. package/apps/pwa/out/trace/index.html +1 -1
  135. package/apps/pwa/out/trace/index.txt +13 -13
  136. package/apps/pwa/out/vault/index.html +1 -1
  137. package/apps/pwa/out/vault/index.txt +13 -13
  138. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  139. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  140. package/apps/pwa/out/workflows/index.html +1 -1
  141. package/apps/pwa/out/workflows/index.txt +13 -13
  142. package/apps/pwa/out/workspace/index.html +1 -1
  143. package/apps/pwa/out/workspace/index.txt +13 -13
  144. package/apps/pwa/out/worktrees/index.html +1 -1
  145. package/apps/pwa/out/worktrees/index.txt +13 -13
  146. package/graphs/implement-loop.yaml +38 -1
  147. package/graphs/steward/recipes.yaml +6 -0
  148. package/graphs/steward/release-day.yaml +23 -0
  149. package/graphs/steward/steward.yaml +9 -1
  150. package/graphs/work/code-pr.yaml +23 -0
  151. package/package.json +1 -1
  152. package/scripts/browser-observe-cycle.ts +76 -2
  153. package/scripts/ci-typecheck-changed.ts +50 -17
  154. package/scripts/detect-test-interference.ts +31 -1
  155. package/scripts/release-loop/docs-node.ts +51 -22
  156. package/scripts/release-loop/gate-node.ts +37 -4
  157. package/scripts/release-loop/node-verdict.ts +3 -1
  158. package/scripts/release-loop/publish-node.ts +3 -0
  159. package/scripts/release-loop/tui-node-checks.ts +5 -2
  160. package/scripts/skills/public-check.ts +5 -2
  161. package/scripts/test-deterministic.ts +520 -37
  162. package/scripts/ux-sim.ts +7 -1
  163. package/scripts/verify-kpack6-live.ts +230 -0
  164. package/src/acp/codex-plugins.ts +17 -0
  165. package/src/agent/task-notification.ts +2 -1
  166. package/src/agent-mission/in-agent-plugin.ts +8 -3
  167. package/src/agent-substrate/self-review-cli.ts +34 -10
  168. package/src/briefing/brief-items.ts +151 -35
  169. package/src/budget/llm-cost.ts +25 -2
  170. package/src/budget/usage-outcomes.ts +32 -0
  171. package/src/claims/claims-ledger.ts +23 -0
  172. package/src/claims/claims-render.ts +22 -2
  173. package/src/cli/brief-cli.ts +131 -10
  174. package/src/cli/claims-cli.ts +24 -4
  175. package/src/cli/msg-cli.ts +165 -9
  176. package/src/cli/plugin-cli.ts +11 -3
  177. package/src/cli/pr-cli.ts +80 -18
  178. package/src/cli/pr-land-refusal.ts +20 -0
  179. package/src/cli/release-cli.ts +2 -0
  180. package/src/cli/rfc-cli.ts +24 -0
  181. package/src/cli/tailscale-serve.ts +5 -8
  182. package/src/cli/tasks-cli.ts +393 -26
  183. package/src/cli/usage-cli.ts +9 -4
  184. package/src/dashboard/first-screen-band.ts +63 -13
  185. package/src/dashboard/index.ts +8 -3
  186. package/src/dashboard/skill-detection-runtime.ts +24 -4
  187. package/src/domains/morning-report.ts +3 -1
  188. package/src/github/pr-labels.ts +10 -4
  189. package/src/graph-runner/runner.ts +101 -14
  190. package/src/harness/harness-clean.ts +21 -3
  191. package/src/harness/harness-cli-command.ts +22 -6
  192. package/src/harness/harness-pod-dispatch.ts +11 -4
  193. package/src/hq/seats.ts +17 -3
  194. package/src/index.ts +34 -5
  195. package/src/intelligence-map/model-bench-scores.ts +102 -0
  196. package/src/loops/orchestrator/inbox.ts +73 -0
  197. package/src/loops/orchestrator/resync-shadow.ts +100 -12
  198. package/src/loops/orchestrator/tick.ts +73 -2
  199. package/src/maturity/feature-maturity.ts +1 -1
  200. package/src/msg/msg-sse-server.ts +141 -0
  201. package/src/msg/msg-store.ts +84 -14
  202. package/src/patents/ledger.ts +128 -0
  203. package/src/plugins/install/market-fetch.ts +32 -1
  204. package/src/pod-lease/admission.ts +116 -41
  205. package/src/release-loop/checklist.ts +11 -0
  206. package/src/release-loop/merged-pr-checklist.ts +24 -5
  207. package/src/release-loop/rubric.ts +70 -0
  208. package/src/rfc-registry/registry.ts +88 -0
  209. package/src/seat-dispatch/seat-request-ledger.ts +4 -0
  210. package/src/seat-loop/knowhow-lint.ts +233 -0
  211. package/src/seat-loop/seat-pack.ts +16 -4
  212. package/src/seat-loop/traffic-signal.ts +7 -0
  213. package/src/self-dev/ask-launch-flow.ts +48 -8
  214. package/src/self-dev/ask-path-candidates.ts +69 -14
  215. package/src/self-dev/decompose-proposal.ts +43 -1
  216. package/src/self-dev/graph-journey-nodes.ts +2 -0
  217. package/src/self-dev/launch-preflight-text.ts +5 -1
  218. package/src/self-dev/launch-preflight.ts +143 -10
  219. package/src/self-implement/gate-cli.ts +6 -2
  220. package/src/self-implement/goal-author.ts +2 -1
  221. package/src/self-implement/goal-new-paths.ts +4 -5
  222. package/src/self-implement/graph-authority.ts +17 -3
  223. package/src/self-implement/graph-yaml.ts +15 -7
  224. package/src/self-implement/headless-elanous-driver.ts +16 -1
  225. package/src/self-implement/host-regate.ts +44 -9
  226. package/src/self-implement/orchestrator.ts +3 -10
  227. package/src/self-implement/rework-policy.ts +80 -2
  228. package/src/self-implement/speed-claims.ts +145 -6
  229. package/src/skills/tools/persistent-grounding.ts +59 -4
  230. package/src/skills/tools/shell.ts +79 -1
  231. package/src/steward/rejudge-tick.ts +12 -1
  232. package/src/steward/release-day.ts +158 -0
  233. package/src/steward/resync-tick.ts +123 -0
  234. package/src/task-agent/actions.ts +1 -0
  235. package/src/task-agent/adopt-pr.ts +140 -0
  236. package/src/task-agent/board.ts +11 -1
  237. package/src/task-agent/card-evidence.ts +37 -5
  238. package/src/task-agent/cover.ts +84 -17
  239. package/src/task-agent/failure-notify.ts +61 -0
  240. package/src/task-agent/green-sweep.ts +170 -0
  241. package/src/task-agent/land-worktree.ts +52 -5
  242. package/src/task-agent/live-move-history.ts +1 -0
  243. package/src/task-agent/live-moves.ts +43 -12
  244. package/src/task-agent/parent-host.ts +1 -1
  245. package/src/task-agent/postland-card.ts +139 -0
  246. package/src/task-agent/rejudge-on-head.ts +106 -7
  247. package/src/task-agent/remote-rejudge-bind.ts +62 -0
  248. package/src/task-agent/repair-branch-cas.ts +182 -0
  249. package/src/task-agent/repair-cycle.ts +275 -0
  250. package/src/task-agent/repair-next-move.ts +31 -0
  251. package/src/task-agent/repair-review.ts +24 -0
  252. package/src/task-agent/repair-worker.ts +87 -0
  253. package/src/task-agent/review-result-capture.ts +2 -1
  254. package/src/task-agent/shadow.ts +13 -7
  255. package/src/task-agent/task-hand.ts +115 -6
  256. package/src/task-orchestrator/surfaces/pod-repair-commit.ts +96 -0
  257. package/src/task-orchestrator/surfaces/self-implement-pod.ts +70 -12
  258. package/src/telegram.ts +30 -1
  259. package/src/user-config.ts +8 -1
  260. package/src/ux-sim/fault-sqlite-busy.ts +50 -0
  261. package/src/version/packed-revision.json +1 -1
  262. package/apps/pwa/out/_next/static/chunks/app/approvals/page-a1ce306364f08da9.js +0 -1
  263. package/apps/pwa/out/_next/static/chunks/app/botlab/page-30007380f2e2a3c1.js +0 -1
  264. package/apps/pwa/out/_next/static/chunks/app/ceo/page-b93c04b5311c97ec.js +0 -1
  265. package/apps/pwa/out/_next/static/chunks/app/chat/page-718a49e86bf25f9c.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/app/consult/page-20662900d6b4b455.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/app/control/page-f707a509515ac50c.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/app/design-check/page-fcb06e305f515527.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/editor/page-6acf5ec4e6c6d1af.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/market/page-8397a5c6ca569bf4.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/observatory/page-d73c3f1e00d93986.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-65caacf54389081e.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-77e04b4f8762c666.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/outputs/page-15c0a4095baddfba.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/reflection/page-63e272a4cd3b6634.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/share/page-e2b63345bba1a11f.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/showroom/page-d376e0c83edc041a.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d6db55fe2c04d128.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/term/page-87b88f551f421d8b.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/trace/page-08019efef111ed2e.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-980ce6fe36ad95eb.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/workspace/page-0ab42e0f05823013.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ad9220422d20ba33.js +0 -1
  284. /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_buildManifest.js +0 -0
  285. /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_ssgManifest.js +0 -0
@@ -0,0 +1,158 @@
1
+ /** Release-day shadow: observe the scheduled phase without performing its action. */
2
+ import { chmodSync, mkdirSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { Database } from 'bun:sqlite';
5
+ import { debug } from '../debug/log.js';
6
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
7
+ import { listSchedules, type ReleaseSchedule } from '../release-loop/release-schedule.js';
8
+
9
+ export type ReleaseDayPhase = 'gate-mode' | 'carry-p0' | 'cut-run' | 'green-back' | 'none' | 'unmeasured';
10
+ export interface ReleaseDayDecision {
11
+ version: string;
12
+ phase: ReleaseDayPhase;
13
+ dueAt: string | null;
14
+ scheduleUpdatedAt: string;
15
+ reason?: string;
16
+ }
17
+
18
+ /** The release record, rather than publishAt (a planned time), opens green-back. */
19
+ export function decideReleaseDayPhase(schedule: ReleaseSchedule, now: Date, publishedAt: string | null): ReleaseDayDecision {
20
+ const cut = Date.parse(schedule.cutAt);
21
+ const land = schedule.landBy ? Date.parse(schedule.landBy) : cut;
22
+ const gate = cut - 30 * 60_000;
23
+ const time = now.getTime();
24
+ let phase: ReleaseDayPhase = 'none';
25
+ let dueAt: string | null = null;
26
+ if (publishedAt && time >= Date.parse(publishedAt)) {
27
+ phase = 'green-back'; dueAt = publishedAt;
28
+ } else if (time >= cut) {
29
+ phase = 'cut-run'; dueAt = schedule.cutAt;
30
+ } else if (time >= land) {
31
+ phase = 'carry-p0'; dueAt = schedule.landBy;
32
+ } else if (time >= gate) {
33
+ phase = 'gate-mode'; dueAt = new Date(gate).toISOString();
34
+ }
35
+ return { version: schedule.version, phase, dueAt, scheduleUpdatedAt: schedule.updatedAt };
36
+ }
37
+
38
+ export type PublicationProbe = { kind: 'published'; at: string } | { kind: 'unpublished' } | { kind: 'unmeasured'; reason: string };
39
+
40
+ function actualPublicationProbe(version: string, root: string): PublicationProbe {
41
+ const path = join(root, 'release', version, 'release.json');
42
+ let raw: string;
43
+ try { raw = readFileSync(path, 'utf8'); }
44
+ catch (error) {
45
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { kind: 'unpublished' };
46
+ return { kind: 'unmeasured', reason: 'release-record-read-failed' };
47
+ }
48
+ try {
49
+ const record = JSON.parse(raw) as { version?: unknown; publishedAt?: unknown };
50
+ if (record.version !== version) return { kind: 'unmeasured', reason: 'release-record-version-mismatch' };
51
+ if (record.publishedAt == null) return { kind: 'unmeasured', reason: 'release-record-published-at-missing' };
52
+ if (typeof record.publishedAt !== 'string' || !Number.isFinite(Date.parse(record.publishedAt)))
53
+ return { kind: 'unmeasured', reason: 'release-record-published-at-invalid' };
54
+ return { kind: 'published', at: record.publishedAt };
55
+ } catch { return { kind: 'unmeasured', reason: 'release-record-json-failed' }; }
56
+ }
57
+
58
+ export interface ReleaseDayScheduleState { publication: PublicationProbe; greenObserved: boolean }
59
+
60
+ /** A late release and the next gate can both be due; return both instead of starving either. */
61
+ export function selectReleaseDaySchedules(
62
+ schedules: ReleaseSchedule[], now: Date, state: (version: string) => ReleaseDayScheduleState,
63
+ ): ReleaseSchedule[] {
64
+ const ordered = schedules.filter(s => Number.isFinite(Date.parse(s.cutAt))).sort((a, b) => Date.parse(a.cutAt) - Date.parse(b.cutAt));
65
+ const time = now.getTime();
66
+ const dueFuture = ordered.filter(s => {
67
+ const cut = Date.parse(s.cutAt);
68
+ const firstDue = Math.min(cut - 30 * 60_000, s.landBy ? Date.parse(s.landBy) : cut);
69
+ return time < cut && time >= firstDue;
70
+ });
71
+ const past = ordered.filter(s => Date.parse(s.cutAt) <= time);
72
+ const unpublished = past.filter(s => state(s.version).publication.kind === 'unpublished');
73
+ const unmeasured = past.filter(s => state(s.version).publication.kind === 'unmeasured');
74
+ const pendingGreen = past.filter(s => {
75
+ const status = state(s.version);
76
+ return status.publication.kind === 'published' && !status.greenObserved;
77
+ });
78
+ const due = [...dueFuture, ...unpublished, ...unmeasured, ...pendingGreen];
79
+ if (due.length) return [...new Map(due.map(s => [s.version, s])).values()];
80
+ const upcoming = ordered.find(s => Date.parse(s.cutAt) > time);
81
+ return upcoming && Date.parse(upcoming.cutAt) - time <= 24 * 60 * 60_000 ? [upcoming] : [];
82
+ }
83
+
84
+ export interface ReleaseDayShadowDeps {
85
+ now?: () => Date;
86
+ releaseRoot?: string;
87
+ stateRoot?: string;
88
+ schedules?: () => ReleaseSchedule[];
89
+ publishedAt?: (version: string, root: string) => string | null;
90
+ log?: (event: string, data: Record<string, unknown>) => void;
91
+ }
92
+
93
+ export interface ReleaseDayShadowResult extends ReleaseDayDecision { recorded: boolean }
94
+
95
+ export function runReleaseDayShadow(deps: ReleaseDayShadowDeps = {}): ReleaseDayShadowResult[] {
96
+ const now = (deps.now ?? (() => new Date()))();
97
+ // The graph child's stamped universe also owns its release schedule under --test.
98
+ const releaseRoot = deps.releaseRoot ?? effectiveInstanceRoot();
99
+ const schedules = (deps.schedules ?? (() => listSchedules(releaseRoot)))();
100
+ const log = deps.log ?? ((event: string, data: Record<string, unknown>) => debug.log('steward.release-day', event, data));
101
+ if (!schedules.length) { log('decision', { phase: 'none', reason: 'no-release-schedule', at: now.toISOString(), recorded: false, mode: 'shadow' }); return []; }
102
+ const dir = join(deps.stateRoot ?? effectiveInstanceRoot(), 'steward');
103
+ mkdirSync(dir, { recursive: true });
104
+ const path = join(dir, 'release-day-shadow.sqlite');
105
+ const db = new Database(path, { create: true, strict: true });
106
+ try {
107
+ chmodSync(path, 0o600);
108
+ db.exec('PRAGMA busy_timeout = 10000');
109
+ db.exec('CREATE TABLE IF NOT EXISTS phase_observations (version TEXT NOT NULL, phase TEXT NOT NULL, due_at TEXT NOT NULL, observed_at TEXT NOT NULL, schedule_updated_at TEXT NOT NULL, PRIMARY KEY (version, phase))');
110
+ const states = new Map<string, ReleaseDayScheduleState>();
111
+ for (const schedule of schedules) {
112
+ const observed = db.query('SELECT phase FROM phase_observations WHERE version = ?').all(schedule.version) as Array<{ phase: string }>;
113
+ const injected = deps.publishedAt?.(schedule.version, releaseRoot);
114
+ states.set(schedule.version, {
115
+ publication: deps.publishedAt
116
+ ? injected == null ? { kind: 'unpublished' }
117
+ : Number.isFinite(Date.parse(injected)) ? { kind: 'published', at: injected }
118
+ : { kind: 'unmeasured', reason: 'injected-published-at-invalid' }
119
+ : actualPublicationProbe(schedule.version, releaseRoot),
120
+ greenObserved: observed.some(row => row.phase === 'green-back'),
121
+ });
122
+ }
123
+ const selected = selectReleaseDaySchedules(schedules, now, version => states.get(version)!);
124
+ if (!selected.length) { log('decision', { phase: 'none', reason: 'outside-release-window', at: now.toISOString(), recorded: false, mode: 'shadow' }); return []; }
125
+ const results: ReleaseDayShadowResult[] = [];
126
+ for (const schedule of selected) {
127
+ const publication = states.get(schedule.version)!.publication;
128
+ if (publication.kind === 'unmeasured') {
129
+ const result: ReleaseDayShadowResult = { version: schedule.version, phase: 'unmeasured', dueAt: null,
130
+ scheduleUpdatedAt: schedule.updatedAt, reason: publication.reason, recorded: false };
131
+ log('unmeasured', { ...result, at: now.toISOString(), mode: 'shadow' });
132
+ results.push(result);
133
+ continue;
134
+ }
135
+ const publishedAt = publication.kind === 'published' ? publication.at : null;
136
+ const decision = decideReleaseDayPhase(schedule, now, publishedAt);
137
+ let recorded = false;
138
+ if (decision.phase !== 'none') {
139
+ const row = db.query('INSERT OR IGNORE INTO phase_observations (version, phase, due_at, observed_at, schedule_updated_at) VALUES (?, ?, ?, ?, ?)')
140
+ .run(decision.version, decision.phase, decision.dueAt!, now.toISOString(), decision.scheduleUpdatedAt);
141
+ recorded = row.changes === 1;
142
+ }
143
+ log('decision', { ...decision, at: now.toISOString(), recorded, mode: 'shadow', publishedAt });
144
+ results.push({ ...decision, recorded });
145
+ }
146
+ return results;
147
+ } finally { db.close(); }
148
+ }
149
+
150
+ if (import.meta.main) {
151
+ try {
152
+ const sinkReady = await (await import('../domains/standalone-log-sink.js')).registerStandaloneLogSink('steward');
153
+ if (!sinkReady) throw new Error('durable log sink unavailable');
154
+ const results = runReleaseDayShadow();
155
+ console.log(`steward release-day shadow: ${results.map(result => `${result.version}:${result.phase}${result.recorded ? ' · recorded' : ''}`).join(', ') || 'none'}`);
156
+ }
157
+ catch (error) { console.error(`steward release-day shadow failed: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
158
+ }
@@ -0,0 +1,123 @@
1
+ /** Resync labeled PRs before the steward's TA rejudge node sees their heads. */
2
+ import { debug } from '../debug/log.js';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { mkdirSync, writeFileSync } from 'node:fs';
5
+ import { dirname, join } from 'node:path';
6
+ import { getElanousConfigDir } from '../elanous-config-dir.js';
7
+ import { resolveHqLaunchSeat } from '../hq/seats.js';
8
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
9
+ import { runResyncShadow, type ResyncShadowSummary } from '../loops/orchestrator/resync-shadow.js';
10
+ import { listSchedules, type ReleaseSchedule } from '../release-loop/release-schedule.js';
11
+
12
+ type ReleaseRunState = 'active' | 'idle' | 'unknown';
13
+ type ReleaseWindowState = 'inside' | 'outside' | 'unknown';
14
+ type ResyncTickResult = { decision: 'ran' | 'failed' | 'skipped-release' | 'skipped-release-window'; summary?: ResyncShadowSummary; reason?: string };
15
+
16
+ export function resyncPendingPath(): string { return join(effectiveInstanceRoot(), 'steward', 'resync-pending.json'); }
17
+
18
+ /** The same registered, detached OP checkout used by steward launch; the installed package is not a git repository. */
19
+ export function stewardResyncRepoRoot(
20
+ resolveSeat: typeof resolveHqLaunchSeat = resolveHqLaunchSeat,
21
+ configDir: () => string = getElanousConfigDir,
22
+ ): string { return resolveSeat('OP', configDir()); }
23
+
24
+ /** A release may detach the OP checkout. Unknown process state also skips remote branch writes. */
25
+ export function probeReleaseRun(
26
+ list: () => { status: number | null; stdout?: string; error?: Error } = () => spawnSync('pgrep', ['-f', 'release[[:space:]]+(run|resume)'], { encoding: 'utf8', timeout: 5_000 }),
27
+ command: (pid: string) => string | null = pid => {
28
+ const result = spawnSync('ps', ['-p', pid, '-o', 'command='], { encoding: 'utf8', timeout: 5_000 });
29
+ return result.status === 0 ? result.stdout.trim() : null;
30
+ },
31
+ ): ReleaseRunState {
32
+ try {
33
+ const result = list();
34
+ if (result.error || result.status === null) return 'unknown';
35
+ if (result.status === 1) return 'idle';
36
+ if (result.status !== 0) return 'unknown';
37
+ const pids = (result.stdout ?? '').trim().split(/\s+/).filter(pid => /^\d+$/.test(pid));
38
+ if (!pids.length) return 'unknown';
39
+ for (const pid of pids) {
40
+ const argv = command(pid);
41
+ if (argv === null) continue;
42
+ const executable = argv.trim().split(/\s+/)[0]?.split('/').at(-1);
43
+ if (['bun', 'node', 'eln', 'elanous'].includes(executable ?? '') && /(?:^|\s)release\s+(?:run|resume)(?:\s|$)/.test(argv)) return 'active';
44
+ // pgrep saw a release-shaped command, but a shell/wrapper cannot prove whether it will launch the CLI.
45
+ }
46
+ return 'unknown';
47
+ } catch { return 'unknown'; }
48
+ }
49
+
50
+ /** A release cut may start after the process probe; the scheduled cut/publication window is a fixed safety boundary. */
51
+ export function releaseWindowState(
52
+ nowMs = Date.now(),
53
+ read: () => ReleaseSchedule[] = listSchedules,
54
+ ): ReleaseWindowState {
55
+ try {
56
+ const schedules = read();
57
+ if (!schedules.length) return 'unknown';
58
+ const rows = schedules.map(row => ({ cut: Date.parse(row.cutAt), publish: Date.parse(row.publishAt ?? '') }));
59
+ if (rows.some(row => !Number.isFinite(row.cut))) return 'unknown';
60
+ const latestPast = rows.filter(row => row.cut <= nowMs).sort((a, b) => b.cut - a.cut)[0];
61
+ if (latestPast && !Number.isFinite(latestPast.publish)) return 'unknown';
62
+ if (latestPast && nowMs <= latestPast.publish + 60 * 60_000) return 'inside';
63
+ const next = rows.filter(row => row.cut > nowMs).sort((a, b) => a.cut - b.cut)[0];
64
+ if (!next || !Number.isFinite(next.publish) || next.publish < next.cut) return 'unknown';
65
+ return nowMs >= next.cut - 40 * 60_000 ? 'inside' : 'outside';
66
+ } catch { return 'unknown'; }
67
+ }
68
+
69
+ export function defaultResyncRun(
70
+ repoRoot = stewardResyncRepoRoot(),
71
+ execute: typeof runResyncShadow = runResyncShadow,
72
+ ): Promise<ResyncShadowSummary> { return execute(3, { repoRoot }, 'live'); }
73
+
74
+ export async function resyncTick(
75
+ run: () => Promise<ResyncShadowSummary> = () => defaultResyncRun(),
76
+ log: (event: string, data: Record<string, unknown>) => void = (event, data) => debug.log('loop.steward', event, data),
77
+ pendingPath: string = resyncPendingPath(),
78
+ ): Promise<ResyncTickResult> { return resyncTickWithReleaseProbe(run, log, pendingPath, probeReleaseRun); }
79
+
80
+ export async function resyncTickWithReleaseProbe(
81
+ run: () => Promise<ResyncShadowSummary>,
82
+ log: (event: string, data: Record<string, unknown>) => void,
83
+ pendingPath: string,
84
+ releaseProbe: () => ReleaseRunState,
85
+ windowProbe: () => ReleaseWindowState = releaseWindowState,
86
+ ): Promise<ResyncTickResult> {
87
+ let result: ResyncTickResult;
88
+ try {
89
+ const release = releaseProbe();
90
+ if (release !== 'idle') {
91
+ result = { decision: 'skipped-release', reason: release === 'active' ? 'release run active' : 'release process probe unavailable' };
92
+ } else {
93
+ const window = windowProbe();
94
+ if (window !== 'outside') {
95
+ result = { decision: 'skipped-release-window', reason: window === 'inside' ? 'scheduled release window' : 'release schedule unavailable' };
96
+ } else {
97
+ const summary = await run();
98
+ if (summary.saved > 0) {
99
+ mkdirSync(dirname(pendingPath), { recursive: true });
100
+ writeFileSync(pendingPath, JSON.stringify({ savedAt: new Date().toISOString() }));
101
+ }
102
+ result = { decision: 'ran', summary };
103
+ }
104
+ }
105
+ }
106
+ catch (error) { result = { decision: 'failed', reason: (error instanceof Error ? error.message : String(error)).slice(-300) }; }
107
+ try { log('resync-tick', { ...result }); }
108
+ catch (error) {
109
+ result = { decision: 'failed', reason: `log failed: ${error instanceof Error ? error.message : String(error)}`.slice(-300) };
110
+ console.error(`steward resync: ${result.reason}`);
111
+ }
112
+ return result;
113
+ }
114
+
115
+ if (import.meta.main) {
116
+ let sinkReady = false;
117
+ try { sinkReady = await (await import('../domains/standalone-log-sink.js')).registerStandaloneLogSink('steward'); }
118
+ catch (error) { console.error(`steward resync: log sink registration failed — ${error instanceof Error ? error.message : String(error)}`); }
119
+ // A missing durable sink must not precede a remote branch update.
120
+ const result = sinkReady ? await resyncTick() : { decision: 'failed' as const, reason: 'log sink unavailable' };
121
+ console.log(`steward resync: ${result.decision}${result.reason ? ` — ${result.reason}` : ''}`);
122
+ process.exitCode = 0;
123
+ }
@@ -166,6 +166,7 @@ export async function executeNextAction(action: NextAction, deps: ActionDeps): P
166
166
  catch { return card('PR labels unavailable'); }
167
167
  if (!Array.isArray(labels) || !labels.every(label => label && typeof label === 'object' && typeof label.name === 'string')) return card('PR labels unavailable');
168
168
  if (labels.some(label => label.name === 'elanous:release-path')) return card('release-path');
169
+ if (labels.some(label => label.name === 'elanous:keep')) return card('elanous:keep');
169
170
  const day = new Date().toLocaleDateString('en-CA', { timeZone: 'Asia/Seoul' });
170
171
  if (deps.landingDay && deps.landingDay !== day) deps.landingsToday = 0;
171
172
  deps.landingDay = day;
@@ -0,0 +1,140 @@
1
+ /** Attach an existing open PR to a TA card without starting another implementation run. */
2
+ import { randomBytes } from 'node:crypto';
3
+ import { loadRunLedger, runLedgerDir, type RunLedgerEntry } from '../self-implement/run-ledger.js';
4
+ import { remoteLedgerMirrorDir } from './parent-host.js';
5
+ import { prodInstanceRoot } from '../instance/resolve.js';
6
+ import { cardPrNumber, updateTaskCard, type TaskCard } from './task-hand.js';
7
+
8
+ export interface AdoptPrView {
9
+ number: number; url: string; state: string; baseRefName: string; headRefOid: string;
10
+ }
11
+ export interface AdoptPrInput {
12
+ pr: AdoptPrView;
13
+ repo: string;
14
+ statePath: string;
15
+ cardId?: string;
16
+ version?: string;
17
+ checklist?: { version: string; id: string; title: string; status: string };
18
+ now?: Date;
19
+ terminalRunEvidence?: boolean;
20
+ unlinkedTerminal?: boolean;
21
+ observedRun?: { runId?: string; runChildId?: string; parentHost?: string; launchId?: string };
22
+ observedChecklist?: { id: string; version: string; status: string; cardVersion?: string };
23
+ checklistAtWrite?: () => { id: string; status: string } | undefined;
24
+ }
25
+
26
+ /** A bound implementation may still push to its PR. Require the child to have stopped on this exact PR first. */
27
+ export function adoptedRunTerminal(card: TaskCard, pr: AdoptPrView,
28
+ load: (runId: string, dir: string) => RunLedgerEntry[] | null = loadRunLedger, allowUnlinked = false): boolean {
29
+ if (!card.runId) return false;
30
+ const dir = card.parentHost ? remoteLedgerMirrorDir(prodInstanceRoot(), card.parentHost.host) : runLedgerDir();
31
+ try {
32
+ const parent = load(card.runId, dir);
33
+ if (!parent) return false;
34
+ const childIds = parent.filter((entry) => entry.event === 'pod-child-run' && typeof entry.data?.childRunId === 'string')
35
+ .map((entry) => String(entry.data.childRunId));
36
+ const candidates = childIds.length ? childIds : [card.runId];
37
+ let allStopped = candidates.length > 0;
38
+ let noPrEvents = true;
39
+ let exactMatches = 0;
40
+ for (const childId of candidates) {
41
+ const child = childId === card.runId ? parent : load(childId, dir);
42
+ if (!child) { allStopped = false; continue; }
43
+ let prAt = -1;
44
+ let statusAt = -1;
45
+ for (let at = child.length - 1; at >= 0 && (prAt < 0 || statusAt < 0); at--) {
46
+ if (prAt < 0 && child[at]?.event === 'pr-opened') prAt = at;
47
+ if (statusAt < 0 && child[at]?.event === 'run-status') statusAt = at;
48
+ }
49
+ const opened = child[prAt];
50
+ const status = child[statusAt];
51
+ if (prAt >= 0) noPrEvents = false;
52
+ if (!status || !['completed', 'failed', 'cancelled'].includes(String(status.data?.runStatus))) allStopped = false;
53
+ if ((!card.runChildId || card.runChildId === childId) && opened?.data?.number === pr.number && opened.data?.url === pr.url && statusAt > prAt) exactMatches++;
54
+ }
55
+ if (!allStopped) return false;
56
+ if (exactMatches === 1) return true;
57
+ if (allowUnlinked && exactMatches === 0 && noPrEvents && (!card.runChildId || candidates.includes(card.runChildId))) return true;
58
+ } catch { /* unreadable is not terminal evidence */ }
59
+ return false;
60
+ }
61
+
62
+ /** The PR is read from the selected repository before this call; rejudge checks it again before every live move. */
63
+ export function adoptExistingPr(input: AdoptPrInput): { card: TaskCard; created: boolean; changed: boolean } {
64
+ const { pr } = input;
65
+ const slug = pr.url.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/\d+\/?$/)?.[1];
66
+ if (!Number.isSafeInteger(pr.number) || pr.number < 1 || pr.state !== 'OPEN' || pr.baseRefName !== 'main'
67
+ || !slug || slug.toLowerCase() !== input.repo.toLowerCase()
68
+ || Number(pr.url.match(/\/pull\/(\d+)/)?.[1]) !== pr.number
69
+ || !/^[a-f0-9]{40}$/i.test(pr.headRefOid)) throw new Error('입양할 PR은 현재 저장소의 OPEN/main PR 번호·URL·head SHA40이어야 한다');
70
+ if ((input.cardId === undefined) === (input.checklist === undefined)) throw new Error('--card 또는 --checklist/--version 중 하나만 준다');
71
+ if (input.cardId && !input.version) throw new Error('기존 카드는 체크리스트 판을 확정해야 한다');
72
+ if (input.checklist && (!input.checklist.id || !input.checklist.title || !input.checklist.version
73
+ || ['green', 'done'].includes(input.checklist.status))) throw new Error('입양할 체크리스트 칸은 미완 판·id·제목이어야 한다');
74
+ const id = input.cardId ?? `ta-${(input.now ?? new Date()).toISOString().slice(0, 10).replaceAll('-', '')}-${randomBytes(3).toString('hex')}`;
75
+ const at = (input.now ?? new Date()).toISOString();
76
+ let created = false;
77
+ let changed = false;
78
+ const result = updateTaskCard(input.statePath, id, (current, tasks) => {
79
+ const duplicate = Object.values(tasks).find((card) => card.id !== id && cardPrNumber(card) === pr.number);
80
+ if (duplicate) throw new Error(`PR #${pr.number}는 이미 카드 ${duplicate.id}에 묶였다`);
81
+ const checklistId = current?.checklistId ?? input.checklist?.id;
82
+ const checklistVersion = input.version ?? input.checklist?.version;
83
+ const sameCell = Object.values(tasks).find((card) => card.id !== id && card.status === 'launched' && card.taLand === true
84
+ && cardPrNumber(card) !== undefined && card.checklistId === checklistId
85
+ && (!card.checklistVersion || !checklistVersion || card.checklistVersion === checklistVersion));
86
+ if (sameCell) throw new Error(`칸 ${checklistId}에 이미 TA 착지 후보 카드 ${sameCell.id}가 있다`);
87
+ if (!input.cardId) {
88
+ const owner = Object.values(tasks).find((card) => card.status === 'launched' && card.checklistId === checklistId
89
+ && (!card.checklistVersion || !checklistVersion || card.checklistVersion === checklistVersion));
90
+ if (owner) throw new Error(`칸 ${checklistId}에 기존 카드 ${owner.id}가 있다 — --card로 그 카드를 선택한다`);
91
+ }
92
+ if (input.cardId && (!input.observedRun || current?.runId !== input.observedRun.runId
93
+ || current?.runChildId !== input.observedRun.runChildId
94
+ || current?.parentHost?.host !== input.observedRun.parentHost
95
+ || current?.launchId !== input.observedRun.launchId))
96
+ throw new Error(`입양 중 카드 런 식별자가 바뀌었다: ${id}`);
97
+ if (input.cardId && (!input.observedChecklist || current?.checklistId !== input.observedChecklist.id
98
+ || current?.checklistVersion !== input.observedChecklist.cardVersion
99
+ || input.version !== input.observedChecklist.version
100
+ || ['green', 'done'].includes(input.observedChecklist.status)))
101
+ throw new Error(`입양 중 카드 칸·판이 바뀌었다: ${id}`);
102
+ const liveCell = input.checklistAtWrite?.();
103
+ if (input.checklistAtWrite && (!liveCell || liveCell.id !== checklistId || ['green', 'done'].includes(liveCell.status)))
104
+ throw new Error(`입양 중 체크리스트 칸이 바뀌거나 완료됐다: ${checklistId}`);
105
+ if (current?.adoptedPr?.pr === pr.number && cardPrNumber(current) === pr.number && current.taLand === true
106
+ && typeof current.pr === 'object' && current.pr.url === pr.url) {
107
+ if (current.status !== 'launched') throw new Error(`이미 닫힌 입양 카드다: ${id} (${current.status})`);
108
+ if (current.checklistVersion !== (input.version ?? input.checklist?.version)) throw new Error(`이미 입양한 카드의 판이 다르다: ${current.checklistVersion ?? '미기록'} ≠ ${input.version ?? input.checklist?.version}`);
109
+ return undefined;
110
+ }
111
+ if (input.cardId) {
112
+ const boundPr = current && cardPrNumber(current);
113
+ const sameBoundPr = boundPr === pr.number && typeof current?.pr === 'object' && current.pr.url === pr.url;
114
+ if (!current || current.status !== 'launched' || !current.checklistId
115
+ || (boundPr !== undefined && !sameBoundPr)
116
+ || (input.checklist && current.checklistId !== input.checklist.id)) throw new Error(`입양할 launched·미바인딩/동일 PR 바인딩 카드가 아니다: ${id}`);
117
+ if (boundPr !== undefined && input.unlinkedTerminal)
118
+ throw new Error(`이미 PR이 바인딩된 카드는 --unlinked-terminal로 입양할 수 없다: ${id}`);
119
+ if (input.terminalRunEvidence !== true) throw new Error(`구현 런 종결·PR 근거가 없다: ${current.runId ?? current.launchId ?? 'runId 없음'} — runId 없는 카드는 tasks close 후 --checklist 새 카드로 입양`);
120
+ if (input.version && current.checklistVersion && current.checklistVersion !== input.version)
121
+ throw new Error(`카드 판이 다르다: ${current.checklistVersion} ≠ ${input.version}`);
122
+ } else {
123
+ if (current) throw new Error(`새 카드 id 충돌: ${id}`);
124
+ }
125
+ const base: TaskCard = current ?? {
126
+ id, text: input.checklist!.title, checklistId: input.checklist!.id,
127
+ createdAt: at, status: 'launched', history: [],
128
+ };
129
+ created = !current;
130
+ changed = true;
131
+ return { ...base, ...(input.version || input.checklist ? { checklistVersion: input.version ?? input.checklist!.version } : {}),
132
+ taLand: true, pr: { number: pr.number, url: pr.url },
133
+ adoptedPr: { pr: pr.number, url: pr.url, head: pr.headRefOid, at },
134
+ history: [...base.history, { at, event: 'pr-adopted', pr: pr.number, head: pr.headRefOid,
135
+ detail: `기존 OPEN PR 입양 · ${pr.url} · 새 구현 발사 0${input.unlinkedTerminal ? ' · 명시적 비연결 종결 원장' : ''}` }],
136
+ };
137
+ });
138
+ if (!result) throw new Error(`카드 쓰기 실패: ${id}`);
139
+ return { card: result, created, changed };
140
+ }
@@ -12,7 +12,7 @@ import { releaseLedgerRoot } from '../instance/resolve.js';
12
12
  import { listChecklist, type ChecklistStatus } from '../release-loop/checklist.js';
13
13
  import { buildRunProgressRows, queryRunningRuns, type RunningRunsResult, type RunProgressRow } from '../self-implement/running-runs.js';
14
14
  import {
15
- cardPrNumber, CLOSED_BY_HAND_EVENT, HAND_MOVE_HISTORY_EVENT, LIVE_MOVE_HISTORY_EVENT, LIVE_MOVE_RESULT_EVENT, readTaskAgentState,
15
+ boundPrTerminal, cardPrNumber, CLOSED_BY_HAND_EVENT, HAND_MOVE_HISTORY_EVENT, LIVE_MOVE_HISTORY_EVENT, LIVE_MOVE_RESULT_EVENT, readTaskAgentState,
16
16
  type CompletionKind, type TaskCard,
17
17
  } from './task-hand.js';
18
18
 
@@ -29,6 +29,10 @@ export interface BoardTaskLedger {
29
29
  lastEvent: { at: string; event: string; detail: string | null } | null;
30
30
  /** 카드가 failed · launch-failed 일 때 마지막 줄의 사유(예: `failed/needs-owner — exit 1 · pod-failure`). 그 밖에는 null. */
31
31
  exitReason: string | null;
32
+ /** Terminal PR observation, only when it still matches this card's binding. */
33
+ prTerminal?: NonNullable<TaskCard['prTerminal']>;
34
+ /** Checklist judgement after merge; partial means this slice ended while the cell remains open. */
35
+ postLand?: NonNullable<TaskCard['postLand']>;
32
36
  }
33
37
 
34
38
  /** `GET /api/task-agent/board` 응답 — progress 는 진전 관측(`queryRunningRuns`)을 싣는다; 관측을 못 얻으면 null(0을 «없다»로 쓰지 않는다). */
@@ -87,11 +91,14 @@ function aggregate(node: BoardNode): void {
87
91
  }
88
92
 
89
93
  function cardStatus(card: TaskCard): BoardStatus {
94
+ const terminal = boundPrTerminal(card);
95
+ if (terminal) return terminal.state === 'MERGED' ? 'done' : 'blocked';
90
96
  if (card.status === 'launch-failed' || card.status === 'failed') return 'blocked';
91
97
  return card.greenProposal ? 'done' : 'open';
92
98
  }
93
99
 
94
100
  function cardLedger(card: TaskCard): BoardTaskLedger {
101
+ const terminal = boundPrTerminal(card);
95
102
  const last = (card.history ?? [])
96
103
  .filter((item) => item.event !== LIVE_MOVE_HISTORY_EVENT && item.event !== LIVE_MOVE_RESULT_EVENT && item.event !== HAND_MOVE_HISTORY_EVENT)
97
104
  .at(-1);
@@ -105,6 +112,9 @@ function cardLedger(card: TaskCard): BoardTaskLedger {
105
112
  lastStatus: card.status,
106
113
  lastEvent: last ? { at: last.at, event: last.event, detail: last.detail ?? null } : null,
107
114
  exitReason: ended ? reason ?? null : null,
115
+ ...(terminal ? { prTerminal: terminal } : {}),
116
+ ...(terminal && terminal.state === 'MERGED' && card.postLand?.pr === terminal.pr && card.postLand.head === terminal.head
117
+ ? { postLand: card.postLand } : {}),
108
118
  };
109
119
  }
110
120
 
@@ -17,6 +17,7 @@ import { appendRunLedgerEntry, type RunLedgerEntry } from '../self-implement/run
17
17
  import { cardPrNumber, taskAgentStatePath, updateTaskCard, readTaskCard, type TaskCard } from './task-hand.js';
18
18
  import { judgeNextMove, type TaskJudgeInput } from './judge.js';
19
19
  import { remoteParentAlive, type SshRunner } from './parent-host.js';
20
+ import { notifyRemoteFailure, remoteFailureReason } from './failure-notify.js';
20
21
 
21
22
  const GOAL_ID = /^[a-f0-9]{16}$/;
22
23
  const PR_URL = /^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/(\d+)$/;
@@ -134,6 +135,8 @@ export interface CardRunEvidenceDeps {
134
135
  dispatchPod?: (runId: string, sinceIso: string) => boolean;
135
136
  runExit?: (runId: string) => RunExit | undefined;
136
137
  ssh?: SshRunner;
138
+ /** Isolated tests can put incident messages in their own mailbox; production uses the resolved default store. */
139
+ incidentStorePath?: string;
137
140
  }
138
141
 
139
142
  async function defaultLoadLedger(): Promise<(runId: string) => RunLedgerEntry[] | null> {
@@ -145,8 +148,7 @@ async function defaultLoadLedger(): Promise<(runId: string) => RunLedgerEntry[]
145
148
  // HARNESS-PARENT-ON-MSB1: 원격 부모 런의 원장 거울(`tasks show` 가 ssh 로 당긴다) — 이 우주 ⊕ 연합 «뒤»에만 붙는다.
146
149
  try {
147
150
  const { remoteLedgerMirrorDirs } = await import('./parent-host.js');
148
- const { prodInstanceRoot } = await import('../instance/resolve.js');
149
- dirs = [...new Set([...dirs, ...remoteLedgerMirrorDirs(prodInstanceRoot())])];
151
+ dirs = [...new Set([...dirs, ...remoteLedgerMirrorDirs(effectiveInstanceRoot())])];
150
152
  } catch { /* 거울이 없으면 종전 그대로 */ }
151
153
  return ledgerLoader(dirs, (runId, dir) => loadRunLedger(runId, dir), federationFailure);
152
154
  }
@@ -216,16 +218,46 @@ export async function readCardRunSnapshot(card: Pick<TaskCard, 'runId' | 'create
216
218
  */
217
219
  export async function refreshCardRunBinding(cardId: string, statePath = taskAgentStatePath(), deps: CardRunEvidenceDeps = {}): Promise<{ card: TaskCard | undefined; evidence?: CardRunEvidence }> {
218
220
  let card = readTaskCard(cardId, statePath);
219
- // 수동 근거(refSource manual)는 덮지 않는다 · 둘 다 차 있으면 다시 읽지 않는다(한 번만) —
220
- // 단 «10분 발사 근거 없음»으로 failed 된 카드는 PR·골이 먼저 묶였어도 되살림 판정을 계속 받는다.
221
+ // 수동 근거(refSource manual)는 아래 바인딩에서 덮지 않는다. 원격 부모의 확정 실패는
222
+ // 수동 근거가 있어도 읽어야 하므로 여기서 반환하지 않는다.
223
+ // 둘 다 차 있으면 다시 읽지 않는다(한 번만) — 단 «10분 발사 근거 없음»으로 failed 된 카드는
224
+ // PR·골이 먼저 묶였어도 되살림 판정을 계속 받는다.
221
225
  const awaitingRevival = card?.status === 'failed' && [...card.history].reverse().find((item) => item.event === 'failed')?.detail === LATE_LAUNCH_FAILURE;
222
- if (!card?.runId || card.refSource === 'manual' || (card.pr !== undefined && card.goalId !== undefined && !awaitingRevival)) return { card };
226
+ if (!card?.runId || (card.pr !== undefined && card.goalId !== undefined && !awaitingRevival)) return { card };
223
227
  const runId = card.runId;
228
+ // A stale HQ mirror cannot prove a terminal remote failure. Refresh it here as well as in `tasks show`,
229
+ // since mission advance and direct callers also reach this function.
230
+ if (card.parentHost && !deps.loadLedger) {
231
+ try {
232
+ const { mirrorRemoteRunLedgers } = await import('./parent-host.js');
233
+ mirrorRemoteRunLedgers(card.parentHost, runId, effectiveInstanceRoot(), deps.ssh);
234
+ } catch (error) {
235
+ try { debug.log('task-agent', 'card-bind-failed', { card: cardId, runId, reason: `remote ledger refresh failed: ${error instanceof Error ? error.message : String(error)}` }); } catch { /* fail-soft */ }
236
+ return { card };
237
+ }
238
+ }
224
239
  const snapshot = await readCardRunSnapshot(card, deps);
225
240
  if (snapshot.readErrors?.length) {
226
241
  try { debug.log('task-agent', 'card-bind-failed', { card: cardId, runId: card.runId, errors: snapshot.readErrors }); } catch { /* fail-soft */ }
227
242
  }
228
243
  const evidence = bindCardEvidence(card, snapshot);
244
+ const remoteReason = remoteFailureReason(card, snapshot, evidence, deps.ssh);
245
+ if (remoteReason && (card.status === 'launched' || card.status === 'failed')) {
246
+ if (card.status === 'launched') {
247
+ const at = (deps.now ?? (() => new Date()))().toISOString();
248
+ card = updateTaskCard(statePath, cardId, (current) => current?.status === 'launched' && current.runId === runId && !current.pr
249
+ ? { ...current, status: 'failed', history: [...current.history, { at, event: 'failed', detail: `failed/needs-owner — ${remoteReason}`, runId }] }
250
+ : undefined) ?? card;
251
+ }
252
+ // An older generic Pod/no-launch failure may have closed this same run before its supervisor tail arrived.
253
+ // The terminal evidence still warrants an incident; message provenance keeps retries at one row.
254
+ const lastFailure = [...card.history].reverse().find((row) => row.event === 'failed');
255
+ if (card.status === 'failed' && lastFailure?.runId === runId && !card.history.some((row) => row.event === 'closed-by-hand')) {
256
+ try { notifyRemoteFailure(card, remoteReason, deps.incidentStorePath); }
257
+ catch (error) { try { debug.log('task-agent', 'failure-notify-failed', { card: cardId, runId, reason: error instanceof Error ? error.message : String(error) }); } catch { /* fail-soft */ } }
258
+ return { card, evidence };
259
+ }
260
+ }
229
261
  let revived = false;
230
262
  const lastFailure = [...card.history].reverse().find((item) => item.event === 'failed');
231
263
  if (card.status === 'failed' && lastFailure?.event === 'failed' && lastFailure.detail === LATE_LAUNCH_FAILURE