elanous 0.2.14 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1307-6e7e0377c1db0bdc.js → 1307-b46d45b705e8c9ae.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-3cb52e5ea8fc2fa5.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/botlab/page-c1e20d7d338713f1.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-482c5dbde699947c.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-8c2d9526164f5d09.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/control/{page-e6b84ab07d3567d5.js → page-a0e2a6d1bfb52fc5.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c53598cfce6156f2.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-13a63953f53c6a23.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-5d3c8429d04f3831.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-3193bdc0bbab047a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-6107ca8ddd5e3bdd.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-dce686e582c23937.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/loops/{page-160089123a40dd91.js → page-1a61c7ca1ceb3ae7.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-51f811fa119308b6.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-4167348049166852.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-de52e8fdf9095f7a.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-ca3952cd78a83ca2.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-e7e10cd3386222e5.js → page-5b042736aa113756.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-1ebfe049c640e171.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-57924393e3e4e539.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-0f5cd57ca928f42b.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-d28573d6792b4275.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-58043db78ba519ae.js → page-59f03fda76c03d22.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-357750dfb91bf960.js → page-d751de5f8062ff5e.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-fe1759f94003b32b.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/today/page-d64c984e855c8c68.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-9afdee66b820c76e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-f8da5dc6f3752276.js → page-1ea64acadd704ace.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-3544f154c87b43ee.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
  53. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  54. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  55. package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +1 -0
  56. package/apps/pwa/out/approvals/index.html +1 -1
  57. package/apps/pwa/out/approvals/index.txt +15 -15
  58. package/apps/pwa/out/autopilot/index.html +1 -1
  59. package/apps/pwa/out/autopilot/index.txt +15 -15
  60. package/apps/pwa/out/board/index.html +1 -1
  61. package/apps/pwa/out/board/index.txt +15 -15
  62. package/apps/pwa/out/botlab/index.html +1 -1
  63. package/apps/pwa/out/botlab/index.txt +15 -15
  64. package/apps/pwa/out/bots/index.html +1 -1
  65. package/apps/pwa/out/bots/index.txt +15 -15
  66. package/apps/pwa/out/ceo/index.html +1 -0
  67. package/apps/pwa/out/ceo/index.txt +33 -0
  68. package/apps/pwa/out/chat/index.html +1 -1
  69. package/apps/pwa/out/chat/index.txt +15 -15
  70. package/apps/pwa/out/consult/index.html +1 -1
  71. package/apps/pwa/out/consult/index.txt +15 -15
  72. package/apps/pwa/out/control/index.html +1 -1
  73. package/apps/pwa/out/control/index.txt +15 -15
  74. package/apps/pwa/out/design-check/index.html +1 -1
  75. package/apps/pwa/out/design-check/index.txt +15 -15
  76. package/apps/pwa/out/editor/index.html +1 -1
  77. package/apps/pwa/out/editor/index.txt +15 -15
  78. package/apps/pwa/out/exec/index.html +1 -1
  79. package/apps/pwa/out/exec/index.txt +15 -15
  80. package/apps/pwa/out/field/index.html +1 -1
  81. package/apps/pwa/out/field/index.txt +15 -15
  82. package/apps/pwa/out/healthz +1 -1
  83. package/apps/pwa/out/index.html +1 -1
  84. package/apps/pwa/out/index.txt +15 -15
  85. package/apps/pwa/out/inside/index.html +1 -1
  86. package/apps/pwa/out/inside/index.txt +15 -15
  87. package/apps/pwa/out/intake/index.html +1 -1
  88. package/apps/pwa/out/intake/index.txt +15 -15
  89. package/apps/pwa/out/live/index.html +1 -1
  90. package/apps/pwa/out/live/index.txt +15 -15
  91. package/apps/pwa/out/loops/index.html +1 -1
  92. package/apps/pwa/out/loops/index.txt +15 -15
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +15 -15
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +15 -15
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +15 -15
  101. package/apps/pwa/out/ops/checklist/index.html +1 -1
  102. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  103. package/apps/pwa/out/ops/release/index.html +1 -1
  104. package/apps/pwa/out/ops/release/index.txt +15 -15
  105. package/apps/pwa/out/ops/seats/index.html +1 -1
  106. package/apps/pwa/out/ops/seats/index.txt +15 -15
  107. package/apps/pwa/out/outputs/index.html +1 -1
  108. package/apps/pwa/out/outputs/index.txt +15 -15
  109. package/apps/pwa/out/reflection/index.html +1 -1
  110. package/apps/pwa/out/reflection/index.txt +16 -16
  111. package/apps/pwa/out/scheduler/index.html +1 -1
  112. package/apps/pwa/out/scheduler/index.txt +15 -15
  113. package/apps/pwa/out/sessions/index.html +1 -1
  114. package/apps/pwa/out/sessions/index.txt +15 -15
  115. package/apps/pwa/out/settings/devices/index.html +1 -1
  116. package/apps/pwa/out/settings/devices/index.txt +15 -15
  117. package/apps/pwa/out/settings/index.html +1 -1
  118. package/apps/pwa/out/settings/index.txt +15 -15
  119. package/apps/pwa/out/setup/done/index.html +1 -1
  120. package/apps/pwa/out/setup/done/index.txt +15 -15
  121. package/apps/pwa/out/setup/index.html +1 -1
  122. package/apps/pwa/out/setup/index.txt +15 -15
  123. package/apps/pwa/out/share/index.html +1 -1
  124. package/apps/pwa/out/share/index.txt +15 -15
  125. package/apps/pwa/out/showroom/index.html +1 -1
  126. package/apps/pwa/out/showroom/index.txt +15 -15
  127. package/apps/pwa/out/tasks/index.html +1 -1
  128. package/apps/pwa/out/tasks/index.txt +15 -15
  129. package/apps/pwa/out/term/index.html +1 -1
  130. package/apps/pwa/out/term/index.txt +15 -15
  131. package/apps/pwa/out/today/index.html +1 -1
  132. package/apps/pwa/out/today/index.txt +15 -15
  133. package/apps/pwa/out/trace/index.html +1 -1
  134. package/apps/pwa/out/trace/index.txt +15 -15
  135. package/apps/pwa/out/vault/index.html +1 -1
  136. package/apps/pwa/out/vault/index.txt +15 -15
  137. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  138. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  139. package/apps/pwa/out/workflows/index.html +1 -1
  140. package/apps/pwa/out/workflows/index.txt +15 -15
  141. package/apps/pwa/out/workspace/index.html +1 -1
  142. package/apps/pwa/out/workspace/index.txt +15 -15
  143. package/apps/pwa/out/worktrees/index.html +1 -1
  144. package/apps/pwa/out/worktrees/index.txt +15 -15
  145. package/graphs/doc-gen/doc-gen.yaml +24 -0
  146. package/graphs/doc-gen/recipes.yaml +3 -0
  147. package/graphs/heal/heal-loop.yaml +1 -1
  148. package/graphs/implement-loop.yaml +1 -1
  149. package/graphs/orchestrator/orchestrator.yaml +7 -0
  150. package/graphs/orchestrator/recipes.yaml +3 -0
  151. package/graphs/release/recipes.yaml +3 -0
  152. package/graphs/release/release-loop.yaml +4 -2
  153. package/graphs/steward/recipes.yaml +2 -1
  154. package/package.json +1 -1
  155. package/scripts/brand/check.ts +7 -5
  156. package/scripts/ci-public-export-import-gate.ts +154 -0
  157. package/scripts/ci-test-interference-gate.ts +5 -1
  158. package/scripts/ci-typecheck-changed.ts +82 -31
  159. package/scripts/docs/doc-rot.ts +164 -0
  160. package/scripts/docs/generate-graph-anatomy.ts +437 -0
  161. package/scripts/docs-cli-check.ts +4 -4
  162. package/scripts/expose-rubric-suggest.ts +18 -12
  163. package/scripts/goals/goal-census.ts +343 -0
  164. package/scripts/lecture-note/node.ts +31 -11
  165. package/scripts/lessons/handbook-page.ts +30 -3
  166. package/scripts/marketing/generate-ax-brief.ts +140 -0
  167. package/scripts/marketing/generate-feature-map.ts +118 -0
  168. package/scripts/measure/author-section-contribution.ts +84 -0
  169. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  170. package/scripts/release-loop/cut-branch.ts +43 -13
  171. package/scripts/release-loop/gate-node.ts +82 -14
  172. package/scripts/release-loop/vault-note-node.ts +257 -8
  173. package/scripts/release-story/draft.ts +57 -9
  174. package/scripts/rhythm/daily.ts +263 -76
  175. package/scripts/rhythm/monthly.ts +97 -0
  176. package/scripts/rhythm/weekly.ts +30 -9
  177. package/scripts/smart-merge/resync-shadow.ts +147 -0
  178. package/src/acp/client.ts +17 -2
  179. package/src/agent-env/profile.ts +161 -0
  180. package/src/agent-mission/driver.ts +51 -1
  181. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  182. package/src/agent-mission/supervisor-metrics.ts +218 -0
  183. package/src/agent-substrate/pr-reviewer.ts +2 -0
  184. package/src/ask-user-question/pending-questions.ts +5 -0
  185. package/src/auth/github-app-token.ts +112 -13
  186. package/src/autopilot/build/llm-conflict-merge.ts +32 -6
  187. package/src/autopilot/track-agent-shadow.ts +2 -1
  188. package/src/briefing/brief-items.ts +276 -0
  189. package/src/budget/usage-outcomes.ts +204 -0
  190. package/src/claims/claims-render.ts +14 -5
  191. package/src/cli/agent-env-cli.ts +21 -0
  192. package/src/cli/brief-cli.ts +73 -0
  193. package/src/cli/decisions-cli.ts +9 -0
  194. package/src/cli/doctor-cli.ts +23 -1
  195. package/src/cli/doctor-node-modules.ts +147 -0
  196. package/src/cli/intake-cli.ts +46 -17
  197. package/src/cli/know-cli.ts +20 -0
  198. package/src/cli/logs-cli.ts +43 -1
  199. package/src/cli/pod-cli.ts +2 -0
  200. package/src/cli/pr-cli.ts +80 -12
  201. package/src/cli/release-cli.ts +66 -29
  202. package/src/cli/seat-requests-cli.ts +52 -0
  203. package/src/cli/self-update.ts +200 -1
  204. package/src/cli/usage-cli.ts +26 -0
  205. package/src/context-bus/context-now-public.ts +16 -6
  206. package/src/context-bus/context-now.ts +3 -1
  207. package/src/context-bus/coord-events.ts +6 -4
  208. package/src/decisions/decision-cards.ts +33 -7
  209. package/src/decisions/proact-meter.ts +286 -0
  210. package/src/decisions/proact1-lite.ts +86 -0
  211. package/src/decisions/web-push-decision-cards.ts +1 -1
  212. package/src/discord-self-message.ts +9 -0
  213. package/src/domains/alert-audit.ts +271 -0
  214. package/src/domains/core-tools.ts +4 -3
  215. package/src/domains/ops-facts-tool.ts +31 -1
  216. package/src/flow-loop/split.ts +24 -0
  217. package/src/harness/harness-cli-command.ts +147 -17
  218. package/src/harness/harness-incidents.ts +2 -0
  219. package/src/harness/harness-pod-dispatch.ts +67 -8
  220. package/src/harness/harness-queue.ts +380 -50
  221. package/src/harness/harness-seams.ts +13 -3
  222. package/src/harness/helper-repair.ts +218 -0
  223. package/src/harness/helper-scan.ts +27 -4
  224. package/src/harness/launch-stamp.ts +236 -0
  225. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  226. package/src/harness/ledger-write-guard.ts +35 -0
  227. package/src/harness/nested-elanous-policy.ts +114 -0
  228. package/src/harness/pod-memory-policy.ts +15 -0
  229. package/src/harness/repository-name.ts +55 -0
  230. package/src/hq/fence-audit.ts +90 -0
  231. package/src/hq/hq.ts +70 -6
  232. package/src/hq/lease.ts +9 -7
  233. package/src/hq/move-plan.ts +126 -0
  234. package/src/hq/promote.ts +31 -0
  235. package/src/hq/transfer.ts +185 -0
  236. package/src/index.ts +189 -13
  237. package/src/intake-plane/check.ts +63 -0
  238. package/src/intake-plane/digest.ts +65 -21
  239. package/src/intake-plane/front-classifier.ts +6 -0
  240. package/src/intake-plane/items.ts +3 -2
  241. package/src/intake-plane/lens.ts +177 -0
  242. package/src/intake-plane/route.ts +145 -1
  243. package/src/knowledge/know-find.ts +105 -0
  244. package/src/llm.ts +75 -19
  245. package/src/loops/budget.ts +36 -5
  246. package/src/loops/checker.ts +2 -2
  247. package/src/loops/orchestrator/author-depth.ts +65 -9
  248. package/src/loops/orchestrator/author-input-check.ts +8 -3
  249. package/src/loops/orchestrator/author-ledger.ts +3 -1
  250. package/src/loops/orchestrator/finish-rate.ts +14 -3
  251. package/src/loops/orchestrator/seat-share.ts +56 -0
  252. package/src/loops/orchestrator/tick.ts +107 -29
  253. package/src/loops/orchestrator/traffic.ts +117 -11
  254. package/src/loops/registry.ts +142 -61
  255. package/src/maturity/bot-slash-catalog.ts +11 -2
  256. package/src/maturity/feature-maturity.ts +4 -3
  257. package/src/maturity/tui-slash-availability.ts +40 -0
  258. package/src/mss/logging/log-store.ts +35 -4
  259. package/src/nexus/api/http-server.ts +28 -14
  260. package/src/nexus/api/loop-edges.ts +173 -0
  261. package/src/nexus/api/schedules-read.ts +13 -0
  262. package/src/nexus/api/seat-requests.ts +79 -56
  263. package/src/oauth/overload-failover-launch.ts +75 -0
  264. package/src/oauth/overload-failover.ts +137 -0
  265. package/src/prompt-enhance/enhance.ts +6 -1
  266. package/src/release-loop/auto-start.ts +6 -3
  267. package/src/release-loop/checklist.ts +19 -5
  268. package/src/release-loop/feature-store.ts +59 -31
  269. package/src/release-loop/merged-pr-checklist.ts +125 -0
  270. package/src/release-loop/placement.ts +98 -29
  271. package/src/seat-dispatch/seat-ask.ts +33 -6
  272. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  273. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  274. package/src/seat-loop/seat-loop.ts +215 -12
  275. package/src/self-dev/ask-launch-flow.ts +22 -1
  276. package/src/self-dev/authored-goal-trace.ts +64 -0
  277. package/src/self-dev/dev-cli.ts +19 -1
  278. package/src/self-dev/dev-pipeline.ts +14 -9
  279. package/src/self-dev/draft-sweep.ts +90 -10
  280. package/src/self-dev/draft-triage-rules.ts +25 -8
  281. package/src/self-dev/entrance-registry.ts +10 -6
  282. package/src/self-dev/old-door.ts +73 -0
  283. package/src/self-dev/orchestrate.ts +15 -1
  284. package/src/self-dev/review-provider-fallback.ts +6 -1
  285. package/src/self-dev/run-supervisor.ts +49 -2
  286. package/src/self-implement/app-api-budget.ts +166 -0
  287. package/src/self-implement/budget-gate.ts +7 -5
  288. package/src/self-implement/follow-up-goals.ts +289 -0
  289. package/src/self-implement/frozen-merges.ts +3 -2
  290. package/src/self-implement/goal-author.ts +50 -6
  291. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  292. package/src/self-implement/heal-intake.ts +120 -2
  293. package/src/self-implement/heal-triage.ts +44 -0
  294. package/src/self-implement/host-regate.ts +9 -2
  295. package/src/self-implement/l8-merge-queue.ts +132 -0
  296. package/src/self-implement/orchestrator.ts +325 -47
  297. package/src/self-implement/pipeline-shape.ts +1 -1
  298. package/src/self-implement/revert-guard.ts +42 -7
  299. package/src/self-implement/run-ledger.ts +6 -0
  300. package/src/self-implement/run-status-mapping.ts +2 -0
  301. package/src/self-implement/seams.ts +45 -16
  302. package/src/self-implement/self-implement-cli.ts +1 -0
  303. package/src/self-implement/speed-claims.ts +338 -0
  304. package/src/session-runtime/index.ts +2 -0
  305. package/src/session-runtime/retry-api.ts +87 -0
  306. package/src/session-runtime/retry-policy.ts +107 -6
  307. package/src/steward/triage.ts +74 -3
  308. package/src/steward/working-backwards.ts +20 -2
  309. package/src/task-cards/card-cli.ts +25 -0
  310. package/src/task-cards/card-store.ts +5 -3
  311. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  312. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  313. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  314. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  315. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  316. package/src/task-orchestrator/surfaces/pod-pool.ts +70 -9
  317. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  318. package/src/task-orchestrator/surfaces/self-implement-pod.ts +166 -46
  319. package/src/task-orchestrator/surfaces/self-implement.ts +7 -1
  320. package/src/telegram-commands.ts +5 -0
  321. package/src/telegram.ts +72 -4
  322. package/src/user-config.ts +64 -7
  323. package/src/version/packed-revision.json +1 -1
  324. package/src/workflow-runtime/nodes/bash.ts +9 -1
  325. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +0 -1
  348. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
  349. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
  350. /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
@@ -66,9 +66,11 @@ import { appendRunLedgerEntry, loadRunLedger, parseRunShardIdentity, queryRunCha
66
66
  import { emitSessionEvent, type SessionEventInput } from '../context-bus/session-events.js';
67
67
  import { decideLineageSupersede, lineageSupersedeCloseComment, type LineageSupersedeOpenDraft } from './lineage-supersede.js';
68
68
  import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
69
+ import { homedir } from 'node:os';
69
70
  import { enqueueControlMemo, readSoftStopRequestStatus, type SoftStopRequestRead } from '../harness/control-inbox.js';
70
71
  import { debug } from '../debug/log.js';
71
72
  import { recordFailureEvent } from './heal-intake.js';
73
+ import { classifyReworkCapExhaustion, type ReworkCapHealDecision } from './heal-triage.js';
72
74
  import { releasePathHold, releasePathHoldComment, RELEASE_PATH_LABEL } from '../self-dev/release-path-guard.js';
73
75
  import { emitDecision } from '../live/detail-switch.js';
74
76
  import { reworkDecision } from './decision-events.js';
@@ -94,12 +96,33 @@ import type { DocumentReferenceStatus } from './self-implement-runtime.js';
94
96
  import { DEV_PIPELINE_SINK_SURFACE } from './self-cli-sink-surface.js';
95
97
  import { registerLoopAgentSafe, type LoopAgentInput } from '../domains/loop-agent-registry.js';
96
98
  import { originObservationFields } from '../agent/origin-observation.js';
99
+ import { resolveLaunchStamp, type LaunchStamp } from '../harness/launch-stamp.js';
97
100
  import { getHarnessSpace, normalizeSpaceId, resolveRunIdentity } from '../harness/harness-space.js';
98
101
  import { getElanousConfigDir } from '../elanous-config-dir.js';
99
102
  import { elanousStateRoot } from '../autopilot/state-paths.js';
100
103
  import { addNextMdReleaseNote, harnessReleaseNote, parseReleaseNoteSection, releaseNotesDir, renderReleaseNoteSection, writeReleaseNote, type ReleaseNoteFragment, type ReleaseNoteSection } from '../release-loop/release-note.js';
101
104
  import { landingFreezeMessage } from '../release-loop/landing-freeze.js';
102
105
  import { admitLandingMerge, owningRepoRoot } from './frozen-merges.js';
106
+ import { recordFollowUpOnMerge, type FollowUpMergeInput, type FollowUpSeams } from './follow-up-goals.js';
107
+
108
+ /** Merge result and exit stay untouched: a draft failure is one debug line. */
109
+ async function draftFollowUpAfterMerge(input: FollowUpMergeInput, followUp: FollowUpSeams | undefined): Promise<void> {
110
+ try {
111
+ await recordFollowUpOnMerge(input, {
112
+ ...(followUp ? {
113
+ ...(followUp.mode ? { mode: followUp.mode } : {}),
114
+ ...(followUp.readDrafts ? { readDrafts: followUp.readDrafts } : {}),
115
+ ...(followUp.appendDraft ? { appendDraft: followUp.appendDraft } : {}),
116
+ ...(followUp.enqueue ? { enqueue: followUp.enqueue } : {}),
117
+ ...(followUp.cellOwner ? { cellOwner: followUp.cellOwner } : {}),
118
+ ...(followUp.stateRoot ? { stateRoot: followUp.stateRoot } : {}),
119
+ } : {}),
120
+ log: (category, event, data) => { debug.log(category, event, data); },
121
+ });
122
+ } catch (error) {
123
+ debug.log('self-implement.follow-up', 'draft-failed', { prNumber: input.prNumber, error: error instanceof Error ? error.message : String(error) });
124
+ }
125
+ }
103
126
  import { plannedSelfImplBranch } from '../harness/worktree-branch-prefix.js';
104
127
  export { slugifyFeature } from '../harness/worktree-branch-prefix.js';
105
128
  import { AUTO_REVIEW_LABEL, resolveAutoReviewLabels } from './context-capsule.js';
@@ -183,7 +206,7 @@ import type { RunOutcome, ReworkBudgetVerdict } from './run-outcome.js';
183
206
  import type { EscalateTier } from './rework-policy.js';
184
207
  import type { ClassifyShadowCallLLM } from './classify-shadow.js';
185
208
  import { hasModuleLoadFailure, type GateTestFailure } from './gate-baseline.js';
186
- import { commitWorktree, defaultBranchRef } from './seams.js';
209
+ import { commitWorktree, defaultBranchRef, preservationHasChanges } from './seams.js';
187
210
  import { detectUncommittedWork } from './uncommitted-work.js';
188
211
  import { spawnSync } from 'node:child_process';
189
212
  import { blockedDraftDisposition } from './blocked-draft-policy.js';
@@ -1037,10 +1060,13 @@ export interface SelfImplementSeams {
1037
1060
  /** Read-only cited-path observer. Omission uses the deterministic target-worktree filesystem observation. */
1038
1061
  observeCitedPathFacts?: (mustFix: readonly string[], cwd: string, round: number) => MustFixCitedPathFact[] | Promise<MustFixCitedPathFact[]>;
1039
1062
  /** PR을 열기 전에 보존할 산출물이 있는지 판별한다. `base` 대비 branch 고유 변경과 미커밋 변경을 함께 본다.
1040
- * 생략 시 기존 주입 seam 호환을 위해 산출물이 있다고 본다. */
1063
+ * Pod에서 생략 시 실제 git 상태로 판정한다. */
1041
1064
  preservationHasChanges?: (ctx: { cwd: string; base?: string }) => boolean | Promise<boolean>;
1042
1065
  /** Test seam for blocked environment branch persistence; return whether the branch was pushed. */
1043
- preserveBlockedBranch?: (ctx: { cwd: string; branch: string; title: string }) => boolean | Promise<boolean>;
1066
+ /** Test seam — the worktree commit to pin before main sync (default `git rev-parse HEAD`). */
1067
+ headCommit?: (cwd: string) => string | undefined;
1068
+ /** `commit` pins the pushed state; without it the worktree HEAD is pushed. */
1069
+ preserveBlockedBranch?: (ctx: { cwd: string; branch: string; title: string; commit?: string }) => boolean | Promise<boolean>;
1044
1070
  /** Hard-cap salvage evidence: a clean worktree and commits ahead of origin/main. */
1045
1071
  readReworkSalvageEvidence?: (cwd: string) => Promise<ReworkSalvageEvidence>;
1046
1072
  /** Starts exactly one detached `elanous dev --file <goal> --base <branch>` follow-up. */
@@ -1052,7 +1078,7 @@ export interface SelfImplementSeams {
1052
1078
  /** PR 생성 뒤 런의 라운드 대화를 게시한다. 실패는 PR 개설을 되돌리지 않는다. */
1053
1079
  postPrComment?: (opts: { number: number; body: string; cwd: string }) => Promise<void>;
1054
1080
  addPrLabel?: (opts: { number: number; label: string; cwd: string }) => Promise<void>;
1055
- readPrFiles?: (opts: { number: number; cwd: string }) => Promise<string[]>;
1081
+ readPrFiles?: (opts: { number: number; cwd: string; base?: string }) => Promise<string[]>;
1056
1082
  /** PR의 base/head SHA와 관측된 PR base를 한 응답에서 고정한다. 미주입·실패·빈 SHA면 검사 대상을 증명할 수 없어 fail-closed로 PR을 열어 둔다. */
1057
1083
  readPrCommitShas?: (opts: { number: number; cwd: string }) => Promise<{ baseCommit: string; headCommit: string; baseRefName?: string }>;
1058
1084
  /** 고정한 base/head SHA 쌍의 unified diff. 가변 PR 조회를 다시 사용하면 ABA head 변경에 안전하지 않으므로 두 SHA는 필수 입력이다. */
@@ -1086,6 +1112,12 @@ export interface SelfImplementSeams {
1086
1112
  readRunLedger?: (runId: string) => RunLedgerEntry[] | null | Promise<RunLedgerEntry[] | null>;
1087
1113
  closeDraft: (input: { number: number; comment: string }) => void | Promise<void>;
1088
1114
  };
1115
+ /**
1116
+ * FOLLOWUP-LOOP① — after a successful merge, leftover findings become one follow-up draft.
1117
+ * Tests inject every store. Omission writes nothing (no operational ledger, no queue).
1118
+ * Production `defaultSeams` fills the state-root ledger and, only in `live`, the harness queue.
1119
+ */
1120
+ followUp?: FollowUpSeams;
1089
1121
  /** ⑥ HITL 게이트 — PR open 전 승인. ★fail-closed: 생략/false 면 PR 안 열림(자동 승인 없음). review 동봉
1090
1122
  * (2026-07-21) — HITL 이 리뷰 verdict 를 보고 판단, auto 모드는 verdict 로 자동 결정. */
1091
1123
  approvePr?: (summary: { branch: string; gateLog?: string; implSummary: string; review?: SelfImplementReview }) => Promise<boolean>;
@@ -1995,6 +2027,8 @@ export interface SelfImplementOptions {
1995
2027
  graphOverlays?: readonly GraphOverlaySpec[];
1996
2028
  /** Validated, run-local graph variant injected at launch; rejected plans never start the pipeline. */
1997
2029
  graphVariantPlan?: import('./graph-variant.js').GraphVariantPlan;
2030
+ /** Door this run was launched through. Absent means the ledger stamp reads `unknown`. */
2031
+ launch?: LaunchStamp;
1998
2032
  /** ★ K run-identity — 이 self-implement 호출의 per-run join anchor. 미지정 시 상속(env)→canonical mint.
1999
2033
  * 리워크 라운드 전부가 이 값을 공유한다(`elanous self run <runId>` 가 호출 전체를 조인). */
2000
2034
  runId?: string;
@@ -2163,6 +2197,8 @@ export interface SelfImplementResult {
2163
2197
  runId: string;
2164
2198
  ok: boolean;
2165
2199
  stage: SelfImplementStage;
2200
+ /** Verbatim must-fix findings retained on a review-blocked terminal result. */
2201
+ mustFix?: readonly string[];
2166
2202
  /** Pipeline node that produced this terminal result. */
2167
2203
  node: PipelineNodeId;
2168
2204
  /** Structured terminal disposition for every normal run exit. */
@@ -2212,6 +2248,8 @@ export interface SelfImplementResult {
2212
2248
  quotaExhaustionAssessment?: QuotaExhaustionAssessment;
2213
2249
  /** Automatic merge was authorized before this run reached its terminal result. */
2214
2250
  mergeApprovalReceived?: true;
2251
+ /** Completed implementation survived a failed main sync on the pushed branch. */
2252
+ harvestable?: true;
2215
2253
  merged?: boolean;
2216
2254
  /** Observed PR base for a confirmed merge; never inferred from the requested launch base. */
2217
2255
  mergedBase?: string;
@@ -2619,7 +2657,9 @@ function prBody(feature: string, implSummary: string, gateLog?: string, review?:
2619
2657
  ...((gateLog ?? '').split('\n').filter((line) => line.startsWith('⚠️ 동작 검증 0:'))),
2620
2658
  ...exposeGateLines(gateLog),
2621
2659
  ...(gateLog ? ['```', gateLog.trim().slice(0, 3000), '```'] : []),
2622
- ...(revertGuard?.reverted.length ? [`- 되돌림 방지: ${revertGuard.reverted.length}파일 제외`] : []),
2660
+ ...(revertGuard?.reverted.length && revertGuard.reverted.length > (revertGuard.staleDeleted?.length ?? 0) ? [`- 되돌림 방지: ${revertGuard.reverted.length - (revertGuard.staleDeleted?.length ?? 0)}파일 제외`] : []),
2661
+ ...(revertGuard?.staleDeleted?.length ? [`- 되돌림 방지: 삭제 ${revertGuard.staleDeleted.length}파일 제외`] : []),
2662
+ ...(revertGuard?.outsideDeleted?.length ? [`- 삭제 경고: 대상 밖 ${revertGuard.outsideDeleted.length}파일 유지 (${revertGuard.outsideDeleted.join(', ')})`] : []),
2623
2663
  ...(revertGuard?.protected.length ? [`- 되돌림 경고: 대상 경로 ${revertGuard.protected.length}파일 유지 (${revertGuard.protected.join(', ')})`] : []),
2624
2664
  ...(revertGuard?.warning ? [`- 되돌림 검사 실패(커밋 계속): ${revertGuard.warning}`] : [])] : []),
2625
2665
  ...relaxation,
@@ -2968,6 +3008,7 @@ export function assembleBlockedDraftPrBody(
2968
3008
  JSON.stringify(classificationRecord),
2969
3009
  ...(state.reasonTruncated ? ['- reason truncated: true'] : []),
2970
3010
  ...(state.terminalStatus ? [`- child terminal status: reached=${state.terminalStatus.reached}; changed=${state.terminalStatus.changed}; toolCalls=${state.terminalStatus.toolCalls}; timedOut=${state.terminalStatus.timedOut}`] : []),
3011
+ ...(state.review?.mustFix.length ? ['## 마지막 리뷰 must-fix', ...state.review.mustFix.map((item) => `- ${item}`)] : []),
2971
3012
  ].join('\n');
2972
3013
  return preparePrBody(raw, persist, 'self-implement-blocked-draft-pr', essentialSuffix);
2973
3014
  }
@@ -3381,7 +3422,26 @@ function mergeDecisionEvidenceCoverage(evidenceCoverage: {
3381
3422
  : { evidenceCoverageMeasured: false };
3382
3423
  }
3383
3424
 
3425
+ function installedBuildForOrigin(env: NodeJS.ProcessEnv): { installedVersion: string; installedCommit: string } {
3426
+ const root = env.ELANOUS_TEST_HOME?.trim()
3427
+ ? join(env.ELANOUS_TEST_HOME.trim(), '.local/share/elanous')
3428
+ : join(homedir(), '.local/share/elanous');
3429
+ try {
3430
+ const parsed: unknown = JSON.parse(readFileSync(join(root, 'install.json'), 'utf8'));
3431
+ if (typeof parsed !== 'object' || parsed === null) return { installedVersion: 'unknown', installedCommit: 'unknown' };
3432
+ const version = (parsed as { version?: unknown }).version;
3433
+ const commit = (parsed as { commit?: unknown }).commit;
3434
+ return {
3435
+ installedVersion: typeof version === 'string' && version.trim() ? version.trim() : 'unknown',
3436
+ installedCommit: typeof commit === 'string' && commit.trim() ? commit.trim() : 'unknown',
3437
+ };
3438
+ } catch {
3439
+ return { installedVersion: 'unknown', installedCommit: 'unknown' };
3440
+ }
3441
+ }
3442
+
3384
3443
  function runOriginData(env: NodeJS.ProcessEnv = process.env): RunOriginData {
3444
+ const installed = installedBuildForOrigin(env);
3385
3445
  return {
3386
3446
  hostId: resolveHostId(env),
3387
3447
  hostname: hostname(),
@@ -3390,6 +3450,8 @@ function runOriginData(env: NodeJS.ProcessEnv = process.env): RunOriginData {
3390
3450
  substrate: env.ELANOUS_SUBSTRATE || 'host',
3391
3451
  instance: resolveInstanceName(),
3392
3452
  elanousVersion: (JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')) as { version: string }).version,
3453
+ installedVersion: installed.installedVersion,
3454
+ installedCommit: installed.installedCommit,
3393
3455
  ...(env.ELANOUS_POD_NAME ? { podName: env.ELANOUS_POD_NAME } : {}),
3394
3456
  ...(env.ELANOUS_NODE_NAME ? { nodeName: env.ELANOUS_NODE_NAME } : {}),
3395
3457
  ...(env.ELANOUS_POD_NAMESPACE ? { podNamespace: env.ELANOUS_POD_NAMESPACE } : {}),
@@ -3413,7 +3475,9 @@ export async function runSelfImplement(opts: SelfImplementOptions): Promise<Self
3413
3475
  const shardIdentity = parseRunShardIdentity(opts.feature);
3414
3476
  const observeOuter = makeRunObserver(runId, opts.goalId, debug.log.bind(debug), opts.seams.writeRunLedger, shardIdentity);
3415
3477
  try {
3416
- const origin = runOriginData();
3478
+ // 10-05 PODPROVIDER: record which child provider this run uses — «default» when the launch named none.
3479
+ const launch = opts.launch ?? resolveLaunchStamp();
3480
+ const origin = { ...runOriginData(), childProvider: opts.childLlm?.provider ?? 'default', ...(opts.childLlm?.model ? { childModel: opts.childLlm.model } : {}), launch };
3417
3481
  (opts.seams.writeRunLedger ?? appendRunLedgerEntry)({
3418
3482
  timestamp: new Date().toISOString(), runId, event: 'run-origin', data: origin,
3419
3483
  ...(opts.goalId ? { goalId: opts.goalId } : {}), ...shardIdentity,
@@ -3491,6 +3555,30 @@ export async function runSelfImplement(opts: SelfImplementOptions): Promise<Self
3491
3555
  let loopRegistered = false;
3492
3556
  let mergeApprovalReceived = false;
3493
3557
  let autoMergeAuthorizedBeforeSync = false;
3558
+ let finishedImplementationAtMainSync = false;
3559
+ // The finished implementation's commit, pinned before main sync starts. A timed-out mergeMain is not
3560
+ // cancellable and may keep moving HEAD in the same worktree, so the harvest pushes this SHA, never HEAD.
3561
+ let preSyncCommit: string | undefined;
3562
+ const preserveMergeHarvest = async (reason: string, inputChars?: number): Promise<boolean> => {
3563
+ if (!finishedImplementationAtMainSync || !worktreePath || !branch) return false;
3564
+ if (!preSyncCommit) {
3565
+ observeOuter('merge-harvest-push-failed', { branch, reason, error: 'pre-sync commit was not pinned' }, { level: 'warn' });
3566
+ return false;
3567
+ }
3568
+ try {
3569
+ const pushed = opts.seams.preserveBlockedBranch
3570
+ ? await opts.seams.preserveBlockedBranch({ cwd: worktreePath, branch, title: prTitle(opts.feature), commit: preSyncCommit })
3571
+ : runGitCommand(worktreePath, ['push', '-u', 'origin', `${preSyncCommit}:refs/heads/${branch}`]).status === 0;
3572
+ if (!pushed) throw new Error('branch push failed');
3573
+ // The worktree is left untouched: a late merge may still be writing it. The harvest source is the pinned commit.
3574
+ try { observeOuter('merge-abandoned-harvestable', { branch, reason, inputChars: inputChars ?? null, commit: preSyncCommit, worktree: 'left-as-is' }); }
3575
+ catch { /* observation cannot undo a confirmed push */ }
3576
+ return true;
3577
+ } catch (error) {
3578
+ observeOuter('merge-harvest-push-failed', { branch, reason, error: safeErrorDescription(error) }, { level: 'warn' });
3579
+ return false;
3580
+ }
3581
+ };
3494
3582
  let lastParsedSupervisorVerdict: ReworkBudgetVerdict | undefined;
3495
3583
  let lastParsedSupervisorReason: string | undefined;
3496
3584
  let providerErrorCount = 0;
@@ -3577,6 +3665,7 @@ export async function runSelfImplement(opts: SelfImplementOptions): Promise<Self
3577
3665
  },
3578
3666
  } satisfies LogSink);
3579
3667
  let traversalFinalized = false;
3668
+ reworkCapProceed = false;
3580
3669
  const progressDelivery = { delivered: 0, unwired: 0, callbackFailed: 0 };
3581
3670
  let capturedPrNumber: number | undefined;
3582
3671
  let openPrInvoked = false;
@@ -3970,6 +4059,8 @@ export async function runSelfImplement(opts: SelfImplementOptions): Promise<Self
3970
4059
  () => credentialFailureCount > 0,
3971
4060
  hasCurrentProviderError,
3972
4061
  hasCurrentCredentialFailure,
4062
+ (commit: string | undefined) => { finishedImplementationAtMainSync = true; preSyncCommit = commit; },
4063
+ preserveMergeHarvest,
3973
4064
  );
3974
4065
  terminalNode = result.node;
3975
4066
  await mergeChildProviderErrors();
@@ -4042,9 +4133,11 @@ export async function runSelfImplement(opts: SelfImplementOptions): Promise<Self
4042
4133
  } catch (e) {
4043
4134
  if (e instanceof StepTimeoutError) {
4044
4135
  observeOuter('step-timeout', { step: e.step, ms: e.ms, branch, feature: opts.feature.slice(0, 80) }, { level: 'warn' });
4136
+ const harvestable = e.step === 'merge' && await preserveMergeHarvest('step-timeout');
4045
4137
  const result: SelfImplementResult = {
4046
4138
  runId,
4047
4139
  ok: false,
4140
+ ...(harvestable ? { harvestable: true } : {}),
4048
4141
  stage: 'timed-out',
4049
4142
  node: 'implement',
4050
4143
  ...resolveRunOutcome({ termination: 'abandoned' }),
@@ -4052,7 +4145,9 @@ export async function runSelfImplement(opts: SelfImplementOptions): Promise<Self
4052
4145
  ...(worktreePath ? { worktreePath } : {}),
4053
4146
  ...(lastParsedSupervisorReason ? { supervisorReason: lastParsedSupervisorReason } : {}),
4054
4147
  ...(mergeApprovalReceived || (e.step === 'merge' && autoMergeAuthorizedBeforeSync) ? { mergeApprovalReceived: true } : {}),
4055
- detail: worktreePath
4148
+ detail: harvestable
4149
+ ? `자율 단계 '${e.step}' wall-clock ${e.ms}ms 초과 — 구현 완료 · 수확 대기 (${branch}, worktree: ${worktreePath})`
4150
+ : worktreePath
4056
4151
  ? `자율 단계 '${e.step}' wall-clock ${e.ms}ms 초과 — hang 방지 종결(worktree 회수 경로: ${worktreePath})`
4057
4152
  : `자율 단계 '${e.step}' wall-clock ${e.ms}ms 초과 — hang 방지 종결(worktree 생성 전)`,
4058
4153
  };
@@ -4134,10 +4229,31 @@ export async function runSelfImplement(opts: SelfImplementOptions): Promise<Self
4134
4229
  const terminalStage = terminalResult?.stage ?? 'crashed';
4135
4230
  try {
4136
4231
  const mappedStatus = mapStageToRunStatus(terminalStage)?.runStatus;
4137
- if (!terminalResult || mappedStatus === 'failed' || (mappedStatus === undefined && terminalResult.ok === false) || signalIncompleteState(roundClassifications).latestSignalIncomplete) {
4232
+ const reworkCapStop = terminalResult?.stage === 'pr-opened' && (
4233
+ terminalResult.outcome === 'budget-exhausted'
4234
+ || (terminalResult.outcome === 'completed' && reworkCapProceed)
4235
+ );
4236
+ if (!terminalResult || mappedStatus === 'failed' || (mappedStatus === undefined && terminalResult.ok === false) || signalIncompleteState(roundClassifications).latestSignalIncomplete || reworkCapStop) {
4237
+ const reworkCap = reworkCapStop ? classifyReworkCapExhaustion({
4238
+ introduced: terminalResult?.gate?.reflectGateFacts?.introduced
4239
+ ? [`introduced=${terminalResult.gate.reflectGateFacts.introduced}`]
4240
+ : [],
4241
+ mustFix: terminalResult?.review?.mustFix ?? terminalResult?.mustFix,
4242
+ summary: terminalResult?.review?.summary,
4243
+ }) : undefined;
4244
+ if (reworkCap) {
4245
+ observeOuter('rework-cap-heal', {
4246
+ stage: terminalStage,
4247
+ defectClass: reworkCap.defectClass,
4248
+ nextAction: reworkCap.nextAction,
4249
+ evidence: reworkCap.evidence,
4250
+ });
4251
+ }
4138
4252
  recordFailureEvent({
4139
- source: 'harness-run', kind: terminalStage, ref: runId,
4140
- summary: opts.feature.split(/\r?\n/, 1)[0]?.trim() || `Harness run ${runId} failed`,
4253
+ source: 'harness-run', kind: reworkCapStop ? 'rework-cap-exhausted' : terminalStage, ref: runId,
4254
+ summary: reworkCap
4255
+ ? reworkCapFailureSummary(opts.feature, reworkCap)
4256
+ : opts.feature.split(/\r?\n/, 1)[0]?.trim() || `Harness run ${runId} failed`,
4141
4257
  at: new Date().toISOString(),
4142
4258
  });
4143
4259
  }
@@ -4499,6 +4615,14 @@ function observeRunRollup(
4499
4615
  }, effectiveOutcome.runStatus === 'failed' ? { level: 'warn' } : undefined);
4500
4616
  }
4501
4617
 
4618
+ let reworkCapProceed = false;
4619
+
4620
+ function reworkCapFailureSummary(feature: string, decision: ReworkCapHealDecision): string {
4621
+ const title = feature.split(/\\r?\\n/, 1)[0]?.trim() || 'rework cap exhausted';
4622
+ const evidence = decision.evidence.length ? decision.evidence.join(' | ') : 'no gate introduced list and no review finding';
4623
+ return `${title} [${decision.defectClass} → ${decision.nextAction}] ${evidence}`;
4624
+ }
4625
+
4502
4626
  async function runSelfImplementInner(
4503
4627
  opts: SelfImplementOptions,
4504
4628
  identity?: { runId: string; source: ReturnType<typeof resolveRunIdentity>['source'] },
@@ -4545,6 +4669,8 @@ async function runSelfImplementInner(
4545
4669
  hasObservedCredentialFailure: () => boolean = hasCredentialFailure,
4546
4670
  hasCurrentProviderError: () => boolean = hasProviderError,
4547
4671
  hasCurrentCredentialFailure: () => boolean = hasCredentialFailure,
4672
+ onMainSyncStarted: (commit: string | undefined) => void = () => {},
4673
+ preserveMergeHarvest: (reason: string, inputChars?: number) => Promise<boolean> = async () => false,
4548
4674
  ): Promise<Omit<SelfImplementResult, 'runId'>> {
4549
4675
  const s = opts.seams;
4550
4676
  const pipelineStartedAtMs = Date.now();
@@ -4983,6 +5109,8 @@ async function runSelfImplementInner(
4983
5109
  } | undefined;
4984
5110
  let gate!: SelfImplementGateResult;
4985
5111
  let review: SelfImplementReview | undefined;
5112
+ /** Review-budget leftovers and the body that carried them, so a later merge drafts from the same text. */
5113
+ let preparedReviewBudgetPrBody: string | undefined;
4986
5114
  const reviewMustFixCountHistory: number[] = [];
4987
5115
  const reviewMustFixTrend = (): string => {
4988
5116
  if (reviewMustFixCountHistory.length < 2) return '아직 못 잰다';
@@ -5318,6 +5446,23 @@ async function runSelfImplementInner(
5318
5446
  observe('pr.comment.seam-missing', { number: pr.number, buffered: prCommentBuffer.length }, { level: 'warn' });
5319
5447
  return;
5320
5448
  }
5449
+ const marker = '<!-- elanous:run-status -->';
5450
+ if (s.findPrComment && s.editPrComment) {
5451
+ try {
5452
+ const existing = await s.findPrComment({ number: pr.number, marker, cwd: wt.path });
5453
+ const history = prCommentBuffer.join('\n\n');
5454
+ const body = existing
5455
+ ? existing.body.includes('</details>')
5456
+ ? existing.body.replace(/<\/details>(?![\s\S]*<\/details>)/, `\n\n${history}\n</details>`)
5457
+ : `${existing.body}\n\n<details>\n<summary>Round history</summary>\n\n${history}\n</details>`
5458
+ : `${marker}\n<details>\n<summary>Round history</summary>\n\n${history}\n</details>`;
5459
+ if (existing) await s.editPrComment({ id: existing.id, body, cwd: wt.path });
5460
+ else await s.postPrComment({ number: pr.number, body, cwd: wt.path });
5461
+ } catch (error) {
5462
+ // Unknown lookup/edit outcome is not proof of absence: never create a second status comment.
5463
+ observe('pr.comment.status-failed', { number: pr.number, error: error instanceof Error ? error.message : String(error) }, { level: 'warn' });
5464
+ }
5465
+ }
5321
5466
  for (const body of prCommentBuffer) {
5322
5467
  try {
5323
5468
  await s.postPrComment({ number: pr.number, body, cwd: wt.path });
@@ -5338,9 +5483,11 @@ async function runSelfImplementInner(
5338
5483
  let finishedChildCommitted = false;
5339
5484
  const revertGuard: RevertGuardResult = { reverted: [], protected: [] };
5340
5485
  const inspectStaleReverts = (targets: readonly string[]): void => {
5341
- const inspected = dropStaleReverts(wt.path, targets, runId, getUserConfig().harness?.revertGuard?.depth);
5486
+ const inspected = dropStaleReverts(wt.path, targets, runId, getUserConfig().harness?.revertGuard?.depth, wt.resolvedBase);
5342
5487
  for (const file of inspected.reverted) if (!revertGuard.reverted.includes(file)) revertGuard.reverted.push(file);
5343
5488
  for (const file of inspected.protected) if (!revertGuard.protected.includes(file)) revertGuard.protected.push(file);
5489
+ for (const file of inspected.staleDeleted ?? []) if (!revertGuard.staleDeleted?.includes(file)) (revertGuard.staleDeleted ??= []).push(file);
5490
+ for (const file of inspected.outsideDeleted ?? []) if (!revertGuard.outsideDeleted?.includes(file)) (revertGuard.outsideDeleted ??= []).push(file);
5344
5491
  if (inspected.warning) revertGuard.warning = inspected.warning;
5345
5492
  };
5346
5493
  const classifyUnfinishedRun = (stage: SelfImplementStage, verdict?: ReworkBudgetVerdict, decomposition?: TerminalDecomposition): AbandonedClassificationResult => {
@@ -5363,6 +5510,14 @@ async function runSelfImplementInner(
5363
5510
  mustFixReported: (review?.mustFix.length ?? 0) > 0,
5364
5511
  });
5365
5512
  };
5513
+ const podReviewBlockedDraft = process.env.ELANOUS_SUBSTRATE === 'pod';
5514
+ let blockedDraftNoChanges = false;
5515
+ let blockedDraftPrFailure: string | undefined;
5516
+ const reviewBlockedResult = (pr: { url: string; number: number } | undefined) => ({
5517
+ stage: blockedDraftNoChanges && podReviewBlockedDraft ? 'no-changes' as const : podReviewBlockedDraft && !pr ? 'aborted' as const : 'review-blocked' as const,
5518
+ mustFix: [...(review?.mustFix ?? [])],
5519
+ ...(pr ? { prUrl: pr.url, prNumber: pr.number } : {}),
5520
+ });
5366
5521
  const preserveBlockedArtifacts = async ({ stage, reason, verdict, gate: preservedGate, typecheck, decomposition, childSummary, reasonTruncated, failureKind, terminalStatus, salvageStatusExpected }: { stage: 'review-blocked' | 'gate-failed' | 'aborted'; reason: string; verdict?: ReworkBudgetVerdict; gate?: SelfImplementGateResult; typecheck?: { passed: boolean; log?: string }; decomposition?: TerminalDecomposition; childSummary?: string; reasonTruncated?: boolean; failureKind?: ReturnType<typeof classifyReviewProviderFailure> | null; terminalStatus?: ImplementTerminalStatus; salvageStatusExpected: boolean }): Promise<{ url: string; number: number } | undefined> => {
5367
5522
  let childSummaryArtifact: ChildSummaryArtifactRef | undefined;
5368
5523
  if (childSummary !== undefined) {
@@ -5455,6 +5610,7 @@ async function runSelfImplementInner(
5455
5610
  ...(childSummaryArtifact ? { childSummaryArtifactPath: childSummaryArtifact.path } : {}),
5456
5611
  };
5457
5612
  if (s.preservationHasChanges && !await s.preservationHasChanges({ cwd: wt.path, base: preservationBase })) {
5613
+ if (stage === 'review-blocked' && podReviewBlockedDraft) blockedDraftNoChanges = true;
5458
5614
  observe('rework-blocked-draft-pr', { ...observation, skipped: 'no-changes' });
5459
5615
  progress(stage, '보존할 변경이 없어 draft PR을 열지 않음');
5460
5616
  return undefined;
@@ -5488,8 +5644,9 @@ async function runSelfImplementInner(
5488
5644
  s.persistPrBodyArtifact,
5489
5645
  );
5490
5646
  // no-changes wins; worktree-only remains local even for environment stops.
5491
- const environmentStop = blockedDraftDisposition(classificationRecord.classification) === 'preserve-branch';
5492
- const worktreeOnly = opts.completion === 'worktree-only';
5647
+ const environmentStop = blockedDraftDisposition(classificationRecord.classification) === 'preserve-branch'
5648
+ && !(stage === 'review-blocked' && podReviewBlockedDraft);
5649
+ const worktreeOnly = opts.completion === 'worktree-only' && !(stage === 'review-blocked' && podReviewBlockedDraft);
5493
5650
  if (worktreeOnly || environmentStop) {
5494
5651
  let artifactPath: string | undefined;
5495
5652
  let artifactError: string | undefined;
@@ -5559,7 +5716,54 @@ async function runSelfImplementInner(
5559
5716
  }
5560
5717
  return undefined;
5561
5718
  }
5719
+ if (stage === 'review-blocked' && podReviewBlockedDraft && !s.preservationHasChanges
5720
+ && !preservationHasChanges(wt.path, preservationBase)) {
5721
+ blockedDraftNoChanges = true;
5722
+ observe('rework-blocked-draft-pr', { ...observation, skipped: 'no-changes' });
5723
+ progress(stage, '보존할 변경이 없어 draft PR을 열지 않음');
5724
+ return undefined;
5725
+ }
5726
+ // Pod review-blocked: a failed commit or PR still pushes the branch, and the result detail says what survived.
5727
+ const preserveReviewBlockedBranch = async (cause: string, commitFirst: boolean, suffix = ''): Promise<undefined> => {
5728
+ let pushed = false;
5729
+ try {
5730
+ if (s.preserveBlockedBranch) {
5731
+ pushed = await s.preserveBlockedBranch({ cwd: wt.path, branch: wt.branch, title: prTitle(opts.feature) });
5732
+ } else {
5733
+ if (commitFirst) {
5734
+ const commit = commitWorktree(wt.path, prTitle(opts.feature));
5735
+ if (!commit.ok && !/nothing to commit/.test(commit.out)) throw new Error(`로컬 브랜치 커밋 실패: ${commit.out}`);
5736
+ }
5737
+ const push = runGitCommand(wt.path, ['push', '-u', 'origin', `HEAD:refs/heads/${wt.branch}`]);
5738
+ if (push.status !== 0) throw new Error(push.stderr || push.stdout || 'git push failed');
5739
+ pushed = true;
5740
+ }
5741
+ if (!pushed) throw new Error('브랜치 push가 성공하지 않았음');
5742
+ } catch (pushError) {
5743
+ const pushReason = safeErrorDescription(pushError);
5744
+ blockedDraftPrFailure = `${cause}; 원격 브랜치 보존 실패: ${pushReason}; worktree: ${wt.path}`;
5745
+ observe('rework-blocked-draft-pr', { ...observation, error: cause, pushed: false, pushError: pushReason, branch: wt.branch }, { level: 'warn' });
5746
+ progress('aborted', blockedDraftPrFailure);
5747
+ return undefined;
5748
+ }
5749
+ blockedDraftPrFailure = `${cause}; 원격 브랜치 보존: ${wt.branch}${suffix}`;
5750
+ observe('rework-blocked-draft-pr', { ...observation, error: cause, pushed: true, branch: wt.branch }, { level: 'warn' });
5751
+ progress('aborted', blockedDraftPrFailure);
5752
+ return undefined;
5753
+ };
5562
5754
  progress('pr-opening', '중단 산출물을 draft PR로 보존…');
5755
+ if (stage === 'review-blocked' && podReviewBlockedDraft) {
5756
+ let commitError: string | undefined;
5757
+ try {
5758
+ const commit = commitWorktree(wt.path, prTitle(opts.feature));
5759
+ if (!commit.ok && !/nothing to commit/.test(commit.out)) commitError = commit.out.trim() || 'git commit failed';
5760
+ } catch (error) {
5761
+ commitError = safeErrorDescription(error);
5762
+ }
5763
+ if (commitError !== undefined) {
5764
+ return preserveReviewBlockedBranch(`review-blocked 작업 커밋 실패: ${commitError}`, false, `; 미커밋 작업은 worktree 에 남음: ${wt.path}`);
5765
+ }
5766
+ }
5563
5767
  let pr: { url: string; number: number };
5564
5768
  try {
5565
5769
  pr = await withStepTimeout(s.openPr({
@@ -5568,6 +5772,7 @@ async function runSelfImplementInner(
5568
5772
  head: wt.branch,
5569
5773
  ...(opts.base ? { base: opts.base } : {}),
5570
5774
  draft: true,
5775
+ ...(stage === 'review-blocked' ? { labels: ['review-blocked'] } : {}),
5571
5776
  cwd: wt.path,
5572
5777
  }), T.pr, 'pr');
5573
5778
  } catch (error) {
@@ -5589,6 +5794,10 @@ async function runSelfImplementInner(
5589
5794
  ...(artifactPath ? { prBodyArtifactPath: artifactPath } : {}),
5590
5795
  ...(artifactError ? { prBodyArtifactError: artifactError } : {}),
5591
5796
  }, { level: 'warn' });
5797
+ if (stage === 'review-blocked' && podReviewBlockedDraft) {
5798
+ return preserveReviewBlockedBranch(`draft PR 생성 실패: ${prError}`, true,
5799
+ `; ${artifactPath ? `판정 본문: ${artifactPath}` : `판정 본문 보존 실패: ${artifactError ?? 'unknown error'}`}`);
5800
+ }
5592
5801
  progress('aborted', artifactPath
5593
5802
  ? `중단 산출물의 draft PR 생성 실패(worktree·branch 보존 · 판정 본문 보존: ${artifactPath})`
5594
5803
  : `중단 산출물의 draft PR 생성 실패(worktree·branch 보존 · 판정 본문 보존 실패: ${artifactError ?? 'unknown error'})`);
@@ -5601,6 +5810,9 @@ async function runSelfImplementInner(
5601
5810
  ...(assembled.truncated ? { prBodyTruncated: true, prBodyOriginalChars: assembled.originalChars } : {}),
5602
5811
  });
5603
5812
  await flushPrComments(pr);
5813
+ if (stage === 'review-blocked') {
5814
+ try { debug.log('self-implement.terminal', 'review-blocked-pr', { runId, prNumber: pr.number, round: Math.max(0, round - 1) }); } catch { /* observation is fail-soft */ }
5815
+ }
5604
5816
  progress('pr-opened', `중단 산출물 draft PR 개설 (#${pr.number}·사람 판단 대기)`);
5605
5817
  return pr;
5606
5818
  };
@@ -5684,6 +5896,7 @@ async function runSelfImplementInner(
5684
5896
  'self-implement-review-budget-pr',
5685
5897
  followUpMustFix,
5686
5898
  );
5899
+ preparedReviewBudgetPrBody = preparedPrBody.body;
5687
5900
  const pr = await withStepTimeout(s.openPr({
5688
5901
  title: prTitle(opts.feature),
5689
5902
  body: preparedPrBody.body,
@@ -6097,6 +6310,7 @@ async function runSelfImplementInner(
6097
6310
  if (budgetDecision.stop) {
6098
6311
  if (budgetDecision.exit === 'proceed') {
6099
6312
  finalizeSupervisorDeliveries('supervisor-rework-proceed');
6313
+ if (round > effectiveMax) reworkCapProceed = true;
6100
6314
  break;
6101
6315
  }
6102
6316
  finalizeSupervisorDeliveries('supervisor-rework-blocked');
@@ -6113,7 +6327,7 @@ async function runSelfImplementInner(
6113
6327
  if (reflectRejectedAudit.length && review) review.shouldFix = [...review.shouldFix, ...reflectRejectedAudit];
6114
6328
  const pr = await preserveBlockedArtifacts({ stage: 'review-blocked', verdict: parsedDecision.verdict, reason: parsedDecision.reason, gate, decomposition: terminalDecomposition, salvageStatusExpected: false });
6115
6329
  const quotaExhaustionAssessment = quotaExhaustionAssessmentForRun();
6116
- return { ok: false, stage: 'review-blocked', node: 'rework', ...terminal, ...supervisorOutcome, sessionId, worktreePath: wt.path, branch: wt.branch, gate, review, quotaExhaustionAssessment, ...(lastSupervisorReason ? { supervisorReason: lastSupervisorReason } : {}), ...(pr ? { prUrl: pr.url, prNumber: pr.number } : {}), detail: `rework judged unconvergeable after ${round} rework round(s): ${parsedDecision.reason}` };
6330
+ return { ok: false, node: 'rework', ...terminal, ...supervisorOutcome, sessionId, worktreePath: wt.path, branch: wt.branch, gate, review, quotaExhaustionAssessment, ...(lastSupervisorReason ? { supervisorReason: lastSupervisorReason } : {}), ...reviewBlockedResult(pr), detail: `${blockedDraftPrFailure ? `${blockedDraftPrFailure}; ` : ''}rework judged unconvergeable after ${round} rework round(s): ${parsedDecision.reason}` };
6117
6331
  }
6118
6332
  const baselineStop = noRetryBaseline();
6119
6333
  const reason = baselineStop.terminationReason ?? parsedDecision.reason;
@@ -6141,7 +6355,7 @@ async function runSelfImplementInner(
6141
6355
  if (isReviewRework(reworkKind)) {
6142
6356
  if (reflectRejectedAudit.length && review) review.shouldFix = [...review.shouldFix, ...reflectRejectedAudit];
6143
6357
  const pr = await preserveBlockedArtifacts({ stage: 'review-blocked', verdict: lastReworkVerdict, reason, gate, salvageStatusExpected: false });
6144
- return { ok: false, stage: 'review-blocked', node: 'rework', ...terminal, sessionId, worktreePath: wt.path, branch: wt.branch, gate, review, quotaExhaustionAssessment: quotaExhaustionAssessmentForRun(), ...(lastSupervisorVerdict === 'CONTRACT-CONFLICT' ? { supervisorVerdict: lastSupervisorVerdict } : {}), ...(pr ? { prUrl: pr.url, prNumber: pr.number } : {}), detail: reason };
6358
+ return { ok: false, node: 'rework', ...terminal, sessionId, worktreePath: wt.path, branch: wt.branch, gate, review, quotaExhaustionAssessment: quotaExhaustionAssessmentForRun(), ...(lastSupervisorVerdict === 'CONTRACT-CONFLICT' ? { supervisorVerdict: lastSupervisorVerdict } : {}), ...reviewBlockedResult(pr), detail: blockedDraftPrFailure ?? reason };
6145
6359
  }
6146
6360
  const baselineStop = noRetryBaseline();
6147
6361
  const pr = await preserveBlockedArtifacts({ stage: 'gate-failed', verdict: lastReworkVerdict, reason: baselineStop.terminationReason ?? reason, gate, salvageStatusExpected: false });
@@ -6157,32 +6371,47 @@ async function runSelfImplementInner(
6157
6371
  if (isReviewRework(reworkKind)) {
6158
6372
  if (reflectRejectedAudit.length && review) review.shouldFix = [...review.shouldFix, ...reflectRejectedAudit];
6159
6373
  const reason = `review must-fix unresolved after ${round - 1} rework round(s)`;
6160
- const acceptance = await acceptReviewBudget(
6161
- review?.mustFix ?? [],
6162
- reworkParts.some((part) => part.source === 'supervisor'),
6163
- );
6164
- if (acceptance.decision.action === 'accept') {
6165
- return {
6166
- ok: true,
6167
- stage: 'pr-opened',
6168
- node: 'open-pr',
6169
- ...terminal,
6170
- sessionId,
6171
- worktreePath: wt.path,
6172
- branch: wt.branch,
6173
- gate,
6174
- review,
6175
- mergeReason: 'review-budget-follow-up-required',
6176
- followUpMustFix: acceptance.decision.unresolvedMustFix,
6177
- followUpMustFixCount: acceptance.decision.unresolvedMustFix.length,
6178
- ...(acceptance.pr ? { prUrl: acceptance.pr.url, prNumber: acceptance.pr.number } : {}),
6179
- detail: `${reason}; accepted with ${acceptance.decision.unresolvedMustFix.length} follow-up must-fix item(s)`,
6180
- };
6374
+ // Pod keeps review-blocked work as a labelled draft (or aborts with the preserved branch) instead of
6375
+ // accepting the review budget into a non-draft PR — restored after #24341 (0.2.16 gate shard 5).
6376
+ if (!podReviewBlockedDraft) {
6377
+ const acceptance = await acceptReviewBudget(
6378
+ review?.mustFix ?? [],
6379
+ reworkParts.some((part) => part.source === 'supervisor'),
6380
+ );
6381
+ if (acceptance.decision.action === 'accept') {
6382
+ if (acceptance.pr) {
6383
+ await draftFollowUpAfterMerge({
6384
+ prNumber: acceptance.pr.number,
6385
+ followUpMustFix: acceptance.decision.unresolvedMustFix,
6386
+ shouldFix: review?.shouldFix,
6387
+ prBody: preparedReviewBudgetPrBody,
6388
+ feature: opts.feature,
6389
+ ...(opts.goalId ? { cellId: opts.goalId } : {}),
6390
+ ...(opts.goalFile ? { goalFile: opts.goalFile } : {}),
6391
+ }, s.followUp);
6392
+ }
6393
+ return {
6394
+ ok: true,
6395
+ stage: 'pr-opened',
6396
+ node: 'open-pr',
6397
+ ...terminal,
6398
+ sessionId,
6399
+ worktreePath: wt.path,
6400
+ branch: wt.branch,
6401
+ gate,
6402
+ review,
6403
+ mergeReason: 'review-budget-follow-up-required',
6404
+ followUpMustFix: acceptance.decision.unresolvedMustFix,
6405
+ followUpMustFixCount: acceptance.decision.unresolvedMustFix.length,
6406
+ ...(acceptance.pr ? { prUrl: acceptance.pr.url, prNumber: acceptance.pr.number } : {}),
6407
+ detail: `${reason}; accepted with ${acceptance.decision.unresolvedMustFix.length} follow-up must-fix item(s)`,
6408
+ };
6409
+ }
6181
6410
  }
6182
6411
  const pr = await preserveBlockedArtifacts({ stage: 'review-blocked', reason, gate, salvageStatusExpected: true });
6183
- const salvage = await salvageHardCap(canSalvage, pr);
6412
+ const salvage = blockedDraftNoChanges || blockedDraftPrFailure ? 'parked' : await salvageHardCap(canSalvage, pr);
6184
6413
  const disposition = { ...terminal, ...(salvage ? { salvage } : {}) };
6185
- return { ok: false, stage: 'review-blocked', node: 'rework', ...disposition, sessionId, worktreePath: wt.path, branch: wt.branch, gate, review, quotaExhaustionAssessment: quotaExhaustionAssessmentForRun(), ...(lastSupervisorVerdict === 'CONTRACT-CONFLICT' ? { supervisorVerdict: lastSupervisorVerdict } : {}), ...(pr ? { prUrl: pr.url, prNumber: pr.number } : {}), detail: reason };
6414
+ return { ok: false, node: 'rework', ...disposition, sessionId, worktreePath: wt.path, branch: wt.branch, gate, review, quotaExhaustionAssessment: quotaExhaustionAssessmentForRun(), ...(lastSupervisorVerdict === 'CONTRACT-CONFLICT' ? { supervisorVerdict: lastSupervisorVerdict } : {}), ...reviewBlockedResult(pr), detail: blockedDraftPrFailure ?? reason };
6186
6415
  }
6187
6416
  const baselineStop = noRetryBaseline();
6188
6417
  const reason = baselineStop.terminationReason ?? `gate failed after ${round - 1} rework round(s)`;
@@ -7073,9 +7302,13 @@ async function runSelfImplementInner(
7073
7302
  `Child completion: ${impl.summary.trim() || '(summary unavailable)'}`,
7074
7303
  ]);
7075
7304
  if (review) {
7076
- bufferPrComment(round, 'reviewer', `Round ${round}: review completed (${review.verdict}).`, [
7077
- `Review summary: ${review.summary.trim() || '(summary unavailable)'}`,
7078
- ]);
7305
+ if (review.reviewed) {
7306
+ bufferPrComment(round, 'reviewer', `Round ${round}: review completed (${review.verdict}).`, [
7307
+ `Review summary: ${review.summary.trim() || '(summary unavailable)'}`,
7308
+ ]);
7309
+ } else {
7310
+ bufferPrComment(round, 'reviewer', `Round ${round}: 리뷰 못 함(사유: ${review.failureReason?.trim() || '사유 미상'}) — 통과 판정 아님`, []);
7311
+ }
7079
7312
  }
7080
7313
  finalizeSupervisorDeliveries('no-next-rework-round');
7081
7314
  break;
@@ -7193,7 +7426,23 @@ async function runSelfImplementInner(
7193
7426
  return { ok: false, stage: 'merge-conflict', node: 'main-sync', ...resolveRunOutcome({ termination: 'abandoned' }), ...decisionSignalResult, sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), detail: 'pre-PR main sync default-branch-unresolved' };
7194
7427
  }
7195
7428
  progress('gating', `최신 ${mergeTarget} 정합 (병렬 드리프트·LLM 충돌해결)…`);
7196
- const sync = await withStepTimeout(s.mergeMain(wt.path, mergeTarget), T.merge, 'merge');
7429
+ onMainSyncStarted((s.headCommit ?? ((cwd: string) => {
7430
+ const head = runGitCommand(cwd, ['rev-parse', '--verify', 'HEAD'], { encoding: 'utf8' });
7431
+ return head.status === 0 ? String(head.stdout).trim() || undefined : undefined;
7432
+ }))(wt.path));
7433
+ let sync: Awaited<ReturnType<NonNullable<SelfImplementSeams['mergeMain']>>>;
7434
+ try {
7435
+ sync = await withStepTimeout(s.mergeMain(wt.path, mergeTarget), T.merge, 'merge');
7436
+ } catch (error) {
7437
+ if (error instanceof StepTimeoutError) throw error;
7438
+ const harvestable = await preserveMergeHarvest('merge-interrupted');
7439
+ if (!harvestable) throw error;
7440
+ canAuto = false;
7441
+ mergeReason = 'main-sync-interrupted';
7442
+ recordMergeDecision();
7443
+ progress('merge-conflict', `⚠️ ${mergeTarget} 정합 중단 — 사람 수확 (${wt.branch})`);
7444
+ return { ok: false, stage: 'merge-conflict', node: 'main-sync', ...resolveRunOutcome({ termination: 'abandoned' }), ...decisionSignalResult, sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), harvestable: true, detail: `pre-PR main sync interrupted: ${safeErrorDescription(error)}` };
7445
+ }
7197
7446
  // ⛔⭐ `OBS-T96` 🅐 — 「무엇을 봤는지」를 남긴다. 수만 남기면 정합 실패가 «재현 불가»가 된다.
7198
7447
  observe('pre-pr-sync', mainSyncObservation(sync, mergeTarget, opts.base, wt.branch));
7199
7448
  if (sync.status === 'conflict-unresolved' || sync.status === 'error') {
@@ -7201,7 +7450,9 @@ async function runSelfImplementInner(
7201
7450
  canAuto = false;
7202
7451
  mergeReason = `main-sync-${sync.status}`;
7203
7452
  recordMergeDecision();
7204
- return { ok: false, stage: 'merge-conflict', node: 'main-sync', ...resolveRunOutcome({ termination: 'abandoned' }), ...decisionSignalResult, sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), detail: `pre-PR main sync ${sync.status}` };
7453
+ const harvestable = (sync.reason === 'conflict-input-too-large' || sync.reason === 'conflict-resolver-interrupted')
7454
+ && await preserveMergeHarvest(sync.reason, sync.inputChars);
7455
+ return { ok: false, stage: 'merge-conflict', node: 'main-sync', ...resolveRunOutcome({ termination: 'abandoned' }), ...decisionSignalResult, sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), ...(harvestable ? { harvestable: true } : {}), detail: harvestable ? sync.reason === 'conflict-input-too-large' ? `충돌 큼 — 사람 수확 (${wt.branch}, 입력 ${sync.inputChars}자)` : `충돌 해결 중단 — 사람 수확 (${wt.branch})` : `pre-PR main sync ${sync.status}` };
7205
7456
  }
7206
7457
  if (sync.status === 'llm-resolved' || sync.status === 'merged') {
7207
7458
  preservationBase = opts.base ?? mergeTarget;
@@ -7385,11 +7636,15 @@ async function runSelfImplementInner(
7385
7636
  let releasePathInspectionError: string | undefined;
7386
7637
  if (s.readPrFiles) {
7387
7638
  try {
7388
- const files = await withStepTimeout(s.readPrFiles({ number: pr.number, cwd: wt.path }), T.pr, 'pr');
7639
+ // Rate-limit recovery may wait up to 15 minutes for X-RateLimit-Reset; the PR step cap must not cut that wait.
7640
+ const files = await s.readPrFiles({ number: pr.number, cwd: wt.path, ...(opts.base ? { base: opts.base } : {}) });
7389
7641
  if (!Array.isArray(files) || !files.every((path) => typeof path === 'string')) throw new Error('invalid PR file list');
7390
7642
  releaseHoldPath = releasePathHold(files);
7391
7643
  } catch (error) {
7392
- releasePathInspectionError = safeErrorDescription(error);
7644
+ const rateLimited = error instanceof Error && (error as { rateLimited?: boolean }).rateLimited === true;
7645
+ releasePathInspectionError = rateLimited
7646
+ ? `rate-limit: ${safeErrorDescription(error)}`
7647
+ : safeErrorDescription(error);
7393
7648
  }
7394
7649
  } else if (canAuto || labels?.includes(AUTO_REVIEW_LABEL)) {
7395
7650
  releasePathInspectionError = 'readPrFiles seam unavailable';
@@ -7428,7 +7683,9 @@ async function runSelfImplementInner(
7428
7683
  if (releasePathInspectionError && (canAuto || labels?.includes(AUTO_REVIEW_LABEL))) {
7429
7684
  observe('release-path-inspection-failed', { number: pr.number, error: releasePathInspectionError }, { level: 'error' });
7430
7685
  canAuto = false;
7431
- mergeSkipReason = mergeReason = 'release-path-inspection-failed';
7686
+ mergeSkipReason = mergeReason = releasePathInspectionError.startsWith('rate-limit:')
7687
+ ? 'release-path-inspection-failed: rate-limit'
7688
+ : 'release-path-inspection-failed';
7432
7689
  recordMergeDecision();
7433
7690
  progress('pr-opened', `OP 승인 대기 (#${pr.number}): PR 경로 조회 실패`);
7434
7691
  return { ok: true, stage: 'pr-opened', node: 'open-pr', ...resolveRunOutcome({ termination: 'completed' }), ...decisionSignalResult, ...preservedCompletionResult, ...(completionStatus ? { completionStatus } : {}), sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), mergeReason, prUrl: pr.url, prNumber: pr.number };
@@ -7522,6 +7779,16 @@ async function runSelfImplementInner(
7522
7779
  const m = await withStepTimeout(merging, T.pr, 'pr');
7523
7780
  observe('merged', { number: pr.number, merged: m.merged, detail: m.detail ?? null });
7524
7781
  if (m.merged) {
7782
+ const mergedBase = m.baseRefName?.trim() || observedPrBase;
7783
+ if (mergedBase && displayBranchName(mergedBase) === 'main') {
7784
+ try {
7785
+ const { syncMergedPrChecklist } = await import('../release-loop/merged-pr-checklist.js');
7786
+ const goalFile = opts.goalFile ?? (opts.goalId ? s.goalDocumentPathByGoalId?.(opts.goalId) : undefined);
7787
+ syncMergedPrChecklist(pr.number, wt.path, goalFile);
7788
+ } catch (error) {
7789
+ observe('merged-pr-checklist-sync-failed', { number: pr.number, error: safeErrorDescription(error) }, { level: 'warn' });
7790
+ }
7791
+ }
7525
7792
  try {
7526
7793
  await s.supersedeDraftsOnMerge?.(pr.number, wt.path);
7527
7794
  } catch (error) {
@@ -7540,7 +7807,6 @@ async function runSelfImplementInner(
7540
7807
  observe('release-note-write-failed', { number: pr.number, prUrl: pr.url, directory, fragment, error: safeErrorDescription(error) }, { level: 'error' });
7541
7808
  throw new MergedReleaseNoteWriteError(pr.url, directory, fragment, error);
7542
7809
  }
7543
- const mergedBase = m.baseRefName?.trim() || observedPrBase;
7544
7810
  const defaultBranch = resolveDefaultBranchTarget(s.defaultBranchRef ?? defaultBranchRef, wt.path).target ?? undefined;
7545
7811
  progress('merged', formatAutoMergeSuccessMessage({ prNumber: pr.number, mergedBase, defaultBranch }));
7546
7812
  await runPostMergeCleanup(s.postMergeCleanup, wt.path, wt.branch, opts.goalFile, mergedBase && defaultBranch && displayBranchName(mergedBase) === displayBranchName(defaultBranch) ? defaultBranch : undefined, observe);
@@ -7553,6 +7819,18 @@ async function runSelfImplementInner(
7553
7819
  prNumber: pr.number,
7554
7820
  openedAt: openedAtFromLedger(mergedLedger, new Date().toISOString()),
7555
7821
  }, observe);
7822
+ // FOLLOWUP-LOOP① — leftover findings become one follow-up draft. Failure is a log line only;
7823
+ // this return (stage, merged, exit via ok) stays the merge result.
7824
+ await draftFollowUpAfterMerge({
7825
+ prNumber: pr.number,
7826
+ // 정상 병합은 리뷰를 통과한 뒤라 미해결 must-fix 가 없다 — 남은 것은 should-fix ⊕ PR 본문 «남은 조각».
7827
+ // 미해결 must-fix 는 리뷰 예산 수락 경로가 직접 넘긴다(위 acceptance 분기).
7828
+ shouldFix: review?.shouldFix,
7829
+ prBody: [preparedReviewBudgetPrBody, preparedPrBody.body].filter(Boolean).join('\n'),
7830
+ feature: opts.feature,
7831
+ ...(opts.goalId ? { cellId: opts.goalId } : {}),
7832
+ ...(opts.goalFile ? { goalFile: opts.goalFile } : {}),
7833
+ }, s.followUp);
7556
7834
  return { ok: true, stage: 'merged', node: 'merge', ...resolveRunOutcome({ termination: 'completed' }), ...decisionSignalResult, ...preservedCompletionResult, ...(completionStatus ? { completionStatus } : {}), sessionId, worktreePath: wt.path, branch: wt.branch, gate, ...(review ? { review } : {}), merged: true, ...(mergedBase ? { mergedBase } : {}), prUrl: pr.url, prNumber: pr.number };
7557
7835
  }
7558
7836
  progress('pr-opened', `병합 실패 — PR 개설됨(#${pr.number}·수동 병합). ${m.detail ?? ''}`);