ai-foreman 1.7.28 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude.d.ts +23 -12
  3. package/dist/adapters/claude.js +228 -24
  4. package/dist/adapters/codex.d.ts +17 -7
  5. package/dist/adapters/codex.js +195 -17
  6. package/dist/adapters/recovering.d.ts +2 -0
  7. package/dist/adapters/recovering.js +5 -1
  8. package/dist/adapters/structured.d.ts +71 -0
  9. package/dist/adapters/structured.js +213 -0
  10. package/dist/adapters/types.d.ts +86 -9
  11. package/dist/agentRun.d.ts +2 -0
  12. package/dist/agentRun.js +2 -1
  13. package/dist/boundedTurnEvidence.d.ts +18 -0
  14. package/dist/boundedTurnEvidence.js +76 -0
  15. package/dist/branch/currentGuard.d.ts +2 -0
  16. package/dist/branch/currentGuard.js +2 -0
  17. package/dist/branch/planner.js +1 -4
  18. package/dist/branch/runner.d.ts +2 -1
  19. package/dist/branch/runner.js +10 -7
  20. package/dist/buildAdmission.js +4 -0
  21. package/dist/buildApproval.d.ts +30 -0
  22. package/dist/buildApproval.js +68 -0
  23. package/dist/buildAssignment.d.ts +1 -0
  24. package/dist/buildAssignment.js +82 -23
  25. package/dist/buildInterventions.d.ts +2 -2
  26. package/dist/buildInterventions.js +11 -4
  27. package/dist/buildLaunchGate.js +3 -2
  28. package/dist/builderGuidanceFollowup.js +17 -6
  29. package/dist/cli/manager.js +22 -20
  30. package/dist/cli/start.js +32 -24
  31. package/dist/cli/tickets.js +40 -24
  32. package/dist/continuity.d.ts +10 -14
  33. package/dist/continuity.js +198 -179
  34. package/dist/continuityOrigin.d.ts +13 -0
  35. package/dist/continuityOrigin.js +91 -0
  36. package/dist/foreman.d.ts +20 -5
  37. package/dist/foreman.js +57 -187
  38. package/dist/graph/boundary.d.ts +1 -1
  39. package/dist/graph/boundary.js +1 -1
  40. package/dist/graph/host.js +8 -19
  41. package/dist/graph/manager.d.ts +1 -1
  42. package/dist/graph/manager.js +1 -1
  43. package/dist/graph/preparation.d.ts +38 -0
  44. package/dist/graph/preparation.js +7 -6
  45. package/dist/graph/semantic.d.ts +1 -0
  46. package/dist/graph/semantic.js +4 -1
  47. package/dist/graph/session.js +7 -2
  48. package/dist/graph/turn.d.ts +2 -3
  49. package/dist/graph/turn.js +28 -40
  50. package/dist/handoffProtocol.js +2 -2
  51. package/dist/handoffs.d.ts +3 -1
  52. package/dist/handoffs.js +90 -59
  53. package/dist/hostGraphApplicability.d.ts +46 -0
  54. package/dist/hostGraphApplicability.js +97 -0
  55. package/dist/hostQaAugmentation.d.ts +18 -0
  56. package/dist/hostQaAugmentation.js +81 -0
  57. package/dist/hostQaComparison.d.ts +21 -2
  58. package/dist/hostQaComparison.js +43 -28
  59. package/dist/hostSourcePreservation.d.ts +38 -0
  60. package/dist/hostSourcePreservation.js +72 -0
  61. package/dist/interviews.d.ts +7 -0
  62. package/dist/interviews.js +18 -1
  63. package/dist/legacyCommunicationDisplay.d.ts +2 -0
  64. package/dist/legacyCommunicationDisplay.js +2 -0
  65. package/dist/managerPacket.js +3 -3
  66. package/dist/markers.d.ts +4 -3
  67. package/dist/markers.js +4 -20
  68. package/dist/probeIpc.d.ts +14 -0
  69. package/dist/probeIpc.js +29 -0
  70. package/dist/qaBuildGate.d.ts +21 -0
  71. package/dist/qaBuildGate.js +48 -12
  72. package/dist/qaBuilderCoverage.js +22 -13
  73. package/dist/qaBuilderEvidence.d.ts +16 -1
  74. package/dist/qaBuilderEvidence.js +48 -8
  75. package/dist/qaContractCoverage.d.ts +2 -2
  76. package/dist/qaContractCoverage.js +11 -7
  77. package/dist/qaContractDelivery.js +34 -20
  78. package/dist/qaCoverageAssembly.d.ts +22 -0
  79. package/dist/qaCoverageAssembly.js +16 -0
  80. package/dist/qaDeliveryJournal.d.ts +2 -0
  81. package/dist/qaDeliveryJournal.js +19 -0
  82. package/dist/qaEffectiveConfig.d.ts +1 -0
  83. package/dist/qaEffectiveConfig.js +6 -1
  84. package/dist/qaFailureDelivery.js +97 -85
  85. package/dist/qaHostTransformation.d.ts +26 -0
  86. package/dist/qaHostTransformation.js +100 -0
  87. package/dist/qaPreparation.d.ts +8 -20
  88. package/dist/qaPreparation.js +133 -229
  89. package/dist/qaPreparationInputs.js +19 -0
  90. package/dist/qaPreparationStore.js +34 -6
  91. package/dist/qaPrerequisites.d.ts +2 -1
  92. package/dist/qaPrerequisites.js +21 -1
  93. package/dist/qaProtocolV2.d.ts +15 -0
  94. package/dist/qaRecovery.js +21 -4
  95. package/dist/qaReview.js +232 -394
  96. package/dist/qaReviewEvidence.d.ts +7 -3
  97. package/dist/qaReviewEvidence.js +15 -4
  98. package/dist/qaRuntime.js +1 -1
  99. package/dist/qaSnapshot.js +71 -21
  100. package/dist/qaStructuredResponse.d.ts +10 -0
  101. package/dist/qaStructuredResponse.js +57 -0
  102. package/dist/qaTimeline.js +5 -3
  103. package/dist/qaToolRecovery.d.ts +12 -0
  104. package/dist/qaToolRecovery.js +44 -0
  105. package/dist/qaVerificationContract.d.ts +3 -2
  106. package/dist/qaVerificationContract.js +58 -34
  107. package/dist/readinessGate.js +5 -4
  108. package/dist/runtimeReadiness.js +23 -15
  109. package/dist/statusReporter.d.ts +2 -0
  110. package/dist/statusReporter.js +2 -0
  111. package/dist/structuredCutover.d.ts +31 -0
  112. package/dist/structuredCutover.js +510 -0
  113. package/dist/structuredCutoverFiles.d.ts +10 -0
  114. package/dist/structuredCutoverFiles.js +181 -0
  115. package/dist/structuredDispatchAuthority.d.ts +22 -0
  116. package/dist/structuredDispatchAuthority.js +110 -0
  117. package/dist/structuredDispatchRecord.d.ts +29 -0
  118. package/dist/structuredDispatchRecord.js +36 -0
  119. package/dist/structuredLiveBudget.d.ts +2 -0
  120. package/dist/structuredLiveBudget.js +23 -0
  121. package/dist/structuredOperation.d.ts +18 -0
  122. package/dist/structuredOperation.js +257 -0
  123. package/dist/supervisedStart.d.ts +1 -1
  124. package/dist/supervisedStart.js +78 -31
  125. package/dist/supervisor.d.ts +18 -2
  126. package/dist/supervisor.js +74 -17
  127. package/dist/ticketPlanning.d.ts +1 -373
  128. package/dist/ticketPlanning.js +2 -26
  129. package/dist/ticketPopulation.d.ts +0 -3
  130. package/dist/ticketPopulation.js +4 -21
  131. package/dist/ticketPopulationPageAuthority.d.ts +13 -0
  132. package/dist/ticketPopulationPageAuthority.js +30 -0
  133. package/dist/ticketPopulationPaging.d.ts +27 -0
  134. package/dist/ticketPopulationPaging.js +105 -0
  135. package/dist/tickets/renderMarkdown.d.ts +14 -0
  136. package/dist/tickets/renderMarkdown.js +39 -5
  137. package/dist/tickets/ticketLoader.js +10 -0
  138. package/dist/tickets/ticketSchema.d.ts +107 -0
  139. package/dist/tickets/ticketSchema.js +4 -1
  140. package/dist/workflowDb.d.ts +100 -2
  141. package/dist/workflowDb.js +781 -35
  142. package/dist/workflowReader.d.ts +1 -0
  143. package/dist/workflowReader.js +9 -0
  144. package/package.json +15 -7
package/dist/qaReview.js CHANGED
@@ -1,18 +1,25 @@
1
+ import { describeLegacyQaTranscript } from "./legacyCommunicationDisplay.js";
2
+ import { BoundedTurnEvents, boundedUtf8, receiptFingerprint } from "./boundedTurnEvidence.js";
3
+ import { qaStructuredResponse, qaStructuredStatus } from "./qaStructuredResponse.js";
4
+ import { journalStructuredDispatch, operationInstruction, operationPayload, operationResponse, nativePolicy } from "./structuredOperation.js";
5
+ import { durableOwnerDispatch } from "./structuredDispatchAuthority.js";
6
+ import { providerWireContract, qaVerdictOutput, qaRecoveryOutput, initializationOutput, remediationOutput } from "rafi-spec";
1
7
  import { withGraphDerivedAccess } from "./graph/derived.js";
2
8
  import { dispatchWithGraphAccess } from "./graph/session.js";
3
- import { finalQaRoleInstructions, resolveEffectiveQaConfiguration } from "./qaEffectiveConfig.js";
4
- import { COVERAGE_INSTRUCTIONS, bindCoverageHostFields, parseContractCoverage, validateContractCoverage } from "./qaContractCoverage.js";
9
+ import { finalQaRoleInstructions, QA_COMMAND_DISCIPLINE, resolveEffectiveQaConfiguration } from "./qaEffectiveConfig.js";
10
+ import { COVERAGE_INSTRUCTIONS, bindCoverageHostFields, validateContractCoverage } from "./qaContractCoverage.js";
5
11
  import { contractDigest, renderVerificationContract } from "./qaVerificationContract.js";
6
12
  import { finalReviewContractContext } from "./qaContractDelivery.js";
7
13
  import { qaCommandOutputExcerptsAllowed, retainedQaCommandObservations } from "./qaReviewEvidence.js";
14
+ import { retainedHostBuilderObservations } from "./qaBuilderEvidence.js";
8
15
  import { qaHostGraphFact } from "./qaHostFacts.js";
9
- import { graphContext, parseGraphRequest, prepareGraphInstruction, sendGraphTurn } from "./graph/turn.js";
16
+ import { graphContext, prepareGraphInstruction, sendGraphTurn } from "./graph/turn.js";
10
17
  import { BuildControlBoundary } from "./buildInterventions.js";
