ai-foreman 1.7.20 → 1.7.22

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 (202) hide show
  1. package/README.md +12 -2
  2. package/dist/activity.d.ts +3 -0
  3. package/dist/activity.js +27 -8
  4. package/dist/adapters/claude.d.ts +19 -0
  5. package/dist/adapters/claude.js +59 -7
  6. package/dist/adapters/codex.d.ts +20 -0
  7. package/dist/adapters/codex.js +99 -5
  8. package/dist/adapters/recovering.d.ts +9 -0
  9. package/dist/adapters/recovering.js +10 -0
  10. package/dist/adapters/types.d.ts +15 -0
  11. package/dist/agentRun.d.ts +7 -0
  12. package/dist/agentRun.js +24 -13
  13. package/dist/branch/currentGuard.d.ts +1 -0
  14. package/dist/branch/currentGuard.js +1 -0
  15. package/dist/branch/dirtyBaseline.d.ts +5 -0
  16. package/dist/branch/dirtyBaseline.js +69 -0
  17. package/dist/branch/finalization.js +6 -1
  18. package/dist/branch/git.d.ts +1 -0
  19. package/dist/branch/git.js +15 -1
  20. package/dist/branch/resume.js +2 -2
  21. package/dist/branch/runner.d.ts +2 -0
  22. package/dist/branch/runner.js +90 -4
  23. package/dist/buildAdmission.js +2 -0
  24. package/dist/buildAssignment.d.ts +33 -0
  25. package/dist/buildAssignment.js +215 -0
  26. package/dist/buildInterventions.d.ts +90 -0
  27. package/dist/buildInterventions.js +334 -0
  28. package/dist/buildOwnershipReconciliation.d.ts +19 -0
  29. package/dist/buildOwnershipReconciliation.js +41 -0
  30. package/dist/buildOwnershipRepair.d.ts +33 -0
  31. package/dist/buildOwnershipRepair.js +111 -0
  32. package/dist/buildRuns.d.ts +6 -0
  33. package/dist/buildRuns.js +23 -17
  34. package/dist/buildSyntheticWork.d.ts +2 -0
  35. package/dist/buildSyntheticWork.js +30 -0
  36. package/dist/buildWorkAdmission.d.ts +17 -0
  37. package/dist/buildWorkAdmission.js +57 -0
  38. package/dist/buildWorkAuthorization.d.ts +2 -0
  39. package/dist/buildWorkAuthorization.js +23 -0
  40. package/dist/buildWorkContext.d.ts +3 -0
  41. package/dist/buildWorkContext.js +6 -0
  42. package/dist/buildWorkMigration.d.ts +9 -0
  43. package/dist/buildWorkMigration.js +215 -0
  44. package/dist/buildWorkReconciliation.d.ts +12 -0
  45. package/dist/buildWorkReconciliation.js +70 -0
  46. package/dist/builderGuidanceFollowup.d.ts +15 -0
  47. package/dist/builderGuidanceFollowup.js +34 -0
  48. package/dist/cli/doctor.js +7 -2
  49. package/dist/cli/graph.d.ts +2 -0
  50. package/dist/cli/graph.js +75 -0
  51. package/dist/cli/manager.d.ts +8 -0
  52. package/dist/cli/manager.js +235 -13
  53. package/dist/cli/recovery.js +3 -2
  54. package/dist/cli/runtimeAuthPrompt.js +28 -8
  55. package/dist/cli/start.js +192 -50
  56. package/dist/cli/status.js +29 -7
  57. package/dist/cli/tickets.d.ts +4 -0
  58. package/dist/cli/tickets.js +37 -13
  59. package/dist/continuity.d.ts +11 -0
  60. package/dist/continuity.js +22 -6
  61. package/dist/foreman.d.ts +10 -2
  62. package/dist/foreman.js +330 -39
  63. package/dist/graph/boundary.d.ts +15 -0
  64. package/dist/graph/boundary.js +50 -0
  65. package/dist/graph/bridge.d.ts +9 -0
  66. package/dist/graph/bridge.js +146 -0
  67. package/dist/graph/bridge.py +205 -0
  68. package/dist/graph/config.d.ts +11 -0
  69. package/dist/graph/config.js +40 -0
  70. package/dist/graph/corpus.d.ts +16 -0
  71. package/dist/graph/corpus.js +188 -0
  72. package/dist/graph/derived.d.ts +17 -0
  73. package/dist/graph/derived.js +73 -0
  74. package/dist/graph/host.d.ts +4 -0
  75. package/dist/graph/host.js +160 -0
  76. package/dist/graph/install.d.ts +3 -0
  77. package/dist/graph/install.js +50 -0
  78. package/dist/graph/lifecycle.d.ts +11 -0
  79. package/dist/graph/lifecycle.js +124 -0
  80. package/dist/graph/maintenance.d.ts +43 -0
  81. package/dist/graph/maintenance.js +301 -0
  82. package/dist/graph/manager.d.ts +9 -0
  83. package/dist/graph/manager.js +92 -0
  84. package/dist/graph/preparation.d.ts +41 -0
  85. package/dist/graph/preparation.js +43 -0
  86. package/dist/graph/read.d.ts +27 -0
  87. package/dist/graph/read.js +152 -0
  88. package/dist/graph/semantic.d.ts +17 -0
  89. package/dist/graph/semantic.js +27 -0
  90. package/dist/graph/session.d.ts +8 -0
  91. package/dist/graph/session.js +92 -0
  92. package/dist/graph/sources.d.ts +4 -0
  93. package/dist/graph/sources.js +11 -0
  94. package/dist/graph/storage.d.ts +38 -0
  95. package/dist/graph/storage.js +159 -0
  96. package/dist/graph/turn.d.ts +17 -0
  97. package/dist/graph/turn.js +360 -0
  98. package/dist/graph/util.d.ts +5 -0
  99. package/dist/graph/util.js +51 -0
  100. package/dist/handoffs.d.ts +1 -0
  101. package/dist/handoffs.js +5 -1
  102. package/dist/humanDecision.d.ts +21 -2
  103. package/dist/humanDecision.js +169 -11
  104. package/dist/index.js +2 -0
  105. package/dist/installOwnership.d.ts +46 -0
  106. package/dist/installOwnership.js +440 -0
  107. package/dist/interviews.d.ts +3 -1
  108. package/dist/interviews.js +7 -0
  109. package/dist/linuxProbeInventory.d.ts +12 -0
  110. package/dist/linuxProbeInventory.js +48 -0
  111. package/dist/log.d.ts +1 -1
  112. package/dist/log.js +7 -1
  113. package/dist/managerEvidence.d.ts +25 -0
  114. package/dist/managerEvidence.js +326 -0
  115. package/dist/managerEvidenceArtifacts.d.ts +38 -0
  116. package/dist/managerEvidenceArtifacts.js +118 -0
  117. package/dist/managerPacket.d.ts +2 -0
  118. package/dist/managerPacket.js +25 -1
  119. package/dist/markers.d.ts +1 -1
  120. package/dist/markers.js +2 -2
  121. package/dist/observability.js +1 -1
  122. package/dist/processIdentity.js +7 -0
  123. package/dist/projectDiagnostics.js +32 -8
  124. package/dist/providerPhase.d.ts +13 -0
  125. package/dist/providerPhase.js +26 -0
  126. package/dist/qaAuthorizedDisposition.d.ts +24 -0
  127. package/dist/qaAuthorizedDisposition.js +26 -0
  128. package/dist/qaBuildGate.d.ts +4 -0
  129. package/dist/qaBuildGate.js +216 -0
  130. package/dist/qaBuilderCoverage.d.ts +5 -0
  131. package/dist/qaBuilderCoverage.js +78 -0
  132. package/dist/qaContractCoverage.d.ts +10 -0
  133. package/dist/qaContractCoverage.js +55 -0
  134. package/dist/qaContractDelivery.d.ts +23 -0
  135. package/dist/qaContractDelivery.js +145 -0
  136. package/dist/qaContractFreshness.d.ts +15 -0
  137. package/dist/qaContractFreshness.js +36 -0
  138. package/dist/qaDeliveryJournal.d.ts +10 -0
  139. package/dist/qaDeliveryJournal.js +16 -1
  140. package/dist/qaEffectiveConfig.d.ts +20 -0
  141. package/dist/qaEffectiveConfig.js +40 -0
  142. package/dist/qaEvidenceReader.d.ts +14 -0
  143. package/dist/qaEvidenceReader.js +176 -0
  144. package/dist/qaFailureDelivery.d.ts +2 -0
  145. package/dist/qaFailureDelivery.js +107 -10
  146. package/dist/qaHandbackMigration.d.ts +1 -1
  147. package/dist/qaHandbackMigration.js +3 -3
  148. package/dist/qaPreparation.d.ts +26 -0
  149. package/dist/qaPreparation.js +150 -0
  150. package/dist/qaPreparationInputs.d.ts +8 -0
  151. package/dist/qaPreparationInputs.js +47 -0
  152. package/dist/qaPreparationMetrics.d.ts +145 -0
  153. package/dist/qaPreparationMetrics.js +120 -0
  154. package/dist/qaPreparationPolicy.d.ts +5 -0
  155. package/dist/qaPreparationPolicy.js +51 -0
  156. package/dist/qaPreparationStore.d.ts +157 -0
  157. package/dist/qaPreparationStore.js +412 -0
  158. package/dist/qaProtocolV2.d.ts +28 -0
  159. package/dist/qaProtocolV2.js +12 -0
  160. package/dist/qaReview.d.ts +6 -0
  161. package/dist/qaReview.js +234 -24
  162. package/dist/qaTimeline.d.ts +18 -0
  163. package/dist/qaTimeline.js +123 -0
  164. package/dist/qaVerificationContract.d.ts +17 -0
  165. package/dist/qaVerificationContract.js +236 -0
  166. package/dist/readinessCleanup.js +12 -5
  167. package/dist/recoveryGuidance.d.ts +9 -0
  168. package/dist/recoveryGuidance.js +13 -0
  169. package/dist/recoveryPolicy.js +1 -1
  170. package/dist/runtimeAuth.d.ts +1 -0
  171. package/dist/runtimeAuth.js +5 -1
  172. package/dist/runtimeReadiness.d.ts +1 -0
  173. package/dist/runtimeReadiness.js +13 -1
  174. package/dist/sessionIdentity.d.ts +2 -0
  175. package/dist/sessionIdentity.js +14 -0
  176. package/dist/sessionLifecycle.js +3 -3
  177. package/dist/sources/sourceRegistry.js +7 -0
  178. package/dist/stateTransfer.js +50 -1
  179. package/dist/statusReporter.d.ts +1 -0
  180. package/dist/statusReporter.js +1 -0
  181. package/dist/supervisedStart.js +19 -8
  182. package/dist/terminalStyle.d.ts +21 -0
  183. package/dist/terminalStyle.js +50 -0
  184. package/dist/ticketPopulation.d.ts +1 -0
  185. package/dist/ticketPopulation.js +5 -1
  186. package/dist/tickets/commands.d.ts +1 -1
  187. package/dist/tickets/commands.js +13 -11
  188. package/dist/tickets/details.js +2 -2
  189. package/dist/tickets/queue.js +1 -1
  190. package/dist/tickets/stateDb.d.ts +4 -1
  191. package/dist/tickets/stateDb.js +18 -2
  192. package/dist/tickets/ticketSchema.d.ts +198 -0
  193. package/dist/tickets/ticketSchema.js +2 -0
  194. package/dist/tickets/validate.js +4 -1
  195. package/dist/workflowDb.d.ts +51 -5
  196. package/dist/workflowDb.js +338 -7
  197. package/dist/workflowReader.d.ts +109 -2
  198. package/dist/workflowReader.js +48 -4
  199. package/package.json +85 -8
  200. package/scripts/copy-graph-assets.mjs +3 -0
  201. package/scripts/graph-benchmark.mjs +20 -0
  202. package/scripts/graph-evaluation.mjs +85 -0
