@wardby/cli 0.4.1 → 0.5.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 (221) hide show
  1. package/.env.example +8 -1
  2. package/README.md +3 -1
  3. package/deploy/local/docker-compose.quickstart-coding.yml +27 -0
  4. package/dist/claude-coding-worker/driver.d.ts +2 -0
  5. package/dist/claude-coding-worker/driver.js +19 -3
  6. package/dist/claude-coding-worker/sdk.js +1 -1
  7. package/dist/cli.js +41 -22
  8. package/dist/coding/continuation-wording.d.ts +10 -0
  9. package/dist/coding/continuation-wording.js +13 -0
  10. package/dist/coding/docker-preflight.d.ts +12 -0
  11. package/dist/coding/docker-preflight.js +40 -0
  12. package/dist/coding/local-git.d.ts +22 -0
  13. package/dist/coding/local-git.js +102 -0
  14. package/dist/coding/local-repo-status.d.ts +7 -0
  15. package/dist/coding/local-repo-status.js +56 -0
  16. package/dist/coding/local-repo.d.ts +17 -0
  17. package/dist/coding/local-repo.js +65 -0
  18. package/dist/coding/observability.d.ts +1 -1
  19. package/dist/coding/observability.js +1 -0
  20. package/dist/coding/profile.d.ts +6 -0
  21. package/dist/coding/profile.js +13 -4
  22. package/dist/coding/protocol.d.ts +32 -7
  23. package/dist/coding/protocol.js +66 -4
  24. package/dist/coding-worker/artifact.d.ts +1 -0
  25. package/dist/coding-worker/errors.js +2 -0
  26. package/dist/config/providers.d.ts +13 -0
  27. package/dist/config/providers.js +21 -9
  28. package/dist/core/attribution.d.ts +13 -0
  29. package/dist/core/attribution.js +30 -0
  30. package/dist/core/budget-groups.d.ts +3 -1
  31. package/dist/core/budget-groups.js +53 -5
  32. package/dist/core/ci-context.d.ts +8 -0
  33. package/dist/core/ci-context.js +59 -0
  34. package/dist/core/delegation-siblings.d.ts +22 -0
  35. package/dist/core/delegation-siblings.js +40 -0
  36. package/dist/core/dispatch.d.ts +28 -4
  37. package/dist/core/dispatch.js +57 -15
  38. package/dist/core/engine-native.d.ts +16 -0
  39. package/dist/core/engine-native.js +49 -2
  40. package/dist/core/host-events.d.ts +25 -2
  41. package/dist/core/host-events.js +361 -19
  42. package/dist/core/host-status.d.ts +8 -3
  43. package/dist/core/host-status.js +38 -7
  44. package/dist/core/issue-events.d.ts +2 -6
  45. package/dist/core/issue-events.js +13 -19
  46. package/dist/core/pull-request-state-sync.d.ts +17 -0
  47. package/dist/core/pull-request-state-sync.js +53 -0
  48. package/dist/core/reconciler.d.ts +10 -1
  49. package/dist/core/reconciler.js +15 -2
  50. package/dist/core/related-pull-requests.d.ts +95 -0
  51. package/dist/core/related-pull-requests.js +338 -0
  52. package/dist/core/repo-access.d.ts +5 -1
  53. package/dist/core/repo-access.js +17 -1
  54. package/dist/core/review-fix-ledger.d.ts +20 -0
  55. package/dist/core/review-fix-ledger.js +41 -0
  56. package/dist/core/review-fix.d.ts +60 -0
  57. package/dist/core/review-fix.js +155 -0
  58. package/dist/core/review-host-tools.d.ts +13 -2
  59. package/dist/core/review-host-tools.js +72 -11
  60. package/dist/core/runner.d.ts +2 -2
  61. package/dist/core/runner.js +379 -184
  62. package/dist/core/serial-gate.d.ts +11 -0
  63. package/dist/core/serial-gate.js +19 -0
  64. package/dist/core/timing.d.ts +7 -0
  65. package/dist/core/timing.js +7 -0
  66. package/dist/generated/prisma/browser.d.ts +23 -0
  67. package/dist/generated/prisma/client.d.ts +23 -0
  68. package/dist/generated/prisma/commonInputTypes.d.ts +22 -0
  69. package/dist/generated/prisma/internal/class.d.ts +33 -0
  70. package/dist/generated/prisma/internal/class.js +4 -4
  71. package/dist/generated/prisma/internal/prismaNamespace.d.ts +274 -1
  72. package/dist/generated/prisma/internal/prismaNamespace.js +47 -2
  73. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +48 -0
  74. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +47 -2
  75. package/dist/generated/prisma/models/Agent.d.ts +422 -1
  76. package/dist/generated/prisma/models/AgentRepository.d.ts +120 -2
  77. package/dist/generated/prisma/models/CodingAgentProfile.d.ts +42 -1
  78. package/dist/generated/prisma/models/CodingRun.d.ts +205 -1
  79. package/dist/generated/prisma/models/DeferredReview.d.ts +1336 -0
  80. package/dist/generated/prisma/models/DeferredReview.js +1 -0
  81. package/dist/generated/prisma/models/LocalPullRequest.d.ts +1384 -0
  82. package/dist/generated/prisma/models/LocalPullRequest.js +1 -0
  83. package/dist/generated/prisma/models/LocalReview.d.ts +1315 -0
  84. package/dist/generated/prisma/models/LocalReview.js +1 -0
  85. package/dist/generated/prisma/models/Run.d.ts +223 -0
  86. package/dist/generated/prisma/models/RunHostCheck.d.ts +148 -1
  87. package/dist/generated/prisma/models.d.ts +3 -0
  88. package/dist/help-index.json +476 -37
  89. package/dist/import/neutral-schema.d.ts +2 -2
  90. package/dist/mcp/auth/access.d.ts +3 -1
  91. package/dist/mcp/auth/host-account-cli.js +2 -2
  92. package/dist/mcp/auth/repo-authorization.d.ts +6 -7
  93. package/dist/mcp/auth/repo-authorization.js +21 -0
  94. package/dist/mcp/index.js +4 -1
  95. package/dist/mcp/tools/agents.js +46 -4
  96. package/dist/mcp/tools/host-accounts.js +2 -2
  97. package/dist/mcp/tools/repositories.js +71 -12
  98. package/dist/mcp/tools/runs.js +23 -0
  99. package/dist/mcp/tools/trigger.js +153 -24
  100. package/dist/providers/engine/types.d.ts +11 -0
  101. package/dist/providers/executor/composition.js +12 -7
  102. package/dist/providers/executor/container.d.ts +40 -3
  103. package/dist/providers/executor/container.js +140 -9
  104. package/dist/providers/executor/types.d.ts +1 -1
  105. package/dist/providers/jobs/fake-kubernetes-api.d.ts +3 -2
  106. package/dist/providers/jobs/fake-kubernetes-api.js +3 -0
  107. package/dist/providers/jobs/kubernetes-api.d.ts +3 -1
  108. package/dist/providers/jobs/kubernetes-client.d.ts +2 -1
  109. package/dist/providers/jobs/kubernetes-client.js +3 -0
  110. package/dist/providers/jobs/kubernetes-isolation.d.ts +7 -0
  111. package/dist/providers/jobs/kubernetes-isolation.js +8 -3
  112. package/dist/providers/jobs/kubernetes-preflight.d.ts +8 -0
  113. package/dist/providers/jobs/kubernetes-preflight.js +7 -0
  114. package/dist/providers/jobs/kubernetes-quota.d.ts +31 -0
  115. package/dist/providers/jobs/kubernetes-quota.js +110 -0
  116. package/dist/providers/jobs/kubernetes.d.ts +9 -0
  117. package/dist/providers/jobs/kubernetes.js +50 -0
  118. package/dist/providers/jobs/types.d.ts +7 -0
  119. package/dist/providers/review-host/ci.d.ts +7 -0
  120. package/dist/providers/review-host/ci.js +20 -0
  121. package/dist/providers/review-host/github-events.d.ts +7 -0
  122. package/dist/providers/review-host/github-events.js +31 -7
  123. package/dist/providers/review-host/github.d.ts +19 -2
  124. package/dist/providers/review-host/github.js +145 -2
  125. package/dist/providers/review-host/index.d.ts +8 -2
  126. package/dist/providers/review-host/index.js +14 -3
  127. package/dist/providers/review-host/local.d.ts +72 -0
  128. package/dist/providers/review-host/local.js +434 -0
  129. package/dist/providers/review-host/types.d.ts +65 -3
  130. package/dist/providers/review-host/types.js +5 -3
  131. package/dist/providers/vcs/git.d.ts +10 -20
  132. package/dist/providers/vcs/git.js +71 -127
  133. package/dist/providers/vcs/github-remote.d.ts +67 -0
  134. package/dist/providers/vcs/github-remote.js +201 -0
  135. package/dist/providers/vcs/github.d.ts +81 -0
  136. package/dist/providers/vcs/github.js +203 -6
  137. package/dist/providers/vcs/index.d.ts +16 -1
  138. package/dist/providers/vcs/index.js +43 -15
  139. package/dist/providers/vcs/local-remote.d.ts +44 -0
  140. package/dist/providers/vcs/local-remote.js +108 -0
  141. package/dist/providers/vcs/remote.d.ts +58 -0
  142. package/dist/providers/vcs/remote.js +1 -0
  143. package/dist/providers/vcs/routing.d.ts +33 -0
  144. package/dist/providers/vcs/routing.js +52 -0
  145. package/dist/providers/vcs/types.d.ts +12 -1
  146. package/dist/quickstart/coding-db.d.ts +11 -0
  147. package/dist/quickstart/coding-db.js +75 -0
  148. package/dist/quickstart/coding-doctor.d.ts +13 -0
  149. package/dist/quickstart/coding-doctor.js +88 -0
  150. package/dist/quickstart/coding-images.d.ts +15 -0
  151. package/dist/quickstart/coding-images.js +79 -0
  152. package/dist/quickstart/coding-seed.d.ts +63 -0
  153. package/dist/quickstart/coding-seed.js +85 -0
  154. package/dist/quickstart/coding.d.ts +66 -0
  155. package/dist/quickstart/coding.js +430 -0
  156. package/dist/quickstart/images.d.ts +19 -0
  157. package/dist/quickstart/images.js +63 -0
  158. package/dist/quickstart/index.js +61 -12
  159. package/dist/quickstart/starter-services.d.ts +45 -0
  160. package/dist/quickstart/starter-services.js +186 -0
  161. package/dist/quickstart-images.json +1 -0
  162. package/dist/serve.js +14 -1
  163. package/dist/viewer/api-schema.d.ts +176 -0
  164. package/dist/viewer/api-schema.js +27 -2
  165. package/dist/viewer/graph.d.ts +7 -2
  166. package/dist/viewer/graph.js +25 -11
  167. package/dist/viewer/http.d.ts +7 -1
  168. package/dist/viewer/http.js +8 -3
  169. package/dist/viewer/infra.d.ts +2 -0
  170. package/dist/viewer/infra.js +23 -0
  171. package/dist/viewer/run-detail.d.ts +2 -1
  172. package/dist/viewer/run-detail.js +2 -2
  173. package/docs/agent-recipes.md +123 -2
  174. package/docs/code-review-agents.md +293 -38
  175. package/docs/coding-agent-setup.md +141 -2
  176. package/docs/coding-packages.md +21 -0
  177. package/docs/coding-services.md +3 -0
  178. package/docs/coding-worker-isolation.md +56 -20
  179. package/docs/getting-started-gke.md +24 -0
  180. package/docs/getting-started.md +82 -8
  181. package/docs/jira-agents.md +26 -5
  182. package/docs/security-deployment.md +9 -0
  183. package/docs/viewer-api.md +21 -2
  184. package/help/admin-viewer.md +11 -1
  185. package/help/agent-recipes.md +25 -1
  186. package/help/builder-agent.md +4 -0
  187. package/help/code-review-agents.md +82 -2
  188. package/help/coding-packages.md +12 -1
  189. package/help/coding-services.md +4 -0
  190. package/help/cost-attribution.md +3 -1
  191. package/help/creating-agents.md +3 -0
  192. package/help/deploy-gke.md +2 -1
  193. package/help/errors/coding-provider-not-configured.md +60 -0
  194. package/help/errors/coding-turn-limit.md +25 -0
  195. package/help/errors/continuation-closed.md +75 -0
  196. package/help/errors/local-branch-conflict.md +37 -0
  197. package/help/errors/local-path-invalid.md +30 -0
  198. package/help/errors/local-ref-invalid.md +36 -0
  199. package/help/errors/local-ref-not-found.md +41 -0
  200. package/help/errors/local-repo-not-allowed.md +46 -0
  201. package/help/errors/local-repo-not-found.md +39 -0
  202. package/help/errors/model-unavailable.md +7 -6
  203. package/help/errors/vcs-github-not-configured.md +34 -0
  204. package/help/getting-started.md +4 -0
  205. package/help/github.md +22 -5
  206. package/help/jira.md +9 -2
  207. package/help/local-repositories.md +182 -0
  208. package/help/related-pull-requests.md +89 -0
  209. package/help/review-fix-rounds.md +69 -0
  210. package/help/troubleshooting/budgets.md +11 -0
  211. package/help/troubleshooting/coding-workers.md +22 -0
  212. package/help/troubleshooting/repository-access.md +5 -0
  213. package/package.json +3 -2
  214. package/prisma/migrations/20261004100000_delegations_and_coding_turns/migration.sql +12 -0
  215. package/prisma/migrations/20261005000000_review_fix_rounds/migration.sql +5 -0
  216. package/prisma/migrations/20261006000000_parallel_delegations/migration.sql +6 -0
  217. package/prisma/migrations/20261007000000_ci_rereview/migration.sql +4 -0
  218. package/prisma/migrations/20261008000000_review_after_ci/migration.sql +33 -0
  219. package/prisma/migrations/20261009000001_coding_run_local_branch/migration.sql +7 -0
  220. package/prisma/migrations/20261009000002_local_review/migration.sql +50 -0
  221. package/prisma/schema.prisma +98 -1