11
18
  import { formatRecoveryCommand } from "./recoveryGuidance.js";
12
19
  import { assertBuildAssignmentReconciled, BuildAssignmentRejected, captureBuildSource } from "./buildAssignment.js";
13
20
  import { checkQaPrerequisites } from "./qaPrerequisites.js";
14
21
  import { boundedQaHistory } from "./qaHandbackHistory.js";
15
- import { QA_FAILURE_REPORT_END, QA_FAILURE_REPORT_START, parseQaResponseContract, qaFailureReportV1Schema, } from "rafi-spec";
22
+ import { QA_FAILURE_REPORT_END, QA_FAILURE_REPORT_START, qaFailureReportV1Schema, } from "rafi-spec";
16
23
  import { SessionUnavailableError } from "./adapters/sessionFailure.js";
17
24
  import { currentActivity, withActivityPhase } from "./activity.js";
18
25
  import { buildQaInstruction, parseStepStatus } from "./foreman.js";
@@ -24,9 +31,12 @@ import { resolve } from "node:path";
24
31
  import { WorkflowDb } from "./workflowDb.js";
25
32
  import { canonicalSessionPath } from "./sessionIdentity.js";
26
33
  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";
34
+ import { appendQaRecoveryAttempt, appendQaRecoveryResource, appendQaRecoveryReviewedState, compareQaRecoveryReviewedState, createQaRecoveryPacket, ensureQaRecoveryExcluded, loadQaRecoveryPacket, materializeQaRecoveryContext, qaRecoveryInventory, qaReportDigest, renderQaRecoveryAcknowledgementInstruction, updateQaRecoveryPosition, updateQaRecoveryReviewIdentity, validateManualQaReport, } from "./qaRecovery.js";
28
35
  import { canonicalJson, createQaFindingRefs, qaDigest } from "./qaProtocolV2.js";