package/dist/cli/start.js CHANGED
@@ -1,12 +1,18 @@
1
+ import { finalQaRoleInstructions, resolveEffectiveQaConfiguration } from "../qaEffectiveConfig.js";
2
+ import { graphContext, sendGraphTurn } from "../graph/turn.js";
3
+ import { initializeSyntheticWork } from "../buildSyntheticWork.js";
4
+ import { admitApprovedTicket } from "../buildWorkAuthorization.js";
1
5
  import { WorkflowReader } from "../workflowReader.js";
2
6
  import { localBuildAuthority, canonicalProject, withBuildInvocation } from "../buildAdmission.js";
3
- import { formatRecoveryCommand } from "../recoveryGuidance.js";
7
+ import { formatRecoveryCommand, formatDecisionCommands, formatExactRunRecovery } from "../recoveryGuidance.js";
4
8
  import { durableReadOnlyProposal } from "../readOnlyProposal.js";
5
9
  import { captureFrozenQaSource } from "../qaSnapshot.js";
6
10
  import { finishStartAdmission, superviseStart, supervisedRunId } from "../supervisedStart.js";
7
- import { durableHumanDecision, HumanDecisionRequired } from "../humanDecision.js";
11
+ import { assertBuildAssignmentReconciled } from "../buildAssignment.js";
12
+ import { durableHumanDecision, HumanDecisionRequired, HumanDecisionCancelled, servicePendingHumanDecisions } from "../humanDecision.js";
8
13
  import { requiresBuildApproval, buildScopeRevision, createBuildApprovalGate } from "../buildApproval.js";
9
14
  import { Command, Option } from "commander";
15
+ import { accent, error, sanitizeTerminalText, success, warning } from "../terminalStyle.js";
10
16
  import { resolve, join, relative } from "node:path";
11
17
  import { existsSync, readFileSync, readdirSync } from "node:fs";
12
18
  import { execFileSync } from "node:child_process";
@@ -28,6 +34,7 @@ import { loadTickets } from "../tickets/ticketLoader.js";
28
34
  import { StateDb } from "../tickets/stateDb.js";
29
35
  import { applySharedDeliveryBranch, applySizeBranchPolicy, buildBranchAuditInstruction, buildBranchPlan, parseAuditDependencies } from "../branch/planner.js";
30
36
  import { currentGitRef, ensureCleanBaseWorktree, generatedTrackerDirtyPaths } from "../branch/git.js";
37
+ import { captureDirtyBaseline } from "../branch/dirtyBaseline.js";
31
38
  import { formatGitHubFailure, preflightGh } from "../branch/github.js";
32
39
  import { preflightGlab } from "../branch/gitlab.js";
33
40
  import { readDeliveryUnitSession, runBranchPlan } from "../branch/runner.js";
@@ -55,7 +62,6 @@ import { ObservabilityStore, RunObserver } from "../observability.js";
55
62
  import { loadProjectAutonomyConfig, resolveAutonomyPolicy, resolveQaEnablement } from "../recoveryPolicy.js";
