@wardby/cli 0.4.1 → 0.5.1

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 +486 -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 +127 -5
  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 +196 -10
  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 +30 -4
  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 +28 -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
@@ -0,0 +1,59 @@
1
+ /**
2
+ * What repo_pr_read tells a reviewer about CI. The note is deterministic and
3
+ * written here, not by the host, so every reviewer prompt gets the same rule:
4
+ * CI on the head commit is authoritative about build and tests; the
5
+ * description's Tests come from Wardby's sandbox, which may have had an
6
+ * incomplete install.
7
+ */
8
+ import { FAILING_CONCLUSIONS } from "../providers/review-host/ci.js";
9
+ import { DEPENDENCY_INSTALL_INCOMPLETE } from "../providers/vcs/github.js";
10
+ const MAX_NAMED = 10;
11
+ function named(ci, pick) {
12
+ const names = ci.checks.filter(pick).map((c) => c.name);
13
+ const shown = names.slice(0, MAX_NAMED).join(", ");
14
+ return names.length > MAX_NAMED ? `${shown} and ${names.length - MAX_NAMED} more` : shown;
15
+ }
16
+ export function ciNote(ci, sandboxInstallIncomplete) {
17
+ const n = ci.checks.length;
18
+ const plural = `${n} check${n === 1 ? "" : "s"}`;
19
+ const sandbox = sandboxInstallIncomplete
20
+ ? ` The description's ${DEPENDENCY_INSTALL_INCOMPLETE.replace(/\.$/, "")} warning means Wardby's sandbox could not install every package, so a failure under its Tests is not evidence against this change.`
21
+ : "";
22
+ const statuses = ci.statusesUnavailable && ci.state !== "unavailable"
23
+ ? " Commit statuses could not be read (often because the App lacks Commit statuses: Read), so only check runs are listed."
24
+ : "";
25
+ let main;
26
+ switch (ci.state) {
27
+ case "passing":
28
+ main = `CI passed on this head (${plural}). The description's Tests were run in Wardby's sandbox; where they disagree with CI, CI is authoritative.`;
29
+ break;
30
+ case "failing":
31
+ main = `CI failed on this head: ${named(ci, (c) => c.status === "completed" && FAILING_CONCLUSIONS.has(c.conclusion ?? ""))}. Base build and test findings on these checks, not on the description's Tests.`;
32
+ break;
33
+ case "pending":
34
+ main = `CI is still running on this head (${named(ci, (c) => c.status !== "completed")}). These results are not final: say they were pending; do not report them as passed or failed, and do not use the description's Tests in their place.`;
35
+ break;
36
+ case "inconclusive":
37
+ main = `CI finished without a clear result on this head (${plural}; some were cancelled, stale, or skipped). Do not report the change as failing CI.`;
38
+ break;
39
+ case "none":
40
+ main = "No CI checks or statuses are reported on this head: CI may not be configured, or has not started yet.";
41
+ break;
42
+ case "unavailable":
43
+ main = `CI results could not be read (${ci.unavailableReason ?? "unknown"}). Do not infer CI results from the description.`;
44
+ break;
45
+ }
46
+ return `${main}${statuses}${sandbox}`;
47
+ }
48
+ export function ciForAgent(view) {
49
+ const ci = view.ci ?? {
50
+ headSha: view.headSha,
51
+ state: "unavailable",
52
+ checks: [],
53
+ truncated: false,
54
+ statusesUnavailable: true,
55
+ unavailableReason: "host_unsupported",
56
+ };
57
+ const sandboxInstallIncomplete = (view.body ?? "").includes(`**${DEPENDENCY_INSTALL_INCOMPLETE}**`);
58
+ return { ...ci, sandboxInstallIncomplete, note: ciNote(ci, sandboxInstallIncomplete) };
59
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The children one run attempt's delegations have running right now, and a
3
+ * signal for when any of them finishes. Used with the delegation gate
4
+ * (serial-gate.ts) under Agent.parallelDelegations: a delegation refused for
5
+ * budget while a sibling still runs waits for that sibling to finish and
6
+ * free its hold, then tries again. A delegation that is waiting is not in
7
+ * flight itself, so two waiting delegations never wait on each other.
8
+ */
9
+ export interface DelegationSiblings {
10
+ /** Children started and not yet finished. */
11
+ readonly inFlight: number;
12
+ /** How many children have finished so far: pass it to nextFinish to not miss a finish in between. */
13
+ readonly finishes: number;
14
+ /** Counts one child as running. Returns its finish, which is idempotent. */
15
+ start(): () => void;
16
+ /**
17
+ * Resolves true once a child finishes after `sinceFinishes` was read (at once if one already
18
+ * has), or after this call when it is omitted; false after timeoutMs.
19
+ */
20
+ nextFinish(timeoutMs: number, sinceFinishes?: number): Promise<boolean>;
21
+ }
22
+ export declare function createDelegationSiblings(): DelegationSiblings;
@@ -0,0 +1,40 @@
1
+ export function createDelegationSiblings() {
2
+ let inFlight = 0;
3
+ let finishes = 0;
4
+ const waiters = new Set();
5
+ return {
6
+ get inFlight() {
7
+ return inFlight;
8
+ },
9
+ get finishes() {
10
+ return finishes;
11
+ },
12
+ start() {
13
+ inFlight += 1;
14
+ let finished = false;
15
+ return () => {
16
+ if (finished)
17
+ return;
18
+ finished = true;
19
+ inFlight -= 1;
20
+ finishes += 1;
21
+ for (const wake of [...waiters])
22
+ wake();
23
+ };
24
+ },
25
+ nextFinish(timeoutMs, sinceFinishes = finishes) {
26
+ if (finishes > sinceFinishes)
27
+ return Promise.resolve(true);
28
+ return new Promise((resolve) => {
29
+ const settle = (woken) => {
30
+ clearTimeout(timer);
31
+ waiters.delete(wake);
32
+ resolve(woken);
33
+ };
34
+ const wake = () => settle(true);
35
+ const timer = setTimeout(() => settle(false), Math.max(0, timeoutMs));
36
+ waiters.add(wake);
37
+ });
38
+ },
39
+ };
40
+ }
@@ -4,7 +4,7 @@ import { type AttributionIntent } from "./attribution.js";
4
4
  import { type BudgetConstraint } from "./budget-groups.js";
5
5
  import { type SelfDefectSink } from "./self-defects.js";
6
6
  export type DispatchDb = Pick<PrismaClient, "agent" | "run" | "runHostCheck" | "codingRun" | "task" | "webhook" | "budgetGroup" | "$transaction" | "$queryRaw">;
7
- export type DispatchTx = Pick<Prisma.TransactionClient, "agent" | "run" | "runHostCheck" | "runHostStatus" | "runIssueStatus" | "codingRun" | "codingService" | "task" | "webhook" | "budgetGroup" | "resourceGrant" | "workItem" | "runAttribution" | "$queryRaw" | "$executeRawUnsafe">;
7
+ export type DispatchTx = Pick<Prisma.TransactionClient, "agent" | "run" | "runHostCheck" | "runHostStatus" | "runIssueStatus" | "codingRun" | "codingService" | "task" | "webhook" | "budgetGroup" | "resourceGrant" | "workItem" | "localPullRequest" | "runAttribution" | "$queryRaw" | "$executeRawUnsafe">;
8
8
  type DispatchAgent = Prisma.AgentGetPayload<{
9
9
  include: {
10
10
  codingProfile: true;
@@ -83,6 +83,14 @@ export interface DispatchRunOptions {
83
83
  * e.g. a sub-agent dispatch tool blocking the parent's own turn.
84
84
  */
85
85
  awaitExecution?: boolean;
86
+ /**
87
+ * Called once, right after the persist transaction commits and before the
88
+ * executor starts the run (refused and failed rows included): the moment
89
+ * the run row is visible to other transactions. The runner releases its
90
+ * delegation admission gate here, so a sibling delegation is admitted
91
+ * while this one's coding run is still starting or queued.
92
+ */
93
+ onPersisted?: (run: Run) => void;
86
94
  }
87
95
  export interface DispatchRunResult {
88
96
  run: Run;
@@ -107,6 +115,13 @@ export interface DispatchRunResult {
107
115
  * @internal Exported only for the real-PostgreSQL tests.
108
116
  */
109
117
  export declare function isSerializationConflict(err: unknown): boolean;
118
+ /**
119
+ * Prisma's interactive-transaction error (P2028): no pooled connection within
120
+ * `maxWait` ("Unable to start a transaction in the given time"), or the
121
+ * transaction outlived its timeout. Either way nothing was committed, so the
122
+ * caller can retry or queue.
123
+ */
124
+ export declare function isTransactionUnavailable(err: unknown): boolean;
110
125
  /**
111
126
  * Record an executor-level failure on a run that never reached a terminal
112
127
  * state itself. Deliberately idempotent: the same failure can arrive twice —
@@ -124,12 +139,13 @@ export declare function markRunFailedFromExecutorError(db: Pick<DispatchDb, "run
124
139
  export declare function budgetExhaustedError(constraint: BudgetConstraint): string;
125
140
  /**
126
141
  * Attempts for the persist transaction, and the jittered backoff between
127
- * them, for serialization failures and deadlocks.
142
+ * them, for serialization failures, deadlocks and transactions that could
143
+ * not start in time (P2028).
128
144
  *
129
145
  * @internal Exported only for the real-PostgreSQL tests.
130
146
  */
131
147
  export declare const PERSIST_ATTEMPTS = 8;
132
- export type ContinuationRefusal = "unknown_run" | "other_repository" | "no_pull_request";
148
+ export type ContinuationRefusal = "unknown_run" | "other_repository" | "no_pull_request" | "other_owner";
133
149
  /** A continuation refused because of the run it names. Its message is fixed text, safe to show the model. */
134
150
  export declare class ContinuationRefusedError extends Error {
135
151
  readonly reason: ContinuationRefusal;
@@ -152,8 +168,16 @@ export type ContinuationCheck = {
152
168
  * run (or its root) is in this database, in that repository, and opened a pull
153
169
  * request. A run id read from a pull request can name a run that another
154
170
  * deployment sharing the same App made, so check before relying on one.
171
+ *
172
+ * `dispatchingOwnerId`, when given (even `null`), additionally fails closed
173
+ * unless the root's opening agent has that exact owner: a sibling-hint or a
174
+ * PR's own marker names a run id, but nothing stops it naming a run opened by
175
+ * another owner's agent in the same repository, and continuing that run would
176
+ * let this dispatch push to a PR another owner's agent controls. `undefined`
177
+ * skips the check for callers that only ask "does this deployment know this
178
+ * run at all" (a user-facing refusal message, not a dispatch).
155
179
  */
156
- export declare function checkContinuation(reader: Pick<DispatchTx, "codingRun">, runId: string, repository: string): Promise<ContinuationCheck>;
180
+ export declare function checkContinuation(reader: Pick<DispatchTx, "codingRun">, runId: string, repository: string, dispatchingOwnerId?: string | null): Promise<ContinuationCheck>;
157
181
  /**
158
182
  * Persists every detached run input in one transaction, then invokes the
159
183
  * executor only after commit. A null result means the caller's transactional
@@ -12,6 +12,7 @@ import { DEFAULT_KNOWLEDGE_BUNDLE_PATH } from "../knowledge/concept.js";
12
12
  import { KNOWLEDGE_INDEX_READ_MAX_BYTES, knowledgeSection } from "../knowledge/note.js";
13
13
  import { logger } from "./logger.js";
14
14
  import { CodingModelProviderMismatchError, resolveCodingEntry } from "./run-pricing.js";
15
+ import { CONTENDED_TX_MAX_WAIT_MS } from "./timing.js";
15
16
  import { fileSelfDefectForRun } from "./self-defects.js";
16
17
  const dispatchLog = logger.child({ module: "dispatch" });
17
18
  /** SQLSTATEs worth retrying the persist transaction for: serialization failure, deadlock. */
@@ -52,6 +53,15 @@ export function isSerializationConflict(err) {
52
53
  }
53
54
  return false;
54
55
  }
56
+ /**
57
+ * Prisma's interactive-transaction error (P2028): no pooled connection within
58
+ * `maxWait` ("Unable to start a transaction in the given time"), or the
59
+ * transaction outlived its timeout. Either way nothing was committed, so the
60
+ * caller can retry or queue.
61
+ */
62
+ export function isTransactionUnavailable(err) {
63
+ return Boolean(err && typeof err === "object" && err.code === "P2028");
64
+ }
55
65
  /**
56
66
  * Record an executor-level failure on a run that never reached a terminal
57
67
  * state itself. Deliberately idempotent: the same failure can arrive twice —
@@ -115,7 +125,8 @@ async function reserveCodingBudget(tx, agent, now, options) {
115
125
  }
116
126
  /**
117
127
  * Attempts for the persist transaction, and the jittered backoff between
118
- * them, for serialization failures and deadlocks.
128
+ * them, for serialization failures, deadlocks and transactions that could
129
+ * not start in time (P2028).
119
130
  *
120
131
  * @internal Exported only for the real-PostgreSQL tests.
121
132
  */
@@ -130,6 +141,7 @@ const CONTINUATION_REFUSALS = {
130
141
  unknown_run: "Cannot continue an unknown coding run.",
131
142
  other_repository: "Cannot continue a coding run from a different repository.",
132
143
  no_pull_request: "Cannot continue a coding run that never opened a pull request.",
144
+ other_owner: "Cannot continue a coding run opened by a different owner's agent.",
133
145
  };
134
146
  /** A continuation refused because of the run it names. Its message is fixed text, safe to show the model. */
135
147
  export class ContinuationRefusedError extends Error {
@@ -140,18 +152,31 @@ export class ContinuationRefusedError extends Error {
140
152
  this.name = "ContinuationRefusedError";
141
153
  }
142
154
  }
155
+ /** Carries the root's opening agent's owner along with the row, for the same-owner check below. */
156
+ const CONTINUATION_ROOT_INCLUDE = { run: { select: { agent: { select: { ownerId: true } } } } };
143
157
  /**
144
158
  * Whether this deployment can continue coding run `runId` in `repository`: the
145
159
  * run (or its root) is in this database, in that repository, and opened a pull
146
160
  * request. A run id read from a pull request can name a run that another
147
161
  * deployment sharing the same App made, so check before relying on one.
162
+ *
163
+ * `dispatchingOwnerId`, when given (even `null`), additionally fails closed
164
+ * unless the root's opening agent has that exact owner: a sibling-hint or a
165
+ * PR's own marker names a run id, but nothing stops it naming a run opened by
166
+ * another owner's agent in the same repository, and continuing that run would
167
+ * let this dispatch push to a PR another owner's agent controls. `undefined`
168
+ * skips the check for callers that only ask "does this deployment know this
169
+ * run at all" (a user-facing refusal message, not a dispatch).
148
170
  */
149
- export async function checkContinuation(reader, runId, repository) {
150
- const candidate = await reader.codingRun.findUnique({ where: { runId } });
171
+ export async function checkContinuation(reader, runId, repository, dispatchingOwnerId) {
172
+ const candidate = await reader.codingRun.findUnique({ where: { runId }, include: CONTINUATION_ROOT_INCLUDE });
151
173
  if (!candidate)
152
174
  return { ok: false, reason: "unknown_run" };
153
175
  const root = candidate.rootCodingRunId
154
- ? await reader.codingRun.findUnique({ where: { runId: candidate.rootCodingRunId } })
176
+ ? await reader.codingRun.findUnique({
177
+ where: { runId: candidate.rootCodingRunId },
178
+ include: CONTINUATION_ROOT_INCLUDE,
179
+ })
155
180
  : candidate;
156
181
  if (!root)
157
182
  return { ok: false, reason: "unknown_run" };
@@ -163,6 +188,14 @@ export async function checkContinuation(reader, runId, repository) {
163
188
  rootResult.pullRequestNumber === undefined) {
164
189
  return { ok: false, reason: "no_pull_request" };
165
190
  }
191
+ if (dispatchingOwnerId !== undefined) {
192
+ const rootOwnerId = root.run?.agent?.ownerId ?? null;
193
+ // A null owner on either side is never treated as a match: an owner-less
194
+ // agent gets no continuation trust, fail closed either direction.
195
+ if (dispatchingOwnerId === null || rootOwnerId === null || dispatchingOwnerId !== rootOwnerId) {
196
+ return { ok: false, reason: "other_owner" };
197
+ }
198
+ }
166
199
  return {
167
200
  ok: true,
168
201
  root: {
@@ -177,13 +210,15 @@ export async function checkContinuation(reader, runId, repository) {
177
210
  * Resolves a coding run's branch: an override, else a continuation root's
178
211
  * (see checkContinuation), else the profile's base.
179
212
  */
180
- async function resolveCodingBranch(reader, options, profile) {
213
+ async function resolveCodingBranch(reader, options, profile,
214
+ /** The dispatched (continuing) agent's current owner; checkContinuation fails closed against it. */
215
+ ownerId) {
181
216
  if (options.continuesCodingRunId === undefined)
182
217
  return { baseRef: options.codingBaseRef ?? profile.baseRef };
183
218
  if (options.codingBaseRef !== undefined) {
184
219
  throw new Error("continuesCodingRunId cannot be combined with codingBaseRef.");
185
220
  }
186
- const check = await checkContinuation(reader, options.continuesCodingRunId, profile.repository);
221
+ const check = await checkContinuation(reader, options.continuesCodingRunId, profile.repository, ownerId);
187
222
  if (!check.ok)
188
223
  throw new ContinuationRefusedError(check.reason);
189
224
  const { root } = check;
@@ -199,19 +234,22 @@ const UNREADABLE_DECLARATION = {
199
234
  github_file_too_large: `it is larger than ${MAX_SERVICE_DECLARATION_BYTES} bytes`,
200
235
  github_file_not_a_file: "it is not a file",
201
236
  github_file_not_utf8: "it is not UTF-8 text",
237
+ local_file_too_large: `it is larger than ${MAX_SERVICE_DECLARATION_BYTES} bytes`,
238
+ local_file_not_a_file: "it is not a file",
239
+ local_file_not_utf8: "it is not UTF-8 text",
202
240
  };
203
241
  /**
204
242
  * The knowledge note's input (docs/knowledge.md): the repository's bundle
205
243
  * index at the run's base, read before the transaction like the services
206
244
  * declaration. Any failure means no note, never a failed dispatch.
207
245
  */
208
- async function readKnowledgeIndex(options, profile, resolvedBranch) {
246
+ async function readKnowledgeIndex(options, profile, resolvedBranch, ownerId) {
209
247
  const executor = options.executor;
210
248
  if (!executor.readCodingRepositoryFile)
211
249
  return undefined;
212
250
  try {
213
251
  // Resolved here, inside the try: a branch problem is the transaction's to report, not the note's.
214
- const branch = resolvedBranch ?? (await resolveCodingBranch(options.db, options, profile));
252
+ const branch = resolvedBranch ?? (await resolveCodingBranch(options.db, options, profile, ownerId));
215
253
  const indexText = await executor.readCodingRepositoryFile({
216
254
  repository: profile.repository,
217
255
  baseRef: normalizeGitRef(branch.baseRef),
@@ -235,8 +273,8 @@ async function readKnowledgeIndex(options, profile, resolvedBranch) {
235
273
  * call, so it runs before the Serializable transaction, which reuses the
236
274
  * branch and resolves the services against the catalog and the agent.
237
275
  */
238
- async function readServiceDeclaration(options, profile) {
239
- const branch = await resolveCodingBranch(options.db, options, profile);
276
+ async function readServiceDeclaration(options, profile, ownerId) {
277
+ const branch = await resolveCodingBranch(options.db, options, profile, ownerId);
240
278
  const read = { repository: profile.repository, profileBaseRef: profile.baseRef, branch };
241
279
  const executor = options.executor;
242
280
  if (!executor.readCodingServiceDeclaration)
@@ -312,6 +350,7 @@ export async function dispatchRun(options) {
312
350
  where: { id: options.agentId },
313
351
  select: {
314
352
  kind: true,
353
+ ownerId: true,
315
354
  budgetGroupId: true,
316
355
  codingProfile: { select: { repository: true, baseRef: true, services: true } },
317
356
  },
@@ -322,11 +361,11 @@ export async function dispatchRun(options) {
322
361
  const declarationRead = preview?.kind === "coding" &&
323
362
  preview.codingProfile &&
324
363
  parseAllowedServiceNames(preview.codingProfile.services).length > 0
325
- ? await readServiceDeclaration(options, preview.codingProfile)
364
+ ? await readServiceDeclaration(options, preview.codingProfile, preview.ownerId ?? null)
326
365
  : undefined;
327
366
  // The knowledge note's input: the repository's bundle index at the run's base (a network call).
328
367
  const knowledgeIndex = preview?.kind === "coding" && preview.codingProfile && options.executor.readCodingRepositoryFile
329
- ? await readKnowledgeIndex(options, preview.codingProfile, declarationRead?.branch)
368
+ ? await readKnowledgeIndex(options, preview.codingProfile, declarationRead?.branch, preview.ownerId ?? null)
330
369
  : undefined;
331
370
  const persistOnce = () => options.db.$transaction(async (tx) => {
332
371
  if (lockGroups)
@@ -438,7 +477,7 @@ export async function dispatchRun(options) {
438
477
  const task = composeCodingTask(agent.systemPrompt, request, servicesInstructionNote(services), knowledgeIndex ? knowledgeSection(knowledgeIndex) : undefined);
439
478
  const budgetUsd = codingBudget.budgetUsd;
440
479
  // Resolved once: before the transaction when the declaration was read from it.
441
- const branch = declarationRead?.branch ?? (await resolveCodingBranch(tx, options, agent.codingProfile));
480
+ const branch = declarationRead?.branch ?? (await resolveCodingBranch(tx, options, agent.codingProfile, agent.ownerId));
442
481
  const { baseRef, continuationOf, rootCodingRunId } = branch;
443
482
  const headRef = branch.headRef ?? `wardby/run-${run.id}`;
444
483
  const input = CodingTaskInputSchema.parse({
@@ -485,6 +524,7 @@ export async function dispatchRun(options) {
485
524
  budgetReservedUsd: budgetUsd,
486
525
  rootCodingRunId,
487
526
  workspaceDiskMb: agent.codingProfile.workspaceDiskMb,
527
+ maxTurns: agent.codingProfile.maxTurns,
488
528
  // Fixed here so a run's tracing never changes mid-run.
489
529
  debugTrace: agent.codingProfile.debugTraceUntil != null &&
490
530
  now.getTime() < agent.codingProfile.debugTraceUntil.getTime(),
@@ -510,7 +550,7 @@ export async function dispatchRun(options) {
510
550
  })
511
551
  : undefined;
512
552
  return { run, task };
513
- }, { isolationLevel: "Serializable" });
553
+ }, { isolationLevel: "Serializable", maxWait: CONTENDED_TX_MAX_WAIT_MS });
514
554
  let persisted = null;
515
555
  for (let attempt = 0; attempt < PERSIST_ATTEMPTS; attempt += 1) {
516
556
  try {
@@ -518,13 +558,15 @@ export async function dispatchRun(options) {
518
558
  break;
519
559
  }
520
560
  catch (err) {
521
- if (!isSerializationConflict(err) || attempt === PERSIST_ATTEMPTS - 1)
561
+ const retryable = isSerializationConflict(err) || isTransactionUnavailable(err);
562
+ if (!retryable || attempt === PERSIST_ATTEMPTS - 1)
522
563
  throw err;
523
564
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs(attempt)));
524
565
  }
525
566
  }
526
567
  if (!persisted)
527
568
  return null;
569
+ options.onPersisted?.(persisted.run);
528
570
  if (persisted.run.status === "refused" || persisted.run.status === "failed") {
529
571
  dispatchLog.warn({ runId: persisted.run.id, agentId: options.agentId, reason: persisted.run.error }, `coding run ${persisted.run.status} at dispatch`);
530
572
  return persisted;
@@ -17,6 +17,22 @@ export declare class NativeEngine implements Engine {
17
17
  */
18
18
  private windDown;
19
19
  private finish;
20
+ /**
21
+ * One turn's tool calls, results in call order. The calls are split into
22
+ * runs of consecutive calls that `ctx.runsConcurrently` accepts; each such
23
+ * run of two or more is one concurrent batch, and every other call
24
+ * (including a lone accepted one) runs alone. Groups run strictly in call
25
+ * order and nothing is reordered: `[a, x, b]` runs a, then x, then b.
26
+ * Without `runsConcurrently` every call runs alone, exactly as before.
27
+ *
28
+ * Durability (DBOS): a step's id is assigned when the step starts, so every
29
+ * step here is started in an order fixed by the turn's recorded tool calls
30
+ * alone, never by timing. A batch's steps are started in one synchronous
31
+ * pass in call order and awaited with allSettled, not Promise.all, so a
32
+ * rejection never leaves sibling steps running unawaited (DBOS guidance);
33
+ * once all have settled, the first rejection by call order is rethrown.
34
+ */
35
+ private runToolCalls;
20
36
  /**
21
37
  * Streams one model turn, applying the mid-stream cutoff against the
22
38
  * *cumulative* running total (this extends Phase 1's single-turn
@@ -117,13 +117,13 @@ export class NativeEngine {
117
117
  assistantMessage.toolCalls = turn.toolCalls;
118
118
  messages.push(assistantMessage);
119
119
  if (turn.toolCalls.length > 0) {
120
+ const results = await this.runToolCalls(ctx, step, turns, turn.toolCalls);
120
121
  for (const [index, toolCall] of turn.toolCalls.entries()) {
121
- const resultJson = await step(`turn:${turns}:tool:${index}`, () => ctx.runSandboxTool(toolCall.name, toolCall.argsJson));
122
122
  messages.push({
123
123
  role: "tool",
124
124
  toolCallId: toolCall.id,
125
125
  name: toolCall.name,
126
- content: wrapUntrustedToolOutput(resultJson),
126
+ content: wrapUntrustedToolOutput(results[index]),
127
127
  });
128
128
  }
129
129
  continue;
@@ -162,6 +162,53 @@ export class NativeEngine {
162
162
  finish(status, finalText, turns, usage, error) {
163
163
  return { status, finalText, turns, usage, ...(error ? { error } : {}) };
164
164
  }
165
+ /**
166
+ * One turn's tool calls, results in call order. The calls are split into
167
+ * runs of consecutive calls that `ctx.runsConcurrently` accepts; each such
168
+ * run of two or more is one concurrent batch, and every other call
169
+ * (including a lone accepted one) runs alone. Groups run strictly in call
170
+ * order and nothing is reordered: `[a, x, b]` runs a, then x, then b.
171
+ * Without `runsConcurrently` every call runs alone, exactly as before.
172
+ *
173
+ * Durability (DBOS): a step's id is assigned when the step starts, so every
174
+ * step here is started in an order fixed by the turn's recorded tool calls
175
+ * alone, never by timing. A batch's steps are started in one synchronous
176
+ * pass in call order and awaited with allSettled, not Promise.all, so a
177
+ * rejection never leaves sibling steps running unawaited (DBOS guidance);
178
+ * once all have settled, the first rejection by call order is rethrown.
179
+ */
180
+ async runToolCalls(ctx, step, turn, toolCalls) {
181
+ const runCall = (index) => step(`turn:${turn}:tool:${index}`, () => ctx.runSandboxTool(toolCalls[index].name, toolCalls[index].argsJson));
182
+ const concurrent = ctx.runsConcurrently;
183
+ const results = new Array(toolCalls.length);
184
+ let index = 0;
185
+ while (index < toolCalls.length) {
186
+ let end = index;
187
+ if (concurrent) {
188
+ while (end < toolCalls.length && concurrent(toolCalls[end].name))
189
+ end += 1;
190
+ }
191
+ if (end - index < 2) {
192
+ results[index] = await runCall(index);
193
+ index += 1;
194
+ continue;
195
+ }
196
+ const batch = Array.from({ length: end - index }, (_, offset) => index + offset);
197
+ engineLog.info({ turn, toolCallIds: batch.map((i) => toolCalls[i].id) }, "running tool calls concurrently");
198
+ const settled = await Promise.allSettled(batch.map((i) => runCall(i)));
199
+ const rejections = settled.filter((outcome) => outcome.status === "rejected");
200
+ for (const extra of rejections.slice(1)) {
201
+ engineLog.warn({ turn, err: extra.reason }, "concurrent tool call also failed");
202
+ }
203
+ if (rejections.length > 0)
204
+ throw rejections[0].reason;
205
+ settled.forEach((outcome, position) => {
206
+ results[batch[position]] = outcome.value;
207
+ });
208
+ index = end;
209
+ }
210
+ return results;
211
+ }
165
212
  /**
166
213
  * Streams one model turn, applying the mid-stream cutoff against the
167
214
  * *cumulative* running total (this extends Phase 1's single-turn
@@ -9,10 +9,11 @@ import type { PrismaClient } from "#prisma";
9
9
  import type { Executor } from "../providers/executor/types.js";
10
10
  import type { IssueTrackerRegistry } from "../providers/issue-tracker/types.js";
11
11
  import type { HostEvent, ReviewHostRegistry } from "../providers/review-host/types.js";
12
+ import { type OpenSibling } from "./related-pull-requests.js";
12
13
  import { type RepoAccessGate } from "./repo-access.js";
13
14
  import { type DriftScope } from "../knowledge/relevance.js";
14
15
  export type { HostEvent };
15
- export type HostEventDb = Pick<PrismaClient, "agentRepository" | "agent" | "run" | "runHostCheck" | "runHostStatus" | "codingRun" | "task" | "webhook" | "budgetGroup" | "issuePullRequest" | "workItem" | "runAttribution" | "agentIssueProject" | "$transaction" | "$queryRaw">;
16
+ export type HostEventDb = Pick<PrismaClient, "agentRepository" | "deferredReview" | "agent" | "run" | "runHostCheck" | "runHostStatus" | "codingRun" | "task" | "webhook" | "budgetGroup" | "issuePullRequest" | "workItem" | "runAttribution" | "agentIssueProject" | "$transaction" | "$queryRaw">;
16
17
  export interface RouteHostEventDeps {
17
18
  db: HostEventDb;
18
19
  executor: Executor;
@@ -24,6 +25,14 @@ export interface RouteHostEventDeps {
24
25
  /** Issue trackers for moving/commenting on the issue a closed PR was opened for; absent → pr_closed is ignored. */
25
26
  issueTrackers?: IssueTrackerRegistry;
26
27
  }
28
+ /** What starting a review needs: everything routing needs except the mention handle. */
29
+ export type ReviewStartDeps = Omit<RouteHostEventDeps, "mentionHandle">;
30
+ /** A deferred review whose head's CI has not finished after this long is started anyway (by the reconciler sweep). */
31
+ export declare const DEFERRED_REVIEW_MAX_WAIT_MS: number;
32
+ /** A deferred review row older than this is dropped without starting. */
33
+ export declare const DEFERRED_REVIEW_MAX_AGE_MS: number;
34
+ /** Upper bound on deferred reviews the sweep starts per pass, so a backlog drains over several passes. */
35
+ export declare const DEFERRED_REVIEW_BATCH = 50;
27
36
  export interface RouteResult {
28
37
  runIds: string[];
29
38
  /** Cosmetic work to do after the webhook response is sent (reactions, status comments). */
@@ -51,6 +60,12 @@ export declare function mergeTaskText(event: PushEvent, scope: DriftScope | null
51
60
  type MentionEvent = Extract<HostEvent, {
52
61
  kind: "mention";
53
62
  }>;
63
+ /** The exact phrase every continuation hint uses (a prompt may match it). */
64
+ export declare function continuePriorRunPhrase(runId: string): string;
65
+ /** The continuation hint a router agent follows to continue a PR's branch instead of opening a new one. */
66
+ export declare function continuationHint(prNumber: number, runId: string): string;
67
+ /** One hint per open sibling (stored rows only; links built, ids re-validated), then the shared guidance. */
68
+ export declare function siblingContinuationHints(siblings: readonly OpenSibling[]): string | undefined;
54
69
  /**
55
70
  * The mention agent's task text. Deterministic so a prompt can parse it.
56
71
  *
@@ -67,6 +82,14 @@ type MentionEvent = Extract<HostEvent, {
67
82
  * never in the system prompt. composeTaskOverride stores both in the one
68
83
  * taskOverride column.
69
84
  */
70
- export declare function mentionTaskText(event: MentionEvent): string;
85
+ export declare function mentionTaskText(event: MentionEvent, siblings?: readonly OpenSibling[]): string;
71
86
  export declare function unknownPriorRunBody(priorRunId: string): string;
87
+ /**
88
+ * The reconciler's fallback for deferred reviews: drops rows older than
89
+ * DEFERRED_REVIEW_MAX_AGE_MS, and starts (regardless of CI) those waiting
90
+ * longer than DEFERRED_REVIEW_MAX_WAIT_MS, at most DEFERRED_REVIEW_BATCH per
91
+ * pass, oldest first. Covers CI that never reported, ran very long, or whose
92
+ * completion webhook was missed. Never throws; returns the runs started.
93
+ */
94
+ export declare function startDeferredReviews(deps: ReviewStartDeps, now?: Date): Promise<string[]>;
72
95
  export declare function routeHostEvent(event: HostEvent, deps: RouteHostEventDeps): Promise<RouteResult>;