ai-foreman 1.7.19 → 1.7.21
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 +29 -5
- package/dist/activity.d.ts +3 -0
- package/dist/activity.js +18 -6
- package/dist/adapters/claude.d.ts +16 -0
- package/dist/adapters/claude.js +28 -3
- package/dist/adapters/codex.d.ts +20 -0
- package/dist/adapters/codex.js +99 -5
- package/dist/adapters/recovering.d.ts +9 -0
- package/dist/adapters/recovering.js +10 -0
- package/dist/adapters/types.d.ts +15 -0
- package/dist/agentRun.d.ts +7 -0
- package/dist/agentRun.js +24 -13
- package/dist/branch/currentGuard.d.ts +1 -0
- package/dist/branch/currentGuard.js +1 -0
- package/dist/branch/finalization.js +6 -1
- package/dist/branch/git.d.ts +1 -0
- package/dist/branch/git.js +15 -1
- package/dist/branch/runner.d.ts +2 -0
- package/dist/branch/runner.js +92 -5
- package/dist/buildAdmission.d.ts +61 -0
- package/dist/buildAdmission.js +286 -0
- package/dist/buildApproval.d.ts +2 -0
- package/dist/buildApproval.js +17 -0
- package/dist/buildAssignment.d.ts +33 -0
- package/dist/buildAssignment.js +215 -0
- package/dist/buildInterventions.d.ts +90 -0
- package/dist/buildInterventions.js +334 -0
- package/dist/buildLaunchGate.d.ts +1 -0
- package/dist/buildLaunchGate.js +36 -0
- package/dist/buildOwnershipReconciliation.d.ts +19 -0
- package/dist/buildOwnershipReconciliation.js +41 -0
- package/dist/buildOwnershipRepair.d.ts +33 -0
- package/dist/buildOwnershipRepair.js +111 -0
- package/dist/buildRuns.d.ts +24 -1
- package/dist/buildRuns.js +216 -70
- package/dist/buildSyntheticWork.d.ts +2 -0
- package/dist/buildSyntheticWork.js +30 -0
- package/dist/buildWorkAdmission.d.ts +17 -0
- package/dist/buildWorkAdmission.js +57 -0
- package/dist/buildWorkAuthorization.d.ts +2 -0
- package/dist/buildWorkAuthorization.js +23 -0
- package/dist/buildWorkContext.d.ts +3 -0
- package/dist/buildWorkContext.js +6 -0
- package/dist/buildWorkMigration.d.ts +9 -0
- package/dist/buildWorkMigration.js +206 -0
- package/dist/buildWorkReconciliation.d.ts +12 -0
- package/dist/buildWorkReconciliation.js +70 -0
- package/dist/builderGuidanceFollowup.d.ts +15 -0
- package/dist/builderGuidanceFollowup.js +34 -0
- package/dist/cli/doctor.js +3 -2
- package/dist/cli/graph.d.ts +2 -0
- package/dist/cli/graph.js +75 -0
- package/dist/cli/manager.d.ts +8 -0
- package/dist/cli/manager.js +234 -12
- package/dist/cli/recovery.js +3 -2
- package/dist/cli/runtimeAuthPrompt.d.ts +5 -0
- package/dist/cli/runtimeAuthPrompt.js +41 -7
- package/dist/cli/start.d.ts +8 -0
- package/dist/cli/start.js +2091 -1849
- package/dist/cli/status.js +20 -5
- package/dist/cli/tickets.js +9 -7
- package/dist/continuity.d.ts +16 -1
- package/dist/continuity.js +68 -12
- package/dist/foreman.d.ts +12 -4
- package/dist/foreman.js +397 -60
- package/dist/graph/boundary.d.ts +15 -0
- package/dist/graph/boundary.js +50 -0
- package/dist/graph/bridge.d.ts +9 -0
- package/dist/graph/bridge.js +146 -0
- package/dist/graph/bridge.py +205 -0
- package/dist/graph/config.d.ts +11 -0
- package/dist/graph/config.js +40 -0
- package/dist/graph/corpus.d.ts +16 -0
- package/dist/graph/corpus.js +188 -0
- package/dist/graph/derived.d.ts +17 -0
- package/dist/graph/derived.js +73 -0
- package/dist/graph/host.d.ts +4 -0
- package/dist/graph/host.js +160 -0
- package/dist/graph/install.d.ts +3 -0
- package/dist/graph/install.js +50 -0
- package/dist/graph/lifecycle.d.ts +11 -0
- package/dist/graph/lifecycle.js +124 -0
- package/dist/graph/maintenance.d.ts +43 -0
- package/dist/graph/maintenance.js +301 -0
- package/dist/graph/manager.d.ts +9 -0
- package/dist/graph/manager.js +92 -0
- package/dist/graph/preparation.d.ts +41 -0
- package/dist/graph/preparation.js +43 -0
- package/dist/graph/read.d.ts +27 -0
- package/dist/graph/read.js +152 -0
- package/dist/graph/semantic.d.ts +17 -0
- package/dist/graph/semantic.js +27 -0
- package/dist/graph/session.d.ts +8 -0
- package/dist/graph/session.js +92 -0
- package/dist/graph/sources.d.ts +4 -0
- package/dist/graph/sources.js +11 -0
- package/dist/graph/storage.d.ts +38 -0
- package/dist/graph/storage.js +159 -0
- package/dist/graph/turn.d.ts +17 -0
- package/dist/graph/turn.js +360 -0
- package/dist/graph/util.d.ts +5 -0
- package/dist/graph/util.js +51 -0
- package/dist/handoffs.d.ts +1 -0
- package/dist/handoffs.js +5 -1
- package/dist/humanDecision.d.ts +21 -2
- package/dist/humanDecision.js +169 -10
- package/dist/index.js +4 -1
- package/dist/installOwnership.d.ts +46 -0
- package/dist/installOwnership.js +440 -0
- package/dist/linuxProbeInventory.d.ts +12 -0
- package/dist/linuxProbeInventory.js +48 -0
- package/dist/log.d.ts +1 -1
- package/dist/log.js +7 -1
- package/dist/managerEvidence.d.ts +25 -0
- package/dist/managerEvidence.js +326 -0
- package/dist/managerEvidenceArtifacts.d.ts +38 -0
- package/dist/managerEvidenceArtifacts.js +118 -0
- package/dist/managerPacket.d.ts +2 -0
- package/dist/managerPacket.js +23 -0
- package/dist/markers.d.ts +1 -1
- package/dist/markers.js +2 -2
- package/dist/observability.js +1 -1
- package/dist/processIdentity.d.ts +21 -1
- package/dist/processIdentity.js +87 -5
- package/dist/providerPhase.d.ts +13 -0
- package/dist/providerPhase.js +26 -0
- package/dist/qaAuthorizedDisposition.d.ts +24 -0
- package/dist/qaAuthorizedDisposition.js +26 -0
- package/dist/qaBuildGate.d.ts +4 -0
- package/dist/qaBuildGate.js +216 -0
- package/dist/qaBuilderCoverage.d.ts +5 -0
- package/dist/qaBuilderCoverage.js +78 -0
- package/dist/qaContractCoverage.d.ts +10 -0
- package/dist/qaContractCoverage.js +55 -0
- package/dist/qaContractDelivery.d.ts +23 -0
- package/dist/qaContractDelivery.js +145 -0
- package/dist/qaContractFreshness.d.ts +15 -0
- package/dist/qaContractFreshness.js +36 -0
- package/dist/qaDeliveryJournal.d.ts +10 -0
- package/dist/qaDeliveryJournal.js +16 -1
- package/dist/qaEffectiveConfig.d.ts +20 -0
- package/dist/qaEffectiveConfig.js +40 -0
- package/dist/qaEvidenceReader.d.ts +14 -0
- package/dist/qaEvidenceReader.js +176 -0
- package/dist/qaFailureDelivery.d.ts +2 -0
- package/dist/qaFailureDelivery.js +107 -10
- package/dist/qaHandbackMigration.d.ts +1 -1
- package/dist/qaHandbackMigration.js +3 -3
- package/dist/qaPreparation.d.ts +26 -0
- package/dist/qaPreparation.js +150 -0
- package/dist/qaPreparationInputs.d.ts +8 -0
- package/dist/qaPreparationInputs.js +47 -0
- package/dist/qaPreparationMetrics.d.ts +145 -0
- package/dist/qaPreparationMetrics.js +120 -0
- package/dist/qaPreparationPolicy.d.ts +5 -0
- package/dist/qaPreparationPolicy.js +51 -0
- package/dist/qaPreparationStore.d.ts +157 -0
- package/dist/qaPreparationStore.js +412 -0
- package/dist/qaProtocolV2.d.ts +28 -0
- package/dist/qaProtocolV2.js +12 -0
- package/dist/qaReview.d.ts +6 -0
- package/dist/qaReview.js +293 -41
- package/dist/qaTimeline.d.ts +18 -0
- package/dist/qaTimeline.js +123 -0
- package/dist/qaVerificationContract.d.ts +17 -0
- package/dist/qaVerificationContract.js +236 -0
- package/dist/readinessCleanup.d.ts +19 -0
- package/dist/readinessCleanup.js +122 -0
- package/dist/readinessGate.d.ts +1 -0
- package/dist/readinessGate.js +82 -0
- package/dist/recoveryGuidance.d.ts +17 -0
- package/dist/recoveryGuidance.js +38 -0
- package/dist/runtimeAuth.d.ts +1 -0
- package/dist/runtimeAuth.js +5 -1
- package/dist/runtimeReadiness.d.ts +13 -0
- package/dist/runtimeReadiness.js +206 -10
- package/dist/sessionIdentity.d.ts +2 -0
- package/dist/sessionIdentity.js +14 -0
- package/dist/sessionLifecycle.js +3 -3
- package/dist/sources/sourceRegistry.js +7 -0
- package/dist/stateTransfer.js +85 -8
- package/dist/statusReporter.d.ts +1 -0
- package/dist/statusReporter.js +1 -0
- package/dist/supervisedStart.d.ts +6 -0
- package/dist/supervisedStart.js +178 -62
- package/dist/supervisor.d.ts +2 -0
- package/dist/supervisor.js +26 -2
- package/dist/terminalStyle.d.ts +21 -0
- package/dist/terminalStyle.js +50 -0
- package/dist/ticketPopulation.d.ts +1 -0
- package/dist/ticketPopulation.js +5 -1
- package/dist/tickets/commands.js +2 -2
- package/dist/tickets/queue.js +1 -1
- package/dist/tickets/ticketSchema.d.ts +198 -0
- package/dist/tickets/ticketSchema.js +2 -0
- package/dist/windowsProbeJob.d.ts +12 -0
- package/dist/windowsProbeJob.js +144 -0
- package/dist/workflowDb.d.ts +121 -6
- package/dist/workflowDb.js +874 -30
- package/dist/workflowReader.d.ts +116 -2
- package/dist/workflowReader.js +131 -4
- package/package.json +89 -8
- package/scripts/copy-graph-assets.mjs +3 -0
- package/scripts/graph-benchmark.mjs +20 -0
- package/scripts/graph-evaluation.mjs +85 -0
package/dist/qaReview.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import { withGraphDerivedAccess } from "./graph/derived.js";
|
|
2
|
+
import { dispatchWithGraphAccess } from "./graph/session.js";
|
|
3
|
+
import { finalQaRoleInstructions, resolveEffectiveQaConfiguration } from "./qaEffectiveConfig.js";
|
|
4
|
+
import { COVERAGE_INSTRUCTIONS, parseContractCoverage, validateContractCoverage } from "./qaContractCoverage.js";
|
|
5
|
+
import { contractDigest, renderVerificationContract } from "./qaVerificationContract.js";
|
|
6
|
+
import { finalReviewContractContext } from "./qaContractDelivery.js";
|
|
7
|
+
import { graphContext, parseGraphRequest, prepareGraphInstruction, sendGraphTurn } from "./graph/turn.js";
|
|
8
|
+
import { BuildControlBoundary } from "./buildInterventions.js";
|
|
9
|
+
import { formatRecoveryCommand } from "./recoveryGuidance.js";
|
|
10
|
+
import { assertBuildAssignmentReconciled, BuildAssignmentRejected, captureBuildSource } from "./buildAssignment.js";
|
|
1
11
|
import { checkQaPrerequisites } from "./qaPrerequisites.js";
|
|
2
12
|
import { boundedQaHistory } from "./qaHandbackHistory.js";
|
|
3
13
|
import { QA_FAILURE_REPORT_END, QA_FAILURE_REPORT_START, parseQaResponseContract, qaFailureReportV1Schema, } from "rafi-spec";
|
|
@@ -55,7 +65,34 @@ function pauseQaReview(opts, stage, detail, frozenState) {
|
|
|
55
65
|
db.close();
|
|
56
66
|
}
|
|
57
67
|
ensureProtocolPausePacket(opts, head, stage, detail, frozenState);
|
|
58
|
-
return { outcome: "needs-human", detail: `${detail}. Resume with:
|
|
68
|
+
return { outcome: "needs-human", detail: `${detail}. Resume with: ${formatRecoveryCommand(opts.recovery.projectDir)}` };
|
|
69
|
+
}
|
|
70
|
+
/** Retry completed protocol failures, never uncertain execution or Builder work.
|
|
71
|
+
* The allowance is shared by all protocol failures for this ticket and survives restart.
|
|
72
|
+
* oneReview's finally closes the predecessor before the loop allocates a new snapshot.
|
|
73
|
+
*/
|
|
74
|
+
function retryFreshQaReview(opts, stage, detail, frozenState) {
|
|
75
|
+
const db = new WorkflowDb(opts.recovery.projectDir);
|
|
76
|
+
let retry = false;
|
|
77
|
+
try {
|
|
78
|
+
retry = db.reserveRecoveryAllowance(opts.recovery.runId, opts.ticket.id, "qa-fresh-protocol-review", 1);
|
|
79
|
+
if (retry) {
|
|
80
|
+
const head = db.qaTicketHead(opts.recovery.runId, opts.ticket.id);
|
|
81
|
+
if (head.state !== "operator-menu")
|
|
82
|
+
db.transitionQa(opts.recovery.runId, opts.ticket.id, head.revision, { type: "operator-menu" });
|
|
83
|
+
db.appendContinuityEvent({ runId: opts.recovery.runId, role: "host", kind: "qa_fresh_review_retry",
|
|
84
|
+
payload: { ticketId: opts.ticket.id, stage, detail }, authoritativeStateRevision: head.revision });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
db.close();
|
|
89
|
+
}
|
|
90
|
+
if (!retry)
|
|
91
|
+
return pauseQaReview(opts, stage, `${detail}; automatic fresh QA review allowance exhausted`, frozenState);
|
|
92
|
+
opts.resumedRecovery = undefined;
|
|
93
|
+
currentActivity()?.update("qa-recovery", `${opts.ticket.id}: ${detail}. Running a complete fresh QA review.`);
|
|
94
|
+
opts.evidence?.({ cycle: 0, outcome: "qa_fresh_review_retry", detail });
|
|
95
|
+
return { outcome: "retry-modification" };
|
|
59
96
|
}
|
|
60
97
|
function pauseQaProtocol(opts, stage, detail, outcome = "needs-human") {
|
|
61
98
|
const db = new WorkflowDb(opts.recovery.projectDir);
|
|
@@ -69,7 +106,7 @@ function pauseQaProtocol(opts, stage, detail, outcome = "needs-human") {
|
|
|
69
106
|
db.close();
|
|
70
107
|
}
|
|
71
108
|
ensureProtocolPausePacket(opts, head, stage, detail);
|
|
72
|
-
return { outcome, detail: `${detail}. Resume with:
|
|
109
|
+
return { outcome, detail: `${detail}. Resume with: ${formatRecoveryCommand(opts.recovery.projectDir)}` };
|
|
73
110
|
}
|
|
74
111
|
function loadDurableQaHistory(db, runId, ticketId) {
|
|
75
112
|
const reviews = db.qaReviewAttempts(runId, ticketId);
|
|
@@ -99,6 +136,24 @@ function loadDurableQaHistory(db, runId, ticketId) {
|
|
|
99
136
|
return { reviews, remediations, history };
|
|
100
137
|
}
|
|
101
138
|
export async function runIsolatedQa(opts) {
|
|
139
|
+
try {
|
|
140
|
+
return await withGraphDerivedAccess([], () => runIsolatedQaOwned(opts));
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (error instanceof BuildControlBoundary)
|
|
144
|
+
return { outcome: "needs-human", detail: error.message };
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function runIsolatedQaOwned(opts) {
|
|
149
|
+
try {
|
|
150
|
+
assertBuildAssignmentReconciled(opts.recovery.projectDir, opts.recovery.runId);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error instanceof BuildAssignmentRejected)
|
|
154
|
+
return { outcome: "needs-human", detail: error.message };
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
102
157
|
if (opts.resumedRecovery && (opts.resumedRecovery.manifest.runId !== opts.recovery.runId || opts.resumedRecovery.manifest.ticketId !== opts.ticket.id)) {
|
|
103
158
|
return { outcome: "needs-human", detail: `QA recovery packet ${opts.resumedRecovery.manifest.runId}/${opts.resumedRecovery.manifest.ticketId} does not match requested scope ${opts.recovery.runId}/${opts.ticket.id}; no QA work was dispatched` };
|
|
104
159
|
}
|
|
@@ -107,8 +162,10 @@ export async function runIsolatedQa(opts) {
|
|
|
107
162
|
let history;
|
|
108
163
|
let durableReviews;
|
|
109
164
|
let durableRemediations;
|
|
165
|
+
let durableRemediationGeneration;
|
|
110
166
|
try {
|
|
111
167
|
scopeDb.ensureRun(opts.recovery.runId);
|
|
168
|
+
scopeDb.assertAdmittedWork(opts.recovery.runId, opts.ticket.id);
|
|
112
169
|
durablePolicy = scopeDb.autonomyPolicy(opts.recovery.runId) ?? scopeDb.freezeAutonomyPolicy(opts.recovery.runId, resolveAutonomyPolicy(loadProjectAutonomyConfig(opts.recovery.projectDir)));
|
|
113
170
|
const stop = scopeDb.qaRemediationStop(opts.recovery.runId, opts.ticket.id);
|
|
114
171
|
if (stop) {
|
|
@@ -128,7 +185,7 @@ export async function runIsolatedQa(opts) {
|
|
|
128
185
|
protocolHead = scopeDb.transitionQa(opts.recovery.runId, opts.ticket.id, protocolHead.revision, { type: "operator-menu" });
|
|
129
186
|
if (!opts.resumedRecovery) {
|
|
130
187
|
ensureProtocolPausePacket(opts, protocolHead, "turn-uncertain", "A QA provider turn may have been dispatched before the prior process stopped");
|
|
131
|
-
return { outcome: "needs-human", detail: `A QA provider turn may have been dispatched before the prior process stopped. Reconcile it before retrying. Resume with:
|
|
188
|
+
return { outcome: "needs-human", detail: `A QA provider turn may have been dispatched before the prior process stopped. Reconcile it before retrying. Resume with: ${formatRecoveryCommand(opts.recovery.projectDir)}` };
|
|
132
189
|
}
|
|
133
190
|
}
|
|
134
191
|
if (protocolHead.state === "remediation-intended" || protocolHead.state === "remediation-uncertain") {
|
|
@@ -137,7 +194,7 @@ export async function runIsolatedQa(opts) {
|
|
|
137
194
|
protocolHead = scopeDb.transitionQa(opts.recovery.runId, opts.ticket.id, protocolHead.revision, { type: "operator-menu" });
|
|
138
195
|
if (!opts.resumedRecovery) {
|
|
139
196
|
ensureProtocolPausePacket(opts, protocolHead, "remediation-uncertain", "Builder remediation has an uncertain external outcome; QA must review current source without replaying Builder");
|
|
140
|
-
return { outcome: "needs-human", detail: `Builder remediation has an uncertain external outcome and cannot be replayed automatically. Resume with:
|
|
197
|
+
return { outcome: "needs-human", detail: `Builder remediation has an uncertain external outcome and cannot be replayed automatically. Resume with: ${formatRecoveryCommand(opts.recovery.projectDir)}` };
|
|
141
198
|
}
|
|
142
199
|
}
|
|
143
200
|
if (protocolHead.state === "source-frozen" || protocolHead.state === "review-ready") {
|
|
@@ -155,6 +212,7 @@ export async function runIsolatedQa(opts) {
|
|
|
155
212
|
scopeDb.updateQaRemediationAttempt(uncertain.attemptId, "uncertain", { detail: "host restarted without a durable Builder completion receipt" });
|
|
156
213
|
durableRemediations = scopeDb.qaRemediationAttempts(opts.recovery.runId, opts.ticket.id);
|
|
157
214
|
({ history } = loadDurableQaHistory(scopeDb, opts.recovery.runId, opts.ticket.id));
|
|
215
|
+
durableRemediationGeneration = scopeDb.qaTicketHead(opts.recovery.runId, opts.ticket.id).remediationGeneration;
|
|
158
216
|
}
|
|
159
217
|
finally {
|
|
160
218
|
scopeDb.close();
|
|
@@ -169,13 +227,57 @@ export async function runIsolatedQa(opts) {
|
|
|
169
227
|
const maxBuilderFixes = Math.min(opts.maxCycles, durableLimit);
|
|
170
228
|
let automaticFixes = persistedQaFixCount(opts);
|
|
171
229
|
let reviewAttempt = Math.max(opts.state.reviews, ...durableReviews.map((item) => item.reviewNumber), 0);
|
|
172
|
-
let remediationGeneration = Math.max(opts.state.remediationGeneration ?? 0, ...durableReviews.map((item) => item.remediationGeneration), ...durableRemediations.map((item) => item.generation), 0);
|
|
230
|
+
let remediationGeneration = Math.max(opts.state.remediationGeneration ?? 0, durableRemediationGeneration, ...durableReviews.map((item) => item.remediationGeneration), ...durableRemediations.map((item) => item.generation), 0);
|
|
173
231
|
const resumedHeadDb = new WorkflowDb(opts.recovery.projectDir);
|
|
232
|
+
resumedHeadDb.reconcileInstructionDeliveries(opts.recovery.runId, opts.ticket.id);
|
|
233
|
+
opts.qaRemediationAuthorization ??= resumedHeadDb.consumeInstructionControls(opts.recovery.runId, opts.ticket.id, "attempts");
|
|
174
234
|
let resumedHead = resumedHeadDb.qaTicketHead(opts.recovery.runId, opts.ticket.id);
|
|
175
235
|
let resumedReport = resumedHead.state === "review-failed"
|
|
176
236
|
? resumedHeadDb.unresolvedQaReports(opts.recovery.runId, opts.ticket.id).at(-1)
|
|
177
237
|
: undefined;
|
|
178
238
|
resumedHeadDb.close();
|
|
239
|
+
if (opts.resumeBuilderGuidance && resumedHead.state === "passed") {
|
|
240
|
+
const controlDb = new WorkflowDb(opts.recovery.projectDir);
|
|
241
|
+
let waiting = false;
|
|
242
|
+
try {
|
|
243
|
+
const preview = controlDb.reserveGuidance(opts.recovery.runId, opts.ticket.id, "builder", randomUUID(), captureBuildSource(opts.builderWorktree).digest, "Builder follow-up", false);
|
|
244
|
+
waiting = preview.ids.length > 0;
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
controlDb.close();
|
|
248
|
+
}
|
|
249
|
+
if (waiting) {
|
|
250
|
+
if (automaticFixes >= maxBuilderFixes && !opts.qaRemediationAuthorization) {
|
|
251
|
+
const detail = `Builder guidance is waiting, but the attempt allowance is exhausted. Authorize one follow-up with /request-attempt ${opts.recovery.runId} ${opts.ticket.id} <reason>, then resume.`;
|
|
252
|
+
ensureProtocolPausePacket(opts, resumedHead, "builder-guidance-budget", detail);
|
|
253
|
+
return { outcome: "needs-human", detail };
|
|
254
|
+
}
|
|
255
|
+
if (!opts.deliverBuilderFollowup)
|
|
256
|
+
return { outcome: "needs-human", detail: "Builder guidance is waiting; resume through the build owner with a validated Builder session" };
|
|
257
|
+
const latest = durableReviews.at(-1);
|
|
258
|
+
const instruction = `Builder guidance follow-up after explicit resume.\nTicket scope: ${opts.ticket.id}. Do not substitute another ticket.\nPreserve the previously completed work and all mandatory requirements. Apply only the authorized waiting guidance within the approved ticket scope. If broader permission is needed, stop and explain it.\nApproved ticket requirements:\n${JSON.stringify(opts.ticket)}\nReport the instruction IDs you applied with evidence, then finish with STEP_STATUS: done | ticket="${opts.ticket.id}" summary="...".`;
|
|
259
|
+
const fix = await opts.deliverBuilderFollowup(instruction, { reviewAttemptId: latest.attemptId, requirementsDigest: qaDigest("admitted-requirements", opts.ticket), maximum: maxBuilderFixes, authorizationId: opts.qaRemediationAuthorization });
|
|
260
|
+
opts.qaRemediationAuthorization = undefined;
|
|
261
|
+
if (!fix.ok) {
|
|
262
|
+
const db = new WorkflowDb(opts.recovery.projectDir);
|
|
263
|
+
try {
|
|
264
|
+
ensureProtocolPausePacket(opts, db.qaTicketHead(opts.recovery.runId, opts.ticket.id), "builder-guidance-stopped", fix.detail ?? "Builder follow-up did not complete");
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
db.close();
|
|
268
|
+
}
|
|
269
|
+
return { outcome: "needs-human", detail: fix.detail ?? "Builder follow-up did not complete; reconcile its outcome before resuming" };
|
|
270
|
+
}
|
|
271
|
+
opts.builderSummary = fix.response;
|
|
272
|
+
opts.state.builderResponseHistory.push({ ticketId: opts.ticket.id, cycle: latest.cycle, kind: "remediation", response: fix.response, summary: boundedBuilderSummary(fix.summary) });
|
|
273
|
+
automaticFixes = persistedQaFixCount(opts);
|
|
274
|
+
remediationGeneration += 1;
|
|
275
|
+
opts.state.remediationGeneration = remediationGeneration;
|
|
276
|
+
// The assignment transaction invalidated the old certificate. A complete
|
|
277
|
+
// fresh review now checks the exact post-follow-up source.
|
|
278
|
+
opts.resumedRecovery = undefined;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
179
281
|
if (resumedHead.state === "review-failed") {
|
|
180
282
|
const resumedReviewNumber = resumedReport?.reviewNumber;
|
|
181
283
|
const causing = resumedReviewNumber === undefined ? undefined : durableReviews.find((item) => item.reviewNumber === resumedReviewNumber)?.attemptId;
|
|
@@ -196,13 +298,20 @@ export async function runIsolatedQa(opts) {
|
|
|
196
298
|
}
|
|
197
299
|
else {
|
|
198
300
|
ensureProtocolPausePacket(opts, resumedHead, "missing-review-evidence", missing);
|
|
199
|
-
return { outcome: "needs-human", detail: `${missing}. Resume with:
|
|
301
|
+
return { outcome: "needs-human", detail: `${missing}. Resume with: ${formatRecoveryCommand(opts.recovery.projectDir)}` };
|
|
200
302
|
}
|
|
201
303
|
}
|
|
202
304
|
}
|
|
203
305
|
if (resumedHead.state === "review-failed" && resumedReport) {
|
|
204
306
|
const durableReport = resumedReport;
|
|
205
|
-
|
|
307
|
+
const attemptControlDb = new WorkflowDb(opts.recovery.projectDir);
|
|
308
|
+
try {
|
|
309
|
+
opts.qaRemediationAuthorization ??= attemptControlDb.consumeInstructionControls(opts.recovery.runId, opts.ticket.id, "attempts");
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
attemptControlDb.close();
|
|
313
|
+
}
|
|
314
|
+
if (automaticFixes >= maxBuilderFixes && !opts.qaRemediationAuthorization)
|
|
206
315
|
return pauseQaProtocol(opts, "qa-nonconvergence", `QA remediation budget is exhausted with unresolved report ${durableReport.reportDigest}`, "nonconverged");
|
|
207
316
|
const report = durableReport.report;
|
|
208
317
|
const causing = durableReviews.find((item) => item.reviewNumber === durableReport.reviewNumber)?.attemptId;
|
|
@@ -211,7 +320,8 @@ export async function runIsolatedQa(opts) {
|
|
|
211
320
|
const request = {
|
|
212
321
|
kind: "validated-report", report, reportDigest: resumedReport.reportDigest, history: [...history], latestBuilderResult: latestBuilderResponse(opts),
|
|
213
322
|
};
|
|
214
|
-
const fix = await observedQaFix(opts, "resuming pending QA remediation", request, causing);
|
|
323
|
+
const fix = await observedQaFix(opts, "resuming pending QA remediation", request, causing, Boolean(opts.qaRemediationAuthorization));
|
|
324
|
+
opts.qaRemediationAuthorization = undefined;
|
|
215
325
|
if (!fix.ok)
|
|
216
326
|
return pauseQaProtocol(opts, "builder-remediation-failed", fix.detail ?? "Builder QA remediation failed during restart recovery", fix.outcome === "needs-input" ? "needs-human" : "blocked");
|
|
217
327
|
const fixSummary = boundedBuilderSummary(fix.summary ?? fix.response ?? "Builder reported remediation complete");
|
|
@@ -264,9 +374,17 @@ export async function runIsolatedQa(opts) {
|
|
|
264
374
|
return review;
|
|
265
375
|
reviewAttempt = Math.max(reviewAttempt, review.reviewAttempt);
|
|
266
376
|
history.push({ cycle, reviewAttempt: review.reviewAttempt, remediationGeneration, attemptId: review.reviewAttemptId, outcome: "qa_fail", detail: review.detail, reportDigest: review.reportDigest, report: review.report, findingIds: review.report?.findings.map((finding) => finding.id) });
|
|
267
|
-
|
|
377
|
+
const controlDb = new WorkflowDb(opts.recovery.projectDir);
|
|
378
|
+
try {
|
|
379
|
+
opts.qaRemediationAuthorization ??= controlDb.consumeInstructionControls(opts.recovery.runId, opts.ticket.id, "attempts");
|
|
380
|
+
}
|
|
381
|
+
finally {
|
|
382
|
+
controlDb.close();
|
|
383
|
+
}
|
|
384
|
+
if (automaticFixes < maxBuilderFixes || opts.qaRemediationAuthorization) {
|
|
268
385
|
const request = { kind: "validated-report", report: review.report, reportDigest: review.reportDigest, history: [...history], latestBuilderResult: latestBuilderResponse(opts) };
|
|
269
|
-
const fix = await observedQaFix(opts, "applying QA fixes", request, review.reviewAttemptId);
|
|
386
|
+
const fix = await observedQaFix(opts, "applying QA fixes", request, review.reviewAttemptId, Boolean(opts.qaRemediationAuthorization));
|
|
387
|
+
opts.qaRemediationAuthorization = undefined;
|
|
270
388
|
if (!fix.ok)
|
|
271
389
|
return pauseQaProtocol(opts, "builder-remediation-failed", fix.detail ?? "Builder QA fix failed", fix.outcome === "needs-input" ? "needs-human" : "blocked");
|
|
272
390
|
const fixSummary = boundedBuilderSummary(fix.summary ?? fix.response ?? fix.detail ?? "Builder reported remediation complete");
|
|
@@ -298,7 +416,7 @@ export async function runIsolatedQa(opts) {
|
|
|
298
416
|
if (decision.action === "pause")
|
|
299
417
|
return pauseQaProtocol(opts, "qa-nonconvergence", detail, "nonconverged");
|
|
300
418
|
if (decision.action === "waive") {
|
|
301
|
-
waiveV2Reports(opts, "Operator explicitly waived unresolved QA findings");
|
|
419
|
+
waiveV2Reports(opts, "Operator explicitly waived unresolved QA findings", decision.authorizationRef);
|
|
302
420
|
markQaRecoveryResolved(opts);
|
|
303
421
|
return { outcome: "waived", detail: history.at(-1)?.detail ?? detail, summary: "QA explicitly waived by user" };
|
|
304
422
|
}
|
|
@@ -561,7 +679,7 @@ async function oneReview(opts, identity) {
|
|
|
561
679
|
finally {
|
|
562
680
|
db.close();
|
|
563
681
|
}
|
|
564
|
-
return { outcome: "needs-human", detail: `${error.message}. No QA session or report was created. Resume with:
|
|
682
|
+
return { outcome: "needs-human", detail: `${error.message}. No QA session or report was created. Resume with: ${formatRecoveryCommand(opts.recovery.projectDir)}` };
|
|
565
683
|
}
|
|
566
684
|
let qa;
|
|
567
685
|
let recoverySnapshot;
|
|
@@ -582,7 +700,7 @@ async function oneReview(opts, identity) {
|
|
|
582
700
|
const missingPrerequisites = prerequisiteEvidence.checks.filter(check => check.outcome === "not_run");
|
|
583
701
|
if (missingPrerequisites.length && !prerequisiteEvidence.sourceDefects.length)
|
|
584
702
|
return { outcome: "blocked", detail: `QA verification prerequisites unavailable; required checks were not_run: ${missingPrerequisites.map(check => check.evidence).join("; ")}. Restore the required environment with existing authority, then resume for fresh QA.` };
|
|
585
|
-
let handoff = buildQaReviewHandoff(opts.ticket, opts.builderSummary, snapshot.manifest.diffDigest, loadTicketSetupConfigWithDefaults(opts.
|
|
703
|
+
let handoff = buildQaReviewHandoff(opts.ticket, opts.builderSummary, snapshot.manifest.diffDigest, (opts.frozenValidationChecklist ??= [...loadTicketSetupConfigWithDefaults(opts.recovery.projectDir).build.validation_checklist]), snapshot.frozenState.changeSummary, opts.qaHistory);
|
|
586
704
|
handoff += `\nHost prerequisite evidence (availability does not prove provider sandbox access): ${JSON.stringify(prerequisiteEvidence)}\nRecord tests prevented from executing as not_run with the actual reason. Never install dependencies, create a lockfile, provision services, or claim an unconditional pass when required verification was not run.`;
|
|
587
705
|
// Every disposable snapshot has a distinct cwd and therefore must have a
|
|
588
706
|
// fresh provider conversation. Cumulative QA state remains in the durable
|
|
@@ -605,7 +723,11 @@ async function oneReview(opts, identity) {
|
|
|
605
723
|
return { outcome: "retry-modification" };
|
|
606
724
|
return { outcome: "needs-human", detail };
|
|
607
725
|
}
|
|
726
|
+
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 });
|
|
727
|
+
let graphPrepared = opts.resumedRecovery ? { instruction: handoff, bytes: 0 } : await prepareGraphInstruction(handoff, graphReviewContext, await qa.contextUsage?.());
|
|
728
|
+
handoff = graphPrepared.instruction;
|
|
608
729
|
const v2Binding = beginV2Review(opts, snapshot.frozenState, snapshot.path, handoff, qaHandle, { attemptId, cycle, remediationGeneration });
|
|
730
|
+
handoff = v2Binding.reviewInstruction;
|
|
609
731
|
bindQaTurnAdapter(qa, v2Binding);
|
|
610
732
|
reviewAttempt = v2Binding.reviewNumber;
|
|
611
733
|
identity = { ...identity, reviewAttempt };
|
|
@@ -715,6 +837,8 @@ async function oneReview(opts, identity) {
|
|
|
715
837
|
resumedPacket = acknowledged.packet;
|
|
716
838
|
const ackErrors = acknowledged.errors;
|
|
717
839
|
if (ackErrors.length) {
|
|
840
|
+
if (acknowledged.retryable)
|
|
841
|
+
return retryFreshQaReview(opts, "invalid-recovery-acknowledgement", `QA recovery acknowledgement was invalid: ${ackErrors.join("; ")}`, snapshot.frozenState);
|
|
718
842
|
const synthetic = { text: "", rawResponse: "", cleanedResponse: "", isError: true, numTurns: 0, costUsd: 0 };
|
|
719
843
|
const recovery = await repairResumedFailureReport(opts, identity, resumedPacket, snapshot, qaHandle, synthetic, ackErrors, qaEvents, recoveryContext, true);
|
|
720
844
|
if (recovery.outcome === "failed")
|
|
@@ -750,12 +874,18 @@ async function oneReview(opts, identity) {
|
|
|
750
874
|
opts.resumedRecovery = undefined;
|
|
751
875
|
}
|
|
752
876
|
if (resumedFullReviewNeedsBinding) {
|
|
753
|
-
const
|
|
877
|
+
const resumedAttemptId = randomUUID();
|
|
878
|
+
graphReviewContext = { ...graphReviewContext, operationId: `graph-review:${resumedAttemptId}`, logicalTaskId: resumedAttemptId };
|
|
879
|
+
graphPrepared = await prepareGraphInstruction(handoff, graphReviewContext, await qa.contextUsage?.());
|
|
880
|
+
handoff = graphPrepared.instruction;
|
|
881
|
+
const resumedReviewBinding = beginV2Review(opts, snapshot.frozenState, snapshot.path, handoff, qaHandle, { attemptId: resumedAttemptId, cycle, remediationGeneration });
|
|
754
882
|
bindQaTurnAdapter(qa, resumedReviewBinding);
|
|
755
883
|
resumedPacket = resumedReviewBinding.recoveryPacket;
|
|
756
884
|
reviewAttempt = resumedReviewBinding.reviewNumber;
|
|
757
885
|
}
|
|
758
|
-
let
|
|
886
|
+
let graphSubturn = 0;
|
|
887
|
+
const activeGraphBinding = qaTurnBindings.get(qa);
|
|
888
|
+
let turn = preparedTurn ?? await sendGraphTurn(qa, handoff, { ...graphReviewContext, reviewBasisRef: activeGraphBinding.reviewBasisDigest }, undefined, (prompt) => sendDurableQaTurn(qa, prompt, qaEvents, graphSubturn++ === 0 ? "initial" : `graph-evidence-${graphSubturn}`), { ...graphPrepared, instruction: activeGraphBinding.reviewInstruction });
|
|
759
889
|
let status = parseStepStatus(turn.text);
|
|
760
890
|
recoveryContext = qaTurnBindings.get(qa)?.recoveryContext;
|
|
761
891
|
try {
|
|
@@ -830,8 +960,10 @@ async function oneReview(opts, identity) {
|
|
|
830
960
|
durableReviewFinished = true;
|
|
831
961
|
return { outcome: "retry-modification" };
|
|
832
962
|
}
|
|
833
|
-
if (
|
|
834
|
-
return
|
|
963
|
+
if (turn.continuityErrors?.length && !turn.failure)
|
|
964
|
+
return retryFreshQaReview(opts, "invalid-continuity", `QA continuity record was invalid: ${turn.continuityErrors.join("; ")}`, reviewedSnapshot.frozenState);
|
|
965
|
+
if (responseProtocolError && !turn.isError && !turn.failure)
|
|
966
|
+
return retryFreshQaReview(opts, "invalid-response-contract", responseProtocolError, reviewedSnapshot.frozenState);
|
|
835
967
|
if (turn.isError || turn.failure)
|
|
836
968
|
return pauseQaReview(opts, "qa-turn-error", `QA turn failed: ${sanitizePreview(turn.text)}`, reviewedSnapshot.frozenState);
|
|
837
969
|
if (status.kind === "blocked")
|
|
@@ -927,7 +1059,7 @@ async function oneReview(opts, identity) {
|
|
|
927
1059
|
markQaRecoveryPendingBuilder(opts);
|
|
928
1060
|
return { outcome: "failed", detail, report, reportDigest: digest, reviewAttempt: acceptedBinding.reviewNumber, reviewAttemptId: acceptedBinding.reviewAttemptId };
|
|
929
1061
|
}
|
|
930
|
-
return
|
|
1062
|
+
return retryFreshQaReview(opts, "unrecognized-qa-outcome", status.error ?? `QA returned ${status.kind}`, reviewedSnapshot.frozenState);
|
|
931
1063
|
}
|
|
932
1064
|
finally {
|
|
933
1065
|
if (durableReviewStarted && !durableReviewFinished) {
|
|
@@ -978,7 +1110,11 @@ async function repairResumedFailureReport(opts, identity, initialPacket, snapsho
|
|
|
978
1110
|
}, { purpose: "Validation result for the first resumed QA report response" });
|
|
979
1111
|
void packet;
|
|
980
1112
|
void requireFreshReviewReason;
|
|
981
|
-
|
|
1113
|
+
if (!originalTurn.isError && !originalTurn.failure) {
|
|
1114
|
+
const result = retryFreshQaReview(opts, "resumed-invalid-report", "Resumed QA report was invalid; a complete fresh QA review is required", snapshot.frozenState);
|
|
1115
|
+
return result.outcome === "retry-modification" ? { outcome: "retry" } : { outcome: "failed", result };
|
|
1116
|
+
}
|
|
1117
|
+
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) };
|
|
982
1118
|
}
|
|
983
1119
|
export function buildQaReviewHandoff(ticket, builderSummary, diffDigest, validationChecklist, changeSummary, history = []) {
|
|
984
1120
|
return [
|
|
@@ -1063,6 +1199,7 @@ async function repairInvalidFailureReport(opts, identity, snapshot, originalQa,
|
|
|
1063
1199
|
let turns = 0;
|
|
1064
1200
|
let recoveryContextSeal;
|
|
1065
1201
|
let protocolViolation;
|
|
1202
|
+
let correctionExecutionFailed = false;
|
|
1066
1203
|
const attemptCorrections = async (count, stage, guidance) => {
|
|
1067
1204
|
for (let index = 0; index < count; index++) {
|
|
1068
1205
|
turns++;
|
|
@@ -1101,6 +1238,7 @@ async function repairInvalidFailureReport(opts, identity, snapshot, originalQa,
|
|
|
1101
1238
|
}
|
|
1102
1239
|
if (!result.isError && !result.failure && contract.valid && contract.report)
|
|
1103
1240
|
return { result, contract };
|
|
1241
|
+
correctionExecutionFailed = Boolean(result.failure || (result.isError && !result.continuityErrors?.length));
|
|
1104
1242
|
errors = result.isError ? [`QA correction turn errored: ${sanitizePreview(result.text)}`] : contract.errors;
|
|
1105
1243
|
}
|
|
1106
1244
|
return undefined;
|
|
@@ -1111,7 +1249,10 @@ async function repairInvalidFailureReport(opts, identity, snapshot, originalQa,
|
|
|
1111
1249
|
if (protocolViolation)
|
|
1112
1250
|
packet = appendQaRecoveryResource(packet, `recovery-history/context-mutation-${packet.manifest.revision + 1}.txt`, protocolViolation, { purpose: "Recovery-context mutation protocol violation", exact: true });
|
|
1113
1251
|
void packet;
|
|
1114
|
-
|
|
1252
|
+
if (protocolViolation || correctionExecutionFailed)
|
|
1253
|
+
return { outcome: "failed", result: pauseQaReview(opts, "report-correction-exhausted", protocolViolation ?? errors.join("; "), snapshot.frozenState) };
|
|
1254
|
+
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);
|
|
1255
|
+
return result.outcome === "retry-modification" ? { outcome: "retry" } : { outcome: "failed", result };
|
|
1115
1256
|
}
|
|
1116
1257
|
async function exhausted(packet, originalResponse, opts, prefix = "QA report correction exhausted after nine turns", qa, snapshot, liveSession = Boolean(qa), unavailableOperatorReason, requireFreshReviewReason) {
|
|
1117
1258
|
const synopsis = parseQaResponseContract(originalResponse).fields.issues?.trim();
|
|
@@ -1170,7 +1311,7 @@ async function exhausted(packet, originalResponse, opts, prefix = "QA report cor
|
|
|
1170
1311
|
db.close();
|
|
1171
1312
|
}
|
|
1172
1313
|
}
|
|
1173
|
-
return { outcome: "failed", result: { outcome: "needs-human", detail: `${currentPrefix}. Recovery packet: ${packet.directory} (${packet.manifest.packetDigest}). Resources:\n${boundedRecoveryInventory(packet)}\n${QA_REPORT_RECOVERY_MENU.map((item, index) => `${index + 1}. ${item}`).join("\n")}\nResume with:
|
|
1314
|
+
return { outcome: "failed", result: { outcome: "needs-human", detail: `${currentPrefix}. Recovery packet: ${packet.directory} (${packet.manifest.packetDigest}). Resources:\n${boundedRecoveryInventory(packet)}\n${QA_REPORT_RECOVERY_MENU.map((item, index) => `${index + 1}. ${item}`).join("\n")}\nResume with: ${formatRecoveryCommand(opts.recovery.projectDir)}` } };
|
|
1174
1315
|
}
|
|
1175
1316
|
function normalizeUnsupportedOperator(result, reason) {
|
|
1176
1317
|
return result.outcome === "operator"
|
|
@@ -1334,7 +1475,8 @@ async function performRecoveryAcknowledgement(packet, qa, prompt, events, contin
|
|
|
1334
1475
|
}
|
|
1335
1476
|
packet = await appendTurnObservation(packet, `${label}-acknowledgement`, first, prompt, errors, "packet-acknowledgement", events);
|
|
1336
1477
|
if (first.isError || first.failure)
|
|
1337
|
-
return { packet, errors: [...errors, "QA acknowledgement provider turn failed"]
|
|
1478
|
+
return { packet, errors: [...errors, first.continuityErrors?.length && !first.failure ? "QA acknowledgement continuity metadata was invalid" : "QA acknowledgement provider turn failed"],
|
|
1479
|
+
retryable: Boolean(first.continuityErrors?.length && !first.failure && !errors.some(error => /mutated/i.test(error))) };
|
|
1338
1480
|
try {
|
|
1339
1481
|
seal?.verify();
|
|
1340
1482
|
}
|
|
@@ -1359,7 +1501,7 @@ async function performRecoveryAcknowledgement(packet, qa, prompt, events, contin
|
|
|
1359
1501
|
}
|
|
1360
1502
|
packet = await appendTurnObservation(packet, `${label}-acknowledgement-repair`, repaired, correction, errors, "packet-acknowledgement-repair", events);
|
|
1361
1503
|
if (repaired.isError || repaired.failure)
|
|
1362
|
-
errors.push("QA acknowledgement repair provider turn failed");
|
|
1504
|
+
errors.push(repaired.continuityErrors?.length && !repaired.failure ? "QA acknowledgement repair continuity metadata was invalid" : "QA acknowledgement repair provider turn failed");
|
|
1363
1505
|
try {
|
|
1364
1506
|
seal?.verify();
|
|
1365
1507
|
}
|
|
@@ -1367,7 +1509,7 @@ async function performRecoveryAcknowledgement(packet, qa, prompt, events, contin
|
|
|
1367
1509
|
errors.push(`recovery context mutated after acknowledgement repair: ${error instanceof Error ? error.message : String(error)}`);
|
|
1368
1510
|
}
|
|
1369
1511
|
}
|
|
1370
|
-
return { packet, errors };
|
|
1512
|
+
return { packet, errors, retryable: !errors.some(error => /mutated|provider turn failed/i.test(error)) };
|
|
1371
1513
|
}
|
|
1372
1514
|
function validateBoundaryReceipt(receipt, packet, successorHandle) {
|
|
1373
1515
|
const observedSuccessor = successorHandle.sessionIdentity();
|
|
@@ -1429,14 +1571,53 @@ const qaTurnBindings = new WeakMap();
|
|
|
1429
1571
|
function beginV2Review(opts, source, reviewedSnapshotPath, instruction, handle, attempt) {
|
|
1430
1572
|
const db = new WorkflowDb(opts.recovery.projectDir);
|
|
1431
1573
|
try {
|
|
1574
|
+
const verification = db.builderVerificationContext(opts.recovery.runId, opts.ticket.id, source.digest);
|
|
1575
|
+
instruction += verification.text;
|
|
1432
1576
|
const sourceV2 = {
|
|
1433
1577
|
version: 2, runId: opts.recovery.runId, ticketId: opts.ticket.id,
|
|
1434
1578
|
originDigest: source.originDigest, contentDigest: source.contentDigest, digest: source.digest,
|
|
1435
1579
|
capturedAt: source.capturedAt, paths: source.pathInventory,
|
|
1436
1580
|
};
|
|
1437
|
-
const checklist = loadTicketSetupConfigWithDefaults(opts.
|
|
1581
|
+
const checklist = (opts.frozenValidationChecklist ??= [...loadTicketSetupConfigWithDefaults(opts.recovery.projectDir).build.validation_checklist]);
|
|
1582
|
+
if (canonicalJson(checklist) !== canonicalJson(loadTicketSetupConfigWithDefaults(opts.recovery.projectDir).build.validation_checklist))
|
|
1583
|
+
throw new Error("Canonical checklist changed before QA dispatch; contract/input reconciliation required");
|
|
1584
|
+
const preparationStore = db.qaPreparationStore();
|
|
1585
|
+
const admission = db.assertAdmittedWork(opts.recovery.runId, opts.ticket.id);
|
|
1586
|
+
const contractHead = preparationStore.head(opts.recovery.runId, opts.ticket.id, admission.requirementsDigest);
|
|
1587
|
+
const contract = preparationStore.policy(opts.recovery.runId)?.mode === "enforce" && contractHead.state === "ready" && contractHead.digest ? preparationStore.contract(contractHead.digest) : undefined;
|
|
1588
|
+
if (preparationStore.policy(opts.recovery.runId)?.mode === "enforce" && !contract)
|
|
1589
|
+
throw new Error("Final QA requires the retained ready contract; exact QA recovery must not replay preparation");
|
|
1590
|
+
if (contract) {
|
|
1591
|
+
if (qaDigest("admitted-requirements", opts.ticket) !== admission.requirementsDigest)
|
|
1592
|
+
throw new Error("Final QA ticket differs from the frozen admitted scope; renewed approval is required");
|
|
1593
|
+
const identity = handle.sessionIdentity();
|
|
1594
|
+
const priorAssessment = preparationStore.artifact(contract.semanticAssessmentDigest, "semantic-assessment");
|
|
1595
|
+
const involvedSessions = new Set([...contract.preparationEvidence.map(evidence => evidence.sessionId), priorAssessment.sessionId, priorAssessment.authorSessionId, ...preparationStore.receipts(contract.contentDigest).map(receipt => receipt.sessionId)]);
|
|
1596
|
+
if (contract.challengeReceiptDigest)
|
|
1597
|
+
involvedSessions.add(preparationStore.artifact(contract.challengeReceiptDigest, "approach-challenge").sessionId);
|
|
1598
|
+
if (involvedSessions.has(identity.sessionId))
|
|
1599
|
+
throw new Error("Final reviewer must be independent of preparation, assessment, challenge and Builder conversations");
|
|
1600
|
+
const effective = resolveEffectiveQaConfiguration(opts.recovery.projectDir, { make: handle.adapter.agent });
|
|
1601
|
+
if (effective.digest !== contract.inputs.find(input => input.kind === "rules")?.digest || handle.effectiveRoleInstructions !== finalQaRoleInstructions(effective.qaRules) || canonicalJson(handle.skills.map(skill => ({ name: skill.name, path: skill.path, content: skill.content, digest: skill.digest }))) !== canonicalJson(effective.skills.map(skill => ({ name: skill.name, path: skill.path, content: skill.content, digest: sha(skill.content) }))))
|
|
1602
|
+
throw new Error("Actual final QA rules/skills differ from the frozen canonical contract configuration");
|
|
1603
|
+
const expected = contract.inputs.find(input => input.kind === "checklist");
|
|
1604
|
+
if (expected?.digest !== contractDigest("input-checklist", checklist))
|
|
1605
|
+
throw new Error("Final QA checklist differs from delivered contract");
|
|
1606
|
+
const builderClaims = db.operations(opts.recovery.runId).flatMap(operation => {
|
|
1607
|
+
const intent = operation.intent;
|
|
1608
|
+
const result = operation.result;
|
|
1609
|
+
return operation.kind === "builder-coverage" && operation.status === "confirmed" && intent.workId === opts.ticket.id && intent.contractDigest === contract.contentDigest && result?.binding?.sourceDigest === source.digest && result.coverageDigest
|
|
1610
|
+
? [{ operationId: operation.idempotencyKey, digest: result.coverageDigest, coverage: preparationStore.artifact(result.coverageDigest, "builder-coverage") }] : [];
|
|
1611
|
+
});
|
|
1612
|
+
instruction += finalReviewContractContext(contract, preparationStore, reviewedSnapshotPath, builderClaims);
|
|
1613
|
+
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 })}`;
|
|
1614
|
+
}
|
|
1615
|
+
const originalInstruction = instruction;
|
|
1616
|
+
const guidance = db.reserveGuidance(opts.recovery.runId, opts.ticket.id, "qa", attempt.attemptId, source.digest, instruction, false);
|
|
1617
|
+
instruction = guidance.text;
|
|
1438
1618
|
const basisWithoutDigest = {
|
|
1439
1619
|
version: 2,
|
|
1620
|
+
...(contract ? { contractBinding: { version: 1, transportVersion: 1, revision: contract.revision, digest: contract.contentDigest, admissionDigest: admission.requirementsDigest, commonConfigDigest: contract.inputs.find(input => input.kind === "rules").digest } } : {}),
|
|
1440
1621
|
ticketDigest: qaDigest("ticket", opts.ticket),
|
|
1441
1622
|
instructionDigest: qaDigest("instruction", instruction),
|
|
1442
1623
|
roleInstructionsDigest: qaDigest("role-instructions", handle.effectiveRoleInstructions),
|
|
@@ -1446,6 +1627,8 @@ function beginV2Review(opts, source, reviewedSnapshotPath, instruction, handle,
|
|
|
1446
1627
|
confinementDigest: handle.confinement.digest,
|
|
1447
1628
|
};
|
|
1448
1629
|
const basis = { ...basisWithoutDigest, digest: qaDigest("review-basis-fields", basisWithoutDigest) };
|
|
1630
|
+
const inputBasisEnvelope = contract ? `\nImmutable input-basis digest: ${basis.digest}. Coverage must bind this inputBasisDigest.` : "";
|
|
1631
|
+
instruction += inputBasisEnvelope;
|
|
1449
1632
|
let head = db.qaTicketHead(opts.recovery.runId, opts.ticket.id);
|
|
1450
1633
|
if (head.state === "waived" || head.state === "completed")
|
|
1451
1634
|
throw new Error(`QA protocol for ${opts.ticket.id} is already terminal`);
|
|
@@ -1476,7 +1659,7 @@ function beginV2Review(opts, source, reviewedSnapshotPath, instruction, handle,
|
|
|
1476
1659
|
head = db.commitQaReviewReady(sourceV2, basis, identity, handle.confinement, attempt, head.revision);
|
|
1477
1660
|
if (recoveryPacket)
|
|
1478
1661
|
recoveryPacket = updateQaRecoveryReviewIdentity(recoveryPacket, head.reviewNumber, attempt.attemptId, attempt.cycle);
|
|
1479
|
-
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 };
|
|
1662
|
+
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 };
|
|
1480
1663
|
}
|
|
1481
1664
|
finally {
|
|
1482
1665
|
db.close();
|
|
@@ -1524,6 +1707,14 @@ function bindQaRecoveryContext(adapter, context) {
|
|
|
1524
1707
|
binding.recoveryContext = context;
|
|
1525
1708
|
}
|
|
1526
1709
|
async function sendDurableQaTurn(adapter, instruction, events, slot) {
|
|
1710
|
+
const binding = qaTurnBindings.get(adapter);
|
|
1711
|
+
if (!binding)
|
|
1712
|
+
throw new Error("QA provider dispatch requires a durable source/review-basis binding");
|
|
1713
|
+
// Repairs and recovery acknowledgements bypass graph navigation, but their
|
|
1714
|
+
// provider history and journal artifacts retain the original access grants.
|
|
1715
|
+
return dispatchWithGraphAccess(binding.projectDir, adapter, instruction, undefined, () => sendDurableQaTurnOwned(adapter, instruction, events, slot));
|
|
1716
|
+
}
|
|
1717
|
+
async function sendDurableQaTurnOwned(adapter, instruction, events, slot) {
|
|
1527
1718
|
const binding = qaTurnBindings.get(adapter);
|
|
1528
1719
|
if (!binding)
|
|
1529
1720
|
throw new Error("QA provider dispatch requires a durable source/review-basis binding");
|
|
@@ -1559,17 +1750,27 @@ async function sendDurableQaTurn(adapter, instruction, events, slot) {
|
|
|
1559
1750
|
const providerSession = { version: 2, provider: session.provider, sessionId: session.sessionId, role: "qa", stream: "qa", generation: session.generation, cwd: session.cwd, configRoot: session.configRoot, createdAt: session.createdAt, validatedAt: session.validatedAt ?? intendedAt };
|
|
1560
1751
|
const db = new WorkflowDb(binding.projectDir);
|
|
1561
1752
|
try {
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1753
|
+
db.atomic(() => {
|
|
1754
|
+
if (slot === "initial") {
|
|
1755
|
+
const guidance = db.reserveGuidance(binding.runId, binding.ticketId, "qa", operationId, binding.sourceStateDigest, binding.originalInstruction);
|
|
1756
|
+
if (guidance.text + (binding.inputBasisEnvelope ?? "") !== binding.reviewInstruction)
|
|
1757
|
+
throw new BuildControlBoundary("Manager guidance changed during QA preparation; restart a full review with the current instruction basis");
|
|
1758
|
+
binding.guidanceIds = guidance.ids;
|
|
1759
|
+
instruction = binding.reviewInstruction;
|
|
1760
|
+
}
|
|
1761
|
+
const instructionDigest = db.putEvidence("qa", Buffer.from(instruction));
|
|
1762
|
+
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 };
|
|
1763
|
+
const head = db.qaTicketHead(binding.runId, binding.ticketId);
|
|
1764
|
+
db.commitQaTurnIntent(intent, head.revision);
|
|
1765
|
+
});
|
|
1566
1766
|
}
|
|
1567
1767
|
finally {
|
|
1568
1768
|
db.close();
|
|
1569
1769
|
}
|
|
1570
1770
|
let result;
|
|
1771
|
+
const providerStartedAt = Date.now();
|
|
1571
1772
|
try {
|
|
1572
|
-
result = await adapter.sendTurn(instruction);
|
|
1773
|
+
result = await adapter.sendTurn(instruction, slot === "session-initialization" ? { purpose: "initialization", responseOnly: true } : slot?.includes("acknowledgement") || slot?.includes("correction") ? { purpose: "response-repair", responseOnly: true } : undefined);
|
|
1573
1774
|
}
|
|
1574
1775
|
catch (error) {
|
|
1575
1776
|
const failedDb = new WorkflowDb(binding.projectDir);
|
|
@@ -1640,8 +1841,25 @@ async function sendDurableQaTurn(adapter, instruction, events, slot) {
|
|
|
1640
1841
|
providerInstructionDigest, rawResponseDigest, cleanedResponseDigest, eventStreamDigest,
|
|
1641
1842
|
terminalEventObserved, sourceStateDigest: binding.sourceStateDigest, reviewBasisDigest: binding.reviewBasisDigest, completedAt,
|
|
1642
1843
|
};
|
|
1643
|
-
receiptDb.
|
|
1844
|
+
receiptDb.atomic(() => {
|
|
1845
|
+
receiptDb.finishQaTurn(receipt);
|
|
1846
|
+
const metricAdmission = receiptDb.admittedWork(binding.runId, binding.ticketId);
|
|
1847
|
+
if (metricAdmission)
|
|
1848
|
+
receiptDb.qaPreparationStore().metric(binding.runId, binding.ticketId, metricAdmission.requirementsDigest, `metric:qa-turn:${operationId}`, slot?.includes("correction") ? "format-repair" : "phase-observation", { phase: slot?.includes("correction") || slot === "session-initialization" ? "repair" : "review", durationMs: Math.max(0, Date.now() - providerStartedAt), costUsd: result.costAuthoritative ? result.costUsd : null, inputTokens: result.inputTokens ?? null, outputTokens: result.outputTokens ?? null });
|
|
1849
|
+
if (slot === "initial")
|
|
1850
|
+
receiptDb.finishGuidance(binding.guidanceIds, "qa", { submitted: terminalEventObserved ? true : undefined, receipt, applied: terminalEventObserved && binding.guidanceIds.every(id => providerInstruction.includes(id)) });
|
|
1851
|
+
});
|
|
1644
1852
|
binding.lastReceiptDigest = qaDigest("turn-receipt", receipt);
|
|
1853
|
+
let graphRequest = false;
|
|
1854
|
+
try {
|
|
1855
|
+
graphRequest = Boolean(parseGraphRequest(result.cleanedResponse ?? result.text));
|
|
1856
|
+
}
|
|
1857
|
+
catch {
|
|
1858
|
+
graphRequest = true;
|
|
1859
|
+
}
|
|
1860
|
+
if (!slot?.startsWith("session-") && !graphRequest)
|
|
1861
|
+
binding.lastBusinessReceiptDigest = binding.lastReceiptDigest;
|
|
1862
|
+
binding.lastResponse = result.text;
|
|
1645
1863
|
if (!terminalEventObserved) {
|
|
1646
1864
|
const head = receiptDb.qaTicketHead(binding.runId, binding.ticketId);
|
|
1647
1865
|
receiptDb.transitionQa(binding.runId, binding.ticketId, head.revision, { type: "turn-uncertain" });
|
|
@@ -1702,35 +1920,61 @@ function finishV2Failure(opts, adapter, reportDigest, report, rawFindingIds, fin
|
|
|
1702
1920
|
}
|
|
1703
1921
|
function finishV2Pass(opts, adapter, detail) {
|
|
1704
1922
|
const binding = qaTurnBindings.get(adapter);
|
|
1705
|
-
if (!binding?.
|
|
1706
|
-
throw new Error("QA pass has no completed
|
|
1923
|
+
if (!binding?.lastBusinessReceiptDigest)
|
|
1924
|
+
throw new Error("QA pass has no completed business-verdict receipt");
|
|
1707
1925
|
const db = new WorkflowDb(binding.projectDir);
|
|
1708
1926
|
try {
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1927
|
+
return db.atomic(() => {
|
|
1928
|
+
const head = db.qaTicketHead(binding.runId, binding.ticketId);
|
|
1929
|
+
let contractCoverage;
|
|
1930
|
+
if (db.qaPreparationStore().policy(binding.runId)?.mode === "enforce") {
|
|
1931
|
+
const admission = db.assertAdmittedWork(binding.runId, binding.ticketId);
|
|
1932
|
+
const contractHead = db.qaPreparationStore().head(binding.runId, binding.ticketId, admission.requirementsDigest);
|
|
1933
|
+
if (!contractHead.digest || contractHead.state !== "ready")
|
|
1934
|
+
throw new Error("Final coverage requires the current ready contract");
|
|
1935
|
+
const contract = db.qaPreparationStore().contract(contractHead.digest);
|
|
1936
|
+
const coverage = parseContractCoverage(binding.lastResponse ?? "");
|
|
1937
|
+
const errors = validateContractCoverage(contract, coverage, { phase: "qa", sourceDigest: binding.sourceStateDigest, inputBasisDigest: binding.reviewBasisDigest, attemptId: binding.reviewAttemptId, sessionId: binding.sessionRef.sessionId });
|
|
1938
|
+
if (errors.length)
|
|
1939
|
+
throw new Error(`Final QA coverage incomplete: ${errors.join("; ")}`);
|
|
1940
|
+
const coverageDigest = db.qaPreparationStore().putArtifact("final-coverage", coverage);
|
|
1941
|
+
contractCoverage = { version: 1, contractDigest: contract.contentDigest, revision: contract.revision, coverageDigest, attemptId: binding.reviewAttemptId, sessionId: binding.sessionRef.sessionId };
|
|
1942
|
+
}
|
|
1943
|
+
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);
|
|
1944
|
+
db.verifyGuidance(binding.guidanceIds, certificate, binding.lastResponse ?? "");
|
|
1945
|
+
db.verifyBuilderGuidance(binding.verificationIds, certificate, binding.lastResponse ?? "", binding.lastBusinessReceiptDigest);
|
|
1946
|
+
return { certificateId: certificate.certificateId, sourceStateDigest: binding.sourceStateDigest, reviewBasisDigest: binding.reviewBasisDigest };
|
|
1947
|
+
});
|
|
1712
1948
|
}
|
|
1713
1949
|
finally {
|
|
1714
1950
|
db.close();
|
|
1715
1951
|
}
|
|
1716
1952
|
}
|
|
1717
|
-
function waiveV2Reports(opts, reason) {
|
|
1953
|
+
function waiveV2Reports(opts, reason, authorizationRef) {
|
|
1718
1954
|
const db = new WorkflowDb(opts.recovery.projectDir);
|
|
1719
1955
|
try {
|
|
1720
1956
|
const head = db.qaTicketHead(opts.recovery.runId, opts.ticket.id);
|
|
1721
|
-
db.commitQaWaiver(opts.recovery.runId, opts.ticket.id, head.revision, reason);
|
|
1957
|
+
db.commitQaWaiver(opts.recovery.runId, opts.ticket.id, head.revision, reason, undefined, authorizationRef);
|
|
1722
1958
|
}
|
|
1723
1959
|
finally {
|
|
1724
1960
|
db.close();
|
|
1725
1961
|
}
|
|
1726
1962
|
}
|
|
1727
1963
|
export async function beginQaFinalization(projectDir, sourceWorktree, runId, ticketId, certificateId, expectedSourceStateDigest, consumer, allowedProjectionPaths = []) {
|
|
1964
|
+
assertBuildAssignmentReconciled(projectDir, runId);
|
|
1965
|
+
const controlDb = new WorkflowDb(projectDir);
|
|
1966
|
+
try {
|
|
1967
|
+
controlDb.assertFinalizationControls(runId, ticketId);
|
|
1968
|
+
}
|
|
1969
|
+
finally {
|
|
1970
|
+
controlDb.close();
|
|
1971
|
+
}
|
|
1728
1972
|
const invalidatePass = (reason) => {
|
|
1729
1973
|
const db = new WorkflowDb(projectDir);
|
|
1730
1974
|
try {
|
|
1731
1975
|
const head = db.qaTicketHead(runId, ticketId);
|
|
1732
1976
|
const paused = db.invalidateQaPassBeforeFinalization({ runId, ticketId, certificateId, expectedSourceStateDigest, expectedRevision: head.revision, reason });
|
|
1733
|
-
throw new Error(`${reason}; a complete QA recheck is required. Resume with:
|
|
1977
|
+
throw new Error(`${reason}; a complete QA recheck is required. Resume with: ${formatRecoveryCommand(projectDir)}`);
|
|
1734
1978
|
}
|
|
1735
1979
|
finally {
|
|
1736
1980
|
db.close();
|
|
@@ -1766,6 +2010,14 @@ export async function beginQaFinalization(projectDir, sourceWorktree, runId, tic
|
|
|
1766
2010
|
}
|
|
1767
2011
|
}
|
|
1768
2012
|
export async function verifyPendingQaFinalizationSource(projectDir, sourceWorktree, runId, ticketId, allowProjection = false, projectionOperationId) {
|
|
2013
|
+
assertBuildAssignmentReconciled(projectDir, runId);
|
|
2014
|
+
const controlDb = new WorkflowDb(projectDir);
|
|
2015
|
+
try {
|
|
2016
|
+
controlDb.assertFinalizationControls(runId, ticketId);
|
|
2017
|
+
}
|
|
2018
|
+
finally {
|
|
2019
|
+
controlDb.close();
|
|
2020
|
+
}
|
|
1769
2021
|
const db = new WorkflowDb(projectDir);
|
|
1770
2022
|
let step;
|
|
1771
2023
|
let projectionAuthorized = !projectionOperationId;
|
|
@@ -1797,7 +2049,7 @@ export async function verifyPendingQaFinalizationSource(projectDir, sourceWorktr
|
|
|
1797
2049
|
const branch = execFileSync("git", ["branch", "--show-current"], { cwd: sourceWorktree, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
1798
2050
|
const head = invalidationDb.qaTicketHead(runId, ticketId);
|
|
1799
2051
|
const paused = invalidationDb.invalidateQaFinalization(runId, ticketId, head.revision, reason, branch);
|
|
1800
|
-
throw new Error(`${reason}; a complete QA recheck is required. Resume with:
|
|
2052
|
+
throw new Error(`${reason}; a complete QA recheck is required. Resume with: ${formatRecoveryCommand(projectDir)}`);
|
|
1801
2053
|
}
|
|
1802
2054
|
finally {
|
|
1803
2055
|
invalidationDb.close();
|