ai-foreman 1.7.28 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude.d.ts +23 -12
  3. package/dist/adapters/claude.js +228 -24
  4. package/dist/adapters/codex.d.ts +17 -7
  5. package/dist/adapters/codex.js +195 -17
  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 +86 -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/planner.js +1 -4
  18. package/dist/branch/runner.d.ts +2 -1
  19. package/dist/branch/runner.js +10 -7
  20. package/dist/buildAdmission.js +4 -0
  21. package/dist/buildApproval.d.ts +30 -0
  22. package/dist/buildApproval.js +68 -0
  23. package/dist/buildAssignment.d.ts +1 -0
  24. package/dist/buildAssignment.js +82 -23
  25. package/dist/buildInterventions.d.ts +2 -2
  26. package/dist/buildInterventions.js +11 -4
  27. package/dist/buildLaunchGate.js +3 -2
  28. package/dist/builderGuidanceFollowup.js +17 -6
  29. package/dist/cli/manager.js +22 -20
  30. package/dist/cli/start.js +32 -24
  31. package/dist/cli/tickets.js +40 -24
  32. package/dist/continuity.d.ts +10 -14
  33. package/dist/continuity.js +198 -179
  34. package/dist/continuityOrigin.d.ts +13 -0
  35. package/dist/continuityOrigin.js +91 -0
  36. package/dist/foreman.d.ts +20 -5
  37. package/dist/foreman.js +57 -187
  38. package/dist/graph/boundary.d.ts +1 -1
  39. package/dist/graph/boundary.js +1 -1
  40. package/dist/graph/host.js +8 -19
  41. package/dist/graph/manager.d.ts +1 -1
  42. package/dist/graph/manager.js +1 -1
  43. package/dist/graph/preparation.d.ts +38 -0
  44. package/dist/graph/preparation.js +7 -6
  45. package/dist/graph/semantic.d.ts +1 -0
  46. package/dist/graph/semantic.js +4 -1
  47. package/dist/graph/session.js +7 -2
  48. package/dist/graph/turn.d.ts +2 -3
  49. package/dist/graph/turn.js +28 -40
  50. package/dist/handoffProtocol.js +2 -2
  51. package/dist/handoffs.d.ts +3 -1
  52. package/dist/handoffs.js +90 -59
  53. package/dist/hostGraphApplicability.d.ts +46 -0
  54. package/dist/hostGraphApplicability.js +97 -0
  55. package/dist/hostQaAugmentation.d.ts +18 -0
  56. package/dist/hostQaAugmentation.js +81 -0
  57. package/dist/hostQaComparison.d.ts +21 -2
  58. package/dist/hostQaComparison.js +43 -28
  59. package/dist/hostSourcePreservation.d.ts +38 -0
  60. package/dist/hostSourcePreservation.js +72 -0
  61. package/dist/interviews.d.ts +7 -0
  62. package/dist/interviews.js +18 -1
  63. package/dist/legacyCommunicationDisplay.d.ts +2 -0
  64. package/dist/legacyCommunicationDisplay.js +2 -0
  65. package/dist/managerPacket.js +3 -3
  66. package/dist/markers.d.ts +4 -3
  67. package/dist/markers.js +4 -20
  68. package/dist/probeIpc.d.ts +14 -0
  69. package/dist/probeIpc.js +29 -0
  70. package/dist/qaBuildGate.d.ts +21 -0
  71. package/dist/qaBuildGate.js +48 -12
  72. package/dist/qaBuilderCoverage.js +22 -13
  73. package/dist/qaBuilderEvidence.d.ts +16 -1
  74. package/dist/qaBuilderEvidence.js +48 -8
  75. package/dist/qaContractCoverage.d.ts +2 -2
  76. package/dist/qaContractCoverage.js +11 -7
  77. package/dist/qaContractDelivery.js +34 -20
  78. package/dist/qaCoverageAssembly.d.ts +22 -0
  79. package/dist/qaCoverageAssembly.js +16 -0
  80. package/dist/qaDeliveryJournal.d.ts +2 -0
  81. package/dist/qaDeliveryJournal.js +19 -0
  82. package/dist/qaEffectiveConfig.d.ts +1 -0
  83. package/dist/qaEffectiveConfig.js +6 -1
  84. package/dist/qaFailureDelivery.js +97 -85
  85. package/dist/qaHostTransformation.d.ts +26 -0
  86. package/dist/qaHostTransformation.js +100 -0
  87. package/dist/qaPreparation.d.ts +8 -20
  88. package/dist/qaPreparation.js +133 -229
  89. package/dist/qaPreparationInputs.js +19 -0
  90. package/dist/qaPreparationStore.js +34 -6
  91. package/dist/qaPrerequisites.d.ts +2 -1
  92. package/dist/qaPrerequisites.js +21 -1
  93. package/dist/qaProtocolV2.d.ts +15 -0
  94. package/dist/qaRecovery.js +21 -4
  95. package/dist/qaReview.js +232 -394
  96. package/dist/qaReviewEvidence.d.ts +7 -3
  97. package/dist/qaReviewEvidence.js +15 -4
  98. package/dist/qaRuntime.js +1 -1
  99. package/dist/qaSnapshot.js +71 -21
  100. package/dist/qaStructuredResponse.d.ts +10 -0
  101. package/dist/qaStructuredResponse.js +57 -0
  102. package/dist/qaTimeline.js +5 -3
  103. package/dist/qaToolRecovery.d.ts +12 -0
  104. package/dist/qaToolRecovery.js +44 -0
  105. package/dist/qaVerificationContract.d.ts +3 -2
  106. package/dist/qaVerificationContract.js +58 -34
  107. package/dist/readinessGate.js +5 -4
  108. package/dist/runtimeReadiness.js +23 -15
  109. package/dist/statusReporter.d.ts +2 -0
  110. package/dist/statusReporter.js +2 -0
  111. package/dist/structuredCutover.d.ts +31 -0
  112. package/dist/structuredCutover.js +510 -0
  113. package/dist/structuredCutoverFiles.d.ts +10 -0
  114. package/dist/structuredCutoverFiles.js +181 -0
  115. package/dist/structuredDispatchAuthority.d.ts +22 -0
  116. package/dist/structuredDispatchAuthority.js +110 -0
  117. package/dist/structuredDispatchRecord.d.ts +29 -0
  118. package/dist/structuredDispatchRecord.js +36 -0
  119. package/dist/structuredLiveBudget.d.ts +2 -0
  120. package/dist/structuredLiveBudget.js +23 -0
  121. package/dist/structuredOperation.d.ts +18 -0
  122. package/dist/structuredOperation.js +257 -0
  123. package/dist/supervisedStart.d.ts +1 -1
  124. package/dist/supervisedStart.js +78 -31
  125. package/dist/supervisor.d.ts +18 -2
  126. package/dist/supervisor.js +74 -17
  127. package/dist/ticketPlanning.d.ts +1 -373
  128. package/dist/ticketPlanning.js +2 -26
  129. package/dist/ticketPopulation.d.ts +0 -3
  130. package/dist/ticketPopulation.js +4 -21
  131. package/dist/ticketPopulationPageAuthority.d.ts +13 -0
  132. package/dist/ticketPopulationPageAuthority.js +30 -0
  133. package/dist/ticketPopulationPaging.d.ts +27 -0
  134. package/dist/ticketPopulationPaging.js +105 -0
  135. package/dist/tickets/renderMarkdown.d.ts +14 -0
  136. package/dist/tickets/renderMarkdown.js +39 -5
  137. package/dist/tickets/ticketLoader.js +10 -0
  138. package/dist/tickets/ticketSchema.d.ts +107 -0
  139. package/dist/tickets/ticketSchema.js +4 -1
  140. package/dist/workflowDb.d.ts +100 -2
  141. package/dist/workflowDb.js +781 -35
  142. package/dist/workflowReader.d.ts +1 -0
  143. package/dist/workflowReader.js +9 -0
  144. package/package.json +15 -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";