29
- import { applyHostQaComparison, createHostQaComparison, findHostQaCheck } from "./hostQaComparison.js";
36
+ import { createHostQaComparison, findHostQaCheck, inspectHostQaComparison } from "./hostQaComparison.js";
37
+ import { createHostGraphApplicability, findGraphQaChecks } from "./hostGraphApplicability.js";
38
+ import { assembleFinalQaCoverage } from "./qaCoverageAssembly.js";
39
+ import { qaUnresolvedToolDecision, recordQaUnresolvedTools } from "./qaToolRecovery.js";
30
40
  function ensureProtocolPausePacket(opts, head, stage, detail, frozenState) {
31
41
  const db = new WorkflowDb(opts.recovery.projectDir);
32
42
  try {
@@ -263,7 +273,7 @@ async function runIsolatedQaOwned(opts) {
263
273
  if (!opts.deliverBuilderFollowup)
264
274
  return { outcome: "needs-human", detail: "Builder guidance is waiting; resume through the build owner with a validated Builder session" };
265
275
  const latest = durableReviews.at(-1);
266
- 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="...".`;
276
+ 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.`;
267
277
  const fix = await opts.deliverBuilderFollowup(instruction, { reviewAttemptId: latest.attemptId, requirementsDigest: qaDigest("admitted-requirements", opts.ticket), maximum: maxBuilderFixes, authorizationId: opts.qaRemediationAuthorization });
268
278
  opts.qaRemediationAuthorization = undefined;
269
279
  if (!fix.ok) {
@@ -514,13 +524,13 @@ async function observedQaFix(opts, phase, request, causingAttemptId, remediation
514
524
  markQaRecoveryAction(opts, "builder-remediation-in-flight");
515
525
  }
516
526
  const result = await withActivityPhase(phase, () => deliverQaFailureOrLegacyFix(opts, request, causingAttemptId));
517
- const responseStatus = typeof result.response === "string" ? parseStepStatus(result.response) : undefined;
527
+ const responseStatus = result.outcome === "remediation-reported" ? { kind: "done" } : undefined;
518
528
  const accepted = result.ok
519
529
  && typeof result.response === "string" && result.response.length > 0
520
530
  && typeof result.summary === "string" && result.summary.length > 0
521
531
  && typeof result.providerTurnId === "string" && result.providerTurnId.length > 0
522
532
  && responseStatus?.kind === "done";
523
- 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;
533
+ 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;
524
534
  finishQaFixAttempt(opts, durable, normalized.ok ? "succeeded" : "failed", normalized.detail, normalized.response, normalized.summary, normalized.providerTurnId);
525
535
  if (normalized.ok)
526
536
  markQaRecoveryAction(opts, "qa-full-review");
@@ -641,16 +651,20 @@ function finishQaFixAttempt(opts, attempt, outcome, detail, response, summary, p
641
651
  return;
642
652
  const db = new WorkflowDb(opts.recovery.projectDir);
643
653
  try {
644
- const responseDigest = typeof response === "string" ? db.putEvidence("qa", Buffer.from(response)) : undefined;
645
- const summaryDigest = typeof summary === "string" ? db.putEvidence("qa", Buffer.from(boundedBuilderSummary(summary))) : undefined;
654
+ let responseDigest = typeof response === "string" ? db.putEvidence("qa", Buffer.from(response)) : undefined;
655
+ let summaryDigest = typeof summary === "string" ? db.putEvidence("qa", Buffer.from(boundedBuilderSummary(summary))) : undefined;
646
656
  const head = db.qaTicketHead(opts.recovery.runId, opts.ticket.id);
647
657
  let receipt;
648
658
  if (outcome === "succeeded") {
649
- if (!responseDigest || !summaryDigest)
650
- throw new Error("successful Builder remediation is missing response evidence");
651
- if (!providerTurnId)
652
- throw new Error("successful Builder remediation is missing its provider turn identity");
653
- 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() };
659
+ const turn = db.qaDeliveryTurns(attempt.operationId).at(-1);
660
+ if (!turn || turn.status !== "completed" || !turn.sealedDispatch || !turn.rawResponseDigest || turn.providerTurnId !== providerTurnId)
661
+ throw new Error("successful Builder remediation requires its persisted native delivery receipt");
662
+ const native = db.replayStructuredDispatch(turn.turnRecordId, remediationOutput);
663
+ if (native.result.kind !== "completed" || native.result.payload.status !== "done")
664
+ throw new Error("Builder remediation has no completed native result");
665
+ responseDigest = turn.rawResponseDigest;
666
+ summaryDigest = db.putEvidence("qa", native.result.payload.report.summary);
667
+ 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() };
654
668
  }
655
669
  db.commitQaRemediationOutcome({ recoveryAttemptId: attempt.recoveryId, remediationAttemptId: attempt.remediationId, outcome, detail, responseDigest, summaryDigest, receipt, expectedRevision: head.revision });
656
670
  }
@@ -699,7 +713,6 @@ async function oneReview(opts, identity) {
699
713
  return { outcome: "needs-human", detail: `${error.message}. No QA session or report was created. Resume with: ${formatRecoveryCommand(opts.recovery.projectDir)}` };
700
714
  }
701
715
  let qa;
702
- let recoverySnapshot;
703
716
  let recoveryContext;
704
717
  let resumedPacket = opts.resumedRecovery;
705
718
  let resumedFullReviewNeedsBinding = false;
@@ -741,7 +754,7 @@ async function oneReview(opts, identity) {
741
754
  return { outcome: "needs-human", detail };
742
755
  }
743
756
  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 });
744
- let graphPrepared = opts.resumedRecovery ? { instruction: handoff, bytes: 0 } : await prepareGraphInstruction(handoff, graphReviewContext, await qa.contextUsage?.());
757
+ let graphPrepared = opts.resumedRecovery ? { instruction: handoff, bytes: 0 } : await prepareGraphInstruction(handoff, graphReviewContext, await qa.contextUsage?.(), qaVerdictOutput);
745
758
  handoff = graphPrepared.instruction;
746
759
  const v2Binding = beginV2Review(opts, snapshot.frozenState, snapshot.path, handoff, qaHandle, { attemptId, cycle, remediationGeneration });
747
760
  handoff = v2Binding.reviewInstruction;
@@ -751,8 +764,6 @@ async function oneReview(opts, identity) {
751
764
  durableReviewStarted = true;
752
765
  if (resumedPacket)
753
766
  resumedPacket = v2Binding.recoveryPacket;
754
- let preparedTurn;
755
- let preparedContract;
756
767
  if (opts.resumedRecovery) {
757
768
  if (opts.resumedRecovery.manifest.ticketId !== opts.ticket.id)
758
769
  return { outcome: "needs-human", detail: `QA recovery packet ticket ${opts.resumedRecovery.manifest.ticketId} does not match current ticket ${opts.ticket.id}` };
@@ -771,24 +782,8 @@ async function oneReview(opts, identity) {
771
782
  catch (error) {
772
783
  const detail = error instanceof Error ? error.message : String(error);
773
784
  const synthetic = { text: "", rawResponse: "", cleanedResponse: "", isError: true, numTurns: 0, costUsd: 0 };
774
- const recovery = await repairResumedFailureReport(opts, identity, resumedPacket, snapshot, qaHandle, synthetic, [detail], qaEvents, undefined, true);
775
- if (recovery.outcome === "failed")
776
- return recovery.result;
777
- if (recovery.outcome === "retry")
778
- return { outcome: "retry-modification" };
779
- if (recovery.outcome === "passed")
780
- return recovery.result;
781
- qa = recovery.qa;
782
- recoverySnapshot = recovery.snapshot;
783
- if (recovery.outcome === "manual") {
784
- const text = `${QA_FAILURE_REPORT_START}\n${JSON.stringify(recovery.report)}\n${QA_FAILURE_REPORT_END}\nSTEP_STATUS: qa_fail | issues="operator repaired report"`;
785
- preparedTurn = { text, rawResponse: text, cleanedResponse: text, isError: false, numTurns: 0, costUsd: 0 };
786
- preparedContract = { valid: true, errors: [], status: "qa_fail", fields: { issues: "operator repaired report" }, report: recovery.report, rawReportJson: JSON.stringify(recovery.report) };
787
- }
788
- else {
789
- preparedTurn = recovery.turn;
790
- preparedContract = recovery.contract;
791
- }
785
+ const recovery = await retainInvalidResumedResponse(opts, identity, resumedPacket, snapshot, synthetic, [detail], qaEvents);
786
+ return recovery.result;
792
787
  }
793
788
  let comparison;
794
789
  try {
@@ -797,103 +792,50 @@ async function oneReview(opts, identity) {
797
792
  catch (error) {
798
793
  const reason = `QA recovery error: current reviewed state cannot be reconstructed: ${error instanceof Error ? error.message : String(error)}`;
799
794
  const synthetic = { text: "", rawResponse: "", cleanedResponse: "", isError: true, numTurns: 0, costUsd: 0 };
800
- const recovery = await repairResumedFailureReport(opts, identity, resumedPacket, snapshot, qaHandle, synthetic, [reason], qaEvents, undefined, true, reason);
801
- if (recovery.outcome === "failed")
802
- return recovery.result;
803
- if (recovery.outcome === "retry")
804
- return { outcome: "retry-modification" };
805
- if (recovery.outcome === "passed")
806
- return recovery.result;
807
- if (recovery.outcome === "manual")
808
- return { outcome: "needs-human", detail: `${reason}; a manual reconstruction cannot replace missing reviewed-state evidence` };
809
- qa = recovery.qa;
810
- recoverySnapshot = recovery.snapshot;
811
- preparedTurn = recovery.turn;
812
- preparedContract = recovery.contract;
813
- const recoveredState = (recovery.snapshot ?? snapshot).frozenState;
814
- comparison = { matches: true, originalDigest: recoveredState.digest, currentDigest: recoveredState.digest, drift: [], frozenState: recoveredState };
795
+ const recovery = await retainInvalidResumedResponse(opts, identity, resumedPacket, snapshot, synthetic, [reason], qaEvents);
796
+ return recovery.result;
815
797
  }
816
- if (!preparedTurn) {
817
- if (!comparison.matches) {
818
- 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" });
819
- if (resumedPacket.manifest.reviewedStateDigest !== snapshot.frozenState.digest) {
820
- resumedPacket = appendQaRecoveryReviewedState(resumedPacket, snapshot.frozenState, `reviewed-state/current-r${resumedPacket.manifest.revision + 1}`);
821
- }
822
- }
823
- try {
824
- recoveryContext = materializeQaRecoveryContext(resumedPacket, snapshot.path);
825
- bindQaRecoveryContext(qa, recoveryContext);
826
- }
827
- catch (error) {
828
- const detail = `QA recovery context could not be materialized: ${error instanceof Error ? error.message : String(error)}`;
829
- const synthetic = { text: "", rawResponse: "", cleanedResponse: "", isError: true, numTurns: 0, costUsd: 0 };
830
- const recovery = await repairResumedFailureReport(opts, identity, resumedPacket, snapshot, qaHandle, synthetic, [detail], qaEvents, undefined, true);
831
- if (recovery.outcome === "failed")
832
- return recovery.result;
833
- if (recovery.outcome === "retry")
834
- return { outcome: "retry-modification" };
835
- if (recovery.outcome === "passed")
836
- return recovery.result;
837
- qa = recovery.qa;
838
- recoverySnapshot = recovery.snapshot;
839
- if (recovery.outcome === "manual") {
840
- const text = `${QA_FAILURE_REPORT_START}\n${JSON.stringify(recovery.report)}\n${QA_FAILURE_REPORT_END}\nSTEP_STATUS: qa_fail | issues="operator repaired report"`;
841
- preparedTurn = { text, rawResponse: text, cleanedResponse: text, isError: false, numTurns: 0, costUsd: 0 };
842
- preparedContract = { valid: true, errors: [], status: "qa_fail", fields: { issues: "operator repaired report" }, report: recovery.report, rawReportJson: JSON.stringify(recovery.report) };
843
- }
844
- else {
845
- preparedTurn = recovery.turn;
846
- preparedContract = recovery.contract;
847
- }
848
- }
849
- if (!preparedTurn) {
850
- if (!recoveryContext)
851
- throw new Error("QA recovery context invariant failed after successful materialization");
852
- const acknowledgement = renderQaRecoveryAcknowledgementInstruction(resumedPacket, recoveryContext.relativePath);
853
- const acknowledged = await performRecoveryAcknowledgement(resumedPacket, qa, acknowledgement, qaEvents, Boolean(opts.continuityManaged), "resume-entry", recoveryContext);
854
- resumedPacket = acknowledged.packet;
855
- const ackErrors = acknowledged.errors;
856
- if (ackErrors.length) {
857
- if (acknowledged.retryable)
858
- return retryFreshQaReview(opts, "invalid-recovery-acknowledgement", `QA recovery acknowledgement was invalid: ${ackErrors.join("; ")}`, snapshot.frozenState);
859
- const synthetic = { text: "", rawResponse: "", cleanedResponse: "", isError: true, numTurns: 0, costUsd: 0 };
860
- const recovery = await repairResumedFailureReport(opts, identity, resumedPacket, snapshot, qaHandle, synthetic, ackErrors, qaEvents, recoveryContext, true);
861
- if (recovery.outcome === "failed")
862
- return recovery.result;
863
- if (recovery.outcome === "retry")
864
- return { outcome: "retry-modification" };
865
- if (recovery.outcome === "passed")
866
- return recovery.result;
867
- qa = recovery.qa;
868
- recoverySnapshot = recovery.snapshot;
869
- if (recovery.outcome === "manual") {
870
- const text = `${QA_FAILURE_REPORT_START}\n${JSON.stringify(recovery.report)}\n${QA_FAILURE_REPORT_END}\nSTEP_STATUS: qa_fail | issues="recovered report"`;
871
- preparedTurn = { text, rawResponse: text, cleanedResponse: text, isError: false, numTurns: 0, costUsd: 0 };
872
- preparedContract = { valid: true, errors: [], status: "qa_fail", fields: { issues: "recovered report" }, report: recovery.report, rawReportJson: JSON.stringify(recovery.report) };
873
- }
874
- else {
875
- preparedTurn = recovery.turn;
876
- preparedContract = recovery.contract;
877
- }
878
- }
879
- if (!preparedTurn) {
880
- try {
881
- recoveryContext.verify();
882
- }
883
- catch (error) {
884
- return pauseQaReview(opts, "recovery-context-mutation", error instanceof Error ? error.message : String(error), (recoverySnapshot ?? snapshot).frozenState);
885
- }
886
- 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)}.`}`;
887
- resumedFullReviewNeedsBinding = true;
888
- }
798
+ if (!comparison.matches) {
799
+ 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" });
800
+ if (resumedPacket.manifest.reviewedStateDigest !== snapshot.frozenState.digest) {
801
+ resumedPacket = appendQaRecoveryReviewedState(resumedPacket, snapshot.frozenState, `reviewed-state/current-r${resumedPacket.manifest.revision + 1}`);
889
802
  }
890
803
  }
804
+ try {
805
+ recoveryContext = materializeQaRecoveryContext(resumedPacket, snapshot.path);
806
+ bindQaRecoveryContext(qa, recoveryContext);
807
+ }
808
+ catch (error) {
809
+ const detail = `QA recovery context could not be materialized: ${error instanceof Error ? error.message : String(error)}`;
810
+ const synthetic = { text: "", rawResponse: "", cleanedResponse: "", isError: true, numTurns: 0, costUsd: 0 };
811
+ const recovery = await retainInvalidResumedResponse(opts, identity, resumedPacket, snapshot, synthetic, [detail], qaEvents);
812
+ return recovery.result;
813
+ }
814
+ if (!recoveryContext)
815
+ throw new Error("QA recovery context invariant failed after successful materialization");
816
+ const acknowledgement = renderQaRecoveryAcknowledgementInstruction(resumedPacket, recoveryContext.relativePath);
817
+ const acknowledged = await performRecoveryAcknowledgement(resumedPacket, qa, acknowledgement, qaEvents, Boolean(opts.continuityManaged), "resume-entry", recoveryContext);
818
+ resumedPacket = acknowledged.packet;
819
+ const ackErrors = acknowledged.errors;
820
+ if (ackErrors.length) {
821
+ const synthetic = { text: "", rawResponse: "", cleanedResponse: "", isError: true, numTurns: 0, costUsd: 0 };
822
+ const recovery = await retainInvalidResumedResponse(opts, identity, resumedPacket, snapshot, synthetic, ackErrors, qaEvents);
823
+ return recovery.result;
824
+ }
825
+ try {
826
+ recoveryContext.verify();
827
+ }
828
+ catch (error) {
829
+ return pauseQaReview(opts, "recovery-context-mutation", error instanceof Error ? error.message : String(error), snapshot.frozenState);
830
+ }
831
+ 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)}.`}`;
832
+ resumedFullReviewNeedsBinding = true;
891
833
  opts.resumedRecovery = undefined;
892
834
  }
893
835
  if (resumedFullReviewNeedsBinding) {
894
836
  const resumedAttemptId = randomUUID();
895
837
  graphReviewContext = { ...graphReviewContext, operationId: `graph-review:${resumedAttemptId}`, logicalTaskId: resumedAttemptId };
896
- graphPrepared = await prepareGraphInstruction(handoff, graphReviewContext, await qa.contextUsage?.());
838
+ graphPrepared = await prepareGraphInstruction(handoff, graphReviewContext, await qa.contextUsage?.(), qaVerdictOutput);
897
839
  handoff = graphPrepared.instruction;
898
840
  const resumedReviewBinding = beginV2Review(opts, snapshot.frozenState, snapshot.path, handoff, qaHandle, { attemptId: resumedAttemptId, cycle, remediationGeneration });
899
841
  bindQaTurnAdapter(qa, resumedReviewBinding);
@@ -902,54 +844,29 @@ async function oneReview(opts, identity) {
902
844
  }
903
845
  let graphSubturn = 0;
904
846
  const activeGraphBinding = qaTurnBindings.get(qa);
905
- 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 });
906
- let status = parseStepStatus(turn.text);
847
+ 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 });
848
+ const status = qaStructuredStatus(turn);
907
849
  recoveryContext = qaTurnBindings.get(qa)?.recoveryContext;
908
850
  try {
909
- if (!preparedTurn)
910
- recoveryContext?.verify();
851
+ recoveryContext?.verify();
911
852
  await opts.observeNativeCompactions?.(qa);
912
853
  recoveryContext?.verify();
913
854
  }
