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.
package/README.md CHANGED
@@ -123,9 +123,13 @@ Branch-mode examples:
123
123
  ```bash
124
124
  ai-foreman start ./my-project --steps 3 --branch-per-ticket --agent codex
125
125
  ai-foreman start ./my-project --steps 3 --create-pr --pr-ready --agent codex
126
+ ai-foreman start ./my-project --steps 3 --completion auto-merge --provider gitlab
127
+ ai-foreman start ./my-project --steps 3 --completion direct-merge
126
128
  ai-foreman start ./my-project --steps 1 --branch-per-ticket --continue --ticket T001
127
129
  ```
128
130
 
131
+ Saved `tickets.build` defaults in `rafi-config.yaml` can enable branch-per-ticket and completion behavior automatically. Use `--completion pr|auto-merge|direct-merge|none`, `--no-branch-per-ticket`, `--no-create-pr`, or `--auto-merge-wait` / `--no-auto-merge-wait` to override saved defaults for one run.
132
+
129
133
  ### GitHub PR Failure Recovery
130
134
 
131
135
  When `--create-pr` cannot prepare GitHub, push a branch, or create a PR, Foreman records a structured failure and blocks the affected ticket cleanly. Blocked worktrees are retained automatically so you can repair the environment and retry without losing the ticket session.
@@ -164,6 +168,10 @@ gh repo view <host>/<owner>/<repo>
164
168
  | `--fast` | flag | off | Lower latency. |
165
169
  | `-y, --yes` | flag | off | Skips confirmation before builder edits ticket files. |
166
170
 
171
+ Use `ai-foreman tickets setup:init` or `setup:update` to save local, Linear, or Jira Cloud ticket sources in `rafi-config.yaml`. Linear imports use `LINEAR_API_KEY`; Jira Cloud imports use `JIRA_EMAIL` and `JIRA_API_TOKEN`. Imported provider payloads are snapshotted under ignored `.tickets/imports/`, and mirrored tickets carry `external_refs`.
172
+
173
+ Use `ai-foreman tickets review` to accept, defer, or dismiss pending split/combine/duplicate recommendations rendered in the progress doc.
174
+
167
175
  ## What Foreman Does
168
176
 
169
177
  During a run, Foreman:
@@ -258,11 +266,15 @@ Init options:
258
266
  | `-p, --project <dir>` | `./my-project` / `../repo` | current directory | Project to initialize. |
259
267
  | `--app-name <name>` | `"My App"` | none | Application name stored in tracker config. |
260
268
  | `--timezone <tz>` | `America/Chicago` / `UTC` | `UTC` | IANA timezone. |
261
- | `--queue-limit <n>` | `25` / `50` / `100` | `50` | Next-queue window size. |
269
+ | `--implementation-limit <n>` | `250` / `500` / `1000` | `500` | Implementation queue window for generated docs and Foreman selection. |
270
+ | `--view-limit <n>` | `1000` / `20000` | `20000` | Default row limit for the display queue command. |
271
+ | `--queue-limit <n>` | `250` / `500` / `1000` | none | Deprecated alias for `--implementation-limit`. |
262
272
  | `--docs-root <dir>` | `docs` / `docs-rafi` | `rafi-config.yaml` docs.root, then `docs` | Root for generated progress/archive docs. |
263
273
 
264
274
  `tickets init` reads `docs.root` from `rafi-config.yaml` when present. `--docs-root` overrides that value. The selected root must be a safe repo-relative directory, and init refuses to overwrite an existing selected `ticket-progress.md`.
265
275
 
276
+ Init writes `implementation_limit` and `view_limit` to `.tickets/config.yaml`. Existing configs with `queue_limit` still load as an implementation limit, with the old default value `50` upgraded to `500`.
277
+
266
278
  This creates:
267
279
 
