elanous 0.2.15 → 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 (254) 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-33ffcc49ab45123b.js → 1307-b46d45b705e8c9ae.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-3cb52e5ea8fc2fa5.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-5491c115b1f9a923.js → page-c1e20d7d338713f1.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-482c5dbde699947c.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-8c2d9526164f5d09.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-c53598cfce6156f2.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-13a63953f53c6a23.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-5d3c8429d04f3831.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-3193bdc0bbab047a.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-6107ca8ddd5e3bdd.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-dce686e582c23937.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/market/{page-955ec2da3be4c560.js → page-51f811fa119308b6.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-9f88fbc15d9058d1.js → page-dacff46049e1557e.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-4167348049166852.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-de52e8fdf9095f7a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-c9fc5706b8abaa11.js → page-36a7d994f4ca0299.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-ca3952cd78a83ca2.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-acb749f1e432bcf3.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-1ebfe049c640e171.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-85f1a1fc0a82738c.js → page-57924393e3e4e539.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-31fa1ce4c4aff9ae.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-0f5cd57ca928f42b.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-d28573d6792b4275.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d751de5f8062ff5e.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/term/{page-f5597e894c02cd33.js → page-fe1759f94003b32b.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/today/{page-0337fdf7fb5e37b6.js → page-d64c984e855c8c68.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/vault/{page-8fcec4c9ea2e7f70.js → page-9afdee66b820c76e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-d4aef67abf264874.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-3544f154c87b43ee.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
  51. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  52. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  53. package/apps/pwa/out/approvals/index.html +1 -1
  54. package/apps/pwa/out/approvals/index.txt +15 -15
  55. package/apps/pwa/out/autopilot/index.html +1 -1
  56. package/apps/pwa/out/autopilot/index.txt +15 -15
  57. package/apps/pwa/out/board/index.html +1 -1
  58. package/apps/pwa/out/board/index.txt +15 -15
  59. package/apps/pwa/out/botlab/index.html +1 -1
  60. package/apps/pwa/out/botlab/index.txt +15 -15
  61. package/apps/pwa/out/bots/index.html +1 -1
  62. package/apps/pwa/out/bots/index.txt +15 -15
  63. package/apps/pwa/out/ceo/index.html +1 -1
  64. package/apps/pwa/out/ceo/index.txt +15 -15
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +15 -15
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +15 -15
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +15 -15
  71. package/apps/pwa/out/design-check/index.html +1 -1
  72. package/apps/pwa/out/design-check/index.txt +15 -15
  73. package/apps/pwa/out/editor/index.html +1 -1
  74. package/apps/pwa/out/editor/index.txt +15 -15
  75. package/apps/pwa/out/exec/index.html +1 -1
  76. package/apps/pwa/out/exec/index.txt +15 -15
  77. package/apps/pwa/out/field/index.html +1 -1
  78. package/apps/pwa/out/field/index.txt +15 -15
  79. package/apps/pwa/out/healthz +1 -1
  80. package/apps/pwa/out/index.html +1 -1
  81. package/apps/pwa/out/index.txt +15 -15
  82. package/apps/pwa/out/inside/index.html +1 -1
  83. package/apps/pwa/out/inside/index.txt +15 -15
  84. package/apps/pwa/out/intake/index.html +1 -1
  85. package/apps/pwa/out/intake/index.txt +15 -15
  86. package/apps/pwa/out/live/index.html +1 -1
  87. package/apps/pwa/out/live/index.txt +15 -15
  88. package/apps/pwa/out/loops/index.html +1 -1
  89. package/apps/pwa/out/loops/index.txt +15 -15
  90. package/apps/pwa/out/market/index.html +1 -1
  91. package/apps/pwa/out/market/index.txt +15 -15
  92. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  93. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  94. package/apps/pwa/out/morning/index.html +1 -1
  95. package/apps/pwa/out/morning/index.txt +15 -15
  96. package/apps/pwa/out/observatory/index.html +1 -1
  97. package/apps/pwa/out/observatory/index.txt +15 -15
  98. package/apps/pwa/out/ops/checklist/index.html +1 -1
  99. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  100. package/apps/pwa/out/ops/release/index.html +1 -1
  101. package/apps/pwa/out/ops/release/index.txt +15 -15
  102. package/apps/pwa/out/ops/seats/index.html +1 -1
  103. package/apps/pwa/out/ops/seats/index.txt +15 -15
  104. package/apps/pwa/out/outputs/index.html +1 -1
  105. package/apps/pwa/out/outputs/index.txt +15 -15
  106. package/apps/pwa/out/reflection/index.html +1 -1
  107. package/apps/pwa/out/reflection/index.txt +16 -16
  108. package/apps/pwa/out/scheduler/index.html +1 -1
  109. package/apps/pwa/out/scheduler/index.txt +15 -15
  110. package/apps/pwa/out/sessions/index.html +1 -1
  111. package/apps/pwa/out/sessions/index.txt +15 -15
  112. package/apps/pwa/out/settings/devices/index.html +1 -1
  113. package/apps/pwa/out/settings/devices/index.txt +15 -15
  114. package/apps/pwa/out/settings/index.html +1 -1
  115. package/apps/pwa/out/settings/index.txt +15 -15
  116. package/apps/pwa/out/setup/done/index.html +1 -1
  117. package/apps/pwa/out/setup/done/index.txt +15 -15
  118. package/apps/pwa/out/setup/index.html +1 -1
  119. package/apps/pwa/out/setup/index.txt +15 -15
  120. package/apps/pwa/out/share/index.html +1 -1
  121. package/apps/pwa/out/share/index.txt +15 -15
  122. package/apps/pwa/out/showroom/index.html +1 -1
  123. package/apps/pwa/out/showroom/index.txt +15 -15
  124. package/apps/pwa/out/tasks/index.html +1 -1
  125. package/apps/pwa/out/tasks/index.txt +15 -15
  126. package/apps/pwa/out/term/index.html +1 -1
  127. package/apps/pwa/out/term/index.txt +15 -15
  128. package/apps/pwa/out/today/index.html +1 -1
  129. package/apps/pwa/out/today/index.txt +15 -15
  130. package/apps/pwa/out/trace/index.html +1 -1
  131. package/apps/pwa/out/trace/index.txt +15 -15
  132. package/apps/pwa/out/vault/index.html +1 -1
  133. package/apps/pwa/out/vault/index.txt +15 -15
  134. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  135. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  136. package/apps/pwa/out/workflows/index.html +1 -1
  137. package/apps/pwa/out/workflows/index.txt +15 -15
  138. package/apps/pwa/out/workspace/index.html +1 -1
  139. package/apps/pwa/out/workspace/index.txt +15 -15
  140. package/apps/pwa/out/worktrees/index.html +1 -1
  141. package/apps/pwa/out/worktrees/index.txt +15 -15
  142. package/graphs/doc-gen/doc-gen.yaml +24 -0
  143. package/graphs/doc-gen/recipes.yaml +3 -0
  144. package/graphs/orchestrator/orchestrator.yaml +7 -0
  145. package/graphs/orchestrator/recipes.yaml +3 -0
  146. package/graphs/steward/recipes.yaml +2 -1
  147. package/package.json +1 -1
  148. package/scripts/ci-public-export-import-gate.ts +154 -0
  149. package/scripts/ci-test-interference-gate.ts +5 -1
  150. package/scripts/ci-typecheck-changed.ts +34 -15
  151. package/scripts/docs/doc-rot.ts +164 -0
  152. package/scripts/docs/generate-graph-anatomy.ts +131 -9
  153. package/scripts/docs-cli-check.ts +4 -4
  154. package/scripts/goals/goal-census.ts +343 -0
  155. package/scripts/measure/author-section-contribution.ts +84 -0
  156. package/scripts/release-loop/cut-branch.ts +43 -13
  157. package/scripts/release-loop/gate-node.ts +3 -1
  158. package/scripts/release-loop/vault-note-node.ts +19 -2
  159. package/scripts/rhythm/monthly.ts +97 -0
  160. package/src/acp/client.ts +17 -2
  161. package/src/agent-mission/driver.ts +51 -1
  162. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  163. package/src/agent-mission/supervisor-metrics.ts +218 -0
  164. package/src/autopilot/track-agent-shadow.ts +2 -1
  165. package/src/briefing/brief-items.ts +191 -36
  166. package/src/budget/usage-outcomes.ts +204 -0
  167. package/src/cli/brief-cli.ts +73 -0
  168. package/src/cli/know-cli.ts +20 -0
  169. package/src/cli/logs-cli.ts +43 -1
  170. package/src/cli/pr-cli.ts +80 -12
  171. package/src/cli/release-cli.ts +11 -8
  172. package/src/cli/self-update.ts +200 -1
  173. package/src/cli/usage-cli.ts +26 -0
  174. package/src/discord-self-message.ts +9 -0
  175. package/src/flow-loop/split.ts +24 -0
  176. package/src/harness/harness-cli-command.ts +36 -2
  177. package/src/harness/harness-pod-dispatch.ts +25 -55
  178. package/src/harness/harness-seams.ts +13 -3
  179. package/src/harness/helper-repair.ts +139 -8
  180. package/src/harness/helper-scan.ts +13 -2
  181. package/src/harness/nested-elanous-policy.ts +114 -0
  182. package/src/hq/hq.ts +8 -2
  183. package/src/hq/lease.ts +5 -3
  184. package/src/hq/transfer.ts +185 -0
  185. package/src/index.ts +112 -6
  186. package/src/intake-plane/digest.ts +48 -26
  187. package/src/knowledge/know-find.ts +105 -0
  188. package/src/llm.ts +75 -19
  189. package/src/loops/orchestrator/finish-rate.ts +14 -3
  190. package/src/loops/orchestrator/tick.ts +96 -25
  191. package/src/maturity/bot-slash-catalog.ts +11 -2
  192. package/src/maturity/feature-maturity.ts +2 -2
  193. package/src/maturity/tui-slash-availability.ts +9 -0
  194. package/src/oauth/overload-failover-launch.ts +75 -0
  195. package/src/oauth/overload-failover.ts +137 -0
  196. package/src/release-loop/checklist.ts +10 -5
  197. package/src/release-loop/feature-store.ts +10 -9
  198. package/src/release-loop/placement.ts +17 -8
  199. package/src/seat-dispatch/seat-ask.ts +33 -6
  200. package/src/seat-loop/seat-loop.ts +90 -3
  201. package/src/self-dev/ask-launch-flow.ts +22 -1
  202. package/src/self-dev/authored-goal-trace.ts +64 -0
  203. package/src/self-dev/dev-cli.ts +19 -1
  204. package/src/self-dev/dev-pipeline.ts +6 -5
  205. package/src/self-dev/entrance-registry.ts +10 -6
  206. package/src/self-dev/old-door.ts +73 -0
  207. package/src/self-implement/app-api-budget.ts +166 -0
  208. package/src/self-implement/budget-gate.ts +7 -5
  209. package/src/self-implement/follow-up-goals.ts +289 -0
  210. package/src/self-implement/goal-author.ts +19 -13
  211. package/src/self-implement/heal-triage.ts +44 -0
  212. package/src/self-implement/orchestrator.ts +138 -7
  213. package/src/self-implement/run-ledger.ts +4 -0
  214. package/src/self-implement/seams.ts +44 -15
  215. package/src/session-runtime/index.ts +2 -0
  216. package/src/session-runtime/retry-api.ts +87 -0
  217. package/src/session-runtime/retry-policy.ts +107 -6
  218. package/src/steward/working-backwards.ts +16 -1
  219. package/src/task-cards/card-cli.ts +25 -0
  220. package/src/task-cards/card-store.ts +5 -3
  221. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  222. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  223. package/src/task-orchestrator/surfaces/pod-pool.ts +14 -5
  224. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  225. package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -10
  226. package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
  227. package/src/telegram-commands.ts +5 -0
  228. package/src/telegram.ts +47 -0
  229. package/src/user-config.ts +36 -3
  230. package/src/version/packed-revision.json +1 -1
  231. package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +0 -1
  232. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
  233. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
  234. package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
  235. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +0 -1
  236. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
  237. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
  238. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
  239. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
  240. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
  241. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
  242. package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
  243. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
  244. package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
  245. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
  246. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
  247. package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +0 -1
  248. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
  249. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
  250. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
  251. package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
  252. package/src/launch-head/seat-goal-queue.ts +0 -198
  253. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_buildManifest.js +0 -0
  254. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
@@ -156,6 +156,50 @@ export interface HealTriageResult {
156
156
  readonly evidence: readonly string[];
157
157
  }
158
158
 
159
+ /** 수리 한도를 다 쓴 런의 결함 셋. 힐 루프가 이 셋 중 하나만 고른다. */
160
+ export type ReworkCapDefectClass = 'test-defect' | 'goal-defect' | 'code-defect';
161
+
162
+ /** 분류 뒤 힐 루프가 고르는 다음 행동. */
163
+ export type ReworkCapNextAction = 'harvest' | 'relaunch' | 'card';
164
+
165
+ export interface ReworkCapHealInput {
166
+ /** 게이트가 이번 diff 가 들여온 실패 수. 없으면 0. */
167
+ readonly introduced?: readonly string[];
168
+ /** 마지막 리뷰 must-fix. 없으면 빈 목록. */
169
+ readonly mustFix?: readonly string[];
170
+ readonly summary?: string;
171
+ }
172
+
173
+ export interface ReworkCapHealDecision {
174
+ readonly defectClass: ReworkCapDefectClass;
175
+ readonly nextAction: ReworkCapNextAction;
176
+ readonly evidence: readonly string[];
177
+ }
178
+
179
+ const GOAL_DEFECT = /골|goal|acceptance|수용 기준|요구가 모순|스펙|경계:/i;
180
+
181
+ /**
182
+ * 수리 한도 소진 런을 «시험 결함 · 골 결함 · 코드 결함» 중 하나로 가르고
183
+ * 수확·재발사·카드 중 하나를 고른다. 근거는 게이트 introduced 목록과 마지막 리뷰 지적이다.
184
+ * 도입 실패가 있으면 코드 결함(재발사). 리뷰가 골·수용 기준을 짚으면 골 결함(카드).
185
+ * 그 밖(리뷰만 있거나 근거가 비면)은 시험 결함(수확)이다.
186
+ */
187
+ export function classifyReworkCapExhaustion(input: ReworkCapHealInput): ReworkCapHealDecision {
188
+ const introduced = (input.introduced ?? []).map((item) => item.trim()).filter(Boolean);
189
+ const mustFix = (input.mustFix ?? []).map((item) => item.trim()).filter(Boolean);
190
+ const evidence = [
191
+ ...introduced.map((item) => `gate introduced: ${item}`),
192
+ ...mustFix.map((item) => `review must-fix: ${item}`),
193
+ ];
194
+ if (introduced.length > 0) {
195
+ return { defectClass: 'code-defect', nextAction: 'relaunch', evidence };
196
+ }
197
+ if (mustFix.some((item) => GOAL_DEFECT.test(item)) || (input.summary ? GOAL_DEFECT.test(input.summary) : false)) {
198
+ return { defectClass: 'goal-defect', nextAction: 'card', evidence };
199
+ }
200
+ return { defectClass: 'test-defect', nextAction: 'harvest', evidence };
201
+ }
202
+
159
203
  const UNTESTABLE_EXT = new Set(['.md', '.json', '.yaml', '.yml', '.txt']);
160
204
  const ENVIRONMENT_CLAIM = /할당량|자격|기판|quota|credential|substrate/i;
161
205
 
@@ -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';
@@ -101,6 +103,26 @@ import { elanousStateRoot } from '../autopilot/state-paths.js';
101
103
  import { addNextMdReleaseNote, harnessReleaseNote, parseReleaseNoteSection, releaseNotesDir, renderReleaseNoteSection, writeReleaseNote, type ReleaseNoteFragment, type ReleaseNoteSection } from '../release-loop/release-note.js';
102
104
  import { landingFreezeMessage } from '../release-loop/landing-freeze.js';
103
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
+ }
104
126
  import { plannedSelfImplBranch } from '../harness/worktree-branch-prefix.js';
105
127
  export { slugifyFeature } from '../harness/worktree-branch-prefix.js';
106
128
  import { AUTO_REVIEW_LABEL, resolveAutoReviewLabels } from './context-capsule.js';
@@ -1056,7 +1078,7 @@ export interface SelfImplementSeams {
1056
1078
  /** PR 생성 뒤 런의 라운드 대화를 게시한다. 실패는 PR 개설을 되돌리지 않는다. */
1057
1079
  postPrComment?: (opts: { number: number; body: string; cwd: string }) => Promise<void>;
1058
1080
  addPrLabel?: (opts: { number: number; label: string; cwd: string }) => Promise<void>;
1059
- readPrFiles?: (opts: { number: number; cwd: string }) => Promise<string[]>;
1081
+ readPrFiles?: (opts: { number: number; cwd: string; base?: string }) => Promise<string[]>;
1060
1082
  /** PR의 base/head SHA와 관측된 PR base를 한 응답에서 고정한다. 미주입·실패·빈 SHA면 검사 대상을 증명할 수 없어 fail-closed로 PR을 열어 둔다. */
1061
1083
  readPrCommitShas?: (opts: { number: number; cwd: string }) => Promise<{ baseCommit: string; headCommit: string; baseRefName?: string }>;
1062
1084
  /** 고정한 base/head SHA 쌍의 unified diff. 가변 PR 조회를 다시 사용하면 ABA head 변경에 안전하지 않으므로 두 SHA는 필수 입력이다. */
@@ -1090,6 +1112,12 @@ export interface SelfImplementSeams {
1090
1112
  readRunLedger?: (runId: string) => RunLedgerEntry[] | null | Promise<RunLedgerEntry[] | null>;
1091
1113
  closeDraft: (input: { number: number; comment: string }) => void | Promise<void>;
1092
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;
1093
1121
  /** ⑥ HITL 게이트 — PR open 전 승인. ★fail-closed: 생략/false 면 PR 안 열림(자동 승인 없음). review 동봉
1094
1122
  * (2026-07-21) — HITL 이 리뷰 verdict 를 보고 판단, auto 모드는 verdict 로 자동 결정. */
1095
1123
  approvePr?: (summary: { branch: string; gateLog?: string; implSummary: string; review?: SelfImplementReview }) => Promise<boolean>;
@@ -3394,7 +3422,26 @@ function mergeDecisionEvidenceCoverage(evidenceCoverage: {
3394
3422
  : { evidenceCoverageMeasured: false };
3395
3423
  }
3396
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
+
3397
3443
  function runOriginData(env: NodeJS.ProcessEnv = process.env): RunOriginData {
3444
+ const installed = installedBuildForOrigin(env);
3398
3445
  return {
3399
3446
  hostId: resolveHostId(env),
3400
3447
  hostname: hostname(),
@@ -3403,6 +3450,8 @@ function runOriginData(env: NodeJS.ProcessEnv = process.env): RunOriginData {
3403
3450
  substrate: env.ELANOUS_SUBSTRATE || 'host',
3404
3451
  instance: resolveInstanceName(),
3405
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,
3406
3455
  ...(env.ELANOUS_POD_NAME ? { podName: env.ELANOUS_POD_NAME } : {}),
3407
3456
  ...(env.ELANOUS_NODE_NAME ? { nodeName: env.ELANOUS_NODE_NAME } : {}),
3408
3457
  ...(env.ELANOUS_POD_NAMESPACE ? { podNamespace: env.ELANOUS_POD_NAMESPACE } : {}),
@@ -3616,6 +3665,7 @@ export async function runSelfImplement(opts: SelfImplementOptions): Promise<Self
3616
3665
  },
3617
3666
  } satisfies LogSink);
3618
3667
  let traversalFinalized = false;
3668
+ reworkCapProceed = false;
3619
3669
  const progressDelivery = { delivered: 0, unwired: 0, callbackFailed: 0 };
3620
3670
  let capturedPrNumber: number | undefined;
3621
3671
  let openPrInvoked = false;
@@ -4179,10 +4229,31 @@ export async function runSelfImplement(opts: SelfImplementOptions): Promise<Self
4179
4229
  const terminalStage = terminalResult?.stage ?? 'crashed';
4180
4230
  try {
4181
4231
  const mappedStatus = mapStageToRunStatus(terminalStage)?.runStatus;
4182
- 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
+ }
4183
4252
  recordFailureEvent({
4184
- source: 'harness-run', kind: terminalStage, ref: runId,
4185
- 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`,
4186
4257
  at: new Date().toISOString(),
4187
4258
  });
4188
4259
  }
@@ -4544,6 +4615,14 @@ function observeRunRollup(
4544
4615
  }, effectiveOutcome.runStatus === 'failed' ? { level: 'warn' } : undefined);
4545
4616
  }
4546
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
+
4547
4626
  async function runSelfImplementInner(
4548
4627
  opts: SelfImplementOptions,
4549
4628
  identity?: { runId: string; source: ReturnType<typeof resolveRunIdentity>['source'] },
@@ -5030,6 +5109,8 @@ async function runSelfImplementInner(
5030
5109
  } | undefined;
5031
5110
  let gate!: SelfImplementGateResult;
5032
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;
5033
5114
  const reviewMustFixCountHistory: number[] = [];
5034
5115
  const reviewMustFixTrend = (): string => {
5035
5116
  if (reviewMustFixCountHistory.length < 2) return '아직 못 잰다';
@@ -5365,6 +5446,23 @@ async function runSelfImplementInner(
5365
5446
  observe('pr.comment.seam-missing', { number: pr.number, buffered: prCommentBuffer.length }, { level: 'warn' });
5366
5447
  return;
5367
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
+ }
5368
5466
  for (const body of prCommentBuffer) {
5369
5467
  try {
5370
5468
  await s.postPrComment({ number: pr.number, body, cwd: wt.path });
@@ -5798,6 +5896,7 @@ async function runSelfImplementInner(
5798
5896
  'self-implement-review-budget-pr',
5799
5897
  followUpMustFix,
5800
5898
  );
5899
+ preparedReviewBudgetPrBody = preparedPrBody.body;
5801
5900
  const pr = await withStepTimeout(s.openPr({
5802
5901
  title: prTitle(opts.feature),
5803
5902
  body: preparedPrBody.body,
@@ -6211,6 +6310,7 @@ async function runSelfImplementInner(
6211
6310
  if (budgetDecision.stop) {
6212
6311
  if (budgetDecision.exit === 'proceed') {
6213
6312
  finalizeSupervisorDeliveries('supervisor-rework-proceed');
6313
+ if (round > effectiveMax) reworkCapProceed = true;
6214
6314
  break;
6215
6315
  }
6216
6316
  finalizeSupervisorDeliveries('supervisor-rework-blocked');
@@ -6271,12 +6371,25 @@ async function runSelfImplementInner(
6271
6371
  if (isReviewRework(reworkKind)) {
6272
6372
  if (reflectRejectedAudit.length && review) review.shouldFix = [...review.shouldFix, ...reflectRejectedAudit];
6273
6373
  const reason = `review must-fix unresolved after ${round - 1} rework round(s)`;
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).
6274
6376
  if (!podReviewBlockedDraft) {
6275
6377
  const acceptance = await acceptReviewBudget(
6276
6378
  review?.mustFix ?? [],
6277
6379
  reworkParts.some((part) => part.source === 'supervisor'),
6278
6380
  );
6279
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
+ }
6280
6393
  return {
6281
6394
  ok: true,
6282
6395
  stage: 'pr-opened',
@@ -7523,11 +7636,15 @@ async function runSelfImplementInner(
7523
7636
  let releasePathInspectionError: string | undefined;
7524
7637
  if (s.readPrFiles) {
7525
7638
  try {
7526
- 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 } : {}) });
7527
7641
  if (!Array.isArray(files) || !files.every((path) => typeof path === 'string')) throw new Error('invalid PR file list');
7528
7642
  releaseHoldPath = releasePathHold(files);
7529
7643
  } catch (error) {
7530
- 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);
7531
7648
  }
7532
7649
  } else if (canAuto || labels?.includes(AUTO_REVIEW_LABEL)) {
7533
7650
  releasePathInspectionError = 'readPrFiles seam unavailable';
@@ -7566,7 +7683,9 @@ async function runSelfImplementInner(
7566
7683
  if (releasePathInspectionError && (canAuto || labels?.includes(AUTO_REVIEW_LABEL))) {
7567
7684
  observe('release-path-inspection-failed', { number: pr.number, error: releasePathInspectionError }, { level: 'error' });
7568
7685
  canAuto = false;
7569
- 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';
7570
7689
  recordMergeDecision();
7571
7690
  progress('pr-opened', `OP 승인 대기 (#${pr.number}): PR 경로 조회 실패`);
7572
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 };
@@ -7700,6 +7819,18 @@ async function runSelfImplementInner(
7700
7819
  prNumber: pr.number,
7701
7820
  openedAt: openedAtFromLedger(mergedLedger, new Date().toISOString()),
7702
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);
7703
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 };
7704
7835
  }
7705
7836
  progress('pr-opened', `병합 실패 — PR 개설됨(#${pr.number}·수동 병합). ${m.detail ?? ''}`);
@@ -94,6 +94,10 @@ export interface RunOriginData extends Record<string, unknown> {
94
94
  substrate: string;
95
95
  instance: string;
96
96
  elanousVersion: string;
97
+ /** install.json version. 못 읽으면 «unknown». */
98
+ installedVersion: string;
99
+ /** install.json commit. 못 읽으면 «unknown». */
100
+ installedCommit: string;
97
101
  podName?: string;
98
102
  nodeName?: string;
99
103
  podNamespace?: string;
@@ -24,6 +24,9 @@ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
24
24
  import { promisify } from 'node:util';
25
25
  import { forkSessionById, HARNESS_SESSION_ORIGIN, isHarnessSessionOrigin } from '../session/index.js';
26
26
  import { configuredWorktreeRoot, getUserConfig, resolveRoleLlm } from '../user-config.js';
27
+ import { elanousStateRoot } from '../autopilot/state-paths.js';
28
+ import { addHarnessQueue } from '../harness/harness-queue.js';
29
+ import { readFollowUpMode } from './follow-up-goals.js';
27
30
  import { getProviderForConfig } from '../llm.js';
28
31
  import { createWorktree, DEFAULT_BRANCH_WORKTREE_BASE, linkWorktreeDependencies, removeWorktree, resolveDefaultBranchBase, resolveMainRepoRoot } from '../git-fs/worktree.js';
29
32
  import { isWorktreeInUse, listActiveTerminalDirectories } from '../harness/harness-clean.js';
@@ -77,7 +80,8 @@ import { tscEnv, assessTypecheckExecution, classifyTypecheckErrors, diffTypechec
77
80
  import { nestCapReached, childNestEnv, nestInfo } from '../agent/nest-depth.js';
78
81
  import { harnessBoundaryEnv, harnessBoundaryRequestsEnv, harnessBoundaryResponsesEnv, harnessSpaceEnv, getHarnessSpace, normalizeSpaceId, getHarnessRunId, resolveRunIdentity } from '../harness/harness-space.js';
79
82
  import { debug } from '../debug/log.js';
80
- import { RELEASE_PATH_HOLD_MARKER, RELEASE_PATH_LABEL, releasePathHoldShouldPost, releasePathHoldCommentsArgs, releasePrFilePaths } from '../self-dev/release-path-guard.js';
83
+ import { RELEASE_PATH_HOLD_MARKER, RELEASE_PATH_LABEL, releasePathHoldShouldPost, releasePathHoldCommentsArgs } from '../self-dev/release-path-guard.js';
84
+ import { readPrFilesWithRateLimitFallback, runGhApp } from './app-api-budget.js';
81
85
  import { emitDecision } from '../live/detail-switch.js';
82
86
  import { gateDecision, reviewDecision, mergeDecision } from './decision-events.js';
83
87
  import { LogStore } from '../mss/logging/log-store.js';
@@ -110,6 +114,10 @@ declare module './orchestrator.js' {
110
114
  /** PR close(+브랜치 삭제). 기본 구성은 PrManager.closePr 위임(같은 인자·같은 반환).
111
115
  * 오케스트레이터는 아직 부르지 않는다 — 그 배선은 조각 ②다. */
112
116
  closePr?: (prUrl: string, comment?: string) => boolean;
117
+ /** Find an issue comment on this PR by its body marker; absence is distinct from a failed lookup. */
118
+ findPrComment?: (opts: { number: number; marker: string; cwd: string; comments?: unknown }) => Promise<{ id: number; body: string } | undefined>;
119
+ /** Replace an existing PR issue comment by ID, using the same GitHub App identity as postPrComment. */
120
+ editPrComment?: (opts: { id: number; body: string; cwd: string }) => Promise<void>;
113
121
  }
114
122
  }
115
123
  import { harnessPolicyEnv } from './harness-policy.js';
@@ -1415,6 +1423,11 @@ export function defaultSeams(o: DefaultSeamsOptions = {}): SelfImplementSeams {
1415
1423
  const { supersedeMergedGoalDrafts } = await import('../harness/harness-cli-command.js');
1416
1424
  await supersedeMergedGoalDrafts(number, cwd);
1417
1425
  },
1426
+ followUp: {
1427
+ mode: readFollowUpMode(),
1428
+ stateRoot: elanousStateRoot(),
1429
+ enqueue: (item) => addHarnessQueue({ seat: item.seat, say: item.say, idempotencyKey: item.idempotencyKey }),
1430
+ },
1418
1431
  lineageSupersede: {
1419
1432
  listOpenDrafts: () => listOpenDraftsForLineage(),
1420
1433
  readRunLedger: (runId) => {
@@ -2394,46 +2407,62 @@ export function defaultSeams(o: DefaultSeamsOptions = {}): SelfImplementSeams {
2394
2407
  postPrComment: async ({ number, body, cwd }) => {
2395
2408
  // Round comments go out as the GitHub App like the PR itself — a personal `gh` login made the owner a
2396
2409
  // participant and mailed them on merge (#22089).
2397
- if (body.startsWith(RELEASE_PATH_HOLD_MARKER)) {
2410
+ if (body.startsWith(RELEASE_PATH_HOLD_MARKER)) {
2398
2411
  // Several merge surfaces may hold the same PR; post the release-path hold comment once.
2399
2412
  let history: unknown;
2400
2413
  try {
2401
- history = JSON.parse((await execFileAsync('gh', releasePathHoldCommentsArgs(number), {
2414
+ history = JSON.parse((await runGhApp('post-pr-comment-history', releasePathHoldCommentsArgs(number), {
2402
2415
  cwd, encoding: 'utf8', timeout: GIT_TIMEOUT, maxBuffer: 64 * 1024 * 1024, env: ghAutomationEnv(process.env),
2403
2416
  })).stdout);
2404
2417
  } catch { history = undefined; }
2405
2418
  if (!releasePathHoldShouldPost(() => history)) return;
2406
2419
  }
2407
- await execFileAsync('gh', ['pr', 'comment', String(number), '--body', body], {
2420
+ await runGhApp('post-pr-comment', ['pr', 'comment', String(number), '--body', body], {
2421
+ cwd, encoding: 'utf8', timeout: GIT_TIMEOUT, maxBuffer: 64 * 1024 * 1024, env: ghAutomationEnv(process.env),
2422
+ });
2423
+ },
2424
+
2425
+ findPrComment: async ({ number, marker, cwd, comments }) => {
2426
+ const pages: unknown = comments ?? JSON.parse((await runGhApp('find-pr-comment', releasePathHoldCommentsArgs(number), {
2427
+ cwd, encoding: 'utf8', timeout: GIT_TIMEOUT, maxBuffer: 64 * 1024 * 1024, env: ghAutomationEnv(process.env),
2428
+ })).stdout);
2429
+ if (!Array.isArray(pages)) throw new Error('gh api PR comments returned an invalid list');
2430
+ const entries = pages.flatMap((page: unknown) => Array.isArray(page) ? page : [page]);
2431
+ for (const entry of entries) {
2432
+ if (!entry || typeof entry !== 'object' || !('body' in entry) || typeof entry.body !== 'string'
2433
+ || !('id' in entry) || typeof entry.id !== 'number' || !Number.isSafeInteger(entry.id) || entry.id <= 0) {
2434
+ throw new Error('gh api PR comments returned an invalid comment');
2435
+ }
2436
+ }
2437
+ return entries.find((entry: { id: number; body: string }) => entry.body.includes(marker));
2438
+ },
2439
+
2440
+ editPrComment: async ({ id, body, cwd }) => {
2441
+ await runGhApp('edit-pr-comment', ['api', '--method', 'PATCH', `repos/{owner}/{repo}/issues/comments/${id}`, '-f', `body=${body}`], {
2408
2442
  cwd, encoding: 'utf8', timeout: GIT_TIMEOUT, maxBuffer: 64 * 1024 * 1024, env: ghAutomationEnv(process.env),
2409
2443
  });
2410
2444
  },
2411
2445
 
2412
2446
  addPrLabel: async ({ number, label, cwd }) => {
2413
2447
  if (label === RELEASE_PATH_LABEL) {
2414
- const existing = await execFileAsync('gh', ['label', 'list', '--search', label, '--json', 'name'], {
2448
+ const existing = await runGhApp('add-pr-label-list', ['label', 'list', '--search', label, '--json', 'name'], {
2415
2449
  cwd, encoding: 'utf8', timeout: GIT_TIMEOUT, env: ghAutomationEnv(process.env),
2416
2450
  });
2417
2451
  const names: unknown = JSON.parse(existing.stdout);
2418
2452
  if (!Array.isArray(names)) throw new Error('gh label list returned an invalid list');
2419
2453
  if (!names.some((item: unknown) => !!item && typeof item === 'object' && 'name' in item && item.name === label)) {
2420
- await execFileAsync('gh', ['label', 'create', label, '--color', 'D93F0B', '--description', 'Release path requires OP approval'], {
2454
+ await runGhApp('add-pr-label-create', ['label', 'create', label, '--color', 'D93F0B', '--description', 'Release path requires OP approval'], {
2421
2455
  cwd, encoding: 'utf8', timeout: GIT_TIMEOUT, env: ghAutomationEnv(process.env),
2422
2456
  });
2423
2457
  }
2424
2458
  }
2425
- await execFileAsync('gh', ['pr', 'edit', String(number), '--add-label', label], {
2459
+ await runGhApp('add-pr-label', ['pr', 'edit', String(number), '--add-label', label], {
2426
2460
  cwd, encoding: 'utf8', timeout: GIT_TIMEOUT, env: ghAutomationEnv(process.env),
2427
2461
  });
2428
2462
  },
2429
- readPrFiles: async ({ number, cwd }) => {
2430
- // Paginate the PR file API: a single `gh pr view --json files` can omit files after the first page.
2431
- const { stdout } = await execFileAsync('gh', [
2432
- 'api', '--paginate', '--slurp', '--method', 'GET', '-f', 'per_page=100',
2433
- `repos/{owner}/{repo}/pulls/${number}/files`,
2434
- ], { cwd, encoding: 'utf8', timeout: GIT_TIMEOUT, maxBuffer: 64 * 1024 * 1024, env: ghAutomationEnv(process.env) });
2435
- return releasePrFilePaths(JSON.parse(stdout));
2436
- },
2463
+ readPrFiles: async ({ number, cwd, base }) => readPrFilesWithRateLimitFallback({
2464
+ number, cwd, env: ghAutomationEnv(process.env), ...(base ? { base } : {}),
2465
+ }),
2437
2466
 
2438
2467
  ...(o.approvePr ? { approvePr: o.approvePr } : {}),
2439
2468
 
@@ -66,6 +66,8 @@ export {
66
66
  export {
67
67
  fetchApiWithRetry,
68
68
  ApiHttpError,
69
+ OverloadFailoverError,
70
+ recordLlmCallOutcome,
69
71
  type FetchApiWithRetryOpts,
70
72
  } from './retry-api.js';
71
73
 
@@ -25,6 +25,8 @@ import {
25
25
  decideRetry,
26
26
  DoomLoopTracker,
27
27
  fingerprintError,
28
+ llmCallOutcomeKind,
29
+ type LlmCallOutcomeKind,
28
30
  type RetryAction,
29
31
  } from './retry-policy.js';
30
32
 
@@ -75,6 +77,35 @@ export interface FetchApiWithRetryOpts {
75
77
  * row" pattern within a single request. Pass a session-scoped
76
78
  * tracker if you want cross-request doom detection. */
77
79
  doomTracker?: DoomLoopTracker;
80
+ /** Fallbacks still unused. When > 0, k consecutive overload-class
81
+ * failures throw OverloadFailoverError before another retry sleep. */
82
+ remainingFallbacks?: number;
83
+ }
84
+
85
+ /** Raised when overload repeats k times and a fallback is still unused.
86
+ * The caller switches provider; this is not a retry exhaustion. */
87
+ export class OverloadFailoverError extends Error {
88
+ readonly provider: string;
89
+ readonly status: number;
90
+ readonly kind: LlmCallOutcomeKind;
91
+ readonly streak: number;
92
+ readonly cause: unknown;
93
+
94
+ constructor(opts: {
95
+ provider: string;
96
+ status: number;
97
+ kind: LlmCallOutcomeKind;
98
+ streak: number;
99
+ cause: unknown;
100
+ }) {
101
+ super(`overload failover after ${opts.streak} ${opts.kind} from ${opts.provider} (status ${opts.status})`);
102
+ this.name = 'OverloadFailoverError';
103
+ this.provider = opts.provider;
104
+ this.status = opts.status;
105
+ this.kind = opts.kind;
106
+ this.streak = opts.streak;
107
+ this.cause = opts.cause;
108
+ }
78
109
  }
79
110
 
80
111
  /** POST a fetch with the standard retry policy. On 2xx returns the
@@ -91,13 +122,17 @@ export async function fetchApiWithRetry(
91
122
  ): Promise<Response> {
92
123
  const maxAttempts = opts.maxAttempts ?? 4;
93
124
  const tracker = opts.doomTracker ?? new DoomLoopTracker();
125
+ const remainingFallbacks = opts.remainingFallbacks ?? 0;
94
126
  let attempt = 0;
127
+ let overloadStreak = 0;
95
128
  let lastErr: unknown;
96
129
  while (attempt < maxAttempts) {
97
130
  let response: Response | null = null;
98
131
  try {
99
132
  response = await fetch(url, init);
100
133
  if (response.ok) {
134
+ recordLlmCallOutcome(opts.provider, response.status, 'ok');
135
+ overloadStreak = 0;
101
136
  return response;
102
137
  }
103
138
  // Drain body so the error can carry the API's explanation. We
@@ -136,6 +171,11 @@ export async function fetchApiWithRetry(
136
171
  // would couple the helper to the caller's signal.
137
172
  if (isAbortLike(err)) throw err;
138
173
 
174
+ const status = err instanceof ApiHttpError ? err.status : 0;
175
+ const kind = llmCallOutcomeKind(status, err instanceof Error ? err.message : String(err));
176
+ recordLlmCallOutcome(opts.provider, status, kind);
177
+ if (kind === 'overloaded' || kind === '5xx' || kind === 'rate-limit') overloadStreak += 1;
178
+ else overloadStreak = 0;
139
179
  const fp = fingerprintError(err, `api:${opts.provider}`);
140
180
  const doomStatus = tracker.record(fp);
141
181
  const retryAfter = err instanceof ApiHttpError ? err.retryAfter : undefined;
@@ -143,6 +183,8 @@ export async function fetchApiWithRetry(
143
183
  attempt,
144
184
  doomStatus,
145
185
  retryAfter,
186
+ remainingFallbacks,
187
+ overloadStreak,
146
188
  });
147
189
  debug.log('llm.retry', decisionEvent(decision.action), {
148
190
  provider: opts.provider,
@@ -151,7 +193,19 @@ export async function fetchApiWithRetry(
151
193
  reason: decision.reason,
152
194
  delayMs: decision.delayMs,
153
195
  status: err instanceof ApiHttpError ? err.status : undefined,
196
+ kind,
197
+ overloadStreak,
198
+ remainingFallbacks,
154
199
  });
200
+ if (decision.action === 'switch-provider') {
201
+ throw new OverloadFailoverError({
202
+ provider: opts.provider,
203
+ status,
204
+ kind,
205
+ streak: overloadStreak,
206
+ cause: err,
207
+ });
208
+ }
155
209
  if (decision.action !== 'retry') {
156
210
  throw err;
157
211
  }
@@ -172,6 +226,39 @@ export async function fetchApiWithRetry(
172
226
  throw lastErr ?? new Error(`${opts.errorPrefix}: retry loop terminated unexpectedly`);
173
227
  }
174
228
 
229
+ type OutcomeWriter = (row: { provider: string; status: number; kind: LlmCallOutcomeKind }) => void;
230
+
231
+ let outcomeWriterForTesting: OutcomeWriter | undefined;
232
+
233
+ /** Test seam. Production uses the log store. `undefined` restores it. */
234
+ export function setLlmCallOutcomeWriterForTesting(writer: OutcomeWriter | undefined): void {
235
+ outcomeWriterForTesting = writer;
236
+ }
237
+
238
+ /** The one event launch failover is allowed to read.
239
+ * Category `llm.call`, event `outcome`, payload {provider,status,kind}.
240
+ * Written straight to the log store — `debug.log` drops the row when no
241
+ * sink is armed, which is the normal launch path. */
242
+ export function recordLlmCallOutcome(provider: string, status: number, kind: LlmCallOutcomeKind): void {
243
+ const data = { provider, status, kind };
244
+ // Trace only — a store sink would otherwise write a second `llm.call`/`outcome` row and halve the streak threshold.
245
+ debug.log('llm.call', 'outcome-trace', data);
246
+ if (outcomeWriterForTesting) {
247
+ outcomeWriterForTesting(data);
248
+ return;
249
+ }
250
+ if (process.env.NODE_ENV === 'test') return;
251
+ try {
252
+ const { getDefaultLogStore } = require('../mss/logging/log-store.js') as typeof import('../mss/logging/log-store.js');
253
+ getDefaultLogStore()?.insertBatch([{
254
+ rec: { ts: new Date().toISOString(), category: 'llm.call', event: 'outcome', data },
255
+ surface: 'llm',
256
+ }]);
257
+ } catch {
258
+ // A failed observation must not fail the call. Launch then sees no sample.
259
+ }
260
+ }
261
+
175
262
  function decisionEvent(action: RetryAction): string {
176
263
  // Stable category names for log filtering. The action is the most
177
264
  // useful axis ("how many turns hit retry vs abort"); the category