914
855
  catch (error) {
915
- return pauseQaReview(opts, "recovery-context-mutation", error instanceof Error ? error.message : String(error), (recoverySnapshot ?? snapshot).frozenState);
916
- }
917
- let responseContract = preparedContract ?? parseQaResponseContract(turn.text);
918
- let responseProtocolError;
919
- if (status.kind !== "qa_fail" && status.kind !== "unknown" && !responseContract.valid) {
920
- responseProtocolError = `invalid QA response contract: ${responseContract.errors.join("; ")}`;
921
- }
922
- if (!turn.isError && status.kind === "blocked" && opts.resolveBlocked) {
923
- const reason = status.reason ?? "QA reported an unspecified blocker";
924
- turn = await sendDurableQaTurn(qa, [
925
- `Your QA review reported a blocker: ${reason}`,
926
- "Resolve the blocker if possible without changing project files. Otherwise return a precise STEP_STATUS: blocked marker.",
927
- "If you can finish the review, return the complete QA response contract.",
928
- ].join("\n\n"), qaEvents, "blocked-recovery");
929
- try {
930
- recoveryContext?.verify();
931
- }
932
- catch (error) {
933
- return pauseQaReview(opts, "recovery-context-mutation", error instanceof Error ? error.message : String(error), (recoverySnapshot ?? snapshot).frozenState);
934
- }
935
- status = parseStepStatus(turn.text);
936
- try {
937
- await opts.observeNativeCompactions?.(qa);
938
- recoveryContext?.verify();
939
- }
940
- catch (error) {
941
- return pauseQaReview(opts, "recovery-context-mutation", error instanceof Error ? error.message : String(error), (recoverySnapshot ?? snapshot).frozenState);
942
- }
943
- responseContract = parseQaResponseContract(turn.text);
944
- responseProtocolError = undefined;
945
- if (status.kind !== "qa_fail" && status.kind !== "unknown" && !responseContract.valid) {
946
- responseProtocolError = `invalid QA response contract after blocker recovery: ${responseContract.errors.join("; ")}`;
947
- }
856
+ return pauseQaReview(opts, "recovery-context-mutation", error instanceof Error ? error.message : String(error), snapshot.frozenState);
857
+ }
858
+ const responseContract = qaStructuredResponse(turn);
859
+ const unresolvedDecision = qaUnresolvedToolDecision(opts.recovery.projectDir, opts.recovery.runId, opts.ticket.id, qaEvents);
860
+ if (unresolvedDecision.action !== "none") {
861
+ recordQaUnresolvedTools(opts.recovery.projectDir, opts.recovery.runId, opts.ticket.id, qaEvents);
862
+ if (unresolvedDecision.action === "retry")
863
+ return retryFreshQaReview(opts, "unresolved-provider-tool", unresolvedDecision.detail, snapshot.frozenState);
864
+ return pauseQaReview(opts, "unresolved-provider-tool", unresolvedDecision.detail, snapshot.frozenState);
948
865
  }
949
866
  opts.state.reviews += 1;
950
867
  opts.state.sessionId = qa.sessionId();
951
868
  opts.state.sessionRef = qa.sessionRef?.();
952
- const reviewedSnapshot = recoverySnapshot ?? snapshot;
869
+ const reviewedSnapshot = snapshot;
953
870
  const changes = await withActivityPhase("checking QA file changes", () => reviewedSnapshot.qaChanges());
954
871
  if (changes.length) {
955
872
  opts.state.modificationViolations += 1;
@@ -978,9 +895,13 @@ async function oneReview(opts, identity) {
978
895
  return { outcome: "retry-modification" };
979
896
  }
980
897
  if (turn.continuityErrors?.length && !turn.failure)
981
- return retryFreshQaReview(opts, "invalid-continuity", `QA continuity record was invalid: ${turn.continuityErrors.join("; ")}`, reviewedSnapshot.frozenState);
982
- if (responseProtocolError && !turn.isError && !turn.failure)
983
- return retryFreshQaReview(opts, "invalid-response-contract", responseProtocolError, reviewedSnapshot.frozenState);
898
+ return pauseQaReview(opts, "invalid-continuity", `QA continuity record was invalid: ${turn.continuityErrors.join("; ")}`, reviewedSnapshot.frozenState);
899
+ if (!responseContract.valid && !turn.failure) {
900
+ const retained = resumedPacket
901
+ ? await retainInvalidResumedResponse(opts, identity, resumedPacket, snapshot, turn, responseContract.errors, qaEvents)
902
+ : await retainInvalidQaResponse(opts, identity, snapshot, qa, qaHandle, handoff, turn, responseContract.errors, qaEvents);
903
+ return retained.result;
904
+ }
984
905
  if (turn.isError || turn.failure)
985
906
  return pauseQaReview(opts, "qa-turn-error", `QA turn failed: ${sanitizePreview(turn.text)}`, reviewedSnapshot.frozenState);
986
907
  if (status.kind === "blocked")
@@ -1007,64 +928,7 @@ async function oneReview(opts, identity) {
1007
928
  if (status.kind === "qa_fail" || responseContract.status === "qa_fail") {
1008
929
  let contract = responseContract;
1009
930
  if (!contract.valid || !contract.report) {
1010
- const recovery = resumedPacket
1011
- ? await repairResumedFailureReport(opts, identity, resumedPacket, snapshot, qaHandle, turn, contract.errors, qaEvents, recoveryContext)
1012
- : await repairInvalidFailureReport(opts, identity, snapshot, qa, qaHandle, handoff, turn, contract.errors, qaEvents);
1013
- if (recovery.outcome === "failed")
1014
- return recovery.result;
1015
- if (recovery.outcome === "retry")
1016
- return { outcome: "retry-modification" };
1017
- if (recovery.outcome === "passed")
1018
- return recovery.result;
1019
- if (recovery.outcome === "manual") {
1020
- qa = recovery.qa;
1021
- recoverySnapshot = recovery.snapshot;
1022
- contract = { valid: true, errors: [], status: "qa_fail", fields: {}, report: recovery.report };
1023
- }
1024
- else {
1025
- qa = recovery.qa;
1026
- recoverySnapshot = recovery.snapshot;
1027
- turn = recovery.turn;
1028
- contract = recovery.contract;
1029
- }
1030
- const recoveryChanges = await (recoverySnapshot ?? snapshot).qaChanges();
1031
- if (recoveryChanges.length)
1032
- return { outcome: "needs-human", detail: `QA modified files during report recovery: ${recoveryChanges.join(", ")}` };
1033
- activeBinding = qaTurnBindings.get(qa);
1034
- if (!activeBinding)
1035
- throw new Error("Recovered QA verdict is missing its durable source/review-basis binding");
1036
- const recoveredLiveSource = await captureFrozenQaSourceAsync(opts.builderWorktree, progress);
1037
- const recoveredSnapshot = recoverySnapshot ?? snapshot;
1038
- if (recoveredLiveSource.digest !== activeBinding.sourceStateDigest || recoveredSnapshot.frozenState.digest !== activeBinding.sourceStateDigest) {
1039
- const driftDb = new WorkflowDb(opts.recovery.projectDir);
1040
- try {
1041
- const head = driftDb.qaTicketHead(opts.recovery.runId, opts.ticket.id);
1042
- if (head.state === "turn-intended")
1043
- driftDb.transitionQa(opts.recovery.runId, opts.ticket.id, head.revision, { type: "source-drift" });
1044
- }
1045
- finally {
1046
- driftDb.close();
1047
- }
1048
- finishDurableQaReview(opts, activeBinding.reviewAttemptId, { status: "interrupted", detail: `Builder source drifted before recovered verdict acceptance (${activeBinding.sourceStateDigest} -> ${recoveredLiveSource.digest})` });
1049
- durableReviewFinished = true;
1050
- return { outcome: "retry-modification" };
1051
- }
1052
- if (contract.valid && contract.status === "qa_pass") {
1053
- if (missingPrerequisites.length)
1054
- return pauseQaReview(opts, "verification-not-run", "Required verification remains not_run; prerequisite recovery and fresh QA required", reviewedSnapshot.frozenState);
1055
- let certificate;
1056
- try {
1057
- certificate = finishV2Pass(opts, qa, contract.fields.summary ?? "qa_pass");
1058
- }
1059
- catch (error) {
1060
- const detail = error instanceof Error ? error.message : String(error);
1061
- return retryFreshQaReview(opts, "invalid-qa-coverage", `Recovered QA pass evidence was not parseable or complete: ${detail}`, reviewedSnapshot.frozenState);
1062
- }
1063
- markQaRecoveryResolved(opts);
1064
- durableReviewFinished = true;
1065
- 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) });
1066
- return { outcome: "passed", summary: contract.fields.summary, passCertificateId: certificate.certificateId, sourceStateDigest: certificate.sourceStateDigest, reviewBasisDigest: certificate.reviewBasisDigest };
1067
- }
931
+ return pauseQaReview(opts, "invalid-native-qa-response", "Failed QA verdict has no validated failure report", reviewedSnapshot.frozenState);
1068
932
  }
1069
933
  const acceptedBinding = activeBinding;
1070
934
  const report = contract.report;
@@ -1090,7 +954,7 @@ async function oneReview(opts, identity) {
1090
954
  markQaRecoveryPendingBuilder(opts);
1091
955
  return { outcome: "failed", detail, report, reportDigest: digest, reviewAttempt: acceptedBinding.reviewNumber, reviewAttemptId: acceptedBinding.reviewAttemptId };
1092
956
  }
1093
- return retryFreshQaReview(opts, "unrecognized-qa-outcome", status.error ?? `QA returned ${status.kind}`, reviewedSnapshot.frozenState);
957
+ return pauseQaReview(opts, "unrecognized-qa-outcome", status.error ?? `QA returned ${status.kind}`, reviewedSnapshot.frozenState);
1094
958
  }