268
280
  ```txt
@@ -24,3 +24,5 @@ export declare function hasWorktreeChanges(worktreePath: string): boolean;
24
24
  export declare function commitAll(worktreePath: string, message: string): string | undefined;
25
25
  export declare function headCommitIfAhead(worktreePath: string, base: string): string | undefined;
26
26
  export declare function pushBranch(worktreePath: string, branch: string): void;
27
+ export declare function squashMergeBranchToLocalBase(projectDir: string, branch: string, baseBranch: string, message: string): string;
28
+ export declare function deleteLocalBranch(projectDir: string, branch: string): void;
@@ -26,7 +26,7 @@ export function generatedTrackerDirtyPaths(paths) {
26
26
  return out;
27
27
  }
28
28
  export function ensureCleanBaseWorktree(cwd, opts = {}) {
29
- const allowedDirtyPaths = new Set((opts.allowedDirtyPaths ?? []).map(normalizeRepoPath));
29
+ const allowedDirtyPaths = new Set([".tickets/.gitignore", ...(opts.allowedDirtyPaths ?? []).map(normalizeRepoPath)]);
30
30
  const status = runGit(cwd, ["status", "--porcelain", "--untracked-files=all"]).stdout;
31
31
  const dirty = status.split("\n").filter(Boolean).filter((line) => {
32
32
  const path = normalizeRepoPath(line.slice(3));
@@ -101,6 +101,23 @@ export function headCommitIfAhead(worktreePath, base) {
101
101
  export function pushBranch(worktreePath, branch) {
102
102
  runGit(worktreePath, ["push", "-u", "origin", branch]);
103
103
  }
104
+ export function squashMergeBranchToLocalBase(projectDir, branch, baseBranch, message) {
105
+ const current = currentGitRef(projectDir);
106
+ if (current !== baseBranch) {
107
+ runGit(projectDir, ["checkout", baseBranch]);
108
+ }
109
+ runGit(projectDir, ["merge", "--squash", branch]);
110
+ runGit(projectDir, ["commit", "-m", message]);
111
+ return runGit(projectDir, ["rev-parse", "--short", "HEAD"]).stdout;
112
+ }
113
+ export function deleteLocalBranch(projectDir, branch) {
114
+ try {
115
+ runGit(projectDir, ["branch", "-D", branch]);
116
+ }
117
+ catch {
118
+ // Branch cleanup is best-effort; worktree removal already frees the checkout.
119
+ }
120
+ }
104
121
  function normalizeRepoPath(path) {
105
122
  return path
106
123
  .replace(/\\/g, "/")
@@ -1,4 +1,4 @@
1
- import type { BranchPlanNode, GitHubFailure, GitHubOperationResult, GitHubReadinessResult, GitHubRemote, GitHubRemoteResult, PrResult } from "./types.js";
1
+ import type { BranchPlanNode, GitHubFailure, GitHubOperationResult, GitHubReadinessResult, GitHubRemote, GitHubRemoteResult, PrResult, ReviewMergeStatus } from "./types.js";
2
2
  export declare function inspectGitHubRemote(cwd: string): GitHubRemoteResult;
3
3
  export declare function originLooksLikeGitHub(cwd: string): boolean;
4
4
  export declare function checkGitHubReadiness(cwd: string): GitHubReadinessResult;
@@ -17,5 +17,7 @@ export interface CreatePrOptions {
17
17
  commit?: string;
18
18
  }
19
19
  export declare function createOrReusePr(cwd: string, opts: CreatePrOptions): PrResult;
20
+ export declare function enableGitHubAutoMerge(cwd: string, branch: string, cleanup: boolean): PrResult;
21
+ export declare function checkGitHubPrMerged(cwd: string, branch: string): ReviewMergeStatus;
20
22
  export declare function formatGitHubFailure(failure: GitHubFailure): string;
21
23
  export {};
@@ -157,6 +157,38 @@ export function createOrReusePr(cwd, opts) {
157
157
  return { status: "created", url: created.stdout };
158
158
  return prFailure(classifyPrFailure(created, opts.node.branch, "Failed to create GitHub PR.", remoteForRepair));
159
159
  }
160
+ export function enableGitHubAutoMerge(cwd, branch, cleanup) {
161
+ const args = ["pr", "merge", branch, "--auto", "--squash"];
162
+ if (cleanup)
163
+ args.push("--delete-branch");
164
+ const result = runCommand(cwd, "gh", args, 30_000);
165
+ if (result.ok)
166
+ return { status: "auto_merge_enabled", url: result.stdout };
167
+ return prFailure(classifyPrFailure(result, branch, "Failed to enable GitHub PR auto-merge."));
168
+ }
169
+ export function checkGitHubPrMerged(cwd, branch) {
170
+ const remote = inspectGitHubRemote(cwd);
171
+ const remoteForRepair = remote.ok ? remote.remote : undefined;
172
+ const result = runCommand(cwd, "gh", [
173
+ "pr",
174
+ "view",
175
+ branch,
176
+ "--json",
177
+ "state,url",
178
+ "--jq",
179
+ "[.state, .url] | @tsv",
180
+ ]);
181
+ if (!result.ok) {
182
+ return classifyPrFailure(result, branch, "Failed to check GitHub PR merge state.", remoteForRepair);
183
+ }
184
+ const [state, url] = result.stdout.split(/\t/);
185
+ return {
186
+ ok: true,
187
+ merged: state === "MERGED",
188
+ state,
189
+ url: url || undefined,
190
+ };
191
+ }
160
192
  export function formatGitHubFailure(failure) {
161
193
  const lines = [
162
194
  `${failure.code}: ${failure.message}`,
@@ -0,0 +1,15 @@
1
+ import type { BranchPlanNode, GitHubOperationResult, PrResult, ReviewMergeStatus } from "./types.js";
2
+ export declare function preflightGlab(cwd: string): GitHubOperationResult;
3
+ export declare function pushBranchForMr(cwd: string, branch: string): GitHubOperationResult;
4
+ export interface CreateMrOptions {
5
+ node: BranchPlanNode;
6
+ ready: boolean;
7
+ runId: string;
8
+ qaEvidence?: string;
9
+ commit?: string;
10
+ autoMerge?: boolean;
11
+ cleanup?: boolean;
12
+ }
13
+ export declare function createOrReuseMr(cwd: string, opts: CreateMrOptions): PrResult;
14
+ export declare function enableGitLabAutoMerge(cwd: string, branch: string, cleanup: boolean): PrResult;
15
+ export declare function checkGitLabMrMerged(cwd: string, branch: string): ReviewMergeStatus;
@@ -0,0 +1,290 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ const DEFAULT_COMMAND_TIMEOUT_MS = 15_000;
5
+ const OUTPUT_LIMIT = 2_000;
6
+ export function preflightGlab(cwd) {
7
+ const version = runCommand(cwd, "glab", ["--version"], 5_000);
8
+ if (!version.ok) {
9
+ return failure("glab_missing", "GitLab CLI is not installed or is not on PATH.", [
10
+ "Install GitLab CLI: https://gitlab.com/gitlab-org/cli",
11
+ "glab --version",
12
+ ], version);
13
+ }
14
+ const remote = runCommand(cwd, "git", ["remote", "get-url", "origin"]);
15
+ if (!remote.ok) {
16
+ return failure("remote_missing", "git remote origin is not configured.", [
17
+ "git remote add origin <gitlab repo url>",
18
+ "git remote get-url origin",
19
+ ], remote);
20
+ }
21
+ if (!looksGitLab(remote.stdout)) {
22
+ return failure("remote_not_gitlab", `origin does not look like a GitLab remote: ${remote.stdout}`, [
23
+ "git remote set-url origin <gitlab repo url>",
24
+ "git remote get-url origin",
25
+ ], remote);
26
+ }
27
+ const auth = runCommand(cwd, "glab", ["auth", "status"], 10_000);
28
+ if (!auth.ok) {
29
+ return classifyCommandFailure(auth, "glab_not_authenticated", "GitLab CLI is not authenticated.", [
30
+ "glab auth login",
31
+ "glab auth status",
32
+ ]);
33
+ }
34
+ return { ok: true, output: version.stdout };
35
+ }
36
+ export function pushBranchForMr(cwd, branch) {
37
+ const result = runCommand(cwd, "git", ["push", "-u", "origin", branch], 30_000);
38
+ if (result.ok)
39
+ return { ok: true, output: result.stdout };
40
+ return classifyCommandFailure(result, "push_failed", [
41
+ `Failed to push branch ${branch} to origin.`,
42
+ "Branch push requires write access and working Git credentials for the remote.",
43
+ ].join(" "), [
44
+ `git push -u origin ${branch}`,
45
+ "git ls-remote origin",
46
+ ]);
47
+ }
48
+ export function createOrReuseMr(cwd, opts) {
49
+ const existing = findExistingMr(cwd, opts.node.branch);
50
+ if (!existing.ok)
51
+ return prFailure(existing);
52
+ if (existing.url)
53
+ return { status: "existing", url: existing.url };
54
+ const bodyPath = join(cwd, ".foreman", "mr-bodies", opts.runId, `${opts.node.ticket.id}.md`);
55
+ try {
56
+ mkdirSync(join(cwd, ".foreman", "mr-bodies", opts.runId), { recursive: true });
57
+ writeFileSync(bodyPath, buildMrBody(opts), "utf8");
58
+ }
59
+ catch (err) {
60
+ return prFailure(failure("mr_create_failed", `Failed to write GitLab MR body file at ${bodyPath}.`, [
61
+ `mkdir -p ${shellQuote(join(".foreman", "mr-bodies", opts.runId))}`,
62
+ ], undefined, err instanceof Error ? err.message : String(err)));
63
+ }
64
+ const args = [
65
+ "mr",
66
+ "create",
67
+ "--source-branch",
68
+ opts.node.branch,
69
+ "--target-branch",
70
+ opts.node.baseBranch,
71
+ "--title",
72
+ `${opts.node.ticket.id}: ${opts.node.ticket.title}`,
73
+ "--description",
74
+ readBodyArg(bodyPath),
75
+ "--yes",
76
+ ];
77
+ if (!opts.ready)
78
+ args.push("--draft");
79
+ if (opts.autoMerge)
80
+ args.push("--auto-merge", "--squash-before-merge");
81
+ if (opts.cleanup)
82
+ args.push("--remove-source-branch");
83
+ const created = runCommand(cwd, "glab", args);
84
+ if (created.ok)
85
+ return { status: "created", url: created.stdout };
86
+ return prFailure(classifyCommandFailure(created, "mr_create_failed", `Failed to create GitLab MR. Branch: ${opts.node.branch}`, [
87
+ `glab mr create --source-branch ${opts.node.branch} --target-branch ${opts.node.baseBranch}`,
88
+ "glab auth status",
89
+ ]));
90
+ }
91
+ export function enableGitLabAutoMerge(cwd, branch, cleanup) {
92
+ const args = ["mr", "merge", branch, "--auto-merge", "--squash", "--yes"];
93
+ if (cleanup)
94
+ args.push("--remove-source-branch");
95
+ const result = runCommand(cwd, "glab", args, 30_000);
96
+ if (result.ok)
97
+ return { status: "auto_merge_enabled", url: result.stdout };
98
+ return prFailure(classifyCommandFailure(result, "mr_create_failed", `Failed to enable GitLab MR auto-merge. Branch: ${branch}`, [
99
+ `glab mr merge ${branch} --auto-merge --squash --yes`,
100
+ "glab auth status",
101
+ ]));
102
+ }
103
+ export function checkGitLabMrMerged(cwd, branch) {
104
+ const result = runCommand(cwd, "glab", [
105
+ "mr",
106
+ "view",
107
+ branch,
108
+ "--output",
109
+ "json",
110
+ ]);
111
+ if (!result.ok) {
112
+ return classifyCommandFailure(result, "mr_create_failed", `Failed to check GitLab MR merge state. Branch: ${branch}`, [
113
+ `glab mr view ${branch} --output json`,
114
+ "glab auth status",
115
+ ]);
116
+ }
117
+ try {
118
+ const parsed = JSON.parse(result.stdout);
119
+ const state = parsed.state;
120
+ return {
121
+ ok: true,
122
+ merged: state === "merged" || state === "merged_results" || Boolean(parsed.merged_at ?? parsed.mergedAt),
123
+ state,
124
+ url: parsed.web_url ?? parsed.webUrl,
125
+ };
126
+ }
127
+ catch {
128
+ return {
129
+ ok: true,
130
+ merged: /merged/i.test(result.stdout),
131
+ state: result.stdout.slice(0, 120),
132
+ };
133
+ }
134
+ }
135
+ function findExistingMr(cwd, branch) {
136
+ const result = runCommand(cwd, "glab", [
137
+ "mr",
138
+ "list",
139
+ "--source-branch",
140
+ branch,
141
+ "--output",
142
+ "json",
143
+ ]);
144
+ if (!result.ok) {
145
+ return classifyCommandFailure(result, "mr_create_failed", "Failed to check for an existing GitLab MR.", [
146
+ `glab mr list --source-branch ${branch}`,
147
+ "glab auth status",
148
+ ]);
149
+ }
150
+ try {
151
+ const parsed = JSON.parse(result.stdout);
152
+ const first = Array.isArray(parsed) ? parsed[0] : undefined;
153
+ return { ok: true, url: first?.web_url ?? first?.webUrl };
154
+ }
155
+ catch {
156
+ return { ok: true, url: undefined };
157
+ }
158
+ }
159
+ function runCommand(cwd, bin, args, timeoutMs = DEFAULT_COMMAND_TIMEOUT_MS) {
160
+ const result = spawnSync(bin, args, {
161
+ cwd,
162
+ encoding: "utf8",
163
+ stdio: ["ignore", "pipe", "pipe"],
164
+ timeout: commandTimeoutMs(timeoutMs),
165
+ });
166
+ const stdout = outputToString(result.stdout).trim();
167
+ const stderr = outputToString(result.stderr).trim();
168
+ const output = truncateOutput([stderr, stdout].filter(Boolean).join("\n").trim());
169
+ const error = result.error;
170
+ return {
171
+ ok: !error && result.status === 0,
172
+ command: commandDisplay(bin, args),
173
+ stdout,
174
+ stderr,
175
+ output,
176
+ status: result.status,
177
+ signal: result.signal,
178
+ errorCode: error?.code,
179
+ errorMessage: error?.message,
180
+ timedOut: error?.code === "ETIMEDOUT" || result.signal === "SIGTERM",
181
+ };
182
+ }
183
+ function commandTimeoutMs(defaultMs) {
184
+ const raw = process.env.RAFI_GITLAB_COMMAND_TIMEOUT_MS;
185
+ if (!raw)
186
+ return defaultMs;
187
+ const parsed = Number.parseInt(raw, 10);
188
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : defaultMs;
189
+ }
190
+ function classifyCommandFailure(result, fallbackCode, fallbackMessage, repairCommands) {
191
+ if (result.errorCode === "ENOENT") {
192
+ return failure("glab_missing", "GitLab CLI is not installed or is not on PATH.", [
193
+ "Install GitLab CLI: https://gitlab.com/gitlab-org/cli",
194
+ "glab --version",
195
+ ], result);
196
+ }
197
+ if (result.timedOut || looksNetworkLike(result.output) || looksNetworkLike(result.errorMessage ?? "")) {
198
+ return failure("network_or_timeout", `${fallbackMessage} The command timed out or hit a network error.`, repairCommands, result);
199
+ }
200
+ if (looksUnauthenticated(result.output)) {
201
+ return failure("glab_not_authenticated", fallbackMessage, repairCommands, result);
202
+ }
203
+ return failure(fallbackCode, fallbackMessage, repairCommands, result);
204
+ }
205
+ function failure(code, message, repairCommands, result, outputOverride) {
206
+ return {
207
+ ok: false,
208
+ code,
209
+ message,
210
+ repairCommands,
211
+ command: result?.command,
212
+ output: outputOverride ?? (result?.output || result?.errorMessage ? truncateOutput([result?.output, result?.errorMessage].filter(Boolean).join("\n")) : undefined),
213
+ };
214
+ }
215
+ function prFailure(failure) {
216
+ return {
217
+ status: "failed",
218
+ error: failure.message,
219
+ code: failure.code,
220
+ message: failure.message,
221
+ repairCommands: failure.repairCommands,
222
+ command: failure.command,
223
+ output: failure.output,
224
+ };
225
+ }
226
+ function looksGitLab(remote) {
227
+ return /gitlab\./i.test(remote) || /gitlab\.com/i.test(remote);
228
+ }
229
+ function looksUnauthenticated(output) {
230
+ return [
231
+ /not logged in/i,
232
+ /authentication required/i,
233
+ /could not authenticate/i,
234
+ /unauthorized/i,
235
+ /\b401\b/,
236
+ ].some((pattern) => pattern.test(output));
237
+ }
238
+ function looksNetworkLike(output) {
239
+ return [
240
+ /timed? out/i,
241
+ /could not resolve host/i,
242
+ /failed to connect/i,
243
+ /connection (?:reset|refused|closed)/i,
244
+ /\b(?:ENOTFOUND|ETIMEDOUT|ECONNRESET|ECONNREFUSED|EAI_AGAIN)\b/i,
245
+ ].some((pattern) => pattern.test(output));
246
+ }
247
+ function readBodyArg(bodyPath) {
248
+ return readFileSync(bodyPath, "utf8");
249
+ }
250
+ function buildMrBody(opts) {
251
+ const ticket = opts.node.ticket;
252
+ return [
253
+ `## ${ticket.id}: ${ticket.title}`,
254
+ "",
255
+ ticket.summary,
256
+ "",
257
+ "## Acceptance Criteria",
258
+ ...ticket.acceptance.map((item) => `- ${item}`),
259
+ "",
260
+ "## Validation",
261
+ opts.qaEvidence ?? "Foreman QA emitted qa_pass.",
262
+ "",
263
+ "## Branch Metadata",
264
+ `- Base: ${opts.node.baseBranch}`,
265
+ `- Head: ${opts.node.branch}`,
266
+ `- Dependencies: ${opts.node.dependencies.length ? opts.node.dependencies.join(", ") : "None"}`,
267
+ `- Commit: ${opts.commit ?? "N/A"}`,
268
+ "",
269
+ "Generated by Rafi ai-foreman branch-per-ticket mode.",
270
+ "",
271
+ ].join("\n");
272
+ }
273
+ function commandDisplay(bin, args) {
274
+ return [bin, ...args.map(shellQuote)].join(" ");
275
+ }
276
+ function shellQuote(value) {
277
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value))
278
+ return value;
279
+ return `'${value.replace(/'/g, "'\"'\"'")}'`;
280
+ }
281
+ function outputToString(value) {
282
+ if (!value)
283
+ return "";
284
+ return Buffer.isBuffer(value) ? value.toString("utf8") : value;
285
+ }
286
+ function truncateOutput(output) {
287
+ if (output.length <= OUTPUT_LIMIT)
288
+ return output;
289
+ return `${output.slice(0, OUTPUT_LIMIT).trimEnd()}\n... truncated ...`;
290
+ }
@@ -12,6 +12,7 @@ export interface BuildBranchPlanOptions {
12
12
  branchPrefix: string;
13
13
  maxBranchDepth: number;
14
14
  auditDependencies?: AuditDependency[];
15
+ rootBaseBranches?: boolean;
15
16
  }
