elanous 0.2.34 → 0.2.36

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 (802) 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/{abb4b98b.163e0f56e84ae35d.js → 053a429e.9a63436527cc751c.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/109.f196abf3484bb97f.js +287 -0
  5. package/apps/pwa/out/_next/static/chunks/1201.afbfc5b1c5e5cbe4.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1517.9275b89572098ff3.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1574.ad11f8db8396ab45.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/1614.7a8c5c40aee0261f.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/1871-f1eb29d8dbb4065b.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/190.285c832bd9626f8c.js +64 -0
  11. package/apps/pwa/out/_next/static/chunks/2193.0ea24c4a7aa1d004.js +36 -0
  12. package/apps/pwa/out/_next/static/chunks/223-ad865c1e80491d78.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/2265-f0d90b6dbec05f81.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/2267.eb4a62a209f09ec4.js +149 -0
  15. package/apps/pwa/out/_next/static/chunks/2341-86cd06c0cf01aa33.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/2352.9abc42422bade876.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/2397-c1653a816061842d.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/2500.cbc1257b774aae52.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/{6086.913a69e6fa3e4a8b.js → 2683.c9773a0c589b031b.js} +4 -4
  20. package/apps/pwa/out/_next/static/chunks/2709.8e1889034255fc5f.js +24 -0
  21. package/apps/pwa/out/_next/static/chunks/{5576-82d3703636d5c4b9.js → 275-02ea4c2a33980422.js} +3 -3
  22. package/apps/pwa/out/_next/static/chunks/{ffdc3a04.b9c23c1b28dd4a96.js → 2812ca6a.3b9f4a7a08885744.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/284.ac77c0db55bfd3a8.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/2941.3c3546f96bc6efa7.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/2976.b000698a3c6512dc.js +24 -0
  26. package/apps/pwa/out/_next/static/chunks/{9550.d5f76828f972058c.js → 2986.45d7652e4f13557e.js} +2 -2
  27. package/apps/pwa/out/_next/static/chunks/3031-85328a92fcce4da6.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/{8817.61bb7ffbba502ecb.js → 3136.30aaaa5fa03662a3.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/{9131-fce7256c4180c4a5.js → 3148-98efd4869d660a87.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/{3498.cf2a15a8a9e5b926.js → 3155.2587a55196ecb394.js} +3 -3
  31. package/apps/pwa/out/_next/static/chunks/3277.785db1cf09f98601.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/3320-f9c77381f85a2c0a.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/{3103.84470c8445962f04.js → 3324.a09703033abcbd2f.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/3386-91049feabff9a94e.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/339.80afb7e8839372af.js +8 -0
  36. package/apps/pwa/out/_next/static/chunks/3410.7f9af5ab77cbe65f.js +115 -0
  37. package/apps/pwa/out/_next/static/chunks/{3819.163d4777f0a597f6.js → 3538.a445e50bb359d068.js} +2 -2
  38. package/apps/pwa/out/_next/static/chunks/3540c645.2595a6af01a97672.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/3621.581b2054cc14bee4.js +286 -0
  40. package/apps/pwa/out/_next/static/chunks/3676.8c28d16fd84ca843.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/{3414.172ad4ca2d9b08e8.js → 3727.7949ccaacaa71242.js} +2 -2
  42. package/apps/pwa/out/_next/static/chunks/375-b84540853523fe52.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/3757-180308bc61021b05.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/379.ee262e37586fe1ae.js +74 -0
  45. package/apps/pwa/out/_next/static/chunks/3840-5825d56d46976a92.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/3969-81054381eaec4c4c.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/{9b48c871.204d5d13e7809bd0.js → 3a3d9966.cc4d4585a2708e7c.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/4067-bf9aa60547972937.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/4116.a84a92a2492c080e.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/4176.e5729c93c29151eb.js +104 -0
  51. package/apps/pwa/out/_next/static/chunks/4227-e6c84686bc8946ca.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/{6740.43de73fc427c9fe2.js → 4286.b70c4520306817e8.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/4304.e9b2512834a9e23e.js +23 -0
  54. package/apps/pwa/out/_next/static/chunks/4327.b9e08a71c0009348.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/{8548.036c344973968847.js → 4615.8525eb0add35eba6.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/4946.bd106eafd668f248.js +148 -0
  57. package/apps/pwa/out/_next/static/chunks/{3411-f87314c6c66ebc73.js → 5085-bce4904931767ca6.js} +1 -1
  58. package/apps/pwa/out/_next/static/chunks/5125.2eb9968b83767193.js +93 -0
  59. package/apps/pwa/out/_next/static/chunks/5128-75b733fd74a9a1eb.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/{3110.0a7065f25c841818.js → 528.38ce20ef8aa251ff.js} +16 -16
  61. package/apps/pwa/out/_next/static/chunks/5489f3fe.4bb1458b09f6dead.js +122 -0
  62. package/apps/pwa/out/_next/static/chunks/{1644.35dd25240ae24e4b.js → 552.b7d061ad91eeac51.js} +2 -2
  63. package/apps/pwa/out/_next/static/chunks/5560-2a4648f6a30f16e7.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/{3837.722e053fb9f321ad.js → 5711.957b095705c12560.js} +1 -1
  65. package/apps/pwa/out/_next/static/chunks/{2124.ed5bf480cea7f85b.js → 575.d929793a53f27c4b.js} +1 -1
  66. package/apps/pwa/out/_next/static/chunks/5772-e29e14f8ff127122.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/5887.5b2f5b44ce0f7e9a.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/5899-4c112a2d908cf97f.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/{7562.948255cbc3e79d33.js → 599.c39c571b6733f2a2.js} +2 -2
  70. package/apps/pwa/out/_next/static/chunks/6041.1b65efb796ebc2a5.js +79 -0
  71. package/apps/pwa/out/_next/static/chunks/6057-765c0a26028c86b2.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/6142.f289e0d039f453a6.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/6199-6bcd61d2a3910e04.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/6236.cdf7293e710a9b2d.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/6387-237e079dac9a3ae6.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/6448-b6d272d074276e14.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/6466-92e65a48490b68a9.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/6566.926a1d1b7c911442.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/6580-cdc0e4a634ce4708.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/6659-b242dc31b20b3f29.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/6690.c5d9b321b1f79384.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/671-e9b6ad0c771c5514.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/6862.bcbf89dd0f4496b3.js +27 -0
  84. package/apps/pwa/out/_next/static/chunks/6912-7b375ee48732c767.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/{5446.edf141cff9ef647d.js → 6925.562d532110879886.js} +1 -1
  86. package/apps/pwa/out/_next/static/chunks/{7005.1623d2aea78eb85f.js → 6943.c6b24124853a47b2.js} +1 -1
  87. package/apps/pwa/out/_next/static/chunks/727.7deb87749b0a343a.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/{8368.014caa8de2f5af34.js → 7481.39f7a4654907bf6e.js} +1 -1
  89. package/apps/pwa/out/_next/static/chunks/7545.b1aa42cd15503319.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/7551-416d298c2dc4a9ab.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/{2554.3b5e200f5f5cfcd6.js → 771.c3cdb6c1f7f47c02.js} +16 -16
  92. package/apps/pwa/out/_next/static/chunks/7784.1f0ac4a16d190cf4.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/7870-309de463786ac1fc.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/7888-e29b151bb6a3bb8b.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/{3121.779fafe3e44612c5.js → 7891.03f910124a737985.js} +1 -1
  96. package/apps/pwa/out/_next/static/chunks/7905-6ba78d9234608f79.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/8037.e254fd797aa224d1.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/8207.f527886b7c464529.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/856.50bda61439d465e6.js +62 -0
  100. package/apps/pwa/out/_next/static/chunks/{9a8cb466.cc9fc9e6e46038aa.js → 873d6c12.09bd499f57171543.js} +1 -1
  101. package/apps/pwa/out/_next/static/chunks/{7431.3051e9b8a4372d2d.js → 8904.7d51af2dc033965c.js} +2 -2
  102. package/apps/pwa/out/_next/static/chunks/9031-92e9b487edb681ee.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/9060.5410d9f6a95f6287.js +90 -0
  104. package/apps/pwa/out/_next/static/chunks/914.8abefd48d579547b.js +94 -0
  105. package/apps/pwa/out/_next/static/chunks/9187-c87a2207d4ac5d46.js +1 -0
  106. package/apps/pwa/out/_next/static/chunks/{1895-60ed0e341c658bd4.js → 9250-072df22eaadaa6b5.js} +1 -1
  107. package/apps/pwa/out/_next/static/chunks/9289.3e92f278821e18e0.js +1 -0
  108. package/apps/pwa/out/_next/static/chunks/9319.7816d3cc741cd34f.js +1 -0
  109. package/apps/pwa/out/_next/static/chunks/9327-e016bb9f1674d24f.js +1 -0
  110. package/apps/pwa/out/_next/static/chunks/9365.3de48bf75193ff9a.js +77 -0
  111. package/apps/pwa/out/_next/static/chunks/9383.5b5e9aa56b5fea97.js +1 -0
  112. package/apps/pwa/out/_next/static/chunks/9387-cb929573ef436f39.js +1 -0
  113. package/apps/pwa/out/_next/static/chunks/9489.c27becacc3283dec.js +1 -0
  114. package/apps/pwa/out/_next/static/chunks/{2547.dd27303624cb4da8.js → 9514.131c52cc75fe6439.js} +1 -1
  115. package/apps/pwa/out/_next/static/chunks/9536.28962663c7734376.js +1 -0
  116. package/apps/pwa/out/_next/static/chunks/9686-12286f5bbf15a5a0.js +1 -0
  117. package/apps/pwa/out/_next/static/chunks/9776.289d40cc5a4c342a.js +1 -0
  118. package/apps/pwa/out/_next/static/chunks/9789.60f723d18097ec30.js +1 -0
  119. package/apps/pwa/out/_next/static/chunks/989.f0c17523c5e61f65.js +1 -0
  120. package/apps/pwa/out/_next/static/chunks/9972-1df393aa8d4f7d52.js +1 -0
  121. package/apps/pwa/out/_next/static/chunks/9f31e478.fefcbcd3c388ebc5.js +1 -0
  122. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-96750aba7430959f.js +1 -0
  123. package/apps/pwa/out/_next/static/chunks/app/approvals/page-4105e5bbbb043024.js +1 -0
  124. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5d4fa75c2acb113e.js +1 -0
  125. package/apps/pwa/out/_next/static/chunks/app/board/page-a79a9bdb3b2142bf.js +1 -0
  126. package/apps/pwa/out/_next/static/chunks/app/botlab/page-f1c2047484992a35.js +1 -0
  127. package/apps/pwa/out/_next/static/chunks/app/bots/page-15ffc49b2d8c70e2.js +1 -0
  128. package/apps/pwa/out/_next/static/chunks/app/ceo/page-67d53a3fc8734135.js +1 -0
  129. package/apps/pwa/out/_next/static/chunks/app/chat/page-e30c8dc696491336.js +1 -0
  130. package/apps/pwa/out/_next/static/chunks/app/consult/page-2136e04fa77696e2.js +1 -0
  131. package/apps/pwa/out/_next/static/chunks/app/control/page-fecd2814790c2e12.js +1 -0
  132. package/apps/pwa/out/_next/static/chunks/app/decisions/page-280aa4a98fc5bb44.js +1 -0
  133. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c14e96b65fb4b883.js +1 -0
  134. package/apps/pwa/out/_next/static/chunks/app/editor/page-187dac74825ed6bf.js +1 -0
  135. package/apps/pwa/out/_next/static/chunks/app/exec/page-6bc7246ea8c92448.js +1 -0
  136. package/apps/pwa/out/_next/static/chunks/app/field/{page-4fb31eb476dcbad3.js → page-083747f1d4518f61.js} +1 -1
  137. package/apps/pwa/out/_next/static/chunks/app/healthz/route-96750aba7430959f.js +1 -0
  138. package/apps/pwa/out/_next/static/chunks/app/inside/page-15ca95a16ec96e1f.js +1 -0
  139. package/apps/pwa/out/_next/static/chunks/app/intake/page-a8ac031b3810eb64.js +1 -0
  140. package/apps/pwa/out/_next/static/chunks/app/layout-8835f93110494cda.js +1 -0
  141. package/apps/pwa/out/_next/static/chunks/app/live/page-e2de2e59111c9aad.js +1 -0
  142. package/apps/pwa/out/_next/static/chunks/app/live-run/page-f2b2e4bf6e981450.js +1 -0
  143. package/apps/pwa/out/_next/static/chunks/app/loops/page-5b92b7f75df20948.js +1 -0
  144. package/apps/pwa/out/_next/static/chunks/app/market/page-468eddf2b1a84ecb.js +1 -0
  145. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2af16e62c12f22df.js +1 -0
  146. package/apps/pwa/out/_next/static/chunks/app/morning/page-18b40867a77a5793.js +1 -0
  147. package/apps/pwa/out/_next/static/chunks/app/not-found-fe934f2979f5a4a2.js +1 -0
  148. package/apps/pwa/out/_next/static/chunks/app/observatory/page-3eec435537357170.js +1 -0
  149. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ff88cbfd2aa99fc8.js +1 -0
  150. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a5b39fb780c70ab5.js +1 -0
  151. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-ae465dde1d1b3933.js +1 -0
  152. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e7abc34a48a6f71e.js +1 -0
  153. package/apps/pwa/out/_next/static/chunks/app/page-cab9674fde9ead26.js +1 -0
  154. package/apps/pwa/out/_next/static/chunks/app/reflection/page-1da49bc8712284d0.js +1 -0
  155. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-5e8aa657551ac912.js +1 -0
  156. package/apps/pwa/out/_next/static/chunks/app/sessions/page-fcc36f36c6dc1fe2.js +1 -0
  157. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-d22db22f2a7296ca.js +1 -0
  158. package/apps/pwa/out/_next/static/chunks/app/settings/page-d12dc7b1773878f0.js +1 -0
  159. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-99debec9ea7fbec0.js +1 -0
  160. package/apps/pwa/out/_next/static/chunks/app/setup/layout-96750aba7430959f.js +1 -0
  161. package/apps/pwa/out/_next/static/chunks/app/setup/page-2a088a78bad7e924.js +1 -0
  162. package/apps/pwa/out/_next/static/chunks/app/share/page-2e8ed57db7e69993.js +1 -0
  163. package/apps/pwa/out/_next/static/chunks/app/showroom/page-4b3a16213c2ae8fe.js +1 -0
  164. package/apps/pwa/out/_next/static/chunks/app/tasks/page-f098747ddf38c98b.js +1 -0
  165. package/apps/pwa/out/_next/static/chunks/app/term/page-c15958835534404b.js +1 -0
  166. package/apps/pwa/out/_next/static/chunks/app/today/page-68279e3c843c1fd7.js +1 -0
  167. package/apps/pwa/out/_next/static/chunks/app/trace/page-a34a74453367a39d.js +1 -0
  168. package/apps/pwa/out/_next/static/chunks/app/vault/page-8767a7db2155a5bd.js +1 -0
  169. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-bd5976683460d7f2.js +1 -0
  170. package/apps/pwa/out/_next/static/chunks/app/workflows/page-b7364835e7f2ef54.js +1 -0
  171. package/apps/pwa/out/_next/static/chunks/app/workspace/page-311af16b3ee0ffca.js +1 -0
  172. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fb670757274ae990.js +1 -0
  173. package/apps/pwa/out/_next/static/chunks/{5014f970-22bef0b5210f7d60.js → ccae1c4b-0cf414f5eff1b10d.js} +1 -1
  174. package/apps/pwa/out/_next/static/chunks/{4fa33ebd.21c1bd5b294496e6.js → ceba0382.650712b56b85b582.js} +1 -1
  175. package/apps/pwa/out/_next/static/chunks/da5ecc5c-b005ad8cac737d57.js +1 -0
  176. package/apps/pwa/out/_next/static/chunks/f9a61e4d.1fc2c1dbdd49fe46.js +1 -0
  177. package/apps/pwa/out/_next/static/chunks/framework-6d497cbf1eef04c6.js +1 -0
  178. package/apps/pwa/out/_next/static/chunks/main-app-012f5edec932f337.js +1 -0
  179. package/apps/pwa/out/_next/static/chunks/main-da5aeb809edc94a2.js +1 -0
  180. package/apps/pwa/out/_next/static/chunks/pages/_app-5ee319af6f749dfe.js +1 -0
  181. package/apps/pwa/out/_next/static/chunks/pages/_error-4bc84653a718d6c6.js +1 -0
  182. package/apps/pwa/out/_next/static/chunks/webpack-8a96cf6775c73d6e.js +1 -0
  183. package/apps/pwa/out/_next/static/{L04y73dlwKeTW-GetaTF3 → uC-N0emedeNOFuaBQFv-I}/_buildManifest.js +1 -1
  184. package/apps/pwa/out/approvals/index.html +1 -1
  185. package/apps/pwa/out/approvals/index.txt +22 -22
  186. package/apps/pwa/out/autopilot/index.html +1 -1
  187. package/apps/pwa/out/autopilot/index.txt +22 -22
  188. package/apps/pwa/out/board/index.html +1 -1
  189. package/apps/pwa/out/board/index.txt +22 -22
  190. package/apps/pwa/out/botlab/index.html +1 -1
  191. package/apps/pwa/out/botlab/index.txt +21 -21
  192. package/apps/pwa/out/bots/index.html +1 -1
  193. package/apps/pwa/out/bots/index.txt +21 -21
  194. package/apps/pwa/out/ceo/index.html +1 -1
  195. package/apps/pwa/out/ceo/index.txt +22 -22
  196. package/apps/pwa/out/chat/index.html +1 -1
  197. package/apps/pwa/out/chat/index.txt +21 -21
  198. package/apps/pwa/out/consult/index.html +1 -1
  199. package/apps/pwa/out/consult/index.txt +22 -22
  200. package/apps/pwa/out/control/index.html +1 -1
  201. package/apps/pwa/out/control/index.txt +22 -22
  202. package/apps/pwa/out/decisions/index.html +1 -1
  203. package/apps/pwa/out/decisions/index.txt +21 -21
  204. package/apps/pwa/out/design-check/index.html +1 -1
  205. package/apps/pwa/out/design-check/index.txt +22 -22
  206. package/apps/pwa/out/editor/index.html +1 -1
  207. package/apps/pwa/out/editor/index.txt +22 -22
  208. package/apps/pwa/out/exec/index.html +1 -1
  209. package/apps/pwa/out/exec/index.txt +22 -22
  210. package/apps/pwa/out/field/index.html +1 -1
  211. package/apps/pwa/out/field/index.txt +22 -22
  212. package/apps/pwa/out/healthz +1 -1
  213. package/apps/pwa/out/index.html +1 -1
  214. package/apps/pwa/out/index.txt +21 -21
  215. package/apps/pwa/out/inside/index.html +1 -1
  216. package/apps/pwa/out/inside/index.txt +21 -21
  217. package/apps/pwa/out/intake/index.html +1 -1
  218. package/apps/pwa/out/intake/index.txt +22 -22
  219. package/apps/pwa/out/live/index.html +1 -1
  220. package/apps/pwa/out/live/index.txt +22 -22
  221. package/apps/pwa/out/live-run/index.html +1 -1
  222. package/apps/pwa/out/live-run/index.txt +22 -22
  223. package/apps/pwa/out/loops/index.html +1 -1
  224. package/apps/pwa/out/loops/index.txt +21 -21
  225. package/apps/pwa/out/market/index.html +1 -1
  226. package/apps/pwa/out/market/index.txt +21 -21
  227. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  228. package/apps/pwa/out/missions/placeholder/index.txt +21 -21
  229. package/apps/pwa/out/morning/index.html +1 -1
  230. package/apps/pwa/out/morning/index.txt +22 -22
  231. package/apps/pwa/out/observatory/index.html +1 -1
  232. package/apps/pwa/out/observatory/index.txt +22 -22
  233. package/apps/pwa/out/ops/checklist/index.html +1 -1
  234. package/apps/pwa/out/ops/checklist/index.txt +21 -21
  235. package/apps/pwa/out/ops/release/index.html +1 -1
  236. package/apps/pwa/out/ops/release/index.txt +21 -21
  237. package/apps/pwa/out/ops/seats/index.html +1 -1
  238. package/apps/pwa/out/ops/seats/index.txt +21 -21
  239. package/apps/pwa/out/outputs/index.html +1 -1
  240. package/apps/pwa/out/outputs/index.txt +21 -21
  241. package/apps/pwa/out/reflection/index.html +1 -1
  242. package/apps/pwa/out/reflection/index.txt +22 -22
  243. package/apps/pwa/out/scheduler/index.html +1 -1
  244. package/apps/pwa/out/scheduler/index.txt +22 -22
  245. package/apps/pwa/out/sessions/index.html +1 -1
  246. package/apps/pwa/out/sessions/index.txt +21 -21
  247. package/apps/pwa/out/settings/devices/index.html +1 -1
  248. package/apps/pwa/out/settings/devices/index.txt +22 -22
  249. package/apps/pwa/out/settings/index.html +1 -1
  250. package/apps/pwa/out/settings/index.txt +22 -22
  251. package/apps/pwa/out/setup/done/index.html +1 -1
  252. package/apps/pwa/out/setup/done/index.txt +22 -22
  253. package/apps/pwa/out/setup/index.html +1 -1
  254. package/apps/pwa/out/setup/index.txt +22 -22
  255. package/apps/pwa/out/share/index.html +1 -1
  256. package/apps/pwa/out/share/index.txt +22 -22
  257. package/apps/pwa/out/showroom/index.html +1 -1
  258. package/apps/pwa/out/showroom/index.txt +21 -21
  259. package/apps/pwa/out/tasks/index.html +1 -1
  260. package/apps/pwa/out/tasks/index.txt +22 -22
  261. package/apps/pwa/out/term/index.html +1 -1
  262. package/apps/pwa/out/term/index.txt +22 -22
  263. package/apps/pwa/out/today/index.html +1 -1
  264. package/apps/pwa/out/today/index.txt +22 -22
  265. package/apps/pwa/out/trace/index.html +1 -1
  266. package/apps/pwa/out/trace/index.txt +22 -22
  267. package/apps/pwa/out/vault/index.html +1 -1
  268. package/apps/pwa/out/vault/index.txt +22 -22
  269. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  270. package/apps/pwa/out/workflows/chat-ui/index.txt +22 -22
  271. package/apps/pwa/out/workflows/index.html +1 -1
  272. package/apps/pwa/out/workflows/index.txt +22 -22
  273. package/apps/pwa/out/workspace/index.html +1 -1
  274. package/apps/pwa/out/workspace/index.txt +22 -22
  275. package/apps/pwa/out/worktrees/index.html +1 -1
  276. package/apps/pwa/out/worktrees/index.txt +22 -22
  277. package/bin/elanous.mjs +3 -2
  278. package/catalog/providers/opengateway.yaml +25 -0
  279. package/docs/goal-context/01-goal-authoring-checks.md +8 -0
  280. package/docs/goal-context/03-verification-menu.md +24 -0
  281. package/docs/goal-context/04-mock-scenarios.md +100 -0
  282. package/docs/goal-context/05-authoring-lessons.jsonl +47 -0
  283. package/docs/goal-context/README.md +7 -0
  284. package/graphs/catalog/catalog.yaml +1 -1
  285. package/graphs/default-loop.yaml +63 -4
  286. package/graphs/document-loop.yaml +51 -3
  287. package/graphs/implement-loop.yaml +77 -4
  288. package/graphs/journey/ta-journey.yaml +11 -1
  289. package/graphs/journey/units/author-dialogue/author-dialogue.yaml +42 -0
  290. package/graphs/journey/units/author-dialogue/observe.ts +96 -0
  291. package/graphs/journey/units/author-dialogue/recipes.yaml +10 -0
  292. package/graphs/operate-loop.yaml +51 -3
  293. package/graphs/release/recipes.yaml +6 -0
  294. package/graphs/release/release-loop.yaml +17 -4
  295. package/graphs/research-loop.yaml +137 -3
  296. package/graphs/test-diet/test-diet.yaml +3 -0
  297. package/graphs/wizard/recipes.yaml +44 -0
  298. package/graphs/wizard/wizard-loop.yaml +44 -0
  299. package/graphs/work/research-report.yaml +2 -0
  300. package/package.json +1 -1
  301. package/scripts/ci-typecheck-changed.ts +139 -5
  302. package/scripts/coord-role-pack.ts +19 -6
  303. package/scripts/first10-gate.ts +416 -0
  304. package/scripts/fleet/wsl-node-setup.sh +432 -0
  305. package/scripts/hq/standby-snapshot.ts +17 -12
  306. package/scripts/release-loop/docs-land-node.ts +21 -5
  307. package/scripts/release-loop/docs-node.ts +204 -8
  308. package/scripts/release-loop/gate-baseline-receipt.ts +155 -0
  309. package/scripts/release-loop/gate-node.ts +427 -72
  310. package/scripts/release-loop/gate-partial.ts +65 -11
  311. package/scripts/release-loop/gate-radius-live.ts +177 -0
  312. package/scripts/release-loop/gate-radius-shadow.ts +1 -1
  313. package/scripts/release-loop/node-verdict.ts +14 -1
  314. package/scripts/release-loop/npm-publish-node.ts +66 -12
  315. package/scripts/release-loop/probe-pod-host-dirs-live.ts +5 -0
  316. package/scripts/release-loop/pwa-remote.ts +166 -0
  317. package/scripts/release-loop/pwa-scenarios-node.ts +19 -3
  318. package/scripts/release-loop/resume-cap.ts +14 -34
  319. package/scripts/release-loop/resume-release.ts +78 -15
  320. package/scripts/release-loop/traffic-node.ts +59 -0
  321. package/scripts/release-loop/traffic-transition.ts +159 -0
  322. package/scripts/release-loop/unattended-release.ts +2 -0
  323. package/scripts/rhythm/daily.ts +137 -3
  324. package/scripts/test-diet/lib.ts +6 -1
  325. package/scripts/test-diet/node.ts +194 -13
  326. package/scripts/ux-sim.ts +92 -0
  327. package/skills/README.md +1 -0
  328. package/src/acp/core-turn-bridge.ts +48 -6
  329. package/src/agent/delegate-hint.ts +16 -0
  330. package/src/agent/runner.ts +82 -2
  331. package/src/agent/self-ambient-cache.ts +8 -0
  332. package/src/agent/task-notification.ts +56 -10
  333. package/src/agent/types.ts +4 -0
  334. package/src/agent-substrate/acp-reviewer.ts +39 -4
  335. package/src/agent-substrate/pr-reviewer.ts +72 -3
  336. package/src/agent-substrate/review-intent.ts +29 -2
  337. package/src/agent-substrate/review-observation.ts +3 -0
  338. package/src/agent-substrate/self-review-cli.ts +229 -28
  339. package/src/autopilot/mission-codebase-gate.ts +141 -12
  340. package/src/autopilot/pr-manager.ts +240 -41
  341. package/src/boot/daemon-tools/self-implement.ts +1 -0
  342. package/src/budget/run-usage-rollup.ts +1 -0
  343. package/src/budget/usage-outcomes.ts +8 -4
  344. package/src/capture/tui-self-report.ts +102 -0
  345. package/src/chat/goal-continue-verdict.ts +41 -0
  346. package/src/chat/index.ts +30 -6
  347. package/src/chat/internal-turn-origin.ts +1 -0
  348. package/src/chat/paste-fold.ts +86 -0
  349. package/src/chat/pickers/modal-runtime.ts +10 -27
  350. package/src/chat/tool-render/block.ts +32 -0
  351. package/src/chat/tool-render/dispatch.ts +2 -20
  352. package/src/chat/tool-render/result-text.ts +98 -0
  353. package/src/chat/turn-typeahead.ts +70 -7
  354. package/src/cli/agent-cli.ts +13 -2
  355. package/src/cli/config-audit.ts +119 -0
  356. package/src/cli/doctor-cli.ts +26 -1
  357. package/src/cli/guardian-cli.ts +10 -0
  358. package/src/cli/herdr-cli.ts +34 -0
  359. package/src/cli/light-entry-route.ts +50 -7
  360. package/src/cli/log-event-names.ts +2 -0
  361. package/src/cli/make-cli.ts +247 -11
  362. package/src/cli/msg-cli.ts +117 -3
  363. package/src/cli/pod-cli.ts +77 -3
  364. package/src/cli/pr-cli.ts +34 -18
  365. package/src/cli/pr-land-base-read.ts +11 -2
  366. package/src/cli/pr-land-heavy-remote.ts +6 -3
  367. package/src/cli/pr-land-overlap-evidence.ts +23 -4
  368. package/src/cli/pr-land-pod-shadow-job.ts +3 -0
  369. package/src/cli/pr-land-recovery.ts +117 -1
  370. package/src/cli/pty-takeover-cli.ts +8 -7
  371. package/src/cli/release-checklist-read-cli.ts +79 -0
  372. package/src/cli/release-cli.ts +92 -106
  373. package/src/cli/release-single-flight.ts +1 -1
  374. package/src/cli/remote-resolve.ts +1 -1
  375. package/src/cli/root-tui-entry.ts +48 -0
  376. package/src/cli/sim-cli.ts +22 -0
  377. package/src/cli/tasks-card-show.ts +162 -0
  378. package/src/cli/tasks-cli.ts +1065 -338
  379. package/src/cli/tasks-read-cli.ts +197 -0
  380. package/src/cli/usage-cli.ts +13 -11
  381. package/src/clipboard/history.ts +179 -13
  382. package/src/config.ts +8 -1
  383. package/src/core-turn/run-core-turn.ts +4 -2
  384. package/src/core-turn/run-goal-loop.ts +73 -16
  385. package/src/core-turn/types.ts +6 -1
  386. package/src/dashboard/chat/acp-chat.ts +8 -0
  387. package/src/dashboard/context/keys.ts +8 -31
  388. package/src/dashboard/index.ts +280 -59
  389. package/src/dashboard/input/agent-wake.ts +7 -1
  390. package/src/dashboard/input/chat-main-plain-turn-runtime.ts +146 -3
  391. package/src/dashboard/input/chat-main-slash-command.ts +4 -2
  392. package/src/dashboard/input/goal-continue.ts +210 -0
  393. package/src/dashboard/input/goal-interim.ts +2 -0
  394. package/src/dashboard/input/internal-turn.ts +2 -8
  395. package/src/dashboard/input/seat-autowake.ts +9 -7
  396. package/src/dashboard/input/turn-autoheal.ts +52 -0
  397. package/src/dashboard/render/theme-resolver.ts +33 -4
  398. package/src/dashboard/seat-boot-preamble.ts +132 -5
  399. package/src/dashboard/slash-runtime/chat-persona.ts +65 -8
  400. package/src/dashboard/slash-runtime/dashboard-handlers.ts +3 -2
  401. package/src/dashboard/slash-runtime/model-choices.ts +15 -0
  402. package/src/dashboard/stale-install-check.ts +3 -99
  403. package/src/debug/batch-queue.ts +119 -0
  404. package/src/debug/tui-log-tag.ts +67 -0
  405. package/src/display/agent-surface.ts +7 -0
  406. package/src/domains/schedule-registry.ts +16 -2
  407. package/src/domains/standalone-log-sink.ts +15 -2
  408. package/src/esc-abort-gate.ts +69 -0
  409. package/src/git-fs/idle-refresh.ts +76 -0
  410. package/src/git-fs/index.ts +18 -7
  411. package/src/git-fs/worktree.ts +137 -5
  412. package/src/graph-kinds/registry.ts +2 -2
  413. package/src/graph-runner/graph-cli.ts +18 -0
  414. package/src/graph-runner/runner.ts +173 -21
  415. package/src/graph-wizard/generate.ts +108 -4
  416. package/src/graph-wizard/steps.ts +15 -0
  417. package/src/grok/credential.ts +10 -3
  418. package/src/grounding/proactive-ground.ts +86 -0
  419. package/src/harness/control-inbox.ts +49 -1
  420. package/src/harness/harness-cli-command.ts +163 -35
  421. package/src/harness/harness-incidents.ts +85 -1
  422. package/src/harness/launch-utterance-ledger.ts +208 -0
  423. package/src/harness/pod-control-send.ts +33 -5
  424. package/src/harness/self-send-remote.ts +69 -0
  425. package/src/harness/self-send-routing-guard.ts +6 -0
  426. package/src/harness/self-send-target.ts +4 -3
  427. package/src/impl-discipline/detector.ts +16 -4
  428. package/src/impl-discipline/system-prompt.ts +11 -0
  429. package/src/impl-discipline/types.ts +2 -2
  430. package/src/index.ts +822 -154
  431. package/src/input-core/context-key-runtime.ts +21 -0
  432. package/src/intake-plane/already-built-hint.ts +4 -2
  433. package/src/intake-plane/requirement-funnel.ts +29 -1
  434. package/src/light-cli.ts +32 -11
  435. package/src/llm/opengateway-models.ts +11 -0
  436. package/src/llm/provider-credentials.ts +14 -0
  437. package/src/llm/provider-detect.ts +8 -1
  438. package/src/llm/runtime-model-family.ts +2 -1
  439. package/src/llm-wellformed.ts +70 -0
  440. package/src/llm.ts +247 -59
  441. package/src/loops/msg-wake.ts +35 -39
  442. package/src/loops/orchestrator/inbox.ts +59 -11
  443. package/src/loops/orchestrator/knowhow-pack.ts +364 -0
  444. package/src/loops/orchestrator/tick.ts +5 -1
  445. package/src/maturity/feature-maturity.ts +1 -1
  446. package/src/model-tier/llm-tier-map.ts +15 -1
  447. package/src/models/prompts.ts +17 -2
  448. package/src/msg/msg-post-office.ts +116 -0
  449. package/src/mss/logging/log-store.ts +79 -22
  450. package/src/native-tool-catalog.ts +15 -1
  451. package/src/nexus/api/decisions-route.ts +38 -18
  452. package/src/nexus/api/graph-wizard-api.ts +20 -2
  453. package/src/nexus/api/loop-resources.ts +13 -1
  454. package/src/nexus/boot/register-mcp-clients.ts +263 -38
  455. package/src/nexus/onboarding/first-boot-wizard.ts +56 -8
  456. package/src/oauth/codex.ts +7 -0
  457. package/src/onboarding/first-run.ts +37 -6
  458. package/src/onboarding/web-first.ts +43 -11
  459. package/src/ops/overview-collect.ts +30 -9
  460. package/src/ops/overview.ts +22 -9
  461. package/src/persona/loader.ts +11 -0
  462. package/src/persona/types.ts +8 -0
  463. package/src/pr-tool/census.ts +261 -27
  464. package/src/prompt-library/session-guidance.ts +14 -0
  465. package/src/provider-summary.ts +8 -0
  466. package/src/pty-shell/herdr-agent-report.ts +118 -9
  467. package/src/pty-shell/herdr-prompt.ts +266 -0
  468. package/src/pty-shell/pty-control-ipc.ts +151 -8
  469. package/src/pty-shell/pty-manifest.ts +7 -0
  470. package/src/pty-shell/pty-special-keys.ts +42 -2
  471. package/src/pty-shell/stale-install-check.ts +99 -0
  472. package/src/pty-shell/tui-run-state.ts +14 -2
  473. package/src/release-loop/auto-start.ts +20 -2
  474. package/src/release-loop/checklist.ts +100 -4
  475. package/src/release-loop/landed-but-yellow.ts +129 -5
  476. package/src/release-loop/light-rc.ts +1 -0
  477. package/src/release-loop/rehearsal.ts +37 -21
  478. package/src/repl/index.ts +192 -17
  479. package/src/repl/ops-msg-bridge.ts +32 -0
  480. package/src/seat-loop/knowhow-lint.ts +1 -1
  481. package/src/self-dev/ask-launch-flow.ts +44 -5
  482. package/src/self-dev/cli-help-text.ts +17 -0
  483. package/src/self-dev/decompose-proposal.ts +172 -20
  484. package/src/self-dev/decompose.ts +61 -0
  485. package/src/self-dev/dev-cli.ts +48 -12
  486. package/src/self-dev/dev-pipeline.ts +8 -2
  487. package/src/self-dev/draft-residue.ts +37 -1
  488. package/src/self-dev/draft-sweep.ts +137 -11
  489. package/src/self-dev/draft-triage-rules.ts +138 -0
  490. package/src/self-dev/fleet-reduce.ts +3 -2
  491. package/src/self-dev/glm-credential-guard.ts +77 -0
  492. package/src/self-dev/graph-front-nodes.ts +112 -0
  493. package/src/self-dev/graph-journey-nodes.ts +2 -0
  494. package/src/self-dev/graph-journey-route.ts +12 -2
  495. package/src/self-dev/harness-run-cli.ts +3 -15
  496. package/src/self-dev/orchestrate-cli-text.ts +10 -0
  497. package/src/self-dev/orchestrate-cli.ts +68 -3
  498. package/src/self-dev/orchestrate.ts +116 -17
  499. package/src/self-dev/review-provider-fallback.ts +35 -1
  500. package/src/self-dev/run-supervisor.ts +55 -2
  501. package/src/self-dev/run-types.ts +1 -1
  502. package/src/self-dev/self-orchestrate-runtime.ts +1 -11
  503. package/src/self-dev/shard-aggregate.ts +42 -5
  504. package/src/self-dev/shard-run.ts +8 -1
  505. package/src/self-health/autoheal.ts +153 -0
  506. package/src/self-health/signatures.ts +6 -0
  507. package/src/self-implement/blocked-draft-policy.ts +20 -0
  508. package/src/self-implement/classify-shadow.ts +3 -7
  509. package/src/self-implement/device-first.ts +116 -0
  510. package/src/self-implement/doctor-visit.ts +257 -2
  511. package/src/self-implement/evidence-command-budget.ts +80 -0
  512. package/src/self-implement/gate-baseline.ts +56 -17
  513. package/src/self-implement/gate-cli.ts +1 -0
  514. package/src/self-implement/gate-remote.ts +31 -20
  515. package/src/self-implement/goal-author-cli.ts +1 -0
  516. package/src/self-implement/goal-author-decompose.ts +534 -45
  517. package/src/self-implement/goal-author.ts +282 -7
  518. package/src/self-implement/goal-elaborate.ts +449 -0
  519. package/src/self-implement/goal-refine.ts +273 -0
  520. package/src/self-implement/graph-overlay-yaml.ts +14 -3
  521. package/src/self-implement/graph-variant.ts +40 -6
  522. package/src/self-implement/headless-elanous-driver.ts +19 -3
  523. package/src/self-implement/off-diff-evidence.ts +58 -6
  524. package/src/self-implement/orchestrator.ts +370 -62
  525. package/src/self-implement/pod-warm-pool-profile.ts +1 -0
  526. package/src/self-implement/pwa-import-graph.ts +1 -1
  527. package/src/self-implement/rework-budget-judgment.ts +5 -8
  528. package/src/self-implement/rework-policy.ts +30 -0
  529. package/src/self-implement/run-ledger.ts +108 -0
  530. package/src/self-implement/run-stop.ts +10 -1
  531. package/src/self-implement/run-ttl.ts +38 -24
  532. package/src/self-implement/running-runs.ts +29 -1
  533. package/src/self-implement/scope-adjacency.ts +115 -0
  534. package/src/self-implement/seams.ts +187 -28
  535. package/src/self-implement/stop-autoheal.ts +87 -1
  536. package/src/self-implement/wiring-check.ts +358 -0
  537. package/src/session-runtime/index.ts +1 -1
  538. package/src/setup/llm-env-detect.ts +5 -0
  539. package/src/shell-env-bootstrap.ts +11 -4
  540. package/src/skills/runner.ts +4 -2
  541. package/src/skills/tools/agent.ts +408 -40
  542. package/src/skills/tools/msg-ask.ts +100 -0
  543. package/src/skills/tools/ops-ledger.ts +2 -1
  544. package/src/skills/tools/schedule-wakeup.ts +23 -4
  545. package/src/skills/tools/shell.ts +61 -6
  546. package/src/skills/tools/terminal-modal-inject.ts +1 -2
  547. package/src/skills/tools/terminal-modal.ts +10 -8
  548. package/src/skills/url-router.ts +41 -1
  549. package/src/smart-wizard/run.ts +66 -0
  550. package/src/status/bar.ts +34 -6
  551. package/src/status/status-provider-info.ts +37 -6
  552. package/src/steward/hold-hygiene.ts +24 -6
  553. package/src/steward/rejudge-tick.ts +10 -2
  554. package/src/steward/ta-repair-event-collect.ts +17 -6
  555. package/src/steward/triage-budget.ts +194 -0
  556. package/src/steward/triage.ts +20 -3
  557. package/src/task-agent/actions.ts +72 -2
  558. package/src/task-agent/green-sweep.ts +4 -0
  559. package/src/task-agent/land-queue.ts +21 -8
  560. package/src/task-agent/live-moves.ts +182 -10
  561. package/src/task-agent/mission.ts +30 -0
  562. package/src/task-agent/parent-host.ts +123 -3
  563. package/src/task-agent/rejudge-on-head.ts +15 -0
  564. package/src/task-agent/repair-cycle.ts +138 -32
  565. package/src/task-agent/repair-next-move.ts +31 -3
  566. package/src/task-agent/repair-outcomes.ts +29 -0
  567. package/src/task-agent/repair-worker.ts +14 -4
  568. package/src/task-agent/shadow.ts +48 -4
  569. package/src/task-agent/task-card-prefix.ts +2 -0
  570. package/src/task-agent/task-hand.ts +525 -57
  571. package/src/task-agent/task-parent-role-llm.ts +36 -0
  572. package/src/task-agent/tier-split.ts +196 -0
  573. package/src/task-agent/work-order-build.ts +2 -0
  574. package/src/task-agent/work-order.ts +3 -2
  575. package/src/task-orchestrator/generator-prompt.ts +60 -11
  576. package/src/task-orchestrator/surfaces/pod-account-broker.ts +42 -0
  577. package/src/task-orchestrator/surfaces/pod-child-providers.ts +1 -1
  578. package/src/task-orchestrator/surfaces/pod-child-reuse.ts +146 -0
  579. package/src/task-orchestrator/surfaces/pod-command-job.ts +130 -11
  580. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +215 -3
  581. package/src/task-orchestrator/surfaces/pod-image-source.ts +89 -1
  582. package/src/task-orchestrator/surfaces/pod-launch-history.ts +133 -0
  583. package/src/task-orchestrator/surfaces/pod-lease.ts +17 -8
  584. package/src/task-orchestrator/surfaces/pod-manifest-test-normalize.ts +19 -2
  585. package/src/task-orchestrator/surfaces/pod-pool.ts +76 -4
  586. package/src/task-orchestrator/surfaces/pod-provenance.ts +82 -0
  587. package/src/task-orchestrator/surfaces/pod-repair-commit.ts +18 -3
  588. package/src/task-orchestrator/surfaces/pod-si-warm-entry.ts +16 -0
  589. package/src/task-orchestrator/surfaces/pod-si-warm-host.ts +224 -0
  590. package/src/task-orchestrator/surfaces/pod-si-warm-protocol.ts +201 -0
  591. package/src/task-orchestrator/surfaces/pod-si-warm-provider.ts +444 -0
  592. package/src/task-orchestrator/surfaces/pod-si-warm-runner.ts +387 -0
  593. package/src/task-orchestrator/surfaces/pod-si-warm-transport.ts +157 -0
  594. package/src/task-orchestrator/surfaces/pod-source-receive.ts +18 -0
  595. package/src/task-orchestrator/surfaces/pod-warm-provenance.ts +81 -0
  596. package/src/task-orchestrator/surfaces/pod-warm-worker-pod.ts +6 -2
  597. package/src/task-orchestrator/surfaces/pod-warm-worker-protocol.ts +7 -0
  598. package/src/task-orchestrator/surfaces/pod-warm-worker-provider.ts +521 -40
  599. package/src/task-orchestrator/surfaces/pod-warm-worker-runtime.ts +3 -0
  600. package/src/task-orchestrator/surfaces/self-implement-pod.ts +759 -50
  601. package/src/task-orchestrator/surfaces/self-implement.ts +65 -2
  602. package/src/tool-runtime/index.ts +15 -2
  603. package/src/tool-runtime/mcp-proxy-bootstrap.ts +29 -0
  604. package/src/tool-runtime/ops-ledger-runtimes.ts +10 -2
  605. package/src/tty-foreground.ts +155 -0
  606. package/src/tui/README.md +2 -0
  607. package/src/tui/chat/background-job-status.ts +54 -0
  608. package/src/tui/chat/goal-interim.ts +173 -0
  609. package/src/tui/chat/log-view.ts +267 -0
  610. package/src/tui/chat/self-health-status.ts +232 -0
  611. package/src/tui/entry.ts +695 -0
  612. package/src/tui/input/composer.ts +151 -0
  613. package/src/tui/seat-mail-boot.ts +59 -0
  614. package/src/tui/slash/compact-command.ts +7 -0
  615. package/src/tui/slash/compact.ts +94 -0
  616. package/src/tui/slash/new-session.ts +35 -0
  617. package/src/tui/slash/resume.ts +75 -0
  618. package/src/tui-client/local-chat-turn.ts +282 -0
  619. package/src/tui-client/local-harness-command.ts +63 -0
  620. package/src/tui-client/local-provider-command.ts +184 -0
  621. package/src/tui-client/next-dashboard-runtime.ts +8 -0
  622. package/src/tui-client/next-loop-command.ts +70 -0
  623. package/src/tui-client/next-wakeup.ts +191 -0
  624. package/src/tui.ts +37 -0
  625. package/src/ui/modal-adapter.ts +2 -2
  626. package/src/user-config.ts +4 -2
  627. package/src/ux-sim/classic-tui-seat-scenarios.ts +516 -0
  628. package/src/ux-sim/cli-config-audit-device.ts +121 -0
  629. package/src/ux-sim/dashboard-event-loop-stall-monitor-device.ts +238 -0
  630. package/src/ux-sim/event-monkey.ts +379 -0
  631. package/src/ux-sim/fake-bin.ts +87 -7
  632. package/src/ux-sim/fake-herdr.ts +238 -0
  633. package/src/ux-sim/fake-llm.ts +111 -6
  634. package/src/ux-sim/fault-glm-no-credential.ts +116 -0
  635. package/src/ux-sim/fault-nextmove-dispatch.ts +367 -0
  636. package/src/ux-sim/fault-pod-codex-503.ts +211 -0
  637. package/src/ux-sim/fault-scope-escape-lift.ts +216 -0
  638. package/src/ux-sim/fault-tui-400-autoheal.ts +297 -0
  639. package/src/ux-sim/goal-device.ts +619 -0
  640. package/src/ux-sim/in-process-cli.ts +22 -5
  641. package/src/ux-sim/mock-authoring-first-run-child.ts +35 -0
  642. package/src/ux-sim/mock-authoring-first-run.ts +97 -0
  643. package/src/ux-sim/mock-seat-pty-frame-child.ts +29 -0
  644. package/src/ux-sim/mock-seat-pty-frame.ts +297 -0
  645. package/src/ux-sim/mock-seat-pty-sequence-child.ts +21 -0
  646. package/src/ux-sim/mock-seat-pty-sequence.ts +206 -0
  647. package/src/ux-sim/next-tui-seat-scenarios.ts +631 -0
  648. package/src/ux-sim/onboarding-first-run-device.ts +269 -0
  649. package/src/ux-sim/pty-key-modifiers-device.ts +85 -0
  650. package/src/ux-sim/subagent-tools-device.ts +81 -0
  651. package/src/ux-sim/task-orchestrator-surfaces-self-implement-pod-device.ts +250 -0
  652. package/src/ux-sim/tty-foreground-steal-child.ts +21 -0
  653. package/src/version/packed-revision.json +1 -1
  654. package/apps/pwa/out/_next/static/chunks/1162.c69abda138c7ce43.js +0 -79
  655. package/apps/pwa/out/_next/static/chunks/1449-1fede62af84c3e43.js +0 -1
  656. package/apps/pwa/out/_next/static/chunks/1598-13fc845935e816df.js +0 -1
  657. package/apps/pwa/out/_next/static/chunks/1639.62c5e16fa20d242f.js +0 -94
  658. package/apps/pwa/out/_next/static/chunks/1661-adb8b2babc9aaf8c.js +0 -1
  659. package/apps/pwa/out/_next/static/chunks/2147.107c0fc6146fb15a.js +0 -23
  660. package/apps/pwa/out/_next/static/chunks/2202-ef1c9b000c3074fc.js +0 -1
  661. package/apps/pwa/out/_next/static/chunks/2247.1e326638ee43e672.js +0 -115
  662. package/apps/pwa/out/_next/static/chunks/2268.56e1e3d58180abae.js +0 -8
  663. package/apps/pwa/out/_next/static/chunks/2469-606819c812414021.js +0 -1
  664. package/apps/pwa/out/_next/static/chunks/2593.e53c1620c0bb3599.js +0 -1
  665. package/apps/pwa/out/_next/static/chunks/2690.e96671114222676a.js +0 -1
  666. package/apps/pwa/out/_next/static/chunks/2898-79ee1a5cc0cd9d13.js +0 -1
  667. package/apps/pwa/out/_next/static/chunks/298.26099a9f65e5c8ff.js +0 -1
  668. package/apps/pwa/out/_next/static/chunks/2987-30cf9c2eb7b8bdd5.js +0 -1
  669. package/apps/pwa/out/_next/static/chunks/2992.51fc2a7295bd8139.js +0 -1
  670. package/apps/pwa/out/_next/static/chunks/3001-5a52788c813b6113.js +0 -1
  671. package/apps/pwa/out/_next/static/chunks/3014-291245fba9df7b5b.js +0 -1
  672. package/apps/pwa/out/_next/static/chunks/3068.ee78201f0251a18d.js +0 -1
  673. package/apps/pwa/out/_next/static/chunks/3083.cd399a17e9365f78.js +0 -1
  674. package/apps/pwa/out/_next/static/chunks/3117-e1c474683fd1f824.js +0 -1
  675. package/apps/pwa/out/_next/static/chunks/3251-2c17bd5a91f8de2e.js +0 -1
  676. package/apps/pwa/out/_next/static/chunks/326.3fb32322bb2ddf6d.js +0 -1
  677. package/apps/pwa/out/_next/static/chunks/3421-7ff7fbaa156985a7.js +0 -1
  678. package/apps/pwa/out/_next/static/chunks/381.f55dc63ea87735e6.js +0 -1
  679. package/apps/pwa/out/_next/static/chunks/4129-ab0a6189f1778633.js +0 -1
  680. package/apps/pwa/out/_next/static/chunks/4361.93d14ba594b3a035.js +0 -104
  681. package/apps/pwa/out/_next/static/chunks/4364.7a7eb19cceb67d2f.js +0 -286
  682. package/apps/pwa/out/_next/static/chunks/4406.d71f506f7c3ee9d3.js +0 -149
  683. package/apps/pwa/out/_next/static/chunks/4461.2ad1514180ae7e29.js +0 -148
  684. package/apps/pwa/out/_next/static/chunks/4479.bd309cbe0e3842ee.js +0 -64
  685. package/apps/pwa/out/_next/static/chunks/4522-05966f777d66d13b.js +0 -1
  686. package/apps/pwa/out/_next/static/chunks/4543-9a3e9812e845de65.js +0 -1
  687. package/apps/pwa/out/_next/static/chunks/4557.f8f1d84a0d90d590.js +0 -74
  688. package/apps/pwa/out/_next/static/chunks/4589-79b11542179ef58c.js +0 -1
  689. package/apps/pwa/out/_next/static/chunks/4633-9f8f9f88ffbb8f5c.js +0 -1
  690. package/apps/pwa/out/_next/static/chunks/4831.23af4d10c0697b5e.js +0 -1
  691. package/apps/pwa/out/_next/static/chunks/4983.5951b85724f61f2a.js +0 -1
  692. package/apps/pwa/out/_next/static/chunks/5028-f0722e6114c313e7.js +0 -1
  693. package/apps/pwa/out/_next/static/chunks/5249.96a848656b582a93.js +0 -1
  694. package/apps/pwa/out/_next/static/chunks/5311.16e0ffbfa5468b80.js +0 -62
  695. package/apps/pwa/out/_next/static/chunks/5489.ec2dd1b5272a19dc.js +0 -24
  696. package/apps/pwa/out/_next/static/chunks/559.df87695df637920c.js +0 -1
  697. package/apps/pwa/out/_next/static/chunks/5618-5c88f2f70178c4ad.js +0 -1
  698. package/apps/pwa/out/_next/static/chunks/5764.05487bf0b73ddb6b.js +0 -1
  699. package/apps/pwa/out/_next/static/chunks/5930.e678826c3f831aab.js +0 -1
  700. package/apps/pwa/out/_next/static/chunks/5dfb57f7.f2e3305326202474.js +0 -1
  701. package/apps/pwa/out/_next/static/chunks/6053-d893974de92e6198.js +0 -1
  702. package/apps/pwa/out/_next/static/chunks/6200-cefd6f33f59d765e.js +0 -1
  703. package/apps/pwa/out/_next/static/chunks/6314-edeba650ae686ac9.js +0 -1
  704. package/apps/pwa/out/_next/static/chunks/6316-bc9a9b74d7842d79.js +0 -1
  705. package/apps/pwa/out/_next/static/chunks/6476-13b9779b73528008.js +0 -1
  706. package/apps/pwa/out/_next/static/chunks/666-04db3ac4c8e3c66f.js +0 -1
  707. package/apps/pwa/out/_next/static/chunks/670.682e55106ef915a6.js +0 -77
  708. package/apps/pwa/out/_next/static/chunks/6816.b7ba9d0a1a4a46a7.js +0 -1
  709. package/apps/pwa/out/_next/static/chunks/690-fbbcc777f189af37.js +0 -1
  710. package/apps/pwa/out/_next/static/chunks/6989.0d3d4020e5cb3df6.js +0 -90
  711. package/apps/pwa/out/_next/static/chunks/7193-cf355cca5ba6c5d5.js +0 -1
  712. package/apps/pwa/out/_next/static/chunks/7217-d17393fe7af78fa6.js +0 -1
  713. package/apps/pwa/out/_next/static/chunks/7232.24edbd938a69c8d0.js +0 -1
  714. package/apps/pwa/out/_next/static/chunks/7295cd1e.b4b557908888eeee.js +0 -1
  715. package/apps/pwa/out/_next/static/chunks/7323.adb02de21991883c.js +0 -1
  716. package/apps/pwa/out/_next/static/chunks/7386.5d8e7e42e818dfb6.js +0 -287
  717. package/apps/pwa/out/_next/static/chunks/7448.478958ffbbba0ac0.js +0 -1
  718. package/apps/pwa/out/_next/static/chunks/7537.ba2208c5c656b8a3.js +0 -1
  719. package/apps/pwa/out/_next/static/chunks/7596-962ae6db6cfc9c83.js +0 -1
  720. package/apps/pwa/out/_next/static/chunks/7671.cff51beac212a2ba.js +0 -1
  721. package/apps/pwa/out/_next/static/chunks/7883.3916734ee16d3e58.js +0 -27
  722. package/apps/pwa/out/_next/static/chunks/7952.b192c31b6a0e00cc.js +0 -1
  723. package/apps/pwa/out/_next/static/chunks/801.5b24e81eded22d1b.js +0 -1
  724. package/apps/pwa/out/_next/static/chunks/8176.8c9a8613f5a01ac1.js +0 -36
  725. package/apps/pwa/out/_next/static/chunks/819e78a6.fa3dd7f7da19bf61.js +0 -1
  726. package/apps/pwa/out/_next/static/chunks/8220-78978450c3183d8d.js +0 -1
  727. package/apps/pwa/out/_next/static/chunks/8221-eb60b5bc692a3361.js +0 -1
  728. package/apps/pwa/out/_next/static/chunks/826.e9a26064f474949e.js +0 -1
  729. package/apps/pwa/out/_next/static/chunks/8459-a351487a4480f584.js +0 -1
  730. package/apps/pwa/out/_next/static/chunks/8482-75823d513a4ce151.js +0 -1
  731. package/apps/pwa/out/_next/static/chunks/8488.8da515248cda3f3a.js +0 -1
  732. package/apps/pwa/out/_next/static/chunks/8584.388a1779d9b203af.js +0 -1
  733. package/apps/pwa/out/_next/static/chunks/8637.a55e098e61ffefb8.js +0 -1
  734. package/apps/pwa/out/_next/static/chunks/9207-8e6a12db30373f32.js +0 -1
  735. package/apps/pwa/out/_next/static/chunks/9263-00ae932c62696a86.js +0 -1
  736. package/apps/pwa/out/_next/static/chunks/9318.caafc3a130e8769c.js +0 -1
  737. package/apps/pwa/out/_next/static/chunks/932.da3fb55951882562.js +0 -93
  738. package/apps/pwa/out/_next/static/chunks/9433.002f8426ac75b6b7.js +0 -1
  739. package/apps/pwa/out/_next/static/chunks/9464.dd631a5a14b1d660.js +0 -24
  740. package/apps/pwa/out/_next/static/chunks/9739.8d5f36ac67a3b236.js +0 -1
  741. package/apps/pwa/out/_next/static/chunks/9743.beeabb9d5f86abe4.js +0 -1
  742. package/apps/pwa/out/_next/static/chunks/9843-a23d3f32dc43f715.js +0 -1
  743. package/apps/pwa/out/_next/static/chunks/994-28211077c7c587d5.js +0 -1
  744. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-3b4c947f174e33d8.js +0 -1
  745. package/apps/pwa/out/_next/static/chunks/app/approvals/page-369d5ce99483f859.js +0 -1
  746. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-63c0ed0eb26e5fce.js +0 -1
  747. package/apps/pwa/out/_next/static/chunks/app/board/page-62802c6003bc85cc.js +0 -1
  748. package/apps/pwa/out/_next/static/chunks/app/botlab/page-ecb41560d4e77be9.js +0 -1
  749. package/apps/pwa/out/_next/static/chunks/app/bots/page-7b65ad8130c93e95.js +0 -1
  750. package/apps/pwa/out/_next/static/chunks/app/ceo/page-8755477c15a9060c.js +0 -1
  751. package/apps/pwa/out/_next/static/chunks/app/chat/page-5725447dd972c471.js +0 -1
  752. package/apps/pwa/out/_next/static/chunks/app/consult/page-b979de0591a69375.js +0 -1
  753. package/apps/pwa/out/_next/static/chunks/app/control/page-6d0796bf4633751a.js +0 -1
  754. package/apps/pwa/out/_next/static/chunks/app/decisions/page-59c31dc0394e67b4.js +0 -1
  755. package/apps/pwa/out/_next/static/chunks/app/design-check/page-12dfc95f6cc5c718.js +0 -1
  756. package/apps/pwa/out/_next/static/chunks/app/editor/page-25788244e66aa4dd.js +0 -1
  757. package/apps/pwa/out/_next/static/chunks/app/exec/page-a2555dc705466322.js +0 -1
  758. package/apps/pwa/out/_next/static/chunks/app/healthz/route-3b4c947f174e33d8.js +0 -1
  759. package/apps/pwa/out/_next/static/chunks/app/inside/page-578729adbeaa40cb.js +0 -1
  760. package/apps/pwa/out/_next/static/chunks/app/intake/page-a1d20fffbec35205.js +0 -1
  761. package/apps/pwa/out/_next/static/chunks/app/layout-4a558f94516b8004.js +0 -1
  762. package/apps/pwa/out/_next/static/chunks/app/live/page-9e3834705b86145e.js +0 -1
  763. package/apps/pwa/out/_next/static/chunks/app/live-run/page-d7bff0f0905a662c.js +0 -1
  764. package/apps/pwa/out/_next/static/chunks/app/loops/page-cb1c5fe746ea4014.js +0 -1
  765. package/apps/pwa/out/_next/static/chunks/app/market/page-9ee320507356582a.js +0 -1
  766. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66fd183b74835886.js +0 -1
  767. package/apps/pwa/out/_next/static/chunks/app/morning/page-3272a064a01a9927.js +0 -1
  768. package/apps/pwa/out/_next/static/chunks/app/not-found-f281b7a7f6ef2529.js +0 -1
  769. package/apps/pwa/out/_next/static/chunks/app/observatory/page-d45a65f8b7124fcd.js +0 -1
  770. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-e9c1fe684dd19a66.js +0 -1
  771. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-b8322932fc6e34de.js +0 -1
  772. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-82c66d9af2290575.js +0 -1
  773. package/apps/pwa/out/_next/static/chunks/app/outputs/page-63c6752009c97b05.js +0 -1
  774. package/apps/pwa/out/_next/static/chunks/app/page-bb33d42ebc358aec.js +0 -1
  775. package/apps/pwa/out/_next/static/chunks/app/reflection/page-7e4c9379197d7214.js +0 -1
  776. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-38bf1e9a7001732b.js +0 -1
  777. package/apps/pwa/out/_next/static/chunks/app/sessions/page-2e5b752012389d56.js +0 -1
  778. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-398c0faa173203f7.js +0 -1
  779. package/apps/pwa/out/_next/static/chunks/app/settings/page-002f53a965b6445c.js +0 -1
  780. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7f75997f61a26269.js +0 -1
  781. package/apps/pwa/out/_next/static/chunks/app/setup/layout-3b4c947f174e33d8.js +0 -1
  782. package/apps/pwa/out/_next/static/chunks/app/setup/page-05631753294f23fb.js +0 -1
  783. package/apps/pwa/out/_next/static/chunks/app/share/page-edd700fd2cc3d7f8.js +0 -1
  784. package/apps/pwa/out/_next/static/chunks/app/showroom/page-105f83ecb4594365.js +0 -1
  785. package/apps/pwa/out/_next/static/chunks/app/tasks/page-5823e87d2017f19c.js +0 -1
  786. package/apps/pwa/out/_next/static/chunks/app/term/page-df3445b207ceceab.js +0 -1
  787. package/apps/pwa/out/_next/static/chunks/app/today/page-b51ca09566c03de7.js +0 -1
  788. package/apps/pwa/out/_next/static/chunks/app/trace/page-8aacd5ded5dbac60.js +0 -1
  789. package/apps/pwa/out/_next/static/chunks/app/vault/page-bae3365a0b3c6a8e.js +0 -1
  790. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-9ed52b48ff1aa7fd.js +0 -1
  791. package/apps/pwa/out/_next/static/chunks/app/workflows/page-b6f8cad35fe00946.js +0 -1
  792. package/apps/pwa/out/_next/static/chunks/app/workspace/page-6d25ceda500d2457.js +0 -1
  793. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-b6401a5f2c962fe8.js +0 -1
  794. package/apps/pwa/out/_next/static/chunks/ebf6cf5e.0d2f887b040e80fb.js +0 -122
  795. package/apps/pwa/out/_next/static/chunks/f09ec7d7-d29abe23edcbb193.js +0 -1
  796. package/apps/pwa/out/_next/static/chunks/framework-9322cb8a56ac081c.js +0 -1
  797. package/apps/pwa/out/_next/static/chunks/main-2fbab294e49a1561.js +0 -1
  798. package/apps/pwa/out/_next/static/chunks/main-app-f36729f56621a8f3.js +0 -1
  799. package/apps/pwa/out/_next/static/chunks/pages/_app-2a62e450b47c6114.js +0 -1
  800. package/apps/pwa/out/_next/static/chunks/pages/_error-5e17f7b445f34d43.js +0 -1
  801. package/apps/pwa/out/_next/static/chunks/webpack-b4263e2190ea7adf.js +0 -1
  802. /package/apps/pwa/out/_next/static/{L04y73dlwKeTW-GetaTF3 → uC-N0emedeNOFuaBQFv-I}/_ssgManifest.js +0 -0
@@ -18,6 +18,8 @@ import { GROUNDING_TOKEN_ENV, GROUNDING_URL_ENV, mintGroundingToken, revokeGroun
18
18
  import { POD_CREDENTIAL_GROK_PATH, POD_CREDENTIAL_GITHUB_PATH, POD_GITHUB_CREDENTIAL_TOKEN_ENV, POD_GITHUB_CREDENTIAL_URL_ENV, installationRepositories } from '../../nexus/api/pod-credential-api.js';
19
19
  import { coalescedInstallationCredential } from '../../auth/github-app-token.js';
20
20
  import { POD_CREDENTIAL_TOKEN_ENV, POD_CREDENTIAL_URL_ENV } from '../../grok/credential.js';
21
+ import { isVerifiedOpenGatewayModel } from '../../llm/opengateway-models.js';
22
+ import { configuredOpenGatewayKeys } from '../../llm/provider-credentials.js';
21
23
  import { collectPodLedgers, createPodLedgerFollower } from './pod-ledger-collect.js';
22
24
  import { collectPodArtifacts, podNoResultDiagnostic } from './pod-artifact-return.js';
23
25
  import { readPodRepairArtifact } from './pod-repair-commit.js';
@@ -28,12 +30,18 @@ import { controlInboxEnv } from '../../harness/control-inbox.js';
28
30
  import { finishPodFragment, writePodFragment } from '../../harness/self-send-target.js';
29
31
  import { mintRunId, normalizeRunId } from '../../harness/harness-space.js';
30
32
  import { TA_LAND_ENV } from '../../task-agent/task-hand.js';
31
- import { appendRunLedgerEntry, loadRunLedger, runLedgerDir, runLedgerPath } from '../../self-implement/run-ledger.js';
33
+ import { appendRunLedgerEntry, loadRunLedger, runLedgerDir, runLedgerPath, type RunLedgerEntry } from '../../self-implement/run-ledger.js';
34
+ import { runSuffixKey } from '../../self-implement/salvage-branches.js';
32
35
  import type { GoalExecutionRecord } from '../../self-implement/orchestrator.js';
33
36
  import { hostRepositoryUnavailable, runHostRegate, type HostRegateResult } from '../../self-implement/host-regate.js';
34
37
  import { resolvePodHostAcpReview, runPodHostAcpReview, type PodHostAcpReviewOutcome } from './pod-host-acp-review.js';
35
38
  import { REVIEW_DEFERRED_TO_HOST_ENV, REVIEW_DEFERRED_TO_HOST_MERGE_REASON } from '../../self-implement/review-deferred-to-host.js';
36
39
  import { classifyPodTerminal, applyPodTerminal, podTerminalPrBody, type PrTerminalRecord, type DraftResidueContext } from '../../self-dev/draft-residue.js';
40
+ import { assessSiWarmReply, decideSiWarmNextStep, type SiWarmCommand, type SiWarmReply,
41
+ type SiWarmReturnedBytes } from './pod-si-warm-protocol.js';
42
+ import { cleanAndReturnChildPod, type ChildPodReuseOperations } from './pod-child-reuse.js';
43
+ import { executeSiWarmTransport, type SiWarmFixedRunner,
44
+ type SiWarmPodIdentity } from './pod-si-warm-transport.js';
37
45
 
38
46
  export const POD_CONTROL_INBOX_DIR = '/tmp/elanous-control.inbox';
39
47
  /** Pod Job 수명 상한(`activeDeadlineSeconds`) — 대표 2026-09-26 90분 → 180분(90분에 `DeadlineExceeded` 로 죽은 런 둘). */
@@ -56,6 +64,9 @@ export const POD_JOB_DEADLINE_SECONDS = 10_800;
56
64
  // POD-DIET (10-06 03:47~05:18 · node-b goal Pods · 89 one-minute samples · 42 Pods): per-Pod observed max median 4.6Gi ·
57
65
  // p75 6.2Gi · p90 11.7Gi — 23/42 went above the old 4Gi request, so the scheduler over-packed. 6Gi ≈ p75; limits stay.
58
66
  // Node allocatable ≈ 343Gi ≫ 25 × 6Gi. One-minute samples can miss the true peak (lower bound).
67
+ import { POD_LAUNCH_FINISHED_EVENT, POD_LAUNCH_STARTED_EVENT, provenanceRowFromAnnotations, siTaskPodProvenance } from './pod-launch-history.js';
68
+ import { podProvenanceMetadata, type PodProvenanceMetadata } from './pod-provenance.js';
69
+ import { packageVersion } from '../../version/code-revision.js';
59
70
  import { podWorkLabels, type PodWorkIdentity, type PodPlacement, type PodPlacementKind, type PodPoolMember, type PodPoolScheduler } from './pod-pool.js';
60
71
  export { POD_CHILD_REQUESTS } from './pod-lease.js';
61
72
  import { measurePoolLease, recommendConcurrency, POD_CHILD_REQUESTS, POD_SI_ADMISSION_CAP_BYTES, POD_HOST_LEASE_ANNOTATION, POD_JOB_TTL_AFTER_FINISHED_SECONDS, LEASE_KUBECTL_MAX_BUFFER, memoryQuantityBytes } from './pod-lease.js';
@@ -68,7 +79,7 @@ import { isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
68
79
  import { findGitDir } from '../../git-fs/locate.js';
69
80
  import { hostLeaseBaseDir } from '../../pod-lease/host-lease.js';
70
81
  import { acquireLockAsync } from '../../storage/file-lock.js';
71
- import { debug } from '../../debug/log.js';
82
+ import { debug, redactSecretText } from '../../debug/log.js';
72
83
  import { enterJourneyNode, exitJourneyNode, passJourneyNode } from '../../self-dev/graph-journey-nodes.js';
73
84
  import { routeJourneyEdge } from '../../self-dev/graph-journey-route.js';
74
85
  import { selectLiveDetail } from '../../live/detail-switch.js';
@@ -94,7 +105,7 @@ import { ZAI_POD_EGRESS_LABEL, checkZaiPodSecret, countActiveZaiJobs, decideZaiP
94
105
  import { ZAI_CODING_DEFAULT_MODEL, ZAI_OPENROUTER_FALLBACK_MODEL, zaiSettingsFromConfig, type ZaiCodingPlanSettings } from '../../llm/zai-coding-plan.js';
95
106
  import { normalizePlanningFallbackReason, selectEnhanceLlm } from '../../prompt-enhance/enhance.js';
96
107
  import { inferProviderFromModel } from '../../llm.js';
97
- import { classifyPodExit, extractPodFailureReason, podTerminalRow, type PodExitClass } from './pod-failure-reason.js';
108
+ import { classifyPodExit, extractPodFailureReason, podChildFailureAttribution, podTerminalRow, POD_TERMINATION_JOB_JSONPATH, POD_TERMINATION_PODS_JSONPATH, readPodTermination, type PodExitClass, type PodTermination } from './pod-failure-reason.js';
98
109
  import { kubectlTransientReason, retryKubectlTransient } from './kubectl-transient-retry.js';
99
110
  import { OLD_DOOR_STAMP_ENV } from '../../self-dev/old-door.js';
100
111
  import { resolveDefaultSourceHead, seedMirrorOnce, type MirrorSeedResult } from './pod-mirror-seed.js';
@@ -102,7 +113,24 @@ import { decideWarmReuse, type WarmReuseEvidenceByConsumer, type WarmReuseSwitch
102
113
  import type { WarmWorkerCommand } from './pod-warm-worker-protocol.js';
103
114
  import type { WarmPodTransportResult } from './pod-warm-worker-pod.js';
104
115
 
105
- export type Kubectl = (args: readonly string[], input?: string) => { status: number | null; stdout: string; stderr: string };
116
+ export type Kubectl = (args: readonly string[], input?: string, timeoutMs?: number) => { status: number | null; stdout: string; stderr: string };
117
+
118
+ /** TC's si-v1 transport supplies this receipt. No lease is considered reusable from a Job exit alone. */
119
+ export interface SiWarmChildAttempt {
120
+ leaseFenced: boolean;
121
+ command: SiWarmCommand | null;
122
+ /** The consumer executes this fenced transport. A provider cannot assert its own reply verdict. */
123
+ transport: { pod: SiWarmPodIdentity; runner: SiWarmFixedRunner; now?: () => number };
124
+ payload: Uint8Array;
125
+ credentialBundle: Uint8Array;
126
+ /** Collect verified ledger/artifact bytes into the existing host stores. */
127
+ adopt: (command: SiWarmCommand, reply: SiWarmReply, bytes: SiWarmReturnedBytes) => Promise<void>;
128
+ cleanup: ChildPodReuseOperations;
129
+ /** The provider fences a worker even when a transport failure hides its command receipt. */
130
+ quarantineUnknown: (reason: string) => Promise<void>;
131
+ }
132
+ type OpenPrByBranch = { status: 'found'; prNumber: number; prUrl: string }
133
+ | { status: 'not-found' | 'failed'; reason: string };
106
134
 
107
135
  /** TC supplies the validated candidate/lease transport. UX only forwards its request command. */
108
136
  export type WarmPodRequestHook = (request: { command: WarmWorkerCommand }) => Promise<WarmPodTransportResult> | WarmPodTransportResult;
@@ -118,6 +146,13 @@ const APP_GH_AUTH_COMMAND = `bun -e '${APP_GH_AUTH_SCRIPT}'`;
118
146
  const APP_GH_EXEC = `unset GH_TOKEN GITHUB_TOKEN; GH_CONFIG_DIR="${APP_GH_CONFIG_PATH}" exec "$@"`;
119
147
 
120
148
  export interface PodSpawnOptions {
149
+ /** si-v1 living worker consumer. Absent means the existing one-shot Job path. */
150
+ siWarmAttempt?: (input: Parameters<SelfImplementJobSpawn>[0]) => Promise<SiWarmChildAttempt | null>;
151
+ /** POD-REUSE-SI-PROVIDER-WIRE: must apply cold-path host regate, ACP, terminal and repair checks before warm adoption. */
152
+ siWarmHostPostprocess?: (result: SelfImplementJobDone, attempt: SiWarmChildAttempt,
153
+ input: Parameters<SelfImplementJobSpawn>[0]) => Promise<SelfImplementJobDone>;
154
+ /** Injected test ledger; production writes to the existing run ledger. */
155
+ siWarmLedger?: (entry: RunLedgerEntry) => void;
121
156
  /** 이미지 판(라벨 elanous.commit) — 주입(시험·감독기가 이미 잰 값). 없으면 docker 로 잰다. */
122
157
  imageCommit?: string | null;
123
158
  /** codex 계정 이름(`~/.elanous/auth.json` 의 `openai-codex:<이름>`). 기본 team. */
@@ -169,6 +204,8 @@ export interface PodSpawnOptions {
169
204
  authorConfig?: () => UserConfig;
170
205
  /** Host fallback chain seam; a Pod has no access to the host config. */
171
206
  fallbackConfig?: () => UserConfig;
207
+ /** Host OpenGateway key config seam; unit tests inject isolated config without bypassing production reads. */
208
+ openGatewayConfig?: () => UserConfig;
172
209
  /** 원천 — 없으면 종전 `git clone --depth 50`. bundle 이면 apply 뒤 kubectl cp 로 싣는다. */
173
210
  source?: PodSource;
174
211
  /** Existing PR repair: pin the source head and export a commit bundle instead of opening a PR. */
@@ -205,6 +242,8 @@ export interface PodSpawnOptions {
205
242
  taskAgentLiveMoves?: () => Promise<ReadonlySet<string>> | ReadonlySet<string>;
206
243
  /** PR comment for host-regate failures that never reached runHostRegate (injectable for tests). */
207
244
  ghComment?: (prNumber: number, body: string) => void;
245
+ /** Read-only host gh lookup; injected fixtures can stand in for gh without creating or closing a PR. */
246
+ lookupOpenPrByBranch?: (branch: string, repoUrl: string) => Promise<OpenPrByBranch> | OpenPrByBranch;
208
247
  draftResidue?: (record: PrTerminalRecord, ctx: DraftResidueContext) => Promise<void> | void;
209
248
  /** 🔑 Pod 필수 스킬의 키(.env)를 이 런의 Secret 으로 넘긴다 — 명시 opt-in(유료 크레딧을 쓴다). */
210
249
  skillEnv?: boolean;
@@ -397,6 +436,11 @@ export async function podCodexCredentialWithPrerefresh(account: string, deps: {
397
436
  // ⑴ 잠금을 기다리는 사이 동료 런이 갱신했으면 그대로 쓴다 — 두 번째 갱신이 회전된 토큰으로 실패하는 자리.
398
437
  const first = reread();
399
438
  if (!('expiringH' in first)) { observe('refresh-skipped-already-fresh', null); return first; }
439
+ // The lease can transfer while an account lock is queued. Recheck inside the
440
+ // critical section, immediately before the only refresh-token operation.
441
+ let stillHolder = false;
442
+ try { stillHolder = await deps.isLeaseHolder(); } catch { stillHolder = false; }
443
+ if (!stillHolder) { observe('no-lease', null, 'lease-lost-before-refresh'); return null; }
400
444
  let refreshed: Awaited<ReturnType<typeof deps.refresh>> | null = null;
401
445
  let failReason = 'threw';
402
446
  try { refreshed = await deps.refresh(account); }
@@ -430,6 +474,7 @@ export async function podCodexCredentialWithPrerefresh(account: string, deps: {
430
474
 
431
475
  /** 기본 임대 판정 — 기존 HQ 펜스(`ledger-cli` 역할 · fail-open 없음)를 그대로 쓴다. */
432
476
  async function defaultHqLeaseHolder(): Promise<boolean> {
477
+ if (process.env.ELANOUS_REMOTE_CODEX_ACCESS_ONLY === '1') return false;
433
478
  const { hqFenceDecision } = await import('../../hq/hq.js');
434
479
  return hqFenceDecision('ledger-cli').run;
435
480
  }
@@ -649,20 +694,45 @@ function githubRepositoryFromUrl(repoUrl: string): string | null {
649
694
  return owner && name && ![owner, name].some((part) => part === '.' || part === '..') ? `${owner}/${name}` : null;
650
695
  }
651
696
 
652
- /** GROK-CHILD-GIT-GUARD — the Pod holds a GitHub write token and our repo cannot protect main (private free plan · API 403),
653
- * so a child tool could `git push origin main`, force-push another run's branch or delete remote branches. A global
654
- * pre-push hook lets a Pod push only its own run branches (self-impl/* · salvage/* · bench/<id>/<arm>) and refuses
655
- * everything else, including deletes and the shared bench base. It stops accidental and reset-then-push overwrites;
656
- * it is not a sandbox — credential isolation from child tools is the next slice. */
697
+ /** GROK-CHILD-GIT-GUARD — the Pod holds a GitHub write token and our repo cannot protect main (private free plan · API 403).
698
+ * The global hook refuses main/tags/unknown refs and most deletes. With a runId, self-impl refs use the same six-hex
699
+ * run suffix as planned branch names; only that suffix and this run's disposable PR scratch create/delete pass.
700
+ * A same-card PR reuse push is the one exception: per-command Git config binds its ref, old head and card;
701
+ * the hook compares the Pod card and proves fast-forward ancestry before accepting it.
702
+ * salvage/* and bench arm refs keep their existing broader rules. A six-hex collision remains possible, so this is
703
+ * an accidental overwrite guard, not a security isolation boundary; child credential isolation is a separate slice. */
657
704
  export const POD_GIT_HOOKS_DIR = '$HOME/.elanous-git-hooks';
658
- export function podGitPushGuardScript(): string {
705
+ export function podGitPushGuardScript(runId?: string): string {
706
+ const runKey = runId ? runSuffixKey(runId) : null;
707
+ const zeroSha = '0'.repeat(40);
659
708
  return [
660
709
  `mkdir -p "${POD_GIT_HOOKS_DIR}" && cat > "${POD_GIT_HOOKS_DIR}/pre-push" <<'ELANOUS_PRE_PUSH'`,
661
710
  '#!/bin/sh',
662
711
  'while read -r lref lsha rref rsha; do',
663
- ' case "$lsha" in *[!0]*) ;; *) echo "[pod] git-guard: refused remote delete $rref" >&2; exit 1 ;; esac',
712
+ ' case "$lsha" in *[!0]*) ;; *)',
713
+ ...(runKey ? [
714
+ ` case "$rref" in refs/heads/self-impl/pr-create-r${runKey}-*) ;; *) echo "[pod] git-guard: refused remote delete $rref" >&2; exit 1 ;; esac ;;`,
715
+ ] : [' echo "[pod] git-guard: refused remote delete $rref" >&2; exit 1 ;;']),
716
+ ' esac',
664
717
  ' case "$rref" in',
665
- ' refs/heads/self-impl/*|refs/heads/salvage/*) ;;',
718
+ ...(runKey ? [
719
+ ` refs/heads/self-impl/pr-create-r${runKey}-*)`,
720
+ ` case "$lsha:$rsha" in ${zeroSha}:*|*:${zeroSha}) ;; *) echo "[pod] git-guard: refused scratch ref update $rref" >&2; exit 1 ;; esac ;;`,
721
+ ` refs/heads/self-impl/*-r${runKey}|refs/heads/self-impl/*-r${runKey}-early) ;;`,
722
+ ' refs/heads/self-impl/*)',
723
+ ' reuse_ref=$(git config --get elanous.sameCellReuseRef 2>/dev/null || true)',
724
+ ' reuse_head=$(git config --get elanous.sameCellReuseHead 2>/dev/null || true)',
725
+ ' reuse_card=$(git config --get elanous.sameCellReuseCard 2>/dev/null || true)',
726
+ ' if [ -z "$reuse_ref" ]; then echo "[pod] git-guard: refused push to $rref (another run)" >&2; exit 1; fi',
727
+ ' if [ "$rref" != "$reuse_ref" ]; then echo "[pod] git-guard: refused same-cell ref-mismatch $rref" >&2; exit 1; fi',
728
+ ' if [ -z "${ELANOUS_TASK_CARD_ID:-}" ] || [ "$reuse_card" != "$ELANOUS_TASK_CARD_ID" ]; then echo "[pod] git-guard: refused same-cell card-mismatch $rref" >&2; exit 1; fi',
729
+ ' if [ -z "$reuse_head" ] || [ "$rsha" != "$reuse_head" ]; then echo "[pod] git-guard: refused same-cell remote-head-mismatch $rref" >&2; exit 1; fi',
730
+ ' if ! git merge-base --is-ancestor "$rsha" "$lsha" 2>/dev/null; then echo "[pod] git-guard: refused same-cell non-fast-forward $rref" >&2; exit 1; fi',
731
+ ' echo "[pod] git-guard: allowed same-cell fast-forward $rref" >&2 ;;',
732
+ ] : [
733
+ ...(runId ? [' refs/heads/self-impl/*) echo "[pod] git-guard: refused push to $rref (invalid run id)" >&2; exit 1 ;;'] : [' refs/heads/self-impl/*) ;;']),
734
+ ]),
735
+ ' refs/heads/salvage/*) ;;',
666
736
  ' refs/heads/bench/*/base) echo "[pod] git-guard: refused push to shared bench base $rref" >&2; exit 1 ;;',
667
737
  ' refs/heads/bench/*/*) ;;',
668
738
  ' *) echo "[pod] git-guard: refused push to $rref (only this run\'s branches)" >&2; exit 1 ;;',
@@ -963,7 +1033,7 @@ export function podMemoryLimitFor(feature: string, env: NodeJS.ProcessEnv | Read
963
1033
  }
964
1034
 
965
1035
  /** A named child provider decides the Pod provider; paid OpenRouter requires an explicit key and model. */
966
- export function podNamedChildProvider(named: string | undefined, credential: { grokSubscription: boolean; grokApiKey: boolean; openrouterKey?: boolean; anthropicKey?: boolean }, childModel?: string): { provider?: PodChildProvider; refuse?: string } {
1036
+ export function podNamedChildProvider(named: string | undefined, credential: { grokSubscription: boolean; grokApiKey: boolean; openrouterKey?: boolean; openGatewayKey?: boolean; anthropicKey?: boolean }, childModel?: string): { provider?: PodChildProvider; refuse?: string } {
967
1037
  // 공백·빈 이름 = «미지정»(종전 기본 경로) — 유료 provider 로 흘러가지 않는다(codex 재리뷰 must-fix).
968
1038
  if (!named?.trim()) return {};
969
1039
  // ⭐ 목록은 한 곳(pod-child-providers.ts) — 발사 관문도 같은 상수·같은 문면으로 거부한다.
@@ -978,6 +1048,12 @@ export function podNamedChildProvider(named: string | undefined, credential: { g
978
1048
  if (!childModel?.trim()) return { refuse: 'pod: openrouter 자식 모델 없음 — --child-llm-model 필요' };
979
1049
  return { provider: 'openrouter' };
980
1050
  }
1051
+ if (named === 'opengateway') {
1052
+ if (!credential.openGatewayKey) return { refuse: 'pod: Pod 에 opengateway 키 없음 — 호스트 키 캐시 또는 OPENGATEWAY_API_KEY 필요' };
1053
+ if (!childModel?.trim()) return { refuse: 'pod: opengateway 자식 모델 없음 — --child-llm-model 필요' };
1054
+ if (!isVerifiedOpenGatewayModel(childModel.trim())) return { refuse: `pod: opengateway model not verified: ${childModel.trim()}` };
1055
+ return { provider: 'opengateway' };
1056
+ }
981
1057
  // BEDROCK-PROVIDER ② — 자격은 클러스터의 이름 있는 Secret 에서 온다. 있나·키가 갖춰졌나는 Pod 발사 «전»(apply 0)에 판정한다(checkBedrockPodSecret).
982
1058
  if (named === 'bedrock') return { provider: 'bedrock' };
983
1059
  // ZAI-POD-CHILD — glm 도 같은 결: 자격은 클러스터의 이름 있는 ZAI Secret · 있나·동시 상한은 Pod 발사 «전»(apply 0)에 판정한다(pod-zai-child.ts).
@@ -1012,7 +1088,8 @@ export function podChildLlmArgs(options: Pick<PodSpawnOptions, 'provider' | 'chi
1012
1088
  const provider = options.provider === 'openai-codex' && !options.childProviderExplicit ? undefined : options.provider;
1013
1089
  if (!provider) return [];
1014
1090
  const model = options.childModel?.trim() || (provider === 'grok' ? defaultGrokModel().id : provider === 'glm' ? ZAI_CODING_DEFAULT_MODEL : undefined);
1015
- if (provider === 'openrouter' && !model) throw new Error('pod: openrouter 자식 모델 없음 — --child-llm-model 필요');
1091
+ if ((provider === 'openrouter' || provider === 'opengateway') && !model) throw new Error(`pod: ${provider} 자식 모델 없음 — --child-llm-model 필요`);
1092
+ if (provider === 'opengateway' && !isVerifiedOpenGatewayModel(model!)) throw new Error(`pod: opengateway model not verified: ${model}`);
1016
1093
  // `self implement` registers no --child-llm-effort — passing it kills the Pod at argument parsing, so effort stays host-only.
1017
1094
  return ['--child-llm-provider', provider, ...(model ? ['--child-llm-model', model] : [])];
1018
1095
  }
@@ -1069,7 +1146,7 @@ export function podSigtermTrapScript(seqFile: string = POD_USAGE_PARTIAL_SEQ_FIL
1069
1146
  return `trap '{ [ -n "$si_pid" ] && kill "$si_pid" 2>/dev/null || true; [ -n "\${usage_partial_pid:-}" ] && kill "$usage_partial_pid" 2>/dev/null || true; si_seq=$(cat ${seqFile} 2>/dev/null || true); case "$si_seq" in ""|*[!0-9]*) si_seq=0;; esac; si_seq=$((10#$si_seq + 1)); { [ -f scripts/usage-rollup.ts ] && bun scripts/usage-rollup.ts --since 12h --partial --seq "$si_seq"; } & si_rollup_pid=$!; for si_i in 1 2 3 4 5 6 7 8; do kill -0 "$si_rollup_pid" 2>/dev/null || break; sleep 1; done; kill "$si_rollup_pid" 2>/dev/null || true; wait "$si_rollup_pid" 2>/dev/null || true; exit 143; }' TERM`;
1070
1147
  }
1071
1148
 
1072
- export function podJobManifest(o: { name: string; namespace: string; image: string; workIdentity?: PodWorkIdentity; boundPrNumber?: number; /** 레지스트리 이미지면 IfNotPresent(노드가 pull) · 반입 이미지면 Never. */ imagePullPolicy?: 'Never' | 'IfNotPresent'; repoUrl: string; source?: PodSource; repairCommitOnly?: { pr: number; baseHead: string }; hostMirror?: string; args: readonly string[]; passEnv: readonly string[]; deadlineSeconds: number; runId?: string; parentRunId?: string; armEnv?: Readonly<Record<string, string>>; hostId?: string; imageCommit?: string | null; skillEnvs?: readonly string[]; memoryLimit?: string; memoryRequest?: string; goalDoc?: string; /** AUTHOR-POD2 — run `harness say` on /creds/feature inside the Pod (authoring happens off the host). */ authorSentence?: boolean; authorGrade?: 'full' | 'lite'; grokCredential?: 'subscription' | 'api_key'; codexAccounts?: readonly string[]; appCredential?: boolean; /** Test seam — default POD_GH_STALE_SECONDS. */ githubStaleSeconds?: number; /** Which goal execution and attempt this Job is — a resumed host verifies it before following the Job (POD9). */ execution?: { key: string; attempt: number }; hostLeaseAdmitted?: boolean; /** POD-ROLE-LLM — OpenRouter 자식 Pod 에 리뷰어용 codex 자격을 깐다. */ reviewerCodexAuth?: boolean; /** BEDROCK-PROVIDER ② — bedrock 자식만: 이름 있는 AWS Secret 의 키(secretKeyRef) ⊕ 리전 ⊕ 이그레스 라벨. */ bedrock?: BedrockPodSpec; /** ZAI-POD-CHILD — glm(ZAI) 자식만: 이름 있는 ZAI Secret 의 키(secretKeyRef) ⊕ 이그레스·동시 상한 라벨(Pod ⊕ Job). */ zai?: ZaiPodSpec; /** Codex Job may use the same named Secret only for an explicitly admitted ZAI fallback. */ zaiFallback?: ZaiPodSpec; fallbackChain?: { steps: readonly FallbackStep[]; source: 'host' | 'default' }; /** TA-POD-WARMBOOT-PARITY — the child's final line (podResultMarkerShellLine), just before `exit $rc`. */ resultMarkerLine?: string }): Record<string, unknown> {
1149
+ export function podJobManifest(o: { name: string; namespace: string; image: string; workIdentity?: PodWorkIdentity; boundPrNumber?: number; /** 레지스트리 이미지면 IfNotPresent(노드가 pull) · 반입 이미지면 Never. */ imagePullPolicy?: 'Never' | 'IfNotPresent'; repoUrl: string; source?: PodSource; repairCommitOnly?: { pr: number; baseHead: string }; hostMirror?: string; args: readonly string[]; passEnv: readonly string[]; deadlineSeconds: number; runId?: string; parentRunId?: string; armEnv?: Readonly<Record<string, string>>; hostId?: string; imageCommit?: string | null; skillEnvs?: readonly string[]; memoryLimit?: string; memoryRequest?: string; goalDoc?: string; /** AUTHOR-POD2 — run `harness say` on /creds/feature inside the Pod (authoring happens off the host). */ authorSentence?: boolean; authorGrade?: 'full' | 'lite'; grokCredential?: 'subscription' | 'api_key'; codexAccounts?: readonly string[]; appCredential?: boolean; /** Test seam — default POD_GH_STALE_SECONDS. */ githubStaleSeconds?: number; /** Which goal execution and attempt this Job is — a resumed host verifies it before following the Job (POD9). */ execution?: { key: string; attempt: number }; hostLeaseAdmitted?: boolean; /** POD-ROLE-LLM — OpenRouter 자식 Pod 에 리뷰어용 codex 자격을 깐다. */ reviewerCodexAuth?: boolean; /** BEDROCK-PROVIDER ② — bedrock 자식만: 이름 있는 AWS Secret 의 키(secretKeyRef) ⊕ 리전 ⊕ 이그레스 라벨. */ bedrock?: BedrockPodSpec; /** ZAI-POD-CHILD — glm(ZAI) 자식만: 이름 있는 ZAI Secret 의 키(secretKeyRef) ⊕ 이그레스·동시 상한 라벨(Pod ⊕ Job). */ zai?: ZaiPodSpec; /** Codex Job may use the same named Secret only for an explicitly admitted ZAI fallback. */ zaiFallback?: ZaiPodSpec; fallbackChain?: { steps: readonly FallbackStep[]; source: 'host' | 'default' }; /** TA-POD-WARMBOOT-PARITY — the child's final line (podResultMarkerShellLine), just before `exit $rc`. */ resultMarkerLine?: string; /** POD-LAUNCH-PROVENANCE — the shared provenance set (labels ⊕ annotations ⊕ child env) for this si-task Job. */ provenance?: PodProvenanceMetadata }): Record<string, unknown> {
1073
1150
  if (o.repairCommitOnly && (o.source?.kind !== 'pr' || o.source.number !== o.repairCommitOnly.pr || !/^[0-9a-f]{40}$/.test(o.repairCommitOnly.baseHead) || o.args.includes('--open-pr') || o.args.includes('--merge-by-host') || o.args.includes('--auto-merge'))) {
1074
1151
  throw new Error('repair commit-only requires matching PR source and no PR/merge flags');
1075
1152
  }
@@ -1078,6 +1155,8 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
1078
1155
  const childModelIndex = o.args.indexOf('--child-llm-model');
1079
1156
  const openrouterModel = childProviderIndex >= 0 && o.args[childProviderIndex + 1] === 'openrouter' && childModelIndex >= 0
1080
1157
  ? o.args[childModelIndex + 1] : undefined;
1158
+ const openGatewayModel = childProviderIndex >= 0 && o.args[childProviderIndex + 1] === 'opengateway' && childModelIndex >= 0
1159
+ ? o.args[childModelIndex + 1] : undefined;
1081
1160
  // POD-ANTHROPIC-PROVIDER — anthropic 자식은 키(env · secretKeyRef)만 쓴다. codex·grok 자격 파일을 깔지 않는다.
1082
1161
  const anthropicChild = childProviderIndex >= 0 && o.args[childProviderIndex + 1] === 'anthropic';
1083
1162
  const goalPath = o.goalDoc ? `'${(o.goalDoc.startsWith('-') ? `./${o.goalDoc}` : o.goalDoc).replace(/'/g, `'\\''`)}'` : undefined;
@@ -1101,7 +1180,9 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
1101
1180
  'mkdir -p "$HOME/.elanous" && install -m 600 /creds/llm-fallback-config.json "$HOME/.elanous/config.json" || exit 6',
1102
1181
  `echo "ELANOUS_POD_FALLBACK_CHAIN source=${o.fallbackChain.source} steps=${o.fallbackChain.steps.join(',')}"`,
1103
1182
  ] : []),
1104
- openrouterModel
1183
+ openGatewayModel
1184
+ ? 'mkdir -p ~/.elanous && export ELANOUS_LLM_PROVIDER=opengateway'
1185
+ : openrouterModel
1105
1186
  ? o.reviewerCodexAuth
1106
1187
  ? 'mkdir -p ~/.elanous ~/.codex && cp /creds/elanous-auth.json ~/.elanous/auth.json && cp /creds/codex-auth.json ~/.codex/auth.json && chmod 600 ~/.elanous/auth.json ~/.codex/auth.json && export ELANOUS_LLM_PROVIDER=openrouter'
1107
1188
  : 'mkdir -p ~/.elanous && export ELANOUS_LLM_PROVIDER=openrouter'
@@ -1120,7 +1201,7 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
1120
1201
  // 🔑 스킬 키(.env) — 이미지엔 없다. 이 런의 Secret 에서 각 스킬 폴더로 0600 복사(값은 로그에 안 나온다).
1121
1202
  ...(o.skillEnvs?.length ? [`for n in ${o.skillEnvs.join(' ')}; do [ -d ~/.claude/skills/$n ] && install -m 600 /creds/skillenv-$n ~/.claude/skills/$n/.env; done; echo "[pod] skill env: ${o.skillEnvs.join(',')}"`] : []),
1122
1203
  'git config --global user.name "elanous pod child" && git config --global user.email "noreply@anthropic.com" && gh auth setup-git',
1123
- podGitPushGuardScript(),
1204
+ podGitPushGuardScript(o.runId),
1124
1205
  'curl -s -m 3 -o /dev/null http://host.orb.internal:31415/health && { echo "[pod] ISOLATION FAIL"; exit 3; }',
1125
1206
  // SI-POD-STAGE-MARKERS — Pod 시계 단계 표지(`ELANOUS_POD_STAGE <stage> <start|end> <epoch초>`): 호스트가 Job 로그에서 읽어
1126
1207
  // pod.lifecycle stages 의 clone · mirror · run 을 채운다. 실패한 단계는 끝 표지가 없다(→ missing · 지어내지 않는다).
@@ -1132,6 +1213,9 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
1132
1213
  // Record that fact before the main command, without treating clone completion as install completion.
1133
1214
  `printf 'ELANOUS_POD_DEPS skipped %s image-prepared-no-install\\n' "$(date +%s)" || true`,
1134
1215
  ...(o.repairCommitOnly ? [`[ "$(git rev-parse HEAD)" = '${o.repairCommitOnly.baseHead}' ] || { echo 'ELANOUS_POD_REPAIR_FAILED source-head-mismatch'; exit 8; }`] : []),
1216
+ // PR checkout and, when requested, the repair HEAD pin have passed before this child starts.
1217
+ // A supervisor input may continue the repair, but cannot launch its source again.
1218
+ ...(o.source?.kind === 'pr' ? ['export ELANOUS_POD_VERIFIED_PR_SOURCE=1'] : []),
1135
1219
  ...(goalPath ? [
1136
1220
  `mkdir -p -- "$(dirname -- ${goalPath})" && cp -- /creds/goal-doc ${goalPath} || exit 6`,
1137
1221
  ] : []),
@@ -1150,7 +1234,7 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
1150
1234
  // AUTHOR-LITE2-POD — 등급은 환경변수 `ELANOUS_AUTHOR_GRADE` 로 싣는다(옛 Pod elanous 는 모르는 환경변수를 무시하고 full 로 돈다).
1151
1235
  siTaskPodStageShell('run', 'start'),
1152
1236
  o.repairCommitOnly
1153
- ? `export ${OLD_DOOR_STAMP_ENV}=self-implement; elanous self implement "$(cat /creds/feature)" --json --no-open-pr ${quoted} > /tmp/si.out 2>&1 & si_pid=$!; wait "$si_pid"; rc=$?; ${RUN_END}; if [ "$rc" -eq 0 ]; then elanous self repair-commit-export '${o.repairCommitOnly.pr}' '${o.repairCommitOnly.baseHead}' /tmp/si.out "$PWD" "$HOME/outbox" || rc=8; fi`
1237
+ ? `export ${OLD_DOOR_STAMP_ENV}=self-implement; elanous self implement "$(cat /creds/feature)" --json --no-open-pr ${quoted} > /tmp/si.out 2>&1 & si_pid=$!; wait "$si_pid"; rc=$?; ${RUN_END}; if elanous self repair-commit-export '${o.repairCommitOnly.pr}' '${o.repairCommitOnly.baseHead}' /tmp/si.out "$PWD" "$HOME/outbox"; then rc=0; elif [ "$rc" -eq 0 ]; then rc=8; fi`
1154
1238
  : o.authorSentence
1155
1239
  ? `echo "ELANOUS_AUTHOR_ON_POD started host=$(hostname) at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"; author_t0=$(date +%s); ${o.authorGrade === 'lite' || o.authorGrade === 'full' ? `ELANOUS_AUTHOR_GRADE=${o.authorGrade} ` : ''}elanous harness say --substrate local --json${quotedAsk}${quotedChildLlm} -- "$(cat /creds/feature)" > /tmp/si.out 2>&1 & si_pid=$!; wait "$si_pid"; rc=$?; ${RUN_END}; echo "ELANOUS_AUTHOR_ON_POD finished host=$(hostname) rc=$rc seconds=$(( $(date +%s) - author_t0 ))"; ${POD_RESULT_FLATTEN}`
1156
1240
  : goalPath
@@ -1225,15 +1309,15 @@ if [ "$found" -eq 0 ]; then echo ELANOUS_RUN_LEDGER_NONE; fi`,
1225
1309
  return {
1226
1310
  apiVersion: 'batch/v1',
1227
1311
  kind: 'Job',
1228
- metadata: { name: o.name, namespace: o.namespace, labels: { 'elanous.substrate': 'pod', 'elanous.job': o.name, ...podRunLabels(o), ...(o.workIdentity ? podWorkLabels(o.workIdentity) : {}), ...(o.zai || o.zaiFallback ? { [ZAI_POD_EGRESS_LABEL]: 'true' } : {}) },
1229
- ...((o.execution || o.hostLeaseAdmitted) ? { annotations: { ...(o.execution ? { [POD_EXECUTION_KEY_ANNOTATION]: o.execution.key, [POD_ATTEMPT_ANNOTATION]: String(o.execution.attempt) } : {}), ...(o.hostLeaseAdmitted ? { [POD_HOST_LEASE_ANNOTATION]: 'true' } : {}) } } : {}) },
1312
+ metadata: { name: o.name, namespace: o.namespace, labels: { 'elanous.substrate': 'pod', 'elanous.job': o.name, ...podRunLabels(o), ...(o.workIdentity ? podWorkLabels(o.workIdentity) : {}), ...(o.zai || o.zaiFallback ? { [ZAI_POD_EGRESS_LABEL]: 'true' } : {}), ...o.provenance?.labels },
1313
+ ...((o.execution || o.hostLeaseAdmitted || o.provenance) ? { annotations: { ...o.provenance?.annotations, ...(o.execution ? { [POD_EXECUTION_KEY_ANNOTATION]: o.execution.key, [POD_ATTEMPT_ANNOTATION]: String(o.execution.attempt) } : {}), ...(o.hostLeaseAdmitted ? { [POD_HOST_LEASE_ANNOTATION]: 'true' } : {}) } } : {}) },
1230
1314
  spec: {
1231
1315
  backoffLimit: 0,
1232
1316
  ttlSecondsAfterFinished: POD_JOB_TTL_AFTER_FINISHED_SECONDS,
1233
1317
  activeDeadlineSeconds: o.deadlineSeconds,
1234
1318
  template: {
1235
1319
  // ⭐ Pod 라벨 — local 팔만 `elanous.egress/local-llm` 을 단다. docker/h1/policy-local-llm.yaml 이 그 라벨에만 호스트 LLM 포트 «하나»를 연다.
1236
- metadata: { labels: { 'elanous.job': o.name, ...podRunLabels(o), ...(o.workIdentity ? podWorkLabels(o.workIdentity) : {}), ...(o.armEnv?.ELANOUS_LLM_PROVIDER === 'local' ? { 'elanous.egress/local-llm': 'true' } : {}), ...(o.bedrock ? { [BEDROCK_POD_EGRESS_LABEL]: 'true' } : {}), ...(o.zai || o.zaiFallback ? { [ZAI_POD_EGRESS_LABEL]: 'true' } : {}) } },
1320
+ metadata: { labels: { 'elanous.job': o.name, ...podRunLabels(o), ...(o.workIdentity ? podWorkLabels(o.workIdentity) : {}), ...(o.armEnv?.ELANOUS_LLM_PROVIDER === 'local' ? { 'elanous.egress/local-llm': 'true' } : {}), ...(o.bedrock ? { [BEDROCK_POD_EGRESS_LABEL]: 'true' } : {}), ...(o.zai || o.zaiFallback ? { [ZAI_POD_EGRESS_LABEL]: 'true' } : {}), ...o.provenance?.labels }, ...(o.provenance ? { annotations: o.provenance.annotations } : {}) },
1237
1321
  spec: {
1238
1322
  restartPolicy: 'Never',
1239
1323
  securityContext: { runAsUser: 1000, fsGroup: 1000 },
@@ -1260,6 +1344,8 @@ if [ "$found" -eq 0 ]; then echo ELANOUS_RUN_LEDGER_NONE; fi`,
1260
1344
  { name: 'ELANOUS_POD_NAMESPACE', valueFrom: { fieldRef: { fieldPath: 'metadata.namespace' } } },
1261
1345
  ...(o.hostId ? [{ name: 'ELANOUS_HOST_ID', value: o.hostId }] : []),
1262
1346
  ...(o.imageCommit ? [{ name: 'ELANOUS_IMAGE_COMMIT', value: o.imageCommit }] : []),
1347
+ // POD-LAUNCH-PROVENANCE — the child knows who launched it, why and when (ELANOUS_POD_<key> · no secrets).
1348
+ ...(o.provenance?.env ?? []),
1263
1349
  // ⭐ 런 계약(graph-run-contract.ts) — 이 칸이 «실제로» Pod 이고, 부모가 정한 계약도 Pod 다. 안의 그래프가 노드마다 대조한다.
1264
1350
  { name: ACTUAL_SUBSTRATE_ENV, value: 'pod' }, { name: RUN_CONTRACT_ENV, value: carryRunContract({ substrate: 'pod' }) },
1265
1351
  ...Object.entries(o.armEnv ?? {}).map(([name, value]) => ({ name, value })),
@@ -1291,6 +1377,93 @@ function defaultGhComment(prNumber: number, body: string): void {
1291
1377
  if (r.status !== 0) throw new Error((r.stderr || r.error?.message || `gh exited ${r.status}`).trim().slice(0, 200));
1292
1378
  }
1293
1379
 
1380
+ function defaultLookupOpenPrByBranch(branch: string, repoUrl: string): OpenPrByBranch {
1381
+ const repo = /^https:\/\/github\.com\/([\w.-]+\/[\w.-]+?)(?:\.git)?\/?$/u.exec(repoUrl)?.[1];
1382
+ if (!repo) return { status: 'failed', reason: 'repository-unrecognized' };
1383
+ if (!branch || !/^[A-Za-z0-9._/-]+$/u.test(branch) || branch.includes('..')) return { status: 'failed', reason: 'branch-invalid' };
1384
+ const result = spawnSync('gh', ['pr', 'list', '--repo', repo, '--state', 'open', '--head', branch,
1385
+ '--json', 'number,url,headRefName,isCrossRepository', '--limit', '100'], { encoding: 'utf8', timeout: 10_000, maxBuffer: 1_048_576 });
1386
+ if (result.status !== 0) return { status: 'failed', reason: result.error ? 'gh-list-unavailable' : `gh-list-rc-${result.status ?? 'null'}` };
1387
+ let rows: unknown;
1388
+ try { rows = JSON.parse(result.stdout); } catch { return { status: 'failed', reason: 'gh-json-invalid' }; }
1389
+ if (!Array.isArray(rows)) return { status: 'failed', reason: 'gh-json-invalid' };
1390
+ if (rows.some((row) => row === null || typeof row !== 'object'
1391
+ || typeof (row as { number?: unknown }).number !== 'number'
1392
+ || !Number.isSafeInteger((row as { number: number }).number)
1393
+ || typeof (row as { url?: unknown }).url !== 'string'
1394
+ || typeof (row as { headRefName?: unknown }).headRefName !== 'string'
1395
+ || typeof (row as { isCrossRepository?: unknown }).isCrossRepository !== 'boolean'))
1396
+ return { status: 'failed', reason: 'gh-json-invalid' };
1397
+ if (rows.length >= 100) return { status: 'failed', reason: 'gh-result-limit-reached' };
1398
+ const matching = rows.filter((row): row is { number: number; url: string; headRefName: string; isCrossRepository: false } => {
1399
+ if (row === null || typeof row !== 'object') return false;
1400
+ const candidate = row as { number?: unknown; url?: unknown; headRefName?: unknown; isCrossRepository?: unknown };
1401
+ return candidate.headRefName === branch && candidate.isCrossRepository === false && typeof candidate.number === 'number'
1402
+ && Number.isSafeInteger(candidate.number) && candidate.number > 0 && typeof candidate.url === 'string';
1403
+ });
1404
+ if (matching.length === 0) return { status: 'not-found', reason: rows.some((row) =>
1405
+ (row as { headRefName: string }).headRefName === branch && (row as { isCrossRepository: boolean }).isCrossRepository)
1406
+ ? 'cross-repository-only' : 'no-open-pr-for-branch' };
1407
+ if (matching.length !== 1) return { status: 'failed', reason: 'ambiguous-open-pr-for-branch' };
1408
+ const row = matching[0]!;
1409
+ if (row.url.replace(/\/$/u, '').toLowerCase() !== `https://github.com/${repo}/pull/${row.number}`.toLowerCase())
1410
+ return { status: 'failed', reason: 'pr-url-mismatch' };
1411
+ return { status: 'found', prNumber: row.number, prUrl: row.url };
1412
+ }
1413
+
1414
+ type PodImageObservation = { podName: string | null; podUid: string | null; podCreatedAt: string | null;
1415
+ imageID: string | null; digest: string | null; digestUnavailable: string | null };
1416
+
1417
+ /** Read the newest Pod owned by this Job. A stale replacement must never be named as the final Pod. */
1418
+ function readJobPodImage(kubectl: Kubectl, namespace: string, job: string, jobUid?: string): PodImageObservation {
1419
+ const unavailable = (reason: string, podName: string | null = null, imageID: string | null = null,
1420
+ podUid: string | null = null, podCreatedAt: string | null = null): PodImageObservation =>
1421
+ ({ podName, podUid, podCreatedAt, imageID, digest: null, digestUnavailable: reason });
1422
+ let result: ReturnType<Kubectl>;
1423
+ try { result = kubectl(['-n', namespace, 'get', 'pods', '-l', `job-name=${job}`, '-o', 'json', '--request-timeout=5s']); }
1424
+ catch { return unavailable('pod-status-threw'); }
1425
+ if (result.status !== 0) return unavailable(`pod-status-rc-${result.status ?? 'null'}`);
1426
+ let rows: unknown;
1427
+ try { rows = (JSON.parse(result.stdout) as { items?: unknown }).items; }
1428
+ catch { return unavailable('pod-status-invalid-json'); }
1429
+ if (!Array.isArray(rows)) return unavailable('pod-status-invalid-items');
1430
+ const owned = rows.filter((row): row is {
1431
+ metadata: { name: string; uid?: string; creationTimestamp?: string; ownerReferences: Array<{ kind: string; name: string; uid?: string }> };
1432
+ status?: { containerStatuses?: Array<{ name?: string; imageID?: string }> };
1433
+ } => {
1434
+ if (!row || typeof row !== 'object') return false;
1435
+ const meta = (row as { metadata?: unknown }).metadata;
1436
+ if (!meta || typeof meta !== 'object') return false;
1437
+ const m = meta as { name?: unknown; ownerReferences?: unknown };
1438
+ if (typeof m.name !== 'string' || !Array.isArray(m.ownerReferences)) return false;
1439
+ return m.ownerReferences.some((owner: unknown) => !!owner && typeof owner === 'object'
1440
+ && (owner as { kind?: unknown }).kind === 'Job' && (owner as { name?: unknown }).name === job
1441
+ && (!jobUid || (owner as { uid?: unknown }).uid === jobUid));
1442
+ }).sort((a, b) => {
1443
+ const at = Date.parse(a.metadata.creationTimestamp ?? '') || 0;
1444
+ const bt = Date.parse(b.metadata.creationTimestamp ?? '') || 0;
1445
+ return bt - at || b.metadata.name.localeCompare(a.metadata.name);
1446
+ });
1447
+ const pod = owned[0];
1448
+ if (!pod) return unavailable(rows.length ? 'no-job-owned-pod' : 'pod-not-created');
1449
+ const podUid = typeof pod.metadata.uid === 'string' ? pod.metadata.uid : null;
1450
+ const podCreatedAt = typeof pod.metadata.creationTimestamp === 'string' ? pod.metadata.creationTimestamp : null;
1451
+ const child = pod.status?.containerStatuses?.find((entry) => entry.name === 'child');
1452
+ if (!child) return unavailable('child-container-not-started', pod.metadata.name, null, podUid, podCreatedAt);
1453
+ const imageID = typeof child.imageID === 'string' ? child.imageID : null;
1454
+ if (!imageID?.trim()) return unavailable('image-id-missing', pod.metadata.name, imageID, podUid, podCreatedAt);
1455
+ const digest = /(?:^|[@/])(sha256:[0-9a-f]{64})$/iu.exec(imageID.trim())?.[1]?.toLowerCase() ?? null;
1456
+ return { podName: pod.metadata.name, podUid, podCreatedAt, imageID, digest, digestUnavailable: digest ? null : 'nonstandard-image-id' };
1457
+ }
1458
+
1459
+ /** A repair Pod succeeds only with a verified result/bundle, even if its child gate exited 1. */
1460
+ export function repairPodExitCode(input: { state: string; repairCommitOnly: boolean; repairArtifactReady: boolean;
1461
+ disposition?: { stage?: string; ok?: boolean } }): 0 | 1 {
1462
+ if (input.state !== 'complete') return 1;
1463
+ if (input.repairCommitOnly) return input.repairArtifactReady ? 0 : 1;
1464
+ return input.disposition?.stage !== 'host-regate-failed' && input.disposition?.ok !== false ? 0 : 1;
1465
+ }
1466
+
1294
1467
  export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplementJobSpawn {
1295
1468
  if (options.repairCommitOnly && (options.source?.kind !== 'pr' || options.source.number !== options.repairCommitOnly.pr || !/^[0-9a-f]{40}$/.test(options.repairCommitOnly.baseHead))) {
1296
1469
  throw new Error('repair commit-only requires a matching PR source and exact head');
@@ -1306,10 +1479,290 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1306
1479
  if (hostMirror && !isAbsolute(hostMirror)) throw new Error('pod.hostMirror must be an absolute directory path');
1307
1480
  const hostSupervised = options.hostSupervised ?? true;
1308
1481
  const resultMarkerOn = options.resultMarker ?? !options.kubectl;
1482
+ const writeWarmLedger = options.siWarmLedger
1483
+ ?? ((entry: RunLedgerEntry) => appendRunLedgerEntry(entry, runLedgerDir(env.ELANOUS_STATE_DIR)));
1309
1484
  return (input) => {
1310
1485
  if (options.repairCommitOnly && (input.autoMerge || input.openPr || input.base !== `pr-${options.repairCommitOnly.pr}`)) {
1311
1486
  throw new Error('repair commit-only refuses auto merge, new PR, or a different base');
1312
1487
  }
1488
+ if (options.siWarmAttempt && !options.siWarmHostPostprocess) {
1489
+ throw new Error('si-v1 warm execution requires cold-path host postprocess parity');
1490
+ }
1491
+ if (options.siWarmAttempt) {
1492
+ const done = (async (): Promise<SelfImplementJobDone> => {
1493
+ let attempt: SiWarmChildAttempt;
1494
+ try {
1495
+ const acquired = await options.siWarmAttempt!(input);
1496
+ if (!acquired) {
1497
+ // No lease was fenced: use the existing one-shot Job path.
1498
+ const { siWarmAttempt: _unused, siWarmHostPostprocess: _postprocess, ...coldOptions } = options;
1499
+ return podSelfImplementSpawn(coldOptions)(input).done;
1500
+ }
1501
+ attempt = acquired;
1502
+ }
1503
+ catch (error) {
1504
+ // A lost exec reply may conceal an already opened PR. Never start another child here.
1505
+ const message = error instanceof Error ? error.message : String(error);
1506
+ debug.log('pod.reuse', 'attempt-unavailable', { spaceId: input.spaceId, reason: message }, { level: 'warn' });
1507
+ return { exitCode: 1, output: '', error: { code: 'si-warm-reconcile-only', message } };
1508
+ }
1509
+ let transport: Awaited<ReturnType<typeof executeSiWarmTransport>>;
1510
+ try {
1511
+ transport = await executeSiWarmTransport({ command: attempt.command,
1512
+ pod: attempt.transport.pod, runner: attempt.transport.runner,
1513
+ leaseFenced: attempt.leaseFenced, payload: attempt.payload,
1514
+ credentialBundle: attempt.credentialBundle, now: attempt.transport.now });
1515
+ } catch (error) {
1516
+ const message = error instanceof Error ? error.message : String(error);
1517
+ try { await attempt.quarantineUnknown('transport-exception'); }
1518
+ catch (quarantineError) {
1519
+ // Both failures must survive: the first transport error names the cause, the
1520
+ // quarantine error names the unfinished disposition. Record the judgement.
1521
+ const quarantineMessage = quarantineError instanceof Error ? quarantineError.message : String(quarantineError);
1522
+ const combined = `transport:${message}; quarantine:${quarantineMessage}`;
1523
+ try { writeWarmLedger({ runId: attempt.command?.runId ?? input.spaceId,
1524
+ event: 'pod-reuse-quarantine-failed',
1525
+ data: { spaceId: input.spaceId, workerId: attempt.command?.workerId ?? null,
1526
+ leaseId: attempt.command?.leaseId ?? null, reason: combined } }); }
1527
+ catch (ledgerError) { debug.log('pod.reuse', 'ledger-unavailable', { spaceId: input.spaceId,
1528
+ event: 'pod-reuse-quarantine-failed', reason: ledgerError instanceof Error ? ledgerError.message : String(ledgerError) }, { level: 'warn' }); }
1529
+ debug.log('pod.reuse', 'quarantine-failed', { spaceId: input.spaceId, reason: combined }, { level: 'error' });
1530
+ return { exitCode: 1, output: '', error: { code: 'si-warm-worker-state-unknown', message: combined } };
1531
+ }
1532
+ // The quarantine is confirmed: record the settled disposal (fail-soft — the
1533
+ // disposition survives a ledger write failure) before the reconcile-only return.
1534
+ try { writeWarmLedger({ runId: attempt.command?.runId ?? input.spaceId,
1535
+ event: 'pod-reuse-quarantined',
1536
+ data: { spaceId: input.spaceId, workerId: attempt.command?.workerId ?? null,
1537
+ leaseId: attempt.command?.leaseId ?? null, reason: 'transport-exception' } }); }
1538
+ catch (ledgerError) { debug.log('pod.reuse', 'ledger-unavailable', { spaceId: input.spaceId,
1539
+ event: 'pod-reuse-quarantined', reason: ledgerError instanceof Error ? ledgerError.message : String(ledgerError) }, { level: 'warn' }); }
1540
+ return { exitCode: 1, output: '', error: { code: 'si-warm-reconcile-only', message } };
1541
+ }
1542
+ const validation = transport.boundary === 'exec-attempted'
1543
+ ? assessSiWarmReply(attempt.command, transport.reply, transport.bytes,
1544
+ attempt.payload, attempt.credentialBundle)
1545
+ : undefined;
1546
+ const next = decideSiWarmNextStep({ boundary: transport.boundary,
1547
+ leaseFenced: attempt.leaseFenced, ...(validation ? { validation } : {}) });
1548
+ debug.log('pod.reuse', 'admission-decision', { spaceId: input.spaceId, boundary: transport.boundary,
1549
+ next: next.next, reason: transport.reason, validationReason: transport.validationReason ?? null });
1550
+ if (next.next === 'cold-job' && transport.boundary === 'not-entered' && attempt.leaseFenced) {
1551
+ const { siWarmAttempt: _unused, ...coldOptions } = options;
1552
+ // The acquired lease must not silently ride along into a cold Job: dispose it first,
1553
+ // record the disposition, and refuse to start the cold Job when disposal fails.
1554
+ const coldLease = attempt.command ? { workerId: attempt.command.workerId,
1555
+ podUid: attempt.command.podUid, leaseId: attempt.command.leaseId,
1556
+ generation: attempt.command.generation, key: attempt.command.key,
1557
+ sourceSha: attempt.command.sourceSha } : null;
1558
+ if (coldLease) {
1559
+ const recordColdDisposition = (event: string, action: string, residue: Record<string, unknown> = {}) => {
1560
+ debug.log('pod.reuse', event, { spaceId: input.spaceId, workerId: coldLease.workerId,
1561
+ leaseId: coldLease.leaseId, action, reason: transport.reason, ...residue });
1562
+ try { writeWarmLedger({ runId: attempt.command?.runId ?? input.spaceId, event,
1563
+ data: { spaceId: input.spaceId, workerId: coldLease.workerId, leaseId: coldLease.leaseId,
1564
+ action, reason: transport.reason, ...residue } }); }
1565
+ catch (error) { debug.log('pod.reuse', 'ledger-unavailable', { spaceId: input.spaceId, event,
1566
+ reason: error instanceof Error ? error.message : String(error) }, { level: 'warn' }); }
1567
+ };
1568
+ try {
1569
+ // The acquired lease is disposed by the same eligibility standard as the
1570
+ // entered path: process count zero, pinned-source git reset/clean/status,
1571
+ // credential count zero, and an observable return decision — before the
1572
+ // lease can go back into the warm pool or a cold Job starts.
1573
+ const returned = await cleanAndReturnChildPod(coldLease, { ...attempt.cleanup,
1574
+ observe: (event, data) => {
1575
+ debug.log('pod.reuse', event, { spaceId: input.spaceId, ...data },
1576
+ { level: event === 'return-decision'
1577
+ && ((data as { outcome?: string }).outcome === 'eligible'
1578
+ || (data as { outcome?: string }).outcome === 'returned') ? 'info' : 'warn' });
1579
+ attempt.cleanup.observe(event, data);
1580
+ // A ledger write failure is an observation failure: the settled return
1581
+ // or quarantine disposition must survive it (fail-soft after the decision).
1582
+ try { writeWarmLedger({ runId: attempt.command?.runId ?? input.spaceId,
1583
+ event: `pod-reuse-${event}`,
1584
+ data: { spaceId: input.spaceId, ...data } }); }
1585
+ catch (ledgerError) { debug.log('pod.reuse', 'ledger-unavailable', { spaceId: input.spaceId,
1586
+ runId: attempt.command?.runId ?? input.spaceId, event: `pod-reuse-${event}`,
1587
+ reason: ledgerError instanceof Error ? ledgerError.message : String(ledgerError) },
1588
+ { level: 'warn' }); }
1589
+ } });
1590
+ recordColdDisposition('pod-reuse-cold-fallback-dispose', returned.outcome,
1591
+ { dispositionReason: returned.reason });
1592
+ } catch (error) {
1593
+ const message = error instanceof Error ? error.message : String(error);
1594
+ try { await attempt.cleanup.quarantine(coldLease, { clean: false, reason: 'cold-fallback-dispose-failed', observedResidue: null }); }
1595
+ catch (quarantineError) {
1596
+ const quarantineMessage = quarantineError instanceof Error ? quarantineError.message : String(quarantineError);
1597
+ recordColdDisposition('pod-reuse-cold-fallback-dispose-failed', 'quarantine-failed',
1598
+ { error: `disposal:${message}; quarantine:${quarantineMessage}` });
1599
+ return { exitCode: 1, output: '', error: { code: 'si-warm-worker-state-unknown',
1600
+ message: `disposal:${message}; quarantine:${quarantineMessage}` } };
1601
+ }
1602
+ recordColdDisposition('pod-reuse-cold-fallback-dispose-failed', 'quarantined', { error: message });
1603
+ return { exitCode: 1, output: '', error: { code: 'si-warm-worker-state-unknown', message } };
1604
+ }
1605
+ }
1606
+ return podSelfImplementSpawn(coldOptions)(input).done;
1607
+ }
1608
+ const command = attempt.command;
1609
+ const lease = command && { workerId: command.workerId, podUid: command.podUid,
1610
+ leaseId: command.leaseId, generation: command.generation, key: command.key,
1611
+ sourceSha: command.sourceSha };
1612
+ const recordQuarantine = (event: string, reason: string, residue: Record<string, unknown> = {}) => {
1613
+ debug.log('pod.reuse', event, { spaceId: input.spaceId, workerId: command?.workerId ?? null,
1614
+ leaseId: command?.leaseId ?? null, reason, ...residue }, { level: 'warn' });
1615
+ try { writeWarmLedger({ runId: command?.runId ?? input.spaceId, event,
1616
+ data: { spaceId: input.spaceId, workerId: command?.workerId ?? null,
1617
+ leaseId: command?.leaseId ?? null, reason, ...residue } }); }
1618
+ catch (error) { debug.log('pod.reuse', 'ledger-unavailable', { spaceId: input.spaceId,
1619
+ event, reason: error instanceof Error ? error.message : String(error) }, { level: 'warn' }); }
1620
+ };
1621
+ const retireLease = async (reason: string) => {
1622
+ recordQuarantine('pod-reuse-quarantine-intent', reason);
1623
+ if (!lease) {
1624
+ try { await attempt.quarantineUnknown(reason); }
1625
+ catch (error) {
1626
+ // The unsettled quarantine is an account of its own: keep both the
1627
+ // primary path reason and the quarantine cause in the run ledger,
1628
+ // then let the caller surface the unknown worker state.
1629
+ const message = error instanceof Error ? error.message : String(error);
1630
+ recordQuarantine('pod-reuse-quarantine-failed', `${reason}; quarantine:${message}`);
1631
+ throw error;
1632
+ }
1633
+ recordQuarantine('pod-reuse-quarantined', reason);
1634
+ return;
1635
+ }
1636
+ // An uncertain exec may still be running. Keep it fenced and revoke its run credentials.
1637
+ let processRemaining: number | null = null;
1638
+ let credentialRemaining: number | null = null;
1639
+ try { processRemaining = (await attempt.cleanup.stopChildProcesses(lease)).remaining; }
1640
+ catch (error) { debug.log('pod.reuse', 'process-stop-failed', { spaceId: input.spaceId,
1641
+ reason: error instanceof Error ? error.message : String(error) }, { level: 'warn' }); }
1642
+ try { credentialRemaining = (await attempt.cleanup.removeRunCredentials(lease)).remaining; }
1643
+ catch (error) { debug.log('pod.reuse', 'credential-revoke-failed', { spaceId: input.spaceId,
1644
+ reason: error instanceof Error ? error.message : String(error) }, { level: 'warn' }); }
1645
+ const observedResidue = [processRemaining, credentialRemaining]
1646
+ .filter((value): value is number => typeof value === 'number'
1647
+ && Number.isSafeInteger(value) && value >= 0)
1648
+ .reduce((sum, value) => sum + value, 0);
1649
+ try {
1650
+ await attempt.cleanup.quarantine(lease, { clean: false, reason,
1651
+ observedResidue: observedResidue > 0 ? observedResidue : null });
1652
+ } catch (error) {
1653
+ // The lease stays unfenced and the worker state unknown: record the
1654
+ // unsettled disposal (primary reason plus the quarantine cause)
1655
+ // before the caller reports the unknown worker state.
1656
+ const message = error instanceof Error ? error.message : String(error);
1657
+ recordQuarantine('pod-reuse-quarantine-failed', `${reason}; quarantine:${message}`);
1658
+ throw error;
1659
+ }
1660
+ recordQuarantine('pod-reuse-quarantined', reason, { processRemaining, credentialRemaining });
1661
+ };
1662
+ if (next.next !== 'adopt-warm' || !transport.measured || !command
1663
+ || !transport.reply || !transport.bytes) {
1664
+ const reconcileDisposition = transport.verifiedBytesForReconcile
1665
+ ? parseSelfImplementJson(new TextDecoder().decode(transport.verifiedBytesForReconcile.terminal))
1666
+ : null;
1667
+ const hostDisposition = reconcileDisposition ? (({ worktreePath: _podOnly, ...host }) => host)(reconcileDisposition) : null;
1668
+ let quarantineError: string | undefined;
1669
+ try { await retireLease(validation?.reason ?? 'response-unknown'); }
1670
+ catch (error) { debug.log('pod.reuse', 'quarantine-failed', { spaceId: input.spaceId,
1671
+ reason: error instanceof Error ? error.message : String(error) }, { level: 'error' });
1672
+ quarantineError = error instanceof Error ? error.message : String(error); }
1673
+ return { exitCode: 1, output: '',
1674
+ ...(hostDisposition ? { disposition: hostDisposition } : {}),
1675
+ error: { code: quarantineError
1676
+ || (transport.quarantineWorker && !transport.quarantineRecorded)
1677
+ ? 'si-warm-worker-state-unknown' : 'si-warm-reconcile-only',
1678
+ message: `si-v1 exec may have run; reconcile before retry (${transport.reason})${transport.quarantineError ? `; transport quarantine: ${transport.quarantineError}` : ''}${quarantineError ? `; worker-state-unknown: ${quarantineError}` : ''}` } };
1679
+ }
1680
+ const terminal = new TextDecoder().decode(transport.bytes.terminal);
1681
+ const disposition = parseSelfImplementJson(terminal);
1682
+ if (!disposition) {
1683
+ let quarantineError: string | undefined;
1684
+ try { await retireLease('terminal-disposition-unreadable'); }
1685
+ catch (error) { debug.log('pod.reuse', 'quarantine-failed', { spaceId: input.spaceId,
1686
+ reason: error instanceof Error ? error.message : String(error) }, { level: 'error' });
1687
+ quarantineError = error instanceof Error ? error.message : String(error); }
1688
+ return { exitCode: 1, output: '', error: { code: quarantineError
1689
+ ? 'si-warm-worker-state-unknown' : 'si-warm-terminal-unreadable',
1690
+ message: quarantineError ?? 'validated si-v1 terminal was not a self-implement disposition' } };
1691
+ }
1692
+ try { await attempt.adopt(command, transport.reply, transport.bytes); }
1693
+ catch (error) {
1694
+ const message = error instanceof Error ? error.message : String(error);
1695
+ let quarantineError: string | undefined;
1696
+ try { await retireLease('adopt-failed'); }
1697
+ catch (quarantineFailure) { debug.log('pod.reuse', 'quarantine-failed', { spaceId: input.spaceId,
1698
+ reason: quarantineFailure instanceof Error ? quarantineFailure.message : String(quarantineFailure) }, { level: 'error' });
1699
+ quarantineError = quarantineFailure instanceof Error ? quarantineFailure.message : String(quarantineFailure); }
1700
+ debug.log('pod.reuse', 'adopt-failed', { spaceId: input.spaceId, reason: message }, { level: 'warn' });
1701
+ return { exitCode: 1, output: '', disposition, error: { code: quarantineError
1702
+ ? 'si-warm-worker-state-unknown' : 'si-warm-reconcile-only', message: quarantineError ?? message } };
1703
+ }
1704
+ let returnOutcome: 'returned' | 'quarantined' = 'quarantined';
1705
+ let workerStateUnknown: string | undefined;
1706
+ try {
1707
+ const returned = await cleanAndReturnChildPod(lease!, { ...attempt.cleanup,
1708
+ observe: (event, data) => {
1709
+ debug.log('pod.reuse', event, { spaceId: input.spaceId, ...data },
1710
+ { level: event === 'return-decision' && (data.outcome === 'eligible' || data.outcome === 'returned') ? 'info' : 'warn' });
1711
+ attempt.cleanup.observe(event, data);
1712
+ // The lease disposition is already settled by the time this callback runs; the warm
1713
+ // ledger is an observation of it. A ledger write failure must stay an observation
1714
+ // failure — escaping here would quarantine a lease that was already returned.
1715
+ try { writeWarmLedger({ runId: command.runId, event: `pod-reuse-${event}`,
1716
+ data: { spaceId: input.spaceId, ...data } }); }
1717
+ catch (error) { debug.log('pod.reuse', 'ledger-unavailable', { spaceId: input.spaceId, runId: command.runId,
1718
+ event: `pod-reuse-${event}`, reason: error instanceof Error ? error.message : String(error) }, { level: 'warn' }); }
1719
+ } });
1720
+ returnOutcome = returned.outcome;
1721
+ } catch (error) {
1722
+ workerStateUnknown = error instanceof Error ? error.message : String(error);
1723
+ debug.log('pod.reuse', 'return-unavailable', { spaceId: input.spaceId,
1724
+ reason: workerStateUnknown }, { level: 'error' });
1725
+ // The inner return/quarantine result is unclear: quarantine the lease explicitly,
1726
+ // and only a second quarantine failure leaves the worker state unknown.
1727
+ try {
1728
+ await attempt.quarantineUnknown(`return-quarantine-unavailable:${workerStateUnknown}`);
1729
+ try { writeWarmLedger({ runId: command!.runId, event: 'pod-reuse-quarantined',
1730
+ data: { spaceId: input.spaceId, workerId: command!.workerId, leaseId: command!.leaseId,
1731
+ reason: `return-quarantine-unavailable:${workerStateUnknown}` } }); }
1732
+ catch { /* ledger loss must not mask the quarantine outcome */ }
1733
+ // The explicit quarantine settled the disposal: the caller result must
1734
+ // account for it as quarantined instead of claiming an unknown state.
1735
+ workerStateUnknown = undefined;
1736
+ } catch (quarantineError) {
1737
+ const quarantineMessage = quarantineError instanceof Error ? quarantineError.message : String(quarantineError);
1738
+ workerStateUnknown = `return:${workerStateUnknown}; quarantine:${quarantineMessage}`;
1739
+ try { writeWarmLedger({ runId: command!.runId, event: 'pod-reuse-quarantine-failed',
1740
+ data: { spaceId: input.spaceId, workerId: command!.workerId, leaseId: command!.leaseId,
1741
+ reason: workerStateUnknown } }); }
1742
+ catch { debug.log('pod.reuse', 'ledger-unavailable', { spaceId: input.spaceId,
1743
+ event: 'pod-reuse-quarantine-failed', reason: workerStateUnknown }, { level: 'warn' }); }
1744
+ }
1745
+ }
1746
+ const log = new TextDecoder().decode(transport.bytes.log);
1747
+ // Pod checkout paths cannot be used by the host supervisor.
1748
+ const { worktreePath: _podOnly, ...hostDisposition } = disposition;
1749
+ if (workerStateUnknown) return { exitCode: 1, output: log.slice(-4096), disposition: hostDisposition,
1750
+ error: { code: 'si-warm-worker-state-unknown', message: workerStateUnknown } };
1751
+ debug.log('pod.reuse', 'child-result', { spaceId: input.spaceId, workerId: command.workerId,
1752
+ leaseId: command.leaseId, returnOutcome, stage: disposition.stage ?? null });
1753
+ try {
1754
+ return await options.siWarmHostPostprocess!({ exitCode: transport.reply.exitCode,
1755
+ output: log.slice(-4096), disposition: hostDisposition }, attempt, input);
1756
+ } catch (error) {
1757
+ const message = error instanceof Error ? error.message : String(error);
1758
+ debug.log('pod.reuse', 'host-postprocess-failed', { spaceId: input.spaceId,
1759
+ reason: message }, { level: 'error' });
1760
+ return { exitCode: 1, output: log.slice(-4096), disposition: hostDisposition,
1761
+ error: { code: 'si-warm-host-postprocess-failed', message } };
1762
+ }
1763
+ })();
1764
+ return { address: `self-impl:${input.spaceId}`, done };
1765
+ }
1313
1766
  const name = podJobName(input.spaceId);
1314
1767
  let parentGoal: string | undefined;
1315
1768
  let parentGoalUnreadable = false;
@@ -1599,12 +2052,13 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1599
2052
  const jobArgs = zaiRoute === 'openrouter' ? zaiOverflowChildArgs(args, ZAI_OPENROUTER_FALLBACK_MODEL) : args;
1600
2053
  const grok = options.provider === 'grok';
1601
2054
  const openrouter = options.provider === 'openrouter' || zaiRoute === 'openrouter';
2055
+ const openGateway = options.provider === 'opengateway';
1602
2056
  const anthropic = options.provider === 'anthropic';
1603
2057
  const bedrockChild = options.provider === 'bedrock';
1604
2058
  const zaiChild = zaiRoute === 'zai';
1605
2059
  /** API 키 자식 — codex 계정·자격 파일을 안 쓴다(키는 작업별 creds Secret 의 `env-<KEY>` · bedrock·glm 은 이름 있는 Secret 으로만). */
1606
- const apiKeyChild = openrouter || anthropic || bedrockChild || zaiChild;
1607
- const plannedAccount = retryAccount ?? (grok ? 'grok' : openrouter ? 'openrouter' : anthropic ? 'anthropic' : bedrockChild ? 'bedrock' : zaiChild ? 'glm' : options.accountBroker?.() ?? options.account ?? 'team');
2060
+ const apiKeyChild = openrouter || openGateway || anthropic || bedrockChild || zaiChild;
2061
+ const plannedAccount = retryAccount ?? (grok ? 'grok' : openrouter ? 'openrouter' : openGateway ? 'opengateway' : anthropic ? 'anthropic' : bedrockChild ? 'bedrock' : zaiChild ? 'glm' : options.accountBroker?.() ?? options.account ?? 'team');
1608
2062
  debug.log('self-implement.pod', 'account', { spaceId: input.spaceId, account: plannedAccount, brokered: Boolean(options.accountBroker) });
1609
2063
  const plannedCodexAccounts = grok || apiKeyChild || !options.accountBroker || !options.rotationAccounts ? undefined : [plannedAccount, ...options.rotationAccounts.filter((candidate) => candidate !== plannedAccount && candidate !== failedAccount)];
1610
2064
  if (plannedCodexAccounts && !options.rotationAccounts!.includes(plannedAccount)) throw new Error(`pod: 배분 계정 ${plannedAccount} 이 회전 계획에 없다`);
@@ -1647,6 +2101,17 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1647
2101
  }) : undefined;
1648
2102
  const openrouterKey = openrouter ? env.OPENROUTER_API_KEY?.trim() || (options.readKeyCache ?? defaultReadKeyCache)('OPENROUTER_API_KEY')?.trim() : undefined;
1649
2103
  if (openrouter && !openrouterKey) throw new Error('pod: Pod 에 openrouter 키 없음 — 호스트 키 캐시 또는 OPENROUTER_API_KEY 필요');
2104
+ const openGatewayEnvKey = openGateway ? env.OPENGATEWAY_API_KEY?.trim() : undefined;
2105
+ const openGatewayCachedKey = openGateway ? (options.readKeyCache ?? defaultReadKeyCache)('OPENGATEWAY_API_KEY')?.trim() : undefined;
2106
+ if (openGatewayEnvKey && openGatewayCachedKey && openGatewayEnvKey !== openGatewayCachedKey)
2107
+ throw new Error('pod: opengateway env와 키 캐시가 충돌 — Pod 발사 거부');
2108
+ const openGatewayHostConfig = openGateway ? (options.openGatewayConfig ?? getUserConfig)() : undefined;
2109
+ const openGatewayConfiguredKeys = openGatewayHostConfig ? configuredOpenGatewayKeys(openGatewayHostConfig.llm) : [];
2110
+ if (openGatewayConfiguredKeys.length > 1 || openGatewayConfiguredKeys.some((key) =>
2111
+ (openGatewayEnvKey && openGatewayEnvKey !== key) || (openGatewayCachedKey && openGatewayCachedKey !== key)))
2112
+ throw new Error('pod: opengateway 호스트 설정과 env/키 캐시가 충돌 — Pod 발사 거부');
2113
+ const openGatewayKey = openGatewayEnvKey || openGatewayCachedKey || openGatewayConfiguredKeys[0];
2114
+ if (openGateway && !openGatewayKey) throw new Error('pod: Pod 에 opengateway 키 없음 — 호스트 키 캐시 또는 OPENGATEWAY_API_KEY 필요');
1650
2115
  // POD-ANTHROPIC-PROVIDER — 제품 자격 해석(`getAnthropicApiKey` · 키 캐시 → env)으로 읽는다. 못 읽으면 Pod 를 안 띄운다(fail-closed).
1651
2116
  // ⛔ 값은 로그·오류 문면에 안 싣는다 — «있었다» 만.
1652
2117
  const anthropicKey = anthropic
@@ -1717,7 +2182,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1717
2182
  }
1718
2183
  }
1719
2184
  // Grok 자격은 전용 Secret 키 하나로만 보낸다 — passEnv 의 API 키 중복 전달은 막는다.
1720
- const passKeys = [...new Set([...(options.passEnv ?? []), ...(openrouter ? ['OPENROUTER_API_KEY'] : []), ...(anthropic ? ['ANTHROPIC_API_KEY'] : [])])].filter((key) => key !== 'ELANOUS_LIVE_DETAIL_UNTIL'
2185
+ const passKeys = [...new Set([...(options.passEnv ?? []), ...(openrouter ? ['OPENROUTER_API_KEY'] : []), ...(openGateway ? ['OPENGATEWAY_API_KEY'] : []), ...(anthropic ? ['ANTHROPIC_API_KEY'] : [])])].filter((key) => key !== 'ELANOUS_LIVE_DETAIL_UNTIL'
1721
2186
  && key !== POD_GITHUB_CREDENTIAL_TOKEN_ENV && key !== POD_GITHUB_CREDENTIAL_URL_ENV
1722
2187
  && (!grok || !['XAI_API_KEY', 'GROK_API_KEY', 'GROK_CODE_XAI_API_KEY'].includes(key)));
1723
2188
  const skillEnvs: Record<string, string> = options.skillEnv
@@ -1863,7 +2328,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1863
2328
  authorChoice = { ...authorChoice, model: childModel, modelSource: 'fallback-child' };
1864
2329
  } else debug.log('self-implement.pod', 'role-llm-armed', { job: name, role: 'planning', model: authorChoice.model, source: authorChoice.modelSource, account: roleCodexAccount });
1865
2330
  }
1866
- const hostKey = (k: string): string | undefined => k === 'OPENROUTER_API_KEY' && openrouter ? openrouterKey : k === 'ANTHROPIC_API_KEY' && anthropic ? anthropicKey : env[k] ?? (options.readKeyCache ?? defaultReadKeyCache)(k);
2331
+ const hostKey = (k: string): string | undefined => k === 'OPENROUTER_API_KEY' && openrouter ? openrouterKey : k === 'OPENGATEWAY_API_KEY' && openGateway ? openGatewayKey : k === 'ANTHROPIC_API_KEY' && anthropic ? anthropicKey : env[k] ?? (options.readKeyCache ?? defaultReadKeyCache)(k);
1867
2332
  const hostFallbackNames = (options.fallbackConfig ?? getUserConfig)().llm.fallbackChain;
1868
2333
  const normalizedFallback = normalizeFallbackChain(hostFallbackNames);
1869
2334
  let fallbackSteps = normalizedFallback.chain;
@@ -1932,6 +2397,10 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1932
2397
  // 🩸 종전엔 여기서 delete 부터 해서, 재개가 원격에서 멀쩡히 돌던 Job 을 죽였다. 실패로 끝난 Job 만 지우고 다시 만든다.
1933
2398
  // ☸️ 노드 레지스트리에 이 판이 있으면 그것을 pull 한다(델타 · 대표 2026-09-26) — 라벨 판정은 로컬 이미지 이름으로 한다.
1934
2399
  const jobImage = member?.imageRef ?? image;
2400
+ // A pool member may pull a different image than the host's local one.
2401
+ // The local Docker label cannot prove that member image's commit.
2402
+ const localImageCommit = options.imageCommit !== undefined ? options.imageCommit : options.kubectl ? null : podImageFreshness({ image }).imageCommit;
2403
+ const jobImageCommit = jobImage === image ? localImageCommit : null;
1935
2404
  const launchRunId = retryAccount || oomRetried || ghLoginRetried ? mintRunId() : childRunId;
1936
2405
  let liveChildRunId = launchRunId;
1937
2406
  // TA-POD-WARMBOOT-PARITY — the id the followed Job prints in its result marker (null = wait on the condition only).
@@ -2167,7 +2636,22 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
2167
2636
  debug.log('self-implement.pod', fit.fitted ? 'grok-deadline-fitted' : 'grok-deadline-kept', { job: name, requestedSeconds: jobDeadlineSeconds, deadlineSeconds: fit.deadlineSeconds, availableSeconds: fit.availableSeconds, relay: Boolean(credentialRelay), marginMs: POD_GROK_EXPIRY_MARGIN_MS });
2168
2637
  jobDeadlineSeconds = fit.deadlineSeconds;
2169
2638
  }
2170
- const job = podJobManifest({ name, namespace, fallbackChain, ...(resultMarkerLine ? { resultMarkerLine } : {}), image: jobImage, workIdentity, ...(env.ELANOUS_TASK_BOUND_PR && /^[1-9]\d*$/.test(env.ELANOUS_TASK_BOUND_PR) && Number.isSafeInteger(Number(env.ELANOUS_TASK_BOUND_PR)) ? { boundPrNumber: Number(env.ELANOUS_TASK_BOUND_PR) } : {}), ...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}), repoUrl, ...(options.source ? { source: options.source } : {}), ...(options.repairCommitOnly ? { repairCommitOnly: options.repairCommitOnly } : {}), ...(hostMirror ? { hostMirror } : {}), args: jobArgs, passEnv: jobPassEnv, deadlineSeconds: jobDeadlineSeconds, ...(goalDoc ? { goalDoc } : env.ELANOUS_POD_AUTHOR_ON_POD === '1' && !shard ? { authorSentence: true, ...(env.ELANOUS_POD_AUTHOR_GRADE === 'lite' || env.ELANOUS_POD_AUTHOR_GRADE === 'full' ? { authorGrade: env.ELANOUS_POD_AUTHOR_GRADE } : {}) } : {}), runId: launchRunId, ...(parentRunId ? { parentRunId } : {}), ...(jobArmEnv ? { armEnv: jobArmEnv } : {}), hostId: resolveHostId(env), skillEnvs: Object.keys(skillEnvs), memoryLimit, memoryRequest, imageCommit: options.imageCommit !== undefined ? options.imageCommit : options.kubectl ? null : podImageFreshness({ image }).imageCommit, ...(grok ? { grokCredential: 'grokAuth' in creds && creds.grokAuth ? 'subscription' as const : 'api_key' as const } : {}), ...(codexAccounts ? { codexAccounts } : {}), ...(hostRefresh ? { appCredential: true } : {}), execution: { key: executionKey, attempt: oomRetried ? 2 : 1 }, hostLeaseAdmitted: !!releaseAdmission, ...(roleCodexCredential ? { reviewerCodexAuth: true } : {}), ...(bedrockSpec ? { bedrock: bedrockSpec } : {}), ...(zaiSpec ? { zai: zaiSpec } : {}), ...(zaiFallbackSpec ? { zaiFallback: zaiFallbackSpec } : {}) }); // kubectl 주입(=시험)이면 docker 를 부르지 않는다
2639
+ // POD-LAUNCH-PROVENANCE — fail-closed: a si-task Job without its ★ identity is never applied.
2640
+ let provenance: PodProvenanceMetadata;
2641
+ try {
2642
+ let version: string | undefined;
2643
+ try { version = packageVersion(); } catch { version = undefined; }
2644
+ provenance = podProvenanceMetadata(siTaskPodProvenance({ env, runId: launchRunId, ...(parentRunId ? { parentRunId } : {}), workIdentity,
2645
+ requestedAt: new Date((options.now ?? Date.now)()).toISOString(), feature: input.feature, launchHost: resolveHostId(env),
2646
+ commit: seedSource.kind === 'commit' ? seedSource.sha : seedCommit ?? jobImageCommit, ...(version ? { version } : {}),
2647
+ ...(memoryRequest ? { memReq: memoryRequest } : {}) }));
2648
+ } catch (error) {
2649
+ const message = error instanceof Error ? error.message : String(error);
2650
+ debug.log('pod.provenance', 'si-task-refused', { job: name, reason: message }, { level: 'warn' });
2651
+ cleanupSecret();
2652
+ return { exitCode: 1, output: '', error: { code: 'pod-provenance', message } };
2653
+ }
2654
+ const job = podJobManifest({ provenance, name, namespace, fallbackChain, ...(resultMarkerLine ? { resultMarkerLine } : {}), image: jobImage, workIdentity, ...(env.ELANOUS_TASK_BOUND_PR && /^[1-9]\d*$/.test(env.ELANOUS_TASK_BOUND_PR) && Number.isSafeInteger(Number(env.ELANOUS_TASK_BOUND_PR)) ? { boundPrNumber: Number(env.ELANOUS_TASK_BOUND_PR) } : {}), ...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}), repoUrl, ...(options.source ? { source: options.source } : {}), ...(options.repairCommitOnly ? { repairCommitOnly: options.repairCommitOnly } : {}), ...(hostMirror ? { hostMirror } : {}), args: jobArgs, passEnv: jobPassEnv, deadlineSeconds: jobDeadlineSeconds, ...(goalDoc ? { goalDoc } : env.ELANOUS_POD_AUTHOR_ON_POD === '1' && !shard ? { authorSentence: true, ...(env.ELANOUS_POD_AUTHOR_GRADE === 'lite' || env.ELANOUS_POD_AUTHOR_GRADE === 'full' ? { authorGrade: env.ELANOUS_POD_AUTHOR_GRADE } : {}) } : {}), runId: launchRunId, ...(parentRunId ? { parentRunId } : {}), ...(jobArmEnv ? { armEnv: jobArmEnv } : {}), hostId: resolveHostId(env), skillEnvs: Object.keys(skillEnvs), memoryLimit, memoryRequest, imageCommit: jobImageCommit, ...(grok ? { grokCredential: 'grokAuth' in creds && creds.grokAuth ? 'subscription' as const : 'api_key' as const } : {}), ...(codexAccounts ? { codexAccounts } : {}), ...(hostRefresh ? { appCredential: true } : {}), execution: { key: executionKey, attempt: oomRetried ? 2 : 1 }, hostLeaseAdmitted: !!releaseAdmission, ...(roleCodexCredential ? { reviewerCodexAuth: true } : {}), ...(bedrockSpec ? { bedrock: bedrockSpec } : {}), ...(zaiSpec ? { zai: zaiSpec } : {}), ...(zaiFallbackSpec ? { zaiFallback: zaiFallbackSpec } : {}) }); // kubectl 주입(=시험)이면 docker 를 부르지 않는다
2171
2655
  const carriedBranch = goalDoc ? 'goal-doc' as const : env.ELANOUS_POD_AUTHOR_ON_POD === '1' && !shard ? 'author' as const : undefined;
2172
2656
  const carriedChildLlm = carriedBranch ? podDelegatedChildLlm(jobArgs) : undefined;
2173
2657
  jobApplyStartedAt = Date.now();
@@ -2175,6 +2659,15 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
2175
2659
  zaiPendingApply.delete(name); // apply 뒤엔 Job 라벨이 센다(성공) · 실패면 셀 것이 없다
2176
2660
  if (a.status !== 0) { cleanupSecret(); return { exitCode: 1, output: a.stderr, error: { code: 'pod-apply', message: a.stderr.trim() } }; }
2177
2661
  releaseAdmission?.applied?.(name, context, namespace);
2662
+ const imageFacts = { job: name, childRunId: liveChildRunId, image: jobImage, imageCommit: jobImageCommit, context };
2663
+ // POD-LAUNCH-PROVENANCE — append-only «started» row: who · why · when, on the parent's existing run ledger.
2664
+ const startedRow = { job: name, run: launchRunId, attempt: oomRetried ? 2 : 1, provenance: provenanceRowFromAnnotations(provenance.annotations) };
2665
+ try { appendRunLedgerEntry({ runId: ledgerRunId, event: POD_LAUNCH_STARTED_EVENT, timestamp: new Date((options.now ?? Date.now)()).toISOString(), data: startedRow }, ledgerDir); }
2666
+ catch (error) { debug.log('pod.provenance', 'history-unavailable', { job: name, row: 'started', reason: error instanceof Error ? error.message : String(error) }, { level: 'warn' }); }
2667
+ debug.log('pod.provenance', 'job-started', startedRow);
2668
+ try { appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-job-image', data: imageFacts }, ledgerDir); }
2669
+ catch (error) { debug.log('self-implement.pod', 'job-image-ledger-unavailable', { job: name, reason: error instanceof Error ? error.message : String(error) }, { level: 'warn' }); }
2670
+ debug.log('self-implement.pod', 'job-image', imageFacts);
2178
2671
  // The injected observer does not acquire a production lease. The gate consumer
2179
2672
  // must compare the Job result and explicitly finalize before enabling replay.
2180
2673
  if (!oomRetried && input.warmWorkerCommand && options.warmPodRequest) {
@@ -2241,11 +2734,14 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
2241
2734
  }
2242
2735
  writePodFragment({ spaceId: input.spaceId, context, namespace, job: name, inboxDir: POD_CONTROL_INBOX_DIR, ...(liveChildRunId === normalizeRunId(liveChildRunId) ? { runId: liveChildRunId } : {}), ...(parentRunId && normalizeRunId(parentRunId) === parentRunId ? { parentRunId } : {}) }, env);
2243
2736
  recorded = true;
2244
- debug.log('self-implement.pod', 'job-applied', { job: name, namespace, ...(member ? { context: member.context } : {}), image: jobImage, spaceId: input.spaceId, passEnv, ...(openrouter ? { openrouterKeyPresent: passEnv.includes('OPENROUTER_API_KEY') } : {}), ...(anthropic ? { anthropicKeyPresent: passEnv.includes('ANTHROPIC_API_KEY') } : {}), ...(bedrockSpec ? { bedrockSecret: bedrockSpec.secretName, bedrockSecretKeys: bedrockSpec.keys, bedrockRegionFrom: bedrockSpec.region.from } : {}), ...(zaiRoute ? { zaiRoute, ...(zaiSpec ? { zaiSecret: zaiSpec.secretName } : {}) } : {}), extraArgs: options.extraArgs ?? [], ...(options.armEnv?.ELANOUS_ARM_ID ? { armId: options.armEnv.ELANOUS_ARM_ID } : {}) });
2737
+ debug.log('self-implement.pod', 'job-applied', { job: name, namespace, ...(member ? { context: member.context } : {}), image: jobImage, spaceId: input.spaceId, passEnv, ...(openrouter ? { openrouterKeyPresent: passEnv.includes('OPENROUTER_API_KEY') } : {}), ...(openGateway ? { openGatewayKeyPresent: passEnv.includes('OPENGATEWAY_API_KEY') } : {}), ...(anthropic ? { anthropicKeyPresent: passEnv.includes('ANTHROPIC_API_KEY') } : {}), ...(bedrockSpec ? { bedrockSecret: bedrockSpec.secretName, bedrockSecretKeys: bedrockSpec.keys, bedrockRegionFrom: bedrockSpec.region.from } : {}), ...(zaiRoute ? { zaiRoute, ...(zaiSpec ? { zaiSecret: zaiSpec.secretName } : {}) } : {}), extraArgs: options.extraArgs ?? [], ...(options.armEnv?.ELANOUS_ARM_ID ? { armId: options.armEnv.ELANOUS_ARM_ID } : {}) });
2245
2738
  let state: 'complete' | 'failed' | 'aborted' = 'failed';
2246
2739
  let failedReason = '';
2247
2740
  let containerReason: string | null = null;
2248
2741
  let containerExitCode: number | null = null;
2742
+ // JOB-FAILURE-TERMINATION-CAPTURE — this Job's Kubernetes facts, read once at the failure verdict (before GC).
2743
+ // Observation only: failedReason · containerReason · containerExitCode keep driving the classification.
2744
+ let podTermination: PodTermination | null = null;
2249
2745
  // METER-POD-ROLLUP-GAP — abort 때 Job 삭제 «전»에 거둔 로그 사본(삭제 뒤엔 PARTIAL 줄도 못 읽는다).
2250
2746
  let abortLogs = '';
2251
2747
  // ⭐ 런 «도중» 원장 증분 회수 — 호스트 슈퍼바이저가 Pod 걸음을 실시간으로 본다(🅣 요청 · 힐 루프 runtime 성형의 전제).
@@ -2253,11 +2749,34 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
2253
2749
  // ⭐ METER-POD-ROLLUP-GAP-3 — 런 «도중» 마지막 PARTIAL 줄을 호스트가 쥔다: Pod 가 지워지면(SIGTERM 등) 종료 로그에서 PARTIAL 이 사라진다.
2254
2750
  let livePartialLine: string | null = null;
2255
2751
  let pollsSinceCapture = 0;
2752
+ let imageSnapshotRecorded = false;
2753
+ let terminalImageRecorded = false;
2754
+ const recordPodImage = (final: boolean): void => {
2755
+ if (final ? terminalImageRecorded : imageSnapshotRecorded) return;
2756
+ const observed = readJobPodImage(kubectl, namespace, name, lifecycleJobUid);
2757
+ if (!final && !observed.podName) return;
2758
+ const append = (isFinal: boolean): void => {
2759
+ const data = { job: name, childRunId: liveChildRunId, attempt: oomRetried ? 2 : 1,
2760
+ image: jobImage, imageCommit: jobImageCommit, context, ...observed, final: isFinal };
2761
+ try {
2762
+ appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-image-digest', data }, ledgerDir);
2763
+ if (isFinal) terminalImageRecorded = true;
2764
+ else imageSnapshotRecorded = true;
2765
+ } catch (error) {
2766
+ debug.log('self-implement.pod', 'pod-image-digest-ledger-unavailable', {
2767
+ job: name, final: isFinal, reason: error instanceof Error ? error.message : String(error) }, { level: 'warn' });
2768
+ }
2769
+ debug.log('self-implement.pod', 'pod-image-digest', data);
2770
+ };
2771
+ if (final && !imageSnapshotRecorded && observed.podName) append(false);
2772
+ append(final);
2773
+ };
2256
2774
  const capturePolls = Math.max(1, options.usagePartialCapturePolls ?? Math.round(POD_USAGE_PARTIAL_CAPTURE_MS / Math.max(1, options.pollMs ?? 15_000)));
2257
2775
  const markerWait = markerToken ? await import('./pod-result-marker-wait.js') : null;
2258
2776
  const waitStartedAt = Date.now();
2259
2777
  for (;;) {
2260
2778
  if (input.signal?.aborted) {
2779
+ recordPodImage(true);
2261
2780
  // METER-POD-ROLLUP-GAP — Job 삭제 «전»에 로그를 한 번 거둔다: PARTIAL 줄이 삭제와 함께 사라지면 사용량이 0 처럼 보인다.
2262
2781
  try {
2263
2782
  const captured = kubectl(['-n', namespace, 'logs', `job/${name}`, '-c', 'child', '--tail=400']);
@@ -2267,16 +2786,24 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
2267
2786
  state = 'aborted';
2268
2787
  break;
2269
2788
  }
2789
+ if (!imageSnapshotRecorded) recordPodImage(false);
2270
2790
  const g = kubectl(['-n', namespace, 'get', 'job', name, '-o', 'jsonpath={.status.conditions[*].type}']);
2271
2791
  const types = g.stdout;
2272
2792
  if (/Complete|SuccessCriteriaMet/.test(types)) { releaseAdmission?.observed?.(); state = 'complete'; break; }
2273
2793
  if (/Failed|FailureTarget/.test(types)) {
2274
2794
  releaseAdmission?.observed?.();
2275
2795
  state = 'failed';
2276
- const reason = kubectl(['-n', namespace, 'get', 'job', name, '-o', 'jsonpath={.status.conditions[?(@.type=="Failed")].reason}']);
2277
- failedReason = reason.status === 0 ? reason.stdout.trim() : '';
2796
+ // One Job read serves both: column one is the classification reason, all three the capture (no re-read a GC could empty).
2797
+ let jobRead: { status: number | null; stdout: string } | null = null;
2798
+ let jobReadError: unknown;
2799
+ try { jobRead = kubectl(['-n', namespace, 'get', 'job', name, '-o', `jsonpath=${POD_TERMINATION_JOB_JSONPATH}`]); }
2800
+ catch (error) { jobReadError = error; }
2801
+ failedReason = jobRead?.status === 0 ? (jobRead.stdout.split('\t')[0] ?? '').trim() : '';
2802
+ // One Pods read serves both: its first three columns are the classification layout, the rest the termination capture.
2803
+ let podsRead: { status: number | null; stdout: string } | null = null;
2278
2804
  try {
2279
- const pods = kubectl(['-n', namespace, 'get', 'pods', '-l', `job-name=${name}`, '-o', 'jsonpath={range .items[*]}{.metadata.creationTimestamp}{"\\t"}{range .status.containerStatuses[?(@.name=="child")]}{.state.terminated.reason}{"\\t"}{.state.terminated.exitCode}{end}{"\\n"}{end}']);
2805
+ const pods = kubectl(['-n', namespace, 'get', 'pods', '-l', `job-name=${name}`, '-o', `jsonpath=${POD_TERMINATION_PODS_JSONPATH}`]);
2806
+ podsRead = pods;
2280
2807
  if (pods.status === 0 && pods.stdout) {
2281
2808
  const rows = pods.stdout.trimEnd().split('\n').map((line) => line.split('\t'));
2282
2809
  const latest = rows.filter(([created]) => created && !Number.isNaN(Date.parse(created)))
@@ -2289,6 +2816,14 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
2289
2816
  }
2290
2817
  } catch { /* Pod 가 사라졌거나 조회 불가 — 종료 사유는 미상. */ }
2291
2818
  debug.log('self-implement.pod', 'container-terminated', { job: name, container: 'child', reason: containerReason, exitCode: containerExitCode, jobReason: failedReason || null, memoryLimit });
2819
+ podTermination = readPodTermination({ kubectl, namespace, job: name, path: 'job-failed-condition', jobRead, podsRead, jobUid: lifecycleJobUid });
2820
+ if (jobReadError !== undefined) {
2821
+ // Today's outcome stays (the throw ends the spawn as pod-error); the capture is in the ledger first.
2822
+ try { appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-child-evidence', data: { childEvidence: null, childRunId: liveChildRunId, job: name, podTermination } }, ledgerDir); }
2823
+ catch (error) { debug.log('self-implement.pod', 'child-evidence-write-failed', { job: name, childRunId: liveChildRunId, reason: error instanceof Error ? error.message : String(error) }, { level: 'warn' }); }
2824
+ debug.log('self-implement.pod', 'pod-termination-captured', { job: name, podTermination, jobReadThrew: true }, { level: 'warn' });
2825
+ throw jobReadError;
2826
+ }
2292
2827
  break;
2293
2828
  }
2294
2829
  // TA-POD-WARMBOOT-PARITY: the condition is not there yet — the child may already be done (k3d condition lag).
@@ -2300,6 +2835,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
2300
2835
  state = seen === 0 ? 'complete' : 'failed';
2301
2836
  // The child's own rc (the Pod's terminated state may lag like the condition) — exit-code paths read it as before.
2302
2837
  if (seen !== 0) containerExitCode = seen;
2838
+ if (seen !== 0) podTermination = readPodTermination({ kubectl, namespace, job: name, path: 'result-marker', jobUid: lifecycleJobUid });
2303
2839
  debug.log('self-implement.pod', 'result-marker-early', { job: name, waitedMs: Date.now() - waitStartedAt, rc: seen });
2304
2840
  break;
2305
2841
  }
@@ -2342,6 +2878,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
2342
2878
  }
2343
2879
  await sleep(options.pollMs ?? 15_000);
2344
2880
  }
2881
+ if (!terminalImageRecorded) recordPodImage(true);
2345
2882
  let logs = '';
2346
2883
  let logTailReason: string | undefined;
2347
2884
  try {
@@ -2521,23 +3058,72 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
2521
3058
  }
2522
3059
  const childFailure = state === 'failed' && !oomKilled && failedReason !== 'DeadlineExceeded'
2523
3060
  ? lastPodChildFailure(logs) : null;
3061
+ let childPrFailure: ReturnType<typeof podPrOpenFailure> = null;
3062
+ if (childFailure) {
3063
+ try {
3064
+ const entries = loadRunLedger(liveChildRunId, ledgerDir);
3065
+ const entry = entries?.slice().reverse().find((row) => row.event === 'pr-open-failed');
3066
+ childPrFailure = podPrOpenFailure(entry?.data, childFailure.rawError);
3067
+ } catch { /* child ledger is optional; terminal result remains available */ }
3068
+ childPrFailure ??= podPrOpenFailure(null, childFailure.rawError);
3069
+ }
3070
+ if (childPrFailure && disposition) {
3071
+ disposition.childEvidence = {
3072
+ ...(disposition.childEvidence ?? {}),
3073
+ prFailure: {
3074
+ stage: childPrFailure.prFailureStage,
3075
+ source: childPrFailure.prFailureSource,
3076
+ errorTail: childPrFailure.childErrorTail,
3077
+ ...(childPrFailure.blockingPrNumber ? { blockingPrNumber: childPrFailure.blockingPrNumber } : {}),
3078
+ ...(childPrFailure.candidateCardId ? { candidateCardId: childPrFailure.candidateCardId } : {}),
3079
+ ...(childPrFailure.blockingCardId ? { blockingCardId: childPrFailure.blockingCardId } : {}),
3080
+ },
3081
+ };
3082
+ }
2524
3083
  let noResultDiagnostic: string | null = null;
2525
- // POD-NORESULT: the Job runs with backoffLimit 0, so any non-zero child exit is «BackoffLimitExceeded» — not a reason to skip the ledger.
2526
- if (state === 'failed' && containerExitCode !== null && containerExitCode !== 0 && !disposition && !oomKilled && failedReason !== 'DeadlineExceeded') {
3084
+ // POD-NORESULT: a terminal Job without a parsed disposition is incomplete even when the Job condition says Complete.
3085
+ const noResultCandidate = !disposition && !oomKilled && failedReason !== 'DeadlineExceeded' &&
3086
+ (state === 'complete' || (state === 'failed' && containerExitCode !== 0));
3087
+ if (noResultCandidate) {
2527
3088
  try { noResultDiagnostic = podNoResultDiagnostic(readFileSync(runLedgerPath(liveChildRunId, ledgerDir), 'utf8')); }
2528
3089
  catch { /* ledger not returned: do not invent a stage */ }
2529
3090
  noResultDiagnostic ??= 'child terminal result missing; last ledger stage=unknown; round=unknown; mustFix=unknown';
2530
3091
  }
2531
- if (childFailure) debug.log('self-implement.pod', 'child-error', { job: name, stage: childFailure.stage, error: childFailure.error }, { compact: { stringMax: 500 } });
3092
+ // POD-NORESULT-LOG-TAIL: the Job runs with backoffLimit 0, so the failed Pod — and its container log —
3093
+ // is collected right after this. When the child ledger cannot even name the stage, read the tail here,
3094
+ // before deletion, so the parent keeps a maskable reason instead of `childEvidence=null`.
3095
+ let noResultLogEvidence: Record<string, unknown> | null = null;
3096
+ if (noResultDiagnostic?.includes('stage=unknown')) {
3097
+ const logRead = readPodNoResultLogTail({ run: kubectl, namespace, job: name, maxLines: POD_NO_RESULT_LOG_TAIL_LINES });
3098
+ if ('tail' in logRead) {
3099
+ noResultLogEvidence = { stderr: logRead.tail.join('\n'), logTail: logRead.tail, logTailLines: logRead.tail.length };
3100
+ debug.log('self-implement.pod', 'no-result-log-tail', { job: name, childRunId: liveChildRunId, diagnostic: noResultDiagnostic, lines: logRead.tail.length, logTail: logRead.tail }, { compact: { stringMax: 500 } });
3101
+ } else {
3102
+ noResultLogEvidence = { stderr: `log-unavailable: ${logRead.unavailable}` };
3103
+ debug.log('self-implement.pod', 'no-result-log-tail', { job: name, childRunId: liveChildRunId, diagnostic: noResultDiagnostic, logUnavailable: logRead.unavailable }, { level: 'warn' });
3104
+ }
3105
+ }
3106
+ if (childFailure) debug.log('self-implement.pod', 'child-error', { job: name, stage: childFailure.stage, error: childFailure.error, ...(childPrFailure ?? {}) }, { compact: { stringMax: 500 } });
2532
3107
  if (noResultDiagnostic && !noResultDiagnostic.includes('stage=unknown')) {
2533
3108
  debug.log('self-implement.pod', 'no-result-ledger-diagnostic', { job: name, childRunId: liveChildRunId, diagnostic: noResultDiagnostic }, { compact: { stringMax: 500 } });
2534
3109
  }
3110
+ // POD-LAUNCH-PROVENANCE — append-only «finished» row paired with its started row on job·run.
3111
+ // Keyed on launchRunId like the started row; a reattached Job's inherited child run rides along as childRun.
3112
+ // exitCode stays null when the container's code was not read (never filled in as 0).
3113
+ const finishedRow = { job: name, run: launchRunId, ...(liveChildRunId !== launchRunId ? { childRun: liveChildRunId } : {}),
3114
+ attempt: oomRetried ? 2 : 1, phase: state, exitCode: containerExitCode,
3115
+ reason: failedReason || containerReason || (state === 'complete' ? 'complete' : 'unknown') };
3116
+ try { appendRunLedgerEntry({ runId: ledgerRunId, event: POD_LAUNCH_FINISHED_EVENT, timestamp: new Date((options.now ?? Date.now)()).toISOString(), data: finishedRow }, ledgerDir); }
3117
+ catch (error) { debug.log('pod.provenance', 'history-unavailable', { job: name, row: 'finished', reason: error instanceof Error ? error.message : String(error) }, { level: 'warn' }); }
3118
+ debug.log('pod.provenance', 'job-finished', finishedRow);
2535
3119
  debug.log('self-implement.pod', 'job-finished', { job: name, ...(member ? { context: member.context } : {}), state, containerReason, stage: disposition?.stage ?? null, prUrl: disposition?.prUrl ?? null, childRunId: liveChildRunId, ledgerCompleteness,
3120
+ ...(podTermination ? { podTermination } : {}),
2536
3121
  ...(state === 'failed' && !oomKilled && failedReason !== 'DeadlineExceeded'
2537
3122
  ? { ...(childFailure ? { childStage: childFailure.stage } : disposition?.stage ? { childStage: disposition.stage } : {}),
2538
3123
  // POD-NORESULT: a parsed result row without error text is not a missing result line (UX 10-07 01:01: 28 of 28 exit-2 Jobs printed one).
2539
- childError: childFailure?.error ?? (disposition?.stage ? 'result-without-error' : 'no-result-line') } : {}) },
2540
- state === 'failed' && !oomKilled && failedReason !== 'DeadlineExceeded' ? { compact: { stringMax: 500 } } : undefined);
3124
+ childError: childFailure?.error ?? (disposition?.stage ? 'result-without-error' : 'no-result-line'),
3125
+ ...(childPrFailure ?? {}) } : {}) },
3126
+ (state === 'failed' && !oomKilled && failedReason !== 'DeadlineExceeded') ? { compact: { stringMax: 500 } } : undefined);
2541
3127
  // POD-LIFECYCLE-STAGES: one `pod.lifecycle stages` line — clone · mirror · run come from the Pod's own
2542
3128
  // `ELANOUS_POD_STAGE` markers (SI-POD-STAGE-MARKERS), read from that Pod's full named log. ⛔ No `--tail=400` fallback:
2543
3129
  // a tail can lose the real `run start` and pair a child-printed look-alike with the real `run end` (review mf) —
@@ -2606,10 +3192,11 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
2606
3192
  appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-oom-retry-finished', data: { job: name, executionKey, fromChildRunId: retryFromChildRunId, childRunId: liveChildRunId, state } }, ledgerDir);
2607
3193
  }
2608
3194
  if (goalFile) {
3195
+ const noResultOutcome = state === 'failed' && containerExitCode !== null && noResultDiagnostic ? 'abandoned' : 'pod-no-result';
2609
3196
  const record: GoalExecutionRecord = {
2610
3197
  runId: liveChildRunId,
2611
3198
  stage: (state === 'aborted' || !disposition ? 'aborted' : disposition.stage) as GoalExecutionRecord['stage'],
2612
- outcome: (!disposition ? noResultDiagnostic ? 'abandoned' : 'pod-no-result' : state === 'complete' && disposition.merged ? 'merged' : state === 'complete' && disposition.ok ? 'completed' : 'abandoned') as GoalExecutionRecord['outcome'],
3199
+ outcome: (!disposition ? noResultOutcome : state === 'complete' && disposition.merged ? 'merged' : state === 'complete' && disposition.ok ? 'completed' : 'abandoned') as GoalExecutionRecord['outcome'],
2613
3200
  ok: state === 'complete' && (disposition?.ok ?? false),
2614
3201
  ...(disposition?.prNumber !== undefined ? { prNumber: disposition.prNumber } : {}),
2615
3202
  completedAt: new Date().toISOString(),
@@ -2619,16 +3206,41 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
2619
3206
  }
2620
3207
  // The host still has the complete Pod log here. The CLI only retains a short stdout tail,
2621
3208
  // so preserve the parsed child facts in the parent ledger before that tail is returned.
2622
- if (state === 'failed' || state === 'aborted') {
3209
+ if (state === 'failed' || state === 'aborted' || noResultLogEvidence || (state === 'complete' && noResultDiagnostic !== null)) {
2623
3210
  // Every terminal attempt writes a marker, including one with no parseable child facts.
2624
3211
  // The next attempt must never inherit an earlier Pod's classification.
2625
- try { appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-child-evidence', data: { childEvidence: disposition?.childEvidence ?? null, childRunId: liveChildRunId, job: name } }, ledgerDir); }
3212
+ const childEvidence = noResultLogEvidence ?? disposition?.childEvidence ?? null;
3213
+ try { appendRunLedgerEntry({ runId: ledgerRunId, event: 'pod-child-evidence', data: { childEvidence, childRunId: liveChildRunId, job: name, ...(podTermination ? { podTermination } : {}) } }, ledgerDir); }
2626
3214
  catch (error) { debug.log('self-implement.pod', 'child-evidence-write-failed', { job: name, childRunId: liveChildRunId, reason: error instanceof Error ? error.message : String(error) }, { level: 'warn' }); }
2627
3215
  }
2628
- const terminalRecord = classifyPodTerminal({ childRunId: liveChildRunId, state, disposition, env,
2629
- prBody: options.draftResidue ? undefined : podTerminalPrBody(disposition),
2630
- goalText: parentGoal ?? input.feature, at: new Date().toISOString() });
2631
- try { await (options.draftResidue ?? applyPodTerminal)(terminalRecord, { ledgerRunId, ledgerDir }); }
3216
+ let prLookup: OpenPrByBranch | undefined;
3217
+ const terminalBranch = typeof disposition?.branch === 'string' ? disposition.branch.trim() : '';
3218
+ if (disposition && (state === 'failed' || state === 'aborted' || disposition.ok === false)
3219
+ && (!Number.isSafeInteger(disposition.prNumber) || (disposition.prNumber ?? 0) <= 0) && terminalBranch) {
3220
+ try { prLookup = await (options.lookupOpenPrByBranch ?? defaultLookupOpenPrByBranch)(terminalBranch, repoUrl); }
3221
+ catch { prLookup = { status: 'failed', reason: 'pr-lookup-threw' }; }
3222
+ }
3223
+ const terminalDisposition = prLookup?.status === 'found' && disposition
3224
+ ? { ...disposition, prNumber: prLookup.prNumber, prUrl: prLookup.prUrl } : disposition;
3225
+ if (prLookup?.status === 'found') debug.log('self-implement.pod', 'pr-terminal-bound-by-branch', {
3226
+ branch: terminalBranch, prNumber: prLookup.prNumber, prUrl: prLookup.prUrl, status: 'found', job: name });
3227
+ else if (prLookup) debug.log('self-implement.pod', 'pr-terminal-branch-lookup', {
3228
+ branch: terminalBranch, status: prLookup.status, reason: prLookup.reason, job: name });
3229
+ const terminalRecord = { ...classifyPodTerminal({ childRunId: liveChildRunId, state, disposition: terminalDisposition, env,
3230
+ prBody: options.draftResidue || prLookup?.status === 'found' ? undefined : podTerminalPrBody(terminalDisposition),
3231
+ goalText: parentGoal ?? input.feature, at: new Date().toISOString() }),
3232
+ ...(prLookup ? { prLookup: prLookup.status === 'found' ? { status: 'found', source: 'branch-lookup' }
3233
+ : { status: prLookup.status, reason: prLookup.reason } } : {}),
3234
+ ...(prLookup?.status === 'found' ? { prNumberSource: 'branch-lookup' } : {}) };
3235
+ try {
3236
+ if (options.draftResidue) await options.draftResidue(terminalRecord, { ledgerRunId, ledgerDir });
3237
+ else if (prLookup?.status === 'found') {
3238
+ // This lookup only repairs the host ledger's PR identity. The draft sweeper owns PR writes.
3239
+ const alreadyRecorded = loadRunLedger(ledgerRunId, ledgerDir)?.some((entry) =>
3240
+ entry.event === 'pr-terminal' && entry.data.childRunId === liveChildRunId);
3241
+ if (!alreadyRecorded) appendRunLedgerEntry({ runId: ledgerRunId, event: 'pr-terminal', data: terminalRecord }, ledgerDir);
3242
+ } else await applyPodTerminal(terminalRecord, { ledgerRunId, ledgerDir });
3243
+ }
2632
3244
  catch (error) { debug.log('draft.residue', 'failed', { childRunId: liveChildRunId, prNumber: terminalRecord.prNumber,
2633
3245
  terminalClass: terminalRecord.terminalClass, owner: terminalRecord.owner, ownerSource: terminalRecord.ownerSource,
2634
3246
  step: 'apply', reason: error instanceof Error ? error.message : String(error) }); }
@@ -2648,9 +3260,15 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
2648
3260
  const finishedDisposition = deadlineExceeded && disposition
2649
3261
  ? { ...disposition, failureClassification: 'run-deadline-exceeded' as const }
2650
3262
  : disposition;
2651
- const podExitCode = state === 'complete' && disposition?.stage !== 'host-regate-failed' && disposition?.ok !== false ? 0 : 1;
3263
+ // A gate environment deficit can exit the child with 1 while still yielding a
3264
+ // verified repair bundle. The host rejudges the new PR head after CAS apply.
3265
+ const podExitCode = repairPodExitCode({ state, repairCommitOnly: !!options.repairCommitOnly,
3266
+ repairArtifactReady, disposition });
2652
3267
  // SCHED-CONTRACT: a failed child must say why outside the Pod, not «no error diagnostic».
2653
- const podReason = podExitCode === 1 ? extractPodFailureReason({ logs, logTailReason, containerReason, jobReason: failedReason || undefined, deadlineSeconds, result: disposition ? { stage: disposition.stage, prUrl: disposition.prUrl ?? null, prNumber: disposition.prNumber ?? null } : null }) : undefined;
3268
+ const rawPodReason = podExitCode === 1 ? extractPodFailureReason({ logs, logTailReason, containerReason, jobReason: failedReason || undefined, deadlineSeconds, result: disposition ? { stage: disposition.stage, prUrl: disposition.prUrl ?? null, prNumber: disposition.prNumber ?? null } : null }) : undefined;
3269
+ const podReason = rawPodReason
3270
+ ? redactSecretText(noResultLogEvidence ? boundedPodLogText(rawPodReason, POD_NO_RESULT_LOG_REASON_MAX_CHARS) : rawPodReason)
3271
+ : undefined;
2654
3272
  if (podReason) debug.log('self-implement.pod', 'failure-reason', { job: name, reason: podReason });
2655
3273
  if (podExitCode === 1) {
2656
3274
  // HARNESS-FULL-GRAPH — 실패 쪽 두 노드: 결과 해석(result-parse) → 수확(salvage, Pod Job 이 push 한 줄로 판정).
@@ -2668,7 +3286,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
2668
3286
  ? { error: { code: 'pod-gh-login-failed', message: `Job ${name} GitHub 앱 로그인 실패 (container=${containerReason ?? 'unknown'}/7, attempt=${ghLoginRetried ? 2 : 1}) — 새 토큰으로 ${ghLoginRetried ? '1회 재시도했으나 다시 실패' : '재시도 불가'}` } }
2669
3287
  : oomKilled
2670
3288
  ? { error: { code: 'pod-oom-killed', message: `Job ${name} failed (OOMKilled/${containerExitCode ?? 'unknown'}, memoryLimit=${memoryLimit})${oomRetried || (memoryGi(reattachedMemoryLimit ?? memoryLimit) ?? 0) >= (memoryGi(podMemoryLimitFor(input.feature, { ...env, ELANOUS_POD_MEMORY_TIER: 'high' }).limit) ?? Infinity) ? ` — OOM · ${oomRetried ? retryTier : 'high'} 에서도` : ''}\n${formatLastMemSample(samples.at(-1))}` } }
2671
- : { error: { code: 'pod-job-failed', message: `Job ${name} failed${failedReason || containerReason ? ` (${[failedReason, containerReason ? `container=${containerReason}/${containerExitCode ?? 'unknown'}` : ''].filter(Boolean).join(', ')})` : ''}${childFailure ? ` — childStage=${childFailure.stage} · childError=${childFailure.error}` : disposition?.stage ? ` — childStage=${disposition.stage} · reason=${podReason}` : ` — childError=no-result-line · reason=${podReason}`}${noResultDiagnostic ? ` · ${noResultDiagnostic}` : ''}` } }
3289
+ : { error: { code: 'pod-job-failed', message: `Job ${name} failed${failedReason || containerReason ? ` (${[failedReason, containerReason ? `container=${containerReason}/${containerExitCode ?? 'unknown'}` : ''].filter(Boolean).join(', ')})` : ''}${childFailure ? ` — childStage=${childFailure.stage} · childError=${childFailure.error}` : disposition?.stage ? ` — childStage=${disposition.stage} · reason=${podReason}` : ` — childError=no-result-line · reason=${podReason}`}${noResultDiagnostic && containerExitCode !== null ? ` · ${noResultDiagnostic}` : ''}` } }
2672
3290
  : podReason && !childFailure?.error ? { error: { code: 'pod-child-failed', message: podReason } } : {}),
2673
3291
  ...(finishedDisposition ? { disposition: finishedDisposition } : {}),
2674
3292
  };
@@ -2736,7 +3354,47 @@ export function recordPodSalvage(logs: string, job: string, log: (category: stri
2736
3354
  return rows.map((row) => row.branch);
2737
3355
  }
2738
3356
 
2739
- function lastPodChildFailure(logs: string): { stage: string; error: string } | null {
3357
+ type PodPrOpenFailure = {
3358
+ childErrorTail: string;
3359
+ prFailureStage: 'pr-open-failed';
3360
+ prFailureSource: 'child-ledger' | 'terminal-result';
3361
+ blockingPrNumber?: number;
3362
+ candidateCardId?: string;
3363
+ blockingCardId?: string;
3364
+ };
3365
+
3366
+ /** Preserve the actionable tail independently of the bounded human error. Never infer a card from a PR number. */
3367
+ function podPrOpenFailure(ledger: Record<string, unknown> | undefined | null, terminalError: string): PodPrOpenFailure | null {
3368
+ const ledgerError = typeof ledger?.error === 'string' ? ledger.error : '';
3369
+ // A previous rework round can leave a pr-open-failed row behind. Only a terminal PR failure may consume it.
3370
+ if (!/draft PR 생성 실패|PR (?:gh|guard) 실패|same-cell reuse:/u.test(terminalError)) return null;
3371
+ // The terminal must contain the complete ledger error. A shared prefix such as "same-cell"
3372
+ // cannot distinguish an earlier rework round from the current failed attempt.
3373
+ const matchingLedger = ledgerError.length >= 20 && terminalError.includes(ledgerError);
3374
+ const source = terminalError || ledgerError;
3375
+ const sanitized = redactSecretText(source).replace(/\/home\/[^/\s]+\//gu, '~/');
3376
+ const tail = sanitized.split(/\r\n|\n|\r/u).filter(Boolean).at(-1) ?? sanitized;
3377
+ const terminalHasIds = /(?:blockers=|blockingPr=|candidateCardId=)/u.test(terminalError);
3378
+ const detail = `${terminalError}\n${matchingLedger ? ledgerError : ''}`;
3379
+ const blocker = /(?:blockers=|blockingPr=)\s*#?(\d+)(?::([A-Za-z0-9_-]{1,128}))?/u.exec(detail);
3380
+ const prNumber = blocker ? Number(blocker[1]) : null;
3381
+ const candidate = /candidateCardId=([A-Za-z0-9_-]{1,128})/u.exec(detail)?.[1];
3382
+ const card = blocker?.[2];
3383
+ return {
3384
+ childErrorTail: tail.slice(-240),
3385
+ prFailureStage: 'pr-open-failed',
3386
+ prFailureSource: terminalHasIds || !matchingLedger ? 'terminal-result' : 'child-ledger',
3387
+ ...(prNumber && Number.isSafeInteger(prNumber) ? { blockingPrNumber: prNumber } : {}),
3388
+ ...(candidate && candidate !== 'unknown' ? { candidateCardId: candidate } : {}),
3389
+ ...(card && card !== 'unknown' ? { blockingCardId: card } : {}),
3390
+ };
3391
+ }
3392
+
3393
+ function lastPodChildFailure(logs: string): { stage: string; error: string; rawError: string } | null {
3394
+ // Keep canonical duplicate-cap attribution while preserving the raw terminal error
3395
+ // used by PR-open failure diagnosis below.
3396
+ const attribution = podChildFailureAttribution(logs);
3397
+ if (!attribution) return null;
2740
3398
  for (const line of logs.split('\n').reverse()) {
2741
3399
  if (!line.trimStart().startsWith('{')) continue;
2742
3400
  let result: unknown;
@@ -2746,14 +3404,65 @@ function lastPodChildFailure(logs: string): { stage: string; error: string } | n
2746
3404
  const terminal = podTerminalRow(row);
2747
3405
  if (!terminal) continue;
2748
3406
  if (terminal.ok !== false || typeof terminal.error !== 'string') return null;
2749
- const lines = terminal.error.replace(/\/home\/[^/\s]+\//g, '~/').split(/\r\n|\n|\r/);
2750
- const first = lines[0]!.slice(0, 240);
2751
- const last = lines.at(-1)!.slice(0, 240);
2752
- return { stage: terminal.stage, error: first === last ? first : `${first}\n${last}` };
3407
+ if (terminal.stage !== attribution.stage) return null;
3408
+ return { ...attribution, error: redactSecretText(attribution.error), rawError: terminal.error };
2753
3409
  }
2754
3410
  return null;
2755
3411
  }
2756
3412
 
3413
+ /** POD-NORESULT-LOG-TAIL: how many container-log lines a failed Pod leaves behind for the parent. */
3414
+ export const POD_NO_RESULT_LOG_TAIL_LINES = 80;
3415
+ const POD_NO_RESULT_LOG_TAIL_LINE_CHARS = 512;
3416
+ const POD_NO_RESULT_LOG_TAIL_MAX_CHARS = 8_192;
3417
+ const POD_NO_RESULT_LOG_REASON_MAX_CHARS = 512;
3418
+
3419
+ function boundedPodLogText(source: string, maxChars: number): string {
3420
+ const redacted = redactSecretText(source).replace(/[\r\n\t]+/g, ' ').replace(/\s+/g, ' ').trim();
3421
+ return redacted.length > maxChars ? `${redacted.slice(0, maxChars)}…[truncated]` : redacted;
3422
+ }
3423
+
3424
+ /** 로그 꼬리 — 마지막 N줄만, 한 줄씩 비밀값 마스킹. */
3425
+ export function podNoResultLogTailLines(source: string, maxLines: number = POD_NO_RESULT_LOG_TAIL_LINES): string[] {
3426
+ const lines = source.replace(/\r\n/g, '\n').split('\n');
3427
+ while (lines.length && lines[lines.length - 1] === '') lines.pop();
3428
+ const result: string[] = [];
3429
+ let chars = 0;
3430
+ for (const line of lines.slice(-Math.max(0, maxLines)).reverse()) {
3431
+ if (chars >= POD_NO_RESULT_LOG_TAIL_MAX_CHARS) break;
3432
+ const bounded = boundedPodLogText(line, Math.min(POD_NO_RESULT_LOG_TAIL_LINE_CHARS, POD_NO_RESULT_LOG_TAIL_MAX_CHARS - chars));
3433
+ if (!bounded) continue;
3434
+ result.push(bounded);
3435
+ chars += bounded.length;
3436
+ }
3437
+ return result.reverse();
3438
+ }
3439
+
3440
+ export type PodNoResultLogRead = { tail: string[] } | { unavailable: string };
3441
+
3442
+ /**
3443
+ * POD-NORESULT-LOG-TAIL: the terminal Job (`backoffLimit 0`) is collected right after it fails, so the
3444
+ * container log has to be read while the Pod still exists. Never throws — an unreadable log becomes a reason.
3445
+ */
3446
+ export function readPodNoResultLogTail(args: {
3447
+ run: (args: readonly string[]) => { status: number | null; stdout: string; stderr: string };
3448
+ namespace: string;
3449
+ job: string;
3450
+ maxLines?: number;
3451
+ }): PodNoResultLogRead {
3452
+ const maxLines = args.maxLines ?? POD_NO_RESULT_LOG_TAIL_LINES;
3453
+ try {
3454
+ const result = args.run(['-n', args.namespace, 'logs', `job/${args.job}`, `--tail=${maxLines}`]);
3455
+ if (result.status !== 0) {
3456
+ const reason = boundedPodLogText(result.stderr || result.stdout || '', POD_NO_RESULT_LOG_REASON_MAX_CHARS);
3457
+ return { unavailable: reason || `kubectl logs exit ${result.status}` };
3458
+ }
3459
+ const tail = podNoResultLogTailLines(result.stdout, maxLines);
3460
+ return tail.length ? { tail } : { unavailable: 'empty log output' };
3461
+ } catch (error) {
3462
+ return { unavailable: boundedPodLogText(error instanceof Error ? error.message : String(error), POD_NO_RESULT_LOG_REASON_MAX_CHARS) || 'unknown error' };
3463
+ }
3464
+ }
3465
+
2757
3466
  /** 사람용 런 결과 한 줄 — 수확 브랜치가 있으면 «수확할 브랜치: …» 를 붙인다. */
2758
3467
  export function podRunResultLine(logs: string, branches: readonly string[]): string {
2759
3468
  const result = logs.split('\n').filter((line) => !line.startsWith('ELANOUS_MEM ')).join('\n').slice(-4000);