1095
959
  finally {
1096
960
  if (durableReviewStarted && !durableReviewFinished) {
@@ -1109,8 +973,6 @@ async function oneReview(opts, identity) {
1109
973
  }
1110
974
  if (qa)
1111
975
  await withActivityPhase("closing QA session", () => qa.close().catch(() => { }));
1112
- if (recoverySnapshot)
1113
- await recoverySnapshot.remove().catch(() => { });
1114
976
  await withActivityPhase("cleaning up disposable QA snapshot", () => opts.observer
1115
977
  ? opts.observer.span("cleanup", "cleaning up disposable QA snapshot", () => snapshot.remove())
1116
978
  : snapshot.remove());
@@ -1128,8 +990,7 @@ function finishDurableQaReview(opts, attemptId, patch) {
1128
990
  function qaFindingRefs(runId, ticketId, reviewAttemptId, reportDigest, rawFindingIds) {
1129
991
  return createQaFindingRefs({ runId, ticketId, reviewAttemptId, reportDigest, rawFindingIds });
1130
992
  }
1131
- async function repairResumedFailureReport(opts, identity, initialPacket, snapshot, originalHandle, originalTurn, originalErrors, qaEvents, recoveryContext, startAtMenu = false, requireFreshReviewReason) {
1132
- const originalQa = originalHandle.adapter;
993
+ async function retainInvalidResumedResponse(opts, identity, initialPacket, snapshot, originalTurn, originalErrors, qaEvents) {
1133
994
  const originalResponse = effectiveTurnText(originalTurn);
1134
995
  let packet = appendQaRecoveryResource(initialPacket, `qa-responses/resumed-${identity.attemptId}.txt`, originalResponse, {
1135
996
  purpose: "Exact first cleaned QA report response after resumed packet acknowledgement", exact: true,
@@ -1139,13 +1000,7 @@ async function repairResumedFailureReport(opts, identity, initialPacket, snapsho
1139
1000
  packet = appendQaRecoveryResource(packet, `validation/resumed-${identity.attemptId}.json`, {
1140
1001
  status: parseStepStatus(originalResponse), errors: originalErrors, recoveryStage: "resumed-fail-closed",
1141
1002
  }, { purpose: "Validation result for the first resumed QA report response" });
1142
- void packet;
1143
- void requireFreshReviewReason;
1144
- if (!originalTurn.isError && !originalTurn.failure) {
1145
- const result = retryFreshQaReview(opts, "resumed-invalid-report", "Resumed QA report was invalid; a complete fresh QA review is required", snapshot.frozenState);
1146
- return result.outcome === "retry-modification" ? { outcome: "retry" } : { outcome: "failed", result };
1147
- }
1148
- 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) };
1003
+ return { outcome: "failed", result: pauseQaReview(opts, "resumed-invalid-contract", `Resumed QA response failed validation. Evidence retained at ${packet.directory}. ${originalErrors.map(error => sanitizePreview(error)).join("; ")}`, snapshot.frozenState) };
1149
1004
  }
1150
1005
  export function buildQaReviewHandoff(ticket, builderSummary, diffDigest, validationChecklist, changeSummary, history = []) {
1151
1006
  return [
@@ -1155,7 +1010,8 @@ export function buildQaReviewHandoff(ticket, builderSummary, diffDigest, validat
1155
1010
  `Project validation checklist: ${validationChecklist.join("; ")}`,
1156
1011
  `Prior authoritative QA/report and Builder-remediation history: ${history.length ? JSON.stringify(boundedQaHistory(history)) : "(none; perform a complete first review)"}`,
1157
1012
  `Failure report JSON Schema: ${JSON.stringify(qaFailureReportV1Schema)}`,
1158
- `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"`,
1013
+ QA_COMMAND_DISCIPLINE,
1014
+ "Submit the native qa-verdict payload. A failed verdict must include the complete failure report; a passed verdict must include all required coverage claims.",
1159
1015
  "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.",
1160
1016
  "Do not change source, configuration, documentation, tickets, or control files. Only ignored cache, coverage, and build output is allowed.",
1161
1017
  ].join("\n");
@@ -1176,16 +1032,7 @@ export async function compactWithRetry(adapter) {
1176
1032
  export const QA_REPORT_RECOVERY_MENU = [
1177
1033
  "Try a complete fresh QA review.", "Manually fix saved JSON for human inspection.", "Give QA specific instructions for a fresh review.", "Pause.",
1178
1034
  ];
1179
- function correctionInstruction(errors, guidance) {
1180
- return [
1181
- "Report correction only. Reconstruct the already-completed QA review without rerunning tools, tests, or repository inspection.",
1182
- `The prior failure report was invalid: ${errors.join("; ")}`,
1183
- guidance ? `Operator guidance: ${guidance}` : "",
1184
- `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"`,
1185
- "Do not omit any completed finding, check, evidence, or observation.",
1186
- ].filter(Boolean).join("\n\n");
1187
- }
1188
- async function repairInvalidFailureReport(opts, identity, snapshot, originalQa, qaHandle, reviewPrompt, originalTurn, originalErrors, qaEvents) {
1035
+ async function retainInvalidQaResponse(opts, identity, snapshot, originalQa, qaHandle, reviewPrompt, originalTurn, originalErrors, qaEvents) {
1189
1036
  const originalResponse = effectiveTurnText(originalTurn);
1190
1037
  const runId = opts.recovery?.runId ?? `volatile-${identity.attemptId}`;
1191
1038
  const continuity = qaContinuityRecoveryContext(opts);
@@ -1203,11 +1050,11 @@ async function repairInvalidFailureReport(opts, identity, snapshot, originalQa,
1203
1050
  "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" },
1204
1051
  "original-host-prompt": { value: originalTurn.hostInstruction ?? reviewPrompt, purpose: "Exact host-requested original QA instruction", exactText: true },
1205
1052
  "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" },
1206
- "original-raw-response": { value: originalTurn.rawResponse ?? originalResponse, purpose: "Exact raw provider response before cleanup", exactText: true },
1207
- "original-cleaned-response": { value: originalResponse, purpose: "Exact cleaned original response used for contract validation", exactText: true },
1053
+ "original-raw-response": { value: originalTurn.transportEvidence ?? { unavailable: true }, purpose: "Retained bounded transport references" },
1054
+ "original-cleaned-response": { value: originalTurn.transportEvidence ?? { unavailable: true }, purpose: "Retained bounded cleaned evidence references" },
1208
1055
  "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" },
1209
1056
  "qa-history": { value: opts.qaHistory ?? [], purpose: "Earlier valid reports, observations, fixes, and unresolved history" },
1210
- "tool-events": { value: qaEvents.length ? qaEvents : { unavailable: true }, purpose: "All QA tool/activity events available through BuilderEvent, or an explicit unavailable marker" },
1057
+ "tool-events": { value: qaEvents instanceof BoundedTurnEvents ? qaEvents.metadata() : { unavailable: true }, purpose: "All QA tool/activity events available through BuilderEvent, or an explicit unavailable marker" },
1211
1058
  continuity: { value: continuity, purpose: "Cumulative continuity checkpoints, later host facts, handoff lineage, and recovery history" },
1212
1059
  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" },
1213
1060
  "context-usage": { value: await originalQa.contextUsage?.().catch(() => undefined) ?? { unavailable: true }, purpose: "Provider context usage sample or explicit unavailable marker" },
@@ -1224,69 +1071,11 @@ async function repairInvalidFailureReport(opts, identity, snapshot, originalQa,
1224
1071
  packetDb.close();
1225
1072
  }
1226
1073
  let packet = existingPacket ? appendQaRecoveryAttempt(existingPacket, packetInput) : createQaRecoveryPacket(packetInput);
1227
- let qa = originalQa;
1228
- let activeQaHandle = qaHandle;
1229
- let errors = originalErrors;
1230
- let turns = 0;
1231
- let recoveryContextSeal;
1232
- let protocolViolation;
1233
- let correctionExecutionFailed = false;
1234
- const attemptCorrections = async (count, stage, guidance) => {
1235
- for (let index = 0; index < count; index++) {
1236
- turns++;
1237
- packet = updateQaRecoveryPosition(packet, stage, turns, "qa-correction");
1238
- const prompt = correctionInstruction(errors, guidance);
1239
- packet = appendQaRecoveryResource(packet, `prompts/${String(turns).padStart(2, "0")}.txt`, prompt, { purpose: `Exact QA report-correction prompt ${turns} (${stage})`, exact: true });
1240
- try {
1241
- recoveryContextSeal?.verify();
1242
- }
1243
- catch (error) {
1244
- protocolViolation = error instanceof Error ? error.message : String(error);
1245
- return undefined;
1246
- }
1247
- const result = await sendDurableQaTurn(qa, prompt, qaEvents, `${stage}:correction-${index + 1}`);
1248
- try {
1249
- recoveryContextSeal?.verify();
1250
- }
1251
- catch (error) {
1252
- protocolViolation = error instanceof Error ? error.message : String(error);
1253
- return undefined;
1254
- }
1255
- const contract = parseQaResponseContract(result.text);
1256
- packet = await appendTurnObservation(packet, `correction-${String(turns).padStart(2, "0")}`, result, prompt, contract.errors, stage, qaEvents);
1257
- packet = appendQaRecoveryResource(packet, `qa-responses/${String(turns).padStart(2, "0")}.txt`, result.text, { purpose: `Exact QA correction response ${turns} (${stage})`, exact: true });
1258
- packet = appendQaRecoveryResource(packet, `validation/${String(turns).padStart(2, "0")}.json`, {
1259
- status: contract.status, fields: contract.fields, errors: contract.errors,
1260
- turn: { isError: result.isError, numTurns: result.numTurns, costUsd: result.costUsd, inputTokens: result.inputTokens ?? "unavailable", outputTokens: result.outputTokens ?? "unavailable" }, stage,
1261
- }, { purpose: `Parsed status, validator result, turn metadata, and recovery stage for correction ${turns}` });
1262
- packet = appendQaRecoveryResource(packet, "context/tool-events.json", qaEvents.length ? qaEvents : { unavailable: true }, { purpose: "Latest complete set of QA BuilderEvent records available to the host" });
1263
- try {
1264
- recoveryContextSeal?.verify();
1265
- }
1266
- catch (error) {
1267
- protocolViolation = error instanceof Error ? error.message : String(error);
1268
- return undefined;
1269
- }
1270
- if (!result.isError && !result.failure && contract.valid && contract.report)
1271
- return { result, contract };
1272
- correctionExecutionFailed = Boolean(result.failure || (result.isError && !result.continuityErrors?.length));
1273
- errors = result.isError ? [`QA correction turn errored: ${sanitizePreview(result.text)}`] : contract.errors;
1274
- }
1275
- return undefined;
1276
- };
1277
- let fixed = await attemptCorrections(1, "same-session");
1278
- if (fixed && !protocolViolation)
1279
- return { outcome: "recovered", qa, turn: fixed.result, contract: fixed.contract };
1280
- if (protocolViolation)
1281
- packet = appendQaRecoveryResource(packet, `recovery-history/context-mutation-${packet.manifest.revision + 1}.txt`, protocolViolation, { purpose: "Recovery-context mutation protocol violation", exact: true });
1282
- void packet;
1283
- if (protocolViolation || correctionExecutionFailed)
1284
- return { outcome: "failed", result: pauseQaReview(opts, "report-correction-exhausted", protocolViolation ?? errors.join("; "), snapshot.frozenState) };
1285
- 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);
1286
- return result.outcome === "retry-modification" ? { outcome: "retry" } : { outcome: "failed", result };
1074
+ 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" });
1075
+ return { outcome: "failed", result: pauseQaReview(opts, "invalid-native-qa-response", `QA response failed validation. Evidence retained at ${packet.directory}. ${originalErrors.join("; ")}`, snapshot.frozenState) };
1287
1076
  }
1288
- async function exhausted(packet, originalResponse, opts, prefix = "QA report correction exhausted after nine turns", qa, snapshot, liveSession = Boolean(qa), unavailableOperatorReason, requireFreshReviewReason) {
1289
- const synopsis = parseQaResponseContract(originalResponse).fields.issues?.trim();
1077
+ async function exhausted(packet, originalResponse, opts, prefix = "QA review requires an explicit recovery decision", qa, snapshot, liveSession = Boolean(qa), unavailableOperatorReason, requireFreshReviewReason) {
1078
+ const synopsis = describeLegacyQaTranscript(originalResponse).issues?.trim();
1290
1079
  let currentPrefix = prefix;
1291
1080
  if (packet.manifest.pendingAction !== "operator-menu") {
1292
1081
  packet = updateQaRecoveryPosition(packet, "operator-menu", packet.manifest.correctionTurns, "operator-menu");
@@ -1433,7 +1222,7 @@ function markQaRecoveryAction(opts, action) {
1433
1222
  }
1434
1223
  const qaEventRecorders = new WeakMap();
1435
1224
  function startQaEventRecorder(adapter) {
1436
- const events = [];
1225
+ const events = new BoundedTurnEvents(providerWireContract(qaVerdictOutput, adapter.agent).maximumResponseBytes);
1437
1226
  qaEventRecorders.set(events, { waits: new Map(), activePumps: 0 });
1438
1227
  attachQaEventRecorder(events, adapter);
1439
1228
  return events;
@@ -1496,14 +1285,14 @@ async function appendTurnObservation(packet, label, turn, requestedPrompt, error
1496
1285
  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 });
1497
1286
  const providerPrompt = turn.providerInstruction;
1498
1287
  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 });
1499
- next = appendQaRecoveryResource(next, `turns/${label}/raw-response.txt`, turn.rawResponse ?? turn.text, { purpose: `Raw provider QA response for ${label}`, exact: true });
1500
- next = appendQaRecoveryResource(next, `turns/${label}/cleaned-response.txt`, effectiveTurnText(turn), { purpose: `Cleaned QA response used for contract validation for ${label}`, exact: true });
1288
+ next = appendQaRecoveryResource(next, `turns/${label}/raw-response.json`, turn.transportEvidence ?? { unavailable: true }, { purpose: `Retained bounded transport references for ${label}` });
1289
+ next = appendQaRecoveryResource(next, `turns/${label}/cleaned-response.json`, turn.transportEvidence ?? { unavailable: true }, { purpose: `Retained bounded QA evidence for ${label}` });
1501
1290
  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}` });
1502
- return appendQaRecoveryResource(next, `turns/${label}/events.json`, events.length ? events : { unavailable: true }, { purpose: `Complete host-observed event set through ${label}` });
1291
+ return appendQaRecoveryResource(next, `turns/${label}/events.json`, events instanceof BoundedTurnEvents ? events.metadata() : { unavailable: true }, { purpose: `Complete host-observed event set through ${label}` });
1503
1292
  }
1504
1293
  async function performRecoveryAcknowledgement(packet, qa, prompt, events, continuityAlreadyValidated, label, seal) {
1505
- // Both turns acknowledge the same already-materialized revision. Observing
1506
- // either response advances the owner-only packet afterward, so validation
1294
+ // Acknowledge the already-materialized revision. Observing the response
1295
+ // advances the owner-only packet afterward, so validation
1507
1296
  // must continue to use this immutable target rather than the new revision.
1508
1297
  const target = packet;
1509
1298
  try {
@@ -1512,8 +1301,16 @@ async function performRecoveryAcknowledgement(packet, qa, prompt, events, contin
1512
1301
  catch (error) {
1513
1302
  return { packet, errors: [`recovery context mutated before acknowledgement: ${error instanceof Error ? error.message : String(error)}`] };
1514
1303
  }
1515
- const first = await sendDurableQaTurn(qa, prompt, events, `${label}:acknowledgement`);
1516
- let errors = validateQaRecoveryAcknowledgement(effectiveTurnText(first), target, { continuityAlreadyValidated });
1304
+ const first = await sendDurableQaTurn(qa, prompt, events, `${label}:acknowledgement`, qaRecoveryOutput);
1305
+ let errors = [];
1306
+ try {
1307
+ const ack = operationPayload(first, qaRecoveryOutput);
1308
+ if (ack.packet !== target.manifest.packetDigest || ack.reviewed_state !== target.manifest.reviewedStateDigest || ack.required_resources_read !== "all")
1309
+ errors.push("Recovery acceptance binding mismatch");
1310
+ }
1311
+ catch (error) {
1312
+ errors.push(String(error));
1313
+ }
1517
1314
  try {
1518
1315
  seal?.verify();
1519
1316
  }
@@ -1522,41 +1319,14 @@ async function performRecoveryAcknowledgement(packet, qa, prompt, events, contin
1522
1319
  }
1523
1320
  packet = await appendTurnObservation(packet, `${label}-acknowledgement`, first, prompt, errors, "packet-acknowledgement", events);
1524
1321
  if (first.isError || first.failure)
1525
- return { packet, errors: [...errors, first.continuityErrors?.length && !first.failure ? "QA acknowledgement continuity metadata was invalid" : "QA acknowledgement provider turn failed"],
1526
- retryable: Boolean(first.continuityErrors?.length && !first.failure && !errors.some(error => /mutated/i.test(error))) };
1322
+ return { packet, errors: [...errors, first.continuityErrors?.length && !first.failure ? "QA acknowledgement continuity metadata was invalid" : "QA acknowledgement provider turn failed"] };
1527
1323
  try {
1528
1324
  seal?.verify();
1529
1325
  }
1530
1326
  catch (error) {
1531
1327
  errors.push(`recovery context mutated after acknowledgement: ${error instanceof Error ? error.message : String(error)}`);
1532
1328
  }
1533
- if (errors.length) {
1534
- const correction = `${prompt}\n\nAcknowledgement correction only. Do not produce the report yet. Errors: ${errors.join("; ")}`;
1535
- try {
1536
- seal?.verify();
1537
- }
1538
- catch (error) {
1539
- return { packet, errors: [`recovery context mutated before acknowledgement repair: ${error instanceof Error ? error.message : String(error)}`] };
1540
- }
1541
- const repaired = await sendDurableQaTurn(qa, correction, events, `${label}:acknowledgement-repair`);
1542
- errors = validateQaRecoveryAcknowledgement(effectiveTurnText(repaired), target, { continuityAlreadyValidated });
1543
- try {
1544
- seal?.verify();
1545
- }
1546
- catch (error) {
1547
- errors.push(`recovery context mutated during acknowledgement repair: ${error instanceof Error ? error.message : String(error)}`);
1548
- }
1549
- packet = await appendTurnObservation(packet, `${label}-acknowledgement-repair`, repaired, correction, errors, "packet-acknowledgement-repair", events);
1550
- if (repaired.isError || repaired.failure)
1551
- errors.push(repaired.continuityErrors?.length && !repaired.failure ? "QA acknowledgement repair continuity metadata was invalid" : "QA acknowledgement repair provider turn failed");
1552
- try {
1553
- seal?.verify();
1554
- }
1555
- catch (error) {
1556
- errors.push(`recovery context mutated after acknowledgement repair: ${error instanceof Error ? error.message : String(error)}`);
1557
- }
1558
- }
1559
- return { packet, errors, retryable: !errors.some(error => /mutated|provider turn failed/i.test(error)) };
1329
+ return { packet, errors };
1560
1330
  }
1561
1331
  function validateBoundaryReceipt(receipt, packet, successorHandle) {
1562
1332
  const observedSuccessor = successorHandle.sessionIdentity();
@@ -1619,6 +1389,24 @@ export function assertQaRecoveryHandoffInventory(resources) {
1619
1389
  }
1620
1390
  function effectiveTurnText(turn) { return turn.cleanedResponse ?? turn.text; }
1621
1391
  const qaTurnBindings = new WeakMap();
1392
+ /** A readable evidence view, never a new native receipt or acceptance decision. */
1393
+ function retainedBuilderReport(db, digest) {
1394
+ const bytes = db.getEvidence(digest);
1395
+ if (!bytes)
1396
+ return { digest, availability: "unavailable" };
1397
+ try {
1398
+ const retained = JSON.parse(bytes.toString());
1399
+ const receipt = retained?.structured;
1400
+ if (receipt?.state === "candidate") {
1401
+ const value = typeof receipt.value === "string" ? JSON.parse(receipt.value) : receipt.value;
1402
+ return { digest, availability: "available", operation: receipt.operation, schemaDigest: receipt.schemaDigest, report: value?.result };
1403
+ }
1404
+ return { digest, availability: "historical", report: retained };
1405
+ }
1406
+ catch {
1407
+ return { digest, availability: "unreadable", reason: "Retained receipt JSON cannot be decoded; do not infer missing evidence from a summary." };
1408
+ }
1409
+ }
1622
1410
  function beginV2Review(opts, source, reviewedSnapshotPath, instruction, handle, attempt) {
1623
1411
  const db = new WorkflowDb(opts.recovery.projectDir);
1624
1412
  try {
@@ -1654,35 +1442,46 @@ function beginV2Review(opts, source, reviewedSnapshotPath, instruction, handle,
1654
1442
  const expected = contract.inputs.find(input => input.kind === "checklist");
1655
1443
  if (expected?.digest !== contractDigest("input-checklist", checklist))
1656
1444
  throw new Error("Final QA checklist differs from delivered contract");
1657
- const needsHistoricalEvidence = contract.checks.some(check => /red|test.?first|ordering|historical/i.test(JSON.stringify(check)));
1658
1445
  const builderClaims = db.operations(opts.recovery.runId).flatMap(operation => {
1659
1446
  const intent = operation.intent;
1660
1447
  const result = operation.result;
1661
- return operation.kind === "builder-coverage" && operation.status === "confirmed" && intent.workId === opts.ticket.id && intent.contractDigest === contract.contentDigest && result?.binding?.sourceDigest === source.digest && result.coverageDigest
1448
+ 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)
1662
1449
  ? [{
1663
1450
  operationId: operation.idempotencyKey,
1664
1451
  digest: result.coverageDigest,
1665
- coverage: preparationStore.artifact(result.coverageDigest, "builder-coverage"),
1666
- ...(needsHistoricalEvidence && result.implementationDigest ? {
1452
+ sourceDigest: result.binding.sourceDigest,
1453
+ currentSource: result.binding.sourceDigest === source.digest,
1454
+ ...(result.binding.sourceDigest === source.digest ? { coverage: preparationStore.artifact(result.coverageDigest, "builder-coverage") } : {}),
1455
+ ...(result.implementationDigest ? {
1667
1456
  // The response is already host-retained by finishBuildAssignment.
1668
- // Expose only this bounded, contract-relevant excerpt to the
1669
- // independent reviewer; do not retain or disclose all Builder
1670
- // output globally.
1671
- implementationResponse: {
1672
- digest: result.implementationDigest,
1673
- response: db.getEvidence(result.implementationDigest)?.toString() ?? "unavailable",
1674
- },
1457
+ // Supply this scoped original report through the bounded context
1458
+ // and its complete read-only resource. Historical reports never
1459
+ // substitute for current-source coverage.
1460
+ implementationResponse: retainedBuilderReport(db, result.implementationDigest),
1675
1461
  } : {}),
1676
1462
  }] : [];
1677
1463
  });
1678
- const commandObservations = retainedQaCommandObservations(opts.recovery.projectDir, opts.recovery.runId, opts.ticket.id, qaCommandOutputExcerptsAllowed(opts.ticket.id));
1464
+ const commandObservations = [
1465
+ ...retainedQaCommandObservations(opts.recovery.projectDir, opts.recovery.runId, opts.ticket.id, qaCommandOutputExcerptsAllowed(opts.ticket.id)),
1466
+ ...retainedHostBuilderObservations(db, opts.recovery.runId, opts.ticket.id),
1467
+ ];
1679
1468
  instruction += `\n${qaHostGraphFact(opts.recovery.projectDir)}\n`;
1680
1469
  instruction += finalReviewContractContext(contract, preparationStore, reviewedSnapshotPath, builderClaims, commandObservations);
1470
+ if (findHostQaCheck(contract)) {
1471
+ const comparison = inspectHostQaComparison({ db, projectDir: opts.recovery.projectDir, worktree: opts.builderWorktree, runId: opts.recovery.runId, ticket: opts.ticket, contract });
1472
+ if (comparison.finalSourceDigest !== source.digest)
1473
+ throw new Error("Host comparison source differs from the frozen QA source");
1474
+ const digest = preparationStore.putArtifact("host-qa-comparison-observation", comparison);
1475
+ const boundedPaths = (paths) => ({ paths: paths.slice(0, 20), omitted: Math.max(0, paths.length - 20) });
1476
+ 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`;
1477
+ }
1681
1478
  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 })}`;
1682
1479
  }
1480
+ instruction += `\n\n${operationInstruction(qaVerdictOutput)}`;
1683
1481
  const originalInstruction = instruction;
1684
1482
  const guidance = db.reserveGuidance(opts.recovery.runId, opts.ticket.id, "qa", attempt.attemptId, source.digest, instruction, false);
1685
- instruction = guidance.text;
1483
+ 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.`;
1484
+ instruction = guidance.text + managerVerificationInstruction;
1686
1485
  const basisWithoutDigest = {
1687
1486
  version: 2,
1688
1487
  ...(contract ? { contractBinding: { version: 1, transportVersion: 1, revision: contract.revision, digest: contract.contentDigest, admissionDigest: admission.requirementsDigest, commonConfigDigest: contract.inputs.find(input => input.kind === "rules").digest } } : {}),
@@ -1727,7 +1526,7 @@ function beginV2Review(opts, source, reviewedSnapshotPath, instruction, handle,
1727
1526
  head = db.commitQaReviewReady(sourceV2, basis, identity, handle.confinement, attempt, head.revision);
1728
1527
  if (recoveryPacket)
1729
1528
  recoveryPacket = updateQaRecoveryReviewIdentity(recoveryPacket, head.reviewNumber, attempt.attemptId, attempt.cycle);
1730
- 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 };
1529
+ 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 };
1731
1530
  }
1732
1531
  finally {
1733
1532
  db.close();
@@ -1774,18 +1573,20 @@ function bindQaRecoveryContext(adapter, context) {
1774
1573
  throw new Error("QA recovery context requires a durable review binding");
1775
1574
  binding.recoveryContext = context;
1776
1575
  }
1777
- async function sendDurableQaTurn(adapter, instruction, events, slot) {
1576
+ async function sendDurableQaTurn(adapter, instruction, events, slot, output = qaVerdictOutput) {
1778
1577
  const binding = qaTurnBindings.get(adapter);
1779
1578
  if (!binding)
1780
1579
  throw new Error("QA provider dispatch requires a durable source/review-basis binding");
1781
1580
  // Repairs and recovery acknowledgements bypass graph navigation, but their
1782
1581
  // provider history and journal artifacts retain the original access grants.
1783
- return dispatchWithGraphAccess(binding.projectDir, adapter, instruction, undefined, () => sendDurableQaTurnOwned(adapter, instruction, events, slot));
1582
+ return dispatchWithGraphAccess(binding.projectDir, adapter, instruction, undefined, () => sendDurableQaTurnOwned(adapter, instruction, events, slot, output));
1784
1583
  }
1785
- async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
1584
+ async function sendDurableQaTurnOwned(adapter, instruction, events, slot, output = qaVerdictOutput) {
1786
1585
  const binding = qaTurnBindings.get(adapter);
1787
1586
  if (!binding)
1788
1587
  throw new Error("QA provider dispatch requires a durable source/review-basis binding");
1588
+ if (slot !== "initial")
1589
+ instruction += `\n\n${operationInstruction(output)}`;
1789
1590
  if (slot !== "session-initialization") {
1790
1591
  try {
1791
1592
  await adapter.prepareAutoCompaction?.();
@@ -1801,7 +1602,7 @@ async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
1801
1602
  "Rafi QA session initialization only.",
1802
1603
  "Do not inspect source, call tools, make findings, or modify files.",
1803
1604
  "Reply briefly that the context is ready, followed by the required continuity record.",
1804
- ].join("\n"), events, "session-initialization");
1605
+ ].join("\n"), events, "session-initialization", initializationOutput);
1805
1606
  const setupCalledTools = events.slice(eventOffset).some((event) => event.kind === "tool");
1806
1607
  if (initialized.isError || setupCalledTools || adapter.requiresAutoCompactionSetupTurn?.()) {
1807
1608
  return { ...initialized, isError: true,
@@ -1821,13 +1622,13 @@ async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
1821
1622
  db.atomic(() => {
1822
1623
  if (slot === "initial") {
1823
1624
  const guidance = db.reserveGuidance(binding.runId, binding.ticketId, "qa", operationId, binding.sourceStateDigest, binding.originalInstruction);
1824
- if (guidance.text + (binding.inputBasisEnvelope ?? "") !== binding.reviewInstruction)
1625
+ if (guidance.text + (binding.managerVerificationInstruction ?? "") + (binding.inputBasisEnvelope ?? "") !== binding.reviewInstruction)
1825
1626
  throw new BuildControlBoundary("Manager guidance changed during QA preparation; restart a full review with the current instruction basis");
1826
1627
  binding.guidanceIds = guidance.ids;
1827
1628
  instruction = binding.reviewInstruction;
1828
1629
  }
1829
1630
  const instructionDigest = db.putEvidence("qa", Buffer.from(instruction));
1830
- 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 };
1631
+ 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 } };
1831
1632
  const head = db.qaTicketHead(binding.runId, binding.ticketId);
1832
1633
  db.commitQaTurnIntent(intent, head.revision);
1833
1634
  });
@@ -1840,18 +1641,18 @@ async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
1840
1641
  // Reading the sealed QA packet is ordinary confined read-only QA work.
1841
1642
  // Only formatting repair and ownership acceptance are response-only; the
1842
1643
  // first resource-inspection turn cannot truthfully read with tools disabled.
1843
- const responseRepair = slot?.includes("correction") || (slot?.includes("acknowledgement") && slot.includes("repair"));
1844
1644
  try {
1845
- result = await adapter.sendTurn(instruction, slot === "session-initialization" ? { purpose: "initialization", responseOnly: true } : responseRepair ? { purpose: "response-repair", responseOnly: true } : undefined);
1645
+ 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));
1846
1646
  }