16
17
  export declare function buildBranchPlan(tickets: TicketDef[], states: Map<string, TicketState>, opts: BuildBranchPlanOptions): BranchPlan;
17
18
  export declare function selectTicketsForBranchRun(tickets: TicketDef[], states: Map<string, TicketState>, steps: number): TicketDef[];
@@ -60,7 +60,9 @@ export function buildBranchPlan(tickets, states, opts) {
60
60
  const dependencies = dependencyMap.get(ticket.id) ?? [];
61
61
  const branch = branchById.get(ticket.id);
62
62
  const baseTicket = chooseBaseDependency(dependencies, dependencyMap);
63
- const baseBranch = baseTicket ? (branchById.get(baseTicket) ?? opts.baseRef) : opts.baseRef;
63
+ const baseBranch = opts.rootBaseBranches
64
+ ? opts.baseRef
65
+ : baseTicket ? (branchById.get(baseTicket) ?? opts.baseRef) : opts.baseRef;
64
66
  const node = {
65
67
  ticket,
66
68
  branch,
@@ -12,6 +12,8 @@ export interface BranchResumeSession {
12
12
  fast?: boolean;
13
13
  qaEnabled?: boolean;
14
14
  createPr?: boolean;
15
+ completionMode?: string;
16
+ reviewProvider?: string;
15
17
  prReady?: boolean;
16
18
  keepWorktrees?: boolean;
17
19
  ts?: string;
@@ -39,6 +39,8 @@ export function findResumableBranchSessions(foremanDir) {
39
39
  fast: booleanField(record.fast),
40
40
  qaEnabled: booleanField(record.qaEnabled),
41
41
  createPr: booleanField(record.createPr),
42
+ completionMode: stringField(record.completionMode),
43
+ reviewProvider: stringField(record.reviewProvider),
42
44
  prReady: booleanField(record.prReady),
43
45
  keepWorktrees: booleanField(record.keepWorktrees),
44
46
  ts: stringField(record.ts),
@@ -63,7 +65,9 @@ export function formatBranchContinueCommand(projectDir, session) {
63
65
  shellQuote(projectDir),
64
66
  "--steps",
65
67
  "1",
66
- session.createPr ? "--create-pr" : "--branch-per-ticket",
68
+ ...(session.completionMode && session.completionMode !== "none"
69
+ ? ["--completion", shellQuote(session.completionMode)]
70
+ : [session.createPr ? "--create-pr" : "--branch-per-ticket"]),
67
71
  "--continue",
68
72
  "--ticket",
69
73
  shellQuote(session.ticket),
@@ -74,6 +78,8 @@ export function formatBranchContinueCommand(projectDir, session) {
74
78
  args.push("--model", shellQuote(session.model));
75
79
  if (session.effort)
76
80
  args.push("--effort", shellQuote(session.effort));
81
+ if (session.reviewProvider)
82
+ args.push("--provider", shellQuote(session.reviewProvider));
77
83
  if (session.fast)
78
84
  args.push("--fast");
79
85
  if (session.qaEnabled === false)
@@ -1,6 +1,6 @@
1
1
  import type { BuilderAdapter, EffortLevel } from "../adapters/types.js";
2
2
  import type { Log } from "../log.js";
3
- import type { BranchPlan, BranchPlanNode, BranchRunSummary } from "./types.js";
3
+ import type { BranchPlan, BranchPlanNode, BranchRunSummary, CompletionMode, ReviewProvider } from "./types.js";
4
4
  export interface BranchRunnerOptions {
5
5
  projectDir: string;
6
6
  runId: string;
@@ -13,8 +13,13 @@ export interface BranchRunnerOptions {
13
13
  notificationsEnabled: boolean;
14
14
  qaEnabled: boolean;
15
15
  createPr: boolean;
16
+ completionMode?: CompletionMode;
17
+ reviewProvider?: ReviewProvider;
16
18
  prReady: boolean;
17
19
  keepWorktrees: boolean;
20
+ cleanupBranches?: boolean;
21
+ autoMergeWait?: boolean;
22
+ autoMergeTimeoutMinutes?: number | null;
18
23
  allowedBaseDirtyPaths?: string[];
19
24
  trackerPaths?: {
20
25
  progressDoc: string;