56
63
  import { loadQaRecoveryPacket, recoverPendingQaRecoveryPublications } from "../qaRecovery.js";
57
64
  import { qaDigest } from "../qaProtocolV2.js";
58
- import { loadSkill } from "special-agents";
59
65
  import { acceptQaRuntimeHandoff, describeQaRuntimeHandle, frozenQaRuntimeSettings } from "../qaRuntime.js";
60
66
  const FOREMAN_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
61
67
  function fail(message) {
@@ -147,11 +153,10 @@ export function formatResumeGuidance(executable, projectDir, steps, sessionId, r
147
153
  return [
148
154
  ...run.decisions.flatMap(decision => [
149
155
  `foreman: input required: ${decision.prompt}`,
150
- `foreman: choices: ${decision.choices.map(choice => `${choice.id} (${choice.label})`).join(", ")}`,
151
- ` rafi build:decide ${shellQuote(projectDir)} --run ${shellQuote(run.runId)} --decision ${shellQuote(decision.decisionId)} --choice <choice-id>`,
156
+ ...formatDecisionCommands(projectDir, run.runId, decision, executable),
152
157
  ]),
153
- "foreman: resume this run with:",
154
- ` ${formatRecoveryCommand(projectDir, "rafi")}`,
158
+ executable === "rafi" ? "foreman: resume this run with:" : "foreman: review recovery for this run with:",
159
+ ` ${formatExactRunRecovery(projectDir, run.runId, executable)}`,
155
160
  ];
156
161
  if (executable === "rafi") {
157
162
  return [
@@ -382,6 +387,8 @@ export function savedStartArguments(command, projectDir, opts) {
382
387
  }
383
388
  /** Pure validation shared by interactive start and persisted preparation. */
384
389
  export function validateStartOptions(opts, requireCount) {
390
+ if (opts.dirtyWork !== undefined && !["preserve", "include"].includes(opts.dirtyWork))
391
+ throw new Error("--dirty-work must be preserve or include");
385
392
  const recovery = Boolean(opts.resume || opts.continue || opts.recoverRun);
386
393
  const tickets = opts.ticket ?? [];
387
394
  if (!recovery && tickets.length > 1)
@@ -486,7 +493,10 @@ export function buildStartCommand() {
486
493
  .option("--auto-compact-threshold <percent>", "initial Builder context compaction threshold (1-99)")
487
494
  .option("--max-branch-depth <n>", "maximum selected branch stack depth", "5")
488
495
  .option("--pr-ready", "create ready-for-review PRs instead of draft PRs")
496
+ .option("--builder-network", "request build-scoped builder internet access with separate explicit approval; rechecked on resume")
497
+ .option("--builder-approvals", "allow the builder to request explicit per-operation sandbox approvals; QA stays restricted")
489
498
  .option("--keep-worktrees", "keep successful ticket worktrees for inspection")
499
+ .option("--dirty-work <mode>", "preserve | include: continue in isolated branches without merging into the dirty checkout")
490
500
  .option("--ticket <id>", "select one new ticket, or identify recovery tickets with --resume/--continue/--recover-run", collectTicket, [])
491
501
  .option("--skip-delivery-unit <id>", "skip one unfinished delivery unit for this run", collectTicket, [])
492
502
  .action(async (project, opts, command) => withBuildInvocation(async () => {
@@ -496,6 +506,11 @@ export function buildStartCommand() {
496
506
  catch (error) {
497
507
  fail(error.message);
498
508
  }
509
+ if (opts.dirtyWork) {
510
+ opts.branchPerTicket = true;
511
+ opts.completion = "none";
512
+ opts.keepWorktrees = true;
513
+ }
499
514
  const requestedTicketIds = opts.ticket ?? [];
500
515
  const recoveryIntent = Boolean(opts.resume || opts.continue || opts.recoverRun);
501
516
  if (!recoveryIntent && requestedTicketIds.length > 1)
@@ -543,6 +558,7 @@ export function buildStartCommand() {
543
558
  if (!existsSync(cwd))
544
559
  fail(`project directory not found: ${cwd}`);
545
560
  const recoveryRecord = opts.recoverRun ? readBuildRuns(cwd).find((run) => run.runId === opts.recoverRun) : undefined;
561
+ let dirtyWork = recoveryRecord?.runDecisions?.dirtyWork;
546
562
  if (opts.recoverRun && !recoveryRecord)
547
563
  fail(`recoverable build run not found: ${opts.recoverRun}`);
548
564
  const autonomyProfile = opts.autonomy;
@@ -558,6 +574,34 @@ export function buildStartCommand() {
558
574
  try {
559
575
  if (await superviseStart(cwd, supervisedInvocationId, opts.supervisor === false ? { ...frozenPolicy, supervisorEnabled: false } : frozenPolicy, { steps: opts.steps, stacks: opts.stacks, detach: opts.detach, launchToken: opts.launchToken, startArgs: savedStartArguments(command, cwd, opts) }))
560
576
  return;
577
+ if (recoveryRecord || opts.preparationRun) {
578
+ assertBuildAssignmentReconciled(cwd, supervisedInvocationId);
579
+ if (!await servicePendingHumanDecisions({ projectDir: cwd, runId: supervisedInvocationId, tickets: requestedTicketIds.length ? requestedTicketIds : recoveryRecord?.tickets.length ? recoveryRecord.tickets : undefined, scopeRevision: isTicketsInitialized(cwd) ? buildScopeRevision(cwd) : undefined }))
580
+ return;
581
+ }
582
+ const requestedNetwork = Boolean(opts.builderNetwork || recoveryRecord?.builderCapabilities?.requestedNetwork);
583
+ let grantedNetwork = false;
584
+ if (requestedNetwork) {
585
+ const permissionDb = new WorkflowDb(cwd);
586
+ let networkKey;
587
+ try {
588
+ networkKey = permissionDb.pendingHumanDecisions(supervisedInvocationId).find(decision => decision.interruptionId.startsWith("builder-network:"))?.interruptionId ?? `builder-network:${process.pid}:${randomUUID()}`;
589
+ }
590
+ finally {
591
+ permissionDb.close();
592
+ }
593
+ const networkDecision = await durableHumanDecision({ projectDir: cwd, runId: supervisedInvocationId, key: networkKey,
594
+ prompt: "Allow the builder shell to access the internet for this build? This permits network use by any builder command, not only downloads. Workspace write confinement remains enforced. QA stays offline. Recovery requires approval again; --yes does not approve this permission.",
595
+ choices: [{ id: "allow-network", label: "Allow builder internet for this build" }, { id: "stay-offline", label: "Stay offline and use a verified local bundle" }],
596
+ operation: async () => {
597
+ const action = await select({ message: "Allow builder internet for this build?", options: [{ value: "stay-offline", label: "Stay offline" }, { value: "allow-network", label: "Allow builder internet for this build" }] });
598
+ return isCancel(action) ? undefined : action;
599
+ } });
600
+ if (networkDecision === undefined || isCancel(networkDecision))
601
+ return;
602
+ grantedNetwork = networkDecision === "allow-network";
603
+ }
604
+ const builderCapabilities = { requestedNetwork, grantedNetwork, ...(grantedNetwork ? { approvedAt: new Date().toISOString(), grantWorkerPid: process.pid } : {}) };
561
605
  const readinessOwner = { project: cwd, runId: supervisedInvocationId, authority: localBuildAuthority(cwd) };
562
606
  const autoApprovePlanUpdates = recoveryRecord
563
607
  ? recoveryRecord.recoveryDecision?.planUpdateApproval === "auto"
@@ -711,6 +755,12 @@ export function buildStartCommand() {
711
755
  fail("current-branch build recovery supports exactly one --ticket");
712
756
  }
713
757
  if (branchMode) {
758
+ if (dirtyWork) {
759
+ branchDefaults.completionMode = "none";
760
+ branchDefaults.createReview = false;
761
+ branchDefaults.cleanupBranches = false;
762
+ opts.keepWorktrees = true;
763
+ }
714
764
  if (opts.resume && continueTickets.length > 1) {
715
765
  fail("--resume <sessionId> in branch mode supports exactly one --ticket; use --continue for multiple tickets");
716
766
  }
@@ -799,7 +849,7 @@ export function buildStartCommand() {
799
849
  }
800
850
  const recoveryTicket = !branchMode && recoveryRecord ? continueTickets[0] : undefined;
801
851
  const preferredTicket = selectedNewTicket ?? recoveryTicket;
802
- const executionTickets = !branchMode && recoveryRecord && recoveryRecord.tickets.length
852
+ let executionTickets = !branchMode && recoveryRecord && recoveryRecord.tickets.length
803
853
  ? recoveryExecutionTickets(recoveryRecord, requestedTicketIds, Boolean(opts.recoveryMode))
804
854
  : selectedNewTicket ? [selectedNewTicket] : undefined;
805
855
  const config = loadConfig(join(cwd, "foreman.yaml"));
@@ -826,11 +876,15 @@ export function buildStartCommand() {
826
876
  }
827
877
  }
828
878
  let activeObserver = observabilityStore ? new RunObserver(observabilityStore, invocationRunId) : undefined;
829
- const finishObservationOnExit = () => {
879
+ const finishObservationOnExit = (exitCode) => {
830
880
  if (!activeObserver)
831
881
  return;
832
- activeObserver.finish("interrupted");
833
- observabilityStore?.closeLogFile(logPath, "interrupted");
882
+ const reader = new WorkflowReader(cwd);
883
+ const durable = reader.getRun(invocationRunId);
884
+ reader.close();
885
+ const outcome = durable?.status === "cancelled" ? "cancelled" : durable?.checkpoint === "waiting-for-human" ? "waiting-for-human" : durable?.status === "failed" ? "failed" : "interrupted";
886
+ activeObserver.finish(outcome, { checkpoint: durable?.checkpoint, failureCategory: `process-exit:${exitCode}` });
887
+ observabilityStore?.closeLogFile(logPath, outcome);
834
888
  observabilityStore?.close();
835
889
  activeObserver = undefined;
836
890
  };
@@ -864,6 +918,8 @@ export function buildStartCommand() {
864
918
  workspaceIdentity: !branchMode ? captureCurrentWorkflowSessionIdentity(builderCwd) : captureWorkspaceIdentity(builderCwd),
865
919
  ticketId: sessionRef?.ticketId,
866
920
  deliveryUnitId: sessionRef?.deliveryUnitId,
921
+ networkAccess: grantedNetwork,
922
+ approvalPolicy: opts.builderApprovals ? "on-request" : "never",
867
923
  permission: createPermissionHandler(builderPolicy, log, { durable: () => ({ projectDir: cwd, runId: activeContinuityRunId ?? invocationRunId, ticketId: activeObserver?.context().ticketId }) }),
868
924
  onQuestionTrace: (trace) => log.write("question-round-trip", { ...trace }),
869
925
  onLifecycleTrace: (trace) => log.write("provider-lifecycle", { ...trace }),
@@ -871,7 +927,7 @@ export function buildStartCommand() {
871
927
  fast: builderFast,
872
928
  systemPromptAppend: roleBundle.system || undefined,
873
929
  skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
874
- autoCompactThresholdPercent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 50,
930
+ autoCompactThresholdPercent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 65,
875
931
  observer: activeObserver,
876
932
  };
877
933
  const adapter = agent === "codex"
@@ -894,13 +950,13 @@ export function buildStartCommand() {
894
950
  role, source: roleDefaults.builder ? "project" : "provider", make: agent, model: model ?? "default",
895
951
  reasoning: builderEffort ?? "default", fast: Boolean(builderFast), session_strategy: roleDefaults.builder?.session_strategy ?? "compact",
896
952
  settings_revision: settingsRevision, display_session_cost: sessionCostOverride ?? roleDefaults.builder?.display_session_cost ?? false,
897
- auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 50,
953
+ auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 65,
898
954
  compact_maximum: roleDefaults.builder?.compact_maximum ?? 10,
899
955
  } : {
900
956
  role, source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default",
901
957
  reasoning: qaEffort ?? "default", fast: qaFast, session_strategy: roleDefaults.qa?.session_strategy ?? "compact",
902
958
  settings_revision: settingsRevision, display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false,
903
- auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 50,
959
+ auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 65,
904
960
  compact_maximum: roleDefaults.qa?.compact_maximum ?? 10,
905
961
  };
906
962
  const liveRoleSettings = (role, base) => {
@@ -953,22 +1009,9 @@ export function buildStartCommand() {
953
1009
  });
954
1010
  const qaRuntimeMetadata = new WeakMap();
955
1011
  const resolveQaRuntimeMetadata = (qaCwd, settings) => {
956
- const bundle = loadRoleBundle("qa", { projectDir: qaCwd });
957
- const effectiveRoleInstructions = `${bundle.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.`;
958
- const skills = bundle.skills.map((name) => {
959
- const runtimeRoot = settings.make === "codex" ? ".codex" : ".claude";
960
- const path = [join(qaCwd, runtimeRoot, "skills", name, "SKILL.md"), join(qaCwd, ".agents", "skills", name, "SKILL.md")].find(existsSync);
961
- if (path) {
962
- const content = `## ${name}\n${readFileSync(path, "utf8").trim()}`;
963
- return { name, path, content, digest: createHash("sha256").update(content).digest("hex") };
964
- }
965
- const bundled = loadSkill(name);
966
- if (!bundled.body?.trim())
967
- throw new Error(`QA runtime ${settings.make} skill ${name} has no exact dispatchable content`);
968
- const content = `## ${bundled.name}\n${bundled.body.trim()}`;
969
- return { name, path: `special-agents:${name}`, content, digest: createHash("sha256").update(content).digest("hex") };
970
- });
971
- return { settings: structuredClone(settings), effectiveRoleInstructions, skills };
1012
+ const effective = resolveEffectiveQaConfiguration(cwd, settings);
1013
+ const effectiveRoleInstructions = finalQaRoleInstructions(effective.qaRules);
1014
+ return { settings: structuredClone(settings), effectiveRoleInstructions, skills: effective.skills.map(skill => ({ ...skill, digest: createHash("sha256").update(skill.content).digest("hex") })) };
972
1015
  };
973
1016
  const createBuilderForSettings = async (builderCwd, settings) => {
974
1017
  const ready = await ensureBuildRuntimeReadyForCommand(builderCwd, settings.make, { build: readinessOwner, durable: { projectDir: cwd, runId: activeContinuityRunId ?? invocationRunId, scopeRevision: buildScopeRevision(cwd, approvalConsequences) }, timeoutMs: frozenPolicy.runtimeDeadlines?.preparation_ms,
@@ -994,6 +1037,8 @@ export function buildStartCommand() {
994
1037
  shutdownTimeoutMs: frozenPolicy.runtimeDeadlines?.shutdown_ms,
995
1038
  turnDeadlineMs: frozenPolicy.runtimeDeadlines?.turn_ms,
996
1039
  model: settings.model === "default" ? undefined : settings.model,
1040
+ networkAccess: grantedNetwork,
1041
+ approvalPolicy: opts.builderApprovals ? "on-request" : "never",
997
1042
  permission: createPermissionHandler(policy, log, { durable: () => ({ projectDir: cwd, runId: activeContinuityRunId ?? invocationRunId, ticketId: activeObserver?.context().ticketId }) }),
998
1043
  onQuestionTrace: (trace) => log.write("question-round-trip", { ...trace }),
999
1044
  onLifecycleTrace: (trace) => log.write("provider-lifecycle", { ...trace }),
@@ -1653,7 +1698,7 @@ export function buildStartCommand() {
1653
1698
  if (branchMode) {
1654
1699
  const ticketsConfig = loadTicketsConfig(cwd);
1655
1700
  const allowedBaseDirtyPaths = generatedTrackerDirtyPaths(ticketsConfig.paths);
1656
- const baseWorktreePolicy = recoveryRecord ? "skip" : "enforce";
1701
+ let baseWorktreePolicy = recoveryRecord ? "skip" : "enforce";
1657
1702
  if (baseWorktreePolicy === "skip") {
1658
1703
  const projection = recoveryRecord ? projectBuildRecovery(cwd, recoveryRecord) : undefined;
1659
1704
  if (projection?.expectedChanges.length) {
@@ -1671,12 +1716,35 @@ export function buildStartCommand() {
1671
1716
  ensureCleanBaseWorktree(cwd, { allowedDirtyPaths: allowedBaseDirtyPaths });
1672
1717
  }
1673
1718
  catch (err) {
1674
- fail(err instanceof Error ? err.message : String(err));
1719
+ let mode = opts.dirtyWork;
1720
+ if (!mode && process.stdin.isTTY && process.stdout.isTTY && !opts.yes) {
1721
+ const choice = await select({ message: "Uncommitted work found. How should the isolated build start?", options: [
1722
+ { value: "preserve", label: "Preserve changes; build from committed code (Recommended)" },
1723
+ { value: "include", label: "Include current files as a private build baseline" },
1724
+ { value: "stop", label: "Stop for manual commit or stash" },
1725
+ ] });
1726
+ if (isCancel(choice) || choice === "stop")
1727
+ return;
1728
+ mode = String(choice);
1729
+ }
1730
+ if (mode !== "preserve" && mode !== "include")
1731
+ fail(`${err instanceof Error ? err.message : String(err)}\nContinue safely with --dirty-work preserve (committed input) or --dirty-work include (current files). Both keep your checkout and index intact and retain isolated worktrees.`);
1732
+ const baseline = captureDirtyBaseline(cwd, mode);
1733
+ dirtyWork = { mode, ...baseline };
1734
+ opts.base = baseline.base;
1735
+ branchDefaults.completionMode = "none";
1736
+ branchDefaults.createReview = false;
1737
+ branchDefaults.cleanupBranches = false;
1738
+ branchDefaults.rootBaseBranches = false;
1739
+ opts.keepWorktrees = true;
1740
+ baseWorktreePolicy = "skip";
1741
+ console.log(`rafi: ${mode} dirty-work baseline ${baseline.base}; evidence ${baseline.recordPath}; results stay in isolated branches.`);
1675
1742
  }
1676
1743
  }
1677
1744
  if (branchDefaults.createReview && branchDefaults.reviewProvider) {
1678
1745
  await ensureReviewProviderReady(cwd, branchDefaults.reviewProvider, log, Boolean(opts.yes));
1679
1746
  }
1747
+ console.log(`foreman: builder capability request: workspace writes, shell network ${grantedNetwork ? "approved for this build" : "off"}, approvals ${opts.builderApprovals ? "per operation" : "never"}; effective policy requires provider confirmation. Use a provenance-verified local source/dependency bundle for offline acquisition.`);
1680
1748
  const ready = await ensureBuildRuntimeReadyForCommand(cwd, agent, { build: readinessOwner, durable: { projectDir: cwd, runId: activeContinuityRunId ?? invocationRunId, scopeRevision: buildScopeRevision(cwd, approvalConsequences) }, timeoutMs: frozenPolicy.runtimeDeadlines?.preparation_ms,
1681
1749
  label: "start",
1682
1750
  yes: Boolean(opts.yes),
@@ -1755,7 +1823,7 @@ export function buildStartCommand() {
1755
1823
  try {
1756
1824
  auditBuilder = await createBuilder(cwd);
1757
1825
  auditViewer = printEvents(auditBuilder.events());
1758
- const audit = await auditBuilder.sendTurn(buildBranchAuditInstruction(plan.nodes.map((node) => node.ticket)));
1826
+ const audit = await sendGraphTurn(auditBuilder, buildBranchAuditInstruction(plan.nodes.map((node) => node.ticket)), graphContext(cwd, auditBuilder.sessionRef?.()?.cwd ?? cwd, "branch-dependency-audit", plan.nodes.map(node => node.ticket.id).join(" ")), { purpose: "planning" });
1759
1827
  if (audit.isError)
1760
1828
  throw new Error(audit.text);
1761
1829
  const auditDependencies = parseAuditDependencies(audit.text);
@@ -1860,23 +1928,23 @@ export function buildStartCommand() {
1860
1928
  make: agent, model: model ?? "default", reasoning: builderEffort ?? "default", fast: Boolean(builderFast),
1861
1929
  session_strategy: roleDefaults.builder?.session_strategy ?? "compact", settings_revision: settingsRevision,
1862
1930
  display_session_cost: sessionCostOverride ?? roleDefaults.builder?.display_session_cost ?? false,
1863
- auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 50,
1931
+ auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 65,
1864
1932
  compact_maximum: roleDefaults.builder?.compact_maximum ?? 10,
1865
1933
  };
1866
1934
  const capturedBranchQa = {
1867
1935
  role: "qa", source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default",
1868
1936
  reasoning: qaEffort ?? "default", fast: qaFast, session_strategy: roleDefaults.qa?.session_strategy ?? "compact", settings_revision: settingsRevision,
1869
1937
  display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false,
1870
- auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 50,
1938
+ auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 65,
1871
1939
  compact_maximum: roleDefaults.qa?.compact_maximum ?? 10,
1872
1940
  };
1873
1941
  const firstResumeRef = plan.nodes[0] ? resumeSessionByTicket.get(plan.nodes[0].ticket.id)?.sessionRef : undefined;
1874
1942
  let masterRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { builder: capturedBranchBuilder, qa: capturedBranchQa, builderSessionId: firstResumeRef?.sessionId ?? null, builderSessionRef: firstResumeRef ?? null, expectedRecoveryDecisionDigest: opts.recoveryDecisionDigest }) : createBuildRun({
1875
1943
  runId: invocationRunId,
1876
- tickets: plan.nodes.map((node) => node.ticket.id), deliveryUnit: selectedStacks.length ? selectedStacks.map((stack) => stack.id).join(",") : deliveryRun?.unit.id,
1944
+ builderCapabilities, tickets: plan.nodes.map((node) => node.ticket.id), deliveryUnit: selectedStacks.length ? selectedStacks.map((stack) => stack.id).join(",") : deliveryRun?.unit.id,
1877
1945
  repositoryRoot: cwd, branchMode: branchPresentation.allocationMode, baseRef: plan.baseRef, builder: capturedBranchBuilder, qa: capturedBranchQa,
1878
1946
  frozenPolicy, autonomyProfile, qaEnabled,
1879
- runDecisions: { workMode: "branch-per-ticket", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBranchBuilder.auto_compact_threshold_percent ?? 50, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
1947
+ runDecisions: { dirtyWork, workMode: "branch-per-ticket", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBranchBuilder.auto_compact_threshold_percent ?? 65, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
1880
1948
  });
1881
1949
  activeContinuityRunId = masterRun.runId;
1882
1950
  if (requiresBuildApproval(cwd, plan.nodes.map(node => node.ticket.id), autoApprovePlanUpdates, approvalConsequences) && plan.issues.every((issue) => !issue.blocking)) {
@@ -1888,10 +1956,25 @@ export function buildStartCommand() {
1888
1956
  ],
1889
1957
  }) });
1890
1958
  if (isCancel(action) || action === "cancel") {
1959
+ const cancelled = action === "cancel";
1960
+ if (cancelled) {
1961
+ const decisions = new WorkflowDb(cwd);
1962
+ try {
1963
+ decisions.cancelPendingHumanDecisions(invocationRunId, "User cancelled build; pending decisions superseded");
1964
+ }
1965
+ finally {
1966
+ decisions.close();
1967
+ }
1968
+ }
1969
+ masterRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, masterRun, cancelled ? "cancelled" : "input-paused", { status: cancelled ? "cancelled" : "recoverable" }), cancelled ? "cancelled" : "recoverable");
1970
+ activeObserver?.finish(cancelled ? "cancelled" : "paused", { checkpoint: cancelled ? "user-cancel" : "prompt-eof-or-cancel" });
1891
1971
  console.log("ai-foreman: cancelled");
1892
1972
  process.exit(0);
1893
1973
  }
1894
1974
  }
1975
+ for (const node of plan.nodes)
1976
+ admitApprovedTicket(cwd, masterRun.runId, node.ticket.id, buildScopeRevision(cwd), `build invocation ${invocationRunId}: approved branch plan`);
1977
+ masterRun = checkpointBuildRun(cwd, masterRun, "capabilities-resolved", { builderCapabilities });
1895
1978
  const qaResumedRecovery = recoveryRecord ? pendingQaRecoveryPacket(cwd, masterRun.runId, requestedTicketIds[0]) : undefined;
1896
1979
  let qaProtocolResumeTicket;
1897
1980
  if (recoveryRecord && !qaResumedRecovery) {
@@ -2149,6 +2232,17 @@ export function buildStartCommand() {
2149
2232
  fireTerminalBell(config.notifications.terminal_bell);
2150
2233
  process.exit(failed ? 2 : 0);
2151
2234
  }
2235
+ if (!recoveryRecord && isTicketsInitialized(cwd)) {
2236
+ const paths = resolveTicketPaths(loadTicketsConfig(cwd), cwd);
2237
+ const state = new StateDb(paths.stateDb);
2238
+ try {
2239
+ executionTickets = executionTickets ?? loadTickets(paths.tickets).filter(ticket => !["done", "canceled", "cancelled", "obsolete"].includes(state.getState(ticket.id)?.status ?? "planned")).sort((a, b) => a.order - b.order).slice(0, steps).map(ticket => ticket.id);
2240
+ }
2241
+ finally {
2242
+ state.close();
2243
+ }
2244
+ }
2245
+ console.log(`foreman: builder capability request: workspace writes, shell network ${grantedNetwork ? "approved for this build" : "off"}, approvals ${opts.builderApprovals ? "per operation" : "never"}; effective policy requires provider confirmation. Use a provenance-verified local source/dependency bundle for offline acquisition.`);
2152
2246
  const ready = await ensureBuildRuntimeReadyForCommand(cwd, agent, { build: readinessOwner, durable: { projectDir: cwd, runId: activeContinuityRunId ?? invocationRunId, scopeRevision: buildScopeRevision(cwd, approvalConsequences) }, timeoutMs: frozenPolicy.runtimeDeadlines?.preparation_ms,
2153
2247
  label: "start",
2154
2248
  yes: Boolean(opts.yes),
@@ -2183,16 +2277,17 @@ export function buildStartCommand() {
2183
2277
  make: agent, model: model ?? "default", reasoning: builderEffort ?? "default", fast: Boolean(builderFast),
2184
2278
  session_strategy: roleDefaults.builder?.session_strategy ?? "compact", settings_revision: settingsRevision,
2185
2279
  display_session_cost: sessionCostOverride ?? roleDefaults.builder?.display_session_cost ?? false,
2186
- auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 50,
2280
+ auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 65,
2187
2281
  compact_maximum: roleDefaults.builder?.compact_maximum ?? 10,
2188
2282
  };
2189
- 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", display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false, auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 50, compact_maximum: roleDefaults.qa?.compact_maximum ?? 10, settings_revision: settingsRevision };
2283
+ 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", display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false, auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 65, compact_maximum: roleDefaults.qa?.compact_maximum ?? 10, settings_revision: settingsRevision };
2190
2284
  let buildRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { builder: capturedBuilder, qa: capturedQa, builderSessionId: resumeSessionId ?? null, builderSessionRef: resumeSessionRef ?? null, expectedRecoveryDecisionDigest: opts.recoveryDecisionDigest }) : createBuildRun({
2191
2285
  runId: invocationRunId,
2192
- tickets: [], repositoryRoot: cwd, branchMode: "current", baseRef: opts.base ?? loadTicketSetupConfig(cwd)?.build.base_branch, builder: capturedBuilder, qa: capturedQa,
2286
+ tickets: [...(executionTickets ?? [])], builderCapabilities, authorizedBatch: { tickets: [...(executionTickets ?? [])], requestedSteps: steps, scopeRevision: buildScopeRevision(cwd, approvalConsequences), startedTickets: [] }, repositoryRoot: cwd, branchMode: "current", baseRef: opts.base ?? loadTicketSetupConfig(cwd)?.build.base_branch, builder: capturedBuilder, qa: capturedQa,
2193
2287
  frozenPolicy, autonomyProfile, qaEnabled,
2194
- runDecisions: { workMode: "current", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBuilder.auto_compact_threshold_percent ?? 50, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
2288
+ runDecisions: { workMode: "current", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBuilder.auto_compact_threshold_percent ?? 65, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
2195
2289
  });
2290
+ buildRun = checkpointBuildRun(cwd, buildRun, "capabilities-resolved", { builderCapabilities });
2196
2291
  activeObserver?.store.attachExecutionLease(activeObserver.executionId, readCurrentWorkflowLease(cwd)?.generation);
2197
2292
  activeContinuityRunId = buildRun.runId;
2198
2293
  const qaResumedRecovery = recoveryRecord ? pendingQaRecoveryPacket(cwd, buildRun.runId, requestedTicketIds[0]) : undefined;
@@ -2346,11 +2441,11 @@ export function buildStartCommand() {
2346
2441
  builderFast ? "fast" : null,
2347
2442
  qaEnabled ? null : "qa=off",
2348
2443
  ].filter(Boolean).join(" ");
2349
- console.log(`foreman: driving a ${agent} builder through ${steps} step(s)${modifiers ? ` [${modifiers}]` : ""}`);
2350
- console.log(`foreman: project ${cwd}`);
2444
+ console.log(`${accent("foreman:")} driving a ${sanitizeTerminalText(agent)} builder through ${steps} step(s)${modifiers ? ` [${sanitizeTerminalText(modifiers)}]` : ""}`);
2445
+ console.log(`${accent("foreman:")} project ${sanitizeTerminalText(cwd)}`);
2351
2446
  if (trackerRelPath)
2352
- console.log(`foreman: tracker ${trackerRelPath}`);
2353
- console.log(`foreman: log ${logPath}\n`);
2447
+ console.log(`${accent("foreman:")} tracker ${sanitizeTerminalText(trackerRelPath)}`);
2448
+ console.log(`${accent("foreman:")} log ${sanitizeTerminalText(logPath)}\n`);
2354
2449
  const viewer = printEvents(builder.events());
2355
2450
  try {
2356
2451
  if (recoveryRecord && currentBranchTicketProgress(cwd, buildRun)?.remaining.length === 0) {
@@ -2439,6 +2534,18 @@ export function buildStartCommand() {
2439
2534
  ],
2440
2535
  }) });
2441
2536
  if (isCancel(action) || action === "cancel") {
2537
+ const cancelled = action === "cancel";
2538
+ if (cancelled) {
2539
+ const decisions = new WorkflowDb(cwd);
2540
+ try {
2541
+ decisions.cancelPendingHumanDecisions(invocationRunId, "User cancelled build; pending decisions superseded");
2542
+ }
2543
+ finally {
2544
+ decisions.close();
2545
+ }
2546
+ }
2547
+ buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, cancelled ? "cancelled" : "input-paused", { status: cancelled ? "cancelled" : "recoverable" }), cancelled ? "cancelled" : "recoverable");
2548
+ activeObserver?.finish(cancelled ? "cancelled" : "paused", { checkpoint: cancelled ? "user-cancel" : "prompt-eof-or-cancel" });
2442
2549
  console.log("ai-foreman: cancelled");
2443
2550
  statusReporter.stop("cancelled");
2444
2551
  await foreman.close();
@@ -2453,7 +2560,9 @@ export function buildStartCommand() {
2453
2560
  message: "Your feedback:",
2454
2561
  validate: (v) => (v?.trim() ? undefined : "Please enter some feedback"),
2455
2562
  }) });
2456
- if (isCancel(fb)) {
2563
+ if (isCancel(fb) || fb === undefined) {
2564
+ buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "input-paused", { status: "recoverable" }), "recoverable");
2565
+ activeObserver?.finish("paused", { checkpoint: "feedback-eof-or-cancel" });
2457
2566
  console.log("ai-foreman: cancelled");
2458
2567
  statusReporter.stop("cancelled");
2459
2568
  await foreman.close();
@@ -2466,10 +2575,16 @@ export function buildStartCommand() {
2466
2575
  }
2467
2576
  });
2468
2577
  await ensureApproved();
2578
+ if (!isTicketsInitialized(cwd)) {
2579
+ const synthetic = initializeSyntheticWork(cwd, buildRun.runId, steps, `Explicit unticketed build invocation ${invocationRunId}`, foreman.approvedPlanText());
2580
+ buildRun = checkpointBuildRun(cwd, buildRun, "synthetic-work-admitted", { tickets: synthetic });
2581
+ }
2469
2582
  const onTicketStart = async (ticketId) => {
2470
2583
  await ensureApproved();
2584
+ admitApprovedTicket(cwd, buildRun.runId, ticketId, buildScopeRevision(cwd), `build invocation ${invocationRunId}: approved ticket selection`);
2471
2585
  const tickets = buildRun.tickets.includes(ticketId) ? buildRun.tickets : [...buildRun.tickets, ticketId];
2472
- buildRun = checkpointBuildRun(cwd, buildRun, "ticket-selected", { tickets, currentTicket: ticketId });
2586
+ buildRun = checkpointBuildRun(cwd, buildRun, "ticket-selected", { tickets, currentTicket: ticketId,
2587
+ ...(buildRun.authorizedBatch ? { authorizedBatch: { ...buildRun.authorizedBatch, approvalRevisions: [...new Set([...(buildRun.authorizedBatch.approvalRevisions ?? [buildRun.authorizedBatch.scopeRevision]), buildScopeRevision(cwd, approvalConsequences)])], startedTickets: [...new Set([...buildRun.authorizedBatch.startedTickets, ticketId])] } } : {}) });
2473
2588
  };
2474
2589
  const batch = () => foreman.runBatch(steps, trackerRelPath, onTicketStart, preferredTicket, executionTickets);
2475
2590
  const result = await (activeObserver ? activeObserver.span("builder_work", "Builder and QA batch", batch) : batch());
@@ -2496,8 +2611,12 @@ export function buildStartCommand() {
2496
2611
  pruneRecoveryTelemetryAtCompletion(cwd, config.observability.detail_retention_days);
2497
2612
  activeObserver = undefined;
2498
2613
  process.off("exit", finishObservationOnExit);
2499
- console.log(`\nforeman: ${result.completed}/${result.requested} step(s) completed`);
2500
- console.log(`foreman: outcome — ${result.outcome}`);
2614
+ console.log(`\n${accent("foreman:")} ${result.completed}/${result.requested} step(s) completed`);
2615
+ const safeOutcome = sanitizeTerminalText(result.outcome);
2616
+ const resultOutcome = /success|complete|pass/i.test(safeOutcome) ? success(safeOutcome)
2617
+ : /fail|error/i.test(safeOutcome) ? error(safeOutcome)
2618
+ : /warn|partial|blocked/i.test(safeOutcome) ? warning(safeOutcome) : safeOutcome;
2619
+ console.log(`${accent("foreman:")} outcome — ${resultOutcome}`);
2501
2620
  if (result.detail)
2502
2621
  console.log(`foreman: ${result.detail}`);
2503
2622
  if (buildRun.status === "completed")
@@ -2561,6 +2680,13 @@ export function buildStartCommand() {
2561
2680
  console.error(`foreman: resume this run with: ${formatRecoveryCommand(cwd)}`);
2562
2681
  process.exit(2);
2563
2682
  }
2683
+ if (err instanceof HumanDecisionCancelled) {
2684
+ buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "cancelled", { status: "cancelled" }), "cancelled");
2685
+ activeObserver?.finish("cancelled", { checkpoint: "question-menu" });
2686
+ await foreman.close().catch(() => { });
2687
+ console.log("ai-foreman: cancelled");
2688
+ process.exit(0);
2689
+ }
2564
2690
  if (err instanceof HumanDecisionRequired) {
2565
2691
  buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "waiting-for-human", { status: "recoverable" }), "recoverable");
2566
2692
  await foreman.close().catch(() => { });
@@ -2585,6 +2711,16 @@ export function buildStartCommand() {
2585
2711
  fail(`run failed: ${err instanceof Error ? err.message : String(err)}`);
2586
2712
  }
2587
2713
  }
2714
+ catch (error) {
2715
+ if (error instanceof HumanDecisionCancelled) {
2716
+ console.log("ai-foreman: cancelled");
2717
+ return;
2718
+ }
2719
+ if (!(error instanceof HumanDecisionRequired))
2720
+ throw error;
2721
+ console.error(error.message);
2722
+ process.exitCode = 2;
2723
+ }
2588
2724
  finally {
2589
2725
  finishStartAdmission(cwd, supervisedInvocationId);
2590
2726
  }
@@ -2680,8 +2816,11 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
2680
2816
  throw new Error("QA nonconvergence requires its owning build run");
2681
2817
  const scope = createHash("sha256").update(JSON.stringify([buildScopeRevision(projectDir), context.ticket, context.history])).digest("hex");
2682
2818
  let round = 0;
2819
+ let waiverDecisionId;
2683
2820
  const ask = (phase, choices, operation) => durableHumanDecision({
2684
2821
  projectDir, runId, ticketId: context.ticket.id, observer: observer?.(), defer: noninteractive,
2822
+ onAnswered: decision => { if (phase.startsWith("QA waiver confirmation") && decision.selectedChoiceId === "yes")
2823
+ waiverDecisionId = decision.decisionId; },
2685
2824
  key: `qa-nonconvergence:${scope}:${round}:${phase}`, prompt: `${context.ticket.id}: ${phase}`, choices, operation,
2686
2825
  });
2687
2826
  while (true) {
@@ -2702,7 +2841,10 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
2702
2841
  ] }));
2703
2842
  if (approved === "yes") {
2704
2843
  const db = new WorkflowDb(projectDir);
2844
+ let authorizationRef;
2705
2845
  try {
2846
+ if (db.qaPreparationStore().policy(runId)?.mode === "enforce")
2847
+ authorizationRef = db.authorizeContractQaWaiver(runId, context.ticket.id, waiverDecisionId);
2706
2848
  const lease = db.currentLease();
2707
2849
  if (lease) {
2708
2850
  const detail = JSON.stringify({ decision: "waive", ticket: context.ticket.id, unresolved: context.history, at: new Date().toISOString() });
@@ -2714,7 +2856,7 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
2714
2856
  db.close();
2715
2857
  }
2716
2858
  console.log(`foreman: QA WAIVER recorded for ${context.ticket.id}; validation_result will be failed`);
2717
- return { action: "waive" };
2859
+ return { action: "waive", ...(authorizationRef ? { authorizationRef } : {}) };
2718
2860
  }
2719
2861
  continue;
2720
2862
  }
@@ -2,12 +2,13 @@ import { Command } from "commander";
2
2
  import { resolve, join } from "node:path";
3
3
  import { existsSync, readdirSync, readFileSync } from "node:fs";
4
4
  import { findResumableBranchSessions, formatBranchContinueCommand } from "../branch/resume.js";
5
- import { recoverableBuildRuns } from "../buildRuns.js";
5
+ import { readBuildRuns, recoverableBuildRuns } from "../buildRuns.js";
6
6
  import { WORKFLOW_DB_FILE } from "../workflowDb.js";
7
7
  import { WorkflowReader } from "../workflowReader.js";
8
8
  import { formatWorkflowIssue } from "../outcomes.js";
9
+ import { accent, error, sanitizeTerminalText, success, warning } from "../terminalStyle.js";
9
10
  function fail(message) {
10
- console.error(`foreman: ${message}`);
11
+ console.error(`${error("foreman:", { stream: process.stderr })} ${message}`);
11
12
  process.exit(1);
12
13
  }
13
14
  export function buildStatusCommand() {
@@ -24,11 +25,21 @@ export function runStatus(project) {
24
25
  const workflowPath = join(projectDir, WORKFLOW_DB_FILE);
25
26
  const workflow = existsSync(workflowPath) ? new WorkflowReader(projectDir, workflowPath) : undefined;
26
27
  const activeWorkflows = workflow?.activeRuns() ?? [];
28
+ for (const run of activeWorkflows) {
29
+ const preparation = workflow?.qaPreparation(run.runId);
30
+ if (!preparation || preparation.mode === "legacy")
31
+ continue;
32
+ console.log(`foreman: QA preparation ${preparation.mode} — ${run.runId}`);
33
+ const metrics = workflow.qaPreparationMetrics(run.runId);
34
+ console.log(` admitted ${metrics.approved}; started ${metrics.started}; first reviews ${metrics.firstReviews}; passes ${metrics.firstReviewPasses}; unknown finding causes ${metrics.causes.unknown}`);
35
+ for (const work of preparation.works)
36
+ console.log(` ${work.workId}: ${work.state}; depth ${work.depth ?? "assessment pending"}; contract ${work.digest ?? "unpublished"}${work.detail ? `; ${work.detail}` : ""}`);
37
+ }
27
38
  if (!existsSync(dir) && activeWorkflows.length === 0) {
28
39
  workflow?.close();
29
40
  fail(`no foreman runs found under ${dir}`);
30
41
  }
31
- const logs = readdirSync(dir)
42
+ const logs = (existsSync(dir) ? readdirSync(dir) : [])
32
43
  .filter((f) => f.endsWith(".jsonl"))
33
44
  .sort();
34
45
  if (logs.length === 0) {
@@ -52,8 +63,12 @@ export function runStatus(project) {
52
63
  const prEvents = records.filter((r) => ["pr-created", "pr-existing", "pr-failed"].includes(String(r.event)));
53
64
  const latestFailure = findLatestGitHubFailure(records);
54
65
  const batchEnd = [...records].reverse().find((r) => r.event === "batch-end");
55
- console.log(`foreman: latest run ${logs[logs.length - 1]}`);
56
- console.log(`foreman: ${steps.length} step record(s), ${escalations.length} escalation(s)`);
66
+ const retainedRuns = readBuildRuns(projectDir).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
67
+ const byId = new Map(retainedRuns.map(run => [run.runId, run]));
68
+ const recordRunId = [...records].reverse().find(record => typeof record.runId === "string" && (byId.has(record.runId) || record.event === "step" || record.event === "batch-end" || String(record.event).startsWith("branch-")))?.runId;
69
+ const retainedRun = typeof recordRunId === "string" ? byId.get(recordRunId) : undefined;
70
+ console.log(`${accent("foreman:")} latest run ${sanitizeTerminalText(logs[logs.length - 1] ?? "")}`);
71
+ console.log(`${accent("foreman:")} ${steps.length} step record(s), ${escalations.length} escalation(s)`);
57
72
  if (branchEvents.length || prEvents.length) {
58
73
  const completed = branchEvents.filter((r) => r.event === "branch-complete").length;
59
74
  const issues = branchEvents.filter((r) => r.event === "branch-issue").length;
@@ -61,12 +76,19 @@ export function runStatus(project) {
61
76
  console.log(`foreman: branch mode — ${completed} completed, ${issues} issue(s), ${prs} PR(s)`);
62
77
  }
63
78
  if (batchEnd) {
64
- console.log(`foreman: outcome — ${batchEnd.outcome} (${batchEnd.completed}/${batchEnd.requested})`);
79
+ const outcome = sanitizeTerminalText(String(batchEnd.outcome ?? ""));
80
+ const styledOutcome = /success|complete|pass/i.test(outcome) ? success(outcome)
81
+ : /fail|error/i.test(outcome) ? error(outcome)
82
+ : /warn|partial|blocked/i.test(outcome) ? warning(outcome) : outcome;
83
+ console.log(`${accent("foreman:")} outcome — ${styledOutcome} (${batchEnd.completed}/${batchEnd.requested})`);
65
84
  if (batchEnd.detail)
66
85
  console.log(`foreman: ${batchEnd.detail}`);
67
86
  }
87
+ else if (retainedRun?.status === "completed") {
88
+ console.log(`${accent("foreman:")} outcome — ${success("completed")} (durable build record ${sanitizeTerminalText(retainedRun.runId)})`);
89
+ }
68
90
  else {
69
- console.log("ai-foreman: run is still in progress or did not finish");
91
+ console.log(`${accent("ai-foreman:")} run is still in progress or did not finish`);
70
92
  }
71
93
  if (latestFailure) {
72
94
  console.log(`foreman: latest GitHub failure — ${latestFailure.code}: ${latestFailure.message}`);