ai-foreman 1.5.0 → 1.6.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 (59) hide show
  1. package/README.md +8 -0
  2. package/dist/adapters/claude.d.ts +6 -0
  3. package/dist/adapters/claude.js +84 -9
  4. package/dist/adapters/recovering.d.ts +30 -0
  5. package/dist/adapters/recovering.js +83 -0
  6. package/dist/adapters/types.d.ts +13 -0
  7. package/dist/adapters/types.js +0 -3
  8. package/dist/agentRun.d.ts +3 -2
  9. package/dist/agentRun.js +89 -24
  10. package/dist/branch/git.d.ts +3 -0
  11. package/dist/branch/git.js +31 -5
  12. package/dist/branch/github.d.ts +2 -2
  13. package/dist/branch/github.js +2 -2
  14. package/dist/branch/gitlab.d.ts +3 -2
  15. package/dist/branch/gitlab.js +10 -4
  16. package/dist/branch/planner.d.ts +3 -1
  17. package/dist/branch/planner.js +24 -2
  18. package/dist/branch/runner.d.ts +10 -1
  19. package/dist/branch/runner.js +41 -13
  20. package/dist/branch/types.d.ts +4 -0
  21. package/dist/buildRuns.d.ts +35 -0
  22. package/dist/buildRuns.js +149 -0
  23. package/dist/cli/doctor.js +91 -4
  24. package/dist/cli/runtimeAuthPrompt.d.ts +3 -1
  25. package/dist/cli/runtimeAuthPrompt.js +25 -6
  26. package/dist/cli/start.js +208 -17
  27. package/dist/cli/status.d.ts +2 -0
  28. package/dist/cli/status.js +71 -55
  29. package/dist/cli/tickets.js +57 -10
  30. package/dist/foreman.d.ts +3 -1
  31. package/dist/foreman.js +52 -5
  32. package/dist/index.js +0 -0
  33. package/dist/interviews.d.ts +26 -4
  34. package/dist/interviews.js +71 -4
  35. package/dist/log.d.ts +1 -1
  36. package/dist/roles.d.ts +11 -0
  37. package/dist/roles.js +8 -0
  38. package/dist/runtimeAuth.d.ts +1 -1
  39. package/dist/runtimeAuth.js +10 -31
  40. package/dist/runtimeReadiness.d.ts +17 -0
  41. package/dist/runtimeReadiness.js +195 -0
  42. package/dist/ticketPlanning.d.ts +83 -0
  43. package/dist/ticketPlanning.js +287 -0
  44. package/dist/tickets/commands.js +16 -0
  45. package/dist/tickets/delivery.d.ts +38 -0
  46. package/dist/tickets/delivery.js +152 -0
  47. package/dist/tickets/importer.js +4 -0
  48. package/dist/tickets/renderMarkdown.js +9 -0
  49. package/dist/tickets/setupConfig.d.ts +8 -0
  50. package/dist/tickets/setupConfig.js +47 -2
  51. package/dist/tickets/sourceFetch.d.ts +24 -0
  52. package/dist/tickets/sourceFetch.js +246 -0
  53. package/dist/tickets/stateDb.d.ts +10 -0
  54. package/dist/tickets/stateDb.js +24 -0
  55. package/dist/tickets/ticketLoader.js +18 -0
  56. package/dist/tickets/ticketSchema.d.ts +93 -0
  57. package/dist/tickets/ticketSchema.js +17 -0
  58. package/dist/tickets/validate.js +12 -0
  59. package/package.json +33 -2
@@ -1,4 +1,5 @@
1
1
  import { RuntimeAuthError, type AgentRuntime } from "../runtimeAuth.js";
2
+ import type { RuntimeProbeResult } from "rafi-spec";
2
3
  export type RuntimeCommandRecoveryChoice = "retry" | "switch" | "cancel";
