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
@@ -15,8 +15,8 @@ import { extname, join, relative, resolve } from 'node:path';
15
15
  import { debug } from '../debug/log.js';
16
16
  import { readTaskAgentState, readTaskCard, taskAgentStatePath, TASK_CARD_PREFIX, type CompletionKind, type TaskCard } from './task-hand.js';
17
17
  import type { SupervisorJobResult, SupervisorStopReason } from '../self-dev/run-supervisor.js';
18
- import { executeNextAction, type DeliveryKind, type NextAction } from './actions.js';
19
- import { judgeNextMove, type TaskJudgeInput, type TaskJudgement } from './judge.js';
18
+ import { executeNextAction, type ActionDeps, type DeliveryKind, type NextAction } from './actions.js';
19
+ import { judgeNextMove, LIVE_RETRY_MOVES, type TaskJudgeInput, type TaskJudgement } from './judge.js';
20
20
  import { HARNESS_RUN_ID_ENV } from '../harness/harness-space.js';
21
21
  import { appendRunLedgerEntry, loadRunLedger } from '../self-implement/run-ledger.js';
22
22
  import { collectWiringEvidence, readCardRunSnapshot, type WiringEvidenceDeps, type CardRunEvidenceDeps } from './card-evidence.js';
@@ -57,8 +57,10 @@ export interface TaskAgentShadowMove {
57
57
 
58
58
  export interface TaskAgentShadowDeps {
59
59
  log?: (category: string, event: string, data: Record<string, unknown>) => void;
60
- /** 시험용 — shadow 실행부가 명령을 부르면 여기로 온다(정상이면 0회). */
61
- command?: (args: string[]) => Promise<{ status: number; stdout: string; stderr?: string }>;
60
+ /** 재발사 live 실행부 명령(주입되지 않으면 shadow). */
61
+ command?: ActionDeps['command'];
62
+ /** 재발사 live 부하 관문(주입되지 않으면 shadow). */
63
+ loadGate?: ActionDeps['loadGate'];
62
64
  readCard?: (taskId: string) => TaskCard | undefined;
63
65
  /** Explicit live invocation only; supervisor's default hook always uses shadow. */
64
66
  mode?: 'shadow' | 'live';
@@ -265,6 +267,9 @@ export async function recordTaskAgentShadowMove(input: TaskAgentShadowInput, dep
265
267
  : { move: 'wait', stage: 'waiting', reason: '전달 후보 없음' }
266
268
  : judgeNextMove(judgeInput);
267
269
  if (cardLookupFailed) judgement.reason = `${judgement.reason} · 종결 종류 미확인(카드 조회 실패)`;
270
+ // 판단마다 해석한다 — 허용되지 않은 수를 판단할 때도 모르는 항목 경고가 남게.
271
+ const liveMoves = deps.liveMoves ?? await configuredTaskAgentLiveMoves();
272
+ const retryLaunches = { count: 0 };
268
273
  let executorResult: string | null = null;
269
274
  let wouldDo: string | null = null;
270
275
  if (judgement.executorKind) {
@@ -279,10 +284,13 @@ export async function recordTaskAgentShadowMove(input: TaskAgentShadowInput, dep
279
284
  checklistId: delivery ? card?.checklistId ?? '' : '',
280
285
  ...(delivery ? { delivery } : {}),
281
286
  };
287
+ const liveRetry = !delivery && deps.command !== undefined && deps.loadGate !== undefined
288
+ && (LIVE_RETRY_MOVES as readonly string[]).includes(judgement.move) && liveMoves.has(judgement.move as TaskAgentLiveMove);
282
289
  executorResult = await executeNextAction(action, {
283
- mode: delivery ? (deps.mode ?? 'shadow') : 'shadow',
290
+ mode: delivery ? (deps.mode ?? 'shadow') : liveRetry ? 'live' : 'shadow',
284
291
  command: deps.command ?? (async () => { throw new Error('task-agent shadow path must not run commands'); }),
285
292
  observe: (_event, data) => { wouldDo = data.reason; },
293
+ ...(liveRetry ? { liveMoves, retryLaunches, loadGate: deps.loadGate } : {}),
286
294
  });
287
295
  }
288
296
  const move: TaskAgentShadowMove = {
@@ -305,8 +313,6 @@ export async function recordTaskAgentShadowMove(input: TaskAgentShadowInput, dep
305
313
  move.wouldDo = null;
306
314
  }
307
315
  const liveKind: TaskAgentLiveMove | null = move.move === 'review' || move.move === 'propose-green' || move.move === 'propose-land' || move.move === 'decision-card' ? move.move : null;
308
- // 판단마다 해석한다 — 허용되지 않은 수를 판단할 때도 모르는 항목 경고가 남게.
309
- const liveMoves = deps.liveMoves ?? await configuredTaskAgentLiveMoves();
310
316
  // 카드 id 는 모든 판단 관측에 싣는다(그림자 일치율을 카드로 잇는다) — 읽기 전용 조회.
311
317
  // live 수는 «근거 결과»에 묶는다 — PR 수는 그 PR 을 낸 결과의 카드·작업 트리, 전달은 전달한 카드(다중 결과에서 남의 카드에 쓰지 않게).
312
318
  const prResult = typeof judgeInput.pr === 'number' ? input.results.find((result) => result.prNumber === judgeInput.pr) : undefined;
@@ -11,6 +11,10 @@ import { execFileSync } from 'node:child_process';
11
11
  import { createHash, randomBytes } from 'node:crypto';
12
12
  import { mkdirSync, readFileSync, statSync, writeFileSync, type Stats } from 'node:fs';
13
13
  import { dirname, join, resolve } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { parse as parseYaml } from 'yaml';
16
+ import { validateUnitCalls } from '../graph-runner/runner.js';
17
+ import { parseGraphTemplateYaml } from '../self-implement/graph-yaml.js';
14
18
  import { resolveDaemonHarnessTarget } from '../intake-plane/harness-target.js';
15
19
  import { HARNESS_RUN_ID_ENV, mintRunId, normalizeRunId } from '../harness/harness-space.js';
16
20
  import { debug } from '../debug/log.js';
@@ -89,6 +93,66 @@ export function taskCardLabelContext(card: Pick<TaskCard, 'id'>): TaskCardLabelC
89
93
  export const COMPLETION_KINDS = ['code-pr', 'artifact', 'research-report', 'ops-action', 'content', 'watch-brief', 'decision-support', 'deliverable'] as const;
90
94
  export type CompletionKind = typeof COMPLETION_KINDS[number];
91
95
 
96
+ export interface TaskComposition {
97
+ file: string;
98
+ /** null = 파일은 있으나 파싱 실패로 판을 확인하지 못했다. */
99
+ version: number | null;
100
+ }
101
+
102
+ export interface CompositionSelection {
103
+ composition?: TaskComposition;
104
+ unitsResolved: boolean;
105
+ missingUnits: string[];
106
+ launcher?: 'harness-say' | 'graph-run';
107
+ error?: string;
108
+ }
109
+
110
+ /** 1판은 조합을 고르고 단위 해석 결과를 기록한다. 단위 실행은 2판의 몫이다. */
111
+ export function selectTaskComposition(completion: CompletionKind, root = join(dirname(fileURLToPath(import.meta.url)), '..', '..')): CompositionSelection {
112
+ const file = `graphs/work/${completion}.yaml`;
113
+ const path = join(root, file);
114
+ let source: string;
115
+ try { source = readFileSync(path, 'utf8'); } catch (error) {
116
+ if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return { unitsResolved: false, missingUnits: [] };
117
+ return { composition: { file, version: null }, unitsResolved: false, missingUnits: [],
118
+ error: `조합 읽기 오류: ${file}: ${error instanceof Error ? error.message : String(error)}` };
119
+ }
120
+ const parsed = parseGraphTemplateYaml(source, path);
121
+ if (!parsed.template || parsed.errors.length) {
122
+ let version: number | null = null;
123
+ try {
124
+ const raw: unknown = parseYaml(source);
125
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
126
+ const candidate = (raw as Record<string, unknown>).version;
127
+ if (typeof candidate === 'number' && Number.isInteger(candidate)) version = candidate;
128
+ }
129
+ } catch { /* 파싱 실패 자체를 아래 원장에 적는다. */ }
130
+ return { composition: { file, version }, unitsResolved: false, missingUnits: [],
131
+ error: `조합 오류: ${file}: ${parsed.errors.map(e => e.message).join('; ')}` };
132
+ }
133
+ const raw: unknown = parseYaml(source);
134
+ const launcher = raw && typeof raw === 'object' && !Array.isArray(raw) ? (raw as Record<string, unknown>).launcher : undefined;
135
+ if (launcher !== undefined && launcher !== 'harness-say' && launcher !== 'graph-run') {
136
+ return { composition: { file, version: parsed.template.version }, unitsResolved: false, missingUnits: [],
137
+ error: `조합 오류: ${file}: 모르는 launcher` };
138
+ }
139
+ if (completion === 'code-pr' && launcher === undefined) {
140
+ return { composition: { file, version: parsed.template.version }, unitsResolved: false, missingUnits: [],
141
+ error: `조합 오류: ${file}: launcher 없음` };
142
+ }
143
+ const missingUnits: string[] = [];
144
+ let unitError: string | undefined;
145
+ try { validateUnitCalls(parsed.template, path, [], call => missingUnits.push(call)); }
146
+ catch (error) { unitError = error instanceof Error ? error.message : String(error); }
147
+ return {
148
+ composition: { file, version: parsed.template.version },
149
+ unitsResolved: missingUnits.length === 0 && !unitError,
150
+ missingUnits,
151
+ ...(launcher === 'harness-say' || launcher === 'graph-run' ? { launcher } : {}),
152
+ ...(unitError ? { error: `조합 단위 해석 오류: ${file}: ${unitError}` } : {}),
153
+ };
154
+ }
155
+
92
156
  /**
93
157
  * `run-bound`·`goal-bound`·`pr-bound` 은 runId(⊕ pr)를 싣는다 — 옛 카드의 칸은 at·event·detail 뿐이다.
94
158
  * TA-LIVE-MOVE-CARD-HISTORY — `live-move` 줄만 kind·executed·ok·executorResult·head·effect 를 더 싣는다(`live-move-history.ts`).
@@ -104,6 +168,8 @@ export interface TaskCardEvent {
104
168
  resultPath?: string;
105
169
  /** TA-REVIEW-RESULT-CAPTURE — `live-move-result` 줄만: 회수한 `self review --json` 의 칸(못 읽은 칸은 null). */
106
170
  verdict?: string | null; reviewed?: boolean | null; mustFix?: number | null; reviewRoute?: string | null;
171
+ /** Exact bytes of the captured review stdout; repair must read the same resultPath and digest. */
172
+ reviewSha256?: string;
107
173
  }
108
174
  /** 카드 history 의 live-move 줄 이벤트 이름 — 상태 사유(«마지막 칸») 판정에서 뺀다. */
109
175
  export const LIVE_MOVE_HISTORY_EVENT = 'live-move';
@@ -117,8 +183,15 @@ export interface TaskCard {
117
183
  text: string;
118
184
  seat?: TaskSeat;
119
185
  checklistId?: string;
186
+ /** Explicit release version for an adopted PR; post-land still confirms the cell exists there. */
187
+ checklistVersion?: string;
120
188
  /** 종결 종류 — 비면 code-pr. */
121
189
  completion?: CompletionKind;
190
+ /** 선택된 일 조합 파일과 판(1판: 실행은 launcher 에 위임). */
191
+ composition?: TaskComposition;
192
+ graphSelectedBy?: 'completion';
193
+ unitsResolved?: boolean;
194
+ missingUnits?: string[];
122
195
  /** 과제 대상 프로젝트(§A4b②) — target 은 절대 경로(git 이 아닐 수 있다). id 는 `task hand --project` 가 싣는다(옛 카드엔 없을 수 있다). */
123
196
  project?: { id?: string; target: string };
124
197
  /** 보드(§A4b① `board.ts`) 소속 — 목표 id · 이정표 id(`task hand --goal/--milestone`). 조각 카드의 `goalId`(골 sha 16자)와 다른 칸이다. */
@@ -130,7 +203,7 @@ export interface TaskCard {
130
203
  * failed = 발사된 런이 PR·수확 가지 없이 죽었거나 끝내 안 떴다(TA-JUDGE-DEAD-RUN · `card-evidence.ts`) — 사유
131
204
  * (`failed/needs-owner|needs-relaunch — …`)는 history 마지막 칸 · 자동 재발사하지 않는다(사람/판단부 몫).
132
205
  */
133
- status: 'handed' | 'launched' | 'launch-failed' | 'failed';
206
+ status: 'handed' | 'launched' | 'launch-failed' | 'failed' | 'done';
134
207
  history: TaskCardEvent[];
135
208
  /** 미션 카드만 — 조각 카드 id(순서대로) · 쪼갠 출처(`mission.ts` splitMission). */
136
209
  pieces?: string[];
@@ -145,6 +218,12 @@ export interface TaskCard {
145
218
  * 번호는 `cardPrNumber` 로 읽는다(옛 카드의 number 도 그대로 읽힌다).
146
219
  */
147
220
  pr?: number | TaskCardPr;
221
+ /** Host-verified existing OPEN PR adopted without launching a new implementation run. */
222
+ adoptedPr?: { pr: number; url: string; head: string; at: string };
223
+ /** Bound PR observed in a terminal GitHub state. Only applies while the card still binds the same PR and URL. */
224
+ prTerminal?: { pr: number; url: string; head: string; state: 'MERGED' | 'CLOSED'; at: string };
225
+ /** Post-land checklist judgement. A partial slice closes this card while leaving the checklist cell yellow. */
226
+ postLand?: { pr: number; head: string; at: string; outcome: 'complete' | 'partial'; evidence: string[]; remaining: string[] };
148
227
  goalId?: string;
149
228
  /** `manual` = pr/goalId 를 사람이 적었다(`recordPieceRef`) — 런 묶기가 건드리지 않는다. */
150
229
  refSource?: 'manual';
@@ -185,6 +264,10 @@ export interface TaskCard {
185
264
  taLand?: true;
186
265
  /** TASKS-HAND-CHILD-LLM — `tasks hand --child-llm <provider>/<model> [--child-llm-effort <level>]` → 발사 인자 `--child-llm-provider/-model/-effort`. */
187
266
  childLlm?: TaskChildLlm;
267
+ /** Same-PR repair launches; exact source head and worker result are retained for CAS and retry limits. */
268
+ repairs?: Array<{ pr: number; head: string; branch: string; at: string; move: string; runId: string;
269
+ resultPath: string; fromModel: string; toModel: string; status: 'requested' | 'unconfirmed' | 'failed' | 'applied';
270
+ detail?: string; commit?: string; bundleSha256?: string; bundlePath?: string }>;
188
271
  }
189
272
 
190
273
  /** 구현 자식 LLM 선택 — 하니스 `--child-llm-provider/--child-llm-model/--child-llm-effort` 로 그대로 넘긴다(검증 권위는 하니스). */
@@ -215,6 +298,12 @@ export function cardPrNumber(card: Pick<TaskCard, 'pr'>): number | undefined {
215
298
  return typeof card.pr === 'number' ? card.pr : card.pr?.number;
216
299
  }
217
300
 
301
+ /** A stale terminal marker must never close a card after its PR binding changes. */
302
+ export function boundPrTerminal(card: Pick<TaskCard, 'pr' | 'prTerminal'>): TaskCard['prTerminal'] | undefined {
303
+ const marker = card.prTerminal;
304
+ return marker && typeof card.pr === 'object' && card.pr.number === marker.pr && card.pr.url === marker.url ? marker : undefined;
305
+ }
306
+
218
307
  /** A uniquely launched code-PR card may own a draft through its PR or its opening run. */
219
308
  export function draftTaskOwner(cards: readonly TaskCard[], draft: { number: number; runId?: string | null }): TaskCard | undefined {
220
309
  const owners = cards.filter((card) => card.status === 'launched' && (card.completion ?? 'code-pr') === 'code-pr'
@@ -374,6 +463,8 @@ export interface HandTaskOptions {
374
463
  childLlm?: string;
375
464
  childLlmEffort?: string;
376
465
  statePath?: string;
466
+ /** 시험에서만 조합 저장소를 바꿔 해석·거부를 검증한다. */
467
+ compositionRoot?: string;
377
468
  /** `--live` 일 때만 불린다. */
378
469
  launcher?: TaskLauncher;
379
470
  now?: () => Date;
@@ -443,6 +534,11 @@ export async function handTask(opts: HandTaskOptions): Promise<HandTaskResult> {
443
534
  }
444
535
  if (opts.childLlmEffort !== undefined && opts.childLlm === undefined) throw new Error('--child-llm-effort 는 --child-llm <provider>/<model> 과 함께 준다');
445
536
  const childLlm = opts.childLlm !== undefined ? parseChildLlm(opts.childLlm, opts.childLlmEffort) : undefined;
537
+ const completion = opts.completion ?? 'code-pr';
538
+ const selected = selectTaskComposition(completion, opts.compositionRoot);
539
+ const selectionFailure = !selected.composition ? `조합 없음: graphs/work/${completion}.yaml`
540
+ : selected.error ?? (completion !== 'code-pr' || selected.launcher !== 'harness-say'
541
+ ? `조합 실행기 없음: ${selected.composition.file} (1판은 code-pr harness-say만 발사)` : undefined);
446
542
  let project: TaskCard['project'];
447
543
  let targetIsGit: boolean | null = null;
448
544
  if (opts.project) {
@@ -467,19 +563,25 @@ export async function handTask(opts: HandTaskOptions): Promise<HandTaskResult> {
467
563
  ...(opts.seat ? { seat: opts.seat } : {}),
468
564
  ...(opts.checklistId ? { checklistId: opts.checklistId } : {}),
469
565
  ...(opts.completion ? { completion: opts.completion } : {}),
566
+ ...(selected.composition ? { composition: selected.composition, graphSelectedBy: 'completion' as const } : {}),
567
+ unitsResolved: selected.unitsResolved,
568
+ missingUnits: selected.missingUnits,
470
569
  ...(project ? { project } : {}),
471
570
  ...(opts.goal ? { goal: opts.goal } : {}),
472
571
  ...(opts.milestone ? { milestone: opts.milestone } : {}),
473
572
  ...(opts.taLand === true ? { taLand: true as const } : {}),
474
573
  ...(childLlm ? { childLlm } : {}),
475
574
  createdAt: now.toISOString(),
476
- status: 'handed',
477
- history: [],
575
+ status: selectionFailure ? 'failed' : 'handed',
576
+ history: selectionFailure ? [{ at: now.toISOString(), event: !selected.composition ? 'composition-missing'
577
+ : selected.error ? 'composition-invalid' : 'composition-launcher-unavailable', detail: selectionFailure }] : [],
478
578
  ...(opts.mission ? { mission: opts.mission, after: [...(opts.after ?? [])] } : {}),
479
- ...(opts.cardFields ?? {}),
579
+ ...(opts.cardFields?.pr !== undefined ? { pr: opts.cardFields.pr } : {}),
580
+ ...(opts.cardFields?.goalId !== undefined ? { goalId: opts.cardFields.goalId } : {}),
581
+ ...(opts.cardFields?.handed !== undefined ? { handed: opts.cardFields.handed } : {}),
480
582
  };
481
- const move = nextMoveFor(card, opts.landed);
482
- if (move.kind !== 'launch') throw new Error(`발사할 수 없는 과제다 — ${move.reason}`);
583
+ const move = selectionFailure ? undefined : nextMoveFor(card, opts.landed);
584
+ if (move && move.kind !== 'launch') throw new Error(`발사할 수 없는 과제다 — ${move.reason}`);
483
585
  const guard = opts.writeGuard;
484
586
  const writeCard = (target: string, next: TaskCard): void => {
485
587
  if (!guard) { writeTaskCards(target, [next]); return; }
@@ -495,6 +597,13 @@ export async function handTask(opts: HandTaskOptions): Promise<HandTaskResult> {
495
597
  if (!owned.ok) throw new TaskCardSupersededError(next.id);
496
598
  };
497
599
  writeCard(path, card);
600
+ try { debug.log('task-agent.graph-select', !selected.composition ? 'composition-missing' : selected.error ? 'composition-invalid' : 'composition-selected', {
601
+ completion, file: selected.composition?.file ?? `graphs/work/${completion}.yaml`,
602
+ unitsResolved: selected.unitsResolved, missingUnits: selected.missingUnits,
603
+ ...(selected.error ? { error: selected.error } : {}),
604
+ }); } catch { /* fail-soft */ }
605
+ if (selectionFailure) throw new Error(`${selectionFailure} (카드 ${id})`);
606
+ if (!move || move.kind !== 'launch') throw new Error('조합 선택 뒤 발사 수가 없다');
498
607
  try { debug.log('task-agent', 'handed', { taskId: id, seat: card.seat ?? null, checklistId: card.checklistId ?? null, mode, projectId: project?.id ?? null, targetIsGit, taLand: card.taLand === true }); } catch { /* fail-soft */ }
499
608
  if (childLlm) {
500
609
  try { debug.log('task-agent', 'child-llm', { taskId: id, input: opts.childLlm, provider: childLlm.provider, model: childLlm.model, effort: childLlm.effort ?? null, mode }); } catch { /* fail-soft */ }
@@ -0,0 +1,96 @@
1
+ /** Pod repair result: a commit based on the exact PR head, returned without opening a PR. */
2
+ import { execFileSync } from 'node:child_process';
3
+ import { createHash } from 'node:crypto';
4
+ import { mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
5
+ import { isAbsolute, join, resolve } from 'node:path';
6
+
7
+ export const REPAIR_REF = 'refs/elanous/repair/result';
8
+ export const REPAIR_BUNDLE_MAX_BYTES = 5 * 1024 * 1024;
9
+ const SHA40 = /^[0-9a-f]{40}$/;
10
+
11
+ export interface PodRepairResult {
12
+ pr: number;
13
+ baseHead: string;
14
+ runId: string;
15
+ commit: string;
16
+ bundleSha256: string;
17
+ }
18
+
19
+ /** Host-side receipt gate. A missing, mismatched, or truncated return is never a repair result. */
20
+ export function readPodRepairArtifact(artifactDir: string, expected: { pr: number; baseHead: string; runId?: string }): PodRepairResult {
21
+ const result = JSON.parse(readFileSync(join(artifactDir, 'result.json'), 'utf8')) as Partial<PodRepairResult>;
22
+ if (result.pr !== expected.pr || result.baseHead !== expected.baseHead || (expected.runId && result.runId !== expected.runId)
23
+ || !result.runId || !SHA40.test(result.commit ?? '') || !/^[0-9a-f]{64}$/.test(result.bundleSha256 ?? '')) {
24
+ throw new Error('repair result mismatch');
25
+ }
26
+ const bundle = readFileSync(join(artifactDir, 'commit.bundle'));
27
+ if (bundle.length > REPAIR_BUNDLE_MAX_BYTES) throw new Error('repair bundle too large');
28
+ if (createHash('sha256').update(bundle).digest('hex') !== result.bundleSha256) throw new Error('repair bundle checksum mismatch');
29
+ return result as PodRepairResult;
30
+ }
31
+
32
+ function git(cwd: string, ...args: string[]): string {
33
+ return execFileSync('git', ['-C', cwd, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
34
+ }
35
+
36
+ function completedWorktree(output: string): { path: string; runId: string } {
37
+ for (const line of output.trimEnd().split(/\r?\n/).reverse()) {
38
+ if (!line.startsWith('{')) continue;
39
+ let value: Record<string, unknown>;
40
+ try { value = JSON.parse(line) as Record<string, unknown>; } catch { continue; }
41
+ if (value.kind === 'self' && value.result && typeof value.result === 'object') value = value.result as Record<string, unknown>;
42
+ if (value.stage !== 'worktree-completed' || value.ok !== true || typeof value.worktreePath !== 'string'
43
+ || typeof value.runId !== 'string' || !value.runId)
44
+ throw new Error('repair child final result is not a successful worktree-only result');
45
+ return { path: value.worktreePath, runId: value.runId };
46
+ }
47
+ throw new Error('repair child did not return a successful worktree-only result');
48
+ }
49
+
50
+ /** Called in the Pod after the child exits successfully and before artifact export. */
51
+ export function writePodRepairCommit(input: { pr: number; baseHead: string; runId: string; output: string; repo: string; outbox: string }): PodRepairResult {
52
+ if (!Number.isSafeInteger(input.pr) || input.pr < 1 || !SHA40.test(input.baseHead)) throw new Error('invalid repair PR or base head');
53
+ if (!input.runId.trim()) throw new Error('repair parent run id missing');
54
+ const { path, runId: childRunId } = completedWorktree(input.output);
55
+ if (!isAbsolute(path)) throw new Error('repair worktree path is not absolute');
56
+ const worktree = realpathSync(path);
57
+ const repo = realpathSync(input.repo);
58
+ if (worktree === repo) throw new Error('repair result must be a separate worktree');
59
+ if (git(worktree, 'rev-parse', '--is-inside-work-tree') !== 'true') throw new Error('repair result is not a git worktree');
60
+ const commonDir = (cwd: string) => realpathSync(resolve(cwd, git(cwd, 'rev-parse', '--git-common-dir')));
61
+ if (commonDir(worktree) !== commonDir(repo)) throw new Error('repair worktree belongs to another repository');
62
+ if (git(worktree, 'config', '--worktree', '--get', 'elanous.harness.owner') !== `dev:${childRunId}`
63
+ || git(worktree, 'config', '--worktree', '--get', 'elanous.harness.command') !== 'elanous dev')
64
+ throw new Error('repair worktree is not owned by the completed child run');
65
+ if (git(repo, 'rev-parse', 'HEAD') !== input.baseHead) throw new Error('Pod source head changed before repair export');
66
+ git(worktree, 'cat-file', '-e', `${input.baseHead}^{commit}`);
67
+ execFileSync('git', ['-C', worktree, 'add', '-A', '--', '.'], { stdio: ['ignore', 'pipe', 'pipe'] });
68
+ const staged = git(worktree, 'diff', '--cached', '--name-only');
69
+ if (staged) git(worktree, 'commit', '-m', `repair: PR #${input.pr} from ${input.baseHead.slice(0, 12)}`);
70
+ const commit = git(worktree, 'rev-parse', 'HEAD');
71
+ if (!SHA40.test(commit) || commit === input.baseHead) throw new Error('repair made no new commit');
72
+ execFileSync('git', ['-C', worktree, 'merge-base', '--is-ancestor', input.baseHead, commit], { stdio: ['ignore', 'pipe', 'pipe'] });
73
+ git(worktree, 'update-ref', REPAIR_REF, commit);
74
+ const dir = join(input.outbox, 'repair');
75
+ mkdirSync(dir, { recursive: true });
76
+ const bundle = join(dir, 'commit.bundle');
77
+ execFileSync('git', ['-C', worktree, 'bundle', 'create', bundle, REPAIR_REF, `^${input.baseHead}`], { stdio: ['ignore', 'pipe', 'pipe'] });
78
+ const size = statSync(bundle).size;
79
+ if (size > REPAIR_BUNDLE_MAX_BYTES) throw new Error(`repair bundle too large: ${size} > ${REPAIR_BUNDLE_MAX_BYTES}`);
80
+ const bundleSha256 = createHash('sha256').update(readFileSync(bundle)).digest('hex');
81
+ const result = { pr: input.pr, baseHead: input.baseHead, runId: input.runId, commit, bundleSha256 };
82
+ writeFileSync(join(dir, 'result.json'), JSON.stringify(result) + '\n', { flag: 'wx', mode: 0o600 });
83
+ return result;
84
+ }
85
+
86
+ if (import.meta.main) {
87
+ try {
88
+ const [pr, baseHead, outputPath, repo, outbox] = process.argv.slice(2);
89
+ if (!pr || !baseHead || !outputPath || !repo || !outbox) throw new Error('usage: pod-repair-commit <pr> <base-head> <child-output> <repo> <outbox>');
90
+ const result = writePodRepairCommit({ pr: Number(pr), baseHead, runId: process.env.ELANOUS_PARENT_RUN_ID ?? '', output: readFileSync(outputPath, 'utf8'), repo, outbox });
91
+ console.log(`ELANOUS_POD_REPAIR_COMMIT ${result.pr} ${result.baseHead} ${result.commit}`);
92
+ } catch (error) {
93
+ console.error(`ELANOUS_POD_REPAIR_FAILED ${error instanceof Error ? error.message : String(error)}`);
94
+ process.exitCode = 1;
95
+ }
96
+ }
@@ -19,6 +19,7 @@ import { coalescedInstallationCredential } from '../../auth/github-app-token.js'
19
19
  import { POD_CREDENTIAL_TOKEN_ENV, POD_CREDENTIAL_URL_ENV } from '../../grok/credential.js';
20
20
  import { collectPodLedgers, createPodLedgerFollower } from './pod-ledger-collect.js';
21
21
  import { collectPodArtifacts, podNoResultDiagnostic } from './pod-artifact-return.js';
22
+ import { readPodRepairArtifact } from './pod-repair-commit.js';
22
23
  import { isValidAccountName, resolveCodexAccount } from '../../oauth/codex-account.js';
23
24
  import { effectiveInstanceRoot } from '../../instance/resolve.js';
24
25
  import { controlInboxEnv } from '../../harness/control-inbox.js';
@@ -27,7 +28,7 @@ import { mintRunId, normalizeRunId } from '../../harness/harness-space.js';
27
28
  import { TA_LAND_ENV } from '../../task-agent/task-hand.js';
28
29
  import { appendRunLedgerEntry, loadRunLedger, runLedgerDir, runLedgerPath } from '../../self-implement/run-ledger.js';
29
30
  import type { GoalExecutionRecord } from '../../self-implement/orchestrator.js';
30
- import { runHostRegate, type HostRegateResult } from '../../self-implement/host-regate.js';
31
+ import { hostRepositoryUnavailable, runHostRegate, type HostRegateResult } from '../../self-implement/host-regate.js';
31
32
  import { resolvePodHostAcpReview, runPodHostAcpReview, type PodHostAcpReviewOutcome } from './pod-host-acp-review.js';
32
33
  import { REVIEW_DEFERRED_TO_HOST_ENV, REVIEW_DEFERRED_TO_HOST_MERGE_REASON } from '../../self-implement/review-deferred-to-host.js';
33
34
  import { classifyPodTerminal, applyPodTerminal, podTerminalPrBody, type PrTerminalRecord, type DraftResidueContext } from '../../self-dev/draft-residue.js';
@@ -137,6 +138,8 @@ export interface PodSpawnOptions {
137
138
  authorConfig?: () => UserConfig;
138
139
  /** 원천 — 없으면 종전 `git clone --depth 50`. bundle 이면 apply 뒤 kubectl cp 로 싣는다. */
139
140
  source?: PodSource;
141
+ /** Existing PR repair: pin the source head and export a commit bundle instead of opening a PR. */
142
+ repairCommitOnly?: { pr: number; baseHead: string };
140
143
  /** 호스트 환경에서 읽어 Pod env 로 넣을 키 이름(예: OPENROUTER_API_KEY · ANTHROPIC_API_KEY) — 벤치마크 과금 경로. */
141
144
  passEnv?: readonly string[];
142
145
  /** 호스트가 재발사를 소유하면 Pod 자식의 자체 감독을 끈다. 독립 호출 기본 true; 실제 CLI 호스트는 자기 감독 상태를 명시한다. */
@@ -953,7 +956,10 @@ export function podSigtermTrapScript(seqFile: string = POD_USAGE_PARTIAL_SEQ_FIL
953
956
  return `trap '{ [ -n "$si_pid" ] && kill "$si_pid" 2>/dev/null || true; [ -n "\${usage_partial_pid:-}" ] && kill "$usage_partial_pid" 2>/dev/null || true; si_seq=$(cat ${seqFile} 2>/dev/null || true); case "$si_seq" in ""|*[!0-9]*) si_seq=0;; esac; si_seq=$((10#$si_seq + 1)); { [ -f scripts/usage-rollup.ts ] && bun scripts/usage-rollup.ts --since 12h --partial --seq "$si_seq"; } & si_rollup_pid=$!; for si_i in 1 2 3 4 5 6 7 8; do kill -0 "$si_rollup_pid" 2>/dev/null || break; sleep 1; done; kill "$si_rollup_pid" 2>/dev/null || true; wait "$si_rollup_pid" 2>/dev/null || true; exit 143; }' TERM`;
954
957
  }
955
958
 
956
- export function podJobManifest(o: { name: string; namespace: string; image: string; workIdentity?: PodWorkIdentity; /** 레지스트리 이미지면 IfNotPresent(노드가 pull) · 반입 이미지면 Never. */ imagePullPolicy?: 'Never' | 'IfNotPresent'; repoUrl: string; source?: PodSource; hostMirror?: string; args: readonly string[]; passEnv: readonly string[]; deadlineSeconds: number; runId?: string; parentRunId?: string; armEnv?: Readonly<Record<string, string>>; hostId?: string; imageCommit?: string | null; skillEnvs?: readonly string[]; memoryLimit?: string; memoryRequest?: string; goalDoc?: string; /** AUTHOR-POD2 — run `harness say` on /creds/feature inside the Pod (authoring happens off the host). */ authorSentence?: boolean; authorGrade?: 'full' | 'lite'; grokCredential?: 'subscription' | 'api_key'; codexAccounts?: readonly string[]; appCredential?: boolean; /** Test seam — default POD_GH_STALE_SECONDS. */ githubStaleSeconds?: number; /** Which goal execution and attempt this Job is — a resumed host verifies it before following the Job (POD9). */ execution?: { key: string; attempt: number }; hostLeaseAdmitted?: boolean; /** POD-ROLE-LLM — OpenRouter 자식 Pod 에 리뷰어용 codex 자격을 깐다. */ reviewerCodexAuth?: boolean; /** BEDROCK-PROVIDER ② — bedrock 자식만: 이름 있는 AWS Secret 의 키(secretKeyRef) ⊕ 리전 ⊕ 이그레스 라벨. */ bedrock?: BedrockPodSpec; /** ZAI-POD-CHILD — glm(ZAI) 자식만: 이름 있는 ZAI Secret 의 키(secretKeyRef) ⊕ 이그레스·동시 상한 라벨(Pod ⊕ Job). */ zai?: ZaiPodSpec }): Record<string, unknown> {
959
+ export function podJobManifest(o: { name: string; namespace: string; image: string; workIdentity?: PodWorkIdentity; /** 레지스트리 이미지면 IfNotPresent(노드가 pull) · 반입 이미지면 Never. */ imagePullPolicy?: 'Never' | 'IfNotPresent'; repoUrl: string; source?: PodSource; repairCommitOnly?: { pr: number; baseHead: string }; hostMirror?: string; args: readonly string[]; passEnv: readonly string[]; deadlineSeconds: number; runId?: string; parentRunId?: string; armEnv?: Readonly<Record<string, string>>; hostId?: string; imageCommit?: string | null; skillEnvs?: readonly string[]; memoryLimit?: string; memoryRequest?: string; goalDoc?: string; /** AUTHOR-POD2 — run `harness say` on /creds/feature inside the Pod (authoring happens off the host). */ authorSentence?: boolean; authorGrade?: 'full' | 'lite'; grokCredential?: 'subscription' | 'api_key'; codexAccounts?: readonly string[]; appCredential?: boolean; /** Test seam — default POD_GH_STALE_SECONDS. */ githubStaleSeconds?: number; /** Which goal execution and attempt this Job is — a resumed host verifies it before following the Job (POD9). */ execution?: { key: string; attempt: number }; hostLeaseAdmitted?: boolean; /** POD-ROLE-LLM — OpenRouter 자식 Pod 에 리뷰어용 codex 자격을 깐다. */ reviewerCodexAuth?: boolean; /** BEDROCK-PROVIDER ② — bedrock 자식만: 이름 있는 AWS Secret 의 키(secretKeyRef) ⊕ 리전 ⊕ 이그레스 라벨. */ bedrock?: BedrockPodSpec; /** ZAI-POD-CHILD — glm(ZAI) 자식만: 이름 있는 ZAI Secret 의 키(secretKeyRef) ⊕ 이그레스·동시 상한 라벨(Pod ⊕ Job). */ zai?: ZaiPodSpec }): Record<string, unknown> {
960
+ if (o.repairCommitOnly && (o.source?.kind !== 'pr' || o.source.number !== o.repairCommitOnly.pr || !/^[0-9a-f]{40}$/.test(o.repairCommitOnly.baseHead) || o.args.includes('--open-pr') || o.args.includes('--merge-by-host') || o.args.includes('--auto-merge'))) {
961
+ throw new Error('repair commit-only requires matching PR source and no PR/merge flags');
962
+ }
957
963
  const quoted = o.args.map((a) => `'${a.replace(/'/g, `'\\''`)}'`).join(' ');
958
964
  const childProviderIndex = o.args.indexOf('--child-llm-provider');
959
965
  const childModelIndex = o.args.indexOf('--child-llm-model');
@@ -962,7 +968,7 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
962
968
  // POD-ANTHROPIC-PROVIDER — anthropic 자식은 키(env · secretKeyRef)만 쓴다. codex·grok 자격 파일을 깔지 않는다.
963
969
  const anthropicChild = childProviderIndex >= 0 && o.args[childProviderIndex + 1] === 'anthropic';
964
970
  const goalPath = o.goalDoc ? `'${(o.goalDoc.startsWith('-') ? `./${o.goalDoc}` : o.goalDoc).replace(/'/g, `'\\''`)}'` : undefined;
965
- const delegated = Boolean(o.goalDoc) || o.authorSentence === true;
971
+ const delegated = !o.repairCommitOnly && (Boolean(o.goalDoc) || o.authorSentence === true);
966
972
  const askBaseIndex = delegated ? o.args.indexOf('--base') : -1;
967
973
  const quotedAsk = (askBaseIndex >= 0 && o.args[askBaseIndex + 1]
968
974
  ? ` '--base' '${o.args[askBaseIndex + 1]!.replace(/'/g, `'\\''`)}'`
@@ -997,12 +1003,13 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
997
1003
  podGitPushGuardScript(),
998
1004
  'curl -s -m 3 -o /dev/null http://host.orb.internal:31415/health && { echo "[pod] ISOLATION FAIL"; exit 3; }',
999
1005
  podSourceScript(o.source ?? { kind: 'default' }, o.repoUrl),
1006
+ ...(o.repairCommitOnly ? [`[ "$(git rev-parse HEAD)" = '${o.repairCommitOnly.baseHead}' ] || { echo 'ELANOUS_POD_REPAIR_FAILED source-head-mismatch'; exit 8; }`] : []),
1000
1007
  ...(goalPath ? [
1001
1008
  `mkdir -p -- "$(dirname -- ${goalPath})" && cp -- /creds/goal-doc ${goalPath} || exit 6`,
1002
1009
  ] : []),
1003
1010
  // Only fixed command shapes leave the Pod; arbitrary argv (including shell -c text) is never emitted.
1004
1011
  `(while :; do { mem=$(if [ -r /sys/fs/cgroup/memory.current ]; then cat /sys/fs/cgroup/memory.current; elif [ -r /sys/fs/cgroup/memory/memory.usage_in_bytes ]; then cat /sys/fs/cgroup/memory/memory.usage_in_bytes; else printf -- -; fi); top=$(ps -eo rss=,comm=,args= --sort=-rss | head -5 | awk 'function encode(s) { gsub(/%/, "%25", s); gsub(/:/, "%3A", s); gsub(/ /, "%20", s); gsub(/\\t/, "%09", s); return s } { rss=$1; name=$2; sub(/^[[:space:]]*[0-9]+[[:space:]]+[^[:space:]]+[[:space:]]*/, ""); n=split($0, a, /[[:space:]]+/); cmd="<redacted>"; if (name=="sleep" && a[2]=="30") cmd="sleep 30" (n>2 ? " <redacted>" : ""); else if (name=="bun") { cmd="bun <redacted>"; if (a[2]=="test") cmd="bun test <redacted>"; else if (a[2]=="run") cmd="bun run <redacted>"; else if ((a[2]=="x" || a[2]=="exec") && a[3]=="tsc") cmd="bun x tsc <redacted>" } else if (name=="tsc") cmd="tsc <redacted>"; else if (name=="elanous") { cmd="elanous <redacted>"; if (a[2]=="self") cmd="elanous self <redacted>"; else if (a[2]=="harness") cmd="elanous harness <redacted>" } else if (name=="node") cmd="node <redacted>"; if (name=="sleep" && cmd=="<redacted>") cmd="sleep <redacted>"; else if (cmd=="<redacted>" && name!="bash" && name!="sh") name="other"; printf " %s:%s:%s", rss, encode(name), encode(substr(cmd,1,120)) }'); printf "ELANOUS_MEM %s %s%s\\n" "$(date +%s)" "$mem" "$top"; } || true; sleep 15 || break; done) & mem_sampler_pid=$!`,
1005
- ...(o.appCredential ? [podGithubWatchdogScript(o.githubStaleSeconds ?? POD_GH_STALE_SECONDS)] : []),
1012
+ ...(o.appCredential && !o.repairCommitOnly ? [podGithubWatchdogScript(o.githubStaleSeconds ?? POD_GH_STALE_SECONDS)] : []),
1006
1013
  // METER-POD-ROLLUP-GAP — 부분 롤업 표본기: 최종 줄까지 못 사는 Pod 도 N 분마다(기본 5 · ELANOUS_USAGE_PARTIAL_MINUTES) PARTIAL 줄을 하나씩 남긴다.
1007
1014
  `(usage_partial_seq=0; while :; do { usage_partial_seq=$((usage_partial_seq+1)); printf '%s\\n' "$usage_partial_seq" > ${POD_USAGE_PARTIAL_SEQ_FILE} 2>/dev/null || true; [ -f scripts/usage-rollup.ts ] && bun scripts/usage-rollup.ts --since 12h --partial --seq "$usage_partial_seq" || true; } || true; usage_partial_sleep=$(( \${ELANOUS_USAGE_PARTIAL_MINUTES:-5} * 60 )); sleep "$usage_partial_sleep" || break; done) & usage_partial_pid=$!`,
1008
1015
  // ⭐ MF-d3b5a70f — si_pid 초기화 + trap: 메인 명령을 아래에서 `& si_pid=$!` 로 띄우고 `wait "$si_pid"` 로
@@ -1013,7 +1020,9 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
1013
1020
  // 마지막 줄 JSON 이 «맨 끝»이어야 한다(parseSelfImplementJson) — rollup 은 그 앞에.
1014
1021
  // ⛔ `--author-grade` 는 Pod 안 `elanous`(이미지에 깔린 판)가 모를 수 있어 넘기지 않는다 — 10-06 «unknown option» 즉사(#24445 되돌림).
1015
1022
  // AUTHOR-LITE2-POD — 등급은 환경변수 `ELANOUS_AUTHOR_GRADE` 로 싣는다(옛 Pod elanous 는 모르는 환경변수를 무시하고 full 로 돈다).
1016
- o.authorSentence
1023
+ o.repairCommitOnly
1024
+ ? `export ${OLD_DOOR_STAMP_ENV}=self-implement; elanous self implement "$(cat /creds/feature)" --json --no-open-pr ${quoted} > /tmp/si.out 2>&1 & si_pid=$!; wait "$si_pid"; rc=$?; if [ "$rc" -eq 0 ]; then elanous self repair-commit-export '${o.repairCommitOnly.pr}' '${o.repairCommitOnly.baseHead}' /tmp/si.out "$PWD" "$HOME/outbox" || rc=8; fi`
1025
+ : o.authorSentence
1017
1026
  ? `echo "ELANOUS_AUTHOR_ON_POD started host=$(hostname) at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"; author_t0=$(date +%s); ${o.authorGrade === 'lite' || o.authorGrade === 'full' ? `ELANOUS_AUTHOR_GRADE=${o.authorGrade} ` : ''}elanous harness say --substrate local --json${quotedAsk}${quotedChildLlm} -- "$(cat /creds/feature)" > /tmp/si.out 2>&1 & si_pid=$!; wait "$si_pid"; rc=$?; echo "ELANOUS_AUTHOR_ON_POD finished host=$(hostname) rc=$rc seconds=$(( $(date +%s) - author_t0 ))"; ${POD_RESULT_FLATTEN}`
1018
1027
  : goalPath
1019
1028
  ? `elanous harness ask ${goalPath} --json${quotedAsk}${quotedChildLlm} > /tmp/si.out 2>&1 & si_pid=$!; wait "$si_pid"; rc=$?; ${POD_RESULT_FLATTEN}`
@@ -1074,12 +1083,12 @@ for ledger in "\${ELANOUS_STATE_DIR:-$HOME/.elanous}"/run-ledger/*.jsonl; do
1074
1083
  done
1075
1084
  if [ "$found" -eq 0 ]; then echo ELANOUS_RUN_LEDGER_NONE; fi`,
1076
1085
  'kill "$mem_sampler_pid" 2>/dev/null || true',
1077
- ...(o.appCredential ? ['kill "$gh_watch_pid" 2>/dev/null || true'] : []),
1086
+ ...(o.appCredential && !o.repairCommitOnly ? ['kill "$gh_watch_pid" 2>/dev/null || true'] : []),
1078
1087
  '[ -n "${usage_partial_pid:-}" ] && kill "$usage_partial_pid" 2>/dev/null || true',
1079
1088
  'wait "$mem_sampler_pid" 2>/dev/null || true',
1080
1089
  '[ -n "${usage_partial_pid:-}" ] && wait "$usage_partial_pid" 2>/dev/null || true',
1081
1090
  'tail -n 1 /tmp/si.out',
1082
- podSalvageScript(),
1091
+ ...(o.repairCommitOnly ? [] : [podSalvageScript()]),
1083
1092
  'exit $rc',
1084
1093
  ].join('\n');
1085
1094
  return {
@@ -1149,6 +1158,9 @@ function defaultGhComment(prNumber: number, body: string): void {
1149
1158
  }
1150
1159
 
1151
1160
  export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplementJobSpawn {
1161
+ if (options.repairCommitOnly && (options.source?.kind !== 'pr' || options.source.number !== options.repairCommitOnly.pr || !/^[0-9a-f]{40}$/.test(options.repairCommitOnly.baseHead))) {
1162
+ throw new Error('repair commit-only requires a matching PR source and exact head');
1163
+ }
1152
1164
  const baseKubectl = options.kubectl ?? defaultKubectl;
1153
1165
  const sleep = options.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
1154
1166
  const namespace = options.namespace ?? 'elanous-test';
@@ -1160,6 +1172,9 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1160
1172
  if (hostMirror && !isAbsolute(hostMirror)) throw new Error('pod.hostMirror must be an absolute directory path');
1161
1173
  const hostSupervised = options.hostSupervised ?? true;
1162
1174
  return (input) => {
1175
+ if (options.repairCommitOnly && (input.autoMerge || input.openPr || input.base !== `pr-${options.repairCommitOnly.pr}`)) {
1176
+ throw new Error('repair commit-only refuses auto merge, new PR, or a different base');
1177
+ }
1163
1178
  const name = podJobName(input.spaceId);
1164
1179
  let parentGoal: string | undefined;
1165
1180
  let parentGoalUnreadable = false;
@@ -1212,6 +1227,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1212
1227
  const childRunId = mintRunId();
1213
1228
  const executionKey = createHash('sha256').update(JSON.stringify({ spaceId: input.spaceId, feature: input.feature, base: input.base, autoMerge: input.autoMerge, autoReview: input.autoReview, openPr: input.openPr, draft: input.draft })).digest('hex');
1214
1229
  const parentRunId = env.ELANOUS_RUN_ID?.trim();
1230
+ if (options.repairCommitOnly && !parentRunId) throw new Error('repair commit-only requires a parent run id');
1215
1231
  // Standalone callers journal retries without altering the existing Pod manifest.
1216
1232
  const ledgerRunId = parentRunId || `run-${executionKey.slice(0, 8)}-${executionKey.slice(8, 12)}-${executionKey.slice(12, 16)}-${executionKey.slice(16, 20)}-${executionKey.slice(20, 32)}`;
1217
1233
  const address = `self-impl:${input.spaceId}`;
@@ -1237,7 +1253,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1237
1253
  ...(input.autoReview ? ['--auto-review'] : []),
1238
1254
  // ⭐ 계약이 미래 노드를 바꾼다(첫 규칙): Pod 는 끝나면 사라지므로 완료 하한 = PR. 작업 트리로만 끝나면 결과가 Pod 와 함께 없어진다.
1239
1255
  // ⚠️ 임시 안전망(🅢 2026-09-26) — 🅣 의 «종결 하한 오버레이»(그래프 쪽)가 서면 이 줄을 지우고 그것으로 바꾼다(채널 합의).
1240
- ...(input.openPr || (!input.autoMerge && completionFloorFor({ substrate: 'pod' }) === 'pr') ? ['--open-pr'] : []),
1256
+ ...(!options.repairCommitOnly && (input.openPr || (!input.autoMerge && completionFloorFor({ substrate: 'pod' }) === 'pr')) ? ['--open-pr'] : []),
1241
1257
  ...(input.draft === false ? ['--no-draft'] : []),
1242
1258
  ...(hostSupervised ? ['--no-supervise'] : []),
1243
1259
  ...(options.extraArgs ?? []),
@@ -1327,7 +1343,8 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1327
1343
  if (unfitDone) return unfitDone;
1328
1344
  // HARNESS-FULL-GRAPH — Pod 입장(입장 관문 ⊕ 멤버 슬롯)은 그래프 노드 pool-admit 이다.
1329
1345
  // 2판: decompose →(substrate=pod) pool-admit 간선은 «여기»(Pod 입장 직전 · 실제 단계 경계)에서 남긴다.
1330
- routeJourneyEdge('decompose', 'pod', { substrate: 'pod' });
1346
+ if (options.repairCommitOnly) routeJourneyEdge('dispatch', 'pr-source', { source: 'pr', substrate: 'pod' });
1347
+ else routeJourneyEdge('decompose', 'pod', { substrate: 'pod' });
1331
1348
  enterJourneyNode('pool-admit', { provenance: 'self-implement-pod', data: { spaceId: input.spaceId } });
1332
1349
  // ⛔ 감싸는 함수(async 래퍼)를 쓰지 않는다 — 마이크로태스크가 늘면 Job apply 순서가 밀린다(시험이 잡았다). 같은 자리 try/catch 로.
1333
1350
  const podAdmitError = (error: unknown): void => {
@@ -1354,6 +1371,8 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1354
1371
  try {
1355
1372
  if (!context) return { exitCode: 1, output: '', error: { code: 'pod-context', message: 'Pod Job context를 확인할 수 없다' } };
1356
1373
  const cleanupSecret = () => { kubectl(['-n', namespace, 'delete', 'secret', `${name}-creds`, '--ignore-not-found']); };
1374
+ let repairResultAnnounced = false;
1375
+ let repairArtifactReady = false;
1357
1376
  const collectFullLogs = (unavailableEvent: string, replace?: ReadonlySet<string>, onEvent?: (event: string, data: Record<string, unknown>) => void, childId?: string): { status: 'ok' | 'unavailable' | 'incomplete'; childLedgerIncomplete: boolean; samples: ReturnType<typeof parseMemSamples> } => {
1358
1377
  let full: ReturnType<Kubectl>;
1359
1378
  try {
@@ -1384,6 +1403,20 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1384
1403
  collectPodArtifacts(full.stdout, { dir: join(effectiveInstanceRoot(), 'pod-artifacts'), job: name, log: record });
1385
1404
  }
1386
1405
  catch (error) { record('self-implement.pod', 'artifact-collect-incomplete', { job: name, reason: error instanceof Error ? error.message : String(error) }); }
1406
+ if (options.repairCommitOnly) {
1407
+ const artifactDir = join(effectiveInstanceRoot(), 'pod-artifacts', name, 'repair');
1408
+ try {
1409
+ const result = readPodRepairArtifact(artifactDir, { ...options.repairCommitOnly, runId: parentRunId });
1410
+ repairArtifactReady = true;
1411
+ if (!repairResultAnnounced) {
1412
+ console.log(`ELANOUS_POD_REPAIR_RESULT ${JSON.stringify({ job: name, artifactDir, pr: result.pr, baseHead: result.baseHead, runId: result.runId, commit: result.commit, bundleSha256: result.bundleSha256 })}`);
1413
+ repairResultAnnounced = true;
1414
+ }
1415
+ } catch (error) {
1416
+ repairArtifactReady = false;
1417
+ record('self-implement.pod', 'repair-artifact-incomplete', { job: name, reason: error instanceof Error ? error.message : String(error) });
1418
+ }
1419
+ }
1387
1420
  return { status: ledgerIncomplete || artifactIncomplete ? 'incomplete' : 'ok', childLedgerIncomplete, samples: parseMemSamples(full.stdout) };
1388
1421
  };
1389
1422
  let retryAccount: string | undefined;
@@ -1839,7 +1872,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1839
1872
  slotId: env.ELANOUS_POD_SLOT_ID,
1840
1873
  work: !goalDoc && env.ELANOUS_POD_AUTHOR_ON_POD === '1' && !shard ? 'authoring' : (oomRetried ? retryTier : memoryTier) === 'lite' ? 'lite' : 'harness',
1841
1874
  };
1842
- const job = podJobManifest({ name, namespace, image: jobImage, workIdentity, ...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}), repoUrl, ...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), args: jobArgs, passEnv: jobPassEnv, deadlineSeconds: options.deadlineSeconds ?? POD_JOB_DEADLINE_SECONDS, ...(goalDoc ? { goalDoc } : env.ELANOUS_POD_AUTHOR_ON_POD === '1' && !shard ? { authorSentence: true, ...(env.ELANOUS_POD_AUTHOR_GRADE === 'lite' || env.ELANOUS_POD_AUTHOR_GRADE === 'full' ? { authorGrade: env.ELANOUS_POD_AUTHOR_GRADE } : {}) } : {}), runId: launchRunId, ...(parentRunId ? { parentRunId } : {}), ...(jobArmEnv ? { armEnv: jobArmEnv } : {}), hostId: resolveHostId(env), skillEnvs: Object.keys(skillEnvs), memoryLimit, memoryRequest, imageCommit: options.imageCommit !== undefined ? options.imageCommit : options.kubectl ? null : podImageFreshness({ image }).imageCommit, ...(grok ? { grokCredential: 'grokAuth' in creds && creds.grokAuth ? 'subscription' as const : 'api_key' as const } : {}), ...(codexAccounts ? { codexAccounts } : {}), ...(hostRefresh ? { appCredential: true } : {}), execution: { key: executionKey, attempt: oomRetried ? 2 : 1 }, hostLeaseAdmitted: !!releaseAdmission, ...(reviewerCredential ? { reviewerCodexAuth: true } : {}), ...(bedrockSpec ? { bedrock: bedrockSpec } : {}), ...(zaiSpec ? { zai: zaiSpec } : {}) }); // kubectl 주입(=시험)이면 docker 를 부르지 않는다
1875
+ const job = podJobManifest({ name, namespace, image: jobImage, workIdentity, ...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}), repoUrl, ...(options.source ? { source: options.source } : {}), ...(options.repairCommitOnly ? { repairCommitOnly: options.repairCommitOnly } : {}), ...(hostMirror ? { hostMirror } : {}), args: jobArgs, passEnv: jobPassEnv, deadlineSeconds: options.deadlineSeconds ?? POD_JOB_DEADLINE_SECONDS, ...(goalDoc ? { goalDoc } : env.ELANOUS_POD_AUTHOR_ON_POD === '1' && !shard ? { authorSentence: true, ...(env.ELANOUS_POD_AUTHOR_GRADE === 'lite' || env.ELANOUS_POD_AUTHOR_GRADE === 'full' ? { authorGrade: env.ELANOUS_POD_AUTHOR_GRADE } : {}) } : {}), runId: launchRunId, ...(parentRunId ? { parentRunId } : {}), ...(jobArmEnv ? { armEnv: jobArmEnv } : {}), hostId: resolveHostId(env), skillEnvs: Object.keys(skillEnvs), memoryLimit, memoryRequest, imageCommit: options.imageCommit !== undefined ? options.imageCommit : options.kubectl ? null : podImageFreshness({ image }).imageCommit, ...(grok ? { grokCredential: 'grokAuth' in creds && creds.grokAuth ? 'subscription' as const : 'api_key' as const } : {}), ...(codexAccounts ? { codexAccounts } : {}), ...(hostRefresh ? { appCredential: true } : {}), execution: { key: executionKey, attempt: oomRetried ? 2 : 1 }, hostLeaseAdmitted: !!releaseAdmission, ...(reviewerCredential ? { reviewerCodexAuth: true } : {}), ...(bedrockSpec ? { bedrock: bedrockSpec } : {}), ...(zaiSpec ? { zai: zaiSpec } : {}) }); // kubectl 주입(=시험)이면 docker 를 부르지 않는다
1843
1876
  const carriedBranch = goalDoc ? 'goal-doc' as const : env.ELANOUS_POD_AUTHOR_ON_POD === '1' && !shard ? 'author' as const : undefined;
1844
1877
  const carriedChildLlm = carriedBranch ? podDelegatedChildLlm(jobArgs) : undefined;
1845
1878
  const a = kubectl(['apply', '-f', '-'], JSON.stringify(job));
@@ -2047,6 +2080,9 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
2047
2080
  }
2048
2081
  // Pod 안 경로는 호스트에서 쓸 수 없다.
2049
2082
  let disposition = parsed ? { ...parsed, worktreePath: undefined } : undefined;
2083
+ if (options.repairCommitOnly && state === 'complete' && !repairArtifactReady) {
2084
+ disposition = { ...disposition, worktreePath: undefined, ok: false, stage: 'repair-artifact-missing', detail: 'Pod repair result or bundle was not collected' };
2085
+ }
2050
2086
  if (input.autoMerge && state === 'complete' && disposition?.stage === 'merge-ready') {
2051
2087
  const prNumber = disposition.prNumber;
2052
2088
  const headCommit = disposition.checkedHeadCommit;
@@ -2097,13 +2133,35 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
2097
2133
  debug.log('harness.host-regate', 'unmeasured', { pr: prNumber ?? null, files: [], os: process.platform, failedStep: 'pod-result' });
2098
2134
  regate = { passed: false, failures: [{ step: 'pod-result', detail: 'missing PR number or checked head commit' }], os: process.platform };
2099
2135
  } else {
2136
+ let hostRoot: string | undefined;
2137
+ let hostCwd: string | undefined;
2100
2138
  try {
2101
- const hostRoot = findGitDir(process.cwd())?.root;
2139
+ hostCwd = process.cwd();
2140
+ hostRoot = findGitDir(hostCwd)?.root;
2102
2141
  if (!hostRoot) throw new Error('host repository unavailable');
2103
2142
  regate = await (options.hostRegate ?? runHostRegate)({ prNumber, headCommit, repoRoot: hostRoot, ...(goalFile ? { goalFile } : {}), ...(taLandOwner ? { noMerge: true as const } : {}) });
2104
2143
  } catch (error) {
2105
2144
  debug.log('harness.host-regate', 'unmeasured', { pr: prNumber, files: [], os: process.platform, failedStep: 'host-regate' });
2106
- regate = { passed: false, failures: [{ step: 'host-regate', detail: error instanceof Error ? error.message : String(error) }], os: process.platform };
2145
+ regate = { passed: false, ...(taLandOwner ? { status: 'unmeasured' as const } : {}), failures: [{ step: 'host-regate', detail: error instanceof Error ? error.message : String(error) }], os: process.platform };
2146
+ }
2147
+ if (taLandOwner && !hostRoot && hostRepositoryUnavailable(regate)) {
2148
+ let recordedRoot: string | undefined;
2149
+ try {
2150
+ const recorded = loadRunLedger(ledgerRunId, ledgerDir)?.find((entry) => entry.event === 'start')?.data.targetRoot;
2151
+ if (typeof recorded === 'string' && isAbsolute(recorded)) {
2152
+ const foundRoot = findGitDir(recorded)?.root;
2153
+ // The recorded path must itself name the repository root, including symlink aliases.
2154
+ if (foundRoot && realpathSync(foundRoot) === realpathSync(recorded)) recordedRoot = foundRoot;
2155
+ }
2156
+ } catch { /* unreadable ledger or path leaves the original unmeasured result intact */ }
2157
+ if (recordedRoot) {
2158
+ try {
2159
+ regate = await (options.hostRegate ?? runHostRegate)({ prNumber, headCommit, repoRoot: recordedRoot, ...(goalFile ? { goalFile } : {}), noMerge: true });
2160
+ } catch (error) {
2161
+ regate = { passed: false, status: 'unmeasured', failures: [{ step: 'host-regate', detail: error instanceof Error ? error.message : String(error) }], os: process.platform };
2162
+ }
2163
+ debug.log('harness.host-regate', 'regate-retry', { pr: prNumber, from: hostCwd, to: recordedRoot, result: regate.status ?? (regate.passed ? 'passed' : 'failed') });
2164
+ }
2107
2165
  }
2108
2166
  }
2109
2167
  // runHostRegate comments on its own failures; these two paths never reached it.