ai-foreman 1.3.0 → 1.4.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.
@@ -1,11 +1,15 @@
1
1
  import { Foreman, MARKER_SPEC } from "../foreman.js";
2
2
  import { fireNotification } from "../notify.js";
3
3
  import { cmdBlock, cmdComplete, cmdUpdate } from "../tickets/commands.js";
4
- import { commitAll, createTicketWorktree, currentWorktreeBranch, ensureCleanBaseWorktree, ensureForemanExcluded, hasTrackerChanges, hasWorktreeChanges, headCommitIfAhead, removeTicketWorktree, } from "./git.js";
5
- import { createOrReusePr, pushBranchForPr } from "./github.js";
4
+ import { commitAll, createTicketWorktree, currentWorktreeBranch, deleteLocalBranch, ensureCleanBaseWorktree, ensureForemanExcluded, hasTrackerChanges, hasWorktreeChanges, headCommitIfAhead, removeTicketWorktree, squashMergeBranchToLocalBase, } from "./git.js";
5
+ import { checkGitHubPrMerged, createOrReusePr, enableGitHubAutoMerge, pushBranchForPr } from "./github.js";
6
+ import { checkGitLabMrMerged, createOrReuseMr, enableGitLabAutoMerge, pushBranchForMr } from "./gitlab.js";
6
7
  export async function runBranchPlan(opts) {
7
8
  ensureCleanBaseWorktree(opts.projectDir, { allowedDirtyPaths: opts.allowedBaseDirtyPaths });
8
9
  ensureForemanExcluded(opts.projectDir);
10
+ const completionMode = opts.completionMode ?? (opts.createPr ? "pr" : "none");
11
+ const reviewProvider = opts.reviewProvider ?? "github";
12
+ const createsReview = completionMode === "pr" || completionMode === "auto-merge";
9
13
  const summaries = [];
10
14
  const successfulBranches = new Set();
11
15
  const pushedBranches = new Set();
@@ -38,7 +42,7 @@ export async function runBranchPlan(opts) {
38
42
  notifyIssue(opts.notificationsEnabled, detail);
39
43
  continue;
40
44
  }
41
- if (opts.createPr) {
45
+ if (createsReview) {
42
46
  const missingPush = node.dependencies.find((dep) => {
43
47
  const depNode = opts.plan.nodes.find((candidate) => candidate.ticket.id === dep);
44
48
  return depNode && !pushedBranches.has(depNode.branch);
@@ -56,6 +60,23 @@ export async function runBranchPlan(opts) {
56
60
  continue;
57
61
  }
58
62
  }
63
+ if (completionMode === "auto-merge" && node.dependencies.length > 0) {
64
+ const mergeReadiness = await waitForAutoMergeDependencies(opts, node, reviewProvider);
65
+ if (!mergeReadiness.ok) {
66
+ summaries.push(summaryFor(node, "skipped", mergeReadiness.message, "skipped"));
67
+ opts.log.write("branch-issue", {
68
+ ticket: node.ticket.id,
69
+ code: mergeReadiness.code,
70
+ message: mergeReadiness.message,
71
+ blocking: false,
72
+ repairCommands: mergeReadiness.repairCommands,
73
+ command: mergeReadiness.command,
74
+ output: mergeReadiness.output,
75
+ });
76
+ notifyIssue(opts.notificationsEnabled, `${node.ticket.id}: ${mergeReadiness.message}`);
77
+ continue;
78
+ }
79
+ }
59
80
  opts.log.write("branch-start", {
60
81
  ticket: node.ticket.id,
61
82
  branch: node.branch,
@@ -100,7 +121,9 @@ export async function runBranchPlan(opts) {
100
121
  effort: opts.effort,
101
122
  fast: opts.fast,
102
123
  qaEnabled: opts.qaEnabled,
103
- createPr: opts.createPr,
124
+ createPr: createsReview,
125
+ completionMode,
126
+ reviewProvider,
104
127
  prReady: opts.prReady,
105
128
  keepWorktrees: opts.keepWorktrees,
106
129
  });
@@ -157,13 +180,15 @@ export async function runBranchPlan(opts) {
157
180
  if (hasWorktreeChanges(worktreePath)) {
158
181
  commit = commitAll(worktreePath, `${node.ticket.id}: ${node.ticket.title}`);
159
182
  }
160
- if (!commit && opts.createPr) {
183
+ if (!commit && (createsReview || completionMode === "direct-merge")) {
161
184
  commit = headCommitIfAhead(worktreePath, node.baseBranch);
162
185
  }
163
186
  const summary = summaryFor(node, "done", commit ? undefined : "no_changes");
164
187
  summary.commit = commit;
165
- if (commit && opts.createPr) {
166
- const push = pushBranchForPr(worktreePath, node.branch);
188
+ if (commit && createsReview) {
189
+ const push = reviewProvider === "gitlab"
190
+ ? pushBranchForMr(worktreePath, node.branch)
191
+ : pushBranchForPr(worktreePath, node.branch);
167
192
  if (push.ok) {
168
193
  opts.log.write("branch-push", { ticket: node.ticket.id, branch: node.branch, status: "pushed" });
169
194
  summary.pushStatus = "pushed";
@@ -194,29 +219,75 @@ export async function runBranchPlan(opts) {
194
219
  continue;
195
220
  }
196
221
  pushedBranches.add(node.branch);
197
- const pr = createOrReusePr(opts.projectDir, {
198
- node,
199
- ready: opts.prReady,
200
- runId: opts.runId,
201
- qaEvidence: qaSummary,
202
- commit,
203
- });
222
+ const pr = reviewProvider === "gitlab"
223
+ ? createOrReuseMr(opts.projectDir, {
224
+ node,
225
+ ready: opts.prReady || completionMode === "auto-merge",
226
+ runId: opts.runId,
227
+ qaEvidence: qaSummary,
228
+ commit,
229
+ autoMerge: completionMode === "auto-merge",
230
+ cleanup: opts.cleanupBranches ?? true,
231
+ })
232
+ : createOrReusePr(opts.projectDir, {
233
+ node,
234
+ ready: opts.prReady || completionMode === "auto-merge",
235
+ runId: opts.runId,
236
+ qaEvidence: qaSummary,
237
+ commit,
238
+ });
204
239
  summary.pr = pr;
205
240
  if (pr.status === "created")
206
- opts.log.write("pr-created", { ticket: node.ticket.id, branch: node.branch, url: pr.url });
241
+ opts.log.write(reviewProvider === "gitlab" ? "mr-created" : "pr-created", { ticket: node.ticket.id, branch: node.branch, url: pr.url });
207
242
  if (pr.status === "existing")
208
- opts.log.write("pr-existing", { ticket: node.ticket.id, branch: node.branch, url: pr.url });
243
+ opts.log.write(reviewProvider === "gitlab" ? "mr-existing" : "pr-existing", { ticket: node.ticket.id, branch: node.branch, url: pr.url });
209
244
  if (pr.status === "failed") {
210
- opts.log.write("pr-failed", failureLogFields(node, pr));
211
- blockBranchIssue(opts, node, summary, pr.code ?? "pr_create_failed", `PR creation failed: ${pr.message ?? pr.error ?? "unknown error"}`, pr);
245
+ opts.log.write(reviewProvider === "gitlab" ? "mr-failed" : "pr-failed", failureLogFields(node, pr));
246
+ blockBranchIssue(opts, node, summary, pr.code ?? (reviewProvider === "gitlab" ? "mr_create_failed" : "pr_create_failed"), `${reviewProvider === "gitlab" ? "MR" : "PR"} creation failed: ${pr.message ?? pr.error ?? "unknown error"}`, pr);
212
247
  summaries.push(summary);
213
248
  continue;
214
249
  }
250
+ if (completionMode === "auto-merge" && reviewProvider === "github") {
251
+ const autoMerge = enableGitHubAutoMerge(opts.projectDir, node.branch, opts.cleanupBranches ?? true);
252
+ summary.pr = autoMerge.status === "failed" ? autoMerge : { ...pr, status: "auto_merge_enabled", url: autoMerge.url ?? pr.url };
253
+ if (autoMerge.status === "failed") {
254
+ opts.log.write("pr-auto-merge-failed", failureLogFields(node, autoMerge));
255
+ blockBranchIssue(opts, node, summary, autoMerge.code ?? "pr_create_failed", `PR auto-merge failed: ${autoMerge.message ?? autoMerge.error ?? "unknown error"}`, autoMerge);
256
+ summaries.push(summary);
257
+ continue;
258
+ }
259
+ opts.log.write("pr-auto-merge-enabled", { ticket: node.ticket.id, branch: node.branch, url: summary.pr.url });
260
+ }
261
+ else if (completionMode === "auto-merge" && reviewProvider === "gitlab" && pr.status === "existing") {
262
+ const autoMerge = enableGitLabAutoMerge(opts.projectDir, node.branch, opts.cleanupBranches ?? true);
263
+ summary.pr = autoMerge.status === "failed" ? autoMerge : { ...pr, status: "auto_merge_enabled", url: autoMerge.url ?? pr.url };
264
+ if (autoMerge.status === "failed") {
265
+ opts.log.write("mr-auto-merge-failed", failureLogFields(node, autoMerge));
266
+ blockBranchIssue(opts, node, summary, autoMerge.code ?? "mr_create_failed", `MR auto-merge failed: ${autoMerge.message ?? autoMerge.error ?? "unknown error"}`, autoMerge);
267
+ summaries.push(summary);
268
+ continue;
269
+ }
270
+ opts.log.write("mr-auto-merge-enabled", { ticket: node.ticket.id, branch: node.branch, url: summary.pr.url });
271
+ }
215
272
  }
216
- else if (opts.createPr) {
273
+ else if (createsReview) {
217
274
  summary.pushStatus = "skipped";
218
275
  summary.pr = { status: "skipped", error: commit ? undefined : "no_changes" };
219
276
  }
277
+ else if (commit && completionMode === "direct-merge") {
278
+ if (!opts.keepWorktrees)
279
+ removeTicketWorktree(opts.projectDir, worktreePath);
280
+ const mergeCommit = squashMergeBranchToLocalBase(opts.projectDir, node.branch, node.baseBranch, `${node.ticket.id}: ${node.ticket.title}`);
281
+ summary.pr = { status: "merged", url: mergeCommit };
282
+ opts.log.write("branch-direct-merge", {
283
+ ticket: node.ticket.id,
284
+ branch: node.branch,
285
+ base: node.baseBranch,
286
+ commit: mergeCommit,
287
+ });
288
+ if (opts.cleanupBranches ?? true)
289
+ deleteLocalBranch(opts.projectDir, node.branch);
290
+ }
220
291
  cmdComplete(opts.projectDir, node.ticket.id, {
221
292
  actor: "foreman",
222
293
  summary: status.summary ?? `Completed ${node.ticket.id}`,
@@ -233,7 +304,7 @@ export async function runBranchPlan(opts) {
233
304
  status: "done",
234
305
  detail: summary.detail,
235
306
  });
236
- if (!opts.keepWorktrees)
307
+ if (!opts.keepWorktrees && completionMode !== "direct-merge")
237
308
  removeTicketWorktree(opts.projectDir, worktreePath);
238
309
  }
239
310
  catch (err) {
@@ -255,6 +326,70 @@ export async function runBranchPlan(opts) {
255
326
  }
256
327
  return summaries;
257
328
  }
329
+ async function waitForAutoMergeDependencies(opts, node, provider) {
330
+ const dependencyNodes = node.dependencies
331
+ .map((dep) => opts.plan.nodes.find((candidate) => candidate.ticket.id === dep))
332
+ .filter((dep) => Boolean(dep));
333
+ if (dependencyNodes.length === 0)
334
+ return { ok: true };
335
+ const timeoutMs = opts.autoMergeTimeoutMinutes === null || opts.autoMergeTimeoutMinutes === undefined
336
+ ? null
337
+ : opts.autoMergeTimeoutMinutes * 60_000;
338
+ const deadline = opts.autoMergeWait && timeoutMs !== null ? Date.now() + timeoutMs : null;
339
+ while (true) {
340
+ const pending = [];
341
+ for (const dep of dependencyNodes) {
342
+ const status = provider === "gitlab"
343
+ ? checkGitLabMrMerged(opts.projectDir, dep.branch)
344
+ : checkGitHubPrMerged(opts.projectDir, dep.branch);
345
+ if (!status.ok) {
346
+ return {
347
+ ok: false,
348
+ code: status.code,
349
+ message: `dependency ${dep.ticket.id} merge check failed: ${status.message}`,
350
+ repairCommands: status.repairCommands,
351
+ command: status.command,
352
+ output: status.output,
353
+ };
354
+ }
355
+ if (!status.merged)
356
+ pending.push(`${dep.ticket.id}${status.state ? ` (${status.state})` : ""}`);
357
+ }
358
+ if (pending.length === 0)
359
+ return { ok: true };
360
+ const message = `auto-merge dependency ${pending.join(", ")} has not merged into the root base yet`;
361
+ if (!opts.autoMergeWait) {
362
+ return {
363
+ ok: false,
364
+ code: "dependency_unavailable",
365
+ message: `${message}; rerun after it merges or enable auto-merge wait in ticket setup`,
366
+ };
367
+ }
368
+ if (deadline !== null && Date.now() >= deadline) {
369
+ return {
370
+ ok: false,
371
+ code: "dependency_unavailable",
372
+ message: `${message}; timed out waiting for dependency merge`,
373
+ };
374
+ }
375
+ opts.log.write("branch-auto-merge-wait", {
376
+ ticket: node.ticket.id,
377
+ pending,
378
+ timeoutMinutes: opts.autoMergeTimeoutMinutes ?? null,
379
+ });
380
+ await sleep(autoMergePollMs());
381
+ }
382
+ }
383
+ function autoMergePollMs() {
384
+ const raw = process.env.RAFI_AUTO_MERGE_POLL_MS;
385
+ if (!raw)
386
+ return 30_000;
387
+ const parsed = Number.parseInt(raw, 10);
388
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : 30_000;
389
+ }
390
+ function sleep(ms) {
391
+ return new Promise((resolve) => setTimeout(resolve, ms));
392
+ }
258
393
  function notifyIssue(enabled, message) {
259
394
  if (enabled)
260
395
  fireNotification("Foreman branch issue", message);
@@ -14,7 +14,7 @@ export interface BranchIssue {
14
14
  message: string;
15
15
  blocking: boolean;
16
16
  }
17
- export type GitHubFailureCode = "gh_missing" | "gh_not_authenticated" | "remote_missing" | "remote_not_github" | "repo_unreachable" | "git_remote_unreachable" | "push_failed" | "pr_create_failed" | "network_or_timeout" | "unknown";
17
+ export type GitHubFailureCode = "gh_missing" | "glab_missing" | "gh_not_authenticated" | "glab_not_authenticated" | "remote_missing" | "remote_not_github" | "remote_not_gitlab" | "repo_unreachable" | "git_remote_unreachable" | "push_failed" | "pr_create_failed" | "mr_create_failed" | "network_or_timeout" | "unknown";
18
18
  export interface GitHubFailure {
19
19
  ok: false;
20
20
  code: GitHubFailureCode;
@@ -43,13 +43,21 @@ export type GitHubOperationResult = {
43
43
  ok: true;
44
44
  output?: string;
45
45
  } | GitHubFailure;
46
+ export type ReviewMergeStatus = {
47
+ ok: true;
48
+ merged: boolean;
49
+ state?: string;
50
+ url?: string;
51
+ } | GitHubFailure;
46
52
  export interface BranchPlan {
47
53
  baseRef: string;
48
54
  nodes: BranchPlanNode[];
49
55
  issues: BranchIssue[];
50
56
  }
57
+ export type CompletionMode = "none" | "pr" | "auto-merge" | "direct-merge";
58
+ export type ReviewProvider = "github" | "gitlab";
51
59
  export interface PrResult {
52
- status: "created" | "existing" | "failed" | "skipped";
60
+ status: "created" | "existing" | "failed" | "skipped" | "auto_merge_enabled" | "merged";
53
61
  url?: string;
54
62
  error?: string;
55
63
  code?: GitHubFailureCode;
package/dist/cli/start.js CHANGED
@@ -18,8 +18,10 @@ import { StateDb } from "../tickets/stateDb.js";
18
18
  import { buildBranchAuditInstruction, buildBranchPlan, parseAuditDependencies } from "../branch/planner.js";
19
19
  import { currentGitRef, ensureCleanBaseWorktree, generatedTrackerDirtyPaths } from "../branch/git.js";
20
20
  import { formatGitHubFailure, preflightGh } from "../branch/github.js";
21
+ import { preflightGlab } from "../branch/gitlab.js";
21
22
  import { runBranchPlan } from "../branch/runner.js";
22
23
  import { findResumableBranchSessions, formatBranchContinueCommand, formatBranchSummaryFollowupCommands, } from "../branch/resume.js";
24
+ import { detectGitProvider, loadTicketSetupConfig } from "../tickets/setupConfig.js";
23
25
  function fail(message) {
24
26
  console.error(`foreman: ${message}`);
25
27
  process.exit(1);
@@ -47,6 +49,80 @@ function collectTicket(value, previous) {
47
49
  function unique(values) {
48
50
  return [...new Set(values)];
49
51
  }
52
+ function optionWasProvided(command, name) {
53
+ const source = command.getOptionValueSource(name);
54
+ return source !== undefined && source !== "default";
55
+ }
56
+ function resolveStartBranchDefaults(cwd, opts, command) {
57
+ const setup = loadTicketSetupConfig(cwd);
58
+ const savedBuild = setup?.build;
59
+ const branchFlagProvided = optionWasProvided(command, "branchPerTicket");
60
+ const branchFlagOn = branchFlagProvided && opts.branchPerTicket !== false;
61
+ const createPrFlagProvided = optionWasProvided(command, "createPr");
62
+ const createPrFlagOn = createPrFlagProvided && opts.createPr !== false;
63
+ const completionFromFlag = typeof opts.completion === "string"
64
+ ? parseCompletionMode(opts.completion)
65
+ : undefined;
66
+ let completionMode = completionFromFlag
67
+ ?? (createPrFlagOn ? "pr" : undefined)
68
+ ?? savedBuild?.completion
69
+ ?? "none";
70
+ if (createPrFlagProvided && opts.createPr === false) {
71
+ completionMode = "none";
72
+ }
73
+ const savedBranchMode = savedBuild?.branch_strategy === "branch-per-ticket";
74
+ let branchMode = Boolean(savedBranchMode || branchFlagOn || createPrFlagOn || completionMode !== "none");
75
+ if (branchFlagProvided && opts.branchPerTicket === false) {
76
+ branchMode = false;
77
+ completionMode = "none";
78
+ }
79
+ if (completionMode !== "none")
80
+ branchMode = true;
81
+ const providerFromFlag = typeof opts.provider === "string" ? parseReviewProviderOption(opts.provider, cwd) : undefined;
82
+ const provider = providerFromFlag ?? (createPrFlagOn ? "github" : providerFromSaved(savedBuild?.provider, cwd));
83
+ const autoMergeWaitProvided = optionWasProvided(command, "autoMergeWait");
84
+ const autoMergeTimeoutMinutes = typeof opts.autoMergeTimeoutMinutes === "string"
85
+ ? parseOptionalPositiveInteger(opts.autoMergeTimeoutMinutes, "--auto-merge-timeout-minutes")
86
+ : savedBuild?.auto_merge_timeout_minutes ?? null;
87
+ return {
88
+ branchMode,
89
+ completionMode,
90
+ reviewProvider: provider,
91
+ createReview: completionMode === "pr" || completionMode === "auto-merge",
92
+ prReady: Boolean(opts.prReady || savedBuild?.pr_ready || completionMode === "auto-merge"),
93
+ cleanupBranches: opts.keepWorktrees ? false : savedBuild?.cleanup ?? true,
94
+ rootBaseBranches: completionMode === "auto-merge" || completionMode === "direct-merge",
95
+ autoMergeWait: autoMergeWaitProvided ? opts.autoMergeWait !== false : savedBuild?.auto_merge_wait ?? false,
96
+ autoMergeTimeoutMinutes,
97
+ };
98
+ }
99
+ function parseCompletionMode(value) {
100
+ if (["pr", "auto-merge", "direct-merge", "none"].includes(value))
101
+ return value;
102
+ fail("--completion must be one of: pr, auto-merge, direct-merge, none");
103
+ }
104
+ function parseReviewProviderOption(value, cwd) {
105
+ if (value === "auto")
106
+ return providerFromSaved("auto", cwd);
107
+ if (value === "github" || value === "gitlab")
108
+ return value;
109
+ fail("--provider must be one of: auto, github, gitlab");
110
+ }
111
+ function parseOptionalPositiveInteger(value, label) {
112
+ if (!value.trim())
113
+ return null;
114
+ const parsed = Number.parseInt(value, 10);
115
+ if (Number.isInteger(parsed) && parsed > 0)
116
+ return parsed;
117
+ fail(`${label} must be a positive integer or blank`);
118
+ }
119
+ function providerFromSaved(value, cwd) {
120
+ if (value === "github" || value === "gitlab")
121
+ return value;
122
+ if (value === "local")
123
+ return undefined;
124
+ return detectGitProvider(cwd);
125
+ }
50
126
  function branchContinueTicketHelp(cwd, flag = "--continue") {
51
127
  const sessions = findResumableBranchSessions(join(cwd, ".foreman"));
52
128
  if (sessions.length === 0) {
@@ -93,6 +169,40 @@ async function ensureGitHubReadyForCreatePr(cwd, log, yes) {
93
169
  }
94
170
  }
95
171
  }
172
+ async function ensureReviewProviderReady(cwd, provider, log, yes) {
173
+ if (provider === "github") {
174
+ await ensureGitHubReadyForCreatePr(cwd, log, yes);
175
+ return;
176
+ }
177
+ while (true) {
178
+ const result = preflightGlab(cwd);
179
+ if (result.ok)
180
+ return;
181
+ log.write("gitlab-readiness-failed", {
182
+ code: result.code,
183
+ message: result.message,
184
+ repairCommands: result.repairCommands,
185
+ command: result.command,
186
+ output: result.output,
187
+ });
188
+ const detail = formatGitHubFailure(result);
189
+ if (yes || !process.stdin.isTTY || !process.stdout.isTTY) {
190
+ fail(`GitLab MR setup failed before building:\n${detail}`);
191
+ }
192
+ console.error(`foreman: GitLab MR setup failed before building:\n${detail}\n`);
193
+ const action = await select({
194
+ message: "Retry GitLab readiness check?",
195
+ options: [
196
+ { value: "retry", label: "Retry" },
197
+ { value: "cancel", label: "Cancel" },
198
+ ],
199
+ });
200
+ if (isCancel(action) || action === "cancel") {
201
+ console.log("ai-foreman: cancelled");
202
+ process.exit(0);
203
+ }
204
+ }
205
+ }
96
206
  export function buildStartCommand() {
97
207
  return new Command("start")
98
208
  .description("Enlist a builder and drive it through a batch of N steps.")
@@ -108,14 +218,21 @@ export function buildStartCommand() {
108
218
  .option("--fast", "fast mode — lower latency (maps to effort=low for codex)")
109
219
  .option("--no-qa", "disable per-ticket QA review (enabled by default)")
110
220
  .option("--branch-per-ticket", "run each selected structured ticket in an isolated git worktree and branch")
221
+ .option("--no-branch-per-ticket", "disable saved branch-per-ticket defaults for this run")
111
222
  .option("--create-pr", "push each successful ticket branch and create a GitHub PR (implies --branch-per-ticket)")
223
+ .option("--no-create-pr", "disable saved PR/MR creation defaults for this run")
224
+ .option("--completion <mode>", "ticket branch completion behavior (pr | auto-merge | direct-merge | none)")
225
+ .option("--provider <provider>", "PR/MR provider for branch completion (auto | github | gitlab)")
226
+ .option("--auto-merge-wait", "wait for dependency PR/MRs to merge before starting dependent tickets")
227
+ .option("--no-auto-merge-wait", "do not wait for dependency PR/MRs before dependent tickets")
228
+ .option("--auto-merge-timeout-minutes <n>", "auto-merge dependency wait timeout in minutes (blank means no timeout)")
112
229
  .option("--base <ref>", "base ref for root ticket branches (default: current branch or HEAD)")
113
230
  .option("--branch-prefix <prefix>", "branch name prefix for ticket branches", "rafi")
114
231
  .option("--max-branch-depth <n>", "maximum selected branch stack depth", "2")
115
232
  .option("--pr-ready", "create ready-for-review PRs instead of draft PRs")
116
233
  .option("--keep-worktrees", "keep successful ticket worktrees for inspection")
117
234
  .option("--ticket <id>", "ticket id to continue in branch mode; repeat for multiple tickets", collectTicket, [])
118
- .action(async (project, opts) => {
235
+ .action(async (project, opts, command) => {
119
236
  const steps = Number.parseInt(opts.steps, 10);
120
237
  if (!Number.isInteger(steps) || steps < 1) {
121
238
  fail("--steps must be a positive integer");
@@ -127,6 +244,7 @@ export function buildStartCommand() {
127
244
  const cwd = resolve(project);
128
245
  if (!existsSync(cwd))
129
246
  fail(`project directory not found: ${cwd}`);
247
+ const branchDefaults = resolveStartBranchDefaults(cwd, opts, command);
130
248
  let agent;
131
249
  try {
132
250
  agent = resolveAgentForProject(cwd, opts.agent);
@@ -165,7 +283,7 @@ export function buildStartCommand() {
165
283
  if (opts.resume && opts.continue) {
166
284
  fail("choose either --resume <sessionId> or --continue, not both");
167
285
  }
168
- const branchMode = Boolean(opts.branchPerTicket || opts.createPr);
286
+ const branchMode = branchDefaults.branchMode;
169
287
  const continueTickets = opts.ticket ?? [];
170
288
  const maxBranchDepth = Number.parseInt(opts.maxBranchDepth, 10);
171
289
  if (!Number.isInteger(maxBranchDepth) || maxBranchDepth < 1) {
@@ -188,6 +306,9 @@ export function buildStartCommand() {
188
306
  fail("--tickets is not supported with --branch-per-ticket; initialize and use .tickets/tickets.yaml");
189
307
  if (!isTicketsInitialized(cwd))
190
308
  fail("--branch-per-ticket requires initialized .tickets/ (run ai-foreman tickets init)");
309
+ if (branchDefaults.createReview && !branchDefaults.reviewProvider) {
310
+ fail("PR/MR completion is enabled but no GitHub or GitLab origin remote was detected; pass --provider github|gitlab or --completion none");
311
+ }
191
312
  }
192
313
  const resumeSessionId = opts.resume ??
193
314
  (!branchMode && opts.continue ? findLastSessionId(join(cwd, ".foreman")) : undefined);
@@ -225,8 +346,8 @@ export function buildStartCommand() {
225
346
  catch (err) {
226
347
  fail(err instanceof Error ? err.message : String(err));
227
348
  }
228
- if (opts.createPr) {
229
- await ensureGitHubReadyForCreatePr(cwd, log, Boolean(opts.yes));
349
+ if (branchDefaults.createReview && branchDefaults.reviewProvider) {
350
+ await ensureReviewProviderReady(cwd, branchDefaults.reviewProvider, log, Boolean(opts.yes));
230
351
  }
231
352
  const ready = await ensureRuntimeReadyForCommand(cwd, agent, {
232
353
  label: "start",
@@ -290,6 +411,7 @@ export function buildStartCommand() {
290
411
  baseRef: opts.base ?? currentGitRef(cwd),
291
412
  branchPrefix: opts.branchPrefix,
292
413
  maxBranchDepth,
414
+ rootBaseBranches: branchDefaults.rootBaseBranches,
293
415
  });
294
416
  let auditBuilder;
295
417
  let auditViewer;
@@ -304,6 +426,7 @@ export function buildStartCommand() {
304
426
  branchPrefix: opts.branchPrefix,
305
427
  maxBranchDepth,
306
428
  auditDependencies,
429
+ rootBaseBranches: branchDefaults.rootBaseBranches,
307
430
  });
308
431
  log.write("branch-plan", {
309
432
  baseRef: plan.baseRef,
@@ -353,9 +476,14 @@ export function buildStartCommand() {
353
476
  fast: opts.fast,
354
477
  notificationsEnabled: config.notifications.enabled,
355
478
  qaEnabled,
356
- createPr: Boolean(opts.createPr),
357
- prReady: Boolean(opts.prReady),
479
+ createPr: branchDefaults.createReview,
480
+ completionMode: branchDefaults.completionMode,
481
+ reviewProvider: branchDefaults.reviewProvider,
482
+ prReady: branchDefaults.prReady,
358
483
  keepWorktrees: Boolean(opts.keepWorktrees),
484
+ cleanupBranches: branchDefaults.cleanupBranches,
485
+ autoMergeWait: branchDefaults.autoMergeWait,
486
+ autoMergeTimeoutMinutes: branchDefaults.autoMergeTimeoutMinutes,
359
487
  allowedBaseDirtyPaths,
360
488
  trackerPaths: {
361
489
  progressDoc: ticketsConfig.paths.progressDoc,
@@ -366,7 +494,7 @@ export function buildStartCommand() {
366
494
  observeBuilder: (builder) => printEvents(builder.events()),
367
495
  });
368
496
  console.log("foreman: branch run summary");
369
- console.log("ticket\tbranch\tbase\tstatus\tcommit\tpush\tpr");
497
+ console.log("ticket\tbranch\tbase\tstatus\tcommit\tpush\tcompletion");
370
498
  for (const row of summaries) {
371
499
  console.log([
372
500
  row.ticket,
@@ -14,4 +14,15 @@ export declare function buildPopulateAgentRunOptions(opts: {
14
14
  yes?: boolean;
15
15
  log: Log;
16
16
  }): RoleBuilderOptions;
17
+ interface PopulateCommandOptions {
18
+ project?: string;
19
+ agent?: string;
20
+ model?: string;
21
+ effort?: string;
22
+ sources?: string[];
23
+ fast?: boolean;
24
+ yes?: boolean;
25
+ }
26
+ export declare function cmdPopulateCli(opts: PopulateCommandOptions): Promise<void>;
17
27
  export declare function buildTicketsCommand(): Command;
28
+ export {};