ai-foreman 1.7.29 → 2.0.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 (156) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude.d.ts +23 -12
  3. package/dist/adapters/claude.js +231 -26
  4. package/dist/adapters/codex.d.ts +19 -7
  5. package/dist/adapters/codex.js +202 -19
  6. package/dist/adapters/recovering.d.ts +2 -0
  7. package/dist/adapters/recovering.js +5 -1
  8. package/dist/adapters/structured.d.ts +71 -0
  9. package/dist/adapters/structured.js +213 -0
  10. package/dist/adapters/types.d.ts +85 -9
  11. package/dist/agentRun.d.ts +2 -0
  12. package/dist/agentRun.js +2 -1
  13. package/dist/boundedTurnEvidence.d.ts +18 -0
  14. package/dist/boundedTurnEvidence.js +76 -0
  15. package/dist/branch/currentGuard.d.ts +2 -0
  16. package/dist/branch/currentGuard.js +2 -0
  17. package/dist/branch/git.js +3 -8
  18. package/dist/branch/planner.js +1 -4
  19. package/dist/branch/runner.d.ts +2 -1
  20. package/dist/branch/runner.js +10 -7
  21. package/dist/buildAdmission.js +4 -0
  22. package/dist/buildApproval.d.ts +15 -0
  23. package/dist/buildApproval.js +28 -9
  24. package/dist/buildAssignment.d.ts +1 -0
  25. package/dist/buildAssignment.js +82 -23
  26. package/dist/buildInterventions.d.ts +2 -2
  27. package/dist/buildInterventions.js +11 -4
  28. package/dist/buildLaunchGate.js +3 -2
  29. package/dist/buildRuns.d.ts +1 -0
  30. package/dist/buildRuns.js +6 -1
  31. package/dist/builderGuidanceFollowup.js +17 -6
  32. package/dist/cli/doctor.js +3 -1
  33. package/dist/cli/manager.js +22 -20
  34. package/dist/cli/start.js +85 -39
  35. package/dist/cli/tickets.d.ts +0 -8
  36. package/dist/cli/tickets.js +50 -60
  37. package/dist/continuity.d.ts +10 -16
  38. package/dist/continuity.js +223 -208
  39. package/dist/continuityOrigin.d.ts +13 -0
  40. package/dist/continuityOrigin.js +91 -0
  41. package/dist/dispatchNotSent.d.ts +4 -0
  42. package/dist/dispatchNotSent.js +10 -0
  43. package/dist/foreman.d.ts +15 -16
  44. package/dist/foreman.js +70 -278
  45. package/dist/gitExclude.d.ts +16 -0
  46. package/dist/gitExclude.js +46 -0
  47. package/dist/graph/boundary.d.ts +1 -1
  48. package/dist/graph/boundary.js +1 -1
  49. package/dist/graph/host.js +8 -19
  50. package/dist/graph/manager.d.ts +1 -1
  51. package/dist/graph/manager.js +1 -1
  52. package/dist/graph/preparation.d.ts +38 -0
  53. package/dist/graph/preparation.js +7 -6
  54. package/dist/graph/semantic.d.ts +1 -0
  55. package/dist/graph/semantic.js +4 -1
  56. package/dist/graph/session.js +7 -2
  57. package/dist/graph/turn.d.ts +3 -4
  58. package/dist/graph/turn.js +29 -41
  59. package/dist/handoffProtocol.js +2 -2
  60. package/dist/handoffs.d.ts +5 -3
  61. package/dist/handoffs.js +92 -88
  62. package/dist/hostGraphApplicability.d.ts +2 -1
  63. package/dist/hostGraphApplicability.js +17 -19
  64. package/dist/hostQaAugmentation.js +21 -4
  65. package/dist/hostQaComparison.d.ts +21 -2
  66. package/dist/hostQaComparison.js +43 -29
  67. package/dist/hostSourcePreservation.d.ts +38 -0
  68. package/dist/hostSourcePreservation.js +72 -0
  69. package/dist/humanDecision.d.ts +7 -0
  70. package/dist/humanDecision.js +11 -0
  71. package/dist/interviews.d.ts +7 -0
  72. package/dist/interviews.js +18 -1
  73. package/dist/legacyCommunicationDisplay.d.ts +2 -0
  74. package/dist/legacyCommunicationDisplay.js +2 -0
  75. package/dist/managerPacket.js +3 -3
  76. package/dist/markers.d.ts +4 -3
  77. package/dist/markers.js +4 -20
  78. package/dist/observability.js +3 -8
  79. package/dist/probeIpc.d.ts +14 -0
  80. package/dist/probeIpc.js +29 -0
  81. package/dist/qaBuildGate.d.ts +21 -0
  82. package/dist/qaBuildGate.js +60 -13
  83. package/dist/qaBuilderCoverage.js +22 -13
  84. package/dist/qaBuilderEvidence.d.ts +16 -1
  85. package/dist/qaBuilderEvidence.js +48 -8
  86. package/dist/qaContractCoverage.d.ts +2 -2
  87. package/dist/qaContractCoverage.js +4 -9
  88. package/dist/qaContractDelivery.js +34 -20
  89. package/dist/qaCoverageAssembly.d.ts +22 -0
  90. package/dist/qaCoverageAssembly.js +16 -0
  91. package/dist/qaDeliveryJournal.d.ts +2 -0
  92. package/dist/qaDeliveryJournal.js +19 -0
  93. package/dist/qaFailureDelivery.js +97 -85
  94. package/dist/qaHostTransformation.d.ts +26 -0
  95. package/dist/qaHostTransformation.js +100 -0
  96. package/dist/qaPreparation.d.ts +8 -20
  97. package/dist/qaPreparation.js +153 -254
  98. package/dist/qaPreparationInputs.js +19 -0
  99. package/dist/qaPreparationStore.d.ts +11 -0
  100. package/dist/qaPreparationStore.js +44 -4
  101. package/dist/qaPrerequisites.d.ts +2 -1
  102. package/dist/qaPrerequisites.js +21 -1
  103. package/dist/qaProtocolV2.d.ts +15 -0
  104. package/dist/qaRecovery.d.ts +0 -3
  105. package/dist/qaRecovery.js +21 -27
  106. package/dist/qaReview.js +241 -406
  107. package/dist/qaReviewEvidence.d.ts +7 -3
  108. package/dist/qaReviewEvidence.js +15 -4
  109. package/dist/qaRuntime.js +1 -1
  110. package/dist/qaSnapshot.js +76 -21
  111. package/dist/qaStructuredResponse.d.ts +18 -0
  112. package/dist/qaStructuredResponse.js +81 -0
  113. package/dist/qaTimeline.js +2 -2
  114. package/dist/qaVerificationContract.d.ts +1 -2
  115. package/dist/qaVerificationContract.js +24 -35
  116. package/dist/readinessGate.js +5 -4
  117. package/dist/recoveryOwners.d.ts +32 -0
  118. package/dist/recoveryOwners.js +87 -0
  119. package/dist/runtimePreflight.d.ts +11 -0
  120. package/dist/runtimePreflight.js +77 -0
  121. package/dist/runtimeReadiness.js +23 -15
  122. package/dist/sources/sourceRegistry.d.ts +0 -3
  123. package/dist/sources/sourceRegistry.js +0 -42
  124. package/dist/statusReporter.d.ts +2 -0
  125. package/dist/statusReporter.js +2 -0
  126. package/dist/structuredCutover.d.ts +31 -0
  127. package/dist/structuredCutover.js +510 -0
  128. package/dist/structuredCutoverFiles.d.ts +10 -0
  129. package/dist/structuredCutoverFiles.js +181 -0
  130. package/dist/structuredDispatchAuthority.d.ts +22 -0
  131. package/dist/structuredDispatchAuthority.js +110 -0
  132. package/dist/structuredDispatchRecord.d.ts +29 -0
  133. package/dist/structuredDispatchRecord.js +36 -0
  134. package/dist/structuredLiveBudget.d.ts +2 -0
  135. package/dist/structuredLiveBudget.js +23 -0
  136. package/dist/structuredOperation.d.ts +18 -0
  137. package/dist/structuredOperation.js +279 -0
  138. package/dist/supervisedStart.js +35 -21
  139. package/dist/ticketPlanning.d.ts +1 -376
  140. package/dist/ticketPlanning.js +2 -49
  141. package/dist/ticketPopulation.d.ts +0 -3
  142. package/dist/ticketPopulation.js +4 -21
  143. package/dist/ticketPopulationPageAuthority.d.ts +13 -0
  144. package/dist/ticketPopulationPageAuthority.js +30 -0
  145. package/dist/ticketPopulationPaging.d.ts +27 -0
  146. package/dist/ticketPopulationPaging.js +127 -0
  147. package/dist/tickets/renderMarkdown.d.ts +14 -0
  148. package/dist/tickets/renderMarkdown.js +39 -5
  149. package/dist/tickets/ticketLoader.js +10 -0
  150. package/dist/tickets/ticketSchema.d.ts +107 -0
  151. package/dist/tickets/ticketSchema.js +4 -1
  152. package/dist/workflowDb.d.ts +116 -3
  153. package/dist/workflowDb.js +852 -61
  154. package/dist/workflowReader.d.ts +1 -0
  155. package/dist/workflowReader.js +9 -0
  156. package/package.json +23 -7