1847
1647
  catch (error) {
1848
1648
  const failedDb = new WorkflowDb(binding.projectDir);
1849
1649
  try {
1850
1650
  const receipt = {
1851
1651
  version: 2, operationId, dispatch: "uncertain", terminalEventObserved: false,
1852
- eventStreamDigest: failedDb.putEvidence("qa", Buffer.from(canonicalJson(events))),
1652
+ eventStreamDigest: failedDb.putEvidence("qa", Buffer.from(canonicalJson(events instanceof BoundedTurnEvents ? events.metadata() : []))),
1853
1653
  sourceStateDigest: binding.sourceStateDigest, reviewBasisDigest: binding.reviewBasisDigest, completedAt: new Date().toISOString(),
1854
1654
  };
1655
+ failedDb.recordCommunicationEvidenceRefs(operationId, [receipt.rawResponseDigest, receipt.cleanedResponseDigest, receipt.eventStreamDigest].filter((digest) => Boolean(digest)));
1855
1656
  failedDb.finishQaTurn(receipt);
1856
1657
  const head = failedDb.qaTicketHead(binding.runId, binding.ticketId);
1857
1658
  if (head.state === "turn-intended")
@@ -1860,13 +1661,15 @@ async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
1860
1661
  finally {
1861
1662
  failedDb.close();
1862
1663
  }
1863
- ensureUncertainQaTurnPacket(binding, instruction, error instanceof Error ? error.message : String(error));
1664
+ ensureUncertainQaTurnPacket(binding, instruction, boundedUtf8(error instanceof Error ? error.message : String(error), 4096).toString());
1864
1665
  throw error;
1865
1666
  }
1866
1667
  await awaitQaTurnEvent(events, result);
1867
1668
  const activeSession = adapter.sessionRef?.();
1868
1669
  const reportedSession = result.providerMetadata?.sessionRef;
1869
- const identityMismatch = adapter.agent !== session.provider
1670
+ const expectedWire = providerWireContract(output, session.provider);
1671
+ 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))
1672
+ || adapter.agent !== session.provider
1870
1673
  || adapter.sessionId() !== session.sessionId
