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.
- package/README.md +1 -1
- package/dist/adapters/claude.d.ts +23 -12
- package/dist/adapters/claude.js +228 -24
- package/dist/adapters/codex.d.ts +17 -7
- package/dist/adapters/codex.js +195 -17
- package/dist/adapters/recovering.d.ts +2 -0
- package/dist/adapters/recovering.js +5 -1
- package/dist/adapters/structured.d.ts +71 -0
- package/dist/adapters/structured.js +213 -0
- package/dist/adapters/types.d.ts +86 -9
- package/dist/agentRun.d.ts +2 -0
- package/dist/agentRun.js +2 -1
- package/dist/boundedTurnEvidence.d.ts +18 -0
- package/dist/boundedTurnEvidence.js +76 -0
- package/dist/branch/currentGuard.d.ts +2 -0
- package/dist/branch/currentGuard.js +2 -0
- package/dist/branch/planner.js +1 -4
- package/dist/branch/runner.d.ts +2 -1
- package/dist/branch/runner.js +10 -7
- package/dist/buildAdmission.js +4 -0
- package/dist/buildApproval.d.ts +30 -0
- package/dist/buildApproval.js +68 -0
- package/dist/buildAssignment.d.ts +1 -0
- package/dist/buildAssignment.js +82 -23
- package/dist/buildInterventions.d.ts +2 -2
- package/dist/buildInterventions.js +11 -4
- package/dist/buildLaunchGate.js +3 -2
- package/dist/builderGuidanceFollowup.js +17 -6
- package/dist/cli/manager.js +22 -20
- package/dist/cli/start.js +32 -24
- package/dist/cli/tickets.js +40 -24
- package/dist/continuity.d.ts +10 -14
- package/dist/continuity.js +198 -179
- package/dist/continuityOrigin.d.ts +13 -0
- package/dist/continuityOrigin.js +91 -0
- package/dist/foreman.d.ts +20 -5
- package/dist/foreman.js +57 -187
- package/dist/graph/boundary.d.ts +1 -1
- package/dist/graph/boundary.js +1 -1
- package/dist/graph/host.js +8 -19
- package/dist/graph/manager.d.ts +1 -1
- package/dist/graph/manager.js +1 -1
- package/dist/graph/preparation.d.ts +38 -0
- package/dist/graph/preparation.js +7 -6
- package/dist/graph/semantic.d.ts +1 -0
- package/dist/graph/semantic.js +4 -1
- package/dist/graph/session.js +7 -2
- package/dist/graph/turn.d.ts +2 -3
- package/dist/graph/turn.js +28 -40
- package/dist/handoffProtocol.js +2 -2
- package/dist/handoffs.d.ts +3 -1
- package/dist/handoffs.js +90 -59
- package/dist/hostGraphApplicability.d.ts +46 -0
- package/dist/hostGraphApplicability.js +97 -0
- package/dist/hostQaAugmentation.d.ts +18 -0
- package/dist/hostQaAugmentation.js +81 -0
- package/dist/hostQaComparison.d.ts +21 -2
- package/dist/hostQaComparison.js +43 -28
- package/dist/hostSourcePreservation.d.ts +38 -0
- package/dist/hostSourcePreservation.js +72 -0
- package/dist/interviews.d.ts +7 -0
- package/dist/interviews.js +18 -1
- package/dist/legacyCommunicationDisplay.d.ts +2 -0
- package/dist/legacyCommunicationDisplay.js +2 -0
- package/dist/managerPacket.js +3 -3
- package/dist/markers.d.ts +4 -3
- package/dist/markers.js +4 -20
- package/dist/probeIpc.d.ts +14 -0
- package/dist/probeIpc.js +29 -0
- package/dist/qaBuildGate.d.ts +21 -0
- package/dist/qaBuildGate.js +48 -12
- package/dist/qaBuilderCoverage.js +22 -13
- package/dist/qaBuilderEvidence.d.ts +16 -1
- package/dist/qaBuilderEvidence.js +48 -8
- package/dist/qaContractCoverage.d.ts +2 -2
- package/dist/qaContractCoverage.js +11 -7
- package/dist/qaContractDelivery.js +34 -20
- package/dist/qaCoverageAssembly.d.ts +22 -0
- package/dist/qaCoverageAssembly.js +16 -0
- package/dist/qaDeliveryJournal.d.ts +2 -0
- package/dist/qaDeliveryJournal.js +19 -0
- package/dist/qaEffectiveConfig.d.ts +1 -0
- package/dist/qaEffectiveConfig.js +6 -1
- package/dist/qaFailureDelivery.js +97 -85
- package/dist/qaHostTransformation.d.ts +26 -0
- package/dist/qaHostTransformation.js +100 -0
- package/dist/qaPreparation.d.ts +8 -20
- package/dist/qaPreparation.js +133 -229
- package/dist/qaPreparationInputs.js +19 -0
- package/dist/qaPreparationStore.js +34 -6
- package/dist/qaPrerequisites.d.ts +2 -1
- package/dist/qaPrerequisites.js +21 -1
- package/dist/qaProtocolV2.d.ts +15 -0
- package/dist/qaRecovery.js +21 -4
- package/dist/qaReview.js +232 -394
- package/dist/qaReviewEvidence.d.ts +7 -3
- package/dist/qaReviewEvidence.js +15 -4
- package/dist/qaRuntime.js +1 -1
- package/dist/qaSnapshot.js +71 -21
- package/dist/qaStructuredResponse.d.ts +10 -0
- package/dist/qaStructuredResponse.js +57 -0
- package/dist/qaTimeline.js +5 -3
- package/dist/qaToolRecovery.d.ts +12 -0
- package/dist/qaToolRecovery.js +44 -0
- package/dist/qaVerificationContract.d.ts +3 -2
- package/dist/qaVerificationContract.js +58 -34
- package/dist/readinessGate.js +5 -4
- package/dist/runtimeReadiness.js +23 -15
- package/dist/statusReporter.d.ts +2 -0
- package/dist/statusReporter.js +2 -0
- package/dist/structuredCutover.d.ts +31 -0
- package/dist/structuredCutover.js +510 -0
- package/dist/structuredCutoverFiles.d.ts +10 -0
- package/dist/structuredCutoverFiles.js +181 -0
- package/dist/structuredDispatchAuthority.d.ts +22 -0
- package/dist/structuredDispatchAuthority.js +110 -0
- package/dist/structuredDispatchRecord.d.ts +29 -0
- package/dist/structuredDispatchRecord.js +36 -0
- package/dist/structuredLiveBudget.d.ts +2 -0
- package/dist/structuredLiveBudget.js +23 -0
- package/dist/structuredOperation.d.ts +18 -0
- package/dist/structuredOperation.js +257 -0
- package/dist/supervisedStart.d.ts +1 -1
- package/dist/supervisedStart.js +78 -31
- package/dist/supervisor.d.ts +18 -2
- package/dist/supervisor.js +74 -17
- package/dist/ticketPlanning.d.ts +1 -373
- package/dist/ticketPlanning.js +2 -26
- package/dist/ticketPopulation.d.ts +0 -3
- package/dist/ticketPopulation.js +4 -21
- package/dist/ticketPopulationPageAuthority.d.ts +13 -0
- package/dist/ticketPopulationPageAuthority.js +30 -0
- package/dist/ticketPopulationPaging.d.ts +27 -0
- package/dist/ticketPopulationPaging.js +105 -0
- package/dist/tickets/renderMarkdown.d.ts +14 -0
- package/dist/tickets/renderMarkdown.js +39 -5
- package/dist/tickets/ticketLoader.js +10 -0
- package/dist/tickets/ticketSchema.d.ts +107 -0
- package/dist/tickets/ticketSchema.js +4 -1
- package/dist/workflowDb.d.ts +100 -2
- package/dist/workflowDb.js +781 -35
- package/dist/workflowReader.d.ts +1 -0
- package/dist/workflowReader.js +9 -0
- 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,
|
|
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,
|
|
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,
|
|
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,
|
|
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 {
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
645
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
|
775
|
-
|
|
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
|
|
801
|
-
|
|
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 (!
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
906
|
-
|
|
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
|
-
|
|
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),
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
if (
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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 =
|
|
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
|
|
982
|
-
if (
|
|
983
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
1207
|
-
"original-cleaned-response": { value:
|
|
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
|
|
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
|
-
|
|
1228
|
-
|
|
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
|
|
1289
|
-
const synopsis =
|
|
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.
|
|
1500
|
-
next = appendQaRecoveryResource(next, `turns/${label}/cleaned-response.
|
|
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
|
|
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
|
-
//
|
|
1506
|
-
//
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1666
|
-
|
|
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
|
-
//
|
|
1669
|
-
//
|
|
1670
|
-
//
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
1885
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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",
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
2014
|
-
?
|
|
2015
|
-
:
|
|
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
|
-
|
|
2024
|
-
|
|
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
|
|
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
|
}
|