@wardby/cli 0.5.2 → 0.6.0

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 (411) hide show
  1. package/.env.example +33 -0
  2. package/README.md +25 -14
  3. package/dist/claude-coding-worker/context.d.ts +22 -0
  4. package/dist/claude-coding-worker/context.js +92 -0
  5. package/dist/claude-coding-worker/driver.d.ts +7 -1
  6. package/dist/claude-coding-worker/driver.js +38 -5
  7. package/dist/claude-coding-worker/sdk.js +13 -4
  8. package/dist/cli-help.d.ts +1 -1
  9. package/dist/cli-help.js +5 -2
  10. package/dist/cli-native-mode.d.ts +9 -0
  11. package/dist/cli-native-mode.js +20 -0
  12. package/dist/cli.d.ts +3 -1
  13. package/dist/cli.js +141 -5
  14. package/dist/coding/claude-context.d.ts +45 -0
  15. package/dist/coding/claude-context.js +434 -0
  16. package/dist/coding/local-git.js +9 -1
  17. package/dist/coding/profile.d.ts +12 -0
  18. package/dist/coding/profile.js +6 -0
  19. package/dist/coding/protocol.d.ts +104 -0
  20. package/dist/coding/protocol.js +87 -3
  21. package/dist/coding/registry/pypi.d.ts +25 -1
  22. package/dist/coding/registry/pypi.js +140 -16
  23. package/dist/coding/registry/types.d.ts +17 -3
  24. package/dist/coding-worker/artifact.d.ts +8 -0
  25. package/dist/coding-worker/artifact.js +2 -2
  26. package/dist/coding-worker/driver.js +2 -0
  27. package/dist/coding-worker/sdk.d.ts +6 -0
  28. package/dist/coding-worker/sdk.js +1 -0
  29. package/dist/coding-worker/skills.d.ts +23 -0
  30. package/dist/coding-worker/skills.js +117 -0
  31. package/dist/coding-worker/types.d.ts +2 -0
  32. package/dist/config/github-allowed-accounts.d.ts +15 -0
  33. package/dist/config/github-allowed-accounts.js +36 -0
  34. package/dist/config/low-power.d.ts +12 -0
  35. package/dist/config/low-power.js +18 -0
  36. package/dist/config/mobile.d.ts +12 -0
  37. package/dist/config/mobile.js +18 -0
  38. package/dist/config/providers.d.ts +56 -2
  39. package/dist/config/providers.js +92 -6
  40. package/dist/core/db.d.ts +16 -6
  41. package/dist/core/db.js +12 -10
  42. package/dist/core/dispatch.d.ts +11 -0
  43. package/dist/core/dispatch.js +36 -1
  44. package/dist/core/engine-native.js +9 -5
  45. package/dist/core/host-events.d.ts +49 -3
  46. package/dist/core/host-events.js +289 -15
  47. package/dist/core/http-runtime.js +13 -6
  48. package/dist/core/issue-bridge.js +42 -2
  49. package/dist/core/issue-events.js +12 -1
  50. package/dist/core/issue-status.js +14 -3
  51. package/dist/core/merge-order-check.d.ts +129 -0
  52. package/dist/core/merge-order-check.js +336 -0
  53. package/dist/core/notification-dispatcher.d.ts +70 -0
  54. package/dist/core/notification-dispatcher.js +368 -0
  55. package/dist/core/notification-templates.d.ts +25 -0
  56. package/dist/core/notification-templates.js +105 -0
  57. package/dist/core/notifications.d.ts +10 -0
  58. package/dist/core/notifications.js +38 -0
  59. package/dist/core/pg-auth.d.ts +22 -0
  60. package/dist/core/pg-auth.js +152 -0
  61. package/dist/core/power-composition.d.ts +16 -0
  62. package/dist/core/power-composition.js +37 -0
  63. package/dist/core/power-idle.d.ts +14 -0
  64. package/dist/core/power-idle.js +30 -0
  65. package/dist/core/power.d.ts +71 -0
  66. package/dist/core/power.js +202 -0
  67. package/dist/core/reconciler.d.ts +4 -2
  68. package/dist/core/reconciler.js +29 -5
  69. package/dist/core/related-pull-requests.d.ts +48 -4
  70. package/dist/core/related-pull-requests.js +113 -11
  71. package/dist/core/repo-access.d.ts +15 -0
  72. package/dist/core/repo-access.js +66 -0
  73. package/dist/core/review-fix.d.ts +37 -0
  74. package/dist/core/review-fix.js +207 -6
  75. package/dist/core/review-host-tools.d.ts +20 -0
  76. package/dist/core/review-host-tools.js +58 -2
  77. package/dist/core/runner.d.ts +336 -3
  78. package/dist/core/runner.js +894 -317
  79. package/dist/core/secret-broker-config.d.ts +103 -0
  80. package/dist/core/secret-broker-config.js +112 -0
  81. package/dist/core/secrets.d.ts +39 -4
  82. package/dist/core/secrets.js +148 -40
  83. package/dist/core/workflow-event-recorder.d.ts +14 -0
  84. package/dist/core/workflow-event-recorder.js +120 -0
  85. package/dist/core/workflow-events.d.ts +82 -0
  86. package/dist/core/workflow-events.js +44 -0
  87. package/dist/core/workflow-run-events.d.ts +16 -0
  88. package/dist/core/workflow-run-events.js +40 -0
  89. package/dist/generated/prisma/browser.d.ts +58 -0
  90. package/dist/generated/prisma/client.d.ts +58 -0
  91. package/dist/generated/prisma/commonInputTypes.d.ts +150 -0
  92. package/dist/generated/prisma/enums.d.ts +27 -0
  93. package/dist/generated/prisma/enums.js +23 -0
  94. package/dist/generated/prisma/internal/class.d.ts +88 -0
  95. package/dist/generated/prisma/internal/class.js +4 -4
  96. package/dist/generated/prisma/internal/prismaNamespace.d.ts +772 -1
  97. package/dist/generated/prisma/internal/prismaNamespace.js +135 -4
  98. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +139 -0
  99. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +135 -4
  100. package/dist/generated/prisma/models/Agent.d.ts +360 -1
  101. package/dist/generated/prisma/models/CodingAgentProfile.d.ts +73 -1
  102. package/dist/generated/prisma/models/CodingRun.d.ts +205 -1
  103. package/dist/generated/prisma/models/DeferredReview.d.ts +78 -1
  104. package/dist/generated/prisma/models/NativeGatewayCall.d.ts +1675 -0
  105. package/dist/generated/prisma/models/NativeGatewayCall.js +1 -0
  106. package/dist/generated/prisma/models/NativeGatewaySession.d.ts +1517 -0
  107. package/dist/generated/prisma/models/NativeGatewaySession.js +1 -0
  108. package/dist/generated/prisma/models/NativeWarmWorker.d.ts +1296 -0
  109. package/dist/generated/prisma/models/NativeWarmWorker.js +1 -0
  110. package/dist/generated/prisma/models/NotificationChannel.d.ts +1543 -0
  111. package/dist/generated/prisma/models/NotificationChannel.js +1 -0
  112. package/dist/generated/prisma/models/NotificationDelivery.d.ts +1502 -0
  113. package/dist/generated/prisma/models/NotificationDelivery.js +1 -0
  114. package/dist/generated/prisma/models/NotificationThread.d.ts +1090 -0
  115. package/dist/generated/prisma/models/NotificationThread.js +1 -0
  116. package/dist/generated/prisma/models/Run.d.ts +826 -1
  117. package/dist/generated/prisma/models/RunHostCheck.d.ts +36 -1
  118. package/dist/generated/prisma/models/Secret.d.ts +37 -1
  119. package/dist/generated/prisma/models/SecretBrokerChange.d.ts +1101 -0
  120. package/dist/generated/prisma/models/SecretBrokerChange.js +1 -0
  121. package/dist/generated/prisma/models/WorkflowEvent.d.ts +1422 -0
  122. package/dist/generated/prisma/models/WorkflowEvent.js +1 -0
  123. package/dist/generated/prisma/models.d.ts +8 -0
  124. package/dist/help/build.js +4 -2
  125. package/dist/help/catalog.d.ts +1 -1
  126. package/dist/help/catalog.js +31 -6
  127. package/dist/help-index.json +1492 -113
  128. package/dist/import/create.js +17 -0
  129. package/dist/import/neutral-schema.d.ts +6 -2
  130. package/dist/import/neutral-schema.js +2 -0
  131. package/dist/mcp/auth/access.d.ts +1 -0
  132. package/dist/mcp/auth/ownership.d.ts +1 -1
  133. package/dist/mcp/auth/repo-authorization.js +14 -0
  134. package/dist/mcp/auth/resource-server.d.ts +2 -1
  135. package/dist/mcp/auth/resource-server.js +7 -2
  136. package/dist/mcp/auth/self-hosted/browser.d.ts +1 -1
  137. package/dist/mcp/auth/self-hosted/browser.js +19 -6
  138. package/dist/mcp/context.d.ts +6 -0
  139. package/dist/mcp/host-events/github-ingress.d.ts +23 -1
  140. package/dist/mcp/host-events/github-ingress.js +68 -0
  141. package/dist/mcp/index.d.ts +13 -0
  142. package/dist/mcp/index.js +59 -10
  143. package/dist/mcp/tools/agents.js +83 -12
  144. package/dist/mcp/tools/notification-channels.d.ts +2 -0
  145. package/dist/mcp/tools/notification-channels.js +293 -0
  146. package/dist/mcp/tools/power.d.ts +2 -0
  147. package/dist/mcp/tools/power.js +77 -0
  148. package/dist/mcp/tools/runs.d.ts +0 -6
  149. package/dist/mcp/tools/runs.js +7 -2
  150. package/dist/mcp/tools/secret-broker-compat.d.ts +12 -0
  151. package/dist/mcp/tools/secret-broker-compat.js +23 -0
  152. package/dist/mcp/tools/secret-elicitation-form.d.ts +22 -2
  153. package/dist/mcp/tools/secret-elicitation-form.js +200 -10
  154. package/dist/mcp/tools/secret-elicitation-server.js +8 -1
  155. package/dist/mcp/tools/secret-elicitation.d.ts +38 -3
  156. package/dist/mcp/tools/secret-elicitation.js +110 -12
  157. package/dist/mcp/tools/secrets.js +184 -7
  158. package/dist/mcp/tools/tools.d.ts +1 -1
  159. package/dist/mcp/tools/tools.js +22 -2
  160. package/dist/native-worker/composition.d.ts +23 -0
  161. package/dist/native-worker/composition.js +77 -0
  162. package/dist/native-worker/docker-isolation.d.ts +65 -0
  163. package/dist/native-worker/docker-isolation.js +183 -0
  164. package/dist/native-worker/docker-launcher.d.ts +74 -0
  165. package/dist/native-worker/docker-launcher.js +237 -0
  166. package/dist/native-worker/gateway-server.d.ts +39 -0
  167. package/dist/native-worker/gateway-server.js +315 -0
  168. package/dist/native-worker/gateway.d.ts +44 -0
  169. package/dist/native-worker/gateway.js +197 -0
  170. package/dist/native-worker/http-server.d.ts +22 -0
  171. package/dist/native-worker/http-server.js +109 -0
  172. package/dist/native-worker/http-transport.d.ts +17 -0
  173. package/dist/native-worker/http-transport.js +88 -0
  174. package/dist/native-worker/kubernetes-isolation.d.ts +90 -0
  175. package/dist/native-worker/kubernetes-isolation.js +218 -0
  176. package/dist/native-worker/kubernetes-launcher.d.ts +102 -0
  177. package/dist/native-worker/kubernetes-launcher.js +335 -0
  178. package/dist/native-worker/launch.d.ts +45 -0
  179. package/dist/native-worker/launch.js +44 -0
  180. package/dist/native-worker/ledger.d.ts +109 -0
  181. package/dist/native-worker/ledger.js +192 -0
  182. package/dist/native-worker/loopback.d.ts +12 -0
  183. package/dist/native-worker/loopback.js +49 -0
  184. package/dist/native-worker/main.d.ts +7 -0
  185. package/dist/native-worker/main.js +74 -0
  186. package/dist/native-worker/protocol.d.ts +744 -0
  187. package/dist/native-worker/protocol.js +200 -0
  188. package/dist/native-worker/sandbox-executor.d.ts +68 -0
  189. package/dist/native-worker/sandbox-executor.js +210 -0
  190. package/dist/native-worker/stdio.d.ts +29 -0
  191. package/dist/native-worker/stdio.js +188 -0
  192. package/dist/native-worker/warm-delivery.d.ts +16 -0
  193. package/dist/native-worker/warm-delivery.js +33 -0
  194. package/dist/native-worker/warm-pool-ledger.d.ts +39 -0
  195. package/dist/native-worker/warm-pool-ledger.js +73 -0
  196. package/dist/native-worker/warm-pool.d.ts +107 -0
  197. package/dist/native-worker/warm-pool.js +283 -0
  198. package/dist/native-worker/worker.d.ts +17 -0
  199. package/dist/native-worker/worker.js +115 -0
  200. package/dist/providers/auth/authorization-server.d.ts +3 -0
  201. package/dist/providers/auth/index.js +2 -0
  202. package/dist/providers/auth/self-hosted.d.ts +13 -1
  203. package/dist/providers/auth/self-hosted.js +54 -4
  204. package/dist/providers/chat/fake.d.ts +46 -0
  205. package/dist/providers/chat/fake.js +39 -0
  206. package/dist/providers/chat/index.d.ts +4 -0
  207. package/dist/providers/chat/index.js +8 -0
  208. package/dist/providers/chat/slack.d.ts +37 -0
  209. package/dist/providers/chat/slack.js +105 -0
  210. package/dist/providers/chat/types.d.ts +42 -0
  211. package/dist/providers/chat/types.js +19 -0
  212. package/dist/providers/coding-proxy/mock-upstream.d.ts +33 -0
  213. package/dist/providers/coding-proxy/mock-upstream.js +116 -0
  214. package/dist/providers/coding-proxy/registry/prisma-store.d.ts +3 -0
  215. package/dist/providers/coding-proxy/registry/prisma-store.js +17 -0
  216. package/dist/providers/coding-proxy/registry/service.d.ts +4 -0
  217. package/dist/providers/coding-proxy/registry/service.js +20 -3
  218. package/dist/providers/coding-proxy/registry/store.d.ts +18 -1
  219. package/dist/providers/coding-proxy/registry/store.js +33 -0
  220. package/dist/providers/coding-proxy/runtime.js +12 -1
  221. package/dist/providers/coding-proxy/types.d.ts +2 -0
  222. package/dist/providers/engine/types.d.ts +19 -0
  223. package/dist/providers/engine/types.js +22 -0
  224. package/dist/providers/executor/composition.d.ts +24 -1
  225. package/dist/providers/executor/composition.js +39 -6
  226. package/dist/providers/executor/container.d.ts +24 -1
  227. package/dist/providers/executor/container.js +101 -11
  228. package/dist/providers/executor/dbos.d.ts +42 -0
  229. package/dist/providers/executor/dbos.js +118 -12
  230. package/dist/providers/executor/deferred.d.ts +56 -0
  231. package/dist/providers/executor/deferred.js +119 -0
  232. package/dist/providers/executor/routing.d.ts +37 -10
  233. package/dist/providers/executor/routing.js +73 -32
  234. package/dist/providers/executor/types.d.ts +12 -0
  235. package/dist/providers/jobs/docker.js +3 -3
  236. package/dist/providers/jobs/fake-kubernetes-api.d.ts +21 -2
  237. package/dist/providers/jobs/fake-kubernetes-api.js +42 -1
  238. package/dist/providers/jobs/kubernetes-api.d.ts +17 -1
  239. package/dist/providers/jobs/kubernetes-api.js +7 -0
  240. package/dist/providers/jobs/kubernetes-client.d.ts +10 -1
  241. package/dist/providers/jobs/kubernetes-client.js +52 -3
  242. package/dist/providers/jobs/kubernetes-dry-run-fixture.d.ts +6 -1
  243. package/dist/providers/jobs/kubernetes-dry-run-fixture.js +6 -3
  244. package/dist/providers/jobs/kubernetes-isolation.d.ts +16 -1
  245. package/dist/providers/jobs/kubernetes-isolation.js +58 -5
  246. package/dist/providers/jobs/kubernetes-platform.d.ts +4 -1
  247. package/dist/providers/jobs/kubernetes-platform.js +38 -1
  248. package/dist/providers/jobs/kubernetes-preflight.d.ts +2 -1
  249. package/dist/providers/jobs/kubernetes-preflight.js +4 -2
  250. package/dist/providers/jobs/kubernetes.d.ts +56 -7
  251. package/dist/providers/jobs/kubernetes.js +117 -36
  252. package/dist/providers/jobs/metadata-probes.d.ts +11 -0
  253. package/dist/providers/jobs/metadata-probes.js +13 -0
  254. package/dist/providers/jobs/types.d.ts +9 -0
  255. package/dist/providers/llm/openai.js +0 -1
  256. package/dist/providers/review-host/github.d.ts +11 -1
  257. package/dist/providers/review-host/github.js +50 -0
  258. package/dist/providers/review-host/types.d.ts +23 -0
  259. package/dist/providers/vcs/github.d.ts +32 -6
  260. package/dist/providers/vcs/github.js +74 -16
  261. package/dist/quickstart/coding-db.js +14 -2
  262. package/dist/quickstart/coding-doctor.d.ts +3 -0
  263. package/dist/quickstart/coding-doctor.js +3 -0
  264. package/dist/quickstart/coding-images.d.ts +9 -0
  265. package/dist/quickstart/coding-images.js +39 -0
  266. package/dist/quickstart/coding-seed.d.ts +20 -1
  267. package/dist/quickstart/coding-seed.js +96 -7
  268. package/dist/quickstart/coding.d.ts +2 -0
  269. package/dist/quickstart/coding.js +75 -3
  270. package/dist/quickstart/config.d.ts +2 -0
  271. package/dist/quickstart/config.js +4 -0
  272. package/dist/quickstart/images.d.ts +13 -0
  273. package/dist/quickstart/images.js +80 -12
  274. package/dist/quickstart/index.d.ts +0 -2
  275. package/dist/quickstart/index.js +10 -14
  276. package/dist/quickstart/manifests.d.ts +56 -0
  277. package/dist/quickstart/manifests.js +523 -0
  278. package/dist/quickstart/python-detect.d.ts +10 -0
  279. package/dist/quickstart/python-detect.js +32 -0
  280. package/dist/quickstart/repo-packages.d.ts +10 -0
  281. package/dist/quickstart/repo-packages.js +132 -0
  282. package/dist/quickstart/reviewer-prompt.d.ts +12 -0
  283. package/dist/quickstart/reviewer-prompt.js +84 -0
  284. package/dist/quickstart/scenarios.d.ts +19 -0
  285. package/dist/quickstart/scenarios.js +28 -0
  286. package/dist/quickstart/sync-reviewer-prompt.d.ts +1 -0
  287. package/dist/quickstart/sync-reviewer-prompt.js +10 -0
  288. package/dist/quickstart-images.json +1 -1
  289. package/dist/sandbox/brokered-fetch.d.ts +23 -0
  290. package/dist/sandbox/brokered-fetch.js +37 -0
  291. package/dist/sandbox/host-functions.d.ts +34 -0
  292. package/dist/sandbox/host-functions.js +144 -80
  293. package/dist/sandbox/prelude.js +7 -1
  294. package/dist/sandbox/run-in-sandbox.d.ts +19 -0
  295. package/dist/sandbox/run-in-sandbox.js +28 -10
  296. package/dist/sandbox/safe-fetch.d.ts +2 -0
  297. package/dist/sandbox/safe-fetch.js +3 -1
  298. package/dist/sandbox/secret-broker-sigv4.d.ts +2 -0
  299. package/dist/sandbox/secret-broker-sigv4.js +49 -0
  300. package/dist/sandbox/secret-broker.d.ts +34 -0
  301. package/dist/sandbox/secret-broker.js +162 -0
  302. package/dist/sandbox/user-tool.d.ts +14 -0
  303. package/dist/sandbox/user-tool.js +32 -0
  304. package/dist/serve.js +18 -0
  305. package/dist/viewer/api-schema.d.ts +214 -18
  306. package/dist/viewer/api-schema.js +22 -0
  307. package/dist/viewer/build-schemas.js +4 -1
  308. package/dist/viewer/event-bus.d.ts +6 -0
  309. package/dist/viewer/event-bus.js +16 -7
  310. package/dist/viewer/graph.d.ts +5 -2
  311. package/dist/viewer/graph.js +7 -2
  312. package/dist/viewer/http.d.ts +0 -1
  313. package/dist/viewer/http.js +2 -2
  314. package/dist/viewer/infra.js +25 -2
  315. package/dist/viewer/limits.d.ts +4 -0
  316. package/dist/viewer/limits.js +4 -0
  317. package/dist/viewer/openapi.d.ts +15 -0
  318. package/dist/viewer/openapi.js +172 -0
  319. package/docs/README.md +7 -0
  320. package/docs/agent-recipes.md +57 -8
  321. package/docs/architecture-runtime.md +16 -1
  322. package/docs/code-review-agents.md +139 -5
  323. package/docs/coding-agent-setup.md +127 -2
  324. package/docs/coding-packages.md +105 -5
  325. package/docs/coding-services.md +3 -1
  326. package/docs/coding-worker-byo-images.md +239 -2
  327. package/docs/coding-worker-isolation.md +88 -23
  328. package/docs/getting-started-gke.md +225 -9
  329. package/docs/getting-started-identity-provider.md +21 -10
  330. package/docs/getting-started.md +140 -11
  331. package/docs/jira-agents.md +5 -1
  332. package/docs/knowledge.md +11 -0
  333. package/docs/native-sandbox.md +635 -0
  334. package/docs/release-verification.md +9 -0
  335. package/docs/security-deployment.md +113 -12
  336. package/docs/slack-notifications.md +252 -0
  337. package/docs/tool-secrets.md +252 -0
  338. package/docs/viewer-api.md +46 -5
  339. package/docs/viewer-api.openapi.yml +1404 -0
  340. package/help/admin-viewer.md +6 -2
  341. package/help/agent-recipes.md +27 -0
  342. package/help/architecture-agent.md +136 -0
  343. package/help/brokered-secrets.md +79 -0
  344. package/help/build-claude-code-image.md +131 -0
  345. package/help/build-worker-image.md +195 -0
  346. package/help/code-review-agents.md +68 -6
  347. package/help/coding-packages.md +31 -2
  348. package/help/creating-agents.md +14 -0
  349. package/help/deploy-gke.md +37 -6
  350. package/help/deployment-targets.md +24 -13
  351. package/help/errors/coding-provider-not-configured.md +2 -2
  352. package/help/errors/database-auth-invalid.md +28 -0
  353. package/help/errors/github-account-not-allowed.md +25 -0
  354. package/help/errors/low-power-disabled.md +29 -0
  355. package/help/errors/low-power-scale-forbidden.md +35 -0
  356. package/help/errors/low-power-wake-timeout.md +30 -0
  357. package/help/errors/mobile-disabled.md +31 -0
  358. package/help/errors/native-sandbox-capacity.md +22 -0
  359. package/help/errors/native-sandbox-deadline-exceeded.md +19 -0
  360. package/help/errors/native-sandbox-docker-failed.md +20 -0
  361. package/help/errors/native-sandbox-gateway-unavailable.md +21 -0
  362. package/help/errors/native-sandbox-image-not-pinned.md +19 -0
  363. package/help/errors/native-sandbox-isolation-mismatch.md +20 -0
  364. package/help/errors/native-sandbox-network-unenforced.md +22 -0
  365. package/help/errors/native-sandbox-requires-catalog.md +31 -0
  366. package/help/errors/native-sandbox-unavailable.md +19 -0
  367. package/help/errors/native-sandbox-warm-delivery-failed.md +21 -0
  368. package/help/errors/native-sandbox-worker-exited.md +20 -0
  369. package/help/errors/native-sandbox-worker-lost.md +20 -0
  370. package/help/errors/native-sandbox-worker-unready.md +22 -0
  371. package/help/errors/secret-broker-body-invalid.md +26 -0
  372. package/help/errors/secret-broker-changed.md +20 -0
  373. package/help/errors/secret-broker-config-invalid.md +30 -0
  374. package/help/errors/secret-broker-conflict.md +27 -0
  375. package/help/errors/secret-broker-destination-denied.md +28 -0
  376. package/help/errors/secret-broker-value-invalid.md +28 -0
  377. package/help/errors/secret-brokered.md +24 -0
  378. package/help/errors/secret-not-brokered.md +25 -0
  379. package/help/errors/slack-auth-failed.md +48 -0
  380. package/help/errors/slack-channel-unreachable.md +39 -0
  381. package/help/errors/slack-not-configured.md +33 -0
  382. package/help/getting-started.md +18 -0
  383. package/help/github.md +18 -1
  384. package/help/identity-and-access.md +5 -1
  385. package/help/local-repositories.md +35 -5
  386. package/help/low-power-mode.md +125 -0
  387. package/help/merge-order-check.md +117 -0
  388. package/help/mobile-app.md +54 -0
  389. package/help/native-sandbox.md +136 -0
  390. package/help/observability.md +8 -0
  391. package/help/related-pull-requests.md +70 -9
  392. package/help/repo-instructions-and-skills.md +91 -0
  393. package/help/review-fix-rounds.md +10 -0
  394. package/help/slack-notifications.md +62 -0
  395. package/help/troubleshooting/coding-workers.md +21 -0
  396. package/package.json +13 -5
  397. package/prisma/migrations/20261009000003_native_execution_mode/migration.sql +12 -0
  398. package/prisma/migrations/20261009000004_native_gateway_ledger/migration.sql +65 -0
  399. package/prisma/migrations/20261009000005_native_gateway_call_child_agent/migration.sql +5 -0
  400. package/prisma/migrations/20261009000006_run_deferred_start_stop/migration.sql +14 -0
  401. package/prisma/migrations/20261009000007_native_gateway_network_ready/migration.sql +5 -0
  402. package/prisma/migrations/20261010000001_workflow_notifications/migration.sql +99 -0
  403. package/prisma/migrations/20261011000001_native_gateway_session_status_index/migration.sql +2 -0
  404. package/prisma/migrations/20261012000001_native_warm_worker/migration.sql +32 -0
  405. package/prisma/migrations/20261013000001_coding_run_merge_order/migration.sql +2 -0
  406. package/prisma/migrations/20261013000001_secret_broker/migration.sql +17 -0
  407. package/prisma/migrations/20261013000002_deferred_review_request/migration.sql +4 -0
  408. package/prisma/migrations/20261014000000_repo_skills/migration.sql +15 -0
  409. package/prisma/migrations/20261015000001_coding_run_merge_order_index/migration.sql +2 -0
  410. package/prisma/migrations/20261015000002_no_change_rereview/migration.sql +2 -0
  411. package/prisma/schema.prisma +362 -91