1871
1674
  || !activeSession
1872
1675
  || stableProviderSessionIdentity(activeSession) !== stableProviderSessionIdentity(session)
@@ -1881,12 +1684,17 @@ async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
1881
1684
  const receipt = {
1882
1685
  version: 2, operationId, dispatch: "uncertain", terminalEventObserved: false,
1883
1686
  providerTurnId: result.turnId,
1884
- rawResponseDigest: failedDb.putEvidence("qa", Buffer.from(result.rawResponse ?? result.text)),
1885
- cleanedResponseDigest: failedDb.putEvidence("qa", Buffer.from(result.cleanedResponse ?? result.text)),
1687
+ rawResponseDigest: failedDb.operation(operationId)?.result?.rawDigest,
1688
+ sealedDispatch: failedDb.structuredOwnerDispatch(operationId),
1689
+ structuredResponseDigest: failedDb.operation(operationId)?.result?.responseDigest,
1690
+ cleanedResponseDigest: failedDb.putEvidence("qa", boundedUtf8(result.cleanedResponse ?? result.text, expectedWire.maximumResponseBytes)),
1691
+ retainedEvidence: { rawOriginalBytes: result.transportEvidence?.rawBytes, rawRetainedBytes: result.transportEvidence?.retainedRawBytes,
1692
+ cleanedOriginalBytes: Buffer.byteLength(result.cleanedResponse ?? result.text), cleanedRetainedBytes: boundedUtf8(result.cleanedResponse ?? result.text, expectedWire.maximumResponseBytes).length },
1886
1693
  providerInstructionDigest: failedDb.putEvidence("qa", Buffer.from(result.providerInstruction ?? result.hostInstruction ?? instruction)),
1887
- eventStreamDigest: failedDb.putEvidence("qa", Buffer.from(canonicalJson({ events, rejectedProviderMetadata: result.providerMetadata, activeSession }))),
1694
+ 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() }))),
1888
1695
  sourceStateDigest: binding.sourceStateDigest, reviewBasisDigest: binding.reviewBasisDigest, completedAt: new Date().toISOString(),
1889
1696
  };
1697
+ failedDb.recordCommunicationEvidenceRefs(operationId, [receipt.rawResponseDigest, receipt.cleanedResponseDigest, receipt.eventStreamDigest].filter((digest) => Boolean(digest)));
1890
1698
  failedDb.finishQaTurn(receipt);
1891
1699
  const head = failedDb.qaTicketHead(binding.runId, binding.ticketId);
1892
1700
  if (head.state === "turn-intended")
@@ -1899,21 +1707,28 @@ async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
1899
1707
  throw new Error(detail);
1900
1708
  }