package/dist/cli/start.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { nativePolicy, operationInstruction, operationPayload, structuredTurnErrorMessage } from "../structuredOperation.js";
1
2
  import { finalQaRoleInstructions, resolveEffectiveQaConfiguration } from "../qaEffectiveConfig.js";
2
3
  import { activateQaRecoveryBuilder } from "../qaBuilderActivation.js";
3
4
  import { graphContext, sendGraphTurn } from "../graph/turn.js";
@@ -10,8 +11,8 @@ import { durableReadOnlyProposal } from "../readOnlyProposal.js";
10
11
  import { captureFrozenQaSource } from "../qaSnapshot.js";
11
12
  import { finishStartAdmission, superviseStart, supervisedRunId } from "../supervisedStart.js";
12
13
  import { assertBuildAssignmentReconciled, isInterruptedBuilderRecovery, assertInterruptedSourceSnapshot } from "../buildAssignment.js";
13
- import { durableHumanDecision, HumanDecisionRequired, HumanDecisionCancelled, servicePendingHumanDecisions } from "../humanDecision.js";
14
- import { requiresBuildApproval, buildScopeRevision, createBuildApprovalGate, validatePlanApprovalBinding } from "../buildApproval.js";
14
+ import { durableHumanDecision, HumanDecisionRequired, HumanDecisionCancelled, reusableCapabilityDecision, servicePendingHumanDecisions } from "../humanDecision.js";
15
+ import { requiresBuildApproval, buildScopeRevision, createBuildApprovalGate, validatePlanApprovalBinding, validateFrozenWorkApprovalBinding } from "../buildApproval.js";
15
16
  import { Command, Option } from "commander";
16
17
  import { accent, error, sanitizeTerminalText, success, warning } from "../terminalStyle.js";
17
18
  import { resolve, join, relative } from "node:path";
@@ -34,7 +35,7 @@ import { isTicketsInitialized, loadTicketsConfig, resolveTicketPaths } from "../
34
35
  import { hasRollbackOrMitigationPlan, loadTickets } from "../tickets/ticketLoader.js";
35
36
  import { StateDb } from "../tickets/stateDb.js";
36
37
  import { ticketDefinitionDigest } from "../tickets/riskWaivers.js";
37
- import { applySharedDeliveryBranch, applySizeBranchPolicy, buildBranchAuditInstruction, buildBranchPlan, parseAuditDependencies } from "../branch/planner.js";
38
+ import { applySharedDeliveryBranch, applySizeBranchPolicy, buildBranchAuditInstruction, buildBranchPlan } from "../branch/planner.js";
38
39
  import { currentGitRef, ensureCleanBaseWorktree, generatedTrackerDirtyPaths } from "../branch/git.js";
39
40
  import { captureDirtyBaseline } from "../branch/dirtyBaseline.js";
40
41
  import { formatGitHubFailure, preflightGh } from "../branch/github.js";
@@ -42,7 +43,7 @@ import { preflightGlab } from "../branch/gitlab.js";
42
43
  import { readDeliveryUnitSession, runBranchPlan } from "../branch/runner.js";
43
44
  import { branchPlanLogMetadata, presentBranchPlan } from "../branch/presentation.js";
44
45
  import { branchTicketScopeProgress, buildRunSessionBinding, checkpointBuildRun, completeBuildRun, createBuildRun, currentBranchRecoveryTicket, currentBranchTicketProgress, recoveryExecutionTickets, finishBranchTicketScope, finishRecoveredTicketScope, heartbeatBuildRun, persistBuildSession, persistCurrentBuildSession, projectBuildRecovery, readBuildRuns, releaseBuildLease, resumeBuildRun } from "../buildRuns.js";
45
- import { parseProviderContract, qaRemediationProposalV1Schema } from "rafi-spec";
46
+ import { branchAuditOutput, plannerRemediationOutput } from "rafi-spec";
46
47
  import { parse as parseYaml } from "yaml";
47
48
  import { findResumableBranchSessions, formatBranchContinueCommand, formatBranchSummaryFollowupCommands, } from "../branch/resume.js";
48
49
  import { detectGitProvider, loadTicketSetupConfig } from "../tickets/setupConfig.js";