@@ -16,6 +16,7 @@ export declare const CodingProfileSchema: z.ZodObject<{
16
16
  toolchainVersion: z.ZodDefault<z.ZodNullable<z.ZodString>>;
17
17
  workerImageRef: z.ZodDefault<z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>>;
18
18
  workspaceDiskMb: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
19
+ maxTurns: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
19
20
  packageAllowlist: z.ZodDefault<z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString, "many">>, Record<string, string[]>, Record<string, string[]>>>;
20
21
  packagePolicy: z.ZodDefault<z.ZodEffects<z.ZodObject<{
21
22
  minReleaseAgeDays: z.ZodOptional<z.ZodNumber>;
@@ -32,6 +33,7 @@ export declare const CodingProfileSchema: z.ZodObject<{
32
33
  }, "strict", z.ZodTypeAny, {
33
34
  repository: string;
34
35
  baseRef: string;
36
+ maxTurns: number | null;
35
37
  services: string[];
36
38
  provider: "codex" | "claude-code";
37
39
  timeoutSec: number;
@@ -50,6 +52,7 @@ export declare const CodingProfileSchema: z.ZodObject<{
50
52
  }, {
51
53
  repository: string;
52
54
  baseRef?: string | undefined;
55
+ maxTurns?: number | null | undefined;
53
56
  services?: string[] | undefined;
54
57
  provider?: "codex" | "claude-code" | undefined;
55
58
  timeoutSec?: number | undefined;
@@ -79,6 +82,7 @@ export declare const CodingProfilePatchSchema: z.ZodObject<{
79
82
  toolchainVersion: z.ZodOptional<z.ZodNullable<z.ZodString>>;
80
83
  workerImageRef: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>>;
81
84
  workspaceDiskMb: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
85
+ maxTurns: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
82
86
  packageAllowlist: z.ZodOptional<z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString, "many">>, Record<string, string[]>, Record<string, string[]>>>;
83
87
  packagePolicy: z.ZodOptional<z.ZodEffects<z.ZodObject<{
84
88
  minReleaseAgeDays: z.ZodOptional<z.ZodNumber>;
@@ -95,6 +99,7 @@ export declare const CodingProfilePatchSchema: z.ZodObject<{
95
99
  }, "strict", z.ZodTypeAny, {
96
100
  repository?: string | undefined;
97
101
  baseRef?: string | undefined;
102
+ maxTurns?: number | null | undefined;
98
103
  services?: string[] | undefined;
99
104
  provider?: "codex" | "claude-code" | undefined;
100
105
  timeoutSec?: number | undefined;
@@ -113,6 +118,7 @@ export declare const CodingProfilePatchSchema: z.ZodObject<{
113
118
  }, {
114
119
  repository?: string | undefined;
115
120
  baseRef?: string | undefined;
121
+ maxTurns?: number | null | undefined;
116
122
  services?: string[] | undefined;
117
123
  provider?: "codex" | "claude-code" | undefined;
118
124
  timeoutSec?: number | undefined;
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { isImmutableDockerImage } from "../providers/jobs/docker-isolation.js";
3
- import { MAX_CODING_TASK_BYTES, normalizeGitHubRepository, normalizeGitRef } from "./protocol.js";
3
+ import { MAX_CODING_TASK_BYTES, MAX_CODING_TURNS, isLocalRepository, normalizeGitHubRepository, normalizeGitRef, normalizeLocalRepository, } from "./protocol.js";
4
4
  import { MAX_COLLECT_EXCLUDE_PATHS, validateCollectExcludePath } from "./collect-exclude.js";
5
5
  import { CODING_PROVIDERS } from "./provider.js";
6
6
  import { WARDBY_PROTECTED_PATHS, isWellFormedProtectedPath, protectsSomePath } from "./protected-paths.js";
@@ -24,18 +24,22 @@ const INVALID_SINGLE_LINE_CONTROL = /[\u0000-\u001F\u007F]/;
24
24
  function byteLength(value) {
25
25
  return Buffer.byteLength(value, "utf8");
26
26
  }
27
+ /** GitHub names, or `local:/abs/path` (syntactic only; trusted roots are enforced where it is authorized and used). */
28
+ function normalizeProfileRepository(value) {
29
+ return isLocalRepository(value) ? normalizeLocalRepository(value) : normalizeGitHubRepository(value);
30
+ }
27
31
  const repositorySchema = z
28
32
  .string()
29
33
  .refine((value) => {
30
34
  try {
31
- normalizeGitHubRepository(value);
35
+ normalizeProfileRepository(value);
32
36
  return true;
33
37
  }
34
38
  catch {
35
39
  return false;
36
40
  }
37
- }, "must be a canonical name or uncredentialed github.com repository")
38
- .transform(normalizeGitHubRepository);
41
+ }, "must be a canonical name, an uncredentialed github.com repository, or local:/absolute/path")
42
+ .transform(normalizeProfileRepository);
39
43
  const baseRefSchema = z
40
44
  .string()
41
45
  .refine((value) => {
@@ -80,6 +84,8 @@ const workerImageRefSchema = z
80
84
  .refine(isImmutableDockerImage, "must be an immutable repository digest")
81
85
  .nullable();
82
86
  const workspaceDiskMbSchema = z.number().int().min(64).max(32_768).nullable();
87
+ /** Claude Code runs only: the most agent turns a run may take; null = the worker default (DEFAULT_CLAUDE_MAX_TURNS). */
88
+ const maxTurnsSchema = z.number().int().min(1).max(MAX_CODING_TURNS).nullable();
83
89
  const packageAllowlistSchema = z
84
90
  .record(z.string(), z.array(z.string().min(1).max(256)).max(256))
85
91
  .superRefine((value, ctx) => {
@@ -115,6 +121,7 @@ const codingProfileFields = {
115
121
  toolchainVersion: toolchainVersionSchema,
116
122
  workerImageRef: workerImageRefSchema,
117
123
  workspaceDiskMb: workspaceDiskMbSchema,
124
+ maxTurns: maxTurnsSchema,
118
125
  protectedPaths: z
119
126
  .array(protectedPathSchema)
120
127
  .min(1)
@@ -144,6 +151,7 @@ export const CodingProfileSchema = z
144
151
  toolchainVersion: codingProfileFields.toolchainVersion.default(null),
145
152
  workerImageRef: codingProfileFields.workerImageRef.default(null),
146
153
  workspaceDiskMb: codingProfileFields.workspaceDiskMb.default(null),
154
+ maxTurns: codingProfileFields.maxTurns.default(null),
147
155
  packageAllowlist: codingProfileFields.packageAllowlist.default({}),
148
156
  packagePolicy: codingProfileFields.packagePolicy.default({}),
149
157
  services: codingProfileFields.services.default([]),
@@ -163,6 +171,7 @@ export const CodingProfilePatchSchema = z
163
171
  toolchainVersion: codingProfileFields.toolchainVersion.optional(),
164
172
  workerImageRef: codingProfileFields.workerImageRef.optional(),
165
173
  workspaceDiskMb: codingProfileFields.workspaceDiskMb.optional(),
174
+ maxTurns: codingProfileFields.maxTurns.optional(),
166
175
  packageAllowlist: codingProfileFields.packageAllowlist.optional(),
167
176
  packagePolicy: codingProfileFields.packagePolicy.optional(),
168
177
  services: codingProfileFields.services.optional(),
@@ -37,6 +37,17 @@ export declare function isReservedServiceEnvName(name: string): boolean;
37
37
  export declare const CodingServiceTestEnvSchema: z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>, Record<string, string>, Record<string, string>>;
38
38
  export declare function normalizeGitHubRepository(value: string): string;
39
39
  export declare function normalizeGitRef(value: string): string;
40
+ /** Repository identity prefix for a git folder on the wardby host: `local:/abs/path`. */
41
+ export declare const LOCAL_REPO_PREFIX = "local:";
42
+ export declare function isLocalRepository(value: string): boolean;
43
+ /** Upper bound on a whole `local:/abs/path` value, in UTF-8 bytes. */
44
+ export declare const MAX_LOCAL_REPOSITORY_BYTES = 4096;
45
+ /**
46
+ * Syntactic only: at most MAX_LOCAL_REPOSITORY_BYTES, no ASCII control
47
+ * characters (NUL included), absolute, normalized; returns "local:/abs/path"
48
+ * (no trailing slash).
49
+ */
50
+ export declare function normalizeLocalRepository(value: string): string;
40
51
  export declare const CodingBaseRefSchema: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
41
52
  export declare const CodingTaskOverrideSchema: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>, string, string>;
42
53
  /**
@@ -69,6 +80,10 @@ export declare function composeCodingTask(instructions: string | null | undefine
69
80
  * re-validates whatever survives.
70
81
  */
71
82
  export declare function normalizeCodingTag(value: unknown): string | null | undefined;
83
+ /** Upper bound on CodingAgentProfile.maxTurns (Claude Code runs). */
84
+ export declare const MAX_CODING_TURNS = 1000;
85
+ /** The Claude Code worker's turn limit when the agent sets none; the run's budget and timeout are the real limits. */
86
+ export declare const DEFAULT_CLAUDE_MAX_TURNS = 200;
72
87
  /** Longest line a debug-traced worker writes to its log (src/coding-worker/debug-trace.ts). */
73
88
  export declare const MAX_DEBUG_TRACE_LINE_BYTES: number;
74
89
  export declare const CodingTaskInputSchema: z.ZodEffects<z.ZodObject<{
@@ -103,6 +118,12 @@ export declare const CodingTaskInputSchema: z.ZodEffects<z.ZodObject<{
103
118
  * untraced run is unchanged for workers that predate it.
104
119
  */
105
120
  debugTrace: z.ZodOptional<z.ZodBoolean>;
121
+ /**
122
+ * Claude Code turn limit (CodingAgentProfile.maxTurns, fixed on the run at
123
+ * dispatch). Absent means the worker default; written only when the agent
124
+ * sets one, so other runs' input is unchanged for workers that predate it.
125
+ */
126
+ maxTurns: z.ZodOptional<z.ZodNumber>;
106
127
  /**
107
128
  * Coding-run services started next to this run (docs/coding-services.md):
108
129
  * each one's catalog name and version, and the variables the agent's shells
@@ -137,6 +158,7 @@ export declare const CodingTaskInputSchema: z.ZodEffects<z.ZodObject<{
137
158
  runId: string;
138
159
  } | undefined;
139
160
  debugTrace?: boolean | undefined;
161
+ maxTurns?: number | undefined;
140
162
  services?: {
141
163
  name: string;
142
164
  version: string;
@@ -156,6 +178,7 @@ export declare const CodingTaskInputSchema: z.ZodEffects<z.ZodObject<{
156
178
  runId: string;
157
179
  } | undefined;
158
180
  debugTrace?: boolean | undefined;
181
+ maxTurns?: number | undefined;
159
182
  services?: {
160
183
  name: string;
161
184
  version: string;
@@ -175,6 +198,7 @@ export declare const CodingTaskInputSchema: z.ZodEffects<z.ZodObject<{
175
198
  runId: string;
176
199
  } | undefined;
177
200
  debugTrace?: boolean | undefined;
201
+ maxTurns?: number | undefined;
178
202
  services?: {
179
203
  name: string;
180
204
  version: string;
@@ -194,6 +218,7 @@ export declare const CodingTaskInputSchema: z.ZodEffects<z.ZodObject<{
194
218
  runId: string;
195
219
  } | undefined;
196
220
  debugTrace?: boolean | undefined;
221
+ maxTurns?: number | undefined;
197
222
  services?: {
198
223
  name: string;
199
224
  version: string;
@@ -259,7 +284,7 @@ export declare const CodingAgentOutputSchema: z.ZodEffects<z.ZodObject<{
259
284
  }>;
260
285
  export declare const CodingRunResultSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
261
286
  schemaVersion: z.ZodLiteral<1>;
262
- outcome: z.ZodEnum<["pull_request_opened", "pull_request_updated", "no_changes", "budget_exhausted"]>;
287
+ outcome: z.ZodEnum<["pull_request_opened", "pull_request_updated", "branch_pushed", "no_changes", "budget_exhausted"]>;
263
288
  repository: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
264
289
  baseRef: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
265
290
  headRef: z.ZodOptional<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
@@ -293,7 +318,7 @@ export declare const CodingRunResultSchema: z.ZodEffects<z.ZodEffects<z.ZodObjec
293
318
  tag: z.ZodEffects<z.ZodOptional<z.ZodNullable<z.ZodString>>, string | undefined, string | null | undefined>;
294
319
  }, "strict", z.ZodTypeAny, {
295
320
  repository: string;
296
- outcome: "no_changes" | "budget_exhausted" | "pull_request_opened" | "pull_request_updated";
321
+ outcome: "no_changes" | "budget_exhausted" | "pull_request_opened" | "pull_request_updated" | "branch_pushed";
297
322
  schemaVersion: 1;
298
323
  baseRef: string;
299
324
  summary: string;
@@ -313,7 +338,7 @@ export declare const CodingRunResultSchema: z.ZodEffects<z.ZodEffects<z.ZodObjec
313
338
  pullRequestNumber?: number | undefined;
314
339
  }, {
315
340
  repository: string;
316
- outcome: "no_changes" | "budget_exhausted" | "pull_request_opened" | "pull_request_updated";
341
+ outcome: "no_changes" | "budget_exhausted" | "pull_request_opened" | "pull_request_updated" | "branch_pushed";
317
342
  schemaVersion: 1;
318
343
  baseRef: string;
319
344
  summary: string;
@@ -333,7 +358,7 @@ export declare const CodingRunResultSchema: z.ZodEffects<z.ZodEffects<z.ZodObjec
333
358
  pullRequestNumber?: number | undefined;
334
359
  }>, {
335
360
  repository: string;
336
- outcome: "no_changes" | "budget_exhausted" | "pull_request_opened" | "pull_request_updated";
361
+ outcome: "no_changes" | "budget_exhausted" | "pull_request_opened" | "pull_request_updated" | "branch_pushed";
337
362
  schemaVersion: 1;
338
363
  baseRef: string;
339
364
  summary: string;
@@ -353,7 +378,7 @@ export declare const CodingRunResultSchema: z.ZodEffects<z.ZodEffects<z.ZodObjec
353
378
  pullRequestNumber?: number | undefined;
354
379
  }, {
355
380
  repository: string;
356
- outcome: "no_changes" | "budget_exhausted" | "pull_request_opened" | "pull_request_updated";
381
+ outcome: "no_changes" | "budget_exhausted" | "pull_request_opened" | "pull_request_updated" | "branch_pushed";
357
382
  schemaVersion: 1;
358
383
  baseRef: string;
359
384
  summary: string;
@@ -379,7 +404,7 @@ export declare const CodingRunResultSchema: z.ZodEffects<z.ZodEffects<z.ZodObjec
379
404
  outcome: "passed" | "failed" | "skipped";
380
405
  }[];
381
406
  repository: string;
382
- outcome: "no_changes" | "budget_exhausted" | "pull_request_opened" | "pull_request_updated";
407
+ outcome: "no_changes" | "budget_exhausted" | "pull_request_opened" | "pull_request_updated" | "branch_pushed";
383
408
  schemaVersion: 1;
384
409
  baseRef: string;
385
410
  usage: {
@@ -393,7 +418,7 @@ export declare const CodingRunResultSchema: z.ZodEffects<z.ZodEffects<z.ZodObjec
393
418
  pullRequestNumber?: number | undefined;
394
419
  }, {
395
420
  repository: string;
396
- outcome: "no_changes" | "budget_exhausted" | "pull_request_opened" | "pull_request_updated";
421
+ outcome: "no_changes" | "budget_exhausted" | "pull_request_opened" | "pull_request_updated" | "branch_pushed";
397
422
  schemaVersion: 1;
398
423
  baseRef: string;
399
424
  summary: string;
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { isAbsolute, resolve } from "node:path";
2
3
  export const CODING_PROTOCOL_VERSION = 1;
3
4
  /** The code host every coding run's repository and pull request live on (see pullRequestUrlSchema); the one place to change when another is added. */
4
5
  export const CODING_CODE_PROVIDER = "github";
@@ -192,10 +193,50 @@ function isGitRef(value) {
192
193
  return false;
193
194
  }
194
195
  }
196
+ /** Repository identity prefix for a git folder on the wardby host: `local:/abs/path`. */
197
+ export const LOCAL_REPO_PREFIX = "local:";
198
+ export function isLocalRepository(value) {
199
+ return value.startsWith(LOCAL_REPO_PREFIX);
200
+ }
201
+ /** Upper bound on a whole `local:/abs/path` value, in UTF-8 bytes. */
202
+ export const MAX_LOCAL_REPOSITORY_BYTES = 4096;
203
+ /**
204
+ * Syntactic only: at most MAX_LOCAL_REPOSITORY_BYTES, no ASCII control
205
+ * characters (NUL included), absolute, normalized; returns "local:/abs/path"
206
+ * (no trailing slash).
207
+ */
208
+ export function normalizeLocalRepository(value) {
209
+ if (!isLocalRepository(value))
210
+ throw new Error("local repository must start with local:");
211
+ if (byteLength(value) > MAX_LOCAL_REPOSITORY_BYTES) {
212
+ throw new Error(`local repository must be at most ${MAX_LOCAL_REPOSITORY_BYTES} UTF-8 bytes`);
213
+ }
214
+ const path = value.slice(LOCAL_REPO_PREFIX.length);
215
+ if (INVALID_SINGLE_LINE_CONTROL.test(path)) {
216
+ throw new Error("local repository path must not contain control characters");
217
+ }
218
+ if (!isAbsolute(path))
219
+ throw new Error("local repository path must be absolute");
220
+ return `${LOCAL_REPO_PREFIX}${resolve(path)}`;
221
+ }
222
+ function isCodingRepository(value) {
223
+ if (!isLocalRepository(value))
224
+ return isGitHubRepository(value);
225
+ try {
226
+ normalizeLocalRepository(value);
227
+ return true;
228
+ }
229
+ catch {
230
+ return false;
231
+ }
232
+ }
233
+ function normalizeCodingRepository(value) {
234
+ return isLocalRepository(value) ? normalizeLocalRepository(value) : normalizeGitHubRepository(value);
235
+ }
195
236
  const repositorySchema = z
196
237
  .string()
197
- .refine(isGitHubRepository, "must be a canonical name or uncredentialed github.com repository")
198
- .transform(normalizeGitHubRepository);
238
+ .refine(isCodingRepository, "must be a canonical name, an uncredentialed github.com repository, or local:/absolute/path")
239
+ .transform(normalizeCodingRepository);
199
240
  export const CodingBaseRefSchema = z.string().refine(isGitRef, "must be a safe branch ref").transform(normalizeGitRef);
200
241
  export const CodingTaskOverrideSchema = boundedText(MAX_CODING_TASK_BYTES);
201
242
  /**
@@ -276,6 +317,10 @@ const testResultSchema = z
276
317
  outcome: z.enum(["passed", "failed", "skipped"]),
277
318
  })
278
319
  .strict();
320
+ /** Upper bound on CodingAgentProfile.maxTurns (Claude Code runs). */
321
+ export const MAX_CODING_TURNS = 1000;
322
+ /** The Claude Code worker's turn limit when the agent sets none; the run's budget and timeout are the real limits. */
323
+ export const DEFAULT_CLAUDE_MAX_TURNS = 200;
279
324
  /** Longest line a debug-traced worker writes to its log (src/coding-worker/debug-trace.ts). */
280
325
  export const MAX_DEBUG_TRACE_LINE_BYTES = 16 * 1024;
281
326
  export const CodingTaskInputSchema = z
@@ -305,6 +350,12 @@ export const CodingTaskInputSchema = z
305
350
  * untraced run is unchanged for workers that predate it.
306
351
  */
307
352
  debugTrace: z.boolean().optional(),
353
+ /**
354
+ * Claude Code turn limit (CodingAgentProfile.maxTurns, fixed on the run at
355
+ * dispatch). Absent means the worker default; written only when the agent
356
+ * sets one, so other runs' input is unchanged for workers that predate it.
357
+ */
358
+ maxTurns: z.number().int().min(1).max(MAX_CODING_TURNS).optional(),
308
359
  /**
309
360
  * Coding-run services started next to this run (docs/coding-services.md):
310
361
  * each one's catalog name and version, and the variables the agent's shells
@@ -367,7 +418,7 @@ const pullRequestUrlSchema = z
367
418
  export const CodingRunResultSchema = z
368
419
  .object({
369
420
  schemaVersion: z.literal(CODING_PROTOCOL_VERSION),
370
- outcome: z.enum(["pull_request_opened", "pull_request_updated", "no_changes", "budget_exhausted"]),
421
+ outcome: z.enum(["pull_request_opened", "pull_request_updated", "branch_pushed", "no_changes", "budget_exhausted"]),
371
422
  repository: repositorySchema,
372
423
  baseRef: CodingBaseRefSchema,
373
424
  headRef: CodingBaseRefSchema.optional(),
@@ -390,7 +441,18 @@ export const CodingRunResultSchema = z
390
441
  if (isPrOutcome && prFields.some((field) => field === undefined)) {
391
442
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: "a PR outcome requires all PR fields" });
392
443
  }
393
- if (!isPrOutcome && prFields.some((field) => field !== undefined)) {
444
+ if (value.outcome === "branch_pushed") {
445
+ if (value.headRef === undefined || value.commitSha === undefined) {
446
+ ctx.addIssue({
447
+ code: z.ZodIssueCode.custom,
448
+ message: "a pushed-branch outcome requires headRef and commitSha",
449
+ });
450
+ }
451
+ if (value.pullRequestUrl !== undefined || value.pullRequestNumber !== undefined) {
452
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "a pushed-branch outcome has no pull request" });
453
+ }
454
+ }
455
+ else if (!isPrOutcome && prFields.some((field) => field !== undefined)) {
394
456
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: "non-PR outcomes must not include PR fields" });
395
457
  }
396
458
  if (value.pullRequestUrl && value.pullRequestNumber) {
@@ -25,6 +25,7 @@ export declare function readCodingInput(path: string): Promise<{
25
25
  runId: string;
26
26
  } | undefined;
27
27
  debugTrace?: boolean | undefined;
28
+ maxTurns?: number | undefined;
28
29
  services?: {
29
30
  name: string;
30
31
  version: string;
@@ -10,6 +10,8 @@ const SAFE_WORKER_ERROR_CODES = new Set([
10
10
  "coding_artifact_nesting_limit",
11
11
  "coding_artifact_duplicate_key",
12
12
  "coding_turn_failed",
13
+ // Claude Code reached the run's turn limit (CodingAgentProfile.maxTurns) before finishing.
14
+ "coding_turn_limit",
13
15
  "coding_stream_failed",
14
16
  "coding_stream_proxy_denied",
15
17
  "coding_stream_rate_limited",
@@ -37,6 +37,8 @@ export interface GitHubVcsConfig {
37
37
  maxChangedFiles?: number;
38
38
  maxDiffBytes?: number;
39
39
  }
40
+ /** Trusted roots for `local:` repositories (LOCAL_REPO_ROOTS, path-delimiter separated); unset = local repositories disabled. */
41
+ export { loadLocalRepoRoots } from "../coding/local-repo.js";
40
42
  export declare function loadGitHubVcsConfig(env?: NodeJS.ProcessEnv): GitHubVcsConfig;
41
43
  /** GitHub App webhook settings for the code-review host ingress; unset = ingress disabled. */
42
44
  export declare function loadGitHubEventConfig(env?: NodeJS.ProcessEnv): {
@@ -100,6 +102,11 @@ export interface ContainerExecutorConfig {
100
102
  maxDiskMb: number;
101
103
  additionalWorkerImages: Record<string, Record<string, string>>;
102
104
  }
105
+ /**
106
+ * An image variable's value, trimmed; empty or blank is unset. An env file that ships
107
+ * `CODING_WORKER_IMAGE=` must read as "no Codex worker", not as an invalid image.
108
+ */
109
+ export declare function imageVariable(value: string | undefined): string | undefined;
103
110
  export declare function loadContainerExecutorConfig(env?: NodeJS.ProcessEnv): ContainerExecutorConfig;
104
111
  /**
105
112
  * Caps coding runs holding a concurrency slot across every control-plane
@@ -146,6 +153,12 @@ export interface KubernetesJobConfig {
146
153
  * alone is sufficient — no separate overall-timeout knob needs to move in lockstep.
147
154
  */
148
155
  enforcementExecTimeoutMs?: number;
156
+ /**
157
+ * The namespace ResourceQuota coding-run pods count against (KUBERNETES_RESOURCE_QUOTA). When
158
+ * set, a run whose pod would not fit the quota's free room waits in the coding queue instead of
159
+ * failing when the API server refuses the pod. Unset = no check (the RBAC grant is by name).
160
+ */
161
+ resourceQuota?: string;
149
162
  }
150
163
  export declare function loadKubernetesJobConfig(env?: NodeJS.ProcessEnv): KubernetesJobConfig;
151
164
  export interface McpConfig {
@@ -29,6 +29,8 @@ function optionalPositiveInteger(value, name) {
29
29
  throw new Error(`${name} must be a positive integer.`);
30
30
  return parsed;
31
31
  }
32
+ /** Trusted roots for `local:` repositories (LOCAL_REPO_ROOTS, path-delimiter separated); unset = local repositories disabled. */
33
+ export { loadLocalRepoRoots } from "../coding/local-repo.js";
32
34
  export function loadGitHubVcsConfig(env = process.env) {
33
35
  return {
34
36
  appId: env.GITHUB_APP_ID,
@@ -152,15 +154,22 @@ function optionalBoundedInteger(value, name, min, max) {
152
154
  }
153
155
  return parsed;
154
156
  }
157
+ /**
158
+ * An image variable's value, trimmed; empty or blank is unset. An env file that ships
159
+ * `CODING_WORKER_IMAGE=` must read as "no Codex worker", not as an invalid image.
160
+ */
161
+ export function imageVariable(value) {
162
+ return value?.trim() || undefined;
163
+ }
155
164
  export function loadContainerExecutorConfig(env = process.env) {
156
165
  const additionalWorkerImages = {};
157
- if (env.CODING_WORKER_IMAGE_NODE_PYTHON_3_12) {
158
- additionalWorkerImages["node-python"] = { "3.12": env.CODING_WORKER_IMAGE_NODE_PYTHON_3_12 };
159
- }
166
+ const nodePythonWorker = imageVariable(env.CODING_WORKER_IMAGE_NODE_PYTHON_3_12);
167
+ if (nodePythonWorker)
168
+ additionalWorkerImages["node-python"] = { "3.12": nodePythonWorker };
160
169
  const claudeToolRunnerImages = {};
161
- if (env.CODING_CLAUDE_TOOL_RUNNER_IMAGE_NODE_PYTHON_3_12) {
162
- claudeToolRunnerImages["node-python"] = { "3.12": env.CODING_CLAUDE_TOOL_RUNNER_IMAGE_NODE_PYTHON_3_12 };
163
- }
170
+ const nodePythonToolRunner = imageVariable(env.CODING_CLAUDE_TOOL_RUNNER_IMAGE_NODE_PYTHON_3_12);
171
+ if (nodePythonToolRunner)
172
+ claudeToolRunnerImages["node-python"] = { "3.12": nodePythonToolRunner };
164
173
  const diskMb = optionalPositiveInteger(env.CODING_DISK_MB, "CODING_DISK_MB") ?? 2048;
165
174
  // Defaults to the effective diskMb: raising the ceiling an agents:write caller can request is an
166
175
  // explicit operator choice, so upgrading with an unchanged environment changes nothing.
@@ -169,9 +178,9 @@ export function loadContainerExecutorConfig(env = process.env) {
169
178
  throw new Error(`CODING_MAX_DISK_MB (${maxDiskMb}) must be at least the effective CODING_DISK_MB (${diskMb}).`);
170
179
  }
171
180
  return {
172
- workerImage: env.CODING_WORKER_IMAGE,
173
- claudeWorkerImage: env.CODING_CLAUDE_WORKER_IMAGE,
174
- claudeToolRunnerImage: env.CODING_CLAUDE_TOOL_RUNNER_IMAGE,
181
+ workerImage: imageVariable(env.CODING_WORKER_IMAGE),
182
+ claudeWorkerImage: imageVariable(env.CODING_CLAUDE_WORKER_IMAGE),
183
+ claudeToolRunnerImage: imageVariable(env.CODING_CLAUDE_TOOL_RUNNER_IMAGE),
175
184
  claudeToolRunnerImages,
176
185
  proxyContainer: env.CODING_PROXY_CONTAINER,
177
186
  stateRoot: env.CODING_JOB_STATE_ROOT,
@@ -228,6 +237,9 @@ export function loadKubernetesJobConfig(env = process.env) {
228
237
  };
229
238
  if (env.KUBERNETES_CONTEXT)
230
239
  config.context = env.KUBERNETES_CONTEXT;
240
+ if (env.KUBERNETES_RESOURCE_QUOTA) {
241
+ config.resourceQuota = dnsLabel(env.KUBERNETES_RESOURCE_QUOTA, "KUBERNETES_RESOURCE_QUOTA", "");
242
+ }
231
243
  if (env.KUBERNETES_RUNTIME_CLASS) {
232
244
  config.runtimeClassName = dnsLabel(env.KUBERNETES_RUNTIME_CLASS, "KUBERNETES_RUNTIME_CLASS", "");
233
245
  }
@@ -77,6 +77,19 @@ export declare function linkedPullRequestAttribution(db: Pick<PrismaClient, "iss
77
77
  timeoutMs?: number;
78
78
  retryOn429?: boolean;
79
79
  }): Promise<AttributionIntent | undefined>;
80
+ /**
81
+ * Attribution for a run on a pull request a wardby coding run opened, taken
82
+ * from that run's own issue. Covers a review that starts before the pull
83
+ * request is linked to its issue: the link is recorded when the lead run that
84
+ * delegated the work finishes, and a review can start earlier. The run id must
85
+ * come from the marker on an App-authored pull request
86
+ * (CodeReviewHost.pullRequestOrigin), never from pull request text. Undefined
87
+ * when that run is not this deployment's or has no issue.
88
+ */
89
+ export declare function openingRunAttribution(db: Pick<PrismaClient, "codingRun" | "workItem">, trackers: IssueTrackerRegistry | undefined, openingRunId: string, opts?: {
90
+ timeoutMs?: number;
91
+ retryOn429?: boolean;
92
+ }): Promise<AttributionIntent | undefined>;
80
93
  /** A refusal whose message is safe to return to the caller. */
81
94
  export declare class AttributionError extends Error {
82
95
  constructor(message: string);
@@ -167,6 +167,36 @@ export async function linkedPullRequestAttribution(db, trackers, pr, opts) {
167
167
  return undefined;
168
168
  return { source: "linked_pr", item: await resolveWorkItem(db, trackers, issue.provider, issue.key, opts) };
169
169
  }
170
+ /**
171
+ * Attribution for a run on a pull request a wardby coding run opened, taken
172
+ * from that run's own issue. Covers a review that starts before the pull
173
+ * request is linked to its issue: the link is recorded when the lead run that
174
+ * delegated the work finishes, and a review can start earlier. The run id must
175
+ * come from the marker on an App-authored pull request
176
+ * (CodeReviewHost.pullRequestOrigin), never from pull request text. Undefined
177
+ * when that run is not this deployment's or has no issue.
178
+ */
179
+ export async function openingRunAttribution(db, trackers, openingRunId, opts) {
180
+ try {
181
+ const row = await db.codingRun.findUnique({
182
+ where: { runId: openingRunId },
183
+ select: {
184
+ issueProvider: true,
185
+ issueKey: true,
186
+ rootCodingRun: { select: { issueProvider: true, issueKey: true } },
187
+ },
188
+ });
189
+ const provider = row?.issueProvider ?? row?.rootCodingRun?.issueProvider;
190
+ const key = row?.issueKey ?? row?.rootCodingRun?.issueKey;
191
+ if (!provider || !key)
192
+ return undefined;
193
+ return { source: "linked_pr", item: await resolveWorkItem(db, trackers, provider, key, opts) };
194
+ }
195
+ catch (err) {
196
+ log.warn({ err, openingRunId }, "opening run lookup failed; the run is unattributed");
197
+ return undefined;
198
+ }
199
+ }
170
200
  /** A refusal whose message is safe to return to the caller. */
171
201
  export class AttributionError extends Error {
172
202
  constructor(message) {
@@ -141,7 +141,9 @@ export interface RunTreeSpend {
141
141
  * twice, and the tree spends inside the root's hold, so counting that hold
142
142
  * would leave the tree nothing. It also leaves out holds of runs that started
143
143
  * after the root (they already counted the root's hold). capUsd is therefore
144
- * the tree's whole ceiling, and remainingUsd = capUsd - spentUsd.
144
+ * the tree's whole ceiling, and remainingUsd = capUsd - spentUsd - live
145
+ * sibling holds. remainingUsd also nets the unspent holds of in-flight
146
+ * sibling branches (parallel delegations, siblingHoldsUsd).
145
147
  */
146
148
  export declare function computeRunTreeSpend(db: BudgetGroupsDb, parentRunId: string, now?: Date): Promise<RunTreeSpend>;
147
149
  export type BudgetConstraint = Period | "run-tree";
@@ -163,17 +163,65 @@ async function collectTreeRunIds(db, rootRunId) {
163
163
  * twice, and the tree spends inside the root's hold, so counting that hold
164
164
  * would leave the tree nothing. It also leaves out holds of runs that started
165
165
  * after the root (they already counted the root's hold). capUsd is therefore
166
- * the tree's whole ceiling, and remainingUsd = capUsd - spentUsd.
166
+ * the tree's whole ceiling, and remainingUsd = capUsd - spentUsd - live
167
+ * sibling holds. remainingUsd also nets the unspent holds of in-flight
168
+ * sibling branches (parallel delegations, siblingHoldsUsd).
167
169
  */
168
170
  export async function computeRunTreeSpend(db, parentRunId, now = new Date()) {
169
171
  const { rootRunId, capUsd, spentUsd, remainingUsd } = await runTreeSpend(db, parentRunId, now);
170
172
  return { rootRunId, capUsd, spentUsd, remainingUsd };
171
173
  }
172
- async function runTreeSpend(db, parentRunId, now) {
174
+ /**
175
+ * The unspent holds of the tree's in-flight runs beside the asking run: every
176
+ * live run (isHoldLive) not on its ancestor chain -- siblings of the asking
177
+ * run or of an ancestor, and everything below them. Sequential delegation
178
+ * never has one; parallel delegations do, and without this each concurrent
179
+ * child would claim the same remainder. Each run holds its own reservation (a
180
+ * coding run's CodingRun.budgetReservedUsd, a native run's agent budgetUsd)
181
+ * minus its own cost: a native branch's budgetUsd bounds only its own model
182
+ * spend, so a coding grandchild under it holds its reservation separately. A
183
+ * finished run holds nothing and counts its real spend only, so the tree's
184
+ * remainder rises when a sibling finishes. With `self`, only runs that
185
+ * started before it count (first come, first served, as in computeGroupSpend).
186
+ */
187
+ function siblingHoldsUsd(rows, parentRunId, now, self) {
188
+ const byId = new Map(rows.map((r) => [r.id, r]));
189
+ const ancestors = new Set();
190
+ for (let id = parentRunId; id && !ancestors.has(id); id = byId.get(id)?.parentRunId ?? null) {
191
+ ancestors.add(id);
192
+ }
193
+ const queueTimeoutSec = loadCodingConcurrencyConfig().queueTimeoutSec;
194
+ let total = 0;
195
+ for (const r of rows) {
196
+ if (ancestors.has(r.id))
197
+ continue;
198
+ if (self && (r.id === self.id || !isOrderedBefore(r, self)))
199
+ continue;
200
+ if (!isHoldLive(r, now, queueTimeoutSec))
201
+ continue;
202
+ const reservation = r.codingRun ? Number(r.codingRun.budgetReservedUsd) : Number(r.agent.budgetUsd);
203
+ total += Math.max(0, reservation - Number(r.costUsd));
204
+ }
205
+ return total;
206
+ }
207
+ async function runTreeSpend(db, parentRunId, now, self) {
173
208
  const root = await findRootRun(db, parentRunId);
174
209
  const treeRunIds = await collectTreeRunIds(db, root.id);
175
- const rows = await db.run.findMany({ where: { id: { in: treeRunIds } }, select: { costUsd: true } });
210
+ const rows = await db.run.findMany({
211
+ where: { id: { in: treeRunIds } },
212
+ select: {
213
+ id: true,
214
+ parentRunId: true,
215
+ status: true,
216
+ costUsd: true,
217
+ startedAt: true,
218
+ heartbeatAt: true,
219
+ agent: { select: { budgetUsd: true } },
220
+ codingRun: { select: { budgetReservedUsd: true, timeoutSec: true } },
221
+ },
222
+ });
176
223
  const spentUsd = rows.reduce((sum, r) => sum + Number(r.costUsd), 0);
224
+ const heldUsd = siblingHoldsUsd(rows, parentRunId, now, self);
177
225
  const rootAgent = await db.agent.findUniqueOrThrow({
178
226
  where: { id: root.agentId },
179
227
  select: { id: true, budgetGroupId: true, budgetUsd: true },
@@ -190,7 +238,7 @@ async function runTreeSpend(db, parentRunId, now) {
190
238
  rootRunId: root.id,
191
239
  capUsd,
192
240
  spentUsd,
193
- remainingUsd: Math.max(0, capUsd - spentUsd),
241
+ remainingUsd: Math.max(0, capUsd - spentUsd - heldUsd),
194
242
  treeRunIds,
195
243
  root: rootOrder,
196
244
  rootBudgetGroupId: rootAgent.budgetGroupId,
@@ -219,7 +267,7 @@ export async function effectiveBudgetForRun(db, agent, now = new Date(), parentR
219
267
  const ownSpend = { excludeReservationRunIds: self ? [self.id] : [], holdsBefore: self };
220
268
  if (!parentRunId)
221
269
  return groupCappedBudget(db, agent, now, ownSpend);
222
- const tree = await runTreeSpend(db, parentRunId, now);
270
+ const tree = await runTreeSpend(db, parentRunId, now, self);
223
271
  const sharesRootGroup = agent.budgetGroupId !== null && agent.budgetGroupId === tree.rootBudgetGroupId;
224
272
  const spend = sharesRootGroup
225
273
  ? { excludeReservationRunIds: [...tree.treeRunIds, ...(self ? [self.id] : [])], holdsBefore: tree.root }
@@ -0,0 +1,8 @@
1
+ import type { CiView, PullRequestView } from "../providers/review-host/types.js";
2
+ export interface AgentCiView extends CiView {
3
+ /** The description carries Wardby's Dependency install incomplete warning. Description text: informational only. */
4
+ sandboxInstallIncomplete: boolean;
5
+ note: string;
6
+ }
7
+ export declare function ciNote(ci: CiView, sandboxInstallIncomplete: boolean): string;
8
+ export declare function ciForAgent(view: Pick<PullRequestView, "headSha" | "body" | "ci">): AgentCiView;