@@ -11,7 +12,7 @@ import { captureFrozenQaSource } from "../qaSnapshot.js";
11
12
  import { finishStartAdmission, superviseStart, supervisedRunId } from "../supervisedStart.js";
12
13
  import { assertBuildAssignmentReconciled, isInterruptedBuilderRecovery, assertInterruptedSourceSnapshot } from "../buildAssignment.js";
13
14
  import { durableHumanDecision, HumanDecisionRequired, HumanDecisionCancelled, servicePendingHumanDecisions } from "../humanDecision.js";
14
- import { requiresBuildApproval, buildScopeRevision, createBuildApprovalGate } from "../buildApproval.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";
@@ -693,6 +694,20 @@ export function buildStartCommand(options = {}) {
693
694
  ? recoveryRecord.recoveryDecision?.planUpdateApproval === "auto"
694
695
  && (recoveryRecord.recoveryDecision.planUpdateApprovalSource === "explicit-yes" || recoveryRecord.recoveryDecision.planUpdateApprovalSource === "explicit-choice")
695
696
  : Boolean(opts.yes);
697
+ const recoveryTickets = recoveryRecord?.recoveryDecision?.executionTickets ?? recoveryRecord?.tickets ?? [];
698
+ // Empty-scope recovery is used by established/supervisor lifecycle
699
+ // recovery fixtures and has no ticket plan to bind. Ticket-bearing
700
+ // recovery remains fail-closed on an exact current-plan approval.
701
+ if (recoveryRecord && autoApprovePlanUpdates && recoveryTickets.length > 0) {
702
+ const decision = recoveryRecord.recoveryDecision;
703
+ if (!decision?.planApproval && !decision?.frozenWorkApproval)
704
+ fail("saved --yes approval has no exact plan binding; rerun rafi build:resume --yes to approve the current plan");
705
+ const approvalCheck = decision.frozenWorkApproval
706
+ ? validateFrozenWorkApprovalBinding(cwd, recoveryRecord.runId, recoveryTickets, decision.frozenWorkApproval)
707
+ : validatePlanApprovalBinding(cwd, recoveryTickets, decision.planApproval);
708
+ if (!approvalCheck.valid)
709
+ fail(`saved --yes approval is stale (${approvalCheck.reason}); rerun rafi build:resume --yes before provider work`);
710
+ }
696
711
  if (opts.recoveryMode) {
697
712
  const allowed = ["exact-session", "fresh-with-handoff", "fresh-recovery-only", "guided-recovery"];
698
713
  if (!allowed.includes(String(opts.recoveryMode)))
@@ -1293,7 +1308,6 @@ export function buildStartCommand(options = {}) {
1293
1308
  try {
1294
1309
  const revision = countDb.continuityHead(runId, "builder")?.authoritativeStateRevision ?? settings.settings_revision;
1295
1310
  countDb.appendContinuityEvent({ runId, role: "builder", kind: "builder_requested_handoff_delta", payload: request.delta, authoritativeStateRevision: revision });
1296
- countDb.publishContinuityCheckpoint({ runId, role: "builder", delta: request.delta, authoritativeStateRevision: revision });
1297
1311
  compactionCount = countDb.successfulCompactionCount(runId, "builder", predecessor.sessionRef?.() ?? sessionId);
1298
1312
  }
1299
1313
  finally {
@@ -1306,6 +1320,7 @@ export function buildStartCommand(options = {}) {
1306
1320
  predecessorSessionId: sessionId,
1307
1321
  predecessorSessionRef: predecessor.sessionRef?.(),
1308
1322
  requestedByBuilder: true,
1323
+ predecessorDispatchId: request.predecessorDispatchId,
1309
1324
  roleState: {
1310
1325
  ...request.roleState,
1311
1326
  frozenActionDigest: createHash("sha256").update(frozenAction).digest("hex"),
@@ -1322,7 +1337,8 @@ export function buildStartCommand(options = {}) {
1322
1337
  resources: [{ label: "frozen-action", content: frozenAction, authoritative: true }],
1323
1338
  }, (_handoff, runtime) => createBuilderForSettings(builderCwd, settingsForRequestedRuntime(effectiveSettings, runtime)), handoffRecoveryOptions("builder"));
1324
1339
  log.write("handoff-transfer", {
1325
- requestedByBuilder: true, reason: request.reason, generation: transfer.manifest.generation,
1340
+ requestedByBuilder: true,
1341
+ predecessorDispatchId: request.predecessorDispatchId, reason: request.reason, generation: transfer.manifest.generation,
1326
1342
  predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId,
1327
1343
  acceptanceCheckpointDigest: transfer.acceptanceCheckpointDigest,
1328
1344
  });
@@ -2015,10 +2031,13 @@ export function buildStartCommand(options = {}) {
2015
2031
  try {
2016
2032
  auditBuilder = await createBuilder(cwd);
2017
2033
  auditViewer = printEvents(auditBuilder.events());
2018
- 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" });
2034
+ 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 }));
2019
2035
  if (audit.isError)
2020
- throw new Error(audit.text);
2021
- const auditDependencies = parseAuditDependencies(audit.text);
2036
+ throw new Error(structuredTurnErrorMessage(audit, "Branch dependency audit"));
2037
+ const auditDependencies = operationPayload(audit, branchAuditOutput).dependencies;
2038
+ const selectedIds = new Set(plan.nodes.map(node => node.ticket.id));
2039
+ if (auditDependencies.some(dep => dep.ticket === dep.dependsOn || !selectedIds.has(dep.ticket) || !selectedIds.has(dep.dependsOn)))
2040
+ throw new Error("Branch audit returned a foreign or self dependency");
2022
2041
  plan = buildBranchPlan(tickets, states, {
2023
2042
  steps,
2024
2043
  baseRef: opts.base ?? loadTicketSetupConfig(cwd)?.build.base_branch ?? currentGitRef(cwd),
@@ -3134,8 +3153,7 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
3134
3153
  `User discussion: ${String(discussion)}`,
3135
3154
  `Complete QA history:\n${JSON.stringify(context.history, null, 2)}`,
3136
3155
  `${diffTruncated ? "Builder diff (TRUNCATED at 128 KiB; inspect the actual worktree before relying on it)" : "Full Builder diff"}:\n${diff || "(no tracked diff)"}`,
3137
- "Return RAFI_QA_REMEDIATION_START, then one JSON object {\"summary\":\"...\",\"fix_instructions\":[\"...\"]}, then RAFI_QA_REMEDIATION_END.",
3138
- "End with STEP_STATUS: plan_complete | summary=\"QA remediation proposed\"",
3156
+ "Submit the native planner-remediation payload with summary and fix_instructions.",
3139
3157
  ].join("\n\n");
3140
3158
  let resumeSessionId;
3141
3159
  let revision = 0;
@@ -3143,24 +3161,14 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
3143
3161
  revision++;
3144
3162
  const proposalDigest = createHash("sha256").update(JSON.stringify([scope, round, revision, instruction, planner, captureFrozenQaSource(context.builderWorktree).digest])).digest("hex");
3145
3163
  const run = await durableReadOnlyProposal({ projectDir, runId, ticketId: context.ticket.id, digest: proposalDigest, operation: () => runRoleInstruction({
3146
- projectDir, role: "planner", instruction, label: "QA Planner remediation", agent: planner?.make,
3164
+ projectDir, role: "planner", instruction, output: plannerRemediationOutput, label: "QA Planner remediation", agent: planner?.make,
3147
3165
  model: explicitDefaultValue(planner?.model), effort: explicitEffort(planner?.reasoning), fast: planner?.fast,
3148
3166
  resumeSessionId, permissionConfig: readOnlyPermissionConfig(), sandboxMode: "read-only",
3149
3167
  logPath: makeRoleLogPath(projectDir, "qa-remediation"), logEvent: "rafi-plan",
3150
3168
  }) });
3151
- if (run.turn.result.isError || run.turn.status.kind !== "plan_complete")
3169
+ if (run.turn.result.isError || run.turn.status.kind !== "done")
3152
3170
  throw new Error(`Planner remediation failed: ${run.turn.status.error ?? run.turn.result.text.slice(0, 500)}`);
3153
- const match = /RAFI_QA_REMEDIATION_START\s*([\s\S]*?)\s*RAFI_QA_REMEDIATION_END/.exec(run.turn.result.text);
3154
- if (!match)
3155
- throw new Error("Planner remediation did not return the required structured proposal");
3156
- let parsed;
3157
- try {
3158
- parsed = JSON.parse(match[1].replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, ""));
3159
- }
3160
- catch (error) {
3161
- throw new Error(`Planner remediation proposal is invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
3162
- }
3163
- const proposal = parseProviderContract(qaRemediationProposalV1Schema, parsed, "RAFI_QA_REMEDIATION");
3171
+ const proposal = operationPayload(run.turn.result, plannerRemediationOutput);
3164
3172
  console.log(`\nPlanner remediation: ${proposal.summary}\n${proposal.fix_instructions.map((item) => `- ${item}`).join("\n")}`);
3165
3173
  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: [
3166
3174
  { value: "approve", label: "Approve (Recommended)" }, { value: "revise", label: "Discuss/revise" }, { value: "cancel", label: "Cancel and return to choices" },
@@ -3173,7 +3181,7 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
3173
3181
  if (isCancel(feedback))
3174
3182
  break;
3175
3183
  resumeSessionId = run.sessionId;
3176
- instruction = `Revise the complete QA remediation proposal using this feedback: ${String(feedback)}. Keep the exact structured envelope and final plan_complete marker.`;
3184
+ instruction = `Revise the complete QA remediation proposal using this feedback: ${String(feedback)}. Use the native planner-remediation contract.`;
3177
3185
  }
3178
3186
  }
3179
3187
  };
@@ -1,9 +1,11 @@
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, riskNoteRepairProposalV1Schema, zodIssues } 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";
@@ -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,6 +33,7 @@ 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
  }
@@ -195,23 +198,13 @@ Return ticket content using Foreman's schema:
195
198
  - title, area, priority, size, risk, depends_on, summary, acceptance, required_tests, likely_files, rollback, notes.
196
199
  - 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.
197
200
  - 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.
198
- - 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.
201
+ - 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.
199
202
  - 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.
200
203
  - 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\`.
201
204
  - Do not store mutable status/progress fields in .tickets/tickets.yaml.
202
205
  - Dependencies must reference slice_ref values. Include the exact IDs of existing populated tickets whose removed slices should become obsolete.
203
206
 
204
- If source content does not cleanly map to the new schema, ask for guidance instead of guessing. Use:
205
- STEP_STATUS: needs_input | question="..." choices="..."
206
-
207
- Output one JSON object between ${TICKET_POPULATION_PROPOSAL_START} and ${TICKET_POPULATION_PROPOSAL_END} with shape:
208
- {"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"]}
209
- Triple-check that every approved slice appears exactly once.
210
-
211
- End with exactly one marker line as the final non-empty line:
212
- STEP_STATUS: done | summary="populated Foreman tickets from existing project ticket sources"
213
- or
214
- STEP_STATUS: blocked | reason="why ticket population cannot proceed"`;
207
+ 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.`;
215
208
  }
216
209
  /** A bounded correction loop ended without a publishable proposal. */
217
210
  export class TicketPopulationRiskCorrectionExhaustedError extends Error {
@@ -531,6 +524,18 @@ async function cmdSetupUpdateCli(opts) {
531
524
  saveSourceRegistry(dir, patch.sourceRegistry);
532
525
  registrySaved = true;
533
526
  syncTicketLimits(dir, next.limits);
527
+ if (isTicketsInitialized(dir)) {
528
+ const ticketsConfig = loadTicketsConfig(dir);
529
+ const trackerRulesPath = join(dir, ticketsConfig.paths.trackerRules);
530
+ if (existsSync(trackerRulesPath)) {
531
+ const before = readFileSync(trackerRulesPath, "utf8");
532
+ const migrated = migrateTrackerRules(before, ticketsConfig.paths.progressDoc);
533
+ if (migrated.disposition !== "unchanged") {
534
+ writeFileSync(trackerRulesPath, migrated.content, "utf8");
535
+ console.log(`foreman tickets setup: ${migrated.disposition === "replaced-legacy" ? "upgraded" : "refreshed"} Rafi-managed tracker rules`);
536
+ }
537
+ }
538
+ }
534
539
  console.log(`foreman tickets setup: updated ticket setup in ${join(dir, "rafi-config.yaml")}`);
535
540
  if (isTicketsInitialized(dir)) {
536
541
  const rows = cmdQueue(dir, 1);
@@ -1465,8 +1470,14 @@ export async function cmdPopulateCli(opts) {
1465
1470
  if (contextSources.length) {
1466
1471
  console.log(`foreman tickets: context sources ${contextSources.join(" ")}`);
1467
1472
  }
1468
- let turn = await foreman.runInstruction(buildPopulateInstruction(approvedPlan.path, contextSources, ticketsConfig.paths.progressDoc));
1469
- planningRecord = checkpointInterview(dir, planningRecord, { runtime: { runtime: roleBuilder.runtime, model: roleBuilder.model, sessionId: builder.sessionId() }, decisions: { ...planningRecord.decisions, proposalResponse: turn.result.text } });
1473
+ const population = (instruction, correction) => streamTicketPopulation({
1474
+ projectDir: dir, interviewId: planningRecord.id, correction, instruction,
1475
+ checkpoint: scope => { planningRecord = checkpointInterview(dir, planningRecord, { decisions: { ...planningRecord.decisions, populationArtifact: scope } }); },
1476
+ dispatch: (instruction, policy) => foreman.runInstruction(instruction, policy),
1477
+ });
1478
+ const initialPopulation = await population(buildPopulateInstruction(approvedPlan.path, contextSources, ticketsConfig.paths.progressDoc), 0);
1479
+ let turn = initialPopulation.turn;
1480
+ 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 } });
1470
1481
  log.write("ticket-populate", {
1471
1482
  statusKind: turn.status.kind,
1472
1483
  summary: turn.status.summary,
@@ -1484,7 +1495,9 @@ export async function cmdPopulateCli(opts) {
1484
1495
  throw new Error(`needs human — ${turn.status.error ?? "builder did not emit done"}`);
1485
1496
  }
1486
1497
  const existing = loadTickets(resolveTicketPaths(ticketsConfig, dir).tickets);
1487
- let proposal = extractTicketPopulationProposal(turn.result.text);
1498
+ if (!turn.result.selectedDispatch)
1499
+ throw new Error("Ticket population response has no sealed dispatch tuple");
1500
+ let proposal = initialPopulation.proposal;
1488
1501
  let proposalArtifact = savePopulationProposalArtifact(dir, planningRecord.id, approvedPlan, proposal);
1489
1502
  let materialized;
1490
1503
  let populationWaiver;
@@ -1510,18 +1523,22 @@ export async function cmdPopulateCli(opts) {
1510
1523
  continue;
1511
1524
  }
1512
1525
  console.log(`foreman tickets: ${ids} need risk plans; asking Ticket Maker for a targeted correction (${correction + 1}/2)`);
1513
- 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.`);
1526
+ 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);
1527
+ const correctionTurn = correctedPopulation.turn;
1514
1528
  if (correctionTurn.result.isError || (correctionTurn.status.kind !== "done" && correctionTurn.status.kind !== "plan_complete")) {
1515
1529
  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)}`);
1516
1530
  }
1517
1531
  turn = correctionTurn;
1518
1532
  planningRecord = checkpointInterview(dir, planningRecord, {
1519
1533
  runtime: { runtime: roleBuilder.runtime, model: roleBuilder.model, sessionId: builder.sessionId() },
1520
- decisions: { ...planningRecord.decisions, proposalResponse: turn.result.text, riskPlanCorrections: correction + 1 },
1534
+ decisions: { ...planningRecord.decisions, proposalResponse: turn.result.text, structuredResponse: turn.result.structured, riskPlanCorrections: correction + 1 },
1521
1535
  });
1522
- const corrected = extractTicketPopulationProposal(turn.result.text);
1536
+ const corrected = correctedPopulation.proposal;
1523
1537
  assertRiskPlanOnlyCorrection(proposal, corrected, err.repairableRiskSliceRefs);
1524
1538
  proposal = corrected;
1539
+ if (!turn.result.selectedDispatch)
1540
+ throw new Error("Ticket population correction has no sealed dispatch tuple");
1541
+ // A correction is immutable lineage, never a rewrite of the terminal proposal.
1525
1542
  proposalArtifact = savePopulationProposalArtifact(dir, planningRecord.id, approvedPlan, proposal);
1526
1543
  }
1527
1544
  }
@@ -1584,13 +1601,12 @@ export async function cmdRepairRiskNotesCli(opts) {
1584
1601
  "You are Ticket Maker in read-only repair mode. Do not edit files.",
1585
1602
  "For every listed ticket, supply a concrete rollback or mitigation plan. Do not change any other field or add/remove tickets.",
1586
1603
  `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)}`,
1587
- `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."}]}.`,
1588
- "End with STEP_STATUS: done | summary=\"risk-note repair proposed\"",
1604
+ `Submit the native risk-note-repair payload: {"version":1,"tickets":[{"id":"T001","rollback":"Concrete reversal or mitigation steps."}]}.`,
1589
1605
  ].join("\n\n");
