ai-foreman 1.7.4 → 1.7.6

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 (55) hide show
  1. package/README.md +6 -1
  2. package/dist/activity.d.ts +63 -0
  3. package/dist/activity.js +244 -0
  4. package/dist/adapters/claude.d.ts +7 -0
  5. package/dist/adapters/claude.js +41 -1
  6. package/dist/adapters/codex.d.ts +1 -0
  7. package/dist/adapters/codex.js +65 -5
  8. package/dist/adapters/recovering.js +7 -1
  9. package/dist/adapters/types.d.ts +15 -0
  10. package/dist/agentRun.d.ts +5 -0
  11. package/dist/agentRun.js +5 -1
  12. package/dist/branch/github.js +5 -3
  13. package/dist/branch/gitlab.js +5 -3
  14. package/dist/branch/planner.d.ts +5 -0
  15. package/dist/branch/planner.js +42 -3
  16. package/dist/branch/presentation.d.ts +17 -0
  17. package/dist/branch/presentation.js +38 -0
  18. package/dist/branch/resume.d.ts +2 -0
  19. package/dist/branch/resume.js +2 -0
  20. package/dist/branch/runner.js +44 -25
  21. package/dist/buildRuns.d.ts +41 -17
  22. package/dist/buildRuns.js +243 -22
  23. package/dist/cli/events.js +24 -5
  24. package/dist/cli/runtimeAuthPrompt.js +5 -1
  25. package/dist/cli/start.js +138 -45
  26. package/dist/cli/tickets.d.ts +10 -1
  27. package/dist/cli/tickets.js +418 -79
  28. package/dist/foreman.d.ts +8 -5
  29. package/dist/foreman.js +38 -14
  30. package/dist/index.js +3 -1
  31. package/dist/providerQuestions.d.ts +23 -0
  32. package/dist/providerQuestions.js +24 -0
  33. package/dist/qaReview.d.ts +1 -0
  34. package/dist/qaReview.js +23 -15
  35. package/dist/qaSnapshot.d.ts +14 -0
  36. package/dist/qaSnapshot.js +117 -1
  37. package/dist/runtimeReadiness.js +5 -0
  38. package/dist/sources/sourceRegistry.js +13 -6
  39. package/dist/tickets/details.d.ts +17 -0
  40. package/dist/tickets/details.js +94 -0
  41. package/dist/tickets/eligibility.d.ts +28 -0
  42. package/dist/tickets/eligibility.js +71 -0
  43. package/dist/tickets/importer.js +13 -8
  44. package/dist/tickets/reset.d.ts +27 -0
  45. package/dist/tickets/reset.js +213 -0
  46. package/dist/tickets/reviewStandards.d.ts +7 -0
  47. package/dist/tickets/reviewStandards.js +53 -0
  48. package/dist/tickets/setupConfig.d.ts +22 -0
  49. package/dist/tickets/setupConfig.js +105 -1
  50. package/dist/tickets/sourceFetch.js +19 -0
  51. package/dist/tickets/stateDb.d.ts +8 -1
  52. package/dist/tickets/stateDb.js +54 -23
  53. package/dist/workflowDb.d.ts +3 -0
  54. package/dist/workflowDb.js +26 -0
  55. package/package.json +15 -3
package/dist/cli/start.js CHANGED
@@ -17,19 +17,22 @@ import { resolveAgentForProject } from "./runtimeSelection.js";
17
17
  import { isTicketsInitialized, loadTicketsConfig, resolveTicketPaths } from "../tickets/config.js";
18
18
  import { loadTickets } from "../tickets/ticketLoader.js";
19
19
  import { StateDb } from "../tickets/stateDb.js";
20
- import { applySharedDeliveryBranch, buildBranchAuditInstruction, buildBranchPlan, parseAuditDependencies } from "../branch/planner.js";
21
- import { collectBaseWorktreeDirtyPaths, currentGitRef, ensureCleanBaseWorktree, generatedTrackerDirtyPaths } from "../branch/git.js";
20
+ import { applySharedDeliveryBranch, applySizeBranchPolicy, buildBranchAuditInstruction, buildBranchPlan, parseAuditDependencies } from "../branch/planner.js";
21
+ import { currentGitRef, ensureCleanBaseWorktree, generatedTrackerDirtyPaths } from "../branch/git.js";
22
22
  import { formatGitHubFailure, preflightGh } from "../branch/github.js";