@@ -539,7 +540,7 @@ export function buildStartCommand(options = {}) {
539
540
  .option("--accept-handoff-role <role>", "role owning the pre-staged handoff (builder | qa)", "builder")
540
541
  .option("--continue", "resume the most recent logged session for this project")
541
542
  .option("-t, --tickets <path>", "path to ticket file (.md, .txt, .yaml, …) — passed to the builder as context")
542
- .option("-y, --yes", "skip pre-flight confirmation prompt")
543
+ .option("-y, --yes", "never stop to ask: approve the pre-flight plan, plan updates, and capabilities requested by flag (--builder-network, --qa-network)")
543
544
  .option("--effort <level>", "reasoning effort level (low|medium|high|xhigh)")
544
545
  .option("--fast", "use native fast mode when available; keep the model and reasoning settings")
545
546
  .option("--no-qa", "disable per-ticket QA review (enabled by default)")
@@ -564,13 +565,15 @@ export function buildStartCommand(options = {}) {
564
565
  .option("--auto-compact-threshold <percent>", "initial Builder context compaction threshold (1-99)")
565
566
  .option("--max-branch-depth <n>", "maximum selected branch stack depth", "5")
566
567
  .option("--pr-ready", "create ready-for-review PRs instead of draft PRs")
567
- .option("--builder-network", "request build-scoped builder internet access with separate explicit approval; rechecked on resume")
568
+ .option("--builder-network", "request build-scoped builder internet access; asks for approval unless --yes; rechecked on resume")
569
+ .option("--qa-network", "allow independent QA network access for this build (e.g. live local services); asks for approval unless --yes; rechecked on resume")
568
570
  .option("--builder-approvals", "allow the builder to request explicit per-operation sandbox approvals; QA stays restricted")
569
571
  .option("--keep-worktrees", "keep successful ticket worktrees for inspection")
570
572
  .option("--dirty-work <mode>", "preserve | include: continue in isolated branches without merging into the dirty checkout")
571
573
  .option("--ticket <id>", "select one new ticket, or identify recovery tickets with --resume/--continue/--recover-run", collectTicket, [])
572
574
  .option("--skip-delivery-unit <id>", "skip one unfinished delivery unit for this run", collectTicket, [])
573
575
  .action(async (project, opts, command) => withBuildInvocation(async () => {
576
+ let waitingForDecision = false;
574
577
  try {
575
578
  validateStartOptions(opts, false);
576
579
  }
@@ -666,18 +669,26 @@ export function buildStartCommand(options = {}) {
666
669
  return;
667
670
  }
668
671
  const requestedNetwork = Boolean(opts.builderNetwork || recoveryRecord?.builderCapabilities?.requestedNetwork);
672
+ // --yes never stops to ask: it approves capabilities the operator
673
+ // requested by flag. A resumed run counts only an explicit --yes.
674
+ const explicitYes = recoveryRecord ? recoveryRecord.recoveryDecision?.planUpdateApprovalSource === "explicit-yes" : Boolean(opts.yes);
669
675
  let grantedNetwork = false;
670
- if (requestedNetwork) {
676
+ let networkDecisionId;
677
+ if (requestedNetwork && explicitYes) {
678
+ grantedNetwork = true;
679
+ console.error("foreman: builder internet for this build approved by --yes (requested with --builder-network)");
680
+ }
681
+ else if (requestedNetwork) {
671
682
  const permissionDb = new WorkflowDb(cwd);
672
683
  let networkKey;
673
684
  try {
674
- networkKey = permissionDb.pendingHumanDecisions(supervisedInvocationId).find(decision => decision.interruptionId.startsWith("builder-network:"))?.interruptionId ?? `builder-network:${process.pid}:${randomUUID()}`;
685
+ networkKey = reusableCapabilityDecision(permissionDb, supervisedInvocationId, "builder-network:", recoveryRecord?.builderCapabilities?.decisionId) ?? `builder-network:${process.pid}:${randomUUID()}`;
675
686
  }
676
687
  finally {
677
688
  permissionDb.close();
678
689
  }
679
- const networkDecision = await durableHumanDecision({ projectDir: cwd, runId: supervisedInvocationId, key: networkKey,
680
- 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.",
690
+ const networkDecision = await durableHumanDecision({ projectDir: cwd, runId: supervisedInvocationId, key: networkKey, onAnswered: decision => { networkDecisionId = decision.decisionId; },
691
+ 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 unless --qa-network is separately approved. Recovery requires approval again unless resumed with --yes.",
681
692
  choices: [{ id: "allow-network", label: "Allow builder internet for this build" }, { id: "stay-offline", label: "Stay offline and use a verified local bundle" }],
682
693
  operation: async () => {
683
694
  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" }] });
@@ -687,7 +698,37 @@ export function buildStartCommand(options = {}) {
687
698
  return;
688
699
  grantedNetwork = networkDecision === "allow-network";
689
700
  }
690
- const builderCapabilities = { requestedNetwork, grantedNetwork, ...(grantedNetwork ? { approvedAt: new Date().toISOString(), grantWorkerPid: process.pid } : {}) };
701
+ const builderCapabilities = { requestedNetwork, grantedNetwork, ...(grantedNetwork ? { approvedAt: new Date().toISOString(), grantWorkerPid: process.pid } : {}), ...(networkDecisionId ? { decisionId: networkDecisionId } : {}) };
702
+ // Independent QA is offline unless the operator grants --qa-network, e.g.
703
+ // so QA can verify a live local service the ticket requires.
704
+ const requestedQaNetwork = Boolean(opts.qaNetwork || recoveryRecord?.qaCapabilities?.requestedNetwork);
705
+ let grantedQaNetwork = false;
706
+ let qaNetworkDecisionId;
707
+ if (requestedQaNetwork && explicitYes) {
708
+ grantedQaNetwork = true;
709
+ console.error("foreman: QA network for this build approved by --yes (requested with --qa-network)");
710
+ }
711
+ else if (requestedQaNetwork) {
712
+ const permissionDb = new WorkflowDb(cwd);
713
+ let qaNetworkKey;
714
+ try {
715
+ qaNetworkKey = reusableCapabilityDecision(permissionDb, supervisedInvocationId, "qa-network:", recoveryRecord?.qaCapabilities?.decisionId) ?? `qa-network:${process.pid}:${randomUUID()}`;
716
+ }
717
+ finally {
718
+ permissionDb.close();
719
+ }
720
+ const qaNetworkDecision = await durableHumanDecision({ projectDir: cwd, runId: supervisedInvocationId, key: qaNetworkKey, onAnswered: decision => { qaNetworkDecisionId = decision.decisionId; },
721
+ prompt: "Allow independent QA shell commands to access the network for this build? This lets QA run live checks (for example against a local service). QA keeps a read-only workspace. Recovery requires approval again unless resumed with --yes.",
722
+ choices: [{ id: "allow-qa-network", label: "Allow QA network for this build" }, { id: "qa-offline", label: "Keep QA offline" }],
723
+ operation: async () => {
724
+ const action = await select({ message: "Allow QA network for this build?", options: [{ value: "qa-offline", label: "Keep QA offline" }, { value: "allow-qa-network", label: "Allow QA network for this build" }] });
725
+ return isCancel(action) ? undefined : action;
726
+ } });
727
+ if (qaNetworkDecision === undefined || isCancel(qaNetworkDecision))
728
+ return;
729
+ grantedQaNetwork = qaNetworkDecision === "allow-qa-network";
730
+ }
731
+ const qaCapabilities = { requestedNetwork: requestedQaNetwork, grantedNetwork: grantedQaNetwork, ...(grantedQaNetwork ? { approvedAt: new Date().toISOString() } : {}), ...(qaNetworkDecisionId ? { decisionId: qaNetworkDecisionId } : {}) };
691
732
  const readinessOwner = { project: cwd, runId: supervisedInvocationId, authority: localBuildAuthority(cwd) };
692
733
  const autoApprovePlanUpdates = recoveryRecord
693
734
  ? recoveryRecord.recoveryDecision?.planUpdateApproval === "auto"
@@ -699,9 +740,11 @@ export function buildStartCommand(options = {}) {
699
740
  // recovery remains fail-closed on an exact current-plan approval.
700
741
  if (recoveryRecord && autoApprovePlanUpdates && recoveryTickets.length > 0) {
701
742
  const decision = recoveryRecord.recoveryDecision;
702
- if (!decision?.planApproval)
743
+ if (!decision?.planApproval && !decision?.frozenWorkApproval)
703
744
  fail("saved --yes approval has no exact plan binding; rerun rafi build:resume --yes to approve the current plan");
704
- const approvalCheck = validatePlanApprovalBinding(cwd, recoveryTickets, decision.planApproval);
745
+ const approvalCheck = decision.frozenWorkApproval
746
+ ? validateFrozenWorkApprovalBinding(cwd, recoveryRecord.runId, recoveryTickets, decision.frozenWorkApproval)
747
+ : validatePlanApprovalBinding(cwd, recoveryTickets, decision.planApproval);
705
748
  if (!approvalCheck.valid)
706
749
  fail(`saved --yes approval is stale (${approvalCheck.reason}); rerun rafi build:resume --yes before provider work`);
707
750
  }
@@ -1098,6 +1141,9 @@ export function buildStartCommand(options = {}) {
1098
1141
  : base;
1099
1142
  const handoffRecoveryOptions = (role) => ({
1100
1143
  allowProviderSwitch: true,
1144
+ // --yes never stops to ask: retry with a fresh successor. The handoff
1145
+ // still pauses after three rejected successors.
1146
+ ...(explicitYes ? { choose: async () => "retry" } : {}),
1101
1147
  desktopNotifications: config.notifications.enabled,
1102
1148
  terminalBell: config.notifications.terminal_bell,
1103
1149
  onAccepted: (accepted) => {
@@ -1182,6 +1228,7 @@ export function buildStartCommand(options = {}) {
1182
1228
  configRoot: cwd,
1183
1229
  sessionRole: "qa",
1184
1230
  sessionStream: "qa",
1231
+ networkAccess: grantedQaNetwork,
1185
1232
  runtimeExecutable: ready.executable,
1186
1233
  runtimePhase: "qa",
1187
1234
  preparationTimeoutMs: frozenPolicy.runtimeDeadlines?.preparation_ms,
@@ -1305,7 +1352,6 @@ export function buildStartCommand(options = {}) {
1305
1352
  try {
1306
1353
  const revision = countDb.continuityHead(runId, "builder")?.authoritativeStateRevision ?? settings.settings_revision;
1307
1354
  countDb.appendContinuityEvent({ runId, role: "builder", kind: "builder_requested_handoff_delta", payload: request.delta, authoritativeStateRevision: revision });
1308
- countDb.publishContinuityCheckpoint({ runId, role: "builder", delta: request.delta, authoritativeStateRevision: revision });
1309
1355
  compactionCount = countDb.successfulCompactionCount(runId, "builder", predecessor.sessionRef?.() ?? sessionId);
1310
1356
  }
1311
1357
  finally {
@@ -1318,6 +1364,7 @@ export function buildStartCommand(options = {}) {
1318
1364
  predecessorSessionId: sessionId,
1319
1365
  predecessorSessionRef: predecessor.sessionRef?.(),
1320
1366
  requestedByBuilder: true,
1367
+ predecessorDispatchId: request.predecessorDispatchId,
1321
1368
  roleState: {
1322
1369
  ...request.roleState,
1323
1370
  frozenActionDigest: createHash("sha256").update(frozenAction).digest("hex"),
@@ -1334,7 +1381,8 @@ export function buildStartCommand(options = {}) {
1334
1381
  resources: [{ label: "frozen-action", content: frozenAction, authoritative: true }],
1335
1382
  }, (_handoff, runtime) => createBuilderForSettings(builderCwd, settingsForRequestedRuntime(effectiveSettings, runtime)), handoffRecoveryOptions("builder"));
1336
1383
  log.write("handoff-transfer", {
1337
- requestedByBuilder: true, reason: request.reason, generation: transfer.manifest.generation,
1384
+ requestedByBuilder: true,
1385
+ predecessorDispatchId: request.predecessorDispatchId, reason: request.reason, generation: transfer.manifest.generation,
1338
1386
  predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId,
1339
1387
  acceptanceCheckpointDigest: transfer.acceptanceCheckpointDigest,
1340
1388
  });
@@ -1452,6 +1500,7 @@ export function buildStartCommand(options = {}) {
1452
1500
  configRoot: cwd,
1453
1501
  sessionRole: "qa",
1454
1502
  sessionStream: "qa",
1503
+ networkAccess: grantedQaNetwork,
1455
1504
  runtimeExecutable: ready.executable,
1456
1505
  runtimePhase: "qa",
1457
1506
  preparationTimeoutMs: frozenPolicy.runtimeDeadlines?.preparation_ms,
@@ -2027,10 +2076,13 @@ export function buildStartCommand(options = {}) {
2027
2076
  try {
2028
2077
  auditBuilder = await createBuilder(cwd);
2029
2078
  auditViewer = printEvents(auditBuilder.events());
2030
- 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" });
2079
+ const audit = await sendGraphTurn(auditBuilder, `${buildBranchAuditInstruction(plan.nodes.map((node) => node.ticket))}\n${operationInstruction(branchAuditOutput)}`, graphContext(cwd, auditBuilder.sessionRef?.()?.cwd ?? cwd, "branch-dependency-audit", plan.nodes.map(node => node.ticket.id).join(" ")), nativePolicy(branchAuditOutput, { purpose: "planning", journalRunId: auditRunId }));
2031
2080
  if (audit.isError)
2032
- throw new Error(audit.text);
2033
- const auditDependencies = parseAuditDependencies(audit.text);
2081
+ throw new Error(structuredTurnErrorMessage(audit, "Branch dependency audit"));
2082
+ const auditDependencies = operationPayload(audit, branchAuditOutput).dependencies;
2083
+ const selectedIds = new Set(plan.nodes.map(node => node.ticket.id));
2084
+ if (auditDependencies.some(dep => dep.ticket === dep.dependsOn || !selectedIds.has(dep.ticket) || !selectedIds.has(dep.dependsOn)))
2085
+ throw new Error("Branch audit returned a foreign or self dependency");
2034
2086
  plan = buildBranchPlan(tickets, states, {
2035
2087
  steps,
2036
2088
  baseRef: opts.base ?? loadTicketSetupConfig(cwd)?.build.base_branch ?? currentGitRef(cwd),
@@ -2148,7 +2200,7 @@ export function buildStartCommand(options = {}) {
2148
2200
  && (opts.qaRevision !== undefined || pendingQaRecoveryPacket(cwd, recoveryRecord.runId, requestedTicketIds[0])));
2149
2201
  let masterRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { ...(branchQaOnlyRecovery ? {} : { builder: capturedBranchBuilder, builderSessionId: firstResumeRef?.sessionId ?? null, builderSessionRef: firstResumeRef ?? null }), qa: capturedBranchQa, expectedRecoveryDecisionDigest: opts.recoveryDecisionDigest }) : createBuildRun({
2150
2202
  runId: invocationRunId,
2151
- builderCapabilities, tickets: plan.nodes.map((node) => node.ticket.id), deliveryUnit: selectedStacks.length ? selectedStacks.map((stack) => stack.id).join(",") : deliveryRun?.unit.id,
2203
+ builderCapabilities, qaCapabilities, tickets: plan.nodes.map((node) => node.ticket.id), deliveryUnit: selectedStacks.length ? selectedStacks.map((stack) => stack.id).join(",") : deliveryRun?.unit.id,
2152
2204
  repositoryRoot: cwd, branchMode: branchPresentation.allocationMode, baseRef: plan.baseRef, builder: capturedBranchBuilder, qa: capturedBranchQa,
2153
2205
  frozenPolicy, autonomyProfile, qaEnabled,
2154
2206
  runDecisions: { dirtyWork, workMode: "branch-per-ticket", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBranchBuilder.auto_compact_threshold_percent ?? 65, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
@@ -2182,7 +2234,7 @@ export function buildStartCommand(options = {}) {
2182
2234
  }
2183
2235
  for (const node of plan.nodes)
2184
2236
  admitApprovedTicket(cwd, masterRun.runId, node.ticket.id, buildScopeRevision(cwd), `build invocation ${invocationRunId}: approved branch plan`);
2185
- masterRun = checkpointBuildRun(cwd, masterRun, "capabilities-resolved", { builderCapabilities });
2237
+ masterRun = checkpointBuildRun(cwd, masterRun, "capabilities-resolved", { builderCapabilities, qaCapabilities });
2186
2238
  const qaResumedRecovery = recoveryRecord && !isInterruptedBuilderRecovery(recoveryRecord) ? pendingQaRecoveryPacket(cwd, masterRun.runId, requestedTicketIds[0]) : undefined;
2187
2239
  let qaProtocolResumeTicket;
2188
2240
  if (recoveryRecord && !qaResumedRecovery && !isInterruptedBuilderRecovery(recoveryRecord)) {
@@ -2506,11 +2558,11 @@ export function buildStartCommand(options = {}) {
2506
2558
  && (opts.qaRevision !== undefined || pendingQaRecoveryPacket(cwd, recoveryRecord.runId, requestedTicketIds[0])));
2507
2559
  let buildRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { ...(qaOnlyRecovery ? {} : { builder: capturedBuilder, builderSessionId: resumeSessionId ?? null, builderSessionRef: resumeSessionRef ?? null }), qa: capturedQa, expectedRecoveryDecisionDigest: opts.recoveryDecisionDigest }) : createBuildRun({
2508
2560
  runId: invocationRunId,
2509
- 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,
2561
+ tickets: [...(executionTickets ?? [])], builderCapabilities, qaCapabilities, 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,
2510
2562
  frozenPolicy, autonomyProfile, qaEnabled,
2511
2563
  runDecisions: { workMode: "current", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBuilder.auto_compact_threshold_percent ?? 65, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
2512
2564
  });
2513
- buildRun = checkpointBuildRun(cwd, buildRun, "capabilities-resolved", { builderCapabilities });
2565
+ buildRun = checkpointBuildRun(cwd, buildRun, "capabilities-resolved", { builderCapabilities, qaCapabilities });
2514
2566
  activeObserver?.store.attachExecutionLease(activeObserver.executionId, readCurrentWorkflowLease(cwd)?.generation);
2515
2567
  activeContinuityRunId = buildRun.runId;
2516
2568
  publishActivatedBuilderSession = ref => { buildRun = persistCurrentBuildSession(cwd, buildRun, "builder", ref); };
@@ -2981,11 +3033,16 @@ export function buildStartCommand(options = {}) {
2981
3033
  if (!(error instanceof HumanDecisionRequired))
2982
3034
  throw error;
2983
3035
  console.error(error.message);
2984
- process.exitCode = 2;
3036
+ waitingForDecision = true;
2985
3037
  }
2986
3038
  finally {
2987
3039
  finishStartAdmission(cwd, supervisedInvocationId);
2988
3040
  }
3041
+ // Exit explicitly: supervisor IPC, heartbeats and runtime handles keep a
3042
+ // supervised worker alive, so it would otherwise wait forever while its
3043
+ // supervisor waits for it, and `rafi resume` refuses the "live" owner.
3044
+ if (waitingForDecision)
3045
+ process.exit(2);
2989
3046
  }));
2990
3047
  }
2991
3048
  function ticketSizeBucket(projectDir, ticketIds) {
@@ -3146,8 +3203,7 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
3146
3203
  `User discussion: ${String(discussion)}`,
3147
3204
  `Complete QA history:\n${JSON.stringify(context.history, null, 2)}`,
3148
3205
  `${diffTruncated ? "Builder diff (TRUNCATED at 128 KiB; inspect the actual worktree before relying on it)" : "Full Builder diff"}:\n${diff || "(no tracked diff)"}`,
3149
- "Return RAFI_QA_REMEDIATION_START, then one JSON object {\"summary\":\"...\",\"fix_instructions\":[\"...\"]}, then RAFI_QA_REMEDIATION_END.",
3150
- "End with STEP_STATUS: plan_complete | summary=\"QA remediation proposed\"",
3206
+ "Submit the native planner-remediation payload with summary and fix_instructions.",
3151
3207
  ].join("\n\n");
3152
3208
  let resumeSessionId;
3153
3209
  let revision = 0;
@@ -3155,24 +3211,14 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
3155
3211
  revision++;
3156
3212
  const proposalDigest = createHash("sha256").update(JSON.stringify([scope, round, revision, instruction, planner, captureFrozenQaSource(context.builderWorktree).digest])).digest("hex");
3157
3213
  const run = await durableReadOnlyProposal({ projectDir, runId, ticketId: context.ticket.id, digest: proposalDigest, operation: () => runRoleInstruction({
3158
- projectDir, role: "planner", instruction, label: "QA Planner remediation", agent: planner?.make,
3214
+ projectDir, role: "planner", instruction, output: plannerRemediationOutput, label: "QA Planner remediation", agent: planner?.make,
3159
3215
  model: explicitDefaultValue(planner?.model), effort: explicitEffort(planner?.reasoning), fast: planner?.fast,
3160
3216
  resumeSessionId, permissionConfig: readOnlyPermissionConfig(), sandboxMode: "read-only",
3161
3217
  logPath: makeRoleLogPath(projectDir, "qa-remediation"), logEvent: "rafi-plan",
3162
3218
  }) });
3163
- if (run.turn.result.isError || run.turn.status.kind !== "plan_complete")
3219
+ if (run.turn.result.isError || run.turn.status.kind !== "done")
3164
3220
  throw new Error(`Planner remediation failed: ${run.turn.status.error ?? run.turn.result.text.slice(0, 500)}`);
3165
- const match = /RAFI_QA_REMEDIATION_START\s*([\s\S]*?)\s*RAFI_QA_REMEDIATION_END/.exec(run.turn.result.text);
3166
- if (!match)
3167
- throw new Error("Planner remediation did not return the required structured proposal");
3168
- let parsed;
3169
- try {
3170
- parsed = JSON.parse(match[1].replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, ""));
3171
- }
3172
- catch (error) {
3173
- throw new Error(`Planner remediation proposal is invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
3174
- }
3175
- const proposal = parseProviderContract(qaRemediationProposalV1Schema, parsed, "RAFI_QA_REMEDIATION");
3221
+ const proposal = operationPayload(run.turn.result, plannerRemediationOutput);
3176
3222
  console.log(`\nPlanner remediation: ${proposal.summary}\n${proposal.fix_instructions.map((item) => `- ${item}`).join("\n")}`);
3177
3223
  const decision = await ask(`QA remediation approval ${revision}: ${JSON.stringify(proposal)}`, [{ id: "approve", label: "Approve" }, { id: "revise", label: "Revise" }, { id: "cancel", label: "Return to choices" }], () => select({ message: "Use this remediation for a new Builder fix session?", options: [
3178
3224
  { value: "approve", label: "Approve (Recommended)" }, { value: "revise", label: "Discuss/revise" }, { value: "cancel", label: "Cancel and return to choices" },
@@ -3185,7 +3231,7 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
3185
3231
  if (isCancel(feedback))
3186
3232
  break;
3187
3233
  resumeSessionId = run.sessionId;
3188
- instruction = `Revise the complete QA remediation proposal using this feedback: ${String(feedback)}. Keep the exact structured envelope and final plan_complete marker.`;
3234
+ instruction = `Revise the complete QA remediation proposal using this feedback: ${String(feedback)}. Use the native planner-remediation contract.`;
3189
3235
  }
3190
3236
  }
3191
3237
  };
@@ -1,17 +1,10 @@
1
1
  import { Command } from "commander";
2
2
  import type { StructuredPlanV1 } from "rafi-spec";
3
3
  import { Log } from "../log.js";
4
- import { type StepStatus } from "../foreman.js";
5
4
  import type { EffortLevel } from "../adapters/types.js";
6
5
  import { type RoleBuilderOptions } from "../agentRun.js";
7
6
  import type { TicketsConfig } from "../tickets/config.js";
8
7
  import { type TicketPopulateRuntimeMode, type TicketSourceConfig, type TicketsSetupConfig } from "../tickets/setupConfig.js";
9
- /**
10
- * Ticket Maker may append an explanatory note after its machine envelope.
11
- * Preserve a schema-valid proposal before Foreman's strict generic marker
12
- * repair path replaces the response with a status-only correction.
13
- */
14
- export declare function acceptTicketPopulationCompletion(text: string): StepStatus | undefined;
15
8
  /** Commander preserves comma-delimited values as one argument; accept both documented forms. */
16
9
  export declare function normalizeRiskWaiverTicketIds(values: readonly string[] | undefined): string[];
17
10
  /** Record an explicit, definition-bound exception for existing risk tickets. */
@@ -23,7 +16,6 @@ export interface RiskNoteRepairProposal {
23
16
  rollback: string;
24
17
  }>;
25
18
  }
26
- export declare function extractRiskNoteRepairProposal(output: string): RiskNoteRepairProposal;
27
19
  /** Validate a deliberately narrow repair: only exactly the missing rollback fields may change. */
28
20
  export declare function applyRiskNoteRepairProposal(tickets: import("../tickets/ticketSchema.js").TicketDef[], proposal: RiskNoteRepairProposal): import("../tickets/ticketSchema.js").TicketDef[];
29
21
  export declare function buildPopulateInstruction(approvedPlanPath: string, contextSources?: string[], progressDoc?: string): string;
@@ -1,13 +1,15 @@
1
+ import { streamTicketPopulation } from "../ticketPopulationPaging.js";
2
+ import { operationPayload } from "../structuredOperation.js";
1
3
  import { Command } from "commander";
2
4
  import { accent, error, sanitizeTerminalText, success, warning } from "../terminalStyle.js";
3
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
6
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
7
  import { parse as parseYaml } from "yaml";
6
- import { riskNoteRepairProposalV1Schema, zodIssues } from "rafi-spec";
8
+ import { riskNoteRepairOutput } from "rafi-spec";
7
9
  import { select, text, confirm, isCancel, multiselect } from "@clack/prompts";
8
10
  import { loadConfig } from "../config.js";
9
11
  import { Log } from "../log.js";
10
- import { Foreman, parseStepStatus } from "../foreman.js";
12
+ import { Foreman } from "../foreman.js";
11
13
  import { printEvents } from "./events.js";
12
14
  import { cmdInit, cmdUpdate, cmdComplete, cmdBlock, cmdUnblock, cmdCancel, cmdDiscover, cmdAcceptFutureWork, cmdReorder, cmdRender, cmdValidate, cmdQueue, cmdStackQueue, cmdArchive, cmdReview, } from "../tickets/commands.js";
13
15
  import { loadDeliveryConfig } from "../tickets/delivery.js";
@@ -20,7 +22,7 @@ import { DEFAULT_TICKET_SETUP, configuredPlanningSources, configuredAppName, def
20
22
  import { deactivateSource, discardStagedSourceCaptures, loadSourceRegistry, refreshSourceRegistry, registerSourceRequests, removeSource, saveSourceRegistry, } from "../sources/sourceRegistry.js";
21
23
  import { fetchAndSnapshotUrl, snapshotExternalLocalFile } from "../tickets/sourceFetch.js";
22
24
  import { checkpointInterview, completeInterview, createInterviewRecord, failInterview, readInterviewRecords, resumeInterviewRecord, } from "../interviews.js";
23
- import { applyTicketPopulation, assertRiskPlanOnlyCorrection, authorizeTicketRetirements, extractTicketPopulationProposal, materializeTicketPopulation, recoverTicketPublications, TicketPopulationValidationError, TICKET_POPULATION_PROPOSAL_END, TICKET_POPULATION_PROPOSAL_START } from "../ticketPopulation.js";
25
+ import { applyTicketPopulation, assertRiskPlanOnlyCorrection, authorizeTicketRetirements, materializeTicketPopulation, recoverTicketPublications, TicketPopulationValidationError } from "../ticketPopulation.js";
24
26
  import { loadTickets, saveTickets, validateTicketDefs } from "../tickets/ticketLoader.js";
25
27
  import { resolveTicketPaths } from "../tickets/config.js";
26
28
  import { formatAllTicketDetails, formatTicketDetails, getAllTicketDetails, getTicketDetails } from "../tickets/details.js";
@@ -31,25 +33,11 @@ import { listTicketGroups, previewTicketGroupRepair, repairTicketGroups } from "
31
33
  import { StateDb } from "../tickets/stateDb.js";
32
34
  import { ticketDefinitionDigest } from "../tickets/riskWaivers.js";
33
35
  import { hasRollbackOrMitigationPlan } from "../tickets/ticketLoader.js";
36
+ import { migrateTrackerRules } from "../tickets/renderMarkdown.js";
34
37
  let nestedTicketCommandDepth = 0;
35
38
  class TicketInterviewCancelled extends Error {
36
39
  }
37
40
  function cancelTicketInterview() { throw new TicketInterviewCancelled("ticket interview cancelled"); }
38
- /**
39
- * Ticket Maker may append an explanatory note after its machine envelope.
40
- * Preserve a schema-valid proposal before Foreman's strict generic marker
41
- * repair path replaces the response with a status-only correction.
42
- */
43
- export function acceptTicketPopulationCompletion(text) {
44
- try {
45
- extractTicketPopulationProposal(text);
46
- }
47
- catch {
48
- return undefined;
49
- }
50
- const status = parseStepStatus(text, { requireFinalLine: false });
51
- return status.kind === "done" || status.kind === "plan_complete" ? status : undefined;
52
- }
53
41
  function fail(msg) {
54
42
  if (nestedTicketCommandDepth > 0)
55
43
  throw new Error(msg);
@@ -128,26 +116,6 @@ function optionWasProvided(command, name) {
128
116
  }
129
117
  const TICKET_POPULATE_ROLE = "ticket-maker";
130
118
  const RAFI_CONFIG_FILES = ["rafi-config.yaml", "project.yaml"];
131
- const RISK_NOTE_REPAIR_START = "RAFI_RISK_NOTE_REPAIR_START";
132
- const RISK_NOTE_REPAIR_END = "RAFI_RISK_NOTE_REPAIR_END";
133
- export function extractRiskNoteRepairProposal(output) {
134
- const start = output.lastIndexOf(RISK_NOTE_REPAIR_START);
135
- const end = output.lastIndexOf(RISK_NOTE_REPAIR_END);
136
- if (start < 0 || end <= start)
137
- throw new Error(`Ticket Maker repair output is missing ${RISK_NOTE_REPAIR_START}/${RISK_NOTE_REPAIR_END}`);
138
- const raw = output.slice(start + RISK_NOTE_REPAIR_START.length, end).trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
139
- let parsed;
140
- try {
141
- parsed = JSON.parse(raw);
142
- }
143
- catch (error) {
144
- throw new Error(`Ticket Maker risk-note repair is invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
145
- }
146
- const validated = riskNoteRepairProposalV1Schema.safeParse(parsed);
147
- if (!validated.success)
148
- throw new Error(`Ticket Maker risk-note repair contract validation failed:\n${zodIssues(validated.error).map((issue) => `- ${issue}`).join("\n")}`);
149
- return validated.data;
150
- }
151
119
  /** Validate a deliberately narrow repair: only exactly the missing rollback fields may change. */
152
120
  export function applyRiskNoteRepairProposal(tickets, proposal) {
153
121
  const targets = tickets.filter(ticket => ticket.risk !== "Low" && !hasRollbackOrMitigationPlan(ticket.rollback));
@@ -210,23 +178,13 @@ Return ticket content using Foreman's schema:
210
178
  - title, area, priority, size, risk, depends_on, summary, acceptance, required_tests, likely_files, rollback, notes.
211
179
  - Keep dependencies, acceptance criteria, testing expectations, file hints, risk notes, and implementation notes from the source material. For each ticket, use only the mapped approved slice's acceptance and required tests; do not copy plan-level acceptance criteria or add requirements about sibling slices or unfinished tickets. A ticket's acceptance must remain implementable and verifiable when its own slice is delivered.
212
180
  - Preserve additive-slice independence without inventing an absence test: each ticket's own behavior and tests should work against the shared approved baseline and must not depend on sibling implementation. Do not add wording such as "with the sibling absent/present," "without [sibling] present," or "with this slice alone" unless the approved plan explicitly requires sibling absence testing or an isolated branch. Otherwise use positive own-ticket checks, the shared project tests, and the combined-result check.
213
- - Preserve the approved slice's documentation policy exactly. Documentation is off by default; do not invent a documentation requirement because a docs directory exists. When documentation is required, retain its exact targets and criteria so the Builder and independent QA can enforce it.
181
+ - Rafi copies documentation policy and QA preparation decisions directly from the approved slice. Do not include these host-owned fields in your proposal or invent additional requirements. Preserve the approved acceptance and tests.
214
182
  - File names and likely_files are implementation hints, not an exhaustive allowlist. Do not invent acceptance criteria or tests that require an exact changed-file list; allow other relevant files needed to complete the approved work. Preserve explicit user-established security, privacy, access, and other safety restrictions. Rafi manages ticket lifecycle state; do not require the Builder to mark tickets done in a document.
215
183
  - For every Medium or High risk ticket, \`rollback\` is required. It must state a concrete rollback or mitigation action: what is disabled, reverted, or contained, and how recovery or impact is verified. \`notes\` may add context but never satisfies this requirement. Low-risk tickets may use \`rollback: null\`.
216
184
  - Do not store mutable status/progress fields in .tickets/tickets.yaml.
217
185
  - Dependencies must reference slice_ref values. Include the exact IDs of existing populated tickets whose removed slices should become obsolete.
218
186
 
219
- If source content does not cleanly map to the new schema, ask for guidance instead of guessing. Use:
220
- STEP_STATUS: needs_input | question="..." choices="..."
221
-
222
- Output one JSON object between ${TICKET_POPULATION_PROPOSAL_START} and ${TICKET_POPULATION_PROPOSAL_END} with shape:
223
- {"version":1,"plan_id":"approved plan ID","revision":1,"tickets":[{"slice_ref":"slc_low","title":"...","area":"...","priority":"P1","size":"S","risk":"Low","summary":"...","acceptance":["..."],"required_tests":["..."],"likely_files":["..."],"documentation":{"required":false,"targets":[],"criteria":[]},"depends_on":[],"rollback":null,"notes":null},{"slice_ref":"slc_medium","title":"...","area":"...","priority":"P1","size":"S","risk":"Medium","summary":"...","acceptance":["..."],"required_tests":["..."],"likely_files":["..."],"documentation":{"required":false,"targets":[],"criteria":[]},"depends_on":["slc_low"],"rollback":"Disable the feature flag, restore the previous handler, and verify the affected error rate returns to baseline.","notes":null}],"retirements":["exact existing ticket ID"]}
224
- Triple-check that every approved slice appears exactly once.
225
-
226
- End with exactly one marker line as the final non-empty line; do not append notes or commentary after it:
227
- STEP_STATUS: done | summary="populated Foreman tickets from existing project ticket sources"
228
- or
229
- STEP_STATUS: blocked | reason="why ticket population cannot proceed"`;
187
+ If source content does not cleanly map to the approved plan, return a needs-input or blocked result instead of guessing. Return the full proposal in result.payload of the native ticket-population response. The proposal contains version, plan_id, revision, tickets, and retirements. Do not allocate final ticket IDs.`;
230
188
  }
231
189
  /** A bounded correction loop ended without a publishable proposal. */
232
190
  export class TicketPopulationRiskCorrectionExhaustedError extends Error {
@@ -546,6 +504,18 @@ async function cmdSetupUpdateCli(opts) {
546
504
  saveSourceRegistry(dir, patch.sourceRegistry);
547
505
  registrySaved = true;
548
506
  syncTicketLimits(dir, next.limits);
507
+ if (isTicketsInitialized(dir)) {
508
+ const ticketsConfig = loadTicketsConfig(dir);
509
+ const trackerRulesPath = join(dir, ticketsConfig.paths.trackerRules);
510
+ if (existsSync(trackerRulesPath)) {
511
+ const before = readFileSync(trackerRulesPath, "utf8");
512
+ const migrated = migrateTrackerRules(before, ticketsConfig.paths.progressDoc);
513
+ if (migrated.disposition !== "unchanged") {
514
+ writeFileSync(trackerRulesPath, migrated.content, "utf8");
515
+ console.log(`foreman tickets setup: ${migrated.disposition === "replaced-legacy" ? "upgraded" : "refreshed"} Rafi-managed tracker rules`);
516
+ }
517
+ }
518
+ }
549
519
  console.log(`foreman tickets setup: updated ticket setup in ${join(dir, "rafi-config.yaml")}`);
550
520
  if (isTicketsInitialized(dir)) {
551
521
  const rows = cmdQueue(dir, 1);
@@ -1480,8 +1450,23 @@ export async function cmdPopulateCli(opts) {
1480
1450
  if (contextSources.length) {
1481
1451
  console.log(`foreman tickets: context sources ${contextSources.join(" ")}`);
1482
1452
  }
1483
- let turn = await foreman.runInstruction(buildPopulateInstruction(approvedPlan.path, contextSources, ticketsConfig.paths.progressDoc), { completion: acceptTicketPopulationCompletion });
1484
- planningRecord = checkpointInterview(dir, planningRecord, { runtime: { runtime: roleBuilder.runtime, model: roleBuilder.model, sessionId: builder.sessionId() }, decisions: { ...planningRecord.decisions, proposalResponse: turn.result.text } });
1453
+ const population = (instruction, correction) => streamTicketPopulation({
1454
+ projectDir: dir, interviewId: planningRecord.id, correction, instruction,
1455
+ checkpoint: scope => { planningRecord = checkpointInterview(dir, planningRecord, { decisions: { ...planningRecord.decisions, populationArtifact: scope } }); },
1456
+ dispatch: (instruction, policy) => foreman.runInstruction(instruction, policy),
1457
+ });
1458
+ const populateInstruction = buildPopulateInstruction(approvedPlan.path, contextSources, ticketsConfig.paths.progressDoc);
1459
+ let initialPopulation;
1460
+ try {
1461
+ initialPopulation = await population(populateInstruction, 0);
1462
+ }
1463
+ catch (error) {
1464
+ // One automatic restart of a failed population before stopping for a human.
1465
+ console.log(`foreman tickets: ticket population failed (${String(error).slice(0, 200)}); restarting it once from the first page.`);
1466
+ initialPopulation = await population(populateInstruction, 0);
1467
+ }
1468
+ let turn = initialPopulation.turn;
1469
+ planningRecord = checkpointInterview(dir, planningRecord, { runtime: { runtime: roleBuilder.runtime, model: roleBuilder.model, sessionId: builder.sessionId() }, decisions: { ...planningRecord.decisions, proposalResponse: turn.result.text, structuredResponse: turn.result.structured } });
1485
1470
  log.write("ticket-populate", {
1486
1471
  statusKind: turn.status.kind,
1487
1472
  summary: turn.status.summary,
@@ -1499,7 +1484,9 @@ export async function cmdPopulateCli(opts) {
1499
1484
  throw new Error(`needs human — ${turn.status.error ?? "builder did not emit done"}`);
1500
1485
  }
1501
1486
  const existing = loadTickets(resolveTicketPaths(ticketsConfig, dir).tickets);
1502
- let proposal = extractTicketPopulationProposal(turn.result.text);
1487
+ if (!turn.result.selectedDispatch)
1488
+ throw new Error("Ticket population response has no sealed dispatch tuple");
1489
+ let proposal = initialPopulation.proposal;
1503
1490
  let proposalArtifact = savePopulationProposalArtifact(dir, planningRecord.id, approvedPlan, proposal);
1504
1491
  let materialized;
1505
1492
  let populationWaiver;
@@ -1525,18 +1512,22 @@ export async function cmdPopulateCli(opts) {
1525
1512
  continue;
1526
1513
  }
1527
1514
  console.log(`foreman tickets: ${ids} need risk plans; asking Ticket Maker for a targeted correction (${correction + 1}/2)`);
1528
- const correctionTurn = await foreman.runInstruction(`Correct the existing ticket population proposal only. Do not change plan_id, revision, slice_ref mappings, ticket IDs, dependencies, scope, ownership, acceptance criteria, tests, retirements, or any field except \`rollback\` for these tickets: ${ids}. Each listed ticket is Medium or High risk and requires a concrete rollback or mitigation plan in \`rollback\`; \`notes\` cannot satisfy this requirement. Return the complete replacement JSON proposal between ${TICKET_POPULATION_PROPOSAL_START} and ${TICKET_POPULATION_PROPOSAL_END}, followed by STEP_STATUS: done.`);
1515
+ const correctedPopulation = await population(`Correct the existing ticket population proposal only. Do not change plan_id, revision, slice_ref mappings, ticket IDs, dependencies, scope, ownership, acceptance criteria, tests, retirements, or any field except \`rollback\` for these tickets: ${ids}. Each listed ticket is Medium or High risk and requires a concrete rollback or mitigation plan in \`rollback\`; \`notes\` cannot satisfy this requirement. Return the complete replacement proposal through native ticket-population pages.`, correction + 1);
1516
+ const correctionTurn = correctedPopulation.turn;
1529
1517
  if (correctionTurn.result.isError || (correctionTurn.status.kind !== "done" && correctionTurn.status.kind !== "plan_complete")) {
1530
1518
  throw new Error(`Ticket Maker could not correct missing risk plans for ${ids}: ${correctionTurn.status.reason ?? correctionTurn.status.error ?? correctionTurn.result.text.slice(0, 200)}`);
1531
1519
  }
1532
1520
  turn = correctionTurn;
1533
1521
  planningRecord = checkpointInterview(dir, planningRecord, {
1534
1522
  runtime: { runtime: roleBuilder.runtime, model: roleBuilder.model, sessionId: builder.sessionId() },
1535
- decisions: { ...planningRecord.decisions, proposalResponse: turn.result.text, riskPlanCorrections: correction + 1 },
1523
+ decisions: { ...planningRecord.decisions, proposalResponse: turn.result.text, structuredResponse: turn.result.structured, riskPlanCorrections: correction + 1 },
1536
1524
  });
1537
- const corrected = extractTicketPopulationProposal(turn.result.text);
1525
+ const corrected = correctedPopulation.proposal;
1538
1526
  assertRiskPlanOnlyCorrection(proposal, corrected, err.repairableRiskSliceRefs);
1539
1527
  proposal = corrected;
1528
+ if (!turn.result.selectedDispatch)
1529
+ throw new Error("Ticket population correction has no sealed dispatch tuple");
1530
+ // A correction is immutable lineage, never a rewrite of the terminal proposal.
1540
1531
  proposalArtifact = savePopulationProposalArtifact(dir, planningRecord.id, approvedPlan, proposal);
1541
1532
  }
1542
1533
  }
@@ -1599,13 +1590,12 @@ export async function cmdRepairRiskNotesCli(opts) {
1599
1590
  "You are Ticket Maker in read-only repair mode. Do not edit files.",
1600
1591
  "For every listed ticket, supply a concrete rollback or mitigation plan. Do not change any other field or add/remove tickets.",
1601
1592
  `Tickets requiring repair:\n${JSON.stringify(targets.map(ticket => ({ id: ticket.id, title: ticket.title, risk: ticket.risk, summary: ticket.summary, acceptance: ticket.acceptance, required_tests: ticket.required_tests })), null, 2)}`,
1602
- `Return exactly one JSON object between ${RISK_NOTE_REPAIR_START} and ${RISK_NOTE_REPAIR_END}: {"version":1,"tickets":[{"id":"T001","rollback":"Concrete reversal or mitigation steps."}]}.`,
1603
- "End with STEP_STATUS: done | summary=\"risk-note repair proposed\"",
1593
+ `Submit the native risk-note-repair payload: {"version":1,"tickets":[{"id":"T001","rollback":"Concrete reversal or mitigation steps."}]}.`,
1604
1594
  ].join("\n\n");
1605
- const turn = await foreman.runInstruction(instruction);
1595
+ const turn = await foreman.runInstruction(instruction, { output: riskNoteRepairOutput });
1606
1596
  if (turn.result.isError || (turn.status.kind !== "done" && turn.status.kind !== "plan_complete"))
1607
1597
  throw new Error(`Ticket Maker could not propose risk-plan repairs: ${turn.status.reason ?? turn.status.error ?? turn.result.text.slice(0, 300)}`);
1608
- const proposal = extractRiskNoteRepairProposal(turn.result.text);
1598
+ const proposal = operationPayload(turn.result, riskNoteRepairOutput);
1609
1599
  const repaired = applyRiskNoteRepairProposal(current, proposal);
1610
1600
  for (const target of targets)
1611
1601
  console.log(` ${target.id}: ${proposal.tickets.find(item => item.id === target.id).rollback.trim()}`);