1590
- const turn = await foreman.runInstruction(instruction);
1606
+ const turn = await foreman.runInstruction(instruction, { output: riskNoteRepairOutput });
1591
1607
  if (turn.result.isError || (turn.status.kind !== "done" && turn.status.kind !== "plan_complete"))
1592
1608
  throw new Error(`Ticket Maker could not propose risk-plan repairs: ${turn.status.reason ?? turn.status.error ?? turn.result.text.slice(0, 300)}`);
1593
- const proposal = extractRiskNoteRepairProposal(turn.result.text);
1609
+ const proposal = operationPayload(turn.result, riskNoteRepairOutput);
1594
1610
  const repaired = applyRiskNoteRepairProposal(current, proposal);
1595
1611
  for (const target of targets)
1596
1612
  console.log(` ${target.id}: ${proposal.tickets.find(item => item.id === target.id).rollback.trim()}`);
@@ -1,5 +1,6 @@
1
1
  import type { ContinuityDelta, ResolvedAgentSettings } from "rafi-spec";
2
2
  import type { BuilderAdapter, CompactResult, ContextUsage, NativeAutoCompactionPolicy, NativeCompaction, ProviderSessionUsage, ProviderSettingSwitch, RuntimeFailure, TurnResult } from "./adapters/types.js";