23
23
  import { preflightGlab } from "../branch/gitlab.js";
24
24
  import { readDeliveryUnitSession, runBranchPlan } from "../branch/runner.js";
25
- import { checkpointBuildRun, completeBuildRun, createBuildRun, heartbeatBuildRun, persistBuildSession, readBuildRuns, releaseBuildLease, resumeBuildRun } from "../buildRuns.js";
25
+ import { branchPlanLogMetadata, presentBranchPlan } from "../branch/presentation.js";
26
+ import { checkpointBuildRun, completeBuildRun, createBuildRun, heartbeatBuildRun, persistBuildSession, projectBuildRecovery, readBuildRuns, releaseBuildLease, resumeBuildRun } from "../buildRuns.js";
26
27
  import { parse as parseYaml } from "yaml";
27
28
  import { findResumableBranchSessions, formatBranchContinueCommand, formatBranchSummaryFollowupCommands, } from "../branch/resume.js";
28
29
  import { detectGitProvider, loadTicketSetupConfig } from "../tickets/setupConfig.js";
29
30
  import { loadDeliveryConfig, selectDeliveryUnitForRun, selectStacksForRun, updateStackDeliveryState } from "../tickets/delivery.js";
30
31
  import { AgentStatusReporter } from "../statusReporter.js";
31
- import { WorkflowDb } from "../workflowDb.js";
32
+ import { currentActivity, withActivityPhase } from "../activity.js";
33
+ import { WorkflowDb, readCurrentWorkflowLease } from "../workflowDb.js";
32
34
  import { makeLogPath as makeRoleLogPath, readOnlyPermissionConfig, runRoleInstruction } from "../agentRun.js";
