elanous 0.2.31 → 0.2.32

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 (282) 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/{1834-50625904996a8fa8.js → 1834-98b64172bc1efe77.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/69-0a2c8209c6d4a158.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/8097-9530c03bcc550f02.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/approvals/page-504204b58c82c253.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3535a4a69f1fafaa.js → page-a9717a67c2eecbaf.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/board/page-5c9ec30aee62dfbf.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/botlab/page-584191533721fa16.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/bots/page-73c4b4bb3ac7ab5f.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/ceo/{page-5a503541353e60a8.js → page-c0f1ca371f710b06.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/chat/{page-6025101384e361a5.js → page-46430d45a9e04eac.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/consult/page-3b2ebf1dc64ff0be.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/control/{page-a645f00a06483e9a.js → page-a27b1e5a0d1b308f.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/decisions/page-25654b82094f7d22.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-e9bab7413b028b28.js → page-8ea49a4c6dd2c6e7.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/editor/page-ce5113aefea0a8d1.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-e547e6ce55f1fb33.js → page-0e5a41fac1c75396.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-d27e1b9850fd54bf.js → page-31aaf1ed37c6df9a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-3dcc1fa411e1f4e0.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-0fec29c7ba01894b.js → page-c69247e7134d329d.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-339a07410cd6f36b.js → layout-66dc5cd3a0999abe.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-26ff7ff651a7217d.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/live-run/page-617b3976981efa96.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/loops/{page-2fd93ed8c9f16580.js → page-d256f5d5d9552823.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/market/{page-9b564dbff0fefc35.js → page-ce8f1907b8a6101c.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-33e182a316263116.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/morning/{page-814ba51385225776.js → page-32866485913d6a04.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/{not-found-397d479626e5a459.js → not-found-7607cca9b84950c6.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-af1bba4e3251b728.js → page-eeab86176b03dc3b.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-3ad3be5e7d3fdce9.js → page-5b93dbae3432b996.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-77a6cb90a9cf2b10.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-4da93ad35d4a8d28.js → page-f47e768b5b242b5a.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/outputs/page-eb41aa3367fc2505.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/page-4d13ab0b8a267e3f.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/reflection/page-84dad32ff0fc0831.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-3a658d5fa6d16a25.js → page-95306f892aa2d0ee.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-0157de6e85f8bb6b.js → page-940fb6c217f6b470.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-0a07363b130182a1.js → page-8c66a3b24a745630.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/settings/{page-841d73d4ac44ed49.js → page-7b675dfd35c36fc9.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-6f43a3d1146e6377.js → page-8bd0d3cf7665bcfc.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/setup/{page-521ee23ac8c0abfa.js → page-c219f2ed171131be.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/share/{page-a9992ee285efaa83.js → page-40f8fa21bdae955b.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-3d48e85fa89ec04f.js → page-468f8313e4f1fbd9.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/tasks/page-7bbb8fa353a246c4.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/term/page-04d00ae9b0890349.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/today/{page-233c71cd5f68f0d7.js → page-4214b855d9233ea8.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/trace/{page-70151d78de2f375e.js → page-936752c7aa58762e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/vault/{page-dcac33a3e2cb72c0.js → page-ec052ad359df1797.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-8305570551091338.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-0448145ccd5c45c1.js → page-fad650e4793adb65.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/workspace/page-d90e64743826e1d8.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-a16bcfd49af86a32.js +1 -0
  54. package/apps/pwa/out/_next/static/css/b5d88eba78a3d7bf.css +1 -0
  55. package/apps/pwa/out/approvals/index.html +1 -1
  56. package/apps/pwa/out/approvals/index.txt +14 -14
  57. package/apps/pwa/out/autopilot/index.html +1 -1
  58. package/apps/pwa/out/autopilot/index.txt +14 -14
  59. package/apps/pwa/out/board/index.html +1 -1
  60. package/apps/pwa/out/board/index.txt +14 -14
  61. package/apps/pwa/out/botlab/index.html +1 -1
  62. package/apps/pwa/out/botlab/index.txt +14 -14
  63. package/apps/pwa/out/bots/index.html +1 -1
  64. package/apps/pwa/out/bots/index.txt +14 -14
  65. package/apps/pwa/out/ceo/index.html +1 -1
  66. package/apps/pwa/out/ceo/index.txt +14 -14
  67. package/apps/pwa/out/chat/index.html +1 -1
  68. package/apps/pwa/out/chat/index.txt +14 -14
  69. package/apps/pwa/out/consult/index.html +1 -1
  70. package/apps/pwa/out/consult/index.txt +14 -14
  71. package/apps/pwa/out/control/index.html +1 -1
  72. package/apps/pwa/out/control/index.txt +14 -14
  73. package/apps/pwa/out/decisions/index.html +1 -1
  74. package/apps/pwa/out/decisions/index.txt +14 -14
  75. package/apps/pwa/out/design-check/index.html +1 -1
  76. package/apps/pwa/out/design-check/index.txt +14 -14
  77. package/apps/pwa/out/editor/index.html +1 -1
  78. package/apps/pwa/out/editor/index.txt +14 -14
  79. package/apps/pwa/out/exec/index.html +1 -1
  80. package/apps/pwa/out/exec/index.txt +14 -14
  81. package/apps/pwa/out/field/index.html +1 -1
  82. package/apps/pwa/out/field/index.txt +14 -14
  83. package/apps/pwa/out/healthz +1 -1
  84. package/apps/pwa/out/index.html +1 -1
  85. package/apps/pwa/out/index.txt +14 -14
  86. package/apps/pwa/out/inside/index.html +1 -1
  87. package/apps/pwa/out/inside/index.txt +14 -14
  88. package/apps/pwa/out/intake/index.html +1 -1
  89. package/apps/pwa/out/intake/index.txt +14 -14
  90. package/apps/pwa/out/live/index.html +1 -1
  91. package/apps/pwa/out/live/index.txt +17 -14
  92. package/apps/pwa/out/live-run/index.html +1 -1
  93. package/apps/pwa/out/live-run/index.txt +14 -14
  94. package/apps/pwa/out/loops/index.html +1 -1
  95. package/apps/pwa/out/loops/index.txt +14 -14
  96. package/apps/pwa/out/market/index.html +1 -1
  97. package/apps/pwa/out/market/index.txt +14 -14
  98. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  99. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  100. package/apps/pwa/out/morning/index.html +1 -1
  101. package/apps/pwa/out/morning/index.txt +14 -14
  102. package/apps/pwa/out/observatory/index.html +1 -1
  103. package/apps/pwa/out/observatory/index.txt +14 -14
  104. package/apps/pwa/out/ops/checklist/index.html +1 -1
  105. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  106. package/apps/pwa/out/ops/release/index.html +1 -1
  107. package/apps/pwa/out/ops/release/index.txt +14 -14
  108. package/apps/pwa/out/ops/seats/index.html +1 -1
  109. package/apps/pwa/out/ops/seats/index.txt +14 -14
  110. package/apps/pwa/out/outputs/index.html +1 -1
  111. package/apps/pwa/out/outputs/index.txt +14 -14
  112. package/apps/pwa/out/reflection/index.html +1 -1
  113. package/apps/pwa/out/reflection/index.txt +15 -15
  114. package/apps/pwa/out/scheduler/index.html +1 -1
  115. package/apps/pwa/out/scheduler/index.txt +14 -14
  116. package/apps/pwa/out/sessions/index.html +1 -1
  117. package/apps/pwa/out/sessions/index.txt +14 -14
  118. package/apps/pwa/out/settings/devices/index.html +1 -1
  119. package/apps/pwa/out/settings/devices/index.txt +14 -14
  120. package/apps/pwa/out/settings/index.html +1 -1
  121. package/apps/pwa/out/settings/index.txt +14 -14
  122. package/apps/pwa/out/setup/done/index.html +1 -1
  123. package/apps/pwa/out/setup/done/index.txt +14 -14
  124. package/apps/pwa/out/setup/index.html +1 -1
  125. package/apps/pwa/out/setup/index.txt +14 -14
  126. package/apps/pwa/out/share/index.html +1 -1
  127. package/apps/pwa/out/share/index.txt +14 -14
  128. package/apps/pwa/out/showroom/index.html +1 -1
  129. package/apps/pwa/out/showroom/index.txt +14 -14
  130. package/apps/pwa/out/tasks/index.html +1 -1
  131. package/apps/pwa/out/tasks/index.txt +14 -14
  132. package/apps/pwa/out/term/index.html +1 -1
  133. package/apps/pwa/out/term/index.txt +14 -14
  134. package/apps/pwa/out/today/index.html +1 -1
  135. package/apps/pwa/out/today/index.txt +14 -14
  136. package/apps/pwa/out/trace/index.html +1 -1
  137. package/apps/pwa/out/trace/index.txt +14 -14
  138. package/apps/pwa/out/vault/index.html +1 -1
  139. package/apps/pwa/out/vault/index.txt +14 -14
  140. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  141. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  142. package/apps/pwa/out/workflows/index.html +1 -1
  143. package/apps/pwa/out/workflows/index.txt +14 -14
  144. package/apps/pwa/out/workspace/index.html +1 -1
  145. package/apps/pwa/out/workspace/index.txt +14 -14
  146. package/apps/pwa/out/worktrees/index.html +1 -1
  147. package/apps/pwa/out/worktrees/index.txt +14 -14
  148. package/graphs/implement-loop.yaml +5 -0
  149. package/package.json +1 -1
  150. package/scripts/ci-typecheck-changed.ts +26 -1
  151. package/scripts/measure-si-task-cpu.ts +143 -0
  152. package/scripts/ratchets/test-interference-patterns.allowlist.txt +0 -2
  153. package/scripts/release-loop/prepare-node.ts +102 -4
  154. package/src/acp/core-turn-bridge.ts +16 -4
  155. package/src/acp/elanous-extensions.ts +3 -0
  156. package/src/acp/server.ts +3 -0
  157. package/src/acp/tool-trace.ts +186 -11
  158. package/src/agent/self-ambient-cache.ts +156 -0
  159. package/src/agent/self-ambient.ts +8 -1
  160. package/src/agent/task-notification.ts +5 -0
  161. package/src/agent-substrate/pr-reviewer.ts +3 -1
  162. package/src/ask-user-question/tool.ts +22 -0
  163. package/src/autopilot/build/llm-conflict-merge.ts +56 -10
  164. package/src/autopilot/pr-manager.ts +23 -5
  165. package/src/capture/frame-state-detect.ts +39 -0
  166. package/src/capture/tui-self-report.ts +134 -6
  167. package/src/chat/index.ts +61 -15
  168. package/src/chat/pickers/modal-runtime.ts +6 -0
  169. package/src/chat/pickers/modals.ts +8 -2
  170. package/src/cli/ask-bare.ts +1 -0
  171. package/src/cli/import-cli.ts +12 -0
  172. package/src/cli/log-event-names.ts +10 -0
  173. package/src/cli/msg-cli.ts +1 -0
  174. package/src/cli/pod-cli.ts +14 -3
  175. package/src/cli/pr-cli.ts +67 -32
  176. package/src/cli/pr-land-base-read.ts +34 -0
  177. package/src/cli/pr-land-heavy-remote.ts +81 -25
  178. package/src/cli/pr-land-overlap-evidence.ts +46 -2
  179. package/src/cli/pr-land-pod-shadow-job.ts +26 -3
  180. package/src/cli/pr-land-refusal.ts +20 -5
  181. package/src/cli/pty-takeover-cli.ts +88 -14
  182. package/src/cli/sim-cli.ts +18 -0
  183. package/src/context-bus/ops-events.ts +6 -2
  184. package/src/core-turn/run-core-turn.ts +1 -0
  185. package/src/core-turn/run-goal-loop.ts +21 -0
  186. package/src/core-turn/types.ts +3 -0
  187. package/src/dashboard/acp-active-model.ts +42 -1
  188. package/src/dashboard/background-jobs-status.ts +66 -0
  189. package/src/dashboard/index.ts +117 -44
  190. package/src/dashboard/input/agent-wake.ts +242 -0
  191. package/src/dashboard/input/chat-main-entry-runtime.ts +5 -0
  192. package/src/dashboard/input/external-input-host.ts +23 -9
  193. package/src/dashboard/input/mouse-wiring.ts +108 -11
  194. package/src/dashboard/log-click-runtime.ts +118 -2
  195. package/src/dashboard/turn-preamble.ts +116 -40
  196. package/src/dashboard/turn-session-layers.ts +4 -2
  197. package/src/dashboard/turn-start-segments.ts +222 -0
  198. package/src/display/prompt-row-owner.ts +146 -0
  199. package/src/display/resize-listener.ts +33 -2
  200. package/src/fold-stack.ts +4 -2
  201. package/src/git-fs/index.ts +263 -36
  202. package/src/harness/gate-reserve.ts +87 -0
  203. package/src/harness/harness-incidents.ts +13 -0
  204. package/src/harness/harness-queue.ts +15 -0
  205. package/src/index.ts +8 -6
  206. package/src/input/external-input-queue.ts +34 -9
  207. package/src/llm/route-decision.ts +36 -0
  208. package/src/llm/tool-loop-steered-rejection.ts +8 -0
  209. package/src/llm-request-shape.ts +209 -0
  210. package/src/llm.ts +166 -267
  211. package/src/loops/orchestrator/tick.ts +24 -2
  212. package/src/maturity/feature-maturity.ts +1 -1
  213. package/src/mss/logging/render-categories.ts +5 -0
  214. package/src/ops/overview.ts +59 -2
  215. package/src/pr-tool/census.ts +19 -4
  216. package/src/pr-tool/inflight-registry.ts +143 -0
  217. package/src/pty-shell/pty-manifest.ts +12 -3
  218. package/src/pty-shell/tui-run-state.ts +34 -3
  219. package/src/seat-loop/traffic-signal.ts +2 -1
  220. package/src/self-dev/ask-launch-flow.ts +13 -0
  221. package/src/self-dev/draft-residue.ts +243 -24
  222. package/src/self-dev/shard-plan.ts +11 -3
  223. package/src/self-dev/shard-run.ts +103 -10
  224. package/src/self-implement/elanous-tui-spawn.ts +14 -0
  225. package/src/self-implement/follow-up-placement.ts +79 -0
  226. package/src/self-implement/gate-artifacts.ts +15 -4
  227. package/src/self-implement/gate-baseline.ts +96 -5
  228. package/src/self-implement/gate-cli.ts +7 -2
  229. package/src/self-implement/gate-remote.ts +26 -1
  230. package/src/self-implement/host-regate.ts +36 -4
  231. package/src/self-implement/orchestrator.ts +101 -11
  232. package/src/self-implement/run-ledger.ts +16 -14
  233. package/src/session-runtime/index.ts +22 -267
  234. package/src/shell-runner/background-surface.ts +15 -5
  235. package/src/skills/tools/agent.ts +39 -6
  236. package/src/skills/tools/list-dir.ts +11 -0
  237. package/src/skills/tools/schedule-wakeup.ts +408 -0
  238. package/src/skills/tools/shell.ts +245 -3
  239. package/src/status/bar.ts +26 -0
  240. package/src/steward/digest-delivery.ts +118 -0
  241. package/src/steward/hold-hygiene.ts +120 -0
  242. package/src/steward/triage.ts +10 -0
  243. package/src/surface/shell-runner-boot.ts +5 -2
  244. package/src/task-agent/task-hand.ts +24 -0
  245. package/src/task-orchestrator/surfaces/pod-command-job.ts +65 -11
  246. package/src/task-orchestrator/surfaces/pod-lease.ts +98 -0
  247. package/src/task-orchestrator/surfaces/pod-ledger-events.ts +1 -1
  248. package/src/task-orchestrator/surfaces/pod-pool.ts +3 -1
  249. package/src/task-orchestrator/surfaces/pod-reuse-cleanup.ts +10 -6
  250. package/src/task-orchestrator/surfaces/pod-warm-worker-pod.ts +525 -0
  251. package/src/task-orchestrator/surfaces/pod-warm-worker-runner.ts +90 -0
  252. package/src/task-orchestrator/surfaces/pod-warm-worker-runtime.ts +156 -2
  253. package/src/task-orchestrator/surfaces/self-implement-pod.ts +7 -5
  254. package/src/tui/chat/execution-badge.ts +5 -1
  255. package/src/tui/input-lag-probe.ts +230 -0
  256. package/src/tui.ts +4 -0
  257. package/src/ux-sim/scenario.ts +255 -0
  258. package/src/version/packed-revision.json +1 -1
  259. package/apps/pwa/out/_next/static/chunks/8097-92f87bd108322f66.js +0 -1
  260. package/apps/pwa/out/_next/static/chunks/app/approvals/page-bf3634a117322df7.js +0 -1
  261. package/apps/pwa/out/_next/static/chunks/app/board/page-b9f51447f757fbf2.js +0 -1
  262. package/apps/pwa/out/_next/static/chunks/app/botlab/page-2ebb8fd59b9461f9.js +0 -1
  263. package/apps/pwa/out/_next/static/chunks/app/bots/page-a2fc4e49a4fa4ba8.js +0 -1
  264. package/apps/pwa/out/_next/static/chunks/app/consult/page-f4b630cc8b5273cd.js +0 -1
  265. package/apps/pwa/out/_next/static/chunks/app/decisions/page-61ac22cbab1ee4ec.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/app/editor/page-c992eafe028b642e.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/app/inside/page-805ac2e137b7a121.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/app/live/page-349f2752d8cd37ad.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/live-run/page-d5c6dcf31c25cecc.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-62f4475d3c38fb9b.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-f219ccdbc0bf27df.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/outputs/page-456666766e5ce6fc.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/page-7404e377e8adb8c5.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/reflection/page-14456240ed2a36ad.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d2ced71132fac9b2.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/term/page-f08358bdf251adb9.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-149dc1caba411bb9.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9cb0c444903da65d.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-17e504c396e0e389.js +0 -1
  280. package/apps/pwa/out/_next/static/css/3f5fc6f96e2cf7b2.css +0 -1
  281. /package/apps/pwa/out/_next/static/{LSXfY0u9QtfDbejY0Ukf_ → IVKr3AlRUVLKsw2fh7eXF}/_buildManifest.js +0 -0
  282. /package/apps/pwa/out/_next/static/{LSXfY0u9QtfDbejY0Ukf_ → IVKr3AlRUVLKsw2fh7eXF}/_ssgManifest.js +0 -0
@@ -34,6 +34,8 @@ import {
34
34
 
35
35
  /** CLI `--deadline` 생략 시 — 기존 Pod Job 수명 상한과 같다. */
36
36
  export const POD_COMMAND_DEADLINE_SECONDS = POD_JOB_DEADLINE_SECONDS;
37
+ /** An opt-in shadow cannot let a synchronous kubectl call hide its host deadline for two minutes. */
38
+ export const POD_COMMAND_CANCEL_KUBECTL_TIMEOUT_MS = 15_000;
37
39
 
38
40
  export const POD_NETWORK_SKILLS = ['omni-crawl', 'omni-digest'] as const;
39
41
  export const POD_LITE_IMAGE = 'elanous-harness-lite:local';
@@ -414,6 +416,12 @@ export interface RunPodCommandOptions {
414
416
  * elsewhere. Omitted = poll without a bound (unchanged for every caller but the release gate).
415
417
  */
416
418
  statusUnreadableMs?: number;
419
+ /** Optional caller cancellation. Omitted callers keep their existing polling behavior. */
420
+ signal?: AbortSignal;
421
+ /** Optional host wall-clock deadline; checked even when a synchronous kubectl delayed the abort timer. */
422
+ hostDeadlineAtMs?: number;
423
+ /** Clock seam for deadline checks. */
424
+ nowMs?: () => number;
417
425
  kubectl?: Kubectl;
418
426
  /** A caller-owned scheduler is self-capped: acquires count only its own Jobs, never cluster occupancy (POOLGLOBAL-REG). */
419
427
  poolScheduler?: PodPoolScheduler;
@@ -486,14 +494,35 @@ export function podCommandTargetCommit(o: { remote: boolean; gitHead: () => stri
486
494
  return o.localImageCommit();
487
495
  }
488
496
 
497
+ /** Only opt-in callers can stop waiting on a pre-Job async operation. Late rejections stay observed. */
498
+ function awaitPodCommandAbort<T>(pending: Promise<T>, signal?: AbortSignal): Promise<T> {
499
+ if (!signal) return pending;
500
+ return new Promise<T>((resolve, reject) => {
501
+ const onAbort = () => { signal.removeEventListener('abort', onAbort); reject(signal.reason ?? new Error('pod command aborted')); };
502
+ signal.addEventListener('abort', onAbort, { once: true });
503
+ pending.then(
504
+ (value) => { signal.removeEventListener('abort', onAbort); resolve(value); },
505
+ (error) => { signal.removeEventListener('abort', onAbort); reject(error); },
506
+ );
507
+ if (signal.aborted) onAbort();
508
+ });
509
+ }
510
+
489
511
  /** 풀 자리 · 이미지 판 · Job 적용 · 완료 대기 · 산출 회수 · Secret 정리. */
490
512
  export async function runPodCommand(options: RunPodCommandOptions): Promise<PodCommandResult> {
513
+ const nowMs = options.nowMs ?? Date.now;
514
+ const ensureLive = () => {
515
+ if (options.signal?.aborted) throw options.signal.reason ?? new Error('pod command aborted');
516
+ if (options.hostDeadlineAtMs !== undefined && nowMs() >= options.hostDeadlineAtMs) throw new Error('pod shadow: host timeout');
517
+ };
518
+ ensureLive();
491
519
  const command = [...options.command];
492
520
  if (command.length === 0) throw new Error('pod command: 명령이 비었다');
493
521
  const skills = [...new Set((options.skills ?? []).map((s) => s.trim()).filter(Boolean))];
494
522
  const llm = options.llm;
495
523
  const log = options.log ?? ((c, e, d) => debug.log(c, e, d));
496
- const baseKubectl = options.kubectl ?? defaultKubectl;
524
+ const baseKubectl = options.kubectl ?? ((args: readonly string[], input?: string) =>
525
+ defaultKubectl(args, input, options.signal ? POD_COMMAND_CANCEL_KUBECTL_TIMEOUT_MS : undefined));
497
526
  const sleep = options.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
498
527
  const namespace = options.namespace ?? 'elanous-test';
499
528
  const image = options.image ?? podCommandImageFor(skills, options.clone === true, options.lite === true);
@@ -534,18 +563,32 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
534
563
  // concurrency cap — its Jobs count against the spec caps, not the cluster's goal Pods (#24253 → 0.2.15 gate ran 1 shard).
535
564
  const selfCapped = options.poolScheduler !== undefined;
536
565
  for (;;) {
537
- member = await pool.tryAcquire(undefined, undefined, { selfCapped });
566
+ ensureLive();
567
+ const acquiring = pool.tryAcquire(undefined, undefined, { selfCapped, signal: options.signal });
568
+ try { member = await awaitPodCommandAbort(acquiring, options.signal); }
569
+ catch (error) {
570
+ // If cancellation won the await at the same instant acquisition completed, return that lease.
571
+ void acquiring.then((late) => { if (late) pool!.release(late); }, () => {});
572
+ throw error;
573
+ }
538
574
  if (member) break;
539
- await sleep(options.pollMs ?? 15_000);
575
+ await awaitPodCommandAbort(sleep(options.pollMs ?? 15_000), options.signal);
540
576
  }
541
577
  }
578
+ if (options.signal?.aborted) {
579
+ if (member && pool) pool.release(member);
580
+ ensureLive();
581
+ }
542
582
  const currentContext = member || options.kubectl ? null : baseKubectl(['config', 'current-context']);
543
583
  const context = member?.context ?? (currentContext?.status === 0 ? currentContext.stdout.trim() : '');
544
- const kubectl: Kubectl = (args, stdin) => baseKubectl(context ? ['--context', context, ...args] : [...args], stdin);
545
- const cleanupSecret = () => { kubectl(['-n', namespace, 'delete', 'secret', '-l', own, '--ignore-not-found']); };
584
+ const kubectlRaw: Kubectl = (args, stdin) => baseKubectl(context ? ['--context', context, ...args] : [...args], stdin);
585
+ const kubectl: Kubectl = (args, stdin) => { ensureLive(); const result = kubectlRaw(args, stdin); ensureLive(); return result; };
586
+ const cleanupSecret = () => { kubectlRaw(['-n', namespace, 'delete', 'secret', '-l', own, '--ignore-not-found']); };
546
587
  const artifactRoot = options.artifactsRoot ?? `${effectiveInstanceRoot()}/pod-artifacts`;
547
588
  const artifactsDir = `${artifactRoot}/${name}`;
589
+ let jobApplied = false;
548
590
  try {
591
+ ensureLive();
549
592
  if (!context && !options.kubectl) throw new Error('pod command: kubectl context 를 확인할 수 없다');
550
593
  let grokAuth: string | undefined;
551
594
  if (llm === 'grok') {
@@ -573,7 +616,7 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
573
616
  const localSkillsDigest = options.kubectl ? null : image === POD_LITE_IMAGE
574
617
  ? podSkillsDigest(POD_NETWORK_SKILLS, resolve(import.meta.dir, '../../../skills')).digest
575
618
  : podSkillsDigest(resolvePodSkills(env).skills).digest;
576
- const syncs = await (options.syncImages ?? syncPoolImages)([member], image, imageCommit, { localSkillsDigest });
619
+ const syncs = await awaitPodCommandAbort((options.syncImages ?? syncPoolImages)([member], image, imageCommit, { localSkillsDigest }), options.signal);
577
620
  const synced = syncs.get(member.context);
578
621
  if (synced && !synced.ok) throw new Error(`pod command: 이미지 판을 못 맞췄다: ${synced.detail}`);
579
622
  if (synced?.imageRef) member = { ...member, imageRef: synced.imageRef };
@@ -586,8 +629,8 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
586
629
  const seedMirror = options.seedMirror ?? (options.kubectl ? undefined : seedMirrorOnce);
587
630
  if (seedMirror && options.clone && options.source?.kind === 'commit' && hostMirror && member?.sshHost) {
588
631
  // Worst case added to a cold start: the push limit (GATE_MIRROR_SEED_LIMIT_MS), once per host·commit.
589
- const seed = await seedMirror({ host: member.sshHost, commit: options.source.sha, cwd: options.mirrorSeedCwd ?? process.cwd() })
590
- .catch((error: unknown) => ({ host: member!.sshHost!, ok: false, ms: 0, detail: String(error).slice(0, 300) }));
632
+ const seed = await awaitPodCommandAbort(seedMirror({ host: member.sshHost, commit: options.source.sha, cwd: options.mirrorSeedCwd ?? process.cwd() })
633
+ .catch((error: unknown) => ({ host: member!.sshHost!, ok: false, ms: 0, detail: String(error).slice(0, 300) })), options.signal);
591
634
  log('pod.command-job', 'mirror-seed', { host: member.sshHost, commit: options.source.sha, ok: seed?.ok ?? false, ms: seed?.ms ?? 0, ...(seed?.detail ? { detail: seed.detail } : {}) });
592
635
  }
593
636
  // GATE-WARM-BOOT-NO-INIT (10-15): the hostPath dirs the uid-1000 child writes are handed over on the node itself, once per
@@ -600,14 +643,15 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
600
643
  if (!member) log('pod.command-job', 'host-dirs-prep', { ok: false, reason: 'no-pool-member', paths: hostDirPaths });
601
644
  else if (!prepareHostDirs) log('pod.command-job', 'host-dirs-prep', { ok: false, reason: 'prep-disabled', paths: hostDirPaths });
602
645
  else {
603
- const prep = await prepareHostDirs({ member, hostPaths: hostDirPaths })
604
- .catch((error: unknown) => ({ host: member!.sshHost ?? 'local', ok: false, ms: 0, detail: String(error).slice(0, 300) }));
646
+ const prep = await awaitPodCommandAbort(prepareHostDirs({ member, hostPaths: hostDirPaths })
647
+ .catch((error: unknown) => ({ host: member!.sshHost ?? 'local', ok: false, ms: 0, detail: String(error).slice(0, 300) })), options.signal);
605
648
  hostDirsPrepared = prep?.ok === true;
606
649
  log('pod.command-job', 'host-dirs-prep', { host: prep?.host ?? member.sshHost ?? 'local', ok: prep?.ok ?? false, ms: prep?.ms ?? 0, paths: hostDirPaths, ...(prep?.detail ? { detail: prep.detail } : {}) });
607
650
  }
608
651
  }
609
652
  // Any path where the node-side hand-over did not succeed keeps the old in-Pod hand-over (review mf: uid 1000 could not write).
610
653
  const hostDirsInit = hostDirPaths.length > 0 && !hostDirsPrepared;
654
+ ensureLive();
611
655
  const ghToken = options.clone ? (options.ghToken ?? defaultGhToken)() : undefined;
612
656
  const secret = commandJobSecret({ name, namespace, launch, skills, skillEnvText, ...(grokAuth !== undefined ? { grokAuth } : {}), ...(ghToken !== undefined ? { ghToken } : {}) });
613
657
  if (secret) {
@@ -623,6 +667,8 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
623
667
  ...(hostDirsInit ? { hostDirsInit: true } : {}),
624
668
  });
625
669
  kubectl(['-n', namespace, 'delete', 'job', '-l', own, '--ignore-not-found']);
670
+ // A wall deadline can elapse inside the synchronous apply; cleanup by this launch's name then remains safe.
671
+ jobApplied = true;
626
672
  const applied = kubectl(['apply', '-f', '-'], JSON.stringify(manifest));
627
673
  if (applied.status !== 0) {
628
674
  cleanupSecret();
@@ -633,6 +679,7 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
633
679
  let markerRc: number | null = null;
634
680
  const appliedAt = Date.now();
635
681
  for (;;) {
682
+ ensureLive();
636
683
  const got = kubectl(['-n', namespace, 'get', 'job', name, '-o', 'jsonpath={.status.conditions[*].type}']);
637
684
  const finished = jobFinished(got.stdout);
638
685
  if (finished) { state = finished; break; }
@@ -655,8 +702,9 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
655
702
  throw new Error(`pod command: Job 상태를 ${Math.round(options.statusUnreadableMs / 1000)}초 동안 못 읽었다(${context}) — ${(got.stderr ?? '').trim().split('\n').pop()?.slice(0, 200) ?? ''}`);
656
703
  }
657
704
  } else unreadableSince = null;
658
- await sleep(options.pollMs ?? 15_000);
705
+ await awaitPodCommandAbort(sleep(options.pollMs ?? 15_000), options.signal);
659
706
  }
707
+ ensureLive();
660
708
  let logsResult = kubectl(['-n', namespace, 'logs', `job/${name}`, '-c', 'child']);
661
709
  // Marker path returns before the Job condition — a full-log read can hit the same transient window the tail read
662
710
  // already survived (container log rotation · API hiccup). Retry it a little before calling the artifacts lost.
@@ -721,6 +769,12 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
721
769
  });
722
770
  return { exitCode, artifactsDir, job: name, image: imageRef ?? image, artifacts };
723
771
  } catch (err) {
772
+ if (jobApplied && options.signal) {
773
+ // Opt-in bounded callers remove their own failed/Pending Job, including status-unreadable exits.
774
+ // On an unreachable API this is best effort; the local lease is always released in finally.
775
+ try { kubectlRaw(['-n', namespace, 'delete', 'job', name, '--ignore-not-found', '--wait=false']); }
776
+ catch { /* preserve the cancellation reason */ }
777
+ }
724
778
  cleanupSecret();
725
779
  throw err;
726
780
  } finally {
@@ -230,6 +230,13 @@ function runningLimit(spec: Record<string, unknown>): number | null {
230
230
 
231
231
  /** A short-lived Pod proves cluster DNS from inside the same namespace as harness Jobs. Image pull failures are not DNS failures. */
232
232
  export type PoolDnsProbe = 'ready' | 'dns' | 'image' | 'unknown';
233
+ /** Separate, opt-in observation. null means the probe could not measure that step. */
234
+ export interface ExternalGitHubProbe {
235
+ externalDns: boolean | null;
236
+ githubHttps: boolean | null;
237
+ measuredAt: string | null;
238
+ reason: string | null;
239
+ }
233
240
  const DNS_CACHE_MS = 10 * 60_000;
234
241
  const DNS_STALE_OK_MS = 30 * 60_000;
235
242
  const DNS_LOCK_WAIT_MS = 6 * 60_000;
@@ -240,6 +247,97 @@ const dnsFlock = process.platform === 'linux' || process.platform === 'darwin'
240
247
  flock: { args: ['int', 'int'], returns: 'int' },
241
248
  }).symbols.flock : null;
242
249
 
250
+ /** A disposable Pod in the Job namespace measures GitHub reachability without changing admission. */
251
+ export function probePoolGitHub(context: string, kubectl: PoolKubectl = leaseKubectl, options: { dir?: string; now?: () => number } = {}): ExternalGitHubProbe {
252
+ const unknown = (reason: string): ExternalGitHubProbe => ({ externalDns: null, githubHttps: null, measuredAt: null, reason });
253
+ const now = options.now ?? Date.now;
254
+ const dir = options.dir ?? hostLeaseBaseDir();
255
+ const key = createHash('sha256').update(context).digest('hex');
256
+ const cache = join(dir, `github-egress-${key}.cache`);
257
+ const lock = join(dir, `github-egress-${key}.flock`);
258
+ const cached = (): ExternalGitHubProbe | null => {
259
+ try {
260
+ const record = object(JSON.parse(readFileSync(cache, 'utf8')));
261
+ if (typeof record?.at !== 'number' || now() < record.at || now() - record.at >= 60_000) return null;
262
+ if (typeof record.measuredAt !== 'string' || ![true, false].includes(record.externalDns as boolean) ||
263
+ ![true, false, null].includes(record.githubHttps as boolean | null) ||
264
+ !(record.reason === null || typeof record.reason === 'string')) return null;
265
+ return { externalDns: record.externalDns as boolean, githubHttps: record.githubHttps as boolean | null,
266
+ measuredAt: record.measuredAt, reason: record.reason as string | null };
267
+ } catch { return null; }
268
+ };
269
+ const fresh = cached();
270
+ if (fresh) return fresh;
271
+ try { mkdirSync(dir, { recursive: true, mode: 0o700 }); } catch { return unknown('probe cache unavailable'); }
272
+ if (!dnsFlock) return unknown('probe lock unavailable');
273
+ let fd: number;
274
+ try { fd = openSync(lock, 'a', 0o600); } catch { return unknown('probe lock unavailable'); }
275
+ let held = false;
276
+ try {
277
+ const deadline = Date.now() + 2_000;
278
+ while (!held) {
279
+ const shared = cached();
280
+ if (shared) return shared;
281
+ if (dnsFlock(fd, 2 | 4) === 0) { held = true; break; }
282
+ if (Date.now() >= deadline) return unknown('probe busy');
283
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
284
+ }
285
+ const shared = cached();
286
+ if (shared) return shared;
287
+ const result = runPoolGitHubProbe(context, kubectl, new Date(now()).toISOString());
288
+ if (result.measuredAt !== null) {
289
+ const temp = `${cache}.${randomUUID()}.tmp`;
290
+ try { writeFileSync(temp, JSON.stringify({ ...result, at: now() }), { mode: 0o600 }); renameSync(temp, cache); }
291
+ catch { try { rmSync(temp, { force: true }); } catch { /* observation remains valid without cache */ } }
292
+ }
293
+ return result;
294
+ } finally {
295
+ if (held) dnsFlock(fd, 8);
296
+ closeSync(fd);
297
+ }
298
+ }
299
+
300
+ function runPoolGitHubProbe(context: string, kubectl: PoolKubectl, measuredAt: string): ExternalGitHubProbe {
301
+ const name = `elanous-egress-${randomUUID().slice(0, 12)}`;
302
+ const base = ['--context', context, '--request-timeout=15s'];
303
+ const unknown = (reason: string): ExternalGitHubProbe => ({ externalDns: null, githubHttps: null, measuredAt: null, reason });
304
+ let created = false;
305
+ try {
306
+ const manifest = JSON.stringify({ apiVersion: 'v1', kind: 'Pod', metadata: { name, namespace: 'elanous-test', labels: { 'elanous.probe': 'github-egress' } }, spec: {
307
+ restartPolicy: 'Never', activeDeadlineSeconds: 35, automountServiceAccountToken: false,
308
+ containers: [{ name: 'probe', image: 'busybox:1.36', command: ['sh', '-c',
309
+ 'if nslookup github.com >/dev/null 2>&1; then echo EXTERNAL_DNS_OK; if wget -q -T 8 -O /dev/null https://github.com/; then echo GITHUB_HTTPS_OK; else echo GITHUB_HTTPS_FAILED; fi; else echo EXTERNAL_DNS_FAILED; fi'],
310
+ resources: { requests: { cpu: '0', memory: '16Mi' }, limits: { cpu: '100m', memory: '64Mi' } } }],
311
+ } });
312
+ if (kubectl([...base, 'create', '-f', '-'], manifest).status !== 0) return unknown('probe create failed');
313
+ created = true;
314
+ const waited = kubectl([...base, '-n', 'elanous-test', 'wait', '--for=jsonpath={.status.phase}=Succeeded', `pod/${name}`, '--timeout=25s']);
315
+ const podResult = kubectl([...base, '-n', 'elanous-test', 'get', `pod/${name}`, '-o', 'json']);
316
+ if (podResult.status !== 0) return unknown('probe status unavailable');
317
+ const pod = object(JSON.parse(podResult.stdout));
318
+ const statuses = object(pod?.status)?.containerStatuses;
319
+ if (Array.isArray(statuses) && statuses.some((c) =>
320
+ ['ErrImagePull', 'ImagePullBackOff', 'InvalidImageName'].includes(String(object(object(object(c)?.state)?.waiting)?.reason))))
321
+ return unknown('probe image unavailable');
322
+ const logs = kubectl([...base, '-n', 'elanous-test', 'logs', `pod/${name}`]);
323
+ if (logs.status !== 0) return unknown(waited.status === 0 ? 'probe output unavailable' : 'probe timeout');
324
+ const markers = logs.stdout.trim().split(/\r?\n/u);
325
+ const dnsOk = markers.includes('EXTERNAL_DNS_OK');
326
+ const dnsFail = markers.includes('EXTERNAL_DNS_FAILED');
327
+ const httpsOk = markers.includes('GITHUB_HTTPS_OK');
328
+ const httpsFail = markers.includes('GITHUB_HTTPS_FAILED');
329
+ if (dnsFail && !dnsOk && !httpsOk && !httpsFail)
330
+ return { externalDns: false, githubHttps: null, measuredAt, reason: 'external DNS failed' };
331
+ if (dnsOk && !dnsFail && httpsOk !== httpsFail)
332
+ return { externalDns: true, githubHttps: httpsOk, measuredAt, reason: httpsFail ? 'GitHub HTTPS failed' : null };
333
+ return unknown(waited.status === 0 ? 'probe output unavailable' : 'probe timeout');
334
+ } catch { return unknown('probe failed'); }
335
+ finally {
336
+ if (created) try { kubectl([...base, '-n', 'elanous-test', 'delete', `pod/${name}`, '--ignore-not-found=true', '--wait=false']); }
337
+ catch { /* best effort; Kubernetes activeDeadlineSeconds also bounds the Pod */ }
338
+ }
339
+ }
340
+
243
341
  /** The cache and lock are shared by CLI processes, keyed by context rather than pool composition. */
244
342
  export function probePoolDns(context: string, kubectl: PoolKubectl = leaseKubectl, options: { dir?: string; now?: () => number } = {}): PoolDnsProbe {
245
343
  const dir = options.dir ?? hostLeaseBaseDir();
@@ -41,7 +41,7 @@ export function ledgerLineToLogEvent(line: string, runId: string): {
41
41
  const sources = [entry, details].filter((source): source is Record<string, unknown> => source !== undefined);
42
42
  const knownSecrets = secretValues(entry);
43
43
  const containsCredential = (value: string) => SECRET_VALUE.test(value) || knownSecrets.some((secret) => value.includes(secret));
44
- if (containsCredential(entry.event) || !EVENT_ALLOWED.test(entry.event)) return undefined;
44
+ if (containsCredential(entry.event) || (!EVENT_ALLOWED.test(entry.event) && entry.event !== 'ask-auto-target-candidates' && entry.event !== 'ask-auto-reauthor')) return undefined;
45
45
 
46
46
  const data: Record<string, unknown> = { runId };
47
47
  let fields = 0;
@@ -474,7 +474,7 @@ export class PodPoolScheduler {
474
474
  * member the 0.2.15 gate launched its shards one at a time.
475
475
  * Ties keep the configured member order. Returns null when every readable member is full.
476
476
  */
477
- async tryAcquire(skip?: ReadonlySet<string>, onEmpty?: (reason: string) => void, mode: { selfCapped?: boolean; kind?: PodPlacementKind | null; onPlacement?: (placement: PodPlacement) => void } = {}): Promise<PodPoolMember | null> {
477
+ async tryAcquire(skip?: ReadonlySet<string>, onEmpty?: (reason: string) => void, mode: { selfCapped?: boolean; kind?: PodPlacementKind | null; onPlacement?: (placement: PodPlacement) => void; signal?: AbortSignal } = {}): Promise<PodPoolMember | null> {
478
478
  const selfCapped = mode.selfCapped === true;
479
479
  const kind = mode.kind ?? null;
480
480
  // POOL-LITE: lite goals try the small member first, implementation goals the big one; full → overflow by free share.
@@ -487,6 +487,8 @@ export class PodPoolScheduler {
487
487
  const reason = error instanceof Error ? error.message : String(error);
488
488
  reading = Object.fromEntries(this.members.map((m) => [m.context, { occupied: null, reason }]));
489
489
  }
490
+ // A caller may abandon a slow occupancy read. Never acquire its lease after cancellation.
491
+ if (mode.signal?.aborted) throw mode.signal.reason ?? new Error('pod pool acquisition aborted');
490
492
  const free: Record<string, number> = {};
491
493
  const measured: Record<string, boolean> = {};
492
494
  let best: { member: PodPoolMember; slots: number; measured: boolean; preferred: boolean } | null = null;
@@ -17,6 +17,8 @@ export interface PodReuseCleanupDecisionInput {
17
17
  statusExitCode: number | null;
18
18
  statusPorcelain: string;
19
19
  remainingPaths?: readonly string[];
20
+ additionalCleanExclusions?: readonly { name: string; pattern: string }[];
21
+ statusExclusions?: readonly string[];
20
22
  }
21
23
 
22
24
  export interface PodReuseCleanupDecision {
@@ -30,19 +32,21 @@ export interface PodReuseCleanupDecision {
30
32
  }
31
33
 
32
34
  /**
33
- * Pure request-boundary decision: return argv for the caller to execute and
34
- * decide whether the worker can be reused from the command results.
35
- * Execution/wiring and lifecycle event emission intentionally live in a later slice.
35
+ * Pure policy: return fixed argv and decide whether the worker can be reused
36
+ * from actual command results. WarmWorkerRuntime owns execution and lifecycle
37
+ * event emission at the serialized request boundary.
36
38
  */
37
39
  export function decidePodReuseCleanup(input: PodReuseCleanupDecisionInput): PodReuseCleanupDecision {
38
40
  const remainingPaths = (input.remainingPaths ?? []).slice(0, 5);
39
41
  const resetArgs = ['reset', '--hard', input.base] as const;
40
42
  const cleanArgs = [
41
43
  'clean', '-ffdx',
42
- ...POD_REUSE_CLEAN_EXCLUSIONS.flatMap(({ pattern }) => pattern ? ['-e', pattern] : []),
44
+ ...[...POD_REUSE_CLEAN_EXCLUSIONS, ...(input.additionalCleanExclusions ?? [])]
45
+ .flatMap(({ pattern }) => pattern ? ['-e', pattern] : []),
43
46
  ] as const;
44
- const statusArgs = ['status', '--porcelain'] as const;
45
- const preserved = POD_REUSE_CLEAN_EXCLUSIONS.map(({ name }) => name);
47
+ const statusArgs = ['status', '--porcelain', '-z', '--', '.', ...(input.statusExclusions ?? [])
48
+ .map((path) => `:(exclude,top,literal)${path}`)] as const;
49
+ const preserved = [...POD_REUSE_CLEAN_EXCLUSIONS, ...(input.additionalCleanExclusions ?? [])].map(({ name }) => name);
46
50
 
47
51
  let reason: PodReuseCleanupDecision['reason'];
48
52
  if (!/^[0-9a-f]{40}$/i.test(input.base)) reason = 'invalid-base';