@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
@@ -0,0 +1,66 @@
1
+ import type { CodingProvider } from "../coding/provider.js";
2
+ import type { DeclaredService } from "../coding/services/declaration.js";
3
+ import { type CodingSeedInput, type CodingSeedResult } from "./coding-seed.js";
4
+ import { type QuickstartPaths, type QuickstartState } from "./config.js";
5
+ import { type StarterService } from "./starter-services.js";
6
+ export declare const CODING_PROXY_CONTAINER = "wardby-coding-proxy";
7
+ export declare const CODING_COMPOSE_FILE = "deploy/local/docker-compose.quickstart-coding.yml";
8
+ export interface CommandResult {
9
+ status: number;
10
+ stdout: string;
11
+ stderr: string;
12
+ }
13
+ export type RunCommand = (command: string, args: string[], options?: {
14
+ cwd?: string;
15
+ env?: NodeJS.ProcessEnv;
16
+ inherit?: boolean;
17
+ }) => CommandResult;
18
+ export interface CodingPrompts {
19
+ line(question: string, fallback?: string): Promise<string>;
20
+ yesNo(question: string, defaultYes: boolean): Promise<boolean>;
21
+ secret(question: string): Promise<string>;
22
+ }
23
+ export interface CodingDeps {
24
+ run: RunCommand;
25
+ prompts: CodingPrompts;
26
+ log: (line: string) => void;
27
+ cwd: string;
28
+ /** The process environment: provider keys and image overrides. */
29
+ env: NodeJS.ProcessEnv;
30
+ packageRoot: string;
31
+ seed: (input: Omit<CodingSeedInput, "ownerId">) => Promise<CodingSeedResult>;
32
+ /** The catalog image for each declared service; `image` is undefined when the catalog has no such entry. */
33
+ catalogImages: (services: DeclaredService[]) => Promise<Array<{
34
+ service: DeclaredService;
35
+ image?: string;
36
+ }>>;
37
+ }
38
+ export interface CodingStepOptions {
39
+ nonInteractive: boolean;
40
+ /** true = --coding, false = --no-coding, undefined = ask (interactive) or skip (non-interactive). */
41
+ coding?: boolean;
42
+ trust: string[];
43
+ provider?: CodingProvider;
44
+ /** --starter-services; undefined = ask (interactive) or none (non-interactive). */
45
+ starterServices?: StarterService[];
46
+ }
47
+ export interface CodingStepResult {
48
+ roots: string[];
49
+ provider: CodingProvider;
50
+ repository?: string;
51
+ seed?: CodingSeedResult;
52
+ }
53
+ /** The top level of the git work tree containing `cwd`, realpath'd; null outside one. */
54
+ export declare function defaultTrustedFolder(cwd: string): string | null;
55
+ export declare function codingEnvUpdates(input: {
56
+ roots: string[];
57
+ provider: string;
58
+ /** The Codex worker; absent for claude-code, which doesn't need one. */
59
+ workerImage?: string;
60
+ runtimeImage: string;
61
+ claudeWorkerImage?: string;
62
+ claudeToolRunnerImage?: string;
63
+ }): Record<string, string>;
64
+ /** The compose files for a quickstart project; the coding file only once its runtime image is configured. */
65
+ export declare function quickstartComposeFiles(packageRoot: string, env: Record<string, string | undefined>): string[];
66
+ export declare function codingStep(paths: QuickstartPaths, state: QuickstartState, opts: CodingStepOptions, deps: CodingDeps): Promise<CodingStepResult | null>;
@@ -0,0 +1,430 @@
1
+ /**
2
+ * The quickstart's optional coding step: trust local folders, start the coding
3
+ * proxy, and seed a coding agent (`local-builder`) and a review agent
4
+ * (`local-reviewer`) against a local git repository — no GitHub App needed.
5
+ *
6
+ * Every Docker/Compose call, prompt, and database write goes through
7
+ * CodingDeps so tests never touch the Docker daemon. The only write into the
8
+ * user's repository is the opt-in starter `.wardby/services.yaml`, made with
9
+ * git plumbing onto its own branch (starter-services.ts).
10
+ */
11
+ import { spawnSync } from "node:child_process";
12
+ import { existsSync, readdirSync, realpathSync, statSync } from "node:fs";
13
+ import { delimiter, join, resolve, sep } from "node:path";
14
+ import { dockerCodingPreflight } from "../coding/docker-preflight.js";
15
+ import { cleanGitEnv } from "../coding/local-git.js";
16
+ import { LOCAL_REPO_PREFIX, loadLocalRepoRoots, resolveLocalRepository } from "../coding/local-repo.js";
17
+ import { BUILDER_AGENT, REVIEWER_AGENT } from "./coding-seed.js";
18
+ import { defaultModel, readQuickstartEnv, writeQuickstartEnv, } from "./config.js";
19
+ import { CodingSkip, prepareImages } from "./coding-images.js";
20
+ import { STARTER_SERVICES_BRANCH, commitStarterServices, inspectDeclaredServices, repoDefaultBranch, starterDeclaredServices, starterServicesYaml, } from "./starter-services.js";
21
+ export const CODING_PROXY_CONTAINER = "wardby-coding-proxy";
22
+ export const CODING_COMPOSE_FILE = "deploy/local/docker-compose.quickstart-coding.yml";
23
+ const BASE_COMPOSE_FILE = "deploy/local/docker-compose.yml";
24
+ const PROVIDER_KEYS = {
25
+ codex: "OPENAI_API_KEY",
26
+ "claude-code": "ANTHROPIC_API_KEY",
27
+ };
28
+ const PROVIDER_LABELS = { codex: "Codex", "claude-code": "Claude Code" };
29
+ /** The top level of the git work tree containing `cwd`, realpath'd; null outside one. */
30
+ export function defaultTrustedFolder(cwd) {
31
+ const result = spawnSync("git", ["-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "-C", cwd, "rev-parse", "--show-toplevel"], { encoding: "utf8", env: cleanGitEnv() });
32
+ const top = result.status === 0 ? result.stdout.trim() : "";
33
+ if (!top)
34
+ return null;
35
+ try {
36
+ return realpathSync(top);
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ export function codingEnvUpdates(input) {
43
+ const updates = {
44
+ LOCAL_REPO_ROOTS: input.roots.join(delimiter),
45
+ JOB_LAUNCHER: "docker",
46
+ WARDBY_RUNTIME_IMAGE: input.runtimeImage,
47
+ CODING_PROXY_CONTAINER,
48
+ };
49
+ if (input.provider !== "claude-code" && input.workerImage)
50
+ updates.CODING_WORKER_IMAGE = input.workerImage;
51
+ if (input.provider === "claude-code" && input.claudeWorkerImage && input.claudeToolRunnerImage) {
52
+ updates.CODING_CLAUDE_WORKER_IMAGE = input.claudeWorkerImage;
53
+ updates.CODING_CLAUDE_TOOL_RUNNER_IMAGE = input.claudeToolRunnerImage;
54
+ }
55
+ return updates;
56
+ }
57
+ /** The compose files for a quickstart project; the coding file only once its runtime image is configured. */
58
+ export function quickstartComposeFiles(packageRoot, env) {
59
+ const files = [join(packageRoot, BASE_COMPOSE_FILE)];
60
+ if (env.WARDBY_RUNTIME_IMAGE)
61
+ files.push(join(packageRoot, CODING_COMPOSE_FILE));
62
+ return files;
63
+ }
64
+ function realFolder(path) {
65
+ let real;
66
+ try {
67
+ real = realpathSync(resolve(path));
68
+ }
69
+ catch {
70
+ throw new CodingSkip(`${path} does not exist`);
71
+ }
72
+ if (!statSync(real).isDirectory())
73
+ throw new CodingSkip(`${path} is not a folder`);
74
+ return real;
75
+ }
76
+ async function isRepositoryTop(path, roots) {
77
+ try {
78
+ await resolveLocalRepository(`${LOCAL_REPO_PREFIX}${path}`, roots);
79
+ return true;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
85
+ async function chooseRoots(opts, existing, deps) {
86
+ const roots = [...existing];
87
+ const add = (path) => {
88
+ const real = realFolder(path);
89
+ if (!roots.includes(real))
90
+ roots.push(real);
91
+ };
92
+ for (const path of opts.trust)
93
+ add(path);
94
+ if (opts.nonInteractive) {
95
+ if (roots.length === 0)
96
+ throw new CodingSkip("pass --trust <dir> to choose the folders coding agents may use");
97
+ return roots;
98
+ }
99
+ if (existing.length > 0) {
100
+ deps.log("Trusted folders:");
101
+ existing.forEach((root, index) => deps.log(` [${index + 1}] ${root}`));
102
+ const remove = await deps.prompts.line("Remove any? Numbers separated by commas (blank = keep all): ", "");
103
+ const drop = new Set(remove.split(",").map((part) => Number(part.trim()) - 1));
104
+ for (const index of [...drop].sort((a, b) => b - a)) {
105
+ if (Number.isInteger(index) && index >= 0 && index < existing.length)
106
+ roots.splice(roots.indexOf(existing[index]), 1);
107
+ }
108
+ }
109
+ const suggested = defaultTrustedFolder(deps.cwd);
110
+ if (suggested && !roots.includes(suggested) && (await deps.prompts.yesNo(`Trust ${suggested}?`, true))) {
111
+ roots.push(suggested);
112
+ }
113
+ for (;;) {
114
+ const answer = await deps.prompts.line("Add another trusted folder (blank = done): ", "");
115
+ if (!answer)
116
+ break;
117
+ try {
118
+ add(answer);
119
+ }
120
+ catch (error) {
121
+ deps.log(`! ${error.message}`);
122
+ }
123
+ }
124
+ if (roots.length === 0)
125
+ throw new CodingSkip("no folder was trusted");
126
+ return roots;
127
+ }
128
+ async function chooseProvider(opts, config, deps) {
129
+ const hasKey = (provider) => Boolean(deps.env[PROVIDER_KEYS[provider]] || config[PROVIDER_KEYS[provider]]);
130
+ let provider = opts.provider;
131
+ if (!provider) {
132
+ const available = ["codex", "claude-code"].filter(hasKey);
133
+ if (available.length === 1 || (opts.nonInteractive && available.length > 0))
134
+ provider = available[0];
135
+ else if (opts.nonInteractive)
136
+ throw new CodingSkip("set OPENAI_API_KEY (Codex) or ANTHROPIC_API_KEY (Claude Code)");
137
+ else {
138
+ const answer = (await deps.prompts.line("Coding agent: [1] Codex, [2] Claude Code [1] ", "1")).toLowerCase();
139
+ if (answer === "1" || answer === "codex")
140
+ provider = "codex";
141
+ else if (answer === "2" || answer === "claude-code" || answer === "claude")
142
+ provider = "claude-code";
143
+ else
144
+ throw new CodingSkip(`unknown coding agent "${answer}"`);
145
+ }
146
+ }
147
+ const keyName = PROVIDER_KEYS[provider];
148
+ if (!hasKey(provider)) {
149
+ if (opts.nonInteractive)
150
+ throw new CodingSkip(`${keyName} is required for ${PROVIDER_LABELS[provider]}`);
151
+ const key = await deps.prompts.secret(`${keyName}: `);
152
+ if (!key)
153
+ throw new CodingSkip(`${keyName} cannot be empty`);
154
+ config[keyName] = key;
155
+ }
156
+ else if (!config[keyName] && deps.env[keyName]) {
157
+ // The coding proxy container reads the key from the quickstart env file.
158
+ config[keyName] = deps.env[keyName];
159
+ }
160
+ return provider;
161
+ }
162
+ function startProxy(paths, state, deps) {
163
+ const env = readQuickstartEnv(paths);
164
+ const files = quickstartComposeFiles(deps.packageRoot, env).flatMap((file) => ["--file", file]);
165
+ const args = ["compose", "--project-name", state.composeProject, "--env-file", paths.envFile, ...files];
166
+ const result = deps.run("docker", [...args, "up", "--detach", "--wait", "--wait-timeout", "90", "coding-proxy"], {
167
+ cwd: paths.projectDir,
168
+ env: { ...deps.env, ...env, WARDBY_PROJECT_DIR: paths.projectDir },
169
+ });
170
+ if (result.status !== 0) {
171
+ throw new CodingSkip(`the coding proxy did not start: ${(result.stderr || result.stdout).trim().split("\n")[0]}`);
172
+ }
173
+ }
174
+ /** Git repositories among the trusted folders: a folder that is one, else its direct (non-hidden) children. */
175
+ async function findRepositories(scan, roots) {
176
+ const found = [];
177
+ const add = (path) => {
178
+ if (!found.includes(path))
179
+ found.push(path);
180
+ };
181
+ for (const root of scan) {
182
+ if (await isRepositoryTop(root, roots)) {
183
+ add(root);
184
+ continue;
185
+ }
186
+ let names;
187
+ try {
188
+ names = readdirSync(root, { withFileTypes: true })
189
+ .filter((entry) => !entry.name.startsWith(".") && (entry.isDirectory() || entry.isSymbolicLink()))
190
+ .map((entry) => entry.name)
191
+ .sort();
192
+ }
193
+ catch {
194
+ continue;
195
+ }
196
+ for (const name of names) {
197
+ let real;
198
+ try {
199
+ real = realpathSync(join(root, name));
200
+ if (!statSync(real).isDirectory())
201
+ continue;
202
+ }
203
+ catch {
204
+ continue;
205
+ }
206
+ // A symlink out of the trusted folder is not a candidate; resolveLocalRepository checks the roots.
207
+ if (real !== root && !real.startsWith(root.endsWith(sep) ? root : root + sep))
208
+ continue;
209
+ // Cheap pre-check (a .git directory or file) before spawning git.
210
+ if (!existsSync(join(real, ".git")))
211
+ continue;
212
+ if (await isRepositoryTop(real, roots))
213
+ add(real);
214
+ }
215
+ }
216
+ return found;
217
+ }
218
+ async function chooseRepository(roots, opts, deps) {
219
+ // Folders passed with --trust on this run take precedence over the saved ones.
220
+ const thisRun = [];
221
+ for (const path of opts.trust) {
222
+ try {
223
+ thisRun.push(realpathSync(resolve(path)));
224
+ }
225
+ catch {
226
+ // chooseRoots already rejected a missing folder.
227
+ }
228
+ }
229
+ let found = await findRepositories(thisRun, roots);
230
+ if (found.length === 0)
231
+ found = await findRepositories(roots, roots);
232
+ if (found.length === 1)
233
+ return found[0];
234
+ if (found.length > 1) {
235
+ if (opts.nonInteractive) {
236
+ deps.log(`Found ${found.length} git repositories in the trusted folders; using ${found[0]}:`);
237
+ for (const repo of found)
238
+ deps.log(` ${repo}`);
239
+ deps.log(" To use another, re-run quickstart with --trust <repo>.");
240
+ return found[0];
241
+ }
242
+ deps.log("Git repositories found in the trusted folders:");
243
+ found.forEach((repo, index) => deps.log(` [${index + 1}] ${repo}`));
244
+ for (;;) {
245
+ const answer = await deps.prompts.line(`Which repository should the agents use? [1-${found.length}] `, "1");
246
+ const index = Number(answer.trim()) - 1;
247
+ if (Number.isInteger(index) && index >= 0 && index < found.length)
248
+ return found[index];
249
+ deps.log(`! Enter a number from 1 to ${found.length}.`);
250
+ }
251
+ }
252
+ if (opts.nonInteractive)
253
+ return null;
254
+ for (;;) {
255
+ const answer = await deps.prompts.line("Git repository for the agents (inside a trusted folder; blank = skip): ", "");
256
+ if (!answer)
257
+ return null;
258
+ try {
259
+ return (await resolveLocalRepository(`${LOCAL_REPO_PREFIX}${resolve(answer)}`, roots)).path;
260
+ }
261
+ catch (error) {
262
+ deps.log(`! ${error.message}`);
263
+ }
264
+ }
265
+ }
266
+ async function chooseStarter(branch, opts, deps) {
267
+ if (opts.starterServices !== undefined)
268
+ return opts.starterServices;
269
+ if (opts.nonInteractive)
270
+ return [];
271
+ const answer = (await deps.prompts.line(`No .wardby/services.yaml on ${branch}. Create a starter on a new branch ${STARTER_SERVICES_BRANCH}? [p]ostgres, [r]edis, [b]oth, [n]one [n] `, "n")).toLowerCase();
272
+ if (answer === "p" || answer === "postgres")
273
+ return ["postgres"];
274
+ if (answer === "r" || answer === "redis")
275
+ return ["redis"];
276
+ if (answer === "b" || answer === "both")
277
+ return ["postgres", "redis"];
278
+ return [];
279
+ }
280
+ async function pullServiceImages(services, deps) {
281
+ if (services.length === 0)
282
+ return;
283
+ for (const { service, image } of await deps.catalogImages(services)) {
284
+ const label = `${service.name} ${service.version}`;
285
+ if (!image)
286
+ deps.log(`! ${label} is not in the service catalog; runs that declare it will be refused.`);
287
+ else if (deps.run("docker", ["pull", image], { inherit: true }).status !== 0) {
288
+ deps.log(`! Could not pull the ${label} image (${image}); a run will pull it when it starts.`);
289
+ }
290
+ }
291
+ }
292
+ /** Reads (or offers to create) the repository's services.yaml; returns the service names the builder may start. */
293
+ async function prepareServices(repo, roots, branch, baseSha, opts, deps) {
294
+ // Read at the exact commit a starter would be built on, so the two cannot disagree.
295
+ const declared = await inspectDeclaredServices(repo, roots, baseSha);
296
+ if (declared.kind === "invalid") {
297
+ deps.log(`! .wardby/services.yaml on ${branch} is invalid: ${declared.reason}`);
298
+ deps.log(" Coding runs on that branch will be refused until it is fixed (see docs/coding-services.md).");
299
+ return [];
300
+ }
301
+ if (declared.kind === "valid") {
302
+ const list = declared.services.map((service) => `${service.name} ${service.version}`).join(", ");
303
+ deps.log(`✓ .wardby/services.yaml on ${branch} declares: ${list || "no services"}`);
304
+ await pullServiceImages(declared.services, deps);
305
+ return declared.services.map((service) => service.name);
306
+ }
307
+ const choices = await chooseStarter(branch, opts, deps);
308
+ if (choices.length === 0) {
309
+ deps.log(`No .wardby/services.yaml on ${branch}; add one to start databases next to runs (docs/coding-services.md).`);
310
+ return [];
311
+ }
312
+ const result = await commitStarterServices({
313
+ dir: repo,
314
+ baseSha,
315
+ content: starterServicesYaml(choices),
316
+ confirmReplace: async (existing) => {
317
+ if (opts.nonInteractive)
318
+ return false;
319
+ return await deps.prompts.yesNo(`Branch ${STARTER_SERVICES_BRANCH} already exists (at ${existing.slice(0, 12)}). Replace it?`, false);
320
+ },
321
+ });
322
+ if (result.status === "refused") {
323
+ deps.log(`! Did not write the starter file: ${result.reason}`);
324
+ return [];
325
+ }
326
+ if (result.status === "kept") {
327
+ deps.log(`! Branch ${STARTER_SERVICES_BRANCH} already exists; left it unchanged.`);
328
+ }
329
+ else {
330
+ deps.log(`✓ Committed a starter .wardby/services.yaml on branch ${STARTER_SERVICES_BRANCH} (your checkout is unchanged).`);
331
+ }
332
+ deps.log(` Merge ${STARTER_SERVICES_BRANCH} into ${branch}, or point ${BUILDER_AGENT} at it:`);
333
+ deps.log(` update_agent with codingProfile.baseRef = "${STARTER_SERVICES_BRANCH}"`);
334
+ const services = starterDeclaredServices(choices);
335
+ await pullServiceImages(services, deps);
336
+ return services.map((service) => service.name);
337
+ }
338
+ function seedLines(seed, repository) {
339
+ const lines = [];
340
+ for (const [name, outcome] of [
341
+ [BUILDER_AGENT, seed.builder],
342
+ [REVIEWER_AGENT, seed.reviewer],
343
+ ]) {
344
+ if (outcome.status === "skipped")
345
+ lines.push(`! ${name}: ${outcome.reason}`);
346
+ else
347
+ lines.push(`✓ ${name} ${outcome.status} for ${repository}`);
348
+ }
349
+ if (seed.builder.status !== "skipped" && seed.reviewer.status !== "skipped") {
350
+ lines.push("", "Try them from your MCP client (or ask your assistant to make these calls):", ` trigger_agent {"agentId": "${seed.builder.id}", "task": "Add a short CONTRIBUTING.md"}`, " The run pushes a branch wardby/run-<run id> into your repository (get_run shows it). Review it with:", ` trigger_agent {"agentId": "${seed.reviewer.id}", "review": {"branch": "wardby/run-<run id>"}}`);
351
+ }
352
+ return lines;
353
+ }
354
+ export async function codingStep(paths, state, opts, deps) {
355
+ if (opts.coding === false)
356
+ return null;
357
+ if (opts.coding === undefined) {
358
+ if (opts.nonInteractive)
359
+ return null;
360
+ if (!(await deps.prompts.yesNo("Set up coding + review agents against a local git repo?", false)))
361
+ return null;
362
+ }
363
+ try {
364
+ const config = readQuickstartEnv(paths);
365
+ const existing = loadLocalRepoRoots({ LOCAL_REPO_ROOTS: config.LOCAL_REPO_ROOTS });
366
+ for (const missing of existing.missing)
367
+ deps.log(`! Dropping trusted folder ${missing}: it no longer exists.`);
368
+ const roots = await chooseRoots(opts, existing.roots, deps);
369
+ const provider = await chooseProvider(opts, config, deps);
370
+ const images = prepareImages(provider, config, deps);
371
+ const next = {
372
+ ...config,
373
+ ...codingEnvUpdates({
374
+ roots,
375
+ provider,
376
+ workerImage: images.worker,
377
+ runtimeImage: images.runtime,
378
+ claudeWorkerImage: images.claudeWorker,
379
+ claudeToolRunnerImage: images.claudeToolRunner,
380
+ }),
381
+ };
382
+ // An earlier quickstart run, or a manual setting, may have pointed CODING_WORKER_IMAGE at the Claude
383
+ // worker; that would start the wrong driver for a Codex run. A real Codex worker from a Codex run stays.
384
+ const staleClaude = [config.CODING_CLAUDE_WORKER_IMAGE, images.claudeWorker].filter(Boolean);
385
+ if (provider === "claude-code" && staleClaude.includes(next.CODING_WORKER_IMAGE))
386
+ delete next.CODING_WORKER_IMAGE;
387
+ writeQuickstartEnv(paths, next);
388
+ deps.log(`✓ Trusted folders: ${roots.join(", ")}`);
389
+ startProxy(paths, state, deps);
390
+ deps.log(`✓ Coding proxy is running (${CODING_PROXY_CONTAINER})`);
391
+ const preflight = await dockerCodingPreflight({
392
+ workerImage: images.worker,
393
+ claudeWorkerImage: images.claudeWorker,
394
+ claudeToolRunnerImage: images.claudeToolRunner,
395
+ proxyContainer: CODING_PROXY_CONTAINER,
396
+ }, async (image) => deps.run("docker", ["image", "inspect", image]).status === 0);
397
+ if (preflight)
398
+ throw new CodingSkip(`coding preflight failed: ${preflight}`);
399
+ deps.log("✓ Coding preflight passed");
400
+ const repo = await chooseRepository(roots, opts, deps);
401
+ if (!repo) {
402
+ deps.log("No git repository was found in or directly under the trusted folders; the agents were not created.");
403
+ return { roots, provider };
404
+ }
405
+ const base = await repoDefaultBranch(repo);
406
+ if (!base) {
407
+ deps.log(`! ${repo} has no checked-out branch with commits; the agents were not created.`);
408
+ return { roots, provider };
409
+ }
410
+ const services = await prepareServices(repo, roots, base.branch, base.sha, opts, deps);
411
+ const repository = `${LOCAL_REPO_PREFIX}${repo}`;
412
+ const seed = await deps.seed({
413
+ provider,
414
+ builderModel: defaultModel(provider === "codex" ? "openai" : "anthropic"),
415
+ reviewerModel: state.model,
416
+ repository,
417
+ baseRef: base.branch,
418
+ services,
419
+ });
420
+ for (const line of seedLines(seed, repository))
421
+ deps.log(line);
422
+ return { roots, provider, repository, seed };
423
+ }
424
+ catch (error) {
425
+ if (!(error instanceof CodingSkip))
426
+ throw error;
427
+ deps.log(`! Skipped the coding step: ${error.message}.`);
428
+ return null;
429
+ }
430
+ }
@@ -0,0 +1,19 @@
1
+ export interface QuickstartImages {
2
+ runtime: string;
3
+ worker: string;
4
+ /** Claude Code's worker and tool runner; absent when this source does not provide them. */
5
+ claudeWorker?: string;
6
+ claudeToolRunner?: string;
7
+ source: "env" | "package" | "build";
8
+ }
9
+ /**
10
+ * Where the coding step gets its runtime and worker images: explicit env
11
+ * overrides, the digest-pinned refs written at release time, or a local build
12
+ * from a source checkout.
13
+ */
14
+ export declare function resolveQuickstartImages(opts: {
15
+ env: NodeJS.ProcessEnv;
16
+ packageRoot: string;
17
+ }): QuickstartImages | {
18
+ unavailable: string;
19
+ };
@@ -0,0 +1,63 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { imageVariable } from "../config/providers.js";
4
+ const DIGEST_REF = /@sha256:[0-9a-f]{64}$/;
5
+ function readPackageImages(packageRoot) {
6
+ try {
7
+ const parsed = JSON.parse(readFileSync(join(packageRoot, "dist", "quickstart-images.json"), "utf8"));
8
+ if (typeof parsed !== "object" || parsed === null)
9
+ return undefined;
10
+ const { runtime, worker, claudeWorker, claudeToolRunner } = parsed;
11
+ if (typeof runtime !== "string" || typeof worker !== "string")
12
+ return undefined;
13
+ // An image is never resolved by tag: a tag can be repointed after release.
14
+ if (!DIGEST_REF.test(runtime) || !DIGEST_REF.test(worker))
15
+ return undefined;
16
+ // The Claude Code images are optional, but both or neither, and pinned when present.
17
+ if (claudeWorker === undefined && claudeToolRunner === undefined)
18
+ return { runtime, worker };
19
+ if (typeof claudeWorker !== "string" || typeof claudeToolRunner !== "string")
20
+ return undefined;
21
+ if (!DIGEST_REF.test(claudeWorker) || !DIGEST_REF.test(claudeToolRunner))
22
+ return undefined;
23
+ return { runtime, worker, claudeWorker, claudeToolRunner };
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ /**
30
+ * Where the coding step gets its runtime and worker images: explicit env
31
+ * overrides, the digest-pinned refs written at release time, or a local build
32
+ * from a source checkout.
33
+ */
34
+ export function resolveQuickstartImages(opts) {
35
+ const { env, packageRoot } = opts;
36
+ // The Claude pair resolves on its own: both env vars together beat the package pair, and one
37
+ // image is never taken from the environment and the other from the package.
38
+ // Blank is unset, as the server reads it (imageVariable).
39
+ const runtimeImage = imageVariable(env.WARDBY_RUNTIME_IMAGE);
40
+ const workerImage = imageVariable(env.CODING_WORKER_IMAGE);
41
+ const claudeWorker = imageVariable(env.CODING_CLAUDE_WORKER_IMAGE);
42
+ const claudeToolRunner = imageVariable(env.CODING_CLAUDE_TOOL_RUNNER_IMAGE);
43
+ const envClaude = claudeWorker && claudeToolRunner ? { claudeWorker, claudeToolRunner } : {};
44
+ if (runtimeImage && workerImage) {
45
+ return { runtime: runtimeImage, worker: workerImage, ...envClaude, source: "env" };
46
+ }
47
+ const pinned = readPackageImages(packageRoot);
48
+ if (pinned)
49
+ return { ...pinned, ...envClaude, source: "package" };
50
+ if (existsSync(join(packageRoot, "deploy", "Dockerfile")) &&
51
+ existsSync(join(packageRoot, "src", "coding-worker", "Dockerfile"))) {
52
+ return {
53
+ runtime: "wardby-runtime:local",
54
+ worker: "wardby-coding-worker:local",
55
+ claudeWorker: "wardby-claude-coding-worker:local",
56
+ claudeToolRunner: "wardby-claude-tool-runner:local",
57
+ source: "build",
58
+ };
59
+ }
60
+ return {
61
+ unavailable: "This version of @wardby/cli was published without coding images. Upgrade @wardby/cli, or run quickstart from a clone of the wardby repository.",
62
+ };
63
+ }