@@ -1,5 +1,6 @@
1
1
  import { type Readable } from "node:stream";
2
2
  import type { KubernetesJobConfig } from "../../config/providers.js";
3
+ import { type Power } from "../../core/power.js";
3
4
  import { type KubernetesApi } from "./kubernetes-api.js";
4
5
  import { type ServiceStateReporter } from "./service-state.js";
5
6
  import type { CodingProvider } from "../../coding/provider.js";
@@ -19,20 +20,29 @@ export interface KubernetesJobLauncherOptions {
19
20
  * the gate probes the address `provision` re-reads for that launch, not this memoized one.
20
21
  */
21
22
  preflight?: () => Promise<KubernetesClusterInfo | void>;
23
+ /**
24
+ * Low-power mode: `launch()` wakes the (possibly scaled-to-zero) coding proxy before its preflight,
25
+ * and `warmUp()` defers the preflight while the proxy is asleep. Default `DISABLED_POWER` (no-op).
26
+ */
27
+ power?: Power;
22
28
  now?: () => number;
23
29
  sleep?: (milliseconds: number) => Promise<void>;
24
30
  readyTimeoutMs?: number;
25
31
  /**
26
32
  * How long to wait for the run's NetworkPolicy to be enforced before seeding. Default 30_000,
27
- * but never *less* than `enforcementExecTimeoutMs * ENFORCEMENT_BLOCKED_STREAK`: see the
28
- * constructor. An explicit override here is a floor raise, not an escape hatch — the gate still
29
- * needs room for a full streak of probes at whatever the effective exec timeout is.
33
+ * but never *less* than one full streak exec's timeout
34
+ * (`enforcementExecTimeoutMs * ENFORCEMENT_BLOCKED_STREAK + ENFORCEMENT_POLL_MS * (ENFORCEMENT_BLOCKED_STREAK - 1)`,
35
+ * 31_000 at the default exec timeout): see the constructor. An explicit override here is a floor
36
+ * raise, not an escape hatch — the gate still needs room for a full streak of probes at whatever
37
+ * the effective per-probe budget is.
30
38
  */
31
39
  enforcementTimeoutMs?: number;
32
40
  /**
33
- * Bound for a single enforcement probe exec. Default 10_000 (DEFAULT_ENFORCEMENT_EXEC_TIMEOUT_MS).
34
- * Raise this on a resource-constrained cluster (e.g. `kind` on a laptop) where a healthy probe
35
- * can legitimately take longer than 10 s to run inside the keeper's CPU/memory limits.
41
+ * Time budget for a single enforcement probe. Default 10_000 (DEFAULT_ENFORCEMENT_EXEC_TIMEOUT_MS).
42
+ * A streak's probes share one exec, whose timeout is this times ENFORCEMENT_BLOCKED_STREAK plus
43
+ * the poll gaps between them. Raise this on a resource-constrained cluster (e.g. `kind` on a
44
+ * laptop) where a healthy probe can legitimately take longer than 10 s to run inside the
45
+ * keeper's CPU/memory limits.
36
46
  */
37
47
  enforcementExecTimeoutMs?: number;
38
48
  createArchive?: (directory: string) => {
@@ -60,7 +70,10 @@ export declare class KubernetesJobLauncher implements WorkspaceJobLauncher {
60
70
  private readonly warn;
61
71
  private readonly enforcementTimeoutMs;
62
72
  private readonly enforcementExecTimeoutMs;
73
+ /** Timeout of one streak exec: ENFORCEMENT_BLOCKED_STREAK probes plus the gaps between them. */
74
+ private readonly streakExecTimeoutMs;
63
75
  private preflightResult?;
76
+ private readonly power;
64
77
  constructor(options: KubernetesJobLauncherOptions);
65
78
  /**
66
79
  * Services run as native sidecars in the run pod (kubernetes-isolation.ts), for either provider:
@@ -88,7 +101,35 @@ export declare class KubernetesJobLauncher implements WorkspaceJobLauncher {
88
101
  materializeWorkspace(handle: JobHandle, destination: string): Promise<void>;
89
102
  stop(handle: JobHandle, _reason?: string): Promise<void>;
90
103
  remove(handle: JobHandle): Promise<void>;
91
- /** Memoized: the preflight (or, without one, a single proxy-witness read) runs once per launcher. */
104
+ /**
105
+ * Low-power mode: whether the proxy is scaled to zero. A failed check is logged by code only (the
106
+ * leading reason code, else the error's class name -- never the raw message, which could carry
107
+ * cluster detail) and counts as awake, so warm-up runs the preflight as it would without the mode.
108
+ */
109
+ private proxyAsleep;
110
+ /**
111
+ * Starts the memoized cluster preflight (`runPreflight`) right away, instead of waiting for the
112
+ * first `launch()`, so a long-running server process's own start-up pays the preflight's cost (a
113
+ * canary pod on `kind`: ~20s) rather than that process's first coding run. Never throws: it only
114
+ * reports the outcome (an info line on success, a warning naming the failure code otherwise) so
115
+ * an operator sees a misconfigured cluster at boot. Any `launch()` already awaiting this same
116
+ * attempt (concurrent with this call) still fails exactly as it does today when the preflight
117
+ * failed — it shares the same rejected promise. But unlike a preflight `launch()` starts on its
118
+ * own, a start-up failure here is **not** left stuck memoized: a transient cluster problem at
119
+ * process start must not fail every coding run until restart, so the failed attempt is cleared
120
+ * (only if nothing has already started a newer one — compared by identity, never by state) and
121
+ * the *next* `launch()` runs the preflight fresh. A successful warm-up stays memoized as always.
122
+ * In low-power mode, while the proxy is asleep, warm-up is skipped (the memo stays unset) rather
123
+ * than waking the proxy only for the preflight; the first `launch()` wakes it and runs the preflight.
124
+ */
125
+ warmUp(): Promise<void>;
126
+ /**
127
+ * Memoized: the preflight (or, without one, a single proxy-witness read) runs once per launcher —
128
+ * including its failure, which stays memoized and fails every subsequent `launch()` immediately,
129
+ * for a preflight `launch()` itself started (there was no `warmUp()`, or `warmUp()`'s own failed
130
+ * attempt was already cleared). `warmUp()` is the one caller that clears a failed attempt instead
131
+ * of leaving it stuck; see its own doc comment.
132
+ */
92
133
  private runPreflight;
93
134
  /**
94
135
  * Waits until the run's NetworkPolicy is enforced on this pod: the keeper (same network namespace
@@ -96,6 +137,14 @@ export declare class KubernetesJobLauncher implements WorkspaceJobLauncher {
96
137
  * proxy on CODING_PROXY_PORT and fail to reach it on CODING_PROXY_DENY_PORT. Anything else resets
97
138
  * the streak; the wall-clock bound still applies.
98
139
  *
140
+ * The whole streak runs in ONE keeper exec (`enforcementStreakScript`), so exec and interpreter
141
+ * start-up is paid once per streak rather than once per probe. That exec exits 0 only after the
142
+ * full streak was proven; otherwise it stops at the first non-proven probe and exits with that
143
+ * probe's code. Any non-zero exit is a broken streak: if the bound has been reached it is the
144
+ * verdict, else the launcher waits ENFORCEMENT_POLL_MS and starts a fresh streak from zero. The
145
+ * exec is bounded by `streakExecTimeoutMs` (the per-probe budget times the streak, plus the gaps);
146
+ * an exec that times out or fails throws, failing the launch exactly as a failed probe exec did.
147
+ *
99
148
  * "Fail to reach" means the connect *timed out* — the packet was dropped. A refusal (RST) is not
100
149
  * a denial: it proves the SYN reached the destination host, so the deny port is merely unserved
101
150
  * and witnesses nothing. That is its own verdict, below.
@@ -29,11 +29,12 @@ import { finished } from "node:stream/promises";
29
29
  import { createHash } from "node:crypto";
30
30
  import { isIP } from "node:net";
31
31
  import { logger } from "../../core/logger.js";
32
- import { MAX_CODING_ARTIFACT_BYTES, MAX_DEBUG_TRACE_LINE_BYTES, parseCodingAgentOutputJson, } from "../../coding/protocol.js";
32
+ import { DISABLED_POWER, wakeTimeoutMs } from "../../core/power.js";
33
+ import { MAX_CODING_ARTIFACT_BYTES, MAX_CODING_INPUT_BYTES, MAX_DEBUG_TRACE_LINE_BYTES, parseCodingAgentOutputJson, } from "../../coding/protocol.js";
33
34
  import { CODING_PROXY_DENY_PORT, CODING_PROXY_PORT } from "./docker-isolation.js";
34
35
  import { parseWorkerDiagnosticLine } from "./docker.js";
35
36
  import { KubernetesAlreadyExistsError, KubernetesConflictError } from "./kubernetes-api.js";
36
- import { ENFORCEMENT_PROBE_DENY_REACHABLE, ENFORCEMENT_PROBE_PROVEN, ENFORCEMENT_PROBE_DENY_REFUSED, ENFORCEMENT_PROBE_PROXY_UNREACHABLE, KEEPER_CONTAINER, KEEPER_SEEDED_MARKER, KUBERNETES_ISOLATION_ERROR, STORAGE_ROOT, TOOL_RUNNER_CONTAINER, WORKER_CONTAINER, assertRunNetworkPolicyMatches, assertRunPodMatches, buildCapabilitySecret, buildRunNetworkPolicy, buildRunPod, enforcementProbeScript, kubernetesRunNames, kubernetesRunNamesForToken, runLabels, serviceContainerName, validateKubernetesSpec, } from "./kubernetes-isolation.js";
37
+ import { ENFORCEMENT_PROBE_DENY_REACHABLE, ENFORCEMENT_PROBE_PROVEN, ENFORCEMENT_PROBE_DENY_REFUSED, ENFORCEMENT_PROBE_PROXY_UNREACHABLE, KEEPER_CONTAINER, KEEPER_SEEDED_MARKER, KUBERNETES_ISOLATION_ERROR, STORAGE_ROOT, TOOL_RUNNER_CONTAINER, WORKER_CONTAINER, assertRunNetworkPolicyMatches, assertRunPodMatches, buildCapabilitySecret, buildRunNetworkPolicy, buildRunPod, enforcementStreakScript, kubernetesRunNames, kubernetesRunNamesForToken, runLabels, serviceContainerName, validateKubernetesSpec, } from "./kubernetes-isolation.js";
37
38
  import { claudeToolSetup } from "./claude-tool-setup.js";
38
39
  import { podQuotaUsage, quotaShortfall } from "./kubernetes-quota.js";
39
40
  import { readProxyWitness } from "./kubernetes-witness.js";
@@ -60,14 +61,17 @@ const ENFORCEMENT_POLL_MS = 500;
60
61
  /** Consecutive proven probes required (8787 reachable, 8788 blocked): one dropped SYN must not open the gate. */
61
62
  const ENFORCEMENT_BLOCKED_STREAK = 3;
62
63
  /**
63
- * Default bound for one enforcement probe exec (the probe makes two sequential connects, so it
64
+ * Default time budget for ONE enforcement probe (the probe makes two sequential connects, so it
64
65
  * gives up after at most 6 s of its own connect timeouts). Overridable per KubernetesJobConfig's
65
66
  * `enforcementExecTimeoutMs` (KUBERNETES_ENFORCEMENT_EXEC_TIMEOUT_MS) — a resource-constrained
66
67
  * keeper (e.g. a laptop `kind` cluster's default 250m CPU / 128Mi limit) can take noticeably
67
- * longer than this to run even a healthy probe to completion. DEFAULT_ENFORCEMENT_TIMEOUT_MS
68
- * above is exactly this default times ENFORCEMENT_BLOCKED_STREAK, which is not a coincidence: see
69
- * the constructor, which derives the effective overall bound the same way from whichever exec
70
- * timeout is actually configured, so the two never drift apart.
68
+ * longer than this to run even a healthy probe to completion.
69
+ *
70
+ * The probes of one streak run inside a single exec (`enforcementStreakScript`), so the exec's own
71
+ * timeout is derived from this per-probe budget: ENFORCEMENT_BLOCKED_STREAK probes plus the
72
+ * ENFORCEMENT_POLL_MS gaps between them (`streakExecTimeoutMs`; 31_000 at this default). The
73
+ * constructor then floors the overall wall-clock bound at that one-streak-exec value, so a raised
74
+ * per-probe budget can never leave the bound shorter than a single streak.
71
75
  */
72
76
  const DEFAULT_ENFORCEMENT_EXEC_TIMEOUT_MS = 10_000;
73
77
  /**
@@ -81,12 +85,10 @@ const DEFAULT_ENFORCEMENT_EXEC_TIMEOUT_MS = 10_000;
81
85
  * and sending an operator to the CNI over a dead exec wastes the one clue they were given.
82
86
  */
83
87
  const ENFORCEMENT_VERDICTS = {
84
- [ENFORCEMENT_PROBE_PROVEN]: {
85
- code: "kubernetes_policy_not_enforced",
86
- // Reaching the bound on a proven probe means the streak kept being broken: enforcement was
87
- // observed, but never ENFORCEMENT_BLOCKED_STREAK times running, so it is not stable evidence.
88
- detail: `the last probe was proven, but never ${ENFORCEMENT_BLOCKED_STREAK} consecutive times within the bound — enforcement is flapping rather than absent`,
89
- },
88
+ // No ENFORCEMENT_PROBE_PROVEN entry, deliberately: the whole streak runs in one exec
89
+ // (enforcementStreakScript), which exits PROVEN only after ENFORCEMENT_BLOCKED_STREAK consecutive
90
+ // proven probes — and that opens the gate before the bound is ever checked. A streak broken by
91
+ // flapping exits with the breaking probe's own code, so the verdict is always one of these.
90
92
  [ENFORCEMENT_PROBE_DENY_REACHABLE]: {
91
93
  code: "kubernetes_policy_not_enforced",
92
94
  detail: `the deny port ${CODING_PROXY_DENY_PORT} accepted a connection, so no policy is blocking it (or the policy is not port-scoped) — look at the CNI`,
@@ -110,12 +112,8 @@ const ENFORCEMENT_PROBE_DID_NOT_RUN = {
110
112
  };
111
113
  /** Same shape as ENFORCEMENT_VERDICTS/ENFORCEMENT_PROBE_DID_NOT_RUN, but for the `pre_marker` stage. */
112
114
  const ENFORCEMENT_VERDICTS_PRE_MARKER = {
113
- [ENFORCEMENT_PROBE_PROVEN]: {
114
- code: "kubernetes_policy_enforcement_lost_before_marker",
115
- detail: `the policy was proven enforced earlier in this launch, but the re-probe run immediately before ` +
116
- `opening the gate never reached ${ENFORCEMENT_BLOCKED_STREAK} consecutive proven probes within the ` +
117
- "bound — enforcement held at the initial proof but is flapping now, immediately before release",
118
- },
115
+ // No ENFORCEMENT_PROBE_PROVEN entry, for the same reason as ENFORCEMENT_VERDICTS: a proven streak
116
+ // exec opens the gate, so it never reaches enforcementVerdict.
119
117
  [ENFORCEMENT_PROBE_DENY_REACHABLE]: {
120
118
  code: "kubernetes_policy_enforcement_lost_before_marker",
121
119
  detail: `the deny port ${CODING_PROXY_DENY_PORT} was proven blocked earlier in this launch, but the re-probe ` +
@@ -386,10 +384,14 @@ export class KubernetesJobLauncher {
386
384
  warn;
387
385
  enforcementTimeoutMs;
388
386
  enforcementExecTimeoutMs;
387
+ /** Timeout of one streak exec: ENFORCEMENT_BLOCKED_STREAK probes plus the gaps between them. */
388
+ streakExecTimeoutMs;
389
389
  preflightResult;
390
+ power;
390
391
  constructor(options) {
391
392
  this.options = options;
392
393
  this.api = options.api;
394
+ this.power = options.power ?? DISABLED_POWER;
393
395
  this.namespace = options.config.namespace;
394
396
  this.workspaceRoot = resolve(options.workspaceRoot);
395
397
  if (this.workspaceRoot === resolve("/"))
@@ -398,13 +400,18 @@ export class KubernetesJobLauncher {
398
400
  this.sleep = options.sleep ?? ((ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms)));
399
401
  this.readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
400
402
  this.enforcementExecTimeoutMs = options.enforcementExecTimeoutMs ?? DEFAULT_ENFORCEMENT_EXEC_TIMEOUT_MS;
401
- // Never less than a full blocked-streak's worth of worst-case probes: a longer configured exec
402
- // timeout that left this unchanged could make the wall-clock bound expire before the loop ever
403
- // has a chance to observe ENFORCEMENT_BLOCKED_STREAK consecutive probes, which would turn "the
404
- // keeper is just slow" into an indistinguishable "the policy is not enforced" false negative.
405
- // DEFAULT_ENFORCEMENT_TIMEOUT_MS (30_000) already equals the default exec timeout times the
406
- // streak (10_000 * 3), so this is a no-op unless a longer exec timeout is actually configured.
407
- this.enforcementTimeoutMs = Math.max(options.enforcementTimeoutMs ?? DEFAULT_ENFORCEMENT_TIMEOUT_MS, this.enforcementExecTimeoutMs * ENFORCEMENT_BLOCKED_STREAK);
403
+ // One exec runs a whole streak: ENFORCEMENT_BLOCKED_STREAK probes, each within its own
404
+ // per-probe budget, with ENFORCEMENT_POLL_MS between consecutive probes.
405
+ this.streakExecTimeoutMs =
406
+ this.enforcementExecTimeoutMs * ENFORCEMENT_BLOCKED_STREAK +
407
+ ENFORCEMENT_POLL_MS * (ENFORCEMENT_BLOCKED_STREAK - 1);
408
+ // Never less than one full streak exec's worst case: a longer configured per-probe budget that
409
+ // left this unchanged could make the wall-clock bound expire inside the first slow streak exec,
410
+ // leaving no room to retry a streak broken while the policy was still being programmed — which
411
+ // would turn "the keeper is just slow" into an indistinguishable "the policy is not enforced"
412
+ // false negative. At the defaults this raises DEFAULT_ENFORCEMENT_TIMEOUT_MS (30_000) to the
413
+ // default streak exec timeout (10_000 * 3 + 500 * 2 = 31_000).
414
+ this.enforcementTimeoutMs = Math.max(options.enforcementTimeoutMs ?? DEFAULT_ENFORCEMENT_TIMEOUT_MS, this.streakExecTimeoutMs);
408
415
  this.createArchive = options.createArchive ?? hostTarArchive;
409
416
  this.warn = options.onWarning ?? ((message) => kubernetesLog.warn(message));
410
417
  }
@@ -491,6 +498,11 @@ export class KubernetesJobLauncher {
491
498
  this.warn(`kubernetes_spec_rejected: ${error.message}`);
492
499
  throw error;
493
500
  }
501
+ // Low-power mode: the proxy may be scaled to zero; wake it before the preflight reads it. Guarded
502
+ // (not just a no-op await) so mode off keeps launch() joining an in-flight warm-up synchronously.
503
+ if (this.power.enabled) {
504
+ await this.power.ensureAwake("proxy", { timeoutMs: wakeTimeoutMs(this.readyTimeoutMs), runId: spec.runId });
505
+ }
494
506
  await this.runPreflight();
495
507
  const names = kubernetesRunNames(spec.runId);
496
508
  const handle = { backend: BACKEND, id: `${this.namespace}/${names.token}` };
@@ -614,7 +626,69 @@ export class KubernetesJobLauncher {
614
626
  }
615
627
  // -------------------------------------------------------------------------
616
628
  // Launch
617
- /** Memoized: the preflight (or, without one, a single proxy-witness read) runs once per launcher. */
629
+ /**
630
+ * Low-power mode: whether the proxy is scaled to zero. A failed check is logged by code only (the
631
+ * leading reason code, else the error's class name -- never the raw message, which could carry
632
+ * cluster detail) and counts as awake, so warm-up runs the preflight as it would without the mode.
633
+ */
634
+ async proxyAsleep() {
635
+ try {
636
+ return await this.power.isAsleep("proxy");
637
+ }
638
+ catch (error) {
639
+ const code = (error instanceof Error && /^[a-z][a-z0-9_]*(?=:)/.exec(error.message)?.[0]) ||
640
+ (error instanceof Error ? error.name : "unknown");
641
+ this.warn(`kubernetes_preflight_power_check_failed: ${code}`);
642
+ return false;
643
+ }
644
+ }
645
+ /**
646
+ * Starts the memoized cluster preflight (`runPreflight`) right away, instead of waiting for the
647
+ * first `launch()`, so a long-running server process's own start-up pays the preflight's cost (a
648
+ * canary pod on `kind`: ~20s) rather than that process's first coding run. Never throws: it only
649
+ * reports the outcome (an info line on success, a warning naming the failure code otherwise) so
650
+ * an operator sees a misconfigured cluster at boot. Any `launch()` already awaiting this same
651
+ * attempt (concurrent with this call) still fails exactly as it does today when the preflight
652
+ * failed — it shares the same rejected promise. But unlike a preflight `launch()` starts on its
653
+ * own, a start-up failure here is **not** left stuck memoized: a transient cluster problem at
654
+ * process start must not fail every coding run until restart, so the failed attempt is cleared
655
+ * (only if nothing has already started a newer one — compared by identity, never by state) and
656
+ * the *next* `launch()` runs the preflight fresh. A successful warm-up stays memoized as always.
657
+ * In low-power mode, while the proxy is asleep, warm-up is skipped (the memo stays unset) rather
658
+ * than waking the proxy only for the preflight; the first `launch()` wakes it and runs the preflight.
659
+ */
660
+ async warmUp() {
661
+ if (this.power.enabled && (await this.proxyAsleep())) {
662
+ kubernetesLog.info("kubernetes_preflight_deferred: proxy asleep (low-power mode); the first coding run runs the preflight");
663
+ return;
664
+ }
665
+ const attempt = this.runPreflight();
666
+ try {
667
+ await attempt;
668
+ kubernetesLog.info("kubernetes_preflight_warm_up_succeeded: cluster preflight passed at start-up");
669
+ }
670
+ catch (error) {
671
+ // Only clear the memo if it still holds this exact attempt: a concurrent launch() or warmUp()
672
+ // may have already seen this same rejection and started a fresh one (which must survive), and
673
+ // nothing else could have replaced this attempt with anything but a newer one, since
674
+ // runPreflight() never overwrites an already-set, still-pending-or-resolved promise.
675
+ if (this.preflightResult === attempt)
676
+ this.preflightResult = undefined;
677
+ // Same code-only convention as every other failure this launcher logs (e.g. launch()'s
678
+ // kubernetes_spec_rejected above): the error's own message is the fixed reason code set by
679
+ // runPreflight's errorWithCode (KUBERNETES_ISOLATION_ERROR) — never the raw cause, which could
680
+ // carry cluster detail.
681
+ const code = error instanceof Error ? error.message : String(error);
682
+ this.warn(`kubernetes_preflight_warm_up_failed: ${code}`);
683
+ }
684
+ }
685
+ /**
686
+ * Memoized: the preflight (or, without one, a single proxy-witness read) runs once per launcher —
687
+ * including its failure, which stays memoized and fails every subsequent `launch()` immediately,
688
+ * for a preflight `launch()` itself started (there was no `warmUp()`, or `warmUp()`'s own failed
689
+ * attempt was already cleared). `warmUp()` is the one caller that clears a failed attempt instead
690
+ * of leaving it stuck; see its own doc comment.
691
+ */
618
692
  runPreflight() {
619
693
  this.preflightResult ??= (async () => {
620
694
  try {
@@ -639,6 +713,14 @@ export class KubernetesJobLauncher {
639
713
  * proxy on CODING_PROXY_PORT and fail to reach it on CODING_PROXY_DENY_PORT. Anything else resets
640
714
  * the streak; the wall-clock bound still applies.
641
715
  *
716
+ * The whole streak runs in ONE keeper exec (`enforcementStreakScript`), so exec and interpreter
717
+ * start-up is paid once per streak rather than once per probe. That exec exits 0 only after the
718
+ * full streak was proven; otherwise it stops at the first non-proven probe and exits with that
719
+ * probe's code. Any non-zero exit is a broken streak: if the bound has been reached it is the
720
+ * verdict, else the launcher waits ENFORCEMENT_POLL_MS and starts a fresh streak from zero. The
721
+ * exec is bounded by `streakExecTimeoutMs` (the per-probe budget times the streak, plus the gaps);
722
+ * an exec that times out or fails throws, failing the launch exactly as a failed probe exec did.
723
+ *
642
724
  * "Fail to reach" means the connect *timed out* — the packet was dropped. A refusal (RST) is not
643
725
  * a denial: it proves the SYN reached the destination host, so the deny port is merely unserved
644
726
  * and witnesses nothing. That is its own verdict, below.
@@ -659,17 +741,16 @@ export class KubernetesJobLauncher {
659
741
  * must not open the gate at the initial proof either.
660
742
  */
661
743
  async waitForPolicyEnforcement(names, proxyIp, stage = "initial") {
662
- const command = ["node", "-e", enforcementProbeScript(proxyIp)];
744
+ const command = ["node", "-e", enforcementStreakScript(proxyIp, ENFORCEMENT_BLOCKED_STREAK, ENFORCEMENT_POLL_MS)];
663
745
  const started = this.now();
664
- let blocked = 0;
665
746
  for (;;) {
666
- // `exitCode` is this iteration's probe, and the bound below is only ever reached from here —
667
- // so the verdict is always the *last* probe's, never a remembered earlier one.
747
+ // `exitCode` is this attempt's streak, which stops at (and exits with) its first non-proven
748
+ // probe, and the bound below is only ever reached from here — so the verdict is always the
749
+ // *last* probe's, never a remembered earlier one.
668
750
  const exitCode = await this.api.exec(this.namespace, names.pod, KEEPER_CONTAINER, command, {
669
- timeoutMs: this.enforcementExecTimeoutMs,
751
+ timeoutMs: this.streakExecTimeoutMs,
670
752
  });
671
- blocked = exitCode === 0 ? blocked + 1 : 0;
672
- if (blocked >= ENFORCEMENT_BLOCKED_STREAK)
753
+ if (exitCode === ENFORCEMENT_PROBE_PROVEN)
673
754
  return;
674
755
  if (this.now() - started >= this.enforcementTimeoutMs) {
675
756
  throw enforcementVerdict(exitCode, proxyIp, stage);
@@ -851,14 +932,14 @@ export class KubernetesJobLauncher {
851
932
  const staging = await mkdtemp(join(tmpdir(), "wardby-k8s-input-"));
852
933
  try {
853
934
  const source = await lstat(inputArtifact);
854
- if (!source.isFile() || source.size > MAX_CODING_ARTIFACT_BYTES) {
935
+ if (!source.isFile() || source.size > MAX_CODING_INPUT_BYTES) {
855
936
  throw new Error("kubernetes_input_artifact_invalid");
856
937
  }
857
938
  const copy = join(staging, "input.json");
858
939
  await copyFile(inputArtifact, copy);
859
940
  // Re-check the copy: the source could have changed between lstat and copy.
860
941
  const copied = await lstat(copy);
861
- if (!copied.isFile() || copied.size > MAX_CODING_ARTIFACT_BYTES) {
942
+ if (!copied.isFile() || copied.size > MAX_CODING_INPUT_BYTES) {
862
943
  throw new Error("kubernetes_input_artifact_invalid");
863
944
  }
864
945
  await this.seedDirectory(names, staging, INPUT_STORAGE, timeoutMs);
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Cloud metadata endpoints a coding or native worker pod must never reach: they hand out node or
3
+ * workload credentials. The instance metadata service (GCE, EC2 IPv4), the EKS Pod Identity agent (IPv4
4
+ * and IPv6), and EC2's IPv6 IMDS endpoint. Probed on port 80, where every one of them serves. A pod without
5
+ * IPv6 fails the last connect immediately, which counts as unreachable — correct either way.
6
+ * src/sandbox/fetch-policy.ts blocks the same addresses for in-process fetches.
7
+ */
8
+ export declare const CLOUD_METADATA_PROBES: readonly {
9
+ host: string;
10
+ port: number;
11
+ }[];
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Cloud metadata endpoints a coding or native worker pod must never reach: they hand out node or
3
+ * workload credentials. The instance metadata service (GCE, EC2 IPv4), the EKS Pod Identity agent (IPv4
4
+ * and IPv6), and EC2's IPv6 IMDS endpoint. Probed on port 80, where every one of them serves. A pod without
5
+ * IPv6 fails the last connect immediately, which counts as unreachable — correct either way.
6
+ * src/sandbox/fetch-policy.ts blocks the same addresses for in-process fetches.
7
+ */
8
+ export const CLOUD_METADATA_PROBES = Object.freeze([
9
+ Object.freeze({ host: "169.254.169.254", port: 80 }),
10
+ Object.freeze({ host: "169.254.170.23", port: 80 }),
11
+ Object.freeze({ host: "fd00:ec2::254", port: 80 }),
12
+ Object.freeze({ host: "fd00:ec2::23", port: 80 }),
13
+ ]);
@@ -88,6 +88,15 @@ export interface JobLauncher {
88
88
  * itself stays the authority.
89
89
  */
90
90
  hasCapacityFor?: (spec: JobSpec) => Promise<boolean>;
91
+ /**
92
+ * Starts any one-time start-up check (e.g. Kubernetes's memoized cluster preflight) right away,
93
+ * instead of waiting for the first `launch()`. Never throws — a failure is only logged — and
94
+ * `launch()` still runs and reports the same check itself, memoized exactly as if this had never
95
+ * been called. Composition roots call this, fire-and-forget, only from a long-running server
96
+ * process's own start-up (`wardby serve`, the MCP server, `wardby scheduler`); never from a
97
+ * one-shot CLI command. Absent on a launcher with nothing to warm up (e.g. Docker).
98
+ */
99
+ warmUp?: () => Promise<void>;
91
100
  launch: (spec: JobSpec) => Promise<JobHandle>;
92
101
  status: (handle: JobHandle) => Promise<JobStatus>;
93
102
  collect: (handle: JobHandle) => Promise<JobResult>;
@@ -153,7 +153,6 @@ export class OpenAiLlmProvider {
153
153
  store: false,
154
154
  ...(req.tools ? { tools: toOpenAiTools(req.tools) } : {}),
155
155
  ...(req.maxTokens !== undefined ? { max_output_tokens: Math.max(req.maxTokens, MIN_MAX_OUTPUT_TOKENS) } : {}),
156
- // The SDK's ReasoningEffort type predates xhigh/max; the API accepts them.
157
156
  ...(sendEffort ? { reasoning: { effort: req.effort } } : {}),
158
157
  ...(sendTemperature ? { temperature: req.temperature } : {}),
159
158
  };
@@ -5,7 +5,7 @@
5
5
  * and revokes it afterwards. See docs/private/2026-09-25-code-review-host-design.md §5.1.
6
6
  */
7
7
  import { type GitHubAppClient } from "../vcs/github.js";
8
- import { ReviewHostError, type CiView, type CodeReviewHost, type CommentInput, type CommentRef, type CompleteCheckInput, type EditCommentInput, type FileListView, type FileReadResult, type HostPermission, type HostUser, type PublishReviewInput, type PublishReviewResult, type PullRequestHead, type PullRequestOrigin, type PullRequestView, type StartCheckInput } from "./types.js";
8
+ import { ReviewHostError, type CiView, type CodeReviewHost, type CommentInput, type CommentRef, type CompleteCheckInput, type EditCommentInput, type FileListView, type FileReadResult, type HostPermission, type HostUser, type PublishReviewInput, type PublishReviewResult, type PullRequestHead, type PullRequestOrigin, type PullRequestView, type StartCheckInput, type UpsertNamedCheckInput } from "./types.js";
9
9
  /** listFiles leaves out vendored trees and lockfiles; every host applies the same filter. */
10
10
  export declare function skippedInListing(path: string): boolean;
11
11
  type Json = Record<string, unknown>;
@@ -16,6 +16,7 @@ export declare class GitHubReviewHost implements CodeReviewHost {
16
16
  private readonly client;
17
17
  readonly provider: "github";
18
18
  constructor(client: GitHubAppClient);
19
+ appOwnerLogin(): Promise<string>;
19
20
  /** Runs `action` with a token for exactly `permissions`, translating failures to ReviewHostError. */
20
21
  protected withToken<T>(repository: string, permissions: Record<string, "read" | "write">, action: (get: (path: string, init?: RequestInit, expected?: number[]) => Promise<Response>, graphql: GraphQl) => Promise<T>): Promise<T>;
21
22
  repositoryPermission(repository: string, user: HostUser): Promise<{
@@ -70,5 +71,14 @@ export declare class GitHubReviewHost implements CodeReviewHost {
70
71
  checkId: string;
71
72
  }>;
72
73
  completeCheck(repository: string, input: CompleteCheckInput): Promise<void>;
74
+ /**
75
+ * Creates or updates, by fixed name, a check run on `input.headSha` that
76
+ * this App owns: lists the commit's check runs filtered to that name and
77
+ * patches the newest one `fromApp` owns, else creates one. Writes nothing
78
+ * when that run already carries the same status, conclusion, and output.
79
+ * A completed run is never moved back to in_progress (GitHub does not
80
+ * allow reopening a completed check run): a fresh one is created instead.
81
+ */
82
+ upsertNamedCheck(repository: string, input: UpsertNamedCheckInput): Promise<void>;
73
83
  }
74
84
  export {};
@@ -22,6 +22,12 @@ export function skippedInListing(path) {
22
22
  return VENDORED.test(path) || path.endsWith("package-lock.json");
23
23
  }
24
24
  const CHECK_TEXT_LIMIT = 65_535;
25
+ /**
26
+ * Cap on a named check's output.title. Not a documented GitHub limit (none
27
+ * is published for the title); a conservative length that keeps the title
28
+ * a one-line heading in the checks UI.
29
+ */
30
+ const CHECK_TITLE_LIMIT = 255;
25
31
  const COMMENT_WRITE = { issues: "write", pull_requests: "write" };
26
32
  const REVIEW_WRITE = { pull_requests: "write", checks: "write" };
27
33
  /** Always implicitly granted to an installation token; enough for the collaborator-permission endpoint. */
@@ -201,6 +207,9 @@ export class GitHubReviewHost {
201
207
  constructor(client) {
202
208
  this.client = client;
203
209
  }
210
+ async appOwnerLogin() {
211
+ return (await this.client.appIdentity()).ownerLogin;
212
+ }
204
213
  /** Runs `action` with a token for exactly `permissions`, translating failures to ReviewHostError. */
205
214
  async withToken(repository, permissions, action) {
206
215
  try {
@@ -708,4 +717,45 @@ export class GitHubReviewHost {
708
717
  const base = repoPath(repository);
709
718
  await this.withToken(repository, { checks: "write" }, (get) => this.patchCheck(get, base, input));
710
719
  }
720
+ /**
721
+ * Creates or updates, by fixed name, a check run on `input.headSha` that
722
+ * this App owns: lists the commit's check runs filtered to that name and
723
+ * patches the newest one `fromApp` owns, else creates one. Writes nothing
724
+ * when that run already carries the same status, conclusion, and output.
725
+ * A completed run is never moved back to in_progress (GitHub does not
726
+ * allow reopening a completed check run): a fresh one is created instead.
727
+ */
728
+ async upsertNamedCheck(repository, input) {
729
+ // A caller's bad input, like path_invalid: never interpolated into the URL.
730
+ if (!SAFE_SHA.test(input.headSha))
731
+ throw new ReviewHostError("host_api_error", "head_sha_invalid");
732
+ const base = repoPath(repository);
733
+ const output = {
734
+ title: input.title.slice(0, CHECK_TITLE_LIMIT),
735
+ summary: input.summary.slice(0, CHECK_TEXT_LIMIT),
736
+ };
737
+ const conclusion = input.status === "completed" ? input.conclusion : undefined;
738
+ await this.withToken(repository, { checks: "write" }, async (get) => {
739
+ const { id: appId } = await this.client.appIdentity();
740
+ const payload = record(await (await get(`${base}/commits/${input.headSha}/check-runs?check_name=${encodeURIComponent(input.name)}`)).json());
741
+ const existing = list(payload.check_runs ?? [])
742
+ .filter((r) => fromApp(r, appId))
743
+ .sort((a, b) => num(b.id) - num(a.id))[0];
744
+ const body = { status: input.status, ...(conclusion ? { conclusion } : {}), output };
745
+ if (existing) {
746
+ const current = record(existing.output ?? {});
747
+ const unchanged = existing.status === input.status &&
748
+ (existing.conclusion ?? undefined) === conclusion &&
749
+ current.title === output.title &&
750
+ current.summary === output.summary;
751
+ if (unchanged)
752
+ return;
753
+ if (!(existing.status === "completed" && input.status !== "completed")) {
754
+ await get(`${base}/check-runs/${encodeURIComponent(String(num(existing.id)))}`, { method: "PATCH", body: JSON.stringify(body) }, [200]);
755
+ return;
756
+ }
757
+ }
758
+ await get(`${base}/check-runs`, { method: "POST", body: JSON.stringify({ name: input.name, head_sha: input.headSha, ...body }) }, [201]);
759
+ });
760
+ }
711
761
  }
@@ -207,6 +207,18 @@ export interface CompleteCheckInput {
207
207
  text?: string;
208
208
  detailsUrl?: string;
209
209
  }
210
+ export type UpsertNamedCheckInput = {
211
+ headSha: string;
212
+ /** The check's fixed name, e.g. MERGE_ORDER_CHECK_NAME ("wardby merge order"); identifies which run to update. */
213
+ name: string;
214
+ title: string;
215
+ summary: string;
216
+ } & ({
217
+ status: "in_progress";
218
+ } | {
219
+ status: "completed";
220
+ conclusion: "success" | "failure";
221
+ });
210
222
  /**
211
223
  * A user's effective permission on one repository, host-neutral and ranked
212
224
  * none < read < triage < write < maintain < admin. GitHub maps its
@@ -222,6 +234,8 @@ export interface HostUser {
222
234
  }
223
235
  export interface CodeReviewHost {
224
236
  readonly provider: ReviewHostProvider;
237
+ /** The GitHub App's owner account (always served; see WARDBY_GITHUB_ALLOWED_ACCOUNTS). Hosts without an App omit it. */
238
+ appOwnerLogin?(): Promise<string>;
225
239
  /**
226
240
  * The user's effective permission on `repository`, asked with the App's own
227
241
  * installation credential (never a user token). Answers only for the user
@@ -274,6 +288,15 @@ export interface CodeReviewHost {
274
288
  checkId: string;
275
289
  }>;
276
290
  completeCheck(repository: string, input: CompleteCheckInput): Promise<void>;
291
+ /**
292
+ * Creates or updates, by fixed name, a check run on one commit that isn't
293
+ * tied to a run's own startCheck/completeCheck lifecycle (e.g. the
294
+ * `wardby merge order` check, recomputed fresh and reposted on demand).
295
+ * Finds the one this App owns among the commit's check runs with that
296
+ * name and patches it; creates one when none exists. Hosts that can't
297
+ * post checks (e.g. a local repository) leave this out.
298
+ */
299
+ upsertNamedCheck?(repository: string, input: UpsertNamedCheckInput): Promise<void>;
277
300
  }
278
301
  export type ReviewHostErrorCode = "host_not_installed" | "host_permission_missing" | "host_api_error" | "host_invalid_response";
279
302
  export declare class ReviewHostError extends Error {
@@ -149,9 +149,16 @@ export interface RelatedPullRequestEntry {
149
149
  state?: RelatedPullRequestState;
150
150
  /** The pull request whose description this section is in. */
151
151
  self?: boolean;
152
+ /**
153
+ * Step in the delegating agent's declared merge order (1-99), from
154
+ * CodingRun.mergeOrder; absent = the delegating agent set no order
155
+ * (an open entry is listed under "Not ordered:" when any other entry,
156
+ * open or done, has one).
157
+ */
158
+ mergeOrder?: number;
152
159
  }
153
160
  export interface RelatedPullRequestsInput {
154
- /** In suggested merge order (the order the request's coding runs were dispatched). */
161
+ /** In merge order: by mergeOrder when set, else the order the request's coding runs were dispatched. */
155
162
  entries: readonly RelatedPullRequestEntry[];
156
163
  issue?: {
157
164
  key: string;
@@ -162,9 +169,18 @@ export interface RelatedPullRequestsInput {
162
169
  /**
163
170
  * The "Related pull requests" block, markers included, built only from
164
171
  * stored rows (never agent text). Open (and draft, and not-yet-known) pull
165
- * requests form the numbered suggested merge order; merged and closed ones
166
- * follow as context. Undefined when no other valid pull request is left after
167
- * validation: a set of one is not a set.
172
+ * requests form the merge order; merged and closed ones follow as context.
173
+ * Undefined when no other valid pull request is left after validation: a
174
+ * set of one is not a set.
175
+ *
176
+ * When any valid entry (open, merged or closed) carries a `mergeOrder` (set
177
+ * by the delegating agent), steps are counted over all of them, so a
178
+ * label never changes as pull requests merge: the open entries that have
179
+ * one are numbered under "Merge order" and labelled with their step (equal
180
+ * `mergeOrder` values share a step label), any open entry without one is
181
+ * listed after under "Not ordered:", and a merged or closed entry that has
182
+ * one carries its step label in the context list too. Otherwise every open
183
+ * entry is numbered in the unordered "Suggested merge order" list.
168
184
  */
169
185
  export declare function renderRelatedSection(input: RelatedPullRequestsInput): string | undefined;
170
186
  /**
@@ -228,11 +244,21 @@ export declare class GitHubAppClient implements GitHubRepositoryAccess {
228
244
  */
229
245
  readFileAtRef(input: RepositoryFileInput): Promise<string | null>;
230
246
  private identity?;
231
- /** The App's id and slug (its @-mention handle), read once with the App JWT. */
232
- appIdentity(): Promise<{
247
+ /**
248
+ * The App's id, slug (its @-mention handle), and owner account, read once
249
+ * with the App JWT and memoized. `fresh` re-reads GET /app and replaces the
250
+ * memo (used before a destructive action, since the owner can change).
251
+ * A rejection is never cached.
252
+ */
253
+ appIdentity(opts?: {
254
+ fresh?: boolean;
255
+ }): Promise<{
233
256
  id: number;
234
257
  slug: string;
258
+ ownerLogin: string;
235
259
  }>;
260
+ /** Removes this App from an installation (an account outside the trusted set). */
261
+ deleteInstallation(installationId: number): Promise<void>;
236
262
  upsertContinuationStatusComment(input: ContinuationStatusCommentInput): Promise<void>;
237
263
  updateContinuationStatusComment(input: ContinuationStatusCommentInput): Promise<void>;
238
264
  createContinuationCheckRun(input: ContinuationCheckRunInput): Promise<void>;