ai-foreman 1.7.20 → 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 +12 -2
- 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 +90 -4
- package/dist/buildAdmission.js +2 -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/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 +6 -0
- package/dist/buildRuns.js +23 -17
- 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.js +28 -8
- package/dist/cli/start.js +152 -48
- package/dist/cli/status.js +20 -5
- package/dist/cli/tickets.js +9 -7
- package/dist/continuity.d.ts +11 -0
- package/dist/continuity.js +22 -6
- package/dist/foreman.d.ts +10 -2
- package/dist/foreman.js +330 -39
- 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 -11
- package/dist/index.js +2 -0
- 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.js +7 -0
- 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 +234 -24
- 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.js +12 -5
- package/dist/recoveryGuidance.d.ts +9 -0
- package/dist/recoveryGuidance.js +13 -0
- package/dist/runtimeAuth.d.ts +1 -0
- package/dist/runtimeAuth.js +5 -1
- package/dist/runtimeReadiness.d.ts +1 -0
- package/dist/runtimeReadiness.js +13 -1
- 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 +50 -1
- package/dist/statusReporter.d.ts +1 -0
- package/dist/statusReporter.js +1 -0
- package/dist/supervisedStart.js +18 -8
- 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/workflowDb.d.ts +51 -5
- package/dist/workflowDb.js +338 -7
- package/dist/workflowReader.d.ts +109 -2
- package/dist/workflowReader.js +48 -4
- package/package.json +84 -7
- package/scripts/copy-graph-assets.mjs +3 -0
- package/scripts/graph-benchmark.mjs +20 -0
- package/scripts/graph-evaluation.mjs +85 -0
package/dist/workflowDb.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import { registerQaGraphJournalWriter, ensureQaGraphJournalGuards } from "./qaDeliveryJournal.js";
|
|
2
|
+
import { canonicalJson } from "./qaProtocolV2.js";
|
|
3
|
+
import { resolveEffectiveQaConfiguration } from "./qaEffectiveConfig.js";
|
|
4
|
+
import { registerGraphDerived, graphDerivedAllowed, currentGraphDerivedAccess } from "./graph/derived.js";
|
|
5
|
+
import { parseContractCoverage, validateContractCoverage } from "./qaContractCoverage.js";
|
|
6
|
+
import { QaPreparationStore, migrateQaPreparation, migrateQaPreparationGuards } from "./qaPreparationStore.js";
|
|
7
|
+
import { GraphStore, migrateGraphStore } from "./graph/storage.js";
|
|
8
|
+
import { reconcileWork } from "./buildWorkReconciliation.js";
|
|
9
|
+
import { migrateBuildInterventions, hasQueuedBuilderGuidance, reconcileGuidance, decisionWorkId, queuedControls, completeControl, verifyGuidance, builderVerificationContext, verifyBuilderGuidance, assertFinalizationControls, reserveGuidance, finishGuidance, instruction } from "./buildInterventions.js";
|
|
10
|
+
import { migrateBuildWork } from "./buildWorkMigration.js";
|
|
11
|
+
import { admitWork, admittedWork, assertAdmittedWork } from "./buildWorkAdmission.js";
|
|
1
12
|
import { cleanupReadiness, inspectReadiness, readinessMetadata } from "./readinessCleanup.js";
|
|
2
13
|
import { originalBuildLease, rememberOriginalLease, forgetOriginalLease, registerLaunchChild, acknowledgeLaunchChild, reconcileLaunch, checkBuildOwnershipSchema, canonicalProject, launchDigest, localBuildAuthority, rememberBuildAuthority, forgetBuildAuthority, migrateBuildAdmission, acquireAdmission, readAdmission, assertAdmission, releaseAdmission, reserveLaunch, setLaunchState, claimLaunch, readLaunch } from "./buildAdmission.js";
|
|
3
14
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -5,6 +16,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statS
|
|
|
5
16
|
import { hostname } from "node:os";
|
|
6
17
|
import { dirname, join, resolve } from "node:path";
|
|
7
18
|
import Database from "better-sqlite3";
|
|
19
|
+
import { validateQaFailureReport } from "rafi-spec";
|
|
8
20
|
import { migrateQaHandback, registerHandbackWriter, reportOccurrenceId } from "./qaHandbackMigration.js";
|
|
9
21
|
import { providerSessionKey } from "./sessionIdentity.js";
|
|
10
22
|
import { taggedProcesses, classifyProcess, processStartIdentity } from "./processIdentity.js";
|
|
@@ -60,6 +72,7 @@ export class WorkflowDb {
|
|
|
60
72
|
writerLease;
|
|
61
73
|
coordinatorTransition = 0;
|
|
62
74
|
lineageTransition = 0;
|
|
75
|
+
workAuthority = 0;
|
|
63
76
|
constructor(projectDir, path = join(resolve(projectDir), WORKFLOW_DB_FILE), readinessAccess) {
|
|
64
77
|
this.projectDir = projectDir;
|
|
65
78
|
this.readinessAccess = readinessAccess;
|
|
@@ -85,7 +98,9 @@ export class WorkflowDb {
|
|
|
85
98
|
this.db.function("rafi_build_lease_generation", () => this.writerLease?.generation ?? -1);
|
|
86
99
|
this.db.function("rafi_build_writer_token", () => this.writerAuthority?.token ?? "");
|
|
87
100
|
this.db.function("rafi_build_writer_run", () => this.writerAuthority?.runId ?? "");
|
|
101
|
+
this.db.function("rafi_work_authority", () => this.workAuthority);
|
|
88
102
|
registerHandbackWriter(this.db);
|
|
103
|
+
registerQaGraphJournalWriter(this.db);
|
|
89
104
|
if (!readinessAccess)
|
|
90
105
|
this.db.pragma("journal_mode = WAL");
|
|
91
106
|
this.db.pragma("synchronous = FULL");
|
|
@@ -95,8 +110,12 @@ export class WorkflowDb {
|
|
|
95
110
|
this.restrictReadinessConnection(readinessAccess);
|
|
96
111
|
else {
|
|
97
112
|
this.migrate();
|
|
113
|
+
migrateQaPreparation(this.db);
|
|
98
114
|
migrateBuildAdmission(this.db);
|
|
99
115
|
this.importLegacyOnce();
|
|
116
|
+
migrateBuildWork(this.db, projectDir);
|
|
117
|
+
migrateBuildInterventions(this.db);
|
|
118
|
+
migrateQaPreparationGuards(this.db);
|
|
100
119
|
}
|
|
101
120
|
}
|
|
102
121
|
catch (error) {
|
|
@@ -119,6 +138,155 @@ export class WorkflowDb {
|
|
|
119
138
|
}
|
|
120
139
|
}
|
|
121
140
|
close() { this.db.close(); }
|
|
141
|
+
qaPreparationStore() { return new QaPreparationStore(this.db, runId => this.assertInstructionOwner(runId), this.projectDir); }
|
|
142
|
+
graphStore() { migrateGraphStore(this.db); return new GraphStore(this.db); }
|
|
143
|
+
admitWork(input) {
|
|
144
|
+
this.assertInstructionOwner(input.runId);
|
|
145
|
+
this.workAuthority++;
|
|
146
|
+
try {
|
|
147
|
+
const result = admitWork(this.db, input);
|
|
148
|
+
this.qaPreparationStore().metric(input.runId, result.workId, result.requirementsDigest, `metric:approved:${input.runId}:${result.workId}:${result.requirementsDigest}`, "approved");
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
this.workAuthority--;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
assertInstructionOwner(runId) {
|
|
156
|
+
if (this.writerAuthority) {
|
|
157
|
+
assertAdmission(this.db, this.writerAuthority);
|
|
158
|
+
if (this.writerAuthority.runId !== runId && !this.db.prepare("SELECT 1 FROM build_child_runs WHERE child=? AND parent=?").get(runId, this.writerAuthority.runId))
|
|
159
|
+
throw new Error("Instruction execution authority belongs to another run");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (this.buildAdmission())
|
|
163
|
+
throw new Error("Instruction consumption requires the original build owner");
|
|
164
|
+
if (this.db.prepare("SELECT 1 FROM build_runtime_runs WHERE run_id=?").get(runId)) {
|
|
165
|
+
const held = this.currentLease();
|
|
166
|
+
if (!held || !this.writerLease || held.owner !== this.writerLease.owner || held.generation !== this.writerLease.generation || held.runId !== runId)
|
|
167
|
+
throw new Error("Instruction execution requires the original workflow lease");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
reserveGuidance(runId, workId, recipient, operationId, sourceDigest, text, reserve = true) {
|
|
171
|
+
this.assertInstructionOwner(runId);
|
|
172
|
+
return reserveGuidance(this.db, runId, workId, recipient, operationId, sourceDigest, text, reserve);
|
|
173
|
+
}
|
|
174
|
+
reconcileInstructionDeliveries(runId, workId) { this.assertInstructionOwner(runId); this.atomic(() => reconcileGuidance(this.db, runId, workId)); }
|
|
175
|
+
decisionWorkId(decision) { return decisionWorkId(this.db, decision); }
|
|
176
|
+
finishGuidance(ids, recipient, result) { for (const id of ids)
|
|
177
|
+
this.assertInstructionOwner(instruction(this.db, id).request.runId); finishGuidance(this.db, ids, recipient, result); }
|
|
178
|
+
builderVerificationContext(runId, workId, sourceDigest) { return builderVerificationContext(this.db, runId, workId, sourceDigest); }
|
|
179
|
+
verifyBuilderGuidance(ids, certificate, response, receiptDigest) { for (const id of ids)
|
|
180
|
+
this.assertInstructionOwner(instruction(this.db, id).request.runId); return this.atomic(() => verifyBuilderGuidance(this.db, ids, certificate, response, receiptDigest)); }
|
|
181
|
+
verifyGuidance(ids, certificate, response) { for (const id of ids)
|
|
182
|
+
this.assertInstructionOwner(instruction(this.db, id).request.runId); return this.atomic(() => verifyGuidance(this.db, ids, certificate, response)); }
|
|
183
|
+
hasQueuedBuilderGuidance(runId, workId) { return hasQueuedBuilderGuidance(this.db, runId, workId); }
|
|
184
|
+
instruction(id) { return instruction(this.db, id); }
|
|
185
|
+
assertFinalizationControls(runId, workId) { assertFinalizationControls(this.db, runId, workId); }
|
|
186
|
+
consumeInstructionControls(runId, workId, kind = "questions") {
|
|
187
|
+
return this.atomic(() => {
|
|
188
|
+
this.assertInstructionOwner(runId);
|
|
189
|
+
const admission = this.assertAdmittedWork(runId, workId);
|
|
190
|
+
const latestReview = this.qaReviewAttempts(runId, workId).at(-1);
|
|
191
|
+
let authorization = kind === "attempts" && latestReview && ["failed", "passed"].includes(latestReview.status) ? this.db.prepare("SELECT a.authorization_id FROM qa_remediation_authorizations a JOIN build_instruction_deliveries d ON json_extract(d.record_json,'$.receipt.authorizationId')=a.authorization_id JOIN build_instructions i USING(instruction_id) WHERE a.run_id=? AND a.ticket_id=? AND a.review_attempt_id=? AND a.consumed_by IS NULL AND d.state='applied' AND json_extract(i.record_json,'$.requirementsDigest')=? AND json_extract(i.record_json,'$.basis.assignmentId')=? AND json_extract(i.record_json,'$.basis.scopeRevision')=? ORDER BY i.sequence LIMIT 1").get(runId, workId, latestReview.attemptId, admission.requirementsDigest, admission.assignmentId, admission.scopeRevision)?.authorization_id : undefined;
|
|
192
|
+
if (authorization) {
|
|
193
|
+
const head = this.qaTicketHead(runId, workId);
|
|
194
|
+
const eligible = latestReview?.status === "passed"
|
|
195
|
+
? head.state === "passed" && this.hasQueuedBuilderGuidance(runId, workId)
|
|
196
|
+
: head.state === "review-failed" && this.unresolvedQaReports(runId, workId).some(report => report.reviewNumber === latestReview.reviewNumber && report.reportDigest === latestReview.reportDigest);
|
|
197
|
+
if (!eligible)
|
|
198
|
+
authorization = undefined;
|
|
199
|
+
}
|
|
200
|
+
for (const record of queuedControls(this.db, runId, workId).filter(record => kind === "questions" ? record.request.action === "answer_question" : record.request.action === "request_attempt")) {
|
|
201
|
+
const priorAuthorization = authorization;
|
|
202
|
+
try {
|
|
203
|
+
this.atomic(() => {
|
|
204
|
+
if (record.requirementsDigest !== admission.requirementsDigest)
|
|
205
|
+
throw new Error("Instruction requirements are stale");
|
|
206
|
+
if (record.request.action === "answer_question") {
|
|
207
|
+
const decision = this.humanDecision(record.request.decisionId);
|
|
208
|
+
if (!decision || decision.runId !== runId || this.decisionWorkId(decision) !== workId || createHash("sha256").update(json(decision)).digest("hex") !== record.request.decisionRevision)
|
|
209
|
+
throw new Error("Pending question identity or revision changed");
|
|
210
|
+
const choice = decision.choices.find(choice => choice.id === record.request.text || choice.label === record.request.text) ?? decision.choices.find(choice => choice.id === "custom" || choice.id === "answer");
|
|
211
|
+
if (!choice)
|
|
212
|
+
throw new Error("Answer must name an offered choice or use a question accepting custom text");
|
|
213
|
+
const answer = this.answerHumanDecision(runId, decision.decisionId, choice.id, new Date(), choice.id === "custom" || choice.id === "answer" ? record.request.text : undefined);
|
|
214
|
+
completeControl(this.db, record.instructionId, { decisionId: answer.decisionId, status: answer.status });
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
const latest = this.qaReviewAttempts(runId, workId).at(-1);
|
|
218
|
+
if (!latest || !["failed", "passed"].includes(latest.status) || latest.attemptId !== record.basis.reviewAttemptId || authorization)
|
|
219
|
+
throw new Error("One extra attempt requires the current eligible review; another authorization is already available");
|
|
220
|
+
if (record.basis.assignmentId !== admission.assignmentId || record.basis.scopeRevision !== admission.scopeRevision)
|
|
221
|
+
throw new Error("Attempt authorization scope changed");
|
|
222
|
+
const head = this.qaTicketHead(runId, workId);
|
|
223
|
+
if (latest.status === "passed") {
|
|
224
|
+
if (head.state !== "passed" || !this.hasQueuedBuilderGuidance(runId, workId))
|
|
225
|
+
throw new Error("Follow-up authorization requires waiting Builder guidance on the current unfinalized pass");
|
|
226
|
+
authorization = this.authorizeQaRemediation(runId, workId, latest.attemptId, record.request.text);
|
|
227
|
+
completeControl(this.db, record.instructionId, { authorizationId: authorization, reviewAttemptId: latest.attemptId, consumed: false, kind: "builder-guidance-followup" });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const report = this.unresolvedQaReports(runId, workId).find(report => report.reviewNumber === latest.reviewNumber && report.reportDigest === latest.reportDigest && report.sourceStateDigest === latest.sourceDigest && report.reviewBasisDigest === head.reviewBasisDigest);
|
|
231
|
+
if (!report || !validateQaFailureReport(report.report).valid || head.reviewNumber !== latest.reviewNumber || head.sourceStateDigest !== latest.sourceDigest || !["review-failed", "operator-menu"].includes(head.state) || this.qaRemediationAttempts(runId, workId).some(attempt => attempt.reviewAttemptId === latest.attemptId))
|
|
232
|
+
throw new Error("Extra attempt requires an unconsumed validated failed-review basis; reconcile uncertain execution separately");
|
|
233
|
+
const decisions = this.db.prepare("SELECT decision_id,status,decision_json FROM human_decisions WHERE run_id=? AND decision_key LIKE ?").all(runId, `${runId}:qa-nonconvergence:%`).filter(row => this.decisionWorkId(parseJson(row.decision_json)) === workId && parseJson(row.decision_json).createdAt >= latest.createdAt);
|
|
234
|
+
if (decisions.some(row => row.status === "answered"))
|
|
235
|
+
throw new Error("The nonconvergence decision was already answered; resume that decision before requesting another attempt");
|
|
236
|
+
authorization = this.authorizeQaRemediation(runId, workId, latest.attemptId, record.request.text);
|
|
237
|
+
for (const row of decisions.filter(row => row.status === "pending")) {
|
|
238
|
+
const decision = parseJson(row.decision_json);
|
|
239
|
+
const at = new Date().toISOString();
|
|
240
|
+
this.db.prepare("UPDATE human_decisions SET status='cancelled',decision_json=?,updated_at=? WHERE decision_id=? AND status='pending'").run(json({ ...decision, status: "cancelled", cancellationReason: `Superseded by scoped attempt instruction ${record.instructionId}`, cancelledAt: at }), at, row.decision_id);
|
|
241
|
+
this.insertEvent(runId, "human_decision_cancelled", "attempt-authorized", { decisionId: row.decision_id, instructionId: record.instructionId }, at);
|
|
242
|
+
}
|
|
243
|
+
if (head.state === "operator-menu")
|
|
244
|
+
this.transitionQa(runId, workId, head.revision, { type: "failed-review-restored", reviewNumber: latest.reviewNumber, sourceStateDigest: latest.sourceDigest, reportDigest: report.reportDigest });
|
|
245
|
+
completeControl(this.db, record.instructionId, { authorizationId: authorization, reviewAttemptId: latest.attemptId, consumed: false });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
authorization = priorAuthorization;
|
|
251
|
+
completeControl(this.db, record.instructionId, undefined, String(error));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return authorization;
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
reconcileWork(request, input) {
|
|
258
|
+
this.assertInstructionOwner(request.runId);
|
|
259
|
+
if (this.writerAuthority)
|
|
260
|
+
assertAdmission(this.db, this.writerAuthority);
|
|
261
|
+
this.workAuthority++;
|
|
262
|
+
try {
|
|
263
|
+
return this.atomic(() => reconcileWork(this.db, request, input));
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
this.workAuthority--;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
reconciliation(id) {
|
|
270
|
+
const row = this.db.prepare("SELECT record_json FROM build_reconciliations WHERE reconciliation_id=?").get(id);
|
|
271
|
+
return row ? JSON.parse(row.record_json) : undefined;
|
|
272
|
+
}
|
|
273
|
+
workDefinitions(runId) {
|
|
274
|
+
return this.db.prepare("SELECT work_id,kind,definition_json FROM build_work_scope WHERE run_id=? AND state='admitted' ORDER BY rowid").all(runId).map(row => ({ workId: row.work_id, kind: row.kind, definition: JSON.parse(row.definition_json) }));
|
|
275
|
+
}
|
|
276
|
+
recordWorkAssignment(runId, workId, operationId, record) {
|
|
277
|
+
this.assertInstructionOwner(runId);
|
|
278
|
+
const admission = this.assertAdmittedWork(runId, workId);
|
|
279
|
+
this.qaPreparationStore().metric(runId, workId, admission.requirementsDigest, `metric:started:${runId}:${workId}:${admission.requirementsDigest}`, "implementation-started");
|
|
280
|
+
this.workAuthority++;
|
|
281
|
+
try {
|
|
282
|
+
this.db.prepare("INSERT INTO build_assignments VALUES(?,?,?,?,?)").run(operationId, runId, workId, operationId, json(record));
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
this.workAuthority--;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
admittedWork(runId, workId) { return admittedWork(this.db, runId, workId); }
|
|
289
|
+
assertAdmittedWork(runId, workId) { return assertAdmittedWork(this.db, runId, workId); }
|
|
122
290
|
/** Atomic local changes only; never await provider or filesystem work here. */
|
|
123
291
|
atomic(work) { return this.db.transaction(work).immediate(); }
|
|
124
292
|
buildAdmission() { return readAdmission(this.db); }
|
|
@@ -685,6 +853,9 @@ export class WorkflowDb {
|
|
|
685
853
|
this.ensureRun(input.runId, "build", now);
|
|
686
854
|
const at = now.toISOString();
|
|
687
855
|
const safePayload = sanitizeContinuityValue(input.payload);
|
|
856
|
+
const graphAccess = currentGraphDerivedAccess();
|
|
857
|
+
if (graphAccess)
|
|
858
|
+
registerGraphDerived(this.db, json(safePayload), graphAccess);
|
|
688
859
|
const session = sessionParts(input.sessionRef, undefined, input.sessionKey);
|
|
689
860
|
const digest = digestJson({ runId: input.runId, role: input.role, kind: input.kind, payload: safePayload, authoritativeStateRevision: input.authoritativeStateRevision, sessionKey: session.key, at });
|
|
690
861
|
const result = this.db.prepare(`INSERT INTO continuity_events(run_id,role,kind,payload_json,digest,authoritative_state_revision,session_key,session_ref_json,created_at)
|
|
@@ -693,6 +864,8 @@ export class WorkflowDb {
|
|
|
693
864
|
}
|
|
694
865
|
continuityEvents(runId, afterSequence = 0) {
|
|
695
866
|
const rows = this.db.prepare("SELECT * FROM continuity_events WHERE run_id=? AND sequence>? ORDER BY sequence").all(runId, afterSequence);
|
|
867
|
+
for (const row of rows)
|
|
868
|
+
this.assertGraphContinuityAccess(row.payload_json);
|
|
696
869
|
return rows.map(continuityEventFromRow);
|
|
697
870
|
}
|
|
698
871
|
publishContinuityCheckpoint(input, now = new Date()) {
|
|
@@ -702,6 +875,9 @@ export class WorkflowDb {
|
|
|
702
875
|
const previous = this.continuityHead(input.runId, input.role);
|
|
703
876
|
const latestEvent = this.db.prepare("SELECT COALESCE(MAX(sequence),0) AS sequence FROM continuity_events WHERE run_id=?").get(input.runId);
|
|
704
877
|
const safeDelta = sanitizeContinuityValue(input.delta);
|
|
878
|
+
const graphAccess = currentGraphDerivedAccess();
|
|
879
|
+
if (graphAccess)
|
|
880
|
+
registerGraphDerived(this.db, json(safeDelta), graphAccess);
|
|
705
881
|
const session = sessionParts(input.sessionRef, undefined, input.sessionKey);
|
|
706
882
|
const digest = digestJson({ runId: input.runId, role: input.role, sequence: latestEvent.sequence, predecessorDigest: previous?.digest, delta: safeDelta, authoritativeStateRevision: input.authoritativeStateRevision, sessionKey: session.key });
|
|
707
883
|
const result = this.db.prepare(`INSERT INTO continuity_checkpoints(run_id,role,event_sequence,state,delta_json,digest,predecessor_digest,authoritative_state_revision,session_key,session_ref_json,created_at)
|
|
@@ -723,12 +899,21 @@ export class WorkflowDb {
|
|
|
723
899
|
const rows = role
|
|
724
900
|
? this.db.prepare("SELECT * FROM continuity_checkpoints WHERE run_id=? AND role=? ORDER BY checkpoint_id").all(runId, role)
|
|
725
901
|
: this.db.prepare("SELECT * FROM continuity_checkpoints WHERE run_id=? ORDER BY checkpoint_id").all(runId);
|
|
902
|
+
for (const row of rows)
|
|
903
|
+
this.assertGraphContinuityAccess(row.delta_json);
|
|
726
904
|
return rows.map(continuityCheckpointFromRow);
|
|
727
905
|
}
|
|
728
906
|
latestContinuityCheckpoint(runId, role) {
|
|
729
907
|
const row = this.db.prepare("SELECT * FROM continuity_checkpoints WHERE run_id=? AND role=? ORDER BY checkpoint_id DESC LIMIT 1").get(runId, role);
|
|
908
|
+
if (row)
|
|
909
|
+
this.assertGraphContinuityAccess(row.delta_json);
|
|
730
910
|
return row ? continuityCheckpointFromRow(row) : undefined;
|
|
731
911
|
}
|
|
912
|
+
assertGraphContinuityAccess(value) {
|
|
913
|
+
const digest = createHash("sha256").update(value).digest("hex");
|
|
914
|
+
if (!graphDerivedAllowed(this.db, this.projectDir, digest))
|
|
915
|
+
throw new Error("Continuity contains revoked graph-derived evidence; explicit source-based recovery is required");
|
|
916
|
+
}
|
|
732
917
|
continuityHead(runId, role) {
|
|
733
918
|
const row = this.db.prepare("SELECT * FROM continuity_heads WHERE run_id=? AND role=?").get(runId, role);
|
|
734
919
|
return row ? continuityHeadFromRow(row) : undefined;
|
|
@@ -858,6 +1043,18 @@ export class WorkflowDb {
|
|
|
858
1043
|
})();
|
|
859
1044
|
return decision;
|
|
860
1045
|
}
|
|
1046
|
+
cancelPendingHumanDecisions(runId, reason, now = new Date()) {
|
|
1047
|
+
this.atomic(() => {
|
|
1048
|
+
for (const decision of this.pendingHumanDecisions(runId)) {
|
|
1049
|
+
const next = { ...decision, status: "cancelled", cancellationReason: reason, cancelledAt: now.toISOString() };
|
|
1050
|
+
this.db.prepare("UPDATE human_decisions SET status='cancelled',decision_json=?,updated_at=? WHERE decision_id=? AND status='pending'").run(json(next), now.toISOString(), decision.decisionId);
|
|
1051
|
+
this.insertEvent(runId, "human_decision_cancelled", "cancelled", { decisionId: decision.decisionId, reason }, now.toISOString());
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
humanDecisionKey(decisionId) {
|
|
1056
|
+
return this.db.prepare("SELECT decision_key FROM human_decisions WHERE decision_id=?").get(decisionId)?.decision_key;
|
|
1057
|
+
}
|
|
861
1058
|
humanDecision(decisionId) {
|
|
862
1059
|
const row = this.db.prepare("SELECT decision_json FROM human_decisions WHERE decision_id=?").get(decisionId);
|
|
863
1060
|
return row ? parseJson(row.decision_json) : undefined;
|
|
@@ -866,21 +1063,39 @@ export class WorkflowDb {
|
|
|
866
1063
|
return this.db.prepare("SELECT decision_json FROM human_decisions WHERE run_id=? AND status='pending' ORDER BY created_at").all(runId)
|
|
867
1064
|
.map((row) => parseJson(row.decision_json));
|
|
868
1065
|
}
|
|
1066
|
+
decisionContinuationAvailable(runId, decisionId) {
|
|
1067
|
+
const prior = this.operations(runId).filter(op => op.kind === "decision-continuation" && op.intent.decisionId === decisionId);
|
|
1068
|
+
return prior.every(op => op.status === "failed" && op.result?.dispatchState === "not-sent");
|
|
1069
|
+
}
|
|
1070
|
+
nextDecisionContinuationKey(runId, decisionId) {
|
|
1071
|
+
if (!this.decisionContinuationAvailable(runId, decisionId))
|
|
1072
|
+
throw new Error("Decision continuation requires reconciliation before replay");
|
|
1073
|
+
const count = this.operations(runId).filter(op => op.kind === "decision-continuation" && op.intent.decisionId === decisionId).length;
|
|
1074
|
+
return `decision-continuation:${decisionId}${count ? `:retry-${count}` : ""}`;
|
|
1075
|
+
}
|
|
869
1076
|
answeredTicketDecisions(runId, scopeRevision) {
|
|
870
1077
|
return this.db.prepare("SELECT decision_key,decision_json FROM human_decisions WHERE run_id=? AND status='answered'").all(runId)
|
|
871
1078
|
.filter(row => row.decision_key.includes(`:${scopeRevision}:`))
|
|
872
1079
|
.map(row => parseJson(row.decision_json))
|
|
873
|
-
.filter(decision => decision.interruptionId.startsWith("ticket:") &&
|
|
1080
|
+
.filter(decision => decision.interruptionId.startsWith("ticket:") && this.decisionContinuationAvailable(runId, decision.decisionId) && !this.operation(`decision-supersession:${decision.decisionId}`));
|
|
874
1081
|
}
|
|
875
1082
|
/** Unconsumed answers to an older definition must not silently reopen a ticket. */
|
|
876
1083
|
staleTicketDecisions(runId, scopeRevision) {
|
|
877
1084
|
return this.db.prepare("SELECT decision_key,decision_json FROM human_decisions WHERE run_id=? AND status='answered'").all(runId)
|
|
878
1085
|
.filter(row => !row.decision_key.includes(`:${scopeRevision}:`))
|
|
879
1086
|
.map(row => parseJson(row.decision_json))
|
|
880
|
-
.filter(decision => decision.interruptionId.startsWith("ticket:") &&
|
|
1087
|
+
.filter(decision => decision.interruptionId.startsWith("ticket:") && this.decisionContinuationAvailable(runId, decision.decisionId) && !this.operation(`decision-supersession:${decision.decisionId}`));
|
|
881
1088
|
}
|
|
882
1089
|
refreshStaleTicketDecisions(runId, scopeRevision, tickets) {
|
|
883
1090
|
this.atomic(() => {
|
|
1091
|
+
for (const prior of this.pendingHumanDecisions(runId)) {
|
|
1092
|
+
const key = this.humanDecisionKey(prior.decisionId) ?? "";
|
|
1093
|
+
if (!prior.interruptionId.startsWith("ticket:") || tickets && !tickets.includes(prior.interruptionId.slice(7)) || key.includes(`:${scopeRevision}:`) || !(key.startsWith(`${runId}:builder:`) || key.startsWith(`${runId}:ticket-question:`)))
|
|
1094
|
+
continue;
|
|
1095
|
+
this.ensureHumanDecision({ runId, decisionKey: `${runId}:ticket-question:${scopeRevision}:supersedes:${prior.decisionId}`, interruptionId: prior.interruptionId, prompt: prior.prompt, choices: prior.choices, evidence: prior.evidence });
|
|
1096
|
+
this.db.prepare("UPDATE human_decisions SET status='cancelled',decision_json=?,updated_at=? WHERE decision_id=? AND status='pending'").run(json({ ...prior, status: "cancelled", cancellationReason: "ticket scope changed before the question was answered" }), new Date().toISOString(), prior.decisionId);
|
|
1097
|
+
this.insertEvent(runId, "human_decision_superseded", "question-revised", { decisionId: prior.decisionId, scopeRevision }, new Date().toISOString());
|
|
1098
|
+
}
|
|
884
1099
|
for (const prior of this.staleTicketDecisions(runId, scopeRevision)) {
|
|
885
1100
|
if (tickets && !tickets.includes(prior.interruptionId.slice(7)))
|
|
886
1101
|
continue;
|
|
@@ -962,6 +1177,12 @@ export class WorkflowDb {
|
|
|
962
1177
|
this.db.transaction(() => {
|
|
963
1178
|
this.db.prepare("UPDATE operation_journal SET status=?,result_json=?,external_id=?,error=?,updated_at=? WHERE idempotency_key=?")
|
|
964
1179
|
.run(status, details.result === undefined ? null : json(details.result), details.externalId ?? null, details.error ?? null, at, idempotencyKey);
|
|
1180
|
+
if (status === "confirmed" && ["ticket-complete", "synthetic-completion"].includes(prior.kind)) {
|
|
1181
|
+
const workId = prior.intent.workId ?? prior.intent.ticket ?? prior.intent.ticketId;
|
|
1182
|
+
const admission = workId && this.admittedWork(prior.runId, workId);
|
|
1183
|
+
if (admission)
|
|
1184
|
+
this.qaPreparationStore().metric(prior.runId, workId, admission.requirementsDigest, `metric:completed:${prior.runId}:${workId}`, "completed", { status: this.getRun(prior.runId)?.state?.qaEnabled === false ? "disabled" : undefined, at });
|
|
1185
|
+
}
|
|
965
1186
|
this.appendContinuityEvent({ runId: prior.runId, role: "host", kind: "operation_receipt", payload: { idempotencyKey, kind: prior.kind, status, externalId: details.externalId, error: details.error }, authoritativeStateRevision: this.continuityHead(prior.runId, "run")?.authoritativeStateRevision ?? 0 }, now);
|
|
966
1187
|
})();
|
|
967
1188
|
return this.operation(idempotencyKey);
|
|
@@ -995,9 +1216,15 @@ export class WorkflowDb {
|
|
|
995
1216
|
throw new Error(`${kind} evidence exceeds the durable ${maximum}-byte item limit`);
|
|
996
1217
|
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
997
1218
|
this.db.prepare("INSERT OR IGNORE INTO content_refs(digest,kind,content,created_at) VALUES(?,?,?,?)").run(digest, kind, bytes, now.toISOString());
|
|
1219
|
+
const graphAccess = currentGraphDerivedAccess();
|
|
1220
|
+
if (graphAccess && (kind === "qa" || kind === "handoff"))
|
|
1221
|
+
registerGraphDerived(this.db, bytes, graphAccess);
|
|
998
1222
|
return digest;
|
|
999
1223
|
}
|
|
1224
|
+
registerGraphEvidence(value, access) { registerGraphDerived(this.db, value, access); }
|
|
1000
1225
|
getEvidence(digest) {
|
|
1226
|
+
if (!graphDerivedAllowed(this.db, this.projectDir, digest))
|
|
1227
|
+
return undefined;
|
|
1001
1228
|
const row = this.db.prepare("SELECT content FROM content_refs WHERE digest=?").get(digest);
|
|
1002
1229
|
return row?.content;
|
|
1003
1230
|
}
|
|
@@ -1052,6 +1279,11 @@ export class WorkflowDb {
|
|
|
1052
1279
|
const next = { ...prior, ...patch, updatedAt: now.toISOString() };
|
|
1053
1280
|
this.db.prepare("UPDATE qa_review_attempts SET status=?,report_digest=?,record_json=?,updated_at=? WHERE attempt_id=?")
|
|
1054
1281
|
.run(next.status, next.reportDigest ?? null, json({ findingIds: next.findingIds, namespacedFindingIds: next.namespacedFindingIds, detail: next.detail }), next.updatedAt, attemptId);
|
|
1282
|
+
const admission = this.admittedWork(next.runId, next.ticketId);
|
|
1283
|
+
const turns = this.db.prepare("SELECT receipt_json FROM qa_turns WHERE run_id=? AND ticket_id=? AND review_number=? AND status='completed'").all(next.runId, next.ticketId, next.reviewNumber);
|
|
1284
|
+
const terminalBlock = turns.some(row => { const receipt = parseJson(row.receipt_json); const response = receipt.cleanedResponseDigest && this.getEvidence(receipt.cleanedResponseDigest); return receipt.terminalEventObserved && response && /STEP_STATUS:\s*(blocked|needs_input)\b/.test(response.toString()); });
|
|
1285
|
+
if (admission && (next.status === "passed" || next.status === "failed" || terminalBlock))
|
|
1286
|
+
this.qaPreparationStore().metric(next.runId, next.ticketId, admission.requirementsDigest, `metric:review:${attemptId}`, "substantive-review", { status: next.status === "passed" ? "passed" : next.status === "failed" ? "failed" : "blocked", at: now.toISOString(), evidenceRefs: next.reportDigest ? [next.reportDigest] : [] });
|
|
1055
1287
|
return next;
|
|
1056
1288
|
}
|
|
1057
1289
|
qaReviewAttempts(runId, ticketId) {
|
|
@@ -1107,6 +1339,8 @@ export class WorkflowDb {
|
|
|
1107
1339
|
.run(record.invocationId, record.runId, record.ticketId, qaDigest("handback-invocation", intent), json(intent), record.status, json(record), record.startedAt, record.completedAt ?? new Date().toISOString());
|
|
1108
1340
|
}
|
|
1109
1341
|
recordQaDeliveryTurn(turn) {
|
|
1342
|
+
if (turn.version === 4)
|
|
1343
|
+
ensureQaGraphJournalGuards(this.db);
|
|
1110
1344
|
const prior = this.qaDeliveryTurns(turn.operationId).find(t => t.turnRecordId === turn.turnRecordId);
|
|
1111
1345
|
if (prior) {
|
|
1112
1346
|
if (prior.status !== "intended") {
|
|
@@ -1146,7 +1380,8 @@ export class WorkflowDb {
|
|
|
1146
1380
|
if (linked.size !== recoveries.length)
|
|
1147
1381
|
throw new Error("QA remediation budget requires legacy attempt reconciliation");
|
|
1148
1382
|
const authorized = new Set(this.db.prepare("SELECT consumed_by FROM qa_remediation_authorizations WHERE run_id=? AND ticket_id=? AND consumed_by IS NOT NULL").all(runId, ticketId).map(r => r.consumed_by));
|
|
1149
|
-
|
|
1383
|
+
const followups = this.operations(runId).filter(operation => operation.kind === "build-assignment" && operation.intent.ticketId === ticketId && operation.intent.managerFollowup);
|
|
1384
|
+
return attempts.filter(a => !authorized.has(a.attemptId)).length + followups.filter(operation => !authorized.has(operation.idempotencyKey)).length;
|
|
1150
1385
|
}
|
|
1151
1386
|
authorizeQaRemediation(runId, ticketId, reviewAttemptId, reason) {
|
|
1152
1387
|
const authorizationId = randomUUID();
|
|
@@ -1397,6 +1632,12 @@ export class WorkflowDb {
|
|
|
1397
1632
|
return this.db.transaction(() => {
|
|
1398
1633
|
const predecessors = this.unresolvedQaReports(input.runId, input.ticketId);
|
|
1399
1634
|
const current = this.recordQaReport(input, findingIds, now);
|
|
1635
|
+
const admission = this.admittedWork(input.runId, input.ticketId), preparation = this.qaPreparationStore();
|
|
1636
|
+
if (admission && preparation.policy(input.runId)) {
|
|
1637
|
+
const evidence = preparation.putArtifact("finding-classification-input", { report: input, contractHead: preparation.head(input.runId, input.ticketId, admission.requirementsDigest), evidenceDisposition: "Cause remains unknown until supported assessment" });
|
|
1638
|
+
for (const findingId of findingIds)
|
|
1639
|
+
preparation.metric(input.runId, input.ticketId, admission.requirementsDigest, `metric:finding-unknown:${current.reportOccurrenceId}:${findingId}`, "finding-classification", { findingId, classifier: "host-evidence-boundary", cause: "unknown", confidence: "unknown", evidenceRefs: [evidence], at: now.toISOString() });
|
|
1640
|
+
}
|
|
1400
1641
|
for (const predecessor of predecessors) {
|
|
1401
1642
|
if (predecessor.reportOccurrenceId === current.reportOccurrenceId)
|
|
1402
1643
|
continue;
|
|
@@ -1468,13 +1709,38 @@ export class WorkflowDb {
|
|
|
1468
1709
|
for (const report of this.unresolvedQaReports(runId, ticketId))
|
|
1469
1710
|
this.setQaReportDisposition(report.reportOccurrenceId, "verified-fixed", "subsequent bound QA review passed", now);
|
|
1470
1711
|
}
|
|
1471
|
-
|
|
1712
|
+
authorizeContractQaWaiver(runId, workId, decisionId) {
|
|
1713
|
+
this.assertInstructionOwner(runId);
|
|
1714
|
+
const decision = this.humanDecision(decisionId);
|
|
1715
|
+
if (!decision || decision.runId !== runId || decision.status !== "answered" || decision.selectedChoiceId !== "yes" || decision.interruptionId !== `ticket:${workId}` || !this.humanDecisionKey(decisionId)?.startsWith(`${runId}:qa-nonconvergence:`) || !decision.prompt.includes("QA waiver confirmation"))
|
|
1716
|
+
throw new Error("Whole-work waiver requires the actual answered operator confirmation");
|
|
1717
|
+
const admission = this.assertAdmittedWork(runId, workId), store = this.qaPreparationStore(), head = store.head(runId, workId, admission.requirementsDigest), qa = this.qaTicketHead(runId, workId);
|
|
1718
|
+
if (head.state !== "ready" || !head.digest)
|
|
1719
|
+
throw new Error("Whole-work waiver requires a current ready contract");
|
|
1720
|
+
return store.putArtifact("waiver-authorization", { decisionId, runId, workId, admissionDigest: admission.requirementsDigest, contractDigest: head.digest, qaRevision: qa.revision, sourceDigest: qa.sourceStateDigest, reviewBasisDigest: qa.reviewBasisDigest, authorizedAt: decision.answeredAt });
|
|
1721
|
+
}
|
|
1722
|
+
commitQaWaiver(runId, ticketId, expectedRevision, reason, now = new Date(), authorizationRef) {
|
|
1472
1723
|
return this.db.transaction(() => {
|
|
1473
1724
|
const head = this.qaTicketHead(runId, ticketId);
|
|
1474
1725
|
if (head.revision !== expectedRevision)
|
|
1475
1726
|
throw new Error(`QA waiver raced for ${runId}/${ticketId}`);
|
|
1727
|
+
const store = this.qaPreparationStore();
|
|
1728
|
+
if (store.policy(runId)?.mode === "enforce") {
|
|
1729
|
+
const admission = this.assertAdmittedWork(runId, ticketId), contractHead = store.head(runId, ticketId, admission.requirementsDigest);
|
|
1730
|
+
if (contractHead.state !== "ready" || !contractHead.digest || !reason.trim())
|
|
1731
|
+
throw new Error("Whole-work waiver requires exact contract scope and operator reason");
|
|
1732
|
+
const authority = authorizationRef ? store.artifact(authorizationRef, "waiver-authorization") : undefined;
|
|
1733
|
+
const decision = authority && this.humanDecision(authority.decisionId);
|
|
1734
|
+
if (!authority || authority.runId !== runId || authority.workId !== ticketId || authority.admissionDigest !== admission.requirementsDigest || authority.contractDigest !== contractHead.digest || authority.qaRevision !== expectedRevision || authority.sourceDigest !== head.sourceStateDigest || authority.reviewBasisDigest !== head.reviewBasisDigest || decision?.status !== "answered" || decision.selectedChoiceId !== "yes")
|
|
1735
|
+
throw new Error("Missing or stale whole-work waiver authorization");
|
|
1736
|
+
const contract = store.contract(contractHead.digest), review = this.qaReviewAttempts(runId, ticketId).at(-1);
|
|
1737
|
+
store.event(runId, ticketId, `whole-work-waiver:${runId}:${ticketId}:${head.revision}`, "whole-work-waiver", { version: 1, runId, workId: ticketId, admissionDigest: admission.requirementsDigest, contractDigest: contract.contentDigest, revision: contract.revision, sourceDigest: review?.sourceDigest ?? null, reviewBasisDigest: head.reviewBasisDigest ?? null, mandatoryCheckIds: contract.checks.filter(check => check.obligation === "mandatory").map(check => check.id), unresolvedReports: this.unresolvedQaReports(runId, ticketId).map(report => report.reportOccurrenceId), reason, actor: "operator", authorizationRef, decisionId: authority.decisionId, mechanism: "existing-qa-nonconvergence-decision", at: now.toISOString(), completion: "waived" });
|
|
1738
|
+
}
|
|
1476
1739
|
for (const report of this.unresolvedQaReports(runId, ticketId))
|
|
1477
1740
|
this.setQaReportDisposition(report.reportOccurrenceId, "waived", reason, now);
|
|
1741
|
+
const admission = this.admittedWork(runId, ticketId);
|
|
1742
|
+
if (admission)
|
|
1743
|
+
store.metric(runId, ticketId, admission.requirementsDigest, `metric:waiver:${runId}:${ticketId}:${head.revision}`, "waived", { at: now.toISOString() });
|
|
1478
1744
|
return this.transitionQa(runId, ticketId, head.revision, { type: "waived" }, now);
|
|
1479
1745
|
})();
|
|
1480
1746
|
}
|
|
@@ -1499,6 +1765,11 @@ export class WorkflowDb {
|
|
|
1499
1765
|
return this.db.transaction(() => {
|
|
1500
1766
|
this.recordBuilderRemediationReceipt(receipt);
|
|
1501
1767
|
this.markQaReportsRecheckRequired(receipt.runId, receipt.ticketId, "Builder remediation received; QA recheck required", now);
|
|
1768
|
+
const admission = this.admittedWork(receipt.runId, receipt.ticketId);
|
|
1769
|
+
const remediation = this.qaRemediationAttempt(receipt.operationId);
|
|
1770
|
+
const review = remediation ? this.qaReviewAttempt(remediation.reviewAttemptId) : undefined;
|
|
1771
|
+
if (admission)
|
|
1772
|
+
this.qaPreparationStore().metric(receipt.runId, receipt.ticketId, admission.requirementsDigest, `metric:remediation:${receipt.operationId}`, "remediation", { phase: "remediation", findingIds: review?.namespacedFindingIds ?? [], at: now.toISOString() });
|
|
1502
1773
|
return this.transitionQa(receipt.runId, receipt.ticketId, expectedRevision, { type: "remediation-received" }, now);
|
|
1503
1774
|
})();
|
|
1504
1775
|
}
|
|
@@ -1520,7 +1791,60 @@ export class WorkflowDb {
|
|
|
1520
1791
|
return this.transitionQa(remediation.runId, remediation.ticketId, head.revision, { type: input.outcome === "uncertain" ? "remediation-uncertain" : "remediation-failed" }, now);
|
|
1521
1792
|
})();
|
|
1522
1793
|
}
|
|
1794
|
+
assertContractCertificate(input) {
|
|
1795
|
+
const store = this.qaPreparationStore();
|
|
1796
|
+
if (store.policy(input.runId)?.mode !== "enforce")
|
|
1797
|
+
return;
|
|
1798
|
+
const binding = input.contractCoverage;
|
|
1799
|
+
if (!binding || binding.version !== 1)
|
|
1800
|
+
throw new Error("Enforcing pass requires tagged contract coverage authority");
|
|
1801
|
+
const admission = this.assertAdmittedWork(input.runId, input.ticketId);
|
|
1802
|
+
const head = store.head(input.runId, input.ticketId, admission.requirementsDigest);
|
|
1803
|
+
if (head.state !== "ready" || head.digest !== binding.contractDigest)
|
|
1804
|
+
throw new Error("Pass contract is stale or incomplete");
|
|
1805
|
+
const contract = store.contract(binding.contractDigest);
|
|
1806
|
+
if (contract.revision !== binding.revision)
|
|
1807
|
+
throw new Error("Pass revision mismatch");
|
|
1808
|
+
const basisRow = this.db.prepare("SELECT basis_json FROM qa_review_bases WHERE digest=? AND run_id=? AND ticket_id=?").get(input.reviewBasisDigest, input.runId, input.ticketId);
|
|
1809
|
+
if (!basisRow)
|
|
1810
|
+
throw new Error("Pass lacks immutable input basis");
|
|
1811
|
+
const basis = parseJson(basisRow.basis_json);
|
|
1812
|
+
const definition = this.workDefinitions(input.runId).find(work => work.workId === input.ticketId)?.definition;
|
|
1813
|
+
if (!definition || basis.ticketDigest !== qaDigest("ticket", definition))
|
|
1814
|
+
throw new Error("Pass ticket basis differs from frozen admitted scope");
|
|
1815
|
+
const { digest, ...fields } = basis;
|
|
1816
|
+
if (digest !== qaDigest("review-basis-fields", fields) || basis.contractBinding?.digest !== contract.contentDigest || basis.contractBinding.admissionDigest !== admission.requirementsDigest)
|
|
1817
|
+
throw new Error("Pass basis/contract binding mismatch");
|
|
1818
|
+
const configuredMake = this.getRun(input.runId)?.state?.qa?.settings?.make;
|
|
1819
|
+
if (configuredMake && resolveEffectiveQaConfiguration(this.projectDir, { make: configuredMake }).digest !== basis.contractBinding.commonConfigDigest)
|
|
1820
|
+
throw new Error("Effective QA configuration changed before pass authority; reconcile contract");
|
|
1821
|
+
const attempt = this.qaReviewAttempt(binding.attemptId);
|
|
1822
|
+
if (!attempt || attempt.runId !== input.runId || attempt.ticketId !== input.ticketId || attempt.sourceDigest !== input.sourceStateDigest || attempt.reviewNumber !== this.qaTicketHead(input.runId, input.ticketId).reviewNumber)
|
|
1823
|
+
throw new Error("Coverage review attempt is stale or foreign");
|
|
1824
|
+
const coverage = store.artifact(binding.coverageDigest, "final-coverage");
|
|
1825
|
+
for (const check of contract.checks)
|
|
1826
|
+
for (const method of check.verification)
|
|
1827
|
+
if (method.equivalentAuthorityId && !store.equivalentResolver()(method.equivalentAuthorityId, contract, check, method, input.sourceStateDigest))
|
|
1828
|
+
throw new Error("Equivalent verification authority is stale or foreign");
|
|
1829
|
+
const errors = validateContractCoverage(contract, coverage, { phase: "qa", sourceDigest: input.sourceStateDigest, inputBasisDigest: input.reviewBasisDigest, attemptId: binding.attemptId, sessionId: binding.sessionId });
|
|
1830
|
+
if (errors.length)
|
|
1831
|
+
throw new Error(errors.join("; "));
|
|
1832
|
+
const initialTurns = this.db.prepare("SELECT intent_json FROM qa_turns WHERE run_id=? AND ticket_id=? AND review_number=? AND source_state_digest=? AND review_basis_digest=?").all(input.runId, input.ticketId, attempt.reviewNumber, input.sourceStateDigest, input.reviewBasisDigest);
|
|
1833
|
+
const initial = initialTurns.map(row => parseJson(row.intent_json)).find(turn => turn.slot === "initial");
|
|
1834
|
+
if (!initial || initial.providerSession.sessionId !== binding.sessionId)
|
|
1835
|
+
throw new Error("Coverage lacks the actual fresh review dispatch identity");
|
|
1836
|
+
const dispatchedInstruction = this.getEvidence(initial.instructionDigest)?.toString();
|
|
1837
|
+
const envelope = `\nImmutable input-basis digest: ${basis.digest}. Coverage must bind this inputBasisDigest.`;
|
|
1838
|
+
if (basis.contractBinding.transportVersion !== 1 || !dispatchedInstruction?.endsWith(envelope) || qaDigest("instruction", dispatchedInstruction.slice(0, -envelope.length)) !== basis.instructionDigest)
|
|
1839
|
+
throw new Error("Actual review dispatch differs from frozen base instruction and tagged input-basis envelope");
|
|
1840
|
+
const receipts = this.db.prepare("SELECT receipt_json FROM qa_turns WHERE run_id=? AND ticket_id=? AND source_state_digest=? AND review_basis_digest=? AND status='completed'").all(input.runId, input.ticketId, input.sourceStateDigest, input.reviewBasisDigest);
|
|
1841
|
+
const receipt = receipts.map(row => parseJson(row.receipt_json)).find(row => qaDigest("turn-receipt", row) === input.turnReceiptDigest && row.terminalEventObserved);
|
|
1842
|
+
const response = receipt?.cleanedResponseDigest ? this.getEvidence(receipt.cleanedResponseDigest) : undefined;
|
|
1843
|
+
if (!receipt || !response || canonicalJson(parseContractCoverage(response.toString())) !== canonicalJson(coverage))
|
|
1844
|
+
throw new Error("Coverage is not bound to the retained terminal review receipt");
|
|
1845
|
+
}
|
|
1523
1846
|
issueQaPassCertificate(input, now = new Date()) {
|
|
1847
|
+
this.assertContractCertificate(input);
|
|
1524
1848
|
if (this.unresolvedQaReports(input.runId, input.ticketId).length)
|
|
1525
1849
|
throw new Error("cannot issue QA pass certificate while reports remain unresolved");
|
|
1526
1850
|
const certificate = { version: 2, certificateId: qaDigest("pass-certificate", { ...input, issuedAt: now.toISOString() }), ...input, unresolvedReportCount: 0, issuedAt: now.toISOString() };
|
|
@@ -1545,6 +1869,7 @@ export class WorkflowDb {
|
|
|
1545
1869
|
|| attempt.sourceDigest !== input.sourceStateDigest || attempt.status !== "started") {
|
|
1546
1870
|
throw new Error(`QA pass attempt binding mismatch: ${attemptId}`);
|
|
1547
1871
|
}
|
|
1872
|
+
this.assertContractCertificate(input);
|
|
1548
1873
|
this.finishQaReviewAttempt(attemptId, { status: "passed", detail }, now);
|
|
1549
1874
|
return this.commitQaPass(input, expectedRevision, now);
|
|
1550
1875
|
})();
|
|
@@ -1556,6 +1881,7 @@ export class WorkflowDb {
|
|
|
1556
1881
|
throw new Error("QA pass certificate is missing or scoped to another run/ticket");
|
|
1557
1882
|
if (row.consumed_at)
|
|
1558
1883
|
throw new Error("QA pass certificate has already been consumed");
|
|
1884
|
+
this.assertContractCertificate(parseJson(row.certificate_json));
|
|
1559
1885
|
if (this.unresolvedQaReports(runId, ticketId).length)
|
|
1560
1886
|
throw new Error("QA pass certificate cannot be consumed with unresolved reports");
|
|
1561
1887
|
const at = now.toISOString();
|
|
@@ -1566,9 +1892,9 @@ export class WorkflowDb {
|
|
|
1566
1892
|
return certificate;
|
|
1567
1893
|
})();
|
|
1568
1894
|
}
|
|
1569
|
-
invalidateUnconsumedQaPassCertificates(runId, ticketId, reason, now = new Date()) {
|
|
1895
|
+
invalidateUnconsumedQaPassCertificates(runId, ticketId, reason, now = new Date(), issuedBefore) {
|
|
1570
1896
|
const at = now.toISOString();
|
|
1571
|
-
this.db.prepare("UPDATE qa_pass_certificates SET consumed_at=?,consumed_by=? WHERE run_id=? AND ticket_id=? AND consumed_at IS NULL").run(at, `invalidated:${reason}`, runId, ticketId);
|
|
1897
|
+
this.db.prepare("UPDATE qa_pass_certificates SET consumed_at=?,consumed_by=? WHERE run_id=? AND ticket_id=? AND consumed_at IS NULL AND (? IS NULL OR issued_at<=?)").run(at, `invalidated:${reason}`, runId, ticketId, issuedBefore ?? null, issuedBefore ?? null);
|
|
1572
1898
|
}
|
|
1573
1899
|
invalidateQaPassBeforeFinalization(input, now = new Date()) {
|
|
1574
1900
|
return this.db.transaction(() => {
|
|
@@ -1584,6 +1910,7 @@ export class WorkflowDb {
|
|
|
1584
1910
|
}
|
|
1585
1911
|
beginQaFinalization(input, now = new Date()) {
|
|
1586
1912
|
return this.db.transaction(() => {
|
|
1913
|
+
this.assertFinalizationControls(input.runId, input.ticketId);
|
|
1587
1914
|
const head = this.qaTicketHead(input.runId, input.ticketId);
|
|
1588
1915
|
if (head.revision !== input.expectedRevision || head.state !== "passed")
|
|
1589
1916
|
throw new Error(`QA finalization start raced for ${input.runId}/${input.ticketId}`);
|
|
@@ -1660,10 +1987,14 @@ export class WorkflowDb {
|
|
|
1660
1987
|
}
|
|
1661
1988
|
completeQaFinalization(runId, ticketId, expectedRevision, receipt, now = new Date()) {
|
|
1662
1989
|
return this.db.transaction(() => {
|
|
1990
|
+
this.assertFinalizationControls(runId, ticketId);
|
|
1663
1991
|
const head = this.qaTicketHead(runId, ticketId);
|
|
1664
1992
|
if (head.revision !== expectedRevision || head.state !== "finalizing")
|
|
1665
1993
|
throw new Error(`QA finalization completion raced for ${runId}/${ticketId}`);
|
|
1666
1994
|
this.finishPendingQaFinalizationSteps(runId, ticketId, receipt, now);
|
|
1995
|
+
const admission = this.admittedWork(runId, ticketId);
|
|
1996
|
+
if (admission)
|
|
1997
|
+
this.qaPreparationStore().metric(runId, ticketId, admission.requirementsDigest, `metric:completed:${runId}:${ticketId}`, "completed", { at: now.toISOString() });
|
|
1667
1998
|
return this.transitionQa(runId, ticketId, head.revision, { type: "completed" }, now);
|
|
1668
1999
|
})();
|
|
1669
2000
|
}
|
|
@@ -2272,7 +2603,7 @@ function leaseVerifiedLive(lease, now, staleMs) {
|
|
|
2272
2603
|
function ensureRecoveryGitignore(projectDir) {
|
|
2273
2604
|
const localExclude = join(projectDir, ".git", "info", "exclude");
|
|
2274
2605
|
const path = existsSync(localExclude) ? localExclude : join(projectDir, ".gitignore");
|
|
2275
|
-
const entries = [WORKFLOW_DB_FILE, `${WORKFLOW_DB_FILE}-wal`, `${WORKFLOW_DB_FILE}-shm`, ".rafi/cache/handoffs/"];
|
|
2606
|
+
const entries = [WORKFLOW_DB_FILE, `${WORKFLOW_DB_FILE}-wal`, `${WORKFLOW_DB_FILE}-shm`, ".rafi/cache/handoffs/", ".rafi/backups/work-admission-v4/"];
|
|
2276
2607
|
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
2277
2608
|
const missing = entries.filter((entry) => !existing.split(/\r?\n/).includes(entry));
|
|
2278
2609
|
if (missing.length)
|