1901
1709
  const completedAt = new Date().toISOString();
1902
- const terminalEventObserved = Boolean(result.turnId && result.failure?.dispatchState !== "unknown"
1903
- && events.some((event) => event.kind === "turn-complete" && (event.turnId ?? event.result.turnId) === result.turnId));
1710
+ const terminalEventObserved = Boolean(!(events instanceof BoundedTurnEvents && events.terminalOverflow) && result.turnId && result.failure?.dispatchState !== "unknown"
1711
+ && events.filter(event => event.kind === "turn-complete" && (event.turnId ?? event.result.turnId) === result.turnId).length === 1
1712
+ && events.some(event => event.kind === "turn-complete" && (event.turnId ?? event.result.turnId) === result.turnId &&
1713
+ receiptFingerprint(event.result) === receiptFingerprint(result)));
1904
1714
  const receiptDb = new WorkflowDb(binding.projectDir);
1905
1715
  try {
1906
- const rawResponseDigest = receiptDb.putEvidence("qa", Buffer.from(result.rawResponse ?? result.text));
1716
+ const rawResponseDigest = (receiptDb.operation(operationId)?.result).rawDigest;
1717
+ const structuredResponseDigest = receiptDb.operation(operationId)?.result?.responseDigest;
1907
1718
  const providerInstruction = result.providerInstruction ?? result.hostInstruction ?? instruction;
1908
1719
  const providerInstructionDigest = receiptDb.putEvidence("qa", Buffer.from(providerInstruction));
1909
- const cleanedResponseDigest = receiptDb.putEvidence("qa", Buffer.from(result.cleanedResponse ?? result.text));
1910
- const eventStreamDigest = receiptDb.putEvidence("qa", Buffer.from(canonicalJson(events)));
1720
+ const cleanedResponseDigest = receiptDb.putEvidence("qa", boundedUtf8(result.cleanedResponse ?? result.text, expectedWire.maximumResponseBytes));
1721
+ const eventStreamDigest = receiptDb.putEvidence("qa", Buffer.from(canonicalJson(events instanceof BoundedTurnEvents ? events.metadata() : [])));
1911
1722
  const receipt = {
1912
- version: 2, operationId, dispatch: terminalEventObserved ? "completed" : "uncertain", providerTurnId: result.turnId,
1913
- providerInstructionDigest, rawResponseDigest, cleanedResponseDigest, eventStreamDigest,
1723
+ version: 2, operationId, dispatch: terminalEventObserved && !result.isError && !result.failure ? "completed" : "uncertain", providerTurnId: result.turnId,
1724
+ sealedDispatch: receiptDb.structuredOwnerDispatch(operationId),
1725
+ providerInstructionDigest, rawResponseDigest, cleanedResponseDigest, eventStreamDigest, structuredResponseDigest,
1726
+ retainedEvidence: { rawOriginalBytes: result.transportEvidence?.rawBytes, rawRetainedBytes: result.transportEvidence?.retainedRawBytes,
1727
+ cleanedOriginalBytes: Buffer.byteLength(result.cleanedResponse ?? result.text), cleanedRetainedBytes: boundedUtf8(result.cleanedResponse ?? result.text, expectedWire.maximumResponseBytes).length },
1914
1728
  terminalEventObserved, sourceStateDigest: binding.sourceStateDigest, reviewBasisDigest: binding.reviewBasisDigest, completedAt,
1915
1729
  };
1916
1730
  receiptDb.atomic(() => {
1731
+ receiptDb.recordCommunicationEvidenceRefs(operationId, [rawResponseDigest, cleanedResponseDigest, eventStreamDigest]);
1917
1732
  receiptDb.finishQaTurn(receipt);
1918
1733
  const metricAdmission = receiptDb.admittedWork(binding.runId, binding.ticketId);
1919
1734
  if (metricAdmission)
@@ -1924,7 +1739,7 @@ async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
1924
1739
  binding.lastReceiptDigest = qaDigest("turn-receipt", receipt);
1925
1740
  let graphRequest = false;
1926
1741
  try {
1927
- graphRequest = Boolean(parseGraphRequest(result.cleanedResponse ?? result.text));
1742
+ graphRequest = operationResponse(result, output).result.kind === "graph-request";
1928
1743
  }
1929
1744
  catch {
1930
1745
  graphRequest = true;
@@ -1932,6 +1747,7 @@ async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
1932
1747
  if (!slot?.startsWith("session-") && !graphRequest)
1933
1748
  binding.lastBusinessReceiptDigest = binding.lastReceiptDigest;
1934
1749
  binding.lastResponse = result.text;
1750
+ binding.lastTurn = result;
1935
1751
  if (!terminalEventObserved) {
1936
1752
  const head = receiptDb.qaTicketHead(binding.runId, binding.ticketId);
1937
1753
  receiptDb.transitionQa(binding.runId, binding.ticketId, head.revision, { type: "turn-uncertain" });
@@ -2006,13 +1822,23 @@ function finishV2Pass(opts, adapter, detail) {
2006
1822
  throw new Error("Final coverage requires the current ready contract");
2007
1823
  const contract = db.qaPreparationStore().contract(contractHead.digest);
2008
1824
  const coverageBinding = { phase: "qa", sourceDigest: binding.sourceStateDigest, inputBasisDigest: binding.reviewBasisDigest, attemptId: binding.reviewAttemptId, sessionId: binding.sessionRef.sessionId };
2009
- const providerCoverage = bindCoverageHostFields(parseContractCoverage(binding.lastResponse ?? ""), contract, coverageBinding);
1825
+ if (!binding.lastTurn)
1826
+ throw new Error("QA coverage requires the retained structured verdict");
1827
+ const claims = operationPayload(binding.lastTurn, qaVerdictOutput).coverage;
1828
+ if (!claims)
1829
+ throw new Error("QA verdict omitted required coverage");
1830
+ const providerCoverage = bindCoverageHostFields(claims, contract, coverageBinding);
2010
1831
  const hostComparison = findHostQaCheck(contract)
2011
1832
  ? createHostQaComparison({ db, projectDir: binding.projectDir, worktree: opts.builderWorktree, runId: binding.runId, ticket: opts.ticket, contract, reviewAttemptId: binding.reviewAttemptId, reviewBasisDigest: binding.reviewBasisDigest })
2012
1833
  : undefined;
2013
- const coverage = hostComparison
2014
- ? applyHostQaComparison(providerCoverage, contract, hostComparison.result, hostComparison.resultDigest, binding.projectDir)
2015
- : providerCoverage;
1834
+ const hostGraph = findGraphQaChecks(contract).length
1835
+ ? createHostGraphApplicability({ projectDir: binding.projectDir, runId: binding.runId, ticketId: binding.ticketId, contract, reviewAttemptId: binding.reviewAttemptId, reviewBasisDigest: binding.reviewBasisDigest, sourceDigest: binding.sourceStateDigest, store: db.qaPreparationStore() })
1836
+ : undefined;
1837
+ const commandObservations = [
1838
+ ...retainedQaCommandObservations(opts.recovery.projectDir, opts.recovery.runId, opts.ticket.id, false),
1839
+ ...retainedHostBuilderObservations(db, opts.recovery.runId, opts.ticket.id),
1840
+ ];
1841
+ const coverage = assembleFinalQaCoverage({ providerCoverage, contract, projectDir: binding.projectDir, hostComparison, hostGraph, commandObservations });
2016
1842
  const errors = validateContractCoverage(contract, coverage, coverageBinding);
2017
1843
  if (errors.length)
2018
1844
  throw new Error(`Final QA coverage incomplete: ${errors.join("; ")}`);
@@ -2020,8 +1846,14 @@ function finishV2Pass(opts, adapter, detail) {
2020
1846
  contractCoverage = { version: 1, contractDigest: contract.contentDigest, revision: contract.revision, coverageDigest, attemptId: binding.reviewAttemptId, sessionId: binding.sessionRef.sessionId };
2021
1847
  }
2022
1848
  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);
2023
- db.verifyGuidance(binding.guidanceIds, certificate, binding.lastResponse ?? "");
2024
- db.verifyBuilderGuidance(binding.verificationIds, certificate, binding.lastResponse ?? "", binding.lastBusinessReceiptDigest);
1849
+ if (!binding.lastTurn)
1850
+ throw new Error("QA guidance verification requires the native turn receipt");
1851
+ const verifiedInstructionIds = operationPayload(binding.lastTurn, qaVerdictOutput).verifiedInstructionIds;
1852
+ const allowedInstructionIds = new Set([...binding.guidanceIds, ...binding.verificationIds]);
1853
+ if (verifiedInstructionIds.some(id => !allowedInstructionIds.has(id)))
1854
+ throw new Error("QA claimed verification of a foreign instruction");
1855
+ db.verifyGuidance(binding.guidanceIds, certificate, verifiedInstructionIds);
1856
+ db.verifyBuilderGuidance(binding.verificationIds, certificate, verifiedInstructionIds, binding.lastBusinessReceiptDigest);
2025
1857
  return { certificateId: certificate.certificateId, sourceStateDigest: binding.sourceStateDigest, reviewBasisDigest: binding.reviewBasisDigest };
2026
1858
  });
2027
1859
  }
@@ -2093,6 +1925,7 @@ export async function verifyPendingQaFinalizationSource(projectDir, sourceWorktr
2093
1925
  const controlDb = new WorkflowDb(projectDir);
2094
1926
  try {
2095
1927
  controlDb.assertFinalizationControls(runId, ticketId);
1928
+ controlDb.assertQaFinalizationAuthority(runId, ticketId);
2096
1929
  }
2097
1930
  finally {
2098
1931
  controlDb.close();
@@ -2156,7 +1989,7 @@ function turnMetadata(turn) {
2156
1989
  inputTokens: turn.inputTokens ?? "unavailable",
2157
1990
  outputTokens: turn.outputTokens ?? "unavailable",
2158
1991
  usage: turn.usage ?? { unavailable: true },
2159
- failure: turn.failure ?? null,
1992
+ failure: turn.failure ? { ...turn.failure, diagnostics: boundedUtf8(turn.failure.diagnostics, 4096).toString() } : null,
2160
1993
  provider: turn.providerMetadata ?? { unavailable: true },
2161
1994
  };
2162
1995
  }
@@ -2172,6 +2005,11 @@ function qaContinuityRecoveryContext(opts) {
2172
2005
  recoveryAttempts: db.recoveryAttempts(opts.recovery.runId).filter((attempt) => attempt.ticket === opts.ticket.id),
2173
2006
  };
2174
2007
  }
2008
+ catch (error) {
2009
+ if (/revoked graph-derived evidence|Graph access changed/.test(String(error)))
2010
+ return { unavailable: "graph-access-revoked", detail: "Historical graph-derived continuity is withheld; explicit source-based recovery is required" };
2011
+ throw error;
2012
+ }
2175
2013
  finally {
2176
2014
  db.close();
2177
2015
  }