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/qaReview.js CHANGED
@@ -1,21 +1,27 @@
1
+ import { BoundedTurnEvents, boundedUtf8, receiptFingerprint } from "./boundedTurnEvidence.js";
2
+ import { describeNativeQaResponse, qaStructuredResponse, qaStructuredStatus } from "./qaStructuredResponse.js";
3
+ import { journalStructuredDispatch, operationInstruction, operationPayload, operationResponse, nativePolicy } from "./structuredOperation.js";
4
+ import { durableOwnerDispatch } from "./structuredDispatchAuthority.js";
5
+ import { providerWireContract, qaVerdictOutput, qaRecoveryOutput, initializationOutput, remediationOutput } from "rafi-spec";
1
6
  import { withGraphDerivedAccess } from "./graph/derived.js";
2
7
  import { dispatchWithGraphAccess } from "./graph/session.js";
3
8
  import { finalQaRoleInstructions, QA_COMMAND_DISCIPLINE, resolveEffectiveQaConfiguration } from "./qaEffectiveConfig.js";
4
- import { COVERAGE_INSTRUCTIONS, bindCoverageHostFields, parseContractCoverage, validateContractCoverage } from "./qaContractCoverage.js";
9
+ import { COVERAGE_INSTRUCTIONS, bindCoverageHostFields, validateContractCoverage } from "./qaContractCoverage.js";
5
10
  import { contractDigest, renderVerificationContract } from "./qaVerificationContract.js";
6
11
  import { finalReviewContractContext } from "./qaContractDelivery.js";
7
12
  import { qaCommandOutputExcerptsAllowed, retainedQaCommandObservations } from "./qaReviewEvidence.js";
13
+ import { retainedHostBuilderObservations } from "./qaBuilderEvidence.js";
8
14
  import { qaHostGraphFact } from "./qaHostFacts.js";
9
- import { graphContext, parseGraphRequest, prepareGraphInstruction, sendGraphTurn } from "./graph/turn.js";
15
+ import { graphContext, prepareGraphInstruction, sendGraphTurn } from "./graph/turn.js";
10
16
  import { BuildControlBoundary } from "./buildInterventions.js";
11
17
  import { formatRecoveryCommand } from "./recoveryGuidance.js";
12
18
  import { assertBuildAssignmentReconciled, BuildAssignmentRejected, captureBuildSource } from "./buildAssignment.js";
13
19
  import { checkQaPrerequisites } from "./qaPrerequisites.js";
14
20
  import { boundedQaHistory } from "./qaHandbackHistory.js";
15
- import { QA_FAILURE_REPORT_END, QA_FAILURE_REPORT_START, parseQaResponseContract, qaFailureReportV1Schema, } from "rafi-spec";
21
+ import { qaFailureReportV1Schema, } from "rafi-spec";
16
22
  import { SessionUnavailableError } from "./adapters/sessionFailure.js";
17
23
  import { currentActivity, withActivityPhase } from "./activity.js";
18
- import { buildQaInstruction, parseStepStatus } from "./foreman.js";
24
+ import { buildQaInstruction } from "./foreman.js";
19
25
  import { captureFrozenQaSourceAsync, captureProspectiveGitTree, createDisposableQaSnapshotAsync, prospectiveGitTreeMatches, QaSourceInstabilityError } from "./qaSnapshot.js";
20
26
  import { loadTicketSetupConfigWithDefaults } from "./tickets/setupConfig.js";
21
27
  import { randomUUID } from "node:crypto";
@@ -24,11 +30,11 @@ import { resolve } from "node:path";
24
30
  import { WorkflowDb } from "./workflowDb.js";
25
31
  import { canonicalSessionPath } from "./sessionIdentity.js";
26
32
  import { loadProjectAutonomyConfig, resolveAutonomyPolicy } from "./recoveryPolicy.js";
27
- import { appendQaRecoveryAttempt, appendQaRecoveryResource, appendQaRecoveryReviewedState, compareQaRecoveryReviewedState, createQaRecoveryPacket, ensureQaRecoveryExcluded, loadQaRecoveryPacket, materializeQaRecoveryContext, qaRecoveryInventory, qaReportDigest, renderQaRecoveryAcknowledgementInstruction, updateQaRecoveryPosition, updateQaRecoveryReviewIdentity, validateManualQaReport, validateQaRecoveryAcknowledgement, } from "./qaRecovery.js";
33
+ import { appendQaRecoveryAttempt, appendQaRecoveryResource, appendQaRecoveryReviewedState, compareQaRecoveryReviewedState, createQaRecoveryPacket, ensureQaRecoveryExcluded, loadQaRecoveryPacket, materializeQaRecoveryContext, qaRecoveryInventory, qaReportDigest, renderQaRecoveryAcknowledgementInstruction, updateQaRecoveryPosition, updateQaRecoveryReviewIdentity, validateManualQaReport, } from "./qaRecovery.js";
28
34
  import { canonicalJson, createQaFindingRefs, qaDigest } from "./qaProtocolV2.js";
29
- import { createHostQaComparison, findHostQaCheck } from "./hostQaComparison.js";
35
+ import { createHostQaComparison, findHostQaCheck, inspectHostQaComparison } from "./hostQaComparison.js";
30
36
  import { createHostGraphApplicability, findGraphQaChecks } from "./hostGraphApplicability.js";
31
- import { applyHostQaAugmentations } from "./hostQaAugmentation.js";
37
+ import { assembleFinalQaCoverage } from "./qaCoverageAssembly.js";
32
38
  import { qaUnresolvedToolDecision, recordQaUnresolvedTools } from "./qaToolRecovery.js";
