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