3
+ import type { BuilderHandoffRequest } from "./handoffs.js";
3
4
  export declare const CONTINUITY_MARKER = "RAFI_CONTINUITY_DELTA:";
4
5
  export declare class ContinuityValidationError extends Error {
5
6
  readonly problems: string[];
@@ -22,12 +23,8 @@ export declare class SessionUnavailableContinuityError extends Error {
22
23
  readonly guidedRecovery: boolean;
23
24
  constructor(runId: string, role: "builder" | "qa", failure: RuntimeFailure);
24
25
  }
26
+ export declare const CONTINUITY_REQUIRED_INSTRUCTION = "Include the required bounded continuity object in the operation response; continuity cannot be null for this session.";
25
27
  export declare function validateContinuityDelta(value: unknown): ContinuityDelta;
26
- export declare function parseContinuityDelta(text: string): {
27
- delta?: ContinuityDelta;
28
- cleanText: string;
29
- error?: ContinuityValidationError;
30
- };
31
28
  export declare function mergeContinuityDeltas(deltas: readonly ContinuityDelta[]): ContinuityDelta;
32
29
  export declare function baselineContinuityDelta(nextAction?: string): ContinuityDelta;
33
30
  export declare function continuityInstruction(role?: "builder" | "qa"): string;
@@ -53,7 +50,7 @@ export interface ContinuityAdapterOptions {
53
50
  /** Explicit compatibility recovery may replace a dead predecessor lease only after its first valid checkpoint. */
54
51
  replaceRecoveryLeaseAfterCheckpoint?: boolean;
55
52
  /** Host-owned validated Builder handoff request processing at a safe turn boundary. */
56
- handleHandoffRequest?: (text: string, predecessor: BuilderAdapter, frozenAction: string) => Promise<BuilderAdapter | undefined>;
53
+ handleHandoffRequest?: (text: string | BuilderHandoffRequest, predecessor: BuilderAdapter, frozenAction: string) => Promise<BuilderAdapter | undefined>;
57
54
  /** QA journals each provider turn separately; wrappers must not repair or redispatch inside one host turn. */
58
55
  durableSingleTurn?: boolean;
59
56
  }
@@ -79,15 +76,16 @@ export declare class ContinuityAdapter implements BuilderAdapter {
79
76
  /** Establish an explicitly requested fresh owner before context probes or work. */
80
77
  validateFreshRecovery(): Promise<void>;
81
78
  get agent(): "claude" | "codex";
82
- sendTurn(instruction: string, policy?: {
83
- handback?: boolean;
84
- responseOnly?: boolean;
85
- logicalActionId?: string;
86
- purpose?: import("./providerPhase.js").ProviderTurnPurpose;
87
- }): Promise<TurnResult>;
79
+ /** Every confirmed dispatch publishes a checkpoint, so continuity is mandatory. */
80
+ structuredDispatchRequirement(): import("./adapters/types.js").StructuredDispatchRequirement;
81
+ sendTurn(instruction: string, policy?: import("./adapters/types.js").TurnRequest): Promise<TurnResult>;
88
82
  private sendTurnOwned;
83
+ postConfirmedDispatch(reference: import("./adapters/types.js").ConfirmedDispatchReference): Promise<TurnResult>;
89
84
  observeEvents(listener: (event: import("./adapters/types.js").BuilderEvent) => void): () => void;
90
85
  acceptHandbackTurn(turn: TurnResult): void;
86
+ /** Explicit restart recovery of the already accepted action; never repeats acceptance. */
87
+ resumePlannedContinuation(): Promise<TurnResult | undefined>;
88
+ recoverConfirmedPublications(): Promise<void>;
91
89
  sessionId(): string | undefined;
92
90
  sessionRef(): import("rafi-spec").ProviderSessionRefV1 | undefined;
93
91
  prepareSession(): Promise<import("rafi-spec").ProviderSessionRefV1>;
@@ -109,9 +107,7 @@ export declare class ContinuityAdapter implements BuilderAdapter {
109
107
  /** Keep the observable wrapper and event stream stable while moving to an accepted successor. */
110
108
  adoptValidatedSuccessor(successor: BuilderAdapter): Promise<void>;
111
109
  private publish;
112
- private moveRecoveryLeaseAfterCheckpoint;
113
110
  private recordSessionUnavailable;
114
- private reconstructionHandoff;
115
111
  private revision;
116
112
  private pumpEvents;
117
113
  }