@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
@@ -110,13 +110,13 @@ export declare const NeutralAgentSchema: z.ZodObject<{
110
110
  name: string;
111
111
  model: string;
112
112
  budgetUsd: string;
113
+ maxTurns: number;
113
114
  provider: "openai" | "bedrock";
114
115
  kind: "native";
115
116
  systemPrompt: string;
116
117
  schedule: string | null;
117
118
  timezone: string;
118
119
  scheduleEnabled: boolean;
119
- maxTurns: number;
120
120
  memoryEnabled: boolean;
121
121
  region: string | null;
122
122
  ownerEmail: string | null;
@@ -129,10 +129,10 @@ export declare const NeutralAgentSchema: z.ZodObject<{
129
129
  name: string;
130
130
  model: string;
131
131
  budgetUsd: string;
132
+ maxTurns: number;
132
133
  provider: "openai" | "bedrock";
133
134
  systemPrompt: string;
134
135
  scheduleEnabled: boolean;
135
- maxTurns: number;
136
136
  kind?: "native" | undefined;
137
137
  schedule?: string | null | undefined;
138
138
  timezone?: string | undefined;
@@ -40,6 +40,7 @@ export declare function requireAgentAccess(ctx: McpRequestContext, id: string, r
40
40
  name: string;
41
41
  model: string;
42
42
  budgetUsd: import("@prisma/client-runtime-utils").Decimal;
43
+ maxTurns: number;
43
44
  createdAt: Date;
44
45
  updatedAt: Date;
45
46
  kind: import("#prisma").AgentKind;
@@ -50,7 +51,8 @@ export declare function requireAgentAccess(ctx: McpRequestContext, id: string, r
50
51
  timezone: string;
51
52
  scheduleEnabled: boolean;
52
53
  lastScheduledAt: Date | null;
53
- maxTurns: number;
54
+ maxDelegationsPerRun: number;
55
+ parallelDelegations: boolean;
54
56
  defectProjectKey: string | null;
55
57
  defectIssueType: string | null;
56
58
  budgetGroupId: string | null;
@@ -6,7 +6,7 @@
6
6
  * principal (link_host_account refuses one already linked elsewhere).
7
7
  */
8
8
  import { parseArgs } from "node:util";
9
- import { REVIEW_HOST_PROVIDERS } from "../../providers/review-host/types.js";
9
+ import { HOST_ACCOUNT_PROVIDERS } from "../../providers/review-host/types.js";
10
10
  const USAGE = "Use auth host-account list [--subject <s>] | host-account unlink --subject <s> [--provider github].";
11
11
  export async function hostAccountCommand(args, db, output = console.log) {
12
12
  const [action, ...rest] = args;
@@ -19,7 +19,7 @@ export async function hostAccountCommand(args, db, output = console.log) {
19
19
  options: { subject: { type: "string" }, provider: { type: "string" } },
20
20
  });
21
21
  const provider = values.provider ?? "github";
22
- if (!REVIEW_HOST_PROVIDERS.includes(provider))
22
+ if (!HOST_ACCOUNT_PROVIDERS.includes(provider))
23
23
  throw new Error(`Unknown provider "${provider}".`);
24
24
  if (action === "list") {
25
25
  const rows = await db.hostIdentity.findMany({
@@ -1,10 +1,3 @@
1
- /**
2
- * Set-time repository authorization for the MCP tools that give an agent
3
- * authority over a repository (link_repository; create_agent/update_agent's
4
- * codingProfile.repository). Returns the stamp to store on the granting row,
5
- * or throws an actionable McpError. See core/repo-access.ts for the use-time
6
- * side and the rules; the host call happens here, before any transaction.
7
- */
8
1
  import { type AuthorizedVia, type RepoAccessKind } from "../../core/repo-access.js";
9
2
  import type { McpRequestContext } from "../context.js";
10
3
  export interface RepositoryAuthorization {
@@ -12,6 +5,12 @@ export interface RepositoryAuthorization {
12
5
  authorizedById: string;
13
6
  authorizedAt: Date;
14
7
  }
8
+ /**
9
+ * The canonical (realpath-based) name of a `local:` repository inside the
10
+ * current trusted roots; a 400 whose message carries the error code otherwise.
11
+ * Agents store this, never the caller's spelling.
12
+ */
13
+ export declare function canonicalLocalRepository(repository: string): Promise<string>;
15
14
  export declare function authorizeRepositoryForSet(ctx: McpRequestContext, input: {
16
15
  /** The agent's owner. The caller is always the owner here (or creating the agent). */
17
16
  ownerId: string | null;
@@ -5,9 +5,25 @@
5
5
  * or throws an actionable McpError. See core/repo-access.ts for the use-time
6
6
  * side and the rules; the host call happens here, before any transaction.
7
7
  */
8
+ import { LocalRepoError, isLocalRepository, loadLocalRepoRoots, resolveLocalRepository, } from "../../coding/local-repo.js";
8
9
  import { requiredLevel } from "../../core/repo-access.js";
9
10
  import { McpError } from "../errors.js";
10
11
  import { requireScope } from "./resource-server.js";
12
+ /**
13
+ * The canonical (realpath-based) name of a `local:` repository inside the
14
+ * current trusted roots; a 400 whose message carries the error code otherwise.
15
+ * Agents store this, never the caller's spelling.
16
+ */
17
+ export async function canonicalLocalRepository(repository) {
18
+ try {
19
+ return (await resolveLocalRepository(repository, loadLocalRepoRoots(process.env).roots)).repository;
20
+ }
21
+ catch (err) {
22
+ if (err instanceof LocalRepoError)
23
+ throw new McpError(400, err.message);
24
+ throw new McpError(400, `local_repo_not_found: ${err.message}`);
25
+ }
26
+ }
11
27
  export async function authorizeRepositoryForSet(ctx, input) {
12
28
  if (!input.ownerId) {
13
29
  throw new McpError(400, "owner_required: agents without an owner can't be given a repository, because an owner-less agent has no owner " +
@@ -18,6 +34,11 @@ export async function authorizeRepositoryForSet(ctx, input) {
18
34
  authorizedById: ctx.principal.id,
19
35
  authorizedAt: new Date(),
20
36
  });
37
+ if (isLocalRepository(input.repository)) {
38
+ // Trusted roots decide, never a host identity or an admin override.
39
+ await canonicalLocalRepository(input.repository);
40
+ return stamp("local_root");
41
+ }
21
42
  if (input.adminOverride) {
22
43
  // Role-gated: the scope alone never suffices (resource-server.ts).
23
44
  requireScope(ctx, ctx.canonicalUri, "agents:admin");
package/dist/mcp/index.js CHANGED
@@ -38,6 +38,7 @@ import { ROLE_NAMES, SCOPES_SUPPORTED, authenticate } from "./auth/resource-serv
38
38
  import { canonicalUrl } from "./transport/http-limits.js";
39
39
  import { createViewerApi } from "../viewer/http.js";
40
40
  import { createViewerEventBus } from "../viewer/event-bus.js";
41
+ import { buildInfraInfo } from "../viewer/infra.js";
41
42
  import { registerAgentTools } from "./tools/agents.js";
42
43
  import { registerServiceTools } from "./tools/services.js";
43
44
  import { registerBudgetGroupTools } from "./tools/budget-groups.js";
@@ -135,7 +136,7 @@ export function buildMcpProviders() {
135
136
  const secrets = buildSecretCipher(providerConfig);
136
137
  const datastore = new PostgresDatastore(prisma, secrets);
137
138
  const memory = new PostgresAgentMemory(prisma);
138
- const reviewHosts = buildReviewHosts();
139
+ const reviewHosts = buildReviewHosts(process.env, prisma);
139
140
  const issueTrackers = buildIssueTrackers();
140
141
  const hostUserAuthorizers = buildHostUserAuthorizers();
141
142
  // One gate (and one cache) for the whole process: set-time checks in the
@@ -317,6 +318,8 @@ export async function startMcp(options = {}) {
317
318
  bus: viewerBus,
318
319
  authenticate: (authorization) => authenticate({ authorization }, { authProvider, db: prisma, providers, canonicalUri: canonicalHref }),
319
320
  canonicalUri: canonicalHref,
321
+ infra: buildInfraInfo(),
322
+ issueSites: jiraConfig ? { jira: jiraConfig.siteUrl } : {},
320
323
  })
321
324
  : undefined;
322
325
  const http = await startHttpServer({
@@ -8,6 +8,7 @@
8
8
  import { Prisma } from "#prisma";
9
9
  import { z } from "zod";
10
10
  import { CodingProfilePatchSchema, CodingProfileSchema } from "../../coding/profile.js";
11
+ import { DEFAULT_CLAUDE_MAX_TURNS, MAX_CODING_TURNS, isLocalRepository } from "../../coding/protocol.js";
11
12
  import { codingProviderSupportsModel } from "../../coding/provider.js";
12
13
  import { assertAgentModelAvailable } from "../../core/run-pricing.js";
13
14
  import { ModelUnavailableError } from "../../providers/llm/catalog-types.js";
@@ -22,7 +23,7 @@ import { EVERYONE_KEY, atLeast, canDelegate, deleteGrantsFor, effectiveAccess }
22
23
  import { projectTool } from "./tools.js";
23
24
  import { requireAnyScope, requireScope } from "../auth/resource-server.js";
24
25
  import { logger } from "../../core/logger.js";
25
- import { authorizeRepositoryForSet } from "../auth/repo-authorization.js";
26
+ import { authorizeRepositoryForSet, canonicalLocalRepository, } from "../auth/repo-authorization.js";
26
27
  import { McpError } from "../errors.js";
27
28
  import { textResult } from "./text-result.js";
28
29
  // workerImageRef is the BYO-arbitrary-image escape hatch (see
@@ -48,12 +49,19 @@ const MAX_AGENT_NAME_CHARS = 200;
48
49
  const MAX_SYSTEM_PROMPT_CHARS = 64 * 1024;
49
50
  const MAX_MODEL_CHARS = 128;
50
51
  const MAX_BUDGET_USD = 1_000_000;
52
+ /** Upper bound on Agent.maxDelegationsPerRun: a lead fanning out to one builder per repository. */
53
+ const MAX_DELEGATIONS_PER_RUN = 20;
54
+ const PARALLEL_DELEGATIONS_DESCRIPTION = "Native agents with sub-agents: when true, the delegate_to_* calls the agent makes in one model turn run at the same time " +
55
+ "instead of one after another (other tool calls still run in order). The run tree still shares one budget, so later " +
56
+ "sub-agents get what earlier ones leave. Coding sub-agents still queue for CODING_MAX_CONCURRENT slots. Default false.";
51
57
  const agentFields = {
52
58
  name: z.string().trim().min(1).max(MAX_AGENT_NAME_CHARS),
53
59
  systemPrompt: z.string().min(1).max(MAX_SYSTEM_PROMPT_CHARS),
54
60
  model: z.string().trim().min(1).max(MAX_MODEL_CHARS),
55
61
  budgetUsd: z.number().finite().positive().max(MAX_BUDGET_USD),
56
62
  maxTurns: z.number().int().min(1).max(100),
63
+ maxDelegationsPerRun: z.number().int().min(1).max(MAX_DELEGATIONS_PER_RUN),
64
+ parallelDelegations: z.boolean(),
57
65
  schedule: z.string().trim().min(1).max(256),
58
66
  timezone: z.string().trim().min(1).max(128),
59
67
  scheduleEnabled: z.boolean(),
@@ -85,6 +93,8 @@ const CreateAgentSchema = z
85
93
  model: agentFields.model,
86
94
  budgetUsd: agentFields.budgetUsd,
87
95
  maxTurns: agentFields.maxTurns.optional(),
96
+ maxDelegationsPerRun: agentFields.maxDelegationsPerRun.optional(),
97
+ parallelDelegations: agentFields.parallelDelegations.optional(),
88
98
  schedule: agentFields.schedule.optional(),
89
99
  timezone: agentFields.timezone.optional(),
90
100
  scheduleEnabled: agentFields.scheduleEnabled.optional(),
@@ -139,6 +149,8 @@ const UpdateAgentSchema = z
139
149
  model: agentFields.model.optional(),
140
150
  budgetUsd: agentFields.budgetUsd.optional(),
141
151
  maxTurns: agentFields.maxTurns.optional(),
152
+ maxDelegationsPerRun: agentFields.maxDelegationsPerRun.optional(),
153
+ parallelDelegations: agentFields.parallelDelegations.optional(),
142
154
  schedule: agentFields.schedule.nullable().optional(),
143
155
  timezone: agentFields.timezone.optional(),
144
156
  scheduleEnabled: agentFields.scheduleEnabled.optional(),
@@ -160,6 +172,13 @@ const REPOSITORY_ADMIN_OVERRIDE = {
160
172
  type: "boolean",
161
173
  description: "Admins only (agents:admin with the admin role): approve codingProfile.repository without checking GitHub access, recorded as an admin approval. On an agent the admin doesn't own, only codingProfile.repository may change.",
162
174
  };
175
+ /** A `local:` repository is stored as its canonical (realpath) name, and refused outside the trusted roots. */
176
+ async function withCanonicalRepository(args) {
177
+ const repository = args.codingProfile?.repository;
178
+ if (repository === undefined || !isLocalRepository(repository))
179
+ return args;
180
+ return { ...args, codingProfile: { ...args.codingProfile, repository: await canonicalLocalRepository(repository) } };
181
+ }
163
182
  /** The profile columns a repository authorization is stamped into. */
164
183
  function profileStamp(authorization) {
165
184
  return {
@@ -175,7 +194,7 @@ const profileJsonSchema = {
175
194
  provider: { type: "string", enum: ["codex", "claude-code"] },
176
195
  repository: {
177
196
  type: "string",
178
- description: "owner/name on GitHub. Your linked GitHub account (link_host_account) must have write access to it, unless a wardby admin approves it (repositoryAdminOverride). Re-checked on every run.",
197
+ description: "owner/name on GitHub, or local:/absolute/path for a git folder inside the server's LOCAL_REPO_ROOTS (stored by its real path; no GitHub access needed). For GitHub, your linked GitHub account (link_host_account) must have write access to it, unless a wardby admin approves it (repositoryAdminOverride). Re-checked on every run.",
179
198
  },
180
199
  baseRef: { type: "string" },
181
200
  defaultTask: { type: ["string", "null"] },
@@ -198,6 +217,12 @@ const profileJsonSchema = {
198
217
  maximum: 32768,
199
218
  description: "Workspace disk size in MiB. Null uses the deployment default (CODING_DISK_MB), capped by the operator's CODING_MAX_DISK_MB.",
200
219
  },
220
+ maxTurns: {
221
+ type: ["integer", "null"],
222
+ minimum: 1,
223
+ maximum: MAX_CODING_TURNS,
224
+ description: `Claude Code runs only: the most agent turns (model calls) a run may take before it stops with coding_turn_limit. Null uses the worker default (${DEFAULT_CLAUDE_MAX_TURNS}). The run's budget and timeout still apply. Codex runs have no turn limit.`,
225
+ },
201
226
  packageAllowlist: {
202
227
  type: "object",
203
228
  description: "Approved top-level packages per ecosystem (npm, pypi). Needs packages:approve or agents:admin.",
@@ -293,6 +318,7 @@ function storedProfile(profile) {
293
318
  toolchainVersion: profile.toolchainVersion,
294
319
  workerImageRef: profile.workerImageRef,
295
320
  workspaceDiskMb: profile.workspaceDiskMb,
321
+ maxTurns: profile.maxTurns,
296
322
  packageAllowlist: profile.packageAllowlist,
297
323
  packagePolicy: profile.packagePolicy,
298
324
  services: profile.services,
@@ -321,6 +347,14 @@ export function registerAgentTools(mcp) {
321
347
  model: { type: "string" },
322
348
  budgetUsd: { type: "number" },
323
349
  maxTurns: { type: "integer" },
350
+ maxDelegationsPerRun: {
351
+ type: "integer",
352
+ minimum: 1,
353
+ maximum: MAX_DELEGATIONS_PER_RUN,
354
+ description: "Native agents with sub-agents: how many delegate_to_* calls one run may make, each to a different sub-agent " +
355
+ "(one after another, or together with parallelDelegations). Default 1: route to exactly one.",
356
+ },
357
+ parallelDelegations: { type: "boolean", description: PARALLEL_DELEGATIONS_DESCRIPTION },
324
358
  schedule: { type: "string" },
325
359
  timezone: { type: "string" },
326
360
  scheduleEnabled: { type: "boolean" },
@@ -340,7 +374,7 @@ export function registerAgentTools(mcp) {
340
374
  required: ["name", "systemPrompt", "model", "budgetUsd"],
341
375
  },
342
376
  handler: async (rawArgs, ctx) => {
343
- const args = parseCreateAgent(rawArgs);
377
+ const args = await withCanonicalRepository(parseCreateAgent(rawArgs));
344
378
  if (args.codingProfile?.workerImageRef != null)
345
379
  requireWorkerImageRefScope(ctx);
346
380
  const packages = args.codingProfile;
@@ -396,6 +430,14 @@ export function registerAgentTools(mcp) {
396
430
  model: { type: "string" },
397
431
  budgetUsd: { type: "number" },
398
432
  maxTurns: { type: "integer" },
433
+ maxDelegationsPerRun: {
434
+ type: "integer",
435
+ minimum: 1,
436
+ maximum: MAX_DELEGATIONS_PER_RUN,
437
+ description: "Native agents with sub-agents: how many delegate_to_* calls one run may make, each to a different sub-agent " +
438
+ "(one after another, or together with parallelDelegations). Default 1: route to exactly one.",
439
+ },
440
+ parallelDelegations: { type: "boolean", description: PARALLEL_DELEGATIONS_DESCRIPTION },
399
441
  schedule: { type: ["string", "null"] },
400
442
  timezone: { type: "string" },
401
443
  scheduleEnabled: { type: "boolean" },
@@ -435,7 +477,7 @@ export function registerAgentTools(mcp) {
435
477
  required: ["id"],
436
478
  },
437
479
  handler: async (rawArgs, ctx) => {
438
- const args = parseUpdateAgent(rawArgs);
480
+ const args = await withCanonicalRepository(parseUpdateAgent(rawArgs));
439
481
  if (args.codingProfile?.workerImageRef !== undefined)
440
482
  requireWorkerImageRefScope(ctx);
441
483
  const debugTraceMinutes = args.codingProfile?.debugTraceMinutes;
@@ -6,12 +6,12 @@
6
6
  * the caller; an operator can list and unlink anyone's with
7
7
  * `wardby auth host-account`.
8
8
  */
9
- import { REVIEW_HOST_PROVIDERS } from "../../providers/review-host/types.js";
9
+ import { HOST_ACCOUNT_PROVIDERS } from "../../providers/review-host/types.js";
10
10
  import { confirmHostIdentityLink, HostLinkError, startHostIdentityLink, userCallbackPath, } from "../../core/host-identity-links.js";
11
11
  import { McpError } from "../errors.js";
12
12
  import { canonicalUrl } from "../transport/http-limits.js";
13
13
  import { textResult } from "./text-result.js";
14
- const PROVIDER_PROP = { type: "string", enum: [...REVIEW_HOST_PROVIDERS] };
14
+ const PROVIDER_PROP = { type: "string", enum: [...HOST_ACCOUNT_PROVIDERS] };
15
15
  function asMcpError(err) {
16
16
  return err instanceof HostLinkError ? new McpError(err.status, err.message) : err;
17
17
  }
@@ -12,17 +12,19 @@
12
12
  * See docs/private/2026-09-26-repo-access-authorization-spec-and-plan.md.
13
13
  */
14
14
  import { Prisma } from "#prisma";
15
- import { normalizeGitHubRepository } from "../../coding/protocol.js";
15
+ import { isLocalRepository, normalizeGitHubRepository, normalizeLocalRepository } from "../../coding/protocol.js";
16
16
  import { requireAgentAccess, requireBindingOwner } from "../auth/access.js";
17
- import { authorizeRepositoryForSet } from "../auth/repo-authorization.js";
17
+ import { authorizeRepositoryForSet, canonicalLocalRepository } from "../auth/repo-authorization.js";
18
18
  import { requireScope } from "../auth/resource-server.js";
19
19
  import { McpError } from "../errors.js";
20
20
  import { textResult } from "./text-result.js";
21
- const PROVIDERS = ["github"];
22
- const TRIGGERS = ["pull_request", "mention", "push"];
21
+ const PROVIDERS = ["github", "local"];
22
+ const TRIGGERS = ["pull_request", "mention", "push", "review_fix"];
23
23
  const CHECK_NAME = /^[A-Za-z0-9][A-Za-z0-9 ._/()-]{0,99}$/;
24
24
  function normalizeRepository(provider, repository) {
25
25
  try {
26
+ if (provider === "local")
27
+ return normalizeLocalRepository(repository);
26
28
  if (provider === "github")
27
29
  return normalizeGitHubRepository(repository);
28
30
  }
@@ -40,6 +42,9 @@ async function assertNoConflicts(tx, args) {
40
42
  if (args.triggers.includes("mention") && others.some((o) => o.triggers.includes("mention"))) {
41
43
  throw new McpError(409, `Another agent already handles @-mentions on ${args.repository}.`);
42
44
  }
45
+ if (args.triggers.includes("review_fix") && others.some((o) => o.triggers.includes("review_fix"))) {
46
+ throw new McpError(409, `Another agent already handles automatic review fixes on ${args.repository}.`);
47
+ }
43
48
  // Against every other link, whatever its triggers: a check name identifies
44
49
  // one agent's verdict on the repository (also a unique index).
45
50
  if (args.checkName !== null && others.some((o) => o.checkName === args.checkName)) {
@@ -54,11 +59,18 @@ export function registerRepositoryTools(mcp) {
54
59
  name: "link_repository",
55
60
  scope: "agents:write",
56
61
  description: "Links a native agent you own to a repository on a code-review host, with its access, event triggers, and checkName. " +
57
- "Triggers: pull_request (review each PR push), mention (answer @mentions), push (start on merges to the default branch). " +
62
+ "Triggers: pull_request (review each PR push), mention (answer @mentions), push (start on merges to the default branch), " +
63
+ "review_fix (automatic review fix rounds). The review_fix trigger makes this agent the one wardby starts, automatically " +
64
+ "and without a human comment, when wardby's own review check requests changes on a pull request a wardby coding run " +
65
+ "opened (capped by reviewFixMaxRounds, default 2). " +
58
66
  "Your linked GitHub account (link_host_account) must have write access to the repository for a write link, or read for a " +
59
67
  "read link; a wardby admin may instead approve it with adminOverride. checkName is only allowed (and required) with the " +
60
- "pull_request trigger, and is unique per repository. Re-linking an already-linked repository replaces its access, " +
61
- "triggers, and checkName (an omitted field is cleared, not kept) and re-checks access — always send the full desired state.",
68
+ "pull_request trigger, and is unique per repository. waitForCi (only with the pull_request trigger) holds this agent's " +
69
+ "review on a pushed head until that head's own CI finishes, instead of racing it; APPROVE is refused while CI on the " +
70
+ "head is failing or still running. Re-linking an already-linked repository replaces its access, triggers, checkName, " +
71
+ "reviewFixMaxRounds, and waitForCi (an omitted field is cleared, not kept) and re-checks access — always send the full " +
72
+ "desired state. A local repository (local:/abs/path, inside the server's trusted roots) takes no triggers or checkName: " +
73
+ "it supports manual reviews only (trigger_agent with review).",
62
74
  inputSchema: {
63
75
  type: "object",
64
76
  additionalProperties: false,
@@ -69,6 +81,17 @@ export function registerRepositoryTools(mcp) {
69
81
  access: { type: "string", enum: ["read", "write"] },
70
82
  triggers: { type: "array", items: { type: "string", enum: [...TRIGGERS] }, uniqueItems: true },
71
83
  checkName: { type: "string", minLength: 1, maxLength: 100 },
84
+ reviewFixMaxRounds: {
85
+ type: "integer",
86
+ minimum: 1,
87
+ maximum: 10,
88
+ description: "Only with the review_fix trigger: automatic fix rounds per pull request before wardby stops (default 2).",
89
+ },
90
+ waitForCi: {
91
+ type: "boolean",
92
+ description: "Only with the pull_request trigger: hold this agent's review on a pushed head until that head's own CI " +
93
+ "finishes, instead of racing it (default false).",
94
+ },
72
95
  adminOverride: {
73
96
  type: "boolean",
74
97
  description: "Admins only (agents:admin with the admin role): approve this link without checking GitHub access, recorded as an admin approval. Allowed on any agent that has an owner, not only the admin's own.",
@@ -94,9 +117,20 @@ export function registerRepositoryTools(mcp) {
94
117
  if (agent.kind !== "native") {
95
118
  throw new McpError(400, "Repository links are for native agents only; coding agents use codingProfile.repository.");
96
119
  }
97
- const provider = args.provider ?? "github";
98
- const repository = normalizeRepository(provider, args.repository);
120
+ const provider = args.provider ?? (isLocalRepository(args.repository) ? "local" : "github");
121
+ if ((provider === "local") !== isLocalRepository(args.repository)) {
122
+ throw new McpError(400, `invalid repository "${args.repository}" for provider ${provider}`);
123
+ }
124
+ const repository = provider === "local"
125
+ ? await canonicalLocalRepository(args.repository)
126
+ : normalizeRepository(provider, args.repository);
99
127
  const triggers = [...new Set(args.triggers ?? [])];
128
+ if (provider === "local") {
129
+ if (triggers.length > 0)
130
+ throw new McpError(400, "local repositories support manual review only");
131
+ // A check is only published for a dispatched host PR; a local link has none.
132
+ args = { ...args, checkName: undefined };
133
+ }
100
134
  if (triggers.length > 0 && args.access !== "write")
101
135
  throw new McpError(400, "Event triggers need write access.");
102
136
  const checkName = args.checkName ?? null;
@@ -108,6 +142,18 @@ export function registerRepositoryTools(mcp) {
108
142
  }
109
143
  if (checkName !== null && !CHECK_NAME.test(checkName))
110
144
  throw new McpError(400, "invalid checkName");
145
+ const reviewFixMaxRounds = args.reviewFixMaxRounds ?? null;
146
+ if (reviewFixMaxRounds !== null && !triggers.includes("review_fix")) {
147
+ throw new McpError(400, "reviewFixMaxRounds needs the review_fix trigger.");
148
+ }
149
+ if (reviewFixMaxRounds !== null &&
150
+ (!Number.isInteger(reviewFixMaxRounds) || reviewFixMaxRounds < 1 || reviewFixMaxRounds > 10)) {
151
+ throw new McpError(400, "reviewFixMaxRounds must be an integer from 1 to 10.");
152
+ }
153
+ const waitForCi = args.waitForCi ?? false;
154
+ if (waitForCi && !triggers.includes("pull_request")) {
155
+ throw new McpError(400, "waitForCi needs the pull_request trigger.");
156
+ }
111
157
  // The host call happens before, and outside, the serializable transaction.
112
158
  const authorization = await authorizeRepositoryForSet(ctx, {
113
159
  ownerId: agent.ownerId,
@@ -120,7 +166,14 @@ export function registerRepositoryTools(mcp) {
120
166
  try {
121
167
  link = await ctx.db.$transaction(async (tx) => {
122
168
  await assertNoConflicts(tx, { agentId: args.agentId, provider, repository, triggers, checkName });
123
- const fields = { access: args.access, triggers, checkName, ...authorization };
169
+ const fields = {
170
+ access: args.access,
171
+ triggers,
172
+ checkName,
173
+ reviewFixMaxRounds,
174
+ waitForCi,
175
+ ...authorization,
176
+ };
124
177
  return tx.agentRepository.upsert({
125
178
  where: { agentId_provider_repository: { agentId: args.agentId, provider, repository } },
126
179
  create: { agentId: args.agentId, provider, repository, ...fields },
@@ -153,8 +206,14 @@ export function registerRepositoryTools(mcp) {
153
206
  handler: async (args, ctx) => {
154
207
  const { agent } = await requireAgentAccess(ctx, args.agentId, "read");
155
208
  requireBindingOwner(ctx, agent);
156
- const provider = args.provider ?? "github";
157
- const repository = normalizeRepository(provider, args.repository);
209
+ const provider = args.provider ?? (isLocalRepository(args.repository) ? "local" : "github");
210
+ let repository;
211
+ if (provider === "local") {
212
+ // The roots may have narrowed since linking; a link must stay removable.
213
+ repository = await canonicalLocalRepository(args.repository).catch(() => normalizeRepository(provider, args.repository));
214
+ }
215
+ else
216
+ repository = normalizeRepository(provider, args.repository);
158
217
  const { count } = await ctx.db.agentRepository.deleteMany({
159
218
  where: { agentId: args.agentId, provider, repository },
160
219
  });
@@ -71,8 +71,14 @@ export function registerRunTools(mcp) {
71
71
  diagnosticId: true,
72
72
  debugTrace: true,
73
73
  services: true,
74
+ resultBranch: true,
75
+ baseSha: true,
74
76
  },
75
77
  });
78
+ const localPr = await ctx.db.localPullRequest.findUnique({
79
+ where: { runId: run.id },
80
+ include: { reviews: { orderBy: { createdAt: "asc" } } },
81
+ });
76
82
  const codingResult = publicCodingRunResult(codingRun?.result);
77
83
  // A pending coding run with queuedAt is waiting for a concurrency slot
78
84
  // (CODING_MAX_CONCURRENT), not stuck.
@@ -108,6 +114,23 @@ export function registerRunTools(mcp) {
108
114
  // An admin turned on the debug trace: the worker's full trace is in its pod log.
109
115
  ...(codingRun?.debugTrace ? { debugTrace: true } : {}),
110
116
  ...(services.length > 0 ? { services } : {}),
117
+ ...(codingRun?.resultBranch ? { resultBranch: codingRun.resultBranch } : {}),
118
+ ...(codingRun?.baseSha ? { baseSha: codingRun.baseSha } : {}),
119
+ ...(localPr
120
+ ? {
121
+ review: {
122
+ number: localPr.number,
123
+ branch: localPr.branch,
124
+ base: localPr.base,
125
+ reviews: localPr.reviews.map((r) => ({
126
+ verdict: r.verdict,
127
+ summary: r.summary,
128
+ body: r.body,
129
+ comments: r.comments,
130
+ })),
131
+ },
132
+ }
133
+ : {}),
111
134
  ...(codingRun ? { packages, packageRefusals, packagePlan } : {}),
112
135
  });
113
136
  },