35
+ import { eligibleTickets, evaluateTicketEligibility } from "../tickets/eligibility.js";
33
36
  function fail(message) {
34
37
  console.error(`foreman: ${message}`);
35
38
  process.exit(1);
@@ -185,7 +188,10 @@ function branchContinueTicketHelp(cwd, flag = "--continue") {
185
188
  }
186
189
  async function ensureGitHubReadyForCreatePr(cwd, log, yes) {
187
190
  while (true) {
188
- const result = preflightGh(cwd);
191
+ const result = await withActivityPhase("checking GitHub readiness", async () => {
192
+ currentActivity()?.update("checking GitHub readiness", "remote, authentication, and repository access");
193
+ return preflightGh(cwd);
194
+ });
189
195
  if (result.ok)
190
196
  return;
191
197
  log.write("github-readiness-failed", {
@@ -211,6 +217,7 @@ async function ensureGitHubReadyForCreatePr(cwd, log, yes) {
211
217
  console.log("ai-foreman: cancelled");
212
218
  process.exit(0);
213
219
  }
220
+ currentActivity()?.note("rafi: retrying GitHub readiness check");
214
221
  }
215
222
  }
216
223
  async function ensureReviewProviderReady(cwd, provider, log, yes) {
@@ -219,7 +226,10 @@ async function ensureReviewProviderReady(cwd, provider, log, yes) {
219
226
  return;
220
227
  }
221
228
  while (true) {
222
- const result = preflightGlab(cwd);
229
+ const result = await withActivityPhase("checking GitLab readiness", async () => {
230
+ currentActivity()?.update("checking GitLab readiness", "remote, authentication, and repository access");
231
+ return preflightGlab(cwd);
232
+ });
223
233
  if (result.ok)
224
234
  return;
225
235
  log.write("gitlab-readiness-failed", {
@@ -245,6 +255,7 @@ async function ensureReviewProviderReady(cwd, provider, log, yes) {
245
255
  console.log("ai-foreman: cancelled");
246
256
  process.exit(0);
247
257
  }
258
+ currentActivity()?.note("rafi: retrying GitLab readiness check");
248
259
  }
249
260
  }
250
261
  export function buildStartCommand() {
@@ -274,13 +285,21 @@ export function buildStartCommand() {
274
285
  .option("--no-auto-merge-wait", "do not wait for dependency PR/MRs before dependent tickets")
275
286
  .option("--auto-merge-timeout-minutes <n>", "auto-merge dependency wait timeout in minutes (blank means no timeout)")
276
287
  .option("--base <ref>", "base ref for root ticket branches (default: current branch or HEAD)")
277
- .option("--branch-prefix <prefix>", "branch name prefix for ticket branches", "rafi")
288
+ .option("--branch-prefix <prefix>", "branch name prefix for ticket branches", "feature")
278
289
  .option("--max-branch-depth <n>", "maximum selected branch stack depth", "5")
279
290
  .option("--pr-ready", "create ready-for-review PRs instead of draft PRs")
280
291
  .option("--keep-worktrees", "keep successful ticket worktrees for inspection")
281
- .option("--ticket <id>", "ticket id to continue in branch mode; repeat for multiple tickets", collectTicket, [])
292
+ .option("--ticket <id>", "select one new ticket, or identify recovery tickets with --resume/--continue/--recover-run", collectTicket, [])
282
293
  .option("--skip-delivery-unit <id>", "skip one unfinished delivery unit for this run", collectTicket, [])
283
294
  .action(async (project, opts, command) => {
295
+ const requestedTicketIds = opts.ticket ?? [];
296
+ const recoveryIntent = Boolean(opts.resume || opts.continue || opts.recoverRun);
297
+ if (!recoveryIntent && requestedTicketIds.length > 1)
298
+ fail("new-ticket selection supports exactly one --ticket");
299
+ if (!recoveryIntent && requestedTicketIds.length === 1 && opts.steps === undefined && opts.stacks === undefined)
300
+ opts.steps = "1";
301
+ if (!recoveryIntent && requestedTicketIds.length && opts.stacks !== undefined)
302
+ fail("new-ticket selection cannot be combined with --stacks");
284
303
  if (opts.steps !== undefined && opts.stacks !== undefined)
285
304
  fail("--steps and --stacks are mutually exclusive");
286
305
  if (opts.steps === undefined && opts.stacks === undefined) {
@@ -409,13 +428,14 @@ export function buildStartCommand() {
409
428
  fail("choose either --resume <sessionId> or --continue, not both");
410
429
  }
411
430
  const branchMode = branchDefaults.branchMode;
412
- const continueTickets = opts.ticket ?? [];
431
+ const continueTickets = recoveryIntent ? requestedTicketIds : [];
432
+ let selectedNewTicket = !recoveryIntent ? requestedTicketIds[0] : undefined;
413
433
  const maxBranchDepth = Number.parseInt(opts.maxBranchDepth, 10);
414
434
  if (!Number.isInteger(maxBranchDepth) || maxBranchDepth < 1) {
415
435
  fail("--max-branch-depth must be a positive integer");
416
436
  }
417
- if (!branchMode && continueTickets.length > 0) {
418
- fail("--ticket is only supported with --branch-per-ticket --continue or --branch-per-ticket --resume");
437
+ if (!branchMode && continueTickets.length > 1) {
438
+ fail("current-branch build recovery supports exactly one --ticket");
419
439
  }
420
440
  if (branchMode) {
421
441
  if (opts.resume && continueTickets.length > 1) {
@@ -424,13 +444,10 @@ export function buildStartCommand() {
424
444
  if ((opts.resume || opts.continue) && continueTickets.length === 0 && !recoveryRecord) {
425
445
  fail(branchContinueTicketHelp(cwd, opts.resume ? "--resume" : "--continue"));
426
446
  }
427
- if (continueTickets.length > 0 && !(opts.resume || opts.continue)) {
428
- fail("--ticket in branch mode requires --continue or --resume <sessionId>");
429
- }
430
447
  if (opts.tickets)
431
- fail("--tickets is not supported with --branch-per-ticket; initialize and use .tickets/tickets.yaml");
448
+ fail("--tickets is not supported with an isolated branch run; initialize and use .tickets/tickets.yaml");
432
449
  if (!isTicketsInitialized(cwd))
433
- fail("--branch-per-ticket requires initialized .tickets/ (run ai-foreman tickets init)");
450
+ fail("isolated branch runs require initialized .tickets/ (run ai-foreman tickets init)");
434
451
  if (branchDefaults.createReview && !branchDefaults.reviewProvider) {
435
452
  fail("PR/MR completion is enabled but no GitHub or GitLab origin remote was detected; pass --provider github|gitlab or --completion none");
436
453
  }
@@ -440,6 +457,60 @@ export function buildStartCommand() {
440
457
  if (!branchMode && opts.continue && !resumeSessionId) {
441
458
  fail(`no previous session id found under ${join(cwd, ".foreman")}`);
442
459
  }
460
+ if (selectedNewTicket) {
461
+ if (!isTicketsInitialized(cwd))
462
+ fail("--ticket requires initialized .tickets/ (run `rafi tickets init`)");
463
+ const eligibilityConfig = loadTicketsConfig(cwd);
464
+ const eligibilityPaths = resolveTicketPaths(eligibilityConfig, cwd);
465
+ const eligibilityDefs = loadTickets(eligibilityPaths.tickets);
466
+ const eligibilityDb = new StateDb(eligibilityPaths.stateDb);
467
+ const eligibilityStates = eligibilityDb.getAllStates();
468
+ eligibilityDb.close();
469
+ const activeLease = readCurrentWorkflowLease(cwd);
470
+ let assessment = evaluateTicketEligibility({
471
+ tickets: eligibilityDefs,
472
+ states: eligibilityStates,
473
+ implementationLimit: eligibilityConfig.implementationLimit,
474
+ delivery: deliveryConfig,
475
+ activeLease: activeLease ? { runId: activeLease.runId } : undefined,
476
+ }, selectedNewTicket);
477
+ if (!assessment.eligible) {
478
+ console.error(`foreman: ${selectedNewTicket} is not eligible:`);
479
+ for (const blocker of assessment.blockers)
480
+ console.error(` - ${blocker.detail}`);
481
+ const dependency = assessment.eligibleDependencies[0];
482
+ if (!dependency || opts.yes || !process.stdin.isTTY || !process.stdout.isTTY)
483
+ fail(`cannot start ${selectedNewTicket}`);
484
+ const answer = await select({ message: `Start eligible dependency ${dependency.id}: ${dependency.title} instead?`, options: [
485
+ { value: "dependency", label: `Start ${dependency.id} (Recommended)` },
486
+ { value: "cancel", label: "Cancel" },
487
+ ] });
488
+ if (isCancel(answer) || answer === "cancel") {
489
+ console.log("ai-foreman: cancelled; nothing changed");
490
+ return;
491
+ }
492
+ selectedNewTicket = dependency.id;
493
+ assessment = evaluateTicketEligibility({ tickets: eligibilityDefs, states: eligibilityStates, implementationLimit: eligibilityConfig.implementationLimit, delivery: deliveryConfig }, selectedNewTicket);
494
+ }
495
+ const recommended = eligibleTickets({ tickets: eligibilityDefs, states: eligibilityStates, implementationLimit: eligibilityConfig.implementationLimit, delivery: deliveryConfig })[0];
496
+ if (assessment.eligible && recommended && recommended.id !== selectedNewTicket) {
497
+ console.log(`foreman: recommended ${recommended.id}: ${recommended.title}`);
498
+ console.log(`foreman: requested ${assessment.ticket.id}: ${assessment.ticket.title}`);
499
+ if (!opts.yes) {
500
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
501
+ fail(`starting non-recommended ${selectedNewTicket} requires --yes`);
502
+ const answer = await select({ message: `Start ${selectedNewTicket} anyway?`, options: [
503
+ { value: "start", label: `Start ${selectedNewTicket}` }, { value: "cancel", label: "Cancel" },
504
+ ] });
505
+ if (isCancel(answer) || answer === "cancel") {
506
+ console.log("ai-foreman: cancelled; nothing changed");
507
+ return;
508
+ }
509
+ }
510
+ }
511
+ }
512
+ const recoveryTicket = !branchMode && recoveryRecord ? continueTickets[0] : undefined;
513
+ const preferredTicket = selectedNewTicket ?? recoveryTicket;
443
514
  const config = loadConfig(join(cwd, "foreman.yaml"));
444
515
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
445
516
  const logPath = join(cwd, ".foreman", `${stamp}.jsonl`);
@@ -531,9 +602,15 @@ export function buildStartCommand() {
531
602
  const allowedBaseDirtyPaths = generatedTrackerDirtyPaths(ticketsConfig.paths);
532
603
  const baseWorktreePolicy = recoveryRecord ? "skip" : "enforce";
533
604
  if (baseWorktreePolicy === "skip") {
534
- const dirty = collectBaseWorktreeDirtyPaths(cwd, { allowedDirtyPaths: allowedBaseDirtyPaths });
535
- if (dirty.length > 0) {
536
- console.warn(`foreman: warning: base worktree has uncommitted changes; build recovery will continue.\n${dirty.join("\n")}`);
605
+ const projection = recoveryRecord ? projectBuildRecovery(cwd, recoveryRecord) : undefined;
606
+ if (projection?.expectedChanges.length) {
607
+ console.log(`foreman: preserved expected in-progress work: ${projection.expectedChanges.join(", ")}`);
608
+ }
609
+ if (projection?.unexpectedChanges.length) {
610
+ console.warn("foreman: warning: unexpected changes may conflict with recovery; recovery is still allowed:");
611
+ for (const item of projection.unexpectedChanges)
612
+ console.warn(` ${item.path}: ${item.risk}`);
613
+ console.warn("foreman: recommended action: preserve or move overlapping changes before continuing");
537
614
  }
538
615
  }
539
616
  else {
@@ -563,6 +640,7 @@ export function buildStartCommand() {
563
640
  db.close();
564
641
  const ticketById = new Map(tickets.map((ticket) => [ticket.id, ticket]));
565
642
  const resumeSessionByTicket = new Map();
643
+ let auditDependencyCount;
566
644
  let plan;
567
645
  if (continueTickets.length > 0) {
568
646
  const sessions = findResumableBranchSessions(join(cwd, ".foreman"));
@@ -589,6 +667,8 @@ export function buildStartCommand() {
589
667
  dependencies: [],
590
668
  depth: 1,
591
669
  worktreePath: session.worktreePath,
670
+ deliveryUnitId: session.deliveryUnitId,
671
+ deliveryUnitFinal: session.deliveryUnitFinal,
592
672
  });
593
673
  }
594
674
  plan = {
@@ -596,22 +676,15 @@ export function buildStartCommand() {
596
676
  nodes,
597
677
  issues: [],
598
678
  };
599
- log.write("branch-plan", {
600
- baseRef: plan.baseRef,
601
- tickets: plan.nodes.map((node) => node.ticket.id),
602
- branches: plan.nodes.map((node) => ({ ticket: node.ticket.id, branch: node.branch, base: node.baseBranch })),
603
- issues: plan.issues,
604
- resume: true,
605
- });
606
679
  }
607
680
  else {
608
681
  plan = buildBranchPlan(tickets, states, {
609
682
  steps,
610
- baseRef: opts.base ?? currentGitRef(cwd),
683
+ baseRef: opts.base ?? loadTicketSetupConfig(cwd)?.build.base_branch ?? currentGitRef(cwd),
611
684
  branchPrefix: opts.branchPrefix,
612
685
  maxBranchDepth,
613
686
  rootBaseBranches: branchDefaults.rootBaseBranches,
614
- ticketIds: selectedStackTickets ?? deliveryRun?.remaining.slice(0, steps) ?? recoveryRecord?.tickets,
687
+ ticketIds: selectedNewTicket ? [selectedNewTicket] : selectedStackTickets ?? deliveryRun?.remaining.slice(0, steps) ?? recoveryRecord?.tickets,
615
688
  });
616
689
  let auditBuilder;
617
690
  let auditViewer;
@@ -624,20 +697,14 @@ export function buildStartCommand() {
624
697
  const auditDependencies = parseAuditDependencies(audit.text);
625
698
  plan = buildBranchPlan(tickets, states, {
626
699
  steps,
627
- baseRef: opts.base ?? currentGitRef(cwd),
700
+ baseRef: opts.base ?? loadTicketSetupConfig(cwd)?.build.base_branch ?? currentGitRef(cwd),
628
701
  branchPrefix: opts.branchPrefix,
629
702
  maxBranchDepth,
630
703
  auditDependencies,
631
704
  rootBaseBranches: branchDefaults.rootBaseBranches,
632
- ticketIds: selectedStackTickets ?? deliveryRun?.remaining.slice(0, steps) ?? recoveryRecord?.tickets,
633
- });
634
- log.write("branch-plan", {
635
- baseRef: plan.baseRef,
636
- tickets: plan.nodes.map((node) => node.ticket.id),
637
- branches: plan.nodes.map((node) => ({ ticket: node.ticket.id, branch: node.branch, base: node.baseBranch })),
638
- issues: plan.issues,
639
- auditDependencyCount: auditDependencies.length,
705
+ ticketIds: selectedNewTicket ? [selectedNewTicket] : selectedStackTickets ?? deliveryRun?.remaining.slice(0, steps) ?? recoveryRecord?.tickets,
640
706
  });
707
+ auditDependencyCount = auditDependencies.length;
641
708
  }
642
709
  finally {
643
710
  await auditBuilder?.close().catch(() => { });
@@ -654,13 +721,27 @@ export function buildStartCommand() {
654
721
  if (selectedStacks.length && deliveryConfig) {
655
722
  plan = applyExplicitStackTopology(plan, selectedStacks, deliveryConfig, opts.branchPrefix);
656
723
  }
724
+ else if (!deliveryRun) {
725
+ const branchPolicy = loadTicketSetupConfig(cwd)?.build.branch_policy;
726
+ if (branchPolicy)
727
+ plan = applySizeBranchPolicy(plan, branchPolicy, deliveryConfig, opts.branchPrefix);
728
+ }
657
729
  if (recoveryRecord && opts.resume && plan.nodes[0]) {
658
730
  const prior = findResumableBranchSessions(join(cwd, ".foreman")).find((session) => session.ticket === (recoveryRecord.currentTicket ?? plan.nodes[0].ticket.id));
659
731
  if (prior)
660
732
  resumeSessionByTicket.set(plan.nodes[0].ticket.id, { worktreePath: prior.worktreePath, sessionId: opts.resume });
661
733
  }
662
734
  }
663
- console.log(`foreman: ${continueTickets.length > 0 ? "resuming branch-per-ticket mode" : "branch-per-ticket mode"} for ${plan.nodes.length} ticket(s)`);
735
+ const branchPresentation = presentBranchPlan(plan, {
736
+ stacked: selectedStacks.length > 0,
737
+ resumed: continueTickets.length > 0 || Boolean(recoveryRecord),
738
+ });
739
+ log.write("branch-plan", {
740
+ ...branchPlanLogMetadata(plan, branchPresentation),
741
+ ...(auditDependencyCount === undefined ? {} : { auditDependencyCount }),
742
+ ...(continueTickets.length > 0 || recoveryRecord ? { resume: true } : {}),
743
+ });
744
+ console.log(branchPresentation.banner);
664
745
  console.log(`foreman: project ${cwd}`);
665
746
  console.log(`foreman: base ${plan.baseRef}`);
666
747
  console.log(`foreman: log ${logPath}\n`);
@@ -680,7 +761,7 @@ export function buildStartCommand() {
680
761
  console.log();
681
762
  if (!opts.yes && plan.issues.every((issue) => !issue.blocking)) {
682
763
  const action = await select({
683
- message: "Proceed with branch-per-ticket run?",
764
+ message: branchPresentation.prompt,
684
765
  options: [
685
766
  { value: "proceed", label: "Proceed" },
686
767
  { value: "cancel", label: "Cancel" },
@@ -702,7 +783,7 @@ export function buildStartCommand() {
702
783
  };
703
784
  let masterRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { builder: capturedBranchBuilder, qa: capturedBranchQa, builderSessionId: opts.resume ? String(opts.resume) : null }) : createBuildRun({
704
785
  tickets: plan.nodes.map((node) => node.ticket.id), deliveryUnit: selectedStacks.length ? selectedStacks.map((stack) => stack.id).join(",") : deliveryRun?.unit.id,
705
- repositoryRoot: cwd, branchMode: "per-ticket", builder: capturedBranchBuilder, qa: capturedBranchQa,
786
+ repositoryRoot: cwd, branchMode: branchPresentation.allocationMode, baseRef: plan.baseRef, builder: capturedBranchBuilder, qa: capturedBranchQa,
706
787
  });
707
788
  const branchHeartbeat = setInterval(() => { masterRun = heartbeatBuildRun(cwd, masterRun); }, 10_000);
708
789
  branchHeartbeat.unref();
@@ -808,7 +889,7 @@ export function buildStartCommand() {
808
889
  };
809
890
  const capturedQa = { role: "qa", source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default", reasoning: qaEffort ?? "default", fast: qaFast, session_strategy: roleDefaults.qa?.session_strategy ?? "compact", settings_revision: settingsRevision };
810
891
  let buildRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { builder: capturedBuilder, qa: capturedQa, builderSessionId: opts.resume ? String(opts.resume) : null }) : createBuildRun({
811
- tickets: [], repositoryRoot: cwd, branchMode: "current", builder: capturedBuilder, qa: capturedQa,
892
+ tickets: [], repositoryRoot: cwd, branchMode: "current", baseRef: opts.base ?? loadTicketSetupConfig(cwd)?.build.base_branch, builder: capturedBuilder, qa: capturedQa,
812
893
  });
813
894
  const heartbeat = setInterval(() => { buildRun = heartbeatBuildRun(cwd, buildRun); }, 10_000);
814
895
  heartbeat.unref();
@@ -827,7 +908,11 @@ export function buildStartCommand() {
827
908
  reasoning: capturedBuilder.reasoning, fast: capturedBuilder.fast, step: 1, total: steps,
828
909
  phase: "builder work session", sessionTransition: resumeSessionId ? "resumed exact session" : "initial session", adapter: () => foreman.builderAdapter(),
829
910
  }, (line, snapshot) => {
830
- console.log(line);
911
+ const activity = currentActivity();
912
+ if (activity)
913
+ activity.writePersistent(line);
914
+ else
915
+ console.log(line);
831
916
  log.write("agent-status", { ...snapshot });
832
917
  const db = new WorkflowDb(cwd);
833
918
  try {
@@ -853,7 +938,7 @@ export function buildStartCommand() {
853
938
  try {
854
939
  console.log("ai-foreman: asking builder to plan the next tickets or steps...\n");
855
940
  buildRun = checkpointBuildRun(cwd, buildRun, "before-preflight");
856
- await foreman.runPreflight(steps, ticketsContent);
941
+ await foreman.runPreflight(steps, ticketsContent, preferredTicket);
857
942
  if (foreman.builderSessionId())
858
943
  buildRun = persistBuildSession(cwd, buildRun, "builder", foreman.builderSessionId());
859
944
  if (foreman.qaSessionId())
@@ -896,7 +981,15 @@ export function buildStartCommand() {
896
981
  await foreman.sendPreflightFeedback(String(fb));
897
982
  }
898
983
  }
899
- const result = await foreman.runBatch(steps, trackerRelPath);
984
+ const result = await foreman.runBatch(steps, trackerRelPath, (ticketId) => {
985
+ const tickets = buildRun.tickets.includes(ticketId)
986
+ ? buildRun.tickets
987
+ : [...buildRun.tickets, ticketId];
988
+ buildRun = checkpointBuildRun(cwd, buildRun, "ticket-selected", {
989
+ tickets,
990
+ currentTicket: ticketId,
991
+ });
992
+ }, preferredTicket);
900
993
  if (foreman.qaSessionId())
901
994
  buildRun = persistBuildSession(cwd, buildRun, "qa", foreman.qaSessionId());
902
995
  buildRun = result.outcome === "all-done" || result.outcome === "plan-complete"
@@ -1045,7 +1138,7 @@ export function applyExplicitStackTopology(plan, stacks, delivery, branchPrefix)
1045
1138
  const unit = unitById.get(unitId);
1046
1139
  if (!unit)
1047
1140
  throw new Error(`stack ${stack.id} references missing delivery unit ${unitId}`);
1048
- const sharedBranch = unit.branch_mode === "shared" ? `${branchPrefix.replace(/^\/+|\/+$/g, "") || "rafi"}/${slug(unitId)}` : undefined;
1141
+ const sharedBranch = unit.branch_mode === "shared" ? `${branchPrefix || "feature"}/${slug(unitId)}` : undefined;
1049
1142
  const unitBaseBranch = predecessorBranch;
1050
1143
  for (let ticketIndex = 0; ticketIndex < unit.tickets.length; ticketIndex++) {
1051
1144
  const ticketId = unit.tickets[ticketIndex];
@@ -24,6 +24,15 @@ interface PopulateCommandOptions {
24
24
  yes?: boolean;
25
25
  authorizeRetire?: string[];
26
26
  }
27
+ export declare function combineRequirementSelections(selected: string[], additions: string): string[];
27
28
  export declare function cmdPopulateCli(opts: PopulateCommandOptions): Promise<void>;
28
- export declare function buildTicketsCommand(): Command;
29
+ export interface TicketOwnershipReceipt {
30
+ path: string;
31
+ category: "tickets";
32
+ origin: string;
33
+ }
34
+ export declare function buildTicketsCommand(options?: {
35
+ prepareOwnership?: (projectDir: string, receipts: TicketOwnershipReceipt[]) => void;
36
+ recordOwnership?: (projectDir: string, receipts: TicketOwnershipReceipt[]) => void;
37
+ }): Command;
29
38
  export {};