33
39
  function ensureProtocolPausePacket(opts, head, stage, detail, frozenState) {
34
40
  const db = new WorkflowDb(opts.recovery.projectDir);
@@ -77,6 +83,9 @@ function pauseQaReview(opts, stage, detail, frozenState) {
77
83
  * The allowance is shared by all protocol failures for this ticket and survives restart.
78
84
  * oneReview's finally closes the predecessor before the loop allocates a new snapshot.
79
85
  */
86
+ /** Frozen source of the review an automatic fresh retry replaces, so a retry
87
+ * that cannot be dispatched safely still pauses at that exact boundary. */
88
+ const freshRetryState = new WeakMap();
80
89
  function retryFreshQaReview(opts, stage, detail, frozenState) {
81
90
  const db = new WorkflowDb(opts.recovery.projectDir);
82
91
  let retry = false;
@@ -95,6 +104,7 @@ function retryFreshQaReview(opts, stage, detail, frozenState) {
95
104
  }
96
105
  if (!retry)
97
106
  return pauseQaReview(opts, stage, `${detail}; automatic fresh QA review allowance exhausted`, frozenState);
107
+ freshRetryState.set(opts, frozenState);
98
108
  opts.resumedRecovery = undefined;
99
109
  currentActivity()?.update("qa-recovery", `${opts.ticket.id}: ${detail}. Running a complete fresh QA review.`);
100
110
  opts.evidence?.({ cycle: 0, outcome: "qa_fresh_review_retry", detail });
@@ -266,7 +276,7 @@ async function runIsolatedQaOwned(opts) {
266
276
  if (!opts.deliverBuilderFollowup)
267
277
  return { outcome: "needs-human", detail: "Builder guidance is waiting; resume through the build owner with a validated Builder session" };
268
278
  const latest = durableReviews.at(-1);
269
- const instruction = `Builder guidance follow-up after explicit resume.\nTicket scope: ${opts.ticket.id}. Do not substitute another ticket.\nPreserve the previously completed work and all mandatory requirements. Apply only the authorized waiting guidance within the approved ticket scope. If broader permission is needed, stop and explain it.\nApproved ticket requirements:\n${JSON.stringify(opts.ticket)}\nReport the instruction IDs you applied with evidence, then finish with STEP_STATUS: done | ticket="${opts.ticket.id}" summary="...".`;
279
+ const instruction = `Builder guidance follow-up after explicit resume.\nTicket scope: ${opts.ticket.id}. Do not substitute another ticket.\nPreserve the previously completed work and all mandatory requirements. Apply only the authorized waiting guidance within the approved ticket scope. If broader permission is needed, stop and explain it.\nApproved ticket requirements:\n${JSON.stringify(opts.ticket)}\nReport the instruction IDs you applied with evidence in the native work-report payload.`;
270
280
  const fix = await opts.deliverBuilderFollowup(instruction, { reviewAttemptId: latest.attemptId, requirementsDigest: qaDigest("admitted-requirements", opts.ticket), maximum: maxBuilderFixes, authorizationId: opts.qaRemediationAuthorization });
271
281
  opts.qaRemediationAuthorization = undefined;
272
282
  if (!fix.ok) {
@@ -359,7 +369,18 @@ async function runIsolatedQaOwned(opts) {
359
369
  opts.qaHistory = [...history];
360
370
  reviewAttempt += 1;
361
371
  const attemptId = randomUUID();
362
- const review = await withActivityPhase(`running QA review ${cycle} (${automaticFixes}/${maxBuilderFixes} fixes used)`, () => observedQaReview(opts, { cycle, reviewAttempt, remediationGeneration, attemptId }));
372
+ let review;
373
+ try {
374
+ review = await withActivityPhase(`running QA review ${cycle} (${automaticFixes}/${maxBuilderFixes} fixes used)`, () => observedQaReview(opts, { cycle, reviewAttempt, remediationGeneration, attemptId }));
375
+ }
376
+ catch (error) {
377
+ // An automatic fresh review that graph-access revocation blocks before
378
+ // dispatch pauses safely instead of failing the build.
379
+ const frozen = freshRetryState.get(opts);
380
+ if (frozen && /Graph access changed/.test(String(error)))
381
+ return pauseQaReview(opts, "graph-access-changed", String(error).replace(/^Error: /, ""), frozen);
382
+ throw error;
383
+ }
363
384
  if (review.outcome === "retry-modification") {
364
385
  const durableFixes = persistedQaFixCount(opts);
365
386
  cycle += Math.max(0, durableFixes - automaticFixes);
@@ -517,13 +538,13 @@ async function observedQaFix(opts, phase, request, causingAttemptId, remediation
517
538
  markQaRecoveryAction(opts, "builder-remediation-in-flight");
518
539
  }
519
540
  const result = await withActivityPhase(phase, () => deliverQaFailureOrLegacyFix(opts, request, causingAttemptId));
520
- const responseStatus = typeof result.response === "string" ? parseStepStatus(result.response) : undefined;
541
+ const responseStatus = result.outcome === "remediation-reported" ? { kind: "done" } : undefined;
521
542
  const accepted = result.ok
522
543
  && typeof result.response === "string" && result.response.length > 0
523
544
  && typeof result.summary === "string" && result.summary.length > 0
524
545
  && typeof result.providerTurnId === "string" && result.providerTurnId.length > 0
525
546
  && responseStatus?.kind === "done";
526
- const normalized = accepted ? result : result.ok ? { ...result, ok: false, detail: result.detail ?? "Builder remediation success requires an actual response, bounded summary, provider turn identity, and STEP_STATUS: done" } : result;
547
+ const normalized = accepted ? result : result.ok ? { ...result, ok: false, detail: result.detail ?? "Builder remediation success requires an actual response, bounded summary, provider turn identity, and a validated completed remediation outcome" } : result;
527
548
  finishQaFixAttempt(opts, durable, normalized.ok ? "succeeded" : "failed", normalized.detail, normalized.response, normalized.summary, normalized.providerTurnId);
528
549
  if (normalized.ok)
529
550
  markQaRecoveryAction(opts, "qa-full-review");
@@ -644,16 +665,20 @@ function finishQaFixAttempt(opts, attempt, outcome, detail, response, summary, p
644
665
  return;
645
666
  const db = new WorkflowDb(opts.recovery.projectDir);
646
667
  try {
647
- const responseDigest = typeof response === "string" ? db.putEvidence("qa", Buffer.from(response)) : undefined;
648
- const summaryDigest = typeof summary === "string" ? db.putEvidence("qa", Buffer.from(boundedBuilderSummary(summary))) : undefined;
668
+ let responseDigest = typeof response === "string" ? db.putEvidence("qa", Buffer.from(response)) : undefined;
669
+ let summaryDigest = typeof summary === "string" ? db.putEvidence("qa", Buffer.from(boundedBuilderSummary(summary))) : undefined;
649
670
  const head = db.qaTicketHead(opts.recovery.runId, opts.ticket.id);
650
671
  let receipt;
651
672
  if (outcome === "succeeded") {
652
- if (!responseDigest || !summaryDigest)
653
- throw new Error("successful Builder remediation is missing response evidence");
654
- if (!providerTurnId)
655
- throw new Error("successful Builder remediation is missing its provider turn identity");
656
- receipt = { version: 2, operationId: attempt.operationId, runId: opts.recovery.runId, ticketId: opts.ticket.id, reportDigest: attempt.reportDigest, sourceStateDigest: attempt.sourceStateDigest, requestDigest: attempt.requestDigest, responseDigest, summaryDigest, providerTurnId, completedAt: new Date().toISOString() };
673
+ const turn = db.qaDeliveryTurns(attempt.operationId).at(-1);
674
+ if (!turn || turn.status !== "completed" || !turn.sealedDispatch || !turn.rawResponseDigest || turn.providerTurnId !== providerTurnId)
675
+ throw new Error("successful Builder remediation requires its persisted native delivery receipt");
676
+ const native = db.replayStructuredDispatch(turn.turnRecordId, remediationOutput);
677
+ if (native.result.kind !== "completed" || native.result.payload.status !== "done")
678
+ throw new Error("Builder remediation has no completed native result");
679
+ responseDigest = turn.rawResponseDigest;
680
+ summaryDigest = db.putEvidence("qa", native.result.payload.report.summary);
681
+ receipt = { version: 3, operationId: attempt.operationId, runId: opts.recovery.runId, ticketId: opts.ticket.id, reportDigest: attempt.reportDigest, reportOccurrenceId: turn.reportOccurrenceId, turnRecordId: turn.turnRecordId, sourceStateDigest: attempt.sourceStateDigest, requestDigest: attempt.requestDigest, responseDigest, summaryDigest, providerTurnId: turn.providerTurnId, completedAt: new Date().toISOString() };
657
682
  }
658
683
  db.commitQaRemediationOutcome({ recoveryAttemptId: attempt.recoveryId, remediationAttemptId: attempt.remediationId, outcome, detail, responseDigest, summaryDigest, receipt, expectedRevision: head.revision });
659
684
  }
@@ -702,7 +727,6 @@ async function oneReview(opts, identity) {
702
727
  return { outcome: "needs-human", detail: `${error.message}. No QA session or report was created. Resume with: ${formatRecoveryCommand(opts.recovery.projectDir)}` };
703
728
  }
704
729
  let qa;
705
- let recoverySnapshot;
706
730
  let recoveryContext;
707
731
  let resumedPacket = opts.resumedRecovery;
708
732
  let resumedFullReviewNeedsBinding = false;
@@ -744,7 +768,7 @@ async function oneReview(opts, identity) {
744
768
  return { outcome: "needs-human", detail };
745
769
  }
746
770
  let graphReviewContext = graphContext(opts.recovery.projectDir, snapshot.path, "final-qa", handoff, { accessWorkspace: opts.builderWorktree, operationId: `graph-review:${attemptId}`, logicalTaskId: attemptId, sourceRef: snapshot.frozenState.digest, runId: opts.recovery.runId, workId: opts.ticket.id });
747
- let graphPrepared = opts.resumedRecovery ? { instruction: handoff, bytes: 0 } : await prepareGraphInstruction(handoff, graphReviewContext, await qa.contextUsage?.());
771
+ let graphPrepared = opts.resumedRecovery ? { instruction: handoff, bytes: 0 } : await prepareGraphInstruction(handoff, graphReviewContext, await qa.contextUsage?.(), qaVerdictOutput);
748
772
  handoff = graphPrepared.instruction;
749
773
  const v2Binding = beginV2Review(opts, snapshot.frozenState, snapshot.path, handoff, qaHandle, { attemptId, cycle, remediationGeneration });
750
774
  handoff = v2Binding.reviewInstruction;
@@ -754,8 +778,6 @@ async function oneReview(opts, identity) {
754
778
  durableReviewStarted = true;
755
779
  if (resumedPacket)
756
780
  resumedPacket = v2Binding.recoveryPacket;
757
- let preparedTurn;
758
- let preparedContract;
759
781
  if (opts.resumedRecovery) {
760
782
  if (opts.resumedRecovery.manifest.ticketId !== opts.ticket.id)
761
783
  return { outcome: "needs-human", detail: `QA recovery packet ticket ${opts.resumedRecovery.manifest.ticketId} does not match current ticket ${opts.ticket.id}` };
@@ -774,24 +796,8 @@ async function oneReview(opts, identity) {
774
796
  catch (error) {
775
797
  const detail = error instanceof Error ? error.message : String(error);
776
798
  const synthetic = { text: "", rawResponse: "", cleanedResponse: "", isError: true, numTurns: 0, costUsd: 0 };
777
- const recovery = await repairResumedFailureReport(opts, identity, resumedPacket, snapshot, qaHandle, synthetic, [detail], qaEvents, undefined, true);
778
- if (recovery.outcome === "failed")
779
- return recovery.result;
780
- if (recovery.outcome === "retry")
781
- return { outcome: "retry-modification" };
782
- if (recovery.outcome === "passed")
783
- return recovery.result;
784
- qa = recovery.qa;
785
- recoverySnapshot = recovery.snapshot;
786
- if (recovery.outcome === "manual") {
787
- const text = `${QA_FAILURE_REPORT_START}\n${JSON.stringify(recovery.report)}\n${QA_FAILURE_REPORT_END}\nSTEP_STATUS: qa_fail | issues="operator repaired report"`;
788
- preparedTurn = { text, rawResponse: text, cleanedResponse: text, isError: false, numTurns: 0, costUsd: 0 };
789
- preparedContract = { valid: true, errors: [], status: "qa_fail", fields: { issues: "operator repaired report" }, report: recovery.report, rawReportJson: JSON.stringify(recovery.report) };
790
- }
791
- else {
792
- preparedTurn = recovery.turn;
793
- preparedContract = recovery.contract;
794
- }
799
+ const recovery = await retainInvalidResumedResponse(opts, identity, resumedPacket, snapshot, synthetic, [detail], qaEvents);
800
+ return recovery.result;
795
801
  }
796
802
  let comparison;
797
803
  try {
@@ -800,103 +806,50 @@ async function oneReview(opts, identity) {
800
806
  catch (error) {
801
807
  const reason = `QA recovery error: current reviewed state cannot be reconstructed: ${error instanceof Error ? error.message : String(error)}`;
802
808
  const synthetic = { text: "", rawResponse: "", cleanedResponse: "", isError: true, numTurns: 0, costUsd: 0 };
803
- const recovery = await repairResumedFailureReport(opts, identity, resumedPacket, snapshot, qaHandle, synthetic, [reason], qaEvents, undefined, true, reason);
804
- if (recovery.outcome === "failed")
805
- return recovery.result;
806
- if (recovery.outcome === "retry")
807
- return { outcome: "retry-modification" };
808
- if (recovery.outcome === "passed")
809
- return recovery.result;
810
- if (recovery.outcome === "manual")
811
- return { outcome: "needs-human", detail: `${reason}; a manual reconstruction cannot replace missing reviewed-state evidence` };
812
- qa = recovery.qa;
813
- recoverySnapshot = recovery.snapshot;
814
- preparedTurn = recovery.turn;
815
- preparedContract = recovery.contract;
816
- const recoveredState = (recovery.snapshot ?? snapshot).frozenState;
817
- comparison = { matches: true, originalDigest: recoveredState.digest, currentDigest: recoveredState.digest, drift: [], frozenState: recoveredState };
809
+ const recovery = await retainInvalidResumedResponse(opts, identity, resumedPacket, snapshot, synthetic, [reason], qaEvents);
810
+ return recovery.result;
818
811
  }
819
- if (!preparedTurn) {
820
- if (!comparison.matches) {
821
- resumedPacket = appendQaRecoveryResource(resumedPacket, `recovery-history/drift-${identity.attemptId}.json`, { originalDigest: comparison.originalDigest, currentDigest: comparison.currentDigest, drift: comparison.drift, priorReports: "historical" }, { purpose: "Deterministic current-source drift summary; every earlier report is historical" });
822
- if (resumedPacket.manifest.reviewedStateDigest !== snapshot.frozenState.digest) {
823
- resumedPacket = appendQaRecoveryReviewedState(resumedPacket, snapshot.frozenState, `reviewed-state/current-r${resumedPacket.manifest.revision + 1}`);
824
- }
825
- }
826
- try {
827
- recoveryContext = materializeQaRecoveryContext(resumedPacket, snapshot.path);
828
- bindQaRecoveryContext(qa, recoveryContext);
829
- }
830
- catch (error) {
831
- const detail = `QA recovery context could not be materialized: ${error instanceof Error ? error.message : String(error)}`;
832
- const synthetic = { text: "", rawResponse: "", cleanedResponse: "", isError: true, numTurns: 0, costUsd: 0 };
833
- const recovery = await repairResumedFailureReport(opts, identity, resumedPacket, snapshot, qaHandle, synthetic, [detail], qaEvents, undefined, true);
834
- if (recovery.outcome === "failed")
835
- return recovery.result;
836
- if (recovery.outcome === "retry")
837
- return { outcome: "retry-modification" };
838
- if (recovery.outcome === "passed")
839
- return recovery.result;
840
- qa = recovery.qa;
841
- recoverySnapshot = recovery.snapshot;
842
- if (recovery.outcome === "manual") {
843
- const text = `${QA_FAILURE_REPORT_START}\n${JSON.stringify(recovery.report)}\n${QA_FAILURE_REPORT_END}\nSTEP_STATUS: qa_fail | issues="operator repaired report"`;
844
- preparedTurn = { text, rawResponse: text, cleanedResponse: text, isError: false, numTurns: 0, costUsd: 0 };
845
- preparedContract = { valid: true, errors: [], status: "qa_fail", fields: { issues: "operator repaired report" }, report: recovery.report, rawReportJson: JSON.stringify(recovery.report) };
846
- }
847
- else {
848
- preparedTurn = recovery.turn;
849
- preparedContract = recovery.contract;
850
- }
851
- }
852
- if (!preparedTurn) {
853
- if (!recoveryContext)
854
- throw new Error("QA recovery context invariant failed after successful materialization");
855
- const acknowledgement = renderQaRecoveryAcknowledgementInstruction(resumedPacket, recoveryContext.relativePath);
856
- const acknowledged = await performRecoveryAcknowledgement(resumedPacket, qa, acknowledgement, qaEvents, Boolean(opts.continuityManaged), "resume-entry", recoveryContext);
857
- resumedPacket = acknowledged.packet;
858
- const ackErrors = acknowledged.errors;
859
- if (ackErrors.length) {
860
- if (acknowledged.retryable)
861
- return retryFreshQaReview(opts, "invalid-recovery-acknowledgement", `QA recovery acknowledgement was invalid: ${ackErrors.join("; ")}`, snapshot.frozenState);
862
- const synthetic = { text: "", rawResponse: "", cleanedResponse: "", isError: true, numTurns: 0, costUsd: 0 };
863
- const recovery = await repairResumedFailureReport(opts, identity, resumedPacket, snapshot, qaHandle, synthetic, ackErrors, qaEvents, recoveryContext, true);
864
- if (recovery.outcome === "failed")
865
- return recovery.result;
866
- if (recovery.outcome === "retry")
867
- return { outcome: "retry-modification" };
868
- if (recovery.outcome === "passed")
869
- return recovery.result;
870
- qa = recovery.qa;
871
- recoverySnapshot = recovery.snapshot;
872
- if (recovery.outcome === "manual") {
873
- const text = `${QA_FAILURE_REPORT_START}\n${JSON.stringify(recovery.report)}\n${QA_FAILURE_REPORT_END}\nSTEP_STATUS: qa_fail | issues="recovered report"`;
874
- preparedTurn = { text, rawResponse: text, cleanedResponse: text, isError: false, numTurns: 0, costUsd: 0 };
875
- preparedContract = { valid: true, errors: [], status: "qa_fail", fields: { issues: "recovered report" }, report: recovery.report, rawReportJson: JSON.stringify(recovery.report) };
876
- }
877
- else {
878
- preparedTurn = recovery.turn;
879
- preparedContract = recovery.contract;
880
- }
881
- }
882
- if (!preparedTurn) {
883
- try {
884
- recoveryContext.verify();
885
- }
886
- catch (error) {
887
- return pauseQaReview(opts, "recovery-context-mutation", error instanceof Error ? error.message : String(error), (recoverySnapshot ?? snapshot).frozenState);
888
- }
889
- handoff = `${handoff}\n\nThis is a fresh QA session. Perform a complete review of the current immutable snapshot; use every prior report only as historical evidence.${comparison.matches ? "" : ` The source has drifted. Original reviewed-state digest: ${comparison.originalDigest}. Current reviewed-state digest: ${comparison.currentDigest}. Deterministic drift inventory: ${JSON.stringify(comparison.drift)}.`}`;
890
- resumedFullReviewNeedsBinding = true;
891
- }
812
+ if (!comparison.matches) {
813
+ resumedPacket = appendQaRecoveryResource(resumedPacket, `recovery-history/drift-${identity.attemptId}.json`, { originalDigest: comparison.originalDigest, currentDigest: comparison.currentDigest, drift: comparison.drift, priorReports: "historical" }, { purpose: "Deterministic current-source drift summary; every earlier report is historical" });
814
+ if (resumedPacket.manifest.reviewedStateDigest !== snapshot.frozenState.digest) {
815
+ resumedPacket = appendQaRecoveryReviewedState(resumedPacket, snapshot.frozenState, `reviewed-state/current-r${resumedPacket.manifest.revision + 1}`);
892
816
  }
893
817
  }
818
+ try {
819
+ recoveryContext = materializeQaRecoveryContext(resumedPacket, snapshot.path);
820
+ bindQaRecoveryContext(qa, recoveryContext);
821
+ }
822
+ catch (error) {
823
+ const detail = `QA recovery context could not be materialized: ${error instanceof Error ? error.message : String(error)}`;
824
+ const synthetic = { text: "", rawResponse: "", cleanedResponse: "", isError: true, numTurns: 0, costUsd: 0 };
825
+ const recovery = await retainInvalidResumedResponse(opts, identity, resumedPacket, snapshot, synthetic, [detail], qaEvents);
826
+ return recovery.result;
827
+ }
828
+ if (!recoveryContext)
829
+ throw new Error("QA recovery context invariant failed after successful materialization");
830
+ const acknowledgement = renderQaRecoveryAcknowledgementInstruction(resumedPacket, recoveryContext.relativePath);
831
+ const acknowledged = await performRecoveryAcknowledgement(resumedPacket, qa, acknowledgement, qaEvents, Boolean(opts.continuityManaged), "resume-entry", recoveryContext);
832
+ resumedPacket = acknowledged.packet;
833
+ const ackErrors = acknowledged.errors;
834
+ if (ackErrors.length) {
835
+ const synthetic = { text: "", rawResponse: "", cleanedResponse: "", isError: true, numTurns: 0, costUsd: 0 };
836
+ const recovery = await retainInvalidResumedResponse(opts, identity, resumedPacket, snapshot, synthetic, ackErrors, qaEvents);
837
+ return recovery.result;
838
+ }
839
+ try {
840
+ recoveryContext.verify();
841
+ }
842
+ catch (error) {
843
+ return pauseQaReview(opts, "recovery-context-mutation", error instanceof Error ? error.message : String(error), snapshot.frozenState);
844
+ }
845
+ handoff = `${handoff}\n\nThis is a fresh QA session. Perform a complete review of the current immutable snapshot; use every prior report only as historical evidence.${comparison.matches ? "" : ` The source has drifted. Original reviewed-state digest: ${comparison.originalDigest}. Current reviewed-state digest: ${comparison.currentDigest}. Deterministic drift inventory: ${JSON.stringify(comparison.drift)}.`}`;
846
+ resumedFullReviewNeedsBinding = true;
894
847
  opts.resumedRecovery = undefined;
895
848
  }
896
849
  if (resumedFullReviewNeedsBinding) {
897
850
  const resumedAttemptId = randomUUID();
898
851
  graphReviewContext = { ...graphReviewContext, operationId: `graph-review:${resumedAttemptId}`, logicalTaskId: resumedAttemptId };
899
- graphPrepared = await prepareGraphInstruction(handoff, graphReviewContext, await qa.contextUsage?.());
852
+ graphPrepared = await prepareGraphInstruction(handoff, graphReviewContext, await qa.contextUsage?.(), qaVerdictOutput);
900
853
  handoff = graphPrepared.instruction;
901
854
  const resumedReviewBinding = beginV2Review(opts, snapshot.frozenState, snapshot.path, handoff, qaHandle, { attemptId: resumedAttemptId, cycle, remediationGeneration });
902
855
  bindQaTurnAdapter(qa, resumedReviewBinding);
@@ -905,61 +858,29 @@ async function oneReview(opts, identity) {
905
858
  }
906
859
  let graphSubturn = 0;
907
860
  const activeGraphBinding = qaTurnBindings.get(qa);
908
- let turn = preparedTurn ?? await sendGraphTurn(qa, handoff, { ...graphReviewContext, reviewBasisRef: activeGraphBinding.reviewBasisDigest }, undefined, (prompt) => sendDurableQaTurn(qa, prompt, qaEvents, graphSubturn++ === 0 ? "initial" : `graph-evidence-${graphSubturn}`), { ...graphPrepared, instruction: activeGraphBinding.reviewInstruction });
909
- let status = parseStepStatus(turn.text);
861
+ const turn = await sendGraphTurn(qa, handoff, { ...graphReviewContext, reviewBasisRef: activeGraphBinding.reviewBasisDigest }, nativePolicy(qaVerdictOutput), durableOwnerDispatch(activeGraphBinding.projectDir, qaVerdictOutput, { runId: activeGraphBinding.runId, actionId: activeGraphBinding.reviewAttemptId }, (prompt) => sendDurableQaTurn(qa, prompt, qaEvents, graphSubturn++ === 0 ? "initial" : `graph-evidence-${graphSubturn}`)), { ...graphPrepared, instruction: activeGraphBinding.reviewInstruction });
862
+ const status = qaStructuredStatus(turn);
910
863
  recoveryContext = qaTurnBindings.get(qa)?.recoveryContext;
911
864
  try {
912
- if (!preparedTurn)
913
- recoveryContext?.verify();
865
+ recoveryContext?.verify();
914
866
  await opts.observeNativeCompactions?.(qa);
915
867
  recoveryContext?.verify();
916
868
  }
917
869
  catch (error) {
918
- return pauseQaReview(opts, "recovery-context-mutation", error instanceof Error ? error.message : String(error), (recoverySnapshot ?? snapshot).frozenState);
919
- }
920
- let responseContract = preparedContract ?? parseQaResponseContract(turn.text);
921
- let responseProtocolError;
922
- if (status.kind !== "qa_fail" && status.kind !== "unknown" && !responseContract.valid) {
923
- responseProtocolError = `invalid QA response contract: ${responseContract.errors.join("; ")}`;
924
- }
925
- if (!turn.isError && status.kind === "blocked" && opts.resolveBlocked) {
926
- const reason = status.reason ?? "QA reported an unspecified blocker";
927
- turn = await sendDurableQaTurn(qa, [
928
- `Your QA review reported a blocker: ${reason}`,
929
- "Resolve the blocker if possible without changing project files. Otherwise return a precise STEP_STATUS: blocked marker.",
930
- "If you can finish the review, return the complete QA response contract.",
931
- ].join("\n\n"), qaEvents, "blocked-recovery");
932
- try {
933
- recoveryContext?.verify();
934
- }
935
- catch (error) {
936
- return pauseQaReview(opts, "recovery-context-mutation", error instanceof Error ? error.message : String(error), (recoverySnapshot ?? snapshot).frozenState);
937
- }
938
- status = parseStepStatus(turn.text);
939
- try {
940
- await opts.observeNativeCompactions?.(qa);
941
- recoveryContext?.verify();
942
- }
943
- catch (error) {
944
- return pauseQaReview(opts, "recovery-context-mutation", error instanceof Error ? error.message : String(error), (recoverySnapshot ?? snapshot).frozenState);
945
- }
946
- responseContract = parseQaResponseContract(turn.text);
947
- responseProtocolError = undefined;
948
- if (status.kind !== "qa_fail" && status.kind !== "unknown" && !responseContract.valid) {
949
- responseProtocolError = `invalid QA response contract after blocker recovery: ${responseContract.errors.join("; ")}`;
950
- }
870
+ return pauseQaReview(opts, "recovery-context-mutation", error instanceof Error ? error.message : String(error), snapshot.frozenState);
951
871
  }
872
+ const responseContract = qaStructuredResponse(turn);
952
873
  const unresolvedDecision = qaUnresolvedToolDecision(opts.recovery.projectDir, opts.recovery.runId, opts.ticket.id, qaEvents);
953
874
  if (unresolvedDecision.action !== "none") {
954
875
  recordQaUnresolvedTools(opts.recovery.projectDir, opts.recovery.runId, opts.ticket.id, qaEvents);
955
876
  if (unresolvedDecision.action === "retry")
956
- return retryFreshQaReview(opts, "unresolved-provider-tool", unresolvedDecision.detail, (recoverySnapshot ?? snapshot).frozenState);
957
- return pauseQaReview(opts, "unresolved-provider-tool", unresolvedDecision.detail, (recoverySnapshot ?? snapshot).frozenState);
877
+ return retryFreshQaReview(opts, "unresolved-provider-tool", unresolvedDecision.detail, snapshot.frozenState);
878
+ return pauseQaReview(opts, "unresolved-provider-tool", unresolvedDecision.detail, snapshot.frozenState);
958
879
  }
959
880
  opts.state.reviews += 1;
960
881
  opts.state.sessionId = qa.sessionId();
961
882
  opts.state.sessionRef = qa.sessionRef?.();
962
- const reviewedSnapshot = recoverySnapshot ?? snapshot;
883
+ const reviewedSnapshot = snapshot;
963
884
  const changes = await withActivityPhase("checking QA file changes", () => reviewedSnapshot.qaChanges());
964
885
  if (changes.length) {
965
886
  opts.state.modificationViolations += 1;
@@ -988,9 +909,13 @@ async function oneReview(opts, identity) {
988
909
  return { outcome: "retry-modification" };
989
910
  }
990
911
  if (turn.continuityErrors?.length && !turn.failure)
991
- return retryFreshQaReview(opts, "invalid-continuity", `QA continuity record was invalid: ${turn.continuityErrors.join("; ")}`, reviewedSnapshot.frozenState);
992
- if (responseProtocolError && !turn.isError && !turn.failure)
993
- return retryFreshQaReview(opts, "invalid-response-contract", responseProtocolError, reviewedSnapshot.frozenState);
912
+ return pauseQaReview(opts, "invalid-continuity", `QA continuity record was invalid: ${turn.continuityErrors.join("; ")}`, reviewedSnapshot.frozenState);
913
+ if (!responseContract.valid && !turn.failure) {
914
+ const retained = resumedPacket
915
+ ? await retainInvalidResumedResponse(opts, identity, resumedPacket, snapshot, turn, responseContract.errors, qaEvents, true)
916
+ : await retainInvalidQaResponse(opts, identity, snapshot, qa, qaHandle, handoff, turn, responseContract.errors, qaEvents, true);
917
+ return retained.result;
918
+ }
994
919
  if (turn.isError || turn.failure)
995
920
  return pauseQaReview(opts, "qa-turn-error", `QA turn failed: ${sanitizePreview(turn.text)}`, reviewedSnapshot.frozenState);
996
921
  if (status.kind === "blocked")
@@ -1017,64 +942,7 @@ async function oneReview(opts, identity) {
1017
942
  if (status.kind === "qa_fail" || responseContract.status === "qa_fail") {
1018
943
  let contract = responseContract;
1019
944
  if (!contract.valid || !contract.report) {
1020
- const recovery = resumedPacket
1021
- ? await repairResumedFailureReport(opts, identity, resumedPacket, snapshot, qaHandle, turn, contract.errors, qaEvents, recoveryContext)
1022
- : await repairInvalidFailureReport(opts, identity, snapshot, qa, qaHandle, handoff, turn, contract.errors, qaEvents);
1023
- if (recovery.outcome === "failed")
1024
- return recovery.result;
1025
- if (recovery.outcome === "retry")
1026
- return { outcome: "retry-modification" };
1027
- if (recovery.outcome === "passed")
1028
- return recovery.result;
1029
- if (recovery.outcome === "manual") {
1030
- qa = recovery.qa;
1031
- recoverySnapshot = recovery.snapshot;
1032
- contract = { valid: true, errors: [], status: "qa_fail", fields: {}, report: recovery.report };
1033
- }
1034
- else {
1035
- qa = recovery.qa;
1036
- recoverySnapshot = recovery.snapshot;
1037
- turn = recovery.turn;
1038
- contract = recovery.contract;
1039
- }
1040
- const recoveryChanges = await (recoverySnapshot ?? snapshot).qaChanges();
1041
- if (recoveryChanges.length)
1042
- return { outcome: "needs-human", detail: `QA modified files during report recovery: ${recoveryChanges.join(", ")}` };
1043
- activeBinding = qaTurnBindings.get(qa);
1044
- if (!activeBinding)
1045
- throw new Error("Recovered QA verdict is missing its durable source/review-basis binding");
1046
- const recoveredLiveSource = await captureFrozenQaSourceAsync(opts.builderWorktree, progress);
1047
- const recoveredSnapshot = recoverySnapshot ?? snapshot;
1048
- if (recoveredLiveSource.digest !== activeBinding.sourceStateDigest || recoveredSnapshot.frozenState.digest !== activeBinding.sourceStateDigest) {
1049
- const driftDb = new WorkflowDb(opts.recovery.projectDir);
1050
- try {
1051
- const head = driftDb.qaTicketHead(opts.recovery.runId, opts.ticket.id);
1052
- if (head.state === "turn-intended")
1053
- driftDb.transitionQa(opts.recovery.runId, opts.ticket.id, head.revision, { type: "source-drift" });
1054
- }
1055
- finally {
1056
- driftDb.close();
1057
- }
1058
- finishDurableQaReview(opts, activeBinding.reviewAttemptId, { status: "interrupted", detail: `Builder source drifted before recovered verdict acceptance (${activeBinding.sourceStateDigest} -> ${recoveredLiveSource.digest})` });
1059
- durableReviewFinished = true;
1060
- return { outcome: "retry-modification" };
1061
- }
1062
- if (contract.valid && contract.status === "qa_pass") {
1063
- if (missingPrerequisites.length)
1064
- return pauseQaReview(opts, "verification-not-run", "Required verification remains not_run; prerequisite recovery and fresh QA required", reviewedSnapshot.frozenState);
1065
- let certificate;
1066
- try {
1067
- certificate = finishV2Pass(opts, qa, contract.fields.summary ?? "qa_pass");
1068
- }
1069
- catch (error) {
1070
- const detail = error instanceof Error ? error.message : String(error);
1071
- return retryFreshQaReview(opts, "invalid-qa-coverage", `Recovered QA pass evidence was not parseable or complete: ${detail}`, reviewedSnapshot.frozenState);
1072
- }
1073
- markQaRecoveryResolved(opts);
1074
- durableReviewFinished = true;
1075
- opts.evidence?.({ cycle, reviewAttempt: activeBinding.reviewNumber, remediationGeneration, attemptId: activeBinding.reviewAttemptId, outcome: "passed", detail: contract.fields.summary ?? "qa_pass", durationMs: Math.max(0, performance.now() - started) });
1076
- return { outcome: "passed", summary: contract.fields.summary, passCertificateId: certificate.certificateId, sourceStateDigest: certificate.sourceStateDigest, reviewBasisDigest: certificate.reviewBasisDigest };
1077
- }
945
+ return pauseQaReview(opts, "invalid-native-qa-response", "Failed QA verdict has no validated failure report", reviewedSnapshot.frozenState);
1078
946
  }
1079
947
  const acceptedBinding = activeBinding;
1080
948
  const report = contract.report;
@@ -1100,7 +968,7 @@ async function oneReview(opts, identity) {
1100
968
  markQaRecoveryPendingBuilder(opts);
1101
969
  return { outcome: "failed", detail, report, reportDigest: digest, reviewAttempt: acceptedBinding.reviewNumber, reviewAttemptId: acceptedBinding.reviewAttemptId };
1102
970
  }
1103
- return retryFreshQaReview(opts, "unrecognized-qa-outcome", status.error ?? `QA returned ${status.kind}`, reviewedSnapshot.frozenState);
971
+ return pauseQaReview(opts, "unrecognized-qa-outcome", status.error ?? `QA returned ${status.kind}`, reviewedSnapshot.frozenState);
1104
972
  }
1105
973
  finally {
1106
974
  if (durableReviewStarted && !durableReviewFinished) {
@@ -1119,8 +987,6 @@ async function oneReview(opts, identity) {
1119
987
  }
1120
988
  if (qa)
1121
989
  await withActivityPhase("closing QA session", () => qa.close().catch(() => { }));
1122
- if (recoverySnapshot)
1123
- await recoverySnapshot.remove().catch(() => { });
1124
990
  await withActivityPhase("cleaning up disposable QA snapshot", () => opts.observer
1125
991
  ? opts.observer.span("cleanup", "cleaning up disposable QA snapshot", () => snapshot.remove())
1126
992
  : snapshot.remove());
@@ -1138,8 +1004,7 @@ function finishDurableQaReview(opts, attemptId, patch) {
1138
1004
  function qaFindingRefs(runId, ticketId, reviewAttemptId, reportDigest, rawFindingIds) {
1139
1005
  return createQaFindingRefs({ runId, ticketId, reviewAttemptId, reportDigest, rawFindingIds });
1140
1006
  }
1141
- async function repairResumedFailureReport(opts, identity, initialPacket, snapshot, originalHandle, originalTurn, originalErrors, qaEvents, recoveryContext, startAtMenu = false, requireFreshReviewReason) {
1142
- const originalQa = originalHandle.adapter;
1007
+ async function retainInvalidResumedResponse(opts, identity, initialPacket, snapshot, originalTurn, originalErrors, qaEvents, retryFresh = false) {
1143
1008
  const originalResponse = effectiveTurnText(originalTurn);
1144
1009
  let packet = appendQaRecoveryResource(initialPacket, `qa-responses/resumed-${identity.attemptId}.txt`, originalResponse, {
1145
1010
  purpose: "Exact first cleaned QA report response after resumed packet acknowledgement", exact: true,
@@ -1147,15 +1012,10 @@ async function repairResumedFailureReport(opts, identity, initialPacket, snapsho
1147
1012
  await awaitQaTurnEvent(qaEvents, originalTurn);
1148
1013
  packet = await appendTurnObservation(packet, `resumed-original-${identity.attemptId}`, originalTurn, originalTurn.hostInstruction, originalErrors, "resumed-fail-closed", qaEvents);
1149
1014
  packet = appendQaRecoveryResource(packet, `validation/resumed-${identity.attemptId}.json`, {
1150
- status: parseStepStatus(originalResponse), errors: originalErrors, recoveryStage: "resumed-fail-closed",
1015
+ status: describeNativeQaResponse(originalResponse).status, errors: originalErrors, recoveryStage: "resumed-fail-closed",
1151
1016
  }, { purpose: "Validation result for the first resumed QA report response" });
1152
- void packet;
1153
- void requireFreshReviewReason;
1154
- if (!originalTurn.isError && !originalTurn.failure) {
1155
- const result = retryFreshQaReview(opts, "resumed-invalid-report", "Resumed QA report was invalid; a complete fresh QA review is required", snapshot.frozenState);
1156
- return result.outcome === "retry-modification" ? { outcome: "retry" } : { outcome: "failed", result };
1157
- }
1158
- return { outcome: "failed", result: pauseQaReview(opts, "resumed-reconstruction-disabled", `Resumed QA recovery could not be validated: ${originalErrors.map(error => sanitizePreview(error)).join("; ")}; a complete fresh QA review is required`, snapshot.frozenState) };
1017
+ const resumedDetail = `Resumed QA response failed validation. Evidence retained at ${packet.directory}. ${originalErrors.map(error => sanitizePreview(error)).join("; ")}`;
1018
+ return { outcome: "failed", result: retryFresh ? retryFreshQaReview(opts, "resumed-invalid-contract", resumedDetail, snapshot.frozenState) : pauseQaReview(opts, "resumed-invalid-contract", resumedDetail, snapshot.frozenState) };
1159
1019
  }
1160
1020
  export function buildQaReviewHandoff(ticket, builderSummary, diffDigest, validationChecklist, changeSummary, history = []) {
1161
1021
  return [
@@ -1166,7 +1026,7 @@ export function buildQaReviewHandoff(ticket, builderSummary, diffDigest, validat
1166
1026
  `Prior authoritative QA/report and Builder-remediation history: ${history.length ? JSON.stringify(boundedQaHistory(history)) : "(none; perform a complete first review)"}`,
1167
1027
  `Failure report JSON Schema: ${JSON.stringify(qaFailureReportV1Schema)}`,
1168
1028
  QA_COMMAND_DISCIPLINE,
1169
- `A failing response must use exactly:\n${QA_FAILURE_REPORT_START}\n{...valid QaFailureReportV1 JSON...}\n${QA_FAILURE_REPORT_END}\nSTEP_STATUS: qa_fail | issues="short plain-text synopsis"`,
1029
+ "Submit the native qa-verdict payload. Every verdict, passed or failed, includes coverage and verifiedInstructionIds. Coverage holds the required coverage claims when a verification contract exists; otherwise it is null. A failed verdict also includes the complete failure report.",
1170
1030
  "Be very thorough. Don't leave anything out. Make sure all required fields are added. Triple check your work to ensure nothing is left out and all required fields are added and populated correctly.",
1171
1031
  "Do not change source, configuration, documentation, tickets, or control files. Only ignored cache, coverage, and build output is allowed.",
1172
1032
  ].join("\n");
@@ -1187,16 +1047,7 @@ export async function compactWithRetry(adapter) {
1187
1047
  export const QA_REPORT_RECOVERY_MENU = [
1188
1048
  "Try a complete fresh QA review.", "Manually fix saved JSON for human inspection.", "Give QA specific instructions for a fresh review.", "Pause.",
1189
1049
  ];
1190
- function correctionInstruction(errors, guidance) {
1191
- return [
1192
- "Report correction only. Reconstruct the already-completed QA review without rerunning tools, tests, or repository inspection.",
1193
- `The prior failure report was invalid: ${errors.join("; ")}`,
1194
- guidance ? `Operator guidance: ${guidance}` : "",
1195
- `Return exactly one valid envelope in this order:\n${QA_FAILURE_REPORT_START}\n{...valid JSON matching the supplied schema...}\n${QA_FAILURE_REPORT_END}\nSTEP_STATUS: qa_fail | issues="short plain-text synopsis"`,
1196
- "Do not omit any completed finding, check, evidence, or observation.",
1197
- ].filter(Boolean).join("\n\n");
1198
- }
1199
- async function repairInvalidFailureReport(opts, identity, snapshot, originalQa, qaHandle, reviewPrompt, originalTurn, originalErrors, qaEvents) {
1050
+ async function retainInvalidQaResponse(opts, identity, snapshot, originalQa, qaHandle, reviewPrompt, originalTurn, originalErrors, qaEvents, retryFresh = false) {
1200
1051
  const originalResponse = effectiveTurnText(originalTurn);
1201
1052
  const runId = opts.recovery?.runId ?? `volatile-${identity.attemptId}`;
1202
1053
  const continuity = qaContinuityRecoveryContext(opts);
@@ -1205,7 +1056,7 @@ async function repairInvalidFailureReport(opts, identity, snapshot, originalQa,
1205
1056
  projectDir: opts.recovery?.projectDir ?? opts.builderWorktree, frozenState: snapshot.frozenState,
1206
1057
  runId, ticketId: opts.ticket.id, cycle: identity.cycle, reviewAttempt: identity.reviewAttempt,
1207
1058
  reviewAttemptId: identity.attemptId,
1208
- recoveryStage: "same-session", correctionTurns: 0, reportJson: extractReportBody(originalResponse),
1059
+ recoveryStage: "same-session", correctionTurns: 0, reportJson: describeNativeQaResponse(originalResponse).reportJson ?? "{}\n",
1209
1060
  resources: {
1210
1061
  ticket: { value: opts.ticket, purpose: "Complete authoritative ticket definition" },
1211
1062
  "qa-review-prompt": { value: reviewPrompt, purpose: "Exact original QA review instruction", exactText: true },
@@ -1214,11 +1065,11 @@ async function repairInvalidFailureReport(opts, identity, snapshot, originalQa,
1214
1065
  "builder-response-history": { value: (opts.state.builderResponseHistory ?? []).filter((entry) => entry.ticketId === opts.ticket.id), purpose: "Every exact host-observed Builder completion/remediation response and its bounded prompt summary" },
1215
1066
  "original-host-prompt": { value: originalTurn.hostInstruction ?? reviewPrompt, purpose: "Exact host-requested original QA instruction", exactText: true },
1216
1067
  "original-provider-prompt": { value: originalTurn.providerInstruction ?? { unavailable: "adapter did not expose provider-dispatched instruction" }, purpose: "Exact provider-dispatched original QA instruction or explicit unavailable marker", exactText: typeof originalTurn.providerInstruction === "string" },
1217
- "original-raw-response": { value: originalTurn.rawResponse ?? originalResponse, purpose: "Exact raw provider response before cleanup", exactText: true },
1218
- "original-cleaned-response": { value: originalResponse, purpose: "Exact cleaned original response used for contract validation", exactText: true },
1219
- "original-validation": { value: { errors: originalErrors, parsedStatus: parseStepStatus(originalResponse), responseBytes: Buffer.byteLength(originalResponse), turn: turnMetadata(originalTurn) }, purpose: "Parsed status, validator result, and complete host-observable turn metadata" },
1068
+ "original-raw-response": { value: originalTurn.transportEvidence ?? { unavailable: true }, purpose: "Retained bounded transport references" },
1069
+ "original-cleaned-response": { value: originalTurn.transportEvidence ?? { unavailable: true }, purpose: "Retained bounded cleaned evidence references" },
1070
+ "original-validation": { value: { errors: originalErrors, parsedStatus: describeNativeQaResponse(originalResponse).status, responseBytes: Buffer.byteLength(originalResponse), turn: turnMetadata(originalTurn) }, purpose: "Parsed status, validator result, and complete host-observable turn metadata" },
1220
1071
  "qa-history": { value: opts.qaHistory ?? [], purpose: "Earlier valid reports, observations, fixes, and unresolved history" },
1221
- "tool-events": { value: qaEvents.length ? qaEvents : { unavailable: true }, purpose: "All QA tool/activity events available through BuilderEvent, or an explicit unavailable marker" },
1072
+ "tool-events": { value: qaEvents instanceof BoundedTurnEvents ? qaEvents.metadata() : { unavailable: true }, purpose: "All QA tool/activity events available through BuilderEvent, or an explicit unavailable marker" },
1222
1073
  continuity: { value: continuity, purpose: "Cumulative continuity checkpoints, later host facts, handoff lineage, and recovery history" },
1223
1074
  settings: { value: { runtime: originalQa.agent, session: qaHandle.sessionIdentity(), runtimeSettings: qaHandle.runtimeContext, effectiveRoleInstructions: qaHandle.effectiveRoleInstructions, roleInstructionDigest: sha(qaHandle.effectiveRoleInstructions), loadedSkills: qaHandle.skills, handoffReceipt: qaHandle.handoffReceipt }, purpose: "Effective QA role instructions, actual loaded skills, runtime settings, and handoff receipt captured at session creation" },
1224
1075
  "context-usage": { value: await originalQa.contextUsage?.().catch(() => undefined) ?? { unavailable: true }, purpose: "Provider context usage sample or explicit unavailable marker" },
@@ -1235,69 +1086,12 @@ async function repairInvalidFailureReport(opts, identity, snapshot, originalQa,
1235
1086
  packetDb.close();
1236
1087
  }
1237
1088
  let packet = existingPacket ? appendQaRecoveryAttempt(existingPacket, packetInput) : createQaRecoveryPacket(packetInput);
1238
- let qa = originalQa;
1239
- let activeQaHandle = qaHandle;
1240
- let errors = originalErrors;
1241
- let turns = 0;
1242
- let recoveryContextSeal;
1243
- let protocolViolation;
1244
- let correctionExecutionFailed = false;
1245
- const attemptCorrections = async (count, stage, guidance) => {
1246
- for (let index = 0; index < count; index++) {
1247
- turns++;
1248
- packet = updateQaRecoveryPosition(packet, stage, turns, "qa-correction");
1249
- const prompt = correctionInstruction(errors, guidance);
1250
- packet = appendQaRecoveryResource(packet, `prompts/${String(turns).padStart(2, "0")}.txt`, prompt, { purpose: `Exact QA report-correction prompt ${turns} (${stage})`, exact: true });
1251
- try {
1252
- recoveryContextSeal?.verify();
1253
- }
1254
- catch (error) {
1255
- protocolViolation = error instanceof Error ? error.message : String(error);
1256
- return undefined;
1257
- }
1258
- const result = await sendDurableQaTurn(qa, prompt, qaEvents, `${stage}:correction-${index + 1}`);
1259
- try {
1260
- recoveryContextSeal?.verify();
1261
- }
1262
- catch (error) {
1263
- protocolViolation = error instanceof Error ? error.message : String(error);
1264
- return undefined;
1265
- }
1266
- const contract = parseQaResponseContract(result.text);
1267
- packet = await appendTurnObservation(packet, `correction-${String(turns).padStart(2, "0")}`, result, prompt, contract.errors, stage, qaEvents);
1268
- packet = appendQaRecoveryResource(packet, `qa-responses/${String(turns).padStart(2, "0")}.txt`, result.text, { purpose: `Exact QA correction response ${turns} (${stage})`, exact: true });
1269
- packet = appendQaRecoveryResource(packet, `validation/${String(turns).padStart(2, "0")}.json`, {
1270
- status: contract.status, fields: contract.fields, errors: contract.errors,
1271
- turn: { isError: result.isError, numTurns: result.numTurns, costUsd: result.costUsd, inputTokens: result.inputTokens ?? "unavailable", outputTokens: result.outputTokens ?? "unavailable" }, stage,
1272
- }, { purpose: `Parsed status, validator result, turn metadata, and recovery stage for correction ${turns}` });
1273
- packet = appendQaRecoveryResource(packet, "context/tool-events.json", qaEvents.length ? qaEvents : { unavailable: true }, { purpose: "Latest complete set of QA BuilderEvent records available to the host" });
1274
- try {
1275
- recoveryContextSeal?.verify();
1276
- }
1277
- catch (error) {
1278
- protocolViolation = error instanceof Error ? error.message : String(error);
1279
- return undefined;
1280
- }
1281
- if (!result.isError && !result.failure && contract.valid && contract.report)
1282
- return { result, contract };
1283
- correctionExecutionFailed = Boolean(result.failure || (result.isError && !result.continuityErrors?.length));
1284
- errors = result.isError ? [`QA correction turn errored: ${sanitizePreview(result.text)}`] : contract.errors;
1285
- }
1286
- return undefined;
1287
- };
1288
- let fixed = await attemptCorrections(1, "same-session");
1289
- if (fixed && !protocolViolation)
1290
- return { outcome: "recovered", qa, turn: fixed.result, contract: fixed.contract };
1291
- if (protocolViolation)
1292
- packet = appendQaRecoveryResource(packet, `recovery-history/context-mutation-${packet.manifest.revision + 1}.txt`, protocolViolation, { purpose: "Recovery-context mutation protocol violation", exact: true });
1293
- void packet;
1294
- if (protocolViolation || correctionExecutionFailed)
1295
- return { outcome: "failed", result: pauseQaReview(opts, "report-correction-exhausted", protocolViolation ?? errors.join("; "), snapshot.frozenState) };
1296
- const result = retryFreshQaReview(opts, "report-correction-exhausted", "QA report correction exhausted after one same-session turn; a complete fresh QA review is required", snapshot.frozenState);
1297
- return result.outcome === "retry-modification" ? { outcome: "retry" } : { outcome: "failed", result };
1089
+ packet = appendQaRecoveryResource(packet, "qa-responses/native-receipt.json", originalTurn.structured ?? { unavailable: true }, { purpose: "Original native provider receipt retained without changing or repeating completed QA work" });
1090
+ const invalidDetail = `QA response failed validation. Evidence retained at ${packet.directory}. ${originalErrors.join("; ")}`;
1091
+ return { outcome: "failed", result: retryFresh ? retryFreshQaReview(opts, "invalid-native-qa-response", invalidDetail, snapshot.frozenState) : pauseQaReview(opts, "invalid-native-qa-response", invalidDetail, snapshot.frozenState) };
1298
1092
  }
1299
- async function exhausted(packet, originalResponse, opts, prefix = "QA report correction exhausted after nine turns", qa, snapshot, liveSession = Boolean(qa), unavailableOperatorReason, requireFreshReviewReason) {
1300
- const synopsis = parseQaResponseContract(originalResponse).fields.issues?.trim();
1093
+ async function exhausted(packet, originalResponse, opts, prefix = "QA review requires an explicit recovery decision", qa, snapshot, liveSession = Boolean(qa), unavailableOperatorReason, requireFreshReviewReason) {
1094
+ const synopsis = describeNativeQaResponse(originalResponse).issues?.trim();
1301
1095
  let currentPrefix = prefix;
1302
1096
  if (packet.manifest.pendingAction !== "operator-menu") {
1303
1097
  packet = updateQaRecoveryPosition(packet, "operator-menu", packet.manifest.correctionTurns, "operator-menu");
@@ -1360,12 +1154,6 @@ function normalizeUnsupportedOperator(result, reason) {
1360
1154
  ? { outcome: "failed", result: { outcome: "needs-human", detail: `Requested QA recovery route is unavailable because ${reason}` } }
1361
1155
  : result;
1362
1156
  }
1363
- function extractReportBody(text) {
1364
- const start = text.split(/\r?\n/).findIndex((line) => line.trim() === QA_FAILURE_REPORT_START);
1365
- const lines = text.split(/\r?\n/);
1366
- const end = lines.findIndex((line, index) => index > start && line.trim() === QA_FAILURE_REPORT_END);
1367
- return start >= 0 && end > start ? lines.slice(start + 1, end).join("\n") : "{}\n";
1368
- }
1369
1157
  function boundedBuilderSummary(text) { return Buffer.from(text).subarray(0, 16 * 1024).toString(); }
1370
1158
  function latestBuilderResponse(opts) {
1371
1159
  return opts.state.builderResponseHistory?.filter((entry) => entry.ticketId === opts.ticket.id).at(-1)?.response ?? opts.builderSummary;
@@ -1444,7 +1232,7 @@ function markQaRecoveryAction(opts, action) {
1444
1232
  }
1445
1233
  const qaEventRecorders = new WeakMap();
1446
1234
  function startQaEventRecorder(adapter) {
1447
- const events = [];
1235
+ const events = new BoundedTurnEvents(providerWireContract(qaVerdictOutput, adapter.agent).maximumResponseBytes);
1448
1236
  qaEventRecorders.set(events, { waits: new Map(), activePumps: 0 });
1449
1237
  attachQaEventRecorder(events, adapter);
1450
1238
  return events;
@@ -1507,14 +1295,14 @@ async function appendTurnObservation(packet, label, turn, requestedPrompt, error
1507
1295
  let next = appendQaRecoveryResource(packet, `turns/${label}/host-prompt.${hostPrompt === undefined ? "json" : "txt"}`, hostPrompt ?? { unavailable: "adapter did not expose the host-requested instruction" }, { purpose: `Host-requested QA instruction for ${label}`, exact: hostPrompt !== undefined });
1508
1296
  const providerPrompt = turn.providerInstruction;
1509
1297
  next = appendQaRecoveryResource(next, `turns/${label}/provider-prompt.${providerPrompt === undefined ? "json" : "txt"}`, providerPrompt ?? { unavailable: "adapter did not expose the provider-dispatched instruction" }, { purpose: `Provider-dispatched QA instruction for ${label}`, exact: providerPrompt !== undefined });
1510
- next = appendQaRecoveryResource(next, `turns/${label}/raw-response.txt`, turn.rawResponse ?? turn.text, { purpose: `Raw provider QA response for ${label}`, exact: true });
1511
- next = appendQaRecoveryResource(next, `turns/${label}/cleaned-response.txt`, effectiveTurnText(turn), { purpose: `Cleaned QA response used for contract validation for ${label}`, exact: true });
1298
+ next = appendQaRecoveryResource(next, `turns/${label}/raw-response.json`, turn.transportEvidence ?? { unavailable: true }, { purpose: `Retained bounded transport references for ${label}` });
1299
+ next = appendQaRecoveryResource(next, `turns/${label}/cleaned-response.json`, turn.transportEvidence ?? { unavailable: true }, { purpose: `Retained bounded QA evidence for ${label}` });
1512
1300
  next = appendQaRecoveryResource(next, `turns/${label}/metadata.json`, { stage, validationErrors: errors, turn: turnMetadata(turn), eventCompletion: turn.turnId ? events.some((event) => event.kind === "turn-complete" && (event.turnId ?? event.result.turnId) === turn.turnId) ? "complete" : "incomplete" : "unavailable" }, { purpose: `Turn metadata, validation result, and event completeness for ${label}` });
1513
- return appendQaRecoveryResource(next, `turns/${label}/events.json`, events.length ? events : { unavailable: true }, { purpose: `Complete host-observed event set through ${label}` });
1301
+ return appendQaRecoveryResource(next, `turns/${label}/events.json`, events instanceof BoundedTurnEvents ? events.metadata() : { unavailable: true }, { purpose: `Complete host-observed event set through ${label}` });
1514
1302
  }
1515
1303
  async function performRecoveryAcknowledgement(packet, qa, prompt, events, continuityAlreadyValidated, label, seal) {
1516
- // Both turns acknowledge the same already-materialized revision. Observing
1517
- // either response advances the owner-only packet afterward, so validation
1304
+ // Acknowledge the already-materialized revision. Observing the response
1305
+ // advances the owner-only packet afterward, so validation
1518
1306
  // must continue to use this immutable target rather than the new revision.
1519
1307
  const target = packet;
1520
1308
  try {
@@ -1523,8 +1311,16 @@ async function performRecoveryAcknowledgement(packet, qa, prompt, events, contin
1523
1311
  catch (error) {
1524
1312
  return { packet, errors: [`recovery context mutated before acknowledgement: ${error instanceof Error ? error.message : String(error)}`] };
1525
1313
  }
1526
- const first = await sendDurableQaTurn(qa, prompt, events, `${label}:acknowledgement`);
1527
- let errors = validateQaRecoveryAcknowledgement(effectiveTurnText(first), target, { continuityAlreadyValidated });
1314
+ const first = await sendDurableQaTurn(qa, prompt, events, `${label}:acknowledgement`, qaRecoveryOutput);
1315
+ let errors = [];
1316
+ try {
1317
+ const ack = operationPayload(first, qaRecoveryOutput);
1318
+ if (ack.packet !== target.manifest.packetDigest || ack.reviewed_state !== target.manifest.reviewedStateDigest || ack.required_resources_read !== "all")
1319
+ errors.push("Recovery acceptance binding mismatch");
1320
+ }
1321
+ catch (error) {
1322
+ errors.push(String(error));
1323
+ }
1528
1324
  try {
1529
1325
  seal?.verify();
1530
1326
  }
@@ -1533,41 +1329,14 @@ async function performRecoveryAcknowledgement(packet, qa, prompt, events, contin
1533
1329
  }
1534
1330
  packet = await appendTurnObservation(packet, `${label}-acknowledgement`, first, prompt, errors, "packet-acknowledgement", events);
1535
1331
  if (first.isError || first.failure)
1536
- return { packet, errors: [...errors, first.continuityErrors?.length && !first.failure ? "QA acknowledgement continuity metadata was invalid" : "QA acknowledgement provider turn failed"],
1537
- retryable: Boolean(first.continuityErrors?.length && !first.failure && !errors.some(error => /mutated/i.test(error))) };
1332
+ return { packet, errors: [...errors, first.continuityErrors?.length && !first.failure ? "QA acknowledgement continuity metadata was invalid" : "QA acknowledgement provider turn failed"] };
1538
1333
  try {
1539
1334
  seal?.verify();
1540
1335
  }
1541
1336
  catch (error) {
1542
1337
  errors.push(`recovery context mutated after acknowledgement: ${error instanceof Error ? error.message : String(error)}`);
1543
1338
  }
1544
- if (errors.length) {
1545
- const correction = `${prompt}\n\nAcknowledgement correction only. Do not produce the report yet. Errors: ${errors.join("; ")}`;
1546
- try {
1547
- seal?.verify();
1548
- }
1549
- catch (error) {
1550
- return { packet, errors: [`recovery context mutated before acknowledgement repair: ${error instanceof Error ? error.message : String(error)}`] };
1551
- }
1552
- const repaired = await sendDurableQaTurn(qa, correction, events, `${label}:acknowledgement-repair`);
1553
- errors = validateQaRecoveryAcknowledgement(effectiveTurnText(repaired), target, { continuityAlreadyValidated });
1554
- try {
1555
- seal?.verify();
1556
- }
1557
- catch (error) {
1558
- errors.push(`recovery context mutated during acknowledgement repair: ${error instanceof Error ? error.message : String(error)}`);
1559
- }
1560
- packet = await appendTurnObservation(packet, `${label}-acknowledgement-repair`, repaired, correction, errors, "packet-acknowledgement-repair", events);
1561
- if (repaired.isError || repaired.failure)
1562
- errors.push(repaired.continuityErrors?.length && !repaired.failure ? "QA acknowledgement repair continuity metadata was invalid" : "QA acknowledgement repair provider turn failed");
1563
- try {
1564
- seal?.verify();
1565
- }
1566
- catch (error) {
1567
- errors.push(`recovery context mutated after acknowledgement repair: ${error instanceof Error ? error.message : String(error)}`);
1568
- }
1569
- }
1570
- return { packet, errors, retryable: !errors.some(error => /mutated|provider turn failed/i.test(error)) };
1339
+ return { packet, errors };
1571
1340
  }
1572
1341
  function validateBoundaryReceipt(receipt, packet, successorHandle) {
1573
1342
  const observedSuccessor = successorHandle.sessionIdentity();
@@ -1630,6 +1399,24 @@ export function assertQaRecoveryHandoffInventory(resources) {
1630
1399
  }
1631
1400
  function effectiveTurnText(turn) { return turn.cleanedResponse ?? turn.text; }
1632
1401
  const qaTurnBindings = new WeakMap();
1402
+ /** A readable evidence view, never a new native receipt or acceptance decision. */
1403
+ function retainedBuilderReport(db, digest) {
1404
+ const bytes = db.getEvidence(digest);
1405
+ if (!bytes)
1406
+ return { digest, availability: "unavailable" };
1407
+ try {
1408
+ const retained = JSON.parse(bytes.toString());
1409
+ const receipt = retained?.structured;
1410
+ if (receipt?.state === "candidate") {
1411
+ const value = typeof receipt.value === "string" ? JSON.parse(receipt.value) : receipt.value;
1412
+ return { digest, availability: "available", operation: receipt.operation, schemaDigest: receipt.schemaDigest, report: value?.result };
1413
+ }
1414
+ return { digest, availability: "historical", report: retained };
1415
+ }
1416
+ catch {
1417
+ return { digest, availability: "unreadable", reason: "Retained receipt JSON cannot be decoded; do not infer missing evidence from a summary." };
1418
+ }
1419
+ }
1633
1420
  function beginV2Review(opts, source, reviewedSnapshotPath, instruction, handle, attempt) {
1634
1421
  const db = new WorkflowDb(opts.recovery.projectDir);
1635
1422
  try {
@@ -1665,35 +1452,46 @@ function beginV2Review(opts, source, reviewedSnapshotPath, instruction, handle,
1665
1452
  const expected = contract.inputs.find(input => input.kind === "checklist");
1666
1453
  if (expected?.digest !== contractDigest("input-checklist", checklist))
1667
1454
  throw new Error("Final QA checklist differs from delivered contract");
1668
- const needsHistoricalEvidence = contract.checks.some(check => /red|test.?first|ordering|historical/i.test(JSON.stringify(check)));
1669
1455
  const builderClaims = db.operations(opts.recovery.runId).flatMap(operation => {
1670
1456
  const intent = operation.intent;
1671
1457
  const result = operation.result;
1672
- return operation.kind === "builder-coverage" && operation.status === "confirmed" && intent.workId === opts.ticket.id && intent.contractDigest === contract.contentDigest && result?.binding?.sourceDigest === source.digest && result.coverageDigest
1458
+ return operation.kind === "builder-coverage" && operation.status === "confirmed" && intent.workId === opts.ticket.id && intent.contractDigest === contract.contentDigest && result?.binding?.sourceDigest && result.coverageDigest && (result.binding.sourceDigest === source.digest || result.implementationDigest)
1673
1459
  ? [{
1674
1460
  operationId: operation.idempotencyKey,
1675
1461
  digest: result.coverageDigest,
1676
- coverage: preparationStore.artifact(result.coverageDigest, "builder-coverage"),
1677
- ...(needsHistoricalEvidence && result.implementationDigest ? {
1462
+ sourceDigest: result.binding.sourceDigest,
1463
+ currentSource: result.binding.sourceDigest === source.digest,
1464
+ ...(result.binding.sourceDigest === source.digest ? { coverage: preparationStore.artifact(result.coverageDigest, "builder-coverage") } : {}),
1465
+ ...(result.implementationDigest ? {
1678
1466
  // The response is already host-retained by finishBuildAssignment.
1679
- // Expose only this bounded, contract-relevant excerpt to the
1680
- // independent reviewer; do not retain or disclose all Builder
1681
- // output globally.
1682
- implementationResponse: {
1683
- digest: result.implementationDigest,
1684
- response: db.getEvidence(result.implementationDigest)?.toString() ?? "unavailable",
1685
- },
1467
+ // Supply this scoped original report through the bounded context
1468
+ // and its complete read-only resource. Historical reports never
1469
+ // substitute for current-source coverage.
1470
+ implementationResponse: retainedBuilderReport(db, result.implementationDigest),
1686
1471
  } : {}),
1687
1472
  }] : [];
1688
1473
  });
1689
- const commandObservations = retainedQaCommandObservations(opts.recovery.projectDir, opts.recovery.runId, opts.ticket.id, qaCommandOutputExcerptsAllowed(opts.ticket.id));
1474
+ const commandObservations = [
1475
+ ...retainedQaCommandObservations(opts.recovery.projectDir, opts.recovery.runId, opts.ticket.id, qaCommandOutputExcerptsAllowed(opts.ticket.id)),
1476
+ ...retainedHostBuilderObservations(db, opts.recovery.runId, opts.ticket.id),
1477
+ ];
1690
1478
  instruction += `\n${qaHostGraphFact(opts.recovery.projectDir)}\n`;
1691
1479
  instruction += finalReviewContractContext(contract, preparationStore, reviewedSnapshotPath, builderClaims, commandObservations);
1480
+ if (findHostQaCheck(contract)) {
1481
+ const comparison = inspectHostQaComparison({ db, projectDir: opts.recovery.projectDir, worktree: opts.builderWorktree, runId: opts.recovery.runId, ticket: opts.ticket, contract });
1482
+ if (comparison.finalSourceDigest !== source.digest)
1483
+ throw new Error("Host comparison source differs from the frozen QA source");
1484
+ const digest = preparationStore.putArtifact("host-qa-comparison-observation", comparison);
1485
+ const boundedPaths = (paths) => ({ paths: paths.slice(0, 20), omitted: Math.max(0, paths.length - 20) });
1486
+ instruction += `\nHost source comparison observed before this review: ${JSON.stringify({ evidenceRef: `host-qa-comparison-observation:${digest}`, contractDigest: comparison.contractDigest, baselineSourceDigest: comparison.baselineSourceDigest, finalSourceDigest: comparison.finalSourceDigest, status: comparison.status, reason: comparison.reason, changedPaths: boundedPaths(comparison.changedPaths), unauthorizedPaths: boundedPaths(comparison.unauthorizedPaths), deletedPaths: boundedPaths(comparison.deletedPaths), protectedPaths: boundedPaths(comparison.protectedPaths), filePreservation: comparison.filePreservation, preservationOmitted: comparison.preservationOmitted })}\nThis retained observation compares complete captured product-path identities against the admitted preimplementation baseline. When omitted is zero the changed-path list is complete; captured paths absent from it are unchanged. A filePreservation row with status compared and baselineIsExactPrefix true proves every baseline byte remains in its original position; if baselineEndsWithNewline is true, every original complete line is preserved. Cite this retained observation for an applicable exact-preservation check. False or unavailable does not prove preservation and is not automatically a failure for requirements that permit other edits. These facts do not prove product behavior or a historical test sequence. Independently verify those applicable requirements with source and tests. Do not wait for a future host comparison or reconstruct the private baseline. Rafi recomputes the comparison against current source before certificate issuance; this observation alone cannot approve work.\n`;
1487
+ }
1692
1488
  instruction += `\n${renderVerificationContract(contract)}\n${COVERAGE_INSTRUCTIONS}\nCoverage binding: ${JSON.stringify({ phase: "qa", runId: opts.recovery.runId, workId: opts.ticket.id, contractDigest: contract.contentDigest, revision: contract.revision, sourceDigest: source.digest, attemptId: attempt.attemptId, sessionId: handle.sessionIdentity().sessionId })}`;
1693
1489
  }
1490
+ instruction += `\n\n${operationInstruction(qaVerdictOutput)}`;
1694
1491
  const originalInstruction = instruction;
1695
1492
  const guidance = db.reserveGuidance(opts.recovery.runId, opts.ticket.id, "qa", attempt.attemptId, source.digest, instruction, false);
1696
- instruction = guidance.text;
1493
+ const managerVerificationInstruction = `\nManager instruction verification: verifiedInstructionIds may contain only independently verified IDs from this exact allowed list: ${JSON.stringify([...guidance.ids, ...verification.ids])}. These are separate Manager instructions, not contract check IDs or test names. If the list is empty, return verifiedInstructionIds: []. Rafi binds the immutable input-basis digest after validating your coverage claims; do not add host bindings to the payload.`;
1494
+ instruction = guidance.text + managerVerificationInstruction;
1697
1495
  const basisWithoutDigest = {
1698
1496
  version: 2,
1699
1497
  ...(contract ? { contractBinding: { version: 1, transportVersion: 1, revision: contract.revision, digest: contract.contentDigest, admissionDigest: admission.requirementsDigest, commonConfigDigest: contract.inputs.find(input => input.kind === "rules").digest } } : {}),
@@ -1738,7 +1536,7 @@ function beginV2Review(opts, source, reviewedSnapshotPath, instruction, handle,
1738
1536
  head = db.commitQaReviewReady(sourceV2, basis, identity, handle.confinement, attempt, head.revision);
1739
1537
  if (recoveryPacket)
1740
1538
  recoveryPacket = updateQaRecoveryReviewIdentity(recoveryPacket, head.reviewNumber, attempt.attemptId, attempt.cycle);
1741
- return { projectDir: opts.recovery.projectDir, runId: opts.recovery.runId, ticketId: opts.ticket.id, reviewNumber: head.reviewNumber, reviewAttemptId: attempt.attemptId, sourceStateDigest: source.digest, reviewBasisDigest: basis.digest, sessionGeneration: identity.generation, sessionRef: identity, reviewedSnapshotPath: identity.cwd, frozenSource: source, recoveryPacket, nextTurn: 0, originalInstruction, inputBasisEnvelope, reviewInstruction: instruction, guidanceIds: guidance.ids, verificationIds: verification.ids };
1539
+ return { projectDir: opts.recovery.projectDir, runId: opts.recovery.runId, ticketId: opts.ticket.id, reviewNumber: head.reviewNumber, reviewAttemptId: attempt.attemptId, sourceStateDigest: source.digest, reviewBasisDigest: basis.digest, sessionGeneration: identity.generation, sessionRef: identity, reviewedSnapshotPath: identity.cwd, frozenSource: source, recoveryPacket, nextTurn: 0, originalInstruction, inputBasisEnvelope, managerVerificationInstruction, reviewInstruction: instruction, guidanceIds: guidance.ids, verificationIds: verification.ids };
1742
1540
  }
1743
1541
  finally {
1744
1542
  db.close();
@@ -1785,18 +1583,20 @@ function bindQaRecoveryContext(adapter, context) {
1785
1583
  throw new Error("QA recovery context requires a durable review binding");
1786
1584
  binding.recoveryContext = context;
1787
1585
  }
1788
- async function sendDurableQaTurn(adapter, instruction, events, slot) {
1586
+ async function sendDurableQaTurn(adapter, instruction, events, slot, output = qaVerdictOutput) {
1789
1587
  const binding = qaTurnBindings.get(adapter);
1790
1588
  if (!binding)
1791
1589
  throw new Error("QA provider dispatch requires a durable source/review-basis binding");
1792
1590
  // Repairs and recovery acknowledgements bypass graph navigation, but their
1793
1591
  // provider history and journal artifacts retain the original access grants.
1794
- return dispatchWithGraphAccess(binding.projectDir, adapter, instruction, undefined, () => sendDurableQaTurnOwned(adapter, instruction, events, slot));
1592
+ return dispatchWithGraphAccess(binding.projectDir, adapter, instruction, undefined, () => sendDurableQaTurnOwned(adapter, instruction, events, slot, output));
1795
1593
  }
1796
- async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
1594
+ async function sendDurableQaTurnOwned(adapter, instruction, events, slot, output = qaVerdictOutput) {
1797
1595
  const binding = qaTurnBindings.get(adapter);
1798
1596
  if (!binding)
1799
1597
  throw new Error("QA provider dispatch requires a durable source/review-basis binding");
1598
+ if (slot !== "initial")
1599
+ instruction += `\n\n${operationInstruction(output)}`;
1800
1600
  if (slot !== "session-initialization") {
1801
1601
  try {
1802
1602
  await adapter.prepareAutoCompaction?.();
@@ -1812,7 +1612,7 @@ async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
1812
1612
  "Rafi QA session initialization only.",
1813
1613
  "Do not inspect source, call tools, make findings, or modify files.",
1814
1614
  "Reply briefly that the context is ready, followed by the required continuity record.",
1815
- ].join("\n"), events, "session-initialization");
1615
+ ].join("\n"), events, "session-initialization", initializationOutput);
1816
1616
  const setupCalledTools = events.slice(eventOffset).some((event) => event.kind === "tool");
1817
1617
  if (initialized.isError || setupCalledTools || adapter.requiresAutoCompactionSetupTurn?.()) {
1818
1618
  return { ...initialized, isError: true,
@@ -1832,13 +1632,13 @@ async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
1832
1632
  db.atomic(() => {
1833
1633
  if (slot === "initial") {
1834
1634
  const guidance = db.reserveGuidance(binding.runId, binding.ticketId, "qa", operationId, binding.sourceStateDigest, binding.originalInstruction);
1835
- if (guidance.text + (binding.inputBasisEnvelope ?? "") !== binding.reviewInstruction)
1635
+ if (guidance.text + (binding.managerVerificationInstruction ?? "") + (binding.inputBasisEnvelope ?? "") !== binding.reviewInstruction)
1836
1636
  throw new BuildControlBoundary("Manager guidance changed during QA preparation; restart a full review with the current instruction basis");
1837
1637
  binding.guidanceIds = guidance.ids;
1838
1638
  instruction = binding.reviewInstruction;
1839
1639
  }
1840
1640
  const instructionDigest = db.putEvidence("qa", Buffer.from(instruction));
1841
- const intent = { version: 2, operationId, runId: binding.runId, ticketId: binding.ticketId, reviewNumber: binding.reviewNumber, sessionGeneration: binding.sessionGeneration, slot: retrySlot, sourceStateDigest: binding.sourceStateDigest, reviewBasisDigest: binding.reviewBasisDigest, providerSession, instructionDigest, intendedAt };
1641
+ const intent = { version: 2, operationId, runId: binding.runId, ticketId: binding.ticketId, reviewNumber: binding.reviewNumber, sessionGeneration: binding.sessionGeneration, slot: retrySlot, sourceStateDigest: binding.sourceStateDigest, reviewBasisDigest: binding.reviewBasisDigest, providerSession, instructionDigest, intendedAt, outputContract: { protocolVersion: output.protocolVersion, operation: output.operation, schemaVersion: output.schemaVersion, schemaDigest: output.schemaDigest } };
1842
1642
  const head = db.qaTicketHead(binding.runId, binding.ticketId);
1843
1643
  db.commitQaTurnIntent(intent, head.revision);
1844
1644
  });
@@ -1851,18 +1651,18 @@ async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
1851
1651
  // Reading the sealed QA packet is ordinary confined read-only QA work.
1852
1652
  // Only formatting repair and ownership acceptance are response-only; the
1853
1653
  // first resource-inspection turn cannot truthfully read with tools disabled.
1854
- const responseRepair = slot?.includes("correction") || (slot?.includes("acknowledgement") && slot.includes("repair"));
1855
1654
  try {
1856
- result = await adapter.sendTurn(instruction, slot === "session-initialization" ? { purpose: "initialization", responseOnly: true } : responseRepair ? { purpose: "response-repair", responseOnly: true } : undefined);
1655
+ result = await journalStructuredDispatch(binding.projectDir, adapter, instruction, nativePolicy(output, { dispatchId: operationId, logicalActionId: binding.reviewAttemptId, journalRunId: binding.runId, ...(slot === "session-initialization" ? { purpose: "initialization", responseOnly: true } : {}) }), (text, policy) => adapter.sendTurn(text, policy));
1857
1656
  }
1858
1657
  catch (error) {
1859
1658
  const failedDb = new WorkflowDb(binding.projectDir);
1860
1659
  try {
1861
1660
  const receipt = {
1862
1661
  version: 2, operationId, dispatch: "uncertain", terminalEventObserved: false,
1863
- eventStreamDigest: failedDb.putEvidence("qa", Buffer.from(canonicalJson(events))),
1662
+ eventStreamDigest: failedDb.putEvidence("qa", Buffer.from(canonicalJson(events instanceof BoundedTurnEvents ? events.metadata() : []))),
1864
1663
  sourceStateDigest: binding.sourceStateDigest, reviewBasisDigest: binding.reviewBasisDigest, completedAt: new Date().toISOString(),
1865
1664
  };
1665
+ failedDb.recordCommunicationEvidenceRefs(operationId, [receipt.rawResponseDigest, receipt.cleanedResponseDigest, receipt.eventStreamDigest].filter((digest) => Boolean(digest)));
1866
1666
  failedDb.finishQaTurn(receipt);
1867
1667
  const head = failedDb.qaTicketHead(binding.runId, binding.ticketId);
1868
1668
  if (head.state === "turn-intended")
@@ -1871,13 +1671,15 @@ async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
1871
1671
  finally {
1872
1672
  failedDb.close();
1873
1673
  }
1874
- ensureUncertainQaTurnPacket(binding, instruction, error instanceof Error ? error.message : String(error));
1674
+ ensureUncertainQaTurnPacket(binding, instruction, boundedUtf8(error instanceof Error ? error.message : String(error), 4096).toString());
1875
1675
  throw error;
1876
1676
  }
1877
1677
  await awaitQaTurnEvent(events, result);
1878
1678
  const activeSession = adapter.sessionRef?.();
1879
1679
  const reportedSession = result.providerMetadata?.sessionRef;
1880
- const identityMismatch = adapter.agent !== session.provider
1680
+ const expectedWire = providerWireContract(output, session.provider);
1681
+ const identityMismatch = (result.structured?.state === "candidate" && (result.structured.dispatchId !== operationId || result.structured.sessionId !== session.sessionId || result.structured.profile !== expectedWire.profile || result.structured.wireContractDigest !== expectedWire.descriptorDigest || result.structured.wireSchemaDigest !== expectedWire.wireSchemaDigest || result.structured.wireVariant !== expectedWire.variant))
1682
+ || adapter.agent !== session.provider
1881
1683
  || adapter.sessionId() !== session.sessionId
1882
1684
  || !activeSession
1883
1685
  || stableProviderSessionIdentity(activeSession) !== stableProviderSessionIdentity(session)
@@ -1892,12 +1694,17 @@ async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
1892
1694
  const receipt = {
1893
1695
  version: 2, operationId, dispatch: "uncertain", terminalEventObserved: false,
1894
1696
  providerTurnId: result.turnId,
1895
- rawResponseDigest: failedDb.putEvidence("qa", Buffer.from(result.rawResponse ?? result.text)),
1896
- cleanedResponseDigest: failedDb.putEvidence("qa", Buffer.from(result.cleanedResponse ?? result.text)),
1697
+ rawResponseDigest: failedDb.operation(operationId)?.result?.rawDigest,
1698
+ sealedDispatch: failedDb.structuredOwnerDispatch(operationId),
1699
+ structuredResponseDigest: failedDb.operation(operationId)?.result?.responseDigest,
1700
+ cleanedResponseDigest: failedDb.putEvidence("qa", boundedUtf8(result.cleanedResponse ?? result.text, expectedWire.maximumResponseBytes)),
1701
+ retainedEvidence: { rawOriginalBytes: result.transportEvidence?.rawBytes, rawRetainedBytes: result.transportEvidence?.retainedRawBytes,
1702
+ cleanedOriginalBytes: Buffer.byteLength(result.cleanedResponse ?? result.text), cleanedRetainedBytes: boundedUtf8(result.cleanedResponse ?? result.text, expectedWire.maximumResponseBytes).length },
1897
1703
  providerInstructionDigest: failedDb.putEvidence("qa", Buffer.from(result.providerInstruction ?? result.hostInstruction ?? instruction)),
1898
- eventStreamDigest: failedDb.putEvidence("qa", Buffer.from(canonicalJson({ events, rejectedProviderMetadata: result.providerMetadata, activeSession }))),
1704
+ eventStreamDigest: failedDb.putEvidence("qa", Buffer.from(canonicalJson({ observation: events instanceof BoundedTurnEvents ? events.metadata() : [], rejectedSessionId: boundedUtf8(result.providerMetadata?.sessionId ?? "unavailable", 256).toString(), activeSessionId: boundedUtf8(activeSession?.sessionId ?? "unavailable", 256).toString() }))),
1899
1705
  sourceStateDigest: binding.sourceStateDigest, reviewBasisDigest: binding.reviewBasisDigest, completedAt: new Date().toISOString(),
1900
1706
  };
1707
+ failedDb.recordCommunicationEvidenceRefs(operationId, [receipt.rawResponseDigest, receipt.cleanedResponseDigest, receipt.eventStreamDigest].filter((digest) => Boolean(digest)));
1901
1708
  failedDb.finishQaTurn(receipt);
1902
1709
  const head = failedDb.qaTicketHead(binding.runId, binding.ticketId);
1903
1710
  if (head.state === "turn-intended")
@@ -1910,21 +1717,28 @@ async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
1910
1717
  throw new Error(detail);
1911
1718
  }
1912
1719
  const completedAt = new Date().toISOString();
1913
- const terminalEventObserved = Boolean(result.turnId && result.failure?.dispatchState !== "unknown"
1914
- && events.some((event) => event.kind === "turn-complete" && (event.turnId ?? event.result.turnId) === result.turnId));
1720
+ const terminalEventObserved = Boolean(!(events instanceof BoundedTurnEvents && events.terminalOverflow) && result.turnId && result.failure?.dispatchState !== "unknown"
1721
+ && events.filter(event => event.kind === "turn-complete" && (event.turnId ?? event.result.turnId) === result.turnId).length === 1
1722
+ && events.some(event => event.kind === "turn-complete" && (event.turnId ?? event.result.turnId) === result.turnId &&
1723
+ receiptFingerprint(event.result) === receiptFingerprint(result)));
1915
1724
  const receiptDb = new WorkflowDb(binding.projectDir);
1916
1725
  try {
1917
- const rawResponseDigest = receiptDb.putEvidence("qa", Buffer.from(result.rawResponse ?? result.text));
1726
+ const rawResponseDigest = (receiptDb.operation(operationId)?.result).rawDigest;
1727
+ const structuredResponseDigest = receiptDb.operation(operationId)?.result?.responseDigest;
1918
1728
  const providerInstruction = result.providerInstruction ?? result.hostInstruction ?? instruction;
1919
1729
  const providerInstructionDigest = receiptDb.putEvidence("qa", Buffer.from(providerInstruction));
1920
- const cleanedResponseDigest = receiptDb.putEvidence("qa", Buffer.from(result.cleanedResponse ?? result.text));
1921
- const eventStreamDigest = receiptDb.putEvidence("qa", Buffer.from(canonicalJson(events)));
1730
+ const cleanedResponseDigest = receiptDb.putEvidence("qa", boundedUtf8(result.cleanedResponse ?? result.text, expectedWire.maximumResponseBytes));
1731
+ const eventStreamDigest = receiptDb.putEvidence("qa", Buffer.from(canonicalJson(events instanceof BoundedTurnEvents ? events.metadata() : [])));
1922
1732
  const receipt = {
1923
- version: 2, operationId, dispatch: terminalEventObserved ? "completed" : "uncertain", providerTurnId: result.turnId,
1924
- providerInstructionDigest, rawResponseDigest, cleanedResponseDigest, eventStreamDigest,
1733
+ version: 2, operationId, dispatch: terminalEventObserved && !result.isError && !result.failure ? "completed" : "uncertain", providerTurnId: result.turnId,
1734
+ sealedDispatch: receiptDb.structuredOwnerDispatch(operationId),
1735
+ providerInstructionDigest, rawResponseDigest, cleanedResponseDigest, eventStreamDigest, structuredResponseDigest,
1736
+ retainedEvidence: { rawOriginalBytes: result.transportEvidence?.rawBytes, rawRetainedBytes: result.transportEvidence?.retainedRawBytes,
1737
+ cleanedOriginalBytes: Buffer.byteLength(result.cleanedResponse ?? result.text), cleanedRetainedBytes: boundedUtf8(result.cleanedResponse ?? result.text, expectedWire.maximumResponseBytes).length },
1925
1738
  terminalEventObserved, sourceStateDigest: binding.sourceStateDigest, reviewBasisDigest: binding.reviewBasisDigest, completedAt,
1926
1739
  };
1927
1740
  receiptDb.atomic(() => {
1741
+ receiptDb.recordCommunicationEvidenceRefs(operationId, [rawResponseDigest, cleanedResponseDigest, eventStreamDigest]);
1928
1742
  receiptDb.finishQaTurn(receipt);
1929
1743
  const metricAdmission = receiptDb.admittedWork(binding.runId, binding.ticketId);
1930
1744
  if (metricAdmission)
@@ -1935,7 +1749,7 @@ async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
1935
1749
  binding.lastReceiptDigest = qaDigest("turn-receipt", receipt);
1936
1750
  let graphRequest = false;
1937
1751
  try {
1938
- graphRequest = Boolean(parseGraphRequest(result.cleanedResponse ?? result.text));
1752
+ graphRequest = operationResponse(result, output).result.kind === "graph-request";
1939
1753
  }
1940
1754
  catch {
1941
1755
  graphRequest = true;
@@ -1943,6 +1757,7 @@ async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
1943
1757
  if (!slot?.startsWith("session-") && !graphRequest)
1944
1758
  binding.lastBusinessReceiptDigest = binding.lastReceiptDigest;
1945
1759
  binding.lastResponse = result.text;
1760
+ binding.lastTurn = result;
1946
1761
  if (!terminalEventObserved) {
1947
1762
  const head = receiptDb.qaTicketHead(binding.runId, binding.ticketId);
1948
1763
  receiptDb.transitionQa(binding.runId, binding.ticketId, head.revision, { type: "turn-uncertain" });
@@ -2017,15 +1832,23 @@ function finishV2Pass(opts, adapter, detail) {
2017
1832
  throw new Error("Final coverage requires the current ready contract");
2018
1833
  const contract = db.qaPreparationStore().contract(contractHead.digest);
2019
1834
  const coverageBinding = { phase: "qa", sourceDigest: binding.sourceStateDigest, inputBasisDigest: binding.reviewBasisDigest, attemptId: binding.reviewAttemptId, sessionId: binding.sessionRef.sessionId };
2020
- const providerCoverage = bindCoverageHostFields(parseContractCoverage(binding.lastResponse ?? ""), contract, coverageBinding);
1835
+ if (!binding.lastTurn)
1836
+ throw new Error("QA coverage requires the retained structured verdict");
1837
+ const claims = operationPayload(binding.lastTurn, qaVerdictOutput).coverage;
1838
+ if (!claims)
1839
+ throw new Error("QA verdict omitted required coverage");
1840
+ const providerCoverage = bindCoverageHostFields(claims, contract, coverageBinding);
2021
1841
  const hostComparison = findHostQaCheck(contract)
2022
1842
  ? createHostQaComparison({ db, projectDir: binding.projectDir, worktree: opts.builderWorktree, runId: binding.runId, ticket: opts.ticket, contract, reviewAttemptId: binding.reviewAttemptId, reviewBasisDigest: binding.reviewBasisDigest })
2023
1843
  : undefined;
2024
1844
  const hostGraph = findGraphQaChecks(contract).length
2025
1845
  ? createHostGraphApplicability({ projectDir: binding.projectDir, runId: binding.runId, ticketId: binding.ticketId, contract, reviewAttemptId: binding.reviewAttemptId, reviewBasisDigest: binding.reviewBasisDigest, sourceDigest: binding.sourceStateDigest, store: db.qaPreparationStore() })
2026
1846
  : undefined;
2027
- const commandObservations = retainedQaCommandObservations(opts.recovery.projectDir, opts.recovery.runId, opts.ticket.id, false);
2028
- const coverage = applyHostQaAugmentations({ coverage: providerCoverage, contract, projectDir: binding.projectDir, hostComparison, hostGraph, commandObservations });
1847
+ const commandObservations = [
1848
+ ...retainedQaCommandObservations(opts.recovery.projectDir, opts.recovery.runId, opts.ticket.id, false),
1849
+ ...retainedHostBuilderObservations(db, opts.recovery.runId, opts.ticket.id),
1850
+ ];
1851
+ const coverage = assembleFinalQaCoverage({ providerCoverage, contract, projectDir: binding.projectDir, hostComparison, hostGraph, commandObservations });
2029
1852
  const errors = validateContractCoverage(contract, coverage, coverageBinding);
2030
1853
  if (errors.length)
2031
1854
  throw new Error(`Final QA coverage incomplete: ${errors.join("; ")}`);
@@ -2033,8 +1856,14 @@ function finishV2Pass(opts, adapter, detail) {
2033
1856
  contractCoverage = { version: 1, contractDigest: contract.contentDigest, revision: contract.revision, coverageDigest, attemptId: binding.reviewAttemptId, sessionId: binding.sessionRef.sessionId };
2034
1857
  }
2035
1858
  const certificate = db.commitQaPassAttempt(binding.reviewAttemptId, { runId: binding.runId, ticketId: binding.ticketId, qaRevision: head.revision + 1, sourceStateDigest: binding.sourceStateDigest, reviewBasisDigest: binding.reviewBasisDigest, turnReceiptDigest: binding.lastBusinessReceiptDigest, ...(contractCoverage ? { contractCoverage } : {}) }, detail, head.revision);
2036
- db.verifyGuidance(binding.guidanceIds, certificate, binding.lastResponse ?? "");
2037
- db.verifyBuilderGuidance(binding.verificationIds, certificate, binding.lastResponse ?? "", binding.lastBusinessReceiptDigest);
1859
+ if (!binding.lastTurn)
1860
+ throw new Error("QA guidance verification requires the native turn receipt");
1861
+ const verifiedInstructionIds = operationPayload(binding.lastTurn, qaVerdictOutput).verifiedInstructionIds;
1862
+ const allowedInstructionIds = new Set([...binding.guidanceIds, ...binding.verificationIds]);
1863
+ if (verifiedInstructionIds.some(id => !allowedInstructionIds.has(id)))
1864
+ throw new Error("QA claimed verification of a foreign instruction");
1865
+ db.verifyGuidance(binding.guidanceIds, certificate, verifiedInstructionIds);
1866
+ db.verifyBuilderGuidance(binding.verificationIds, certificate, verifiedInstructionIds, binding.lastBusinessReceiptDigest);
2038
1867
  return { certificateId: certificate.certificateId, sourceStateDigest: binding.sourceStateDigest, reviewBasisDigest: binding.reviewBasisDigest };
2039
1868
  });
2040
1869
  }
@@ -2106,6 +1935,7 @@ export async function verifyPendingQaFinalizationSource(projectDir, sourceWorktr
2106
1935
  const controlDb = new WorkflowDb(projectDir);
2107
1936
  try {
2108
1937
  controlDb.assertFinalizationControls(runId, ticketId);
1938
+ controlDb.assertQaFinalizationAuthority(runId, ticketId);
2109
1939
  }
2110
1940
  finally {
2111
1941
  controlDb.close();
@@ -2169,7 +1999,7 @@ function turnMetadata(turn) {
2169
1999
  inputTokens: turn.inputTokens ?? "unavailable",
2170
2000
  outputTokens: turn.outputTokens ?? "unavailable",
2171
2001
  usage: turn.usage ?? { unavailable: true },
2172
- failure: turn.failure ?? null,
2002
+ failure: turn.failure ? { ...turn.failure, diagnostics: boundedUtf8(turn.failure.diagnostics, 4096).toString() } : null,
2173
2003
  provider: turn.providerMetadata ?? { unavailable: true },
2174
2004
  };
2175
2005
  }
@@ -2185,6 +2015,11 @@ function qaContinuityRecoveryContext(opts) {
2185
2015
  recoveryAttempts: db.recoveryAttempts(opts.recovery.runId).filter((attempt) => attempt.ticket === opts.ticket.id),
2186
2016
  };
2187
2017
  }
2018
+ catch (error) {
2019
+ if (/revoked graph-derived evidence|Graph access changed/.test(String(error)))
2020
+ return { unavailable: "graph-access-revoked", detail: "Historical graph-derived continuity is withheld; explicit source-based recovery is required" };
2021
+ throw error;
2022
+ }
2188
2023
  finally {
2189
2024
  db.close();
2190
2025
  }