3
4
  export interface RuntimeCommandRecoveryContext {
4
5
  otherRuntime: AgentRuntime;
@@ -9,7 +10,7 @@ export interface RuntimeReadyForCommandOptions {
9
10
  yes?: boolean;
10
11
  allowSwitch?: boolean;
11
12
  model?: string | undefined;
12
- check?: (projectDir: string, runtime: AgentRuntime) => void;
13
+ check?: (projectDir: string, runtime: AgentRuntime) => void | RuntimeProbeResult | Promise<void | RuntimeProbeResult>;
13
14
  checkClaudeSdk?: () => Promise<void>;
14
15
  choose?: (err: RuntimeAuthError, context: RuntimeCommandRecoveryContext) => Promise<RuntimeCommandRecoveryChoice>;
15
16
  }
@@ -17,5 +18,6 @@ export interface RuntimeReadyForCommandResult {
17
18
  runtime: AgentRuntime;
18
19
  model?: string;
19
20
  fellBack: boolean;
21
+ executable: string;
20
22
  }
21
23
  export declare function ensureRuntimeReadyForCommand(projectDir: string, runtime: AgentRuntime, labelOrOptions: string | RuntimeReadyForCommandOptions): Promise<RuntimeReadyForCommandResult>;
@@ -1,6 +1,7 @@
1
1
  import { select, isCancel, log } from "@clack/prompts";
2
2
  import { requireClaudeSDK } from "../adapters/claude.js";
3
3
  import { checkRuntimeReady, RuntimeAuthError, runtimeCommandLabel, } from "../runtimeAuth.js";
4
+ import { resolveExecutablePath } from "../runtimeReadiness.js";
4
5
  import { otherRuntime, runtimeDisplayName } from "./runtimeSelection.js";
5
6
  export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrOptions) {
6
7
  const opts = typeof labelOrOptions === "string"
@@ -12,13 +13,23 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
12
13
  const nonInteractive = !opts.choose && (Boolean(opts.yes) || !process.stdin.isTTY || !process.stdout.isTTY);
13
14
  while (true) {
14
15
  try {
15
- check(projectDir, runtime);
16
- return { runtime, model: opts.model, fellBack: false };
16
+ const probe = await check(projectDir, runtime);
17
+ if (runtime === "claude")
18
+ await checkClaudeSdk();
19
+ const executable = probe?.executable ?? resolveExecutablePath(runtime === "claude" ? "claude" : "codex");
20
+ if (!executable)
21
+ throw new Error(`${runtime} executable disappeared after readiness`);
22
+ return { runtime, model: opts.model, fellBack: false, executable };
17
23
  }
18
24
  catch (err) {
19
25
  const failure = err instanceof RuntimeAuthError
20
26
  ? err
21
- : new RuntimeAuthError({ runtime, context: opts.label, cause: err });
27
+ : new RuntimeAuthError({
28
+ runtime,
29
+ context: opts.label,
30
+ stderr: err instanceof Error ? err.message : String(err),
31
+ cause: err,
32
+ });
22
33
  if (nonInteractive) {
23
34
  throw failure;
24
35
  }
@@ -30,7 +41,7 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
30
41
  continue;
31
42
  if (choice === "switch" && allowSwitch) {
32
43
  try {
33
- check(projectDir, fallbackRuntime);
44
+ const probe = await check(projectDir, fallbackRuntime);
34
45
  if (fallbackRuntime === "claude") {
35
46
  await checkClaudeSdk();
36
47
  }
@@ -38,12 +49,20 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
38
49
  console.log(`foreman: ignored --model ${opts.model} after switching to ${fallbackRuntime}; model names are provider-specific.`);
39
50
  }
40
51
  console.log(`foreman: using ${runtimeDisplayName(fallbackRuntime)} for this run.`);
41
- return { runtime: fallbackRuntime, model: undefined, fellBack: true };
52
+ const executable = probe?.executable ?? resolveExecutablePath(fallbackRuntime === "claude" ? "claude" : "codex");
53
+ if (!executable)
54
+ throw new Error(`${fallbackRuntime} executable disappeared after readiness`);
55
+ return { runtime: fallbackRuntime, model: undefined, fellBack: true, executable };
42
56
  }
43
57
  catch (switchErr) {
44
58
  const switchFailure = switchErr instanceof RuntimeAuthError
45
59
  ? switchErr
46
- : new RuntimeAuthError({ runtime: fallbackRuntime, context: opts.label, cause: switchErr });
60
+ : new RuntimeAuthError({
61
+ runtime: fallbackRuntime,
62
+ context: opts.label,
63
+ stderr: switchErr instanceof Error ? switchErr.message : String(switchErr),
64
+ cause: switchErr,
65
+ });
47
66
  log.error(`Fallback runtime is not ready:\n${switchFailure.message}`);
48
67
  continue;
49
68
  }
package/dist/cli/start.js CHANGED
@@ -7,6 +7,7 @@ import { Log } from "../log.js";
7
7
  import { PermissionPolicy } from "../permissions/policy.js";
8
8
  import { ClaudeAdapter } from "../adapters/claude.js";
9
9
  import { CodexAdapter } from "../adapters/codex.js";
10
+ import { RecoveringAdapter } from "../adapters/recovering.js";
10
11
  import { Foreman, createPermissionHandler } from "../foreman.js";
11
12
  import { printEvents } from "./events.js";
12
13
  import { loadRoleBundle } from "../roles.js";
@@ -15,13 +16,16 @@ import { resolveAgentForProject } from "./runtimeSelection.js";
15
16
  import { isTicketsInitialized, loadTicketsConfig, resolveTicketPaths } from "../tickets/config.js";
16
17
  import { loadTickets } from "../tickets/ticketLoader.js";
17
18
  import { StateDb } from "../tickets/stateDb.js";
18
- import { buildBranchAuditInstruction, buildBranchPlan, parseAuditDependencies } from "../branch/planner.js";
19
+ import { applySharedDeliveryBranch, buildBranchAuditInstruction, buildBranchPlan, parseAuditDependencies } from "../branch/planner.js";
19
20
  import { currentGitRef, ensureCleanBaseWorktree, generatedTrackerDirtyPaths } from "../branch/git.js";
20
21
  import { formatGitHubFailure, preflightGh } from "../branch/github.js";
21
22
  import { preflightGlab } from "../branch/gitlab.js";
22
- import { runBranchPlan } from "../branch/runner.js";
23
+ import { readDeliveryUnitSession, runBranchPlan } from "../branch/runner.js";
24
+ import { checkpointBuildRun, completeBuildRun, createBuildRun, heartbeatBuildRun, persistBuildSession, releaseBuildLease } from "../buildRuns.js";
25
+ import { parse as parseYaml } from "yaml";
23
26
  import { findResumableBranchSessions, formatBranchContinueCommand, formatBranchSummaryFollowupCommands, } from "../branch/resume.js";
24
27
  import { detectGitProvider, loadTicketSetupConfig } from "../tickets/setupConfig.js";
28
+ import { loadDeliveryConfig, selectDeliveryUnitForRun } from "../tickets/delivery.js";
25
29
  function fail(message) {
26
30
  console.error(`foreman: ${message}`);
27
31
  process.exit(1);
@@ -53,7 +57,7 @@ function optionWasProvided(command, name) {
53
57
  const source = command.getOptionValueSource(name);
54
58
  return source !== undefined && source !== "default";
55
59
  }
56
- function resolveStartBranchDefaults(cwd, opts, command) {
60
+ function resolveStartBranchDefaults(cwd, opts, command, delivery) {
57
61
  const setup = loadTicketSetupConfig(cwd);
58
62
  const savedBuild = setup?.build;
59
63
  const branchFlagProvided = optionWasProvided(command, "branchPerTicket");
@@ -63,8 +67,10 @@ function resolveStartBranchDefaults(cwd, opts, command) {
63
67
  const completionFromFlag = typeof opts.completion === "string"
64
68
  ? parseCompletionMode(opts.completion)
65
69
  : undefined;
70
+ const deliveryCompletion = delivery?.unit.completion;
66
71
  let completionMode = completionFromFlag
67
72
  ?? (createPrFlagOn ? "pr" : undefined)
73
+ ?? deliveryCompletion
68
74
  ?? savedBuild?.completion
69
75
  ?? "none";
70
76
  if (createPrFlagProvided && opts.createPr === false) {
@@ -72,6 +78,9 @@ function resolveStartBranchDefaults(cwd, opts, command) {
72
78
  }
73
79
  const savedBranchMode = savedBuild?.branch_strategy === "branch-per-ticket";
74
80
  let branchMode = Boolean(savedBranchMode || branchFlagOn || createPrFlagOn || completionMode !== "none");
81
+ if (!branchFlagProvided && !createPrFlagProvided && !optionWasProvided(command, "completion") && delivery) {
82
+ branchMode = delivery.unit.branch_mode !== "current";
83
+ }
75
84
  if (branchFlagProvided && opts.branchPerTicket === false) {
76
85
  branchMode = false;
77
86
  completionMode = "none";
@@ -79,7 +88,7 @@ function resolveStartBranchDefaults(cwd, opts, command) {
79
88
  if (completionMode !== "none")
80
89
  branchMode = true;
81
90
  const providerFromFlag = typeof opts.provider === "string" ? parseReviewProviderOption(opts.provider, cwd) : undefined;
82
- const provider = providerFromFlag ?? (createPrFlagOn ? "github" : providerFromSaved(savedBuild?.provider, cwd));
91
+ const provider = providerFromFlag ?? (createPrFlagOn ? "github" : providerFromSaved(delivery?.unit.provider ?? savedBuild?.provider, cwd));
83
92
  const autoMergeWaitProvided = optionWasProvided(command, "autoMergeWait");
84
93
  const autoMergeTimeoutMinutes = typeof opts.autoMergeTimeoutMinutes === "string"
85
94
  ? parseOptionalPositiveInteger(opts.autoMergeTimeoutMinutes, "--auto-merge-timeout-minutes")
@@ -89,13 +98,19 @@ function resolveStartBranchDefaults(cwd, opts, command) {
89
98
  completionMode,
90
99
  reviewProvider: provider,
91
100
  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,
101
+ prReady: Boolean(opts.prReady || delivery?.unit.pr_ready || savedBuild?.pr_ready || completionMode === "auto-merge"),
102
+ cleanupBranches: opts.keepWorktrees ? false : delivery?.unit.cleanup ?? savedBuild?.cleanup ?? true,
94
103
  rootBaseBranches: completionMode === "auto-merge" || completionMode === "direct-merge",
95
104
  autoMergeWait: autoMergeWaitProvided ? opts.autoMergeWait !== false : savedBuild?.auto_merge_wait ?? false,
96
105
  autoMergeTimeoutMinutes,
106
+ mergeMethod: typeof opts.mergeMethod === "string" ? parseMergeMethod(opts.mergeMethod) : delivery?.unit.merge_method ?? savedBuild?.merge_method ?? "squash",
97
107
  };
98
108
  }
109
+ function parseMergeMethod(value) {
110
+ if (["squash", "merge", "rebase"].includes(value))
111
+ return value;
112
+ fail("--merge-method must be one of: squash, merge, rebase");
113
+ }
99
114
  function parseCompletionMode(value) {
100
115
  if (["pr", "auto-merge", "direct-merge", "none"].includes(value))
101
116
  return value;
@@ -222,6 +237,7 @@ export function buildStartCommand() {
222
237
  .option("--create-pr", "push each successful ticket branch and create a GitHub PR (implies --branch-per-ticket)")
223
238
  .option("--no-create-pr", "disable saved PR/MR creation defaults for this run")
224
239
  .option("--completion <mode>", "ticket branch completion behavior (pr | auto-merge | direct-merge | none)")
240
+ .option("--merge-method <method>", "merge method for local or remote completion (squash | merge | rebase)")
225
241
  .option("--provider <provider>", "PR/MR provider for branch completion (auto | github | gitlab)")
226
242
  .option("--auto-merge-wait", "wait for dependency PR/MRs to merge before starting dependent tickets")
227
243
  .option("--no-auto-merge-wait", "do not wait for dependency PR/MRs before dependent tickets")
@@ -232,6 +248,7 @@ export function buildStartCommand() {
232
248
  .option("--pr-ready", "create ready-for-review PRs instead of draft PRs")
233
249
  .option("--keep-worktrees", "keep successful ticket worktrees for inspection")
234
250
  .option("--ticket <id>", "ticket id to continue in branch mode; repeat for multiple tickets", collectTicket, [])
251
+ .option("--skip-delivery-unit <id>", "skip one unfinished delivery unit for this run", collectTicket, [])
235
252
  .action(async (project, opts, command) => {
236
253
  const steps = Number.parseInt(opts.steps, 10);
237
254
  if (!Number.isInteger(steps) || steps < 1) {
@@ -244,15 +261,45 @@ export function buildStartCommand() {
244
261
  const cwd = resolve(project);
245
262
  if (!existsSync(cwd))
246
263
  fail(`project directory not found: ${cwd}`);
247
- const branchDefaults = resolveStartBranchDefaults(cwd, opts, command);
264
+ const roleDefaults = readAgentDefaults(cwd);
265
+ let deliveryRun;
266
+ if (isTicketsInitialized(cwd)) {
267
+ const delivery = loadDeliveryConfig(cwd);
268
+ if (delivery) {
269
+ const earlyConfig = loadTicketsConfig(cwd);
270
+ const earlyPaths = resolveTicketPaths(earlyConfig, cwd);
271
+ const earlyDb = new StateDb(earlyPaths.stateDb);
272
+ const earlyStates = earlyDb.getAllStates();
273
+ earlyDb.close();
274
+ deliveryRun = selectDeliveryUnitForRun(delivery, earlyStates, opts.skipDeliveryUnit ?? []);
275
+ if (deliveryRun?.state === "resume" && !opts.yes && process.stdin.isTTY && process.stdout.isTTY) {
276
+ const answer = await select({ message: `Resume unfinished delivery unit ${deliveryRun.unit.id} (${deliveryRun.remaining.length} remaining)?`, options: [
277
+ { value: "resume", label: "Resume (Recommended)" }, { value: "skip", label: "Skip for this run" },
278
+ ] });
279
+ if (isCancel(answer))
280
+ process.exit(0);
281
+ if (answer === "skip")
282
+ deliveryRun = selectDeliveryUnitForRun(delivery, earlyStates, [deliveryRun.unit.id, ...(opts.skipDeliveryUnit ?? [])]);
283
+ }
284
+ }
285
+ }
286
+ const branchDefaults = resolveStartBranchDefaults(cwd, opts, command, deliveryRun);
248
287
  let agent;
249
288
  try {
250
- agent = resolveAgentForProject(cwd, opts.agent);
289
+ agent = resolveAgentForProject(cwd, opts.agent ?? roleDefaults.builder?.make);
251
290
  }
252
291
  catch (err) {
253
292
  fail(err instanceof Error ? err.message : String(err));
254
293
  }
255
- let model = opts.model;
294
+ let model = opts.model ?? explicitDefaultValue(roleDefaults.builder?.model);
295
+ let agentExecutable;
296
+ const builderEffort = opts.effort ?? explicitEffort(roleDefaults.builder?.reasoning);
297
+ const builderFast = opts.fast ?? roleDefaults.builder?.fast;
298
+ let qaAgent = resolveAgentForProject(cwd, roleDefaults.qa?.make);
299
+ let qaModel = explicitDefaultValue(roleDefaults.qa?.model);
300
+ let qaExecutable;
301
+ const qaEffort = explicitEffort(roleDefaults.qa?.reasoning);
302
+ const qaFast = roleDefaults.qa?.fast ?? false;
256
303
  const configuredTrackerPath = isTicketsInitialized(cwd)
257
304
  ? loadTicketsConfig(cwd).paths.progressDoc
258
305
  : undefined;
@@ -320,16 +367,18 @@ export function buildStartCommand() {
320
367
  const logPath = join(cwd, ".foreman", `${stamp}.jsonl`);
321
368
  const log = new Log(logPath);
322
369
  const qaEnabled = opts.qa !== false && config.qa.enabled !== false;
323
- const createBuilder = async (builderCwd, sessionId) => {
370
+ const createRawBuilder = async (builderCwd, sessionId) => {
324
371
  const builderPolicy = new PermissionPolicy(config.permissions, builderCwd);
325
372
  const roleBundle = loadRoleBundle("builder", { projectDir: builderCwd });
326
373
  const adapterOpts = {
327
374
  cwd: builderCwd,
375
+ runtimeExecutable: agentExecutable,
376
+ runtimePhase: "builder",
328
377
  model,
329
378
  resumeSessionId: sessionId,
330
379
  permission: createPermissionHandler(builderPolicy, log),
331
- effort: opts.effort,
332
- fast: opts.fast,
380
+ effort: builderEffort,
381
+ fast: builderFast,
333
382
  systemPromptAppend: roleBundle.system || undefined,
334
383
  skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
335
384
  };
@@ -337,6 +386,67 @@ export function buildStartCommand() {
337
386
  ? new CodexAdapter(adapterOpts)
338
387
  : await ClaudeAdapter.create(adapterOpts);
339
388
  };
389
+ const createBuilder = async (builderCwd, sessionId) => {
390
+ const initial = await createRawBuilder(builderCwd, sessionId);
391
+ return new RecoveringAdapter({
392
+ initial,
393
+ runtime: agent,
394
+ label: "builder turn",
395
+ enabled: !opts.yes && Boolean(process.stdin.isTTY && process.stdout.isTTY),
396
+ allowSwitch: !(opts.resume || opts.continue),
397
+ recreate: async (nextRuntime, resumeSessionId) => {
398
+ const nextReady = await ensureRuntimeReadyForCommand(builderCwd, nextRuntime, {
399
+ label: "builder recovery",
400
+ allowSwitch: false,
401
+ model: nextRuntime === agent ? model : undefined,
402
+ });
403
+ agent = nextReady.runtime;
404
+ model = nextReady.model;
405
+ agentExecutable = nextReady.executable;
406
+ return createRawBuilder(builderCwd, resumeSessionId);
407
+ },
408
+ });
409
+ };
410
+ const createRawQa = async (qaCwd, sessionId) => {
411
+ const qaPolicy = new PermissionPolicy(config.permissions, qaCwd);
412
+ const roleBundle = loadRoleBundle("qa", { projectDir: qaCwd });
413
+ const adapterOpts = {
414
+ cwd: qaCwd,
415
+ runtimeExecutable: qaExecutable,
416
+ runtimePhase: "qa",
417
+ model: qaModel,
418
+ resumeSessionId: sessionId,
419
+ permission: createPermissionHandler(qaPolicy, log),
420
+ effort: qaEffort,
421
+ fast: qaFast,
422
+ systemPromptAppend: `${roleBundle.system}\n\nYou are an independent QA reviewer. Do not edit source, tickets, configuration, or project documentation. You may run tests and create only harmless ignored caches or coverage output.`,
423
+ skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
424
+ };
425
+ return qaAgent === "codex"
426
+ ? new CodexAdapter(adapterOpts)
427
+ : await ClaudeAdapter.create(adapterOpts);
428
+ };
429
+ const createQa = async (qaCwd, sessionId) => {
430
+ const initial = await createRawQa(qaCwd, sessionId);
431
+ return new RecoveringAdapter({
432
+ initial,
433
+ runtime: qaAgent,
434
+ label: "QA turn",
435
+ enabled: !opts.yes && Boolean(process.stdin.isTTY && process.stdout.isTTY),
436
+ allowSwitch: !sessionId,
437
+ recreate: async (nextRuntime, resumeSessionId) => {
438
+ const nextReady = await ensureRuntimeReadyForCommand(qaCwd, nextRuntime, {
439
+ label: "QA recovery",
440
+ allowSwitch: false,
441
+ model: nextRuntime === qaAgent ? qaModel : undefined,
442
+ });
443
+ qaAgent = nextReady.runtime;
444
+ qaModel = nextReady.model;
445
+ qaExecutable = nextReady.executable;
446
+ return createRawQa(qaCwd, resumeSessionId);
447
+ },
448
+ });
449
+ };
340
450
  if (branchMode) {
341
451
  const ticketsConfig = loadTicketsConfig(cwd);
342
452
  const allowedBaseDirtyPaths = generatedTrackerDirtyPaths(ticketsConfig.paths);
@@ -357,6 +467,7 @@ export function buildStartCommand() {
357
467
  });
358
468
  agent = ready.runtime;
359
469
  model = ready.model;
470
+ agentExecutable = ready.executable;
360
471
  const ticketPaths = resolveTicketPaths(ticketsConfig, cwd);
361
472
  const tickets = loadTickets(ticketPaths.tickets);
362
473
  const db = new StateDb(ticketPaths.stateDb);
@@ -412,6 +523,7 @@ export function buildStartCommand() {
412
523
  branchPrefix: opts.branchPrefix,
413
524
  maxBranchDepth,
414
525
  rootBaseBranches: branchDefaults.rootBaseBranches,
526
+ ticketIds: deliveryRun?.remaining.slice(0, steps),
415
527
  });
416
528
  let auditBuilder;
417
529
  let auditViewer;
@@ -419,6 +531,8 @@ export function buildStartCommand() {
419
531
  auditBuilder = await createBuilder(cwd);
420
532
  auditViewer = printEvents(auditBuilder.events());
421
533
  const audit = await auditBuilder.sendTurn(buildBranchAuditInstruction(plan.nodes.map((node) => node.ticket)));
534
+ if (audit.isError)
535
+ throw new Error(audit.text);
422
536
  const auditDependencies = parseAuditDependencies(audit.text);
423
537
  plan = buildBranchPlan(tickets, states, {
424
538
  steps,
@@ -427,6 +541,7 @@ export function buildStartCommand() {
427
541
  maxBranchDepth,
428
542
  auditDependencies,
429
543
  rootBaseBranches: branchDefaults.rootBaseBranches,
544
+ ticketIds: deliveryRun?.remaining.slice(0, steps),
430
545
  });
431
546
  log.write("branch-plan", {
432
547
  baseRef: plan.baseRef,
@@ -440,11 +555,26 @@ export function buildStartCommand() {
440
555
  await auditBuilder?.close().catch(() => { });
441
556
  await auditViewer?.catch(() => { });
442
557
  }
558
+ if (deliveryRun?.unit.branch_mode === "shared") {
559
+ plan = applySharedDeliveryBranch(plan, deliveryRun.unit.id, deliveryRun.remaining, opts.branchPrefix);
560
+ const saved = readDeliveryUnitSession(cwd, deliveryRun.unit.id);
561
+ const first = plan.nodes[0];
562
+ if (saved && first && saved.branch === first.branch && existsSync(saved.worktreePath)) {
563
+ resumeSessionByTicket.set(first.ticket.id, { worktreePath: saved.worktreePath, sessionId: saved.sessionId });
564
+ }
565
+ }
443
566
  }
444
567
  console.log(`foreman: ${continueTickets.length > 0 ? "resuming branch-per-ticket mode" : "branch-per-ticket mode"} for ${plan.nodes.length} ticket(s)`);
445
568
  console.log(`foreman: project ${cwd}`);
446
569
  console.log(`foreman: base ${plan.baseRef}`);
447
570
  console.log(`foreman: log ${logPath}\n`);
571
+ if (deliveryRun) {
572
+ console.log(`foreman: delivery unit ${deliveryRun.unit.id} — ${deliveryRun.completed}/${deliveryRun.unit.tickets.length} complete, ${deliveryRun.remaining.length} remaining`);
573
+ if (deliveryRun.unit.branch_mode === "shared" && steps < deliveryRun.remaining.length)
574
+ console.log(`foreman: --steps ${steps} completes ${steps} ticket(s) in this ${deliveryRun.remaining.length}-ticket remainder; branch/session is preserved and PR completion is deferred`);
575
+ if (deliveryRun.unit.dependency_mode === "stack")
576
+ console.log("foreman: stacked delivery — dependent branches must be rebased or retargeted after prerequisite merges");
577
+ }
448
578
  for (const node of plan.nodes) {
449
579
  console.log(` ${node.ticket.id} ${node.branch} base=${node.baseBranch}`);
450
580
  }
@@ -472,8 +602,8 @@ export function buildStartCommand() {
472
602
  log,
473
603
  agent,
474
604
  model,
475
- effort: opts.effort,
476
- fast: opts.fast,
605
+ effort: builderEffort,
606
+ fast: builderFast,
477
607
  notificationsEnabled: config.notifications.enabled,
478
608
  qaEnabled,
479
609
  createPr: branchDefaults.createReview,
@@ -484,6 +614,7 @@ export function buildStartCommand() {
484
614
  cleanupBranches: branchDefaults.cleanupBranches,
485
615
  autoMergeWait: branchDefaults.autoMergeWait,
486
616
  autoMergeTimeoutMinutes: branchDefaults.autoMergeTimeoutMinutes,
617
+ mergeMethod: branchDefaults.mergeMethod,
487
618
  allowedBaseDirtyPaths,
488
619
  trackerPaths: {
489
620
  progressDoc: ticketsConfig.paths.progressDoc,
@@ -525,12 +656,38 @@ export function buildStartCommand() {
525
656
  });
526
657
  agent = ready.runtime;
527
658
  model = ready.model;
659
+ agentExecutable = ready.executable;
660
+ if (qaEnabled) {
661
+ const qaReady = await ensureRuntimeReadyForCommand(cwd, qaAgent, { label: "independent QA", yes: Boolean(opts.yes), model: qaModel });
662
+ qaAgent = qaReady.runtime;
663
+ qaModel = qaReady.model;
664
+ qaExecutable = qaReady.executable;
665
+ }
666
+ const capturedBuilder = {
667
+ role: "builder", source: opts.agent || opts.model || opts.effort || opts.fast ? "cli" : roleDefaults.builder ? "project" : "provider",
668
+ make: agent, model: model ?? "default", reasoning: builderEffort ?? "default", fast: Boolean(builderFast),
669
+ };
670
+ const capturedQa = { role: "qa", source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default", reasoning: qaEffort ?? "default", fast: qaFast };
671
+ let buildRun = createBuildRun({
672
+ tickets: [], repositoryRoot: cwd, branchMode: "current", builder: capturedBuilder, qa: capturedQa,
673
+ });
674
+ const heartbeat = setInterval(() => { buildRun = heartbeatBuildRun(cwd, buildRun); }, 10_000);
675
+ heartbeat.unref();
676
+ const checkpointSignal = () => {
677
+ try {
678
+ buildRun = releaseBuildLease(cwd, buildRun, "interrupted");
679
+ }
680
+ catch { /* best effort */ }
681
+ };
682
+ process.once("SIGINT", checkpointSignal);
683
+ process.once("SIGTERM", checkpointSignal);
528
684
  const builder = await createBuilder(cwd, resumeSessionId);
529
- const foreman = new Foreman(builder, log, config.notifications.enabled, qaEnabled, 3, cwd);
685
+ const qaReviewer = qaEnabled ? await createQa(cwd) : undefined;
686
+ const foreman = new Foreman(builder, log, config.notifications.enabled, qaEnabled, 3, cwd, qaReviewer);
530
687
  const modifiers = [
531
688
  model ? `model=${model}` : null,
532
- opts.effort ? `effort=${opts.effort}` : null,
533
- opts.fast ? "fast" : null,
689
+ builderEffort ? `effort=${builderEffort}` : null,
690
+ builderFast ? "fast" : null,
534
691
  qaEnabled ? null : "qa=off",
535
692
  ].filter(Boolean).join(" ");
536
693
  console.log(`foreman: driving a ${agent} builder through ${steps} step(s)${modifiers ? ` [${modifiers}]` : ""}`);
@@ -541,7 +698,13 @@ export function buildStartCommand() {
541
698
  const viewer = printEvents(builder.events());
542
699
  try {
543
700
  console.log("ai-foreman: asking builder to plan the next tickets or steps...\n");
701
+ buildRun = checkpointBuildRun(cwd, buildRun, "before-preflight");
544
702
  await foreman.runPreflight(steps, ticketsContent);
703
+ if (builder.sessionId())
704
+ buildRun = persistBuildSession(cwd, buildRun, "builder", builder.sessionId());
705
+ if (qaReviewer?.sessionId())
706
+ buildRun = persistBuildSession(cwd, buildRun, "qa", qaReviewer.sessionId());
707
+ buildRun = checkpointBuildRun(cwd, buildRun, "preflight-complete");
545
708
  if (!opts.yes) {
546
709
  while (true) {
547
710
  console.log();
@@ -578,6 +741,11 @@ export function buildStartCommand() {
578
741
  }
579
742
  }
580
743
  const result = await foreman.runBatch(steps, trackerRelPath);
744
+ buildRun = result.outcome === "all-done" || result.outcome === "plan-complete"
745
+ ? completeBuildRun(cwd, buildRun)
746
+ : releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "builder-or-qa-interrupted", { status: "recoverable" }), "recoverable");
747
+ clearInterval(heartbeat);
748
+ await qaReviewer?.close().catch(() => { });
581
749
  await builder.close();
582
750
  await viewer;
583
751
  console.log(`\nforeman: ${result.completed}/${result.requested} step(s) completed`);
@@ -590,9 +758,32 @@ export function buildStartCommand() {
590
758
  process.exit(result.outcome === "needs-human" ? 2 : 0);
591
759
  }
592
760
  catch (err) {
761
+ clearInterval(heartbeat);
762
+ buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "failed", { status: "failed", failure: { category: "unknown", summary: err instanceof Error ? err.message.slice(0, 1000) : String(err).slice(0, 1000), at: new Date().toISOString() } }), "failed");
763
+ await qaReviewer?.close().catch(() => { });
593
764
  await builder.close().catch(() => { });
594
765
  log.write("error", { message: String(err) });
595
766
  fail(`run failed: ${err instanceof Error ? err.message : String(err)}`);
596
767
  }
597
768
  });
598
769
  }
770
+ function readAgentDefaults(projectDir) {
771
+ const path = join(projectDir, "rafi-config.yaml");
772
+ if (!existsSync(path))
773
+ return {};
774
+ try {
775
+ const parsed = parseYaml(readFileSync(path, "utf8"));
776
+ if (parsed?.agent_defaults?.version !== 1)
777
+ return {};
778
+ return { builder: parsed.agent_defaults.roles.builder, qa: parsed.agent_defaults.roles.qa };
779
+ }
780
+ catch {
781
+ return {};
782
+ }
783
+ }
784
+ function explicitDefaultValue(value) {
785
+ return value && value !== "default" ? value : undefined;
786
+ }
787
+ function explicitEffort(value) {
788
+ return value && ["low", "medium", "high", "xhigh"].includes(value) ? value : undefined;
789
+ }
@@ -1,2 +1,4 @@
1
1
  import { Command } from "commander";
2
2
  export declare function buildStatusCommand(): Command;
3
+ /** Print the existing Foreman status report for a resolved project. */
4
+ export declare function runStatus(project: string): void;