ai-foreman 1.7.20 → 1.7.22
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 +27 -8
- package/dist/adapters/claude.d.ts +19 -0
- package/dist/adapters/claude.js +59 -7
- 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/dirtyBaseline.d.ts +5 -0
- package/dist/branch/dirtyBaseline.js +69 -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/resume.js +2 -2
- 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 +215 -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 +7 -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 +235 -13
- package/dist/cli/recovery.js +3 -2
- package/dist/cli/runtimeAuthPrompt.js +28 -8
- package/dist/cli/start.js +192 -50
- package/dist/cli/status.js +29 -7
- package/dist/cli/tickets.d.ts +4 -0
- package/dist/cli/tickets.js +37 -13
- 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/interviews.d.ts +3 -1
- package/dist/interviews.js +7 -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 +25 -1
- 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/projectDiagnostics.js +32 -8
- 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/recoveryPolicy.js +1 -1
- 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 +19 -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.d.ts +1 -1
- package/dist/tickets/commands.js +13 -11
- package/dist/tickets/details.js +2 -2
- package/dist/tickets/queue.js +1 -1
- package/dist/tickets/stateDb.d.ts +4 -1
- package/dist/tickets/stateDb.js +18 -2
- package/dist/tickets/ticketSchema.d.ts +198 -0
- package/dist/tickets/ticketSchema.js +2 -0
- package/dist/tickets/validate.js +4 -1
- 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 +85 -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/branch/runner.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { renderBuildWorkContext } from "../buildWorkContext.js";
|
|
2
|
+
import { loadTicketSetupConfigWithDefaults } from "../tickets/setupConfig.js";
|
|
3
|
+
import { deliverBuilderGuidanceFollowup } from "../builderGuidanceFollowup.js";
|
|
4
|
+
import { qaDigest } from "../qaProtocolV2.js";
|
|
5
|
+
import { loadTickets } from "../tickets/ticketLoader.js";
|
|
1
6
|
import { formatRecoveryCommand } from "../recoveryGuidance.js";
|
|
2
7
|
import { WorkflowReader } from "../workflowReader.js";
|
|
3
8
|
import { buildScopeRevision } from "../buildApproval.js";
|
|
@@ -15,6 +20,7 @@ import { checkGitLabMrMerged, createOrReuseMr, enableGitLabAutoMerge, pushBranch
|
|
|
15
20
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
16
21
|
import { join, resolve } from "node:path";
|
|
17
22
|
import { WorkflowDb } from "../workflowDb.js";
|
|
23
|
+
import { assertBuildAssignmentReconciled, BuildAssignmentRejected } from "../buildAssignment.js";
|
|
18
24
|
import { currentActivity, withActivityPhase } from "../activity.js";
|
|
19
25
|
import { SessionUnavailableError } from "../adapters/sessionFailure.js";
|
|
20
26
|
import { SessionUnavailableContinuityError } from "../continuity.js";
|
|
@@ -46,6 +52,7 @@ function deliverySessionPath(projectDir, unitId) {
|
|
|
46
52
|
return join(projectDir, ".foreman", "delivery-sessions", `${unitId}.json`);
|
|
47
53
|
}
|
|
48
54
|
export async function runBranchPlan(opts) {
|
|
55
|
+
assertBuildAssignmentReconciled(opts.projectDir, opts.runId);
|
|
49
56
|
let qaResumedRecovery = opts.qaResumedRecovery;
|
|
50
57
|
if (qaResumedRecovery) {
|
|
51
58
|
const matches = opts.plan.nodes.filter((node) => node.ticket.id === qaResumedRecovery.manifest.ticketId);
|
|
@@ -165,6 +172,15 @@ export async function runBranchPlan(opts) {
|
|
|
165
172
|
const commitOperation = finalizationOperationKey(protocolDb, opts.runId, node.ticket.id, "commit");
|
|
166
173
|
const completionOperation = finalizationOperationKey(protocolDb, opts.runId, node.ticket.id, "ticket-complete");
|
|
167
174
|
let completedDirectMerge = finalizationRecovery ? protocolDb.operation(directMergeOperation) : undefined;
|
|
175
|
+
try {
|
|
176
|
+
if (finalizationRecovery)
|
|
177
|
+
protocolDb.assertFinalizationControls(opts.runId, node.ticket.id);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
protocolDb.close();
|
|
181
|
+
summaries.push(summaryFor(node, "needs-human", error instanceof Error ? error.message : String(error)));
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
168
184
|
if (completedDirectMerge?.status === "in_progress") {
|
|
169
185
|
try {
|
|
170
186
|
const intent = readDirectMergeIntent(completedDirectMerge.intent);
|
|
@@ -271,6 +287,13 @@ export async function runBranchPlan(opts) {
|
|
|
271
287
|
continue;
|
|
272
288
|
}
|
|
273
289
|
}
|
|
290
|
+
const admissionDb = new WorkflowDb(opts.projectDir);
|
|
291
|
+
try {
|
|
292
|
+
admissionDb.assertAdmittedWork(opts.runId, node.ticket.id);
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
admissionDb.close();
|
|
296
|
+
}
|
|
274
297
|
opts.log.write("branch-start", {
|
|
275
298
|
ticket: node.ticket.id,
|
|
276
299
|
branch: node.branch,
|
|
@@ -308,7 +331,7 @@ export async function runBranchPlan(opts) {
|
|
|
308
331
|
summary: resumeSession ? `Resuming branch ${node.branch}` : `Starting branch ${node.branch}`,
|
|
309
332
|
});
|
|
310
333
|
});
|
|
311
|
-
let ticketInstruction = resumeSession ? buildBranchTicketResumeInstruction(node, opts.trackerPaths) : buildBranchTicketInstruction(node, opts.trackerPaths);
|
|
334
|
+
let ticketInstruction = resumeSession ? buildBranchTicketResumeInstruction(node, { ...opts.trackerPaths, validationChecklist: loadTicketSetupConfigWithDefaults(opts.projectDir).build.validation_checklist }) : buildBranchTicketInstruction(node, { ...opts.trackerPaths, validationChecklist: loadTicketSetupConfigWithDefaults(opts.projectDir).build.validation_checklist });
|
|
312
335
|
if (continuations.length)
|
|
313
336
|
ticketInstruction += "\n\nScoped answers authorizing this ticket continuation:\n" + continuations.map(decision => `${decision.prompt}\nAnswer: ${decision.answer ?? decision.selectedChoiceId}`).join("\n");
|
|
314
337
|
// Reattach the predecessor even for a `fresh` strategy so the host can
|
|
@@ -337,7 +360,7 @@ export async function runBranchPlan(opts) {
|
|
|
337
360
|
if (!qaOnlyRecovery)
|
|
338
361
|
builderWorkSessions += 1;
|
|
339
362
|
viewer = opts.observeBuilder?.(builder);
|
|
340
|
-
const foreman = new Foreman(builder, opts.log, { desktop: opts.notificationsEnabled, terminalBell: opts.terminalBellEnabled ?? true }, opts.qaEnabled, 3, opts.projectDir, undefined,
|
|
363
|
+
const foreman = new Foreman(builder, opts.log, { desktop: opts.notificationsEnabled, terminalBell: opts.terminalBellEnabled ?? true }, opts.qaEnabled, 3, opts.projectDir, undefined, opts.createQa, opts.qaSessionStrategy ?? "compact", undefined, opts.builderSessionStrategy ?? "compact", undefined, opts.beforeBuilderTurn ? (adapter, action) => opts.beforeBuilderTurn(adapter, action, worktreePath) : undefined, undefined, undefined, undefined, async (adapter) => opts.observeBuilderNativeCompactions?.(adapter, worktreePath), opts.observer, undefined, false, undefined, undefined, opts.runId, opts.continueIndependentTickets ?? true, node.ticket.id, worktreePath);
|
|
341
364
|
const turn = async () => {
|
|
342
365
|
const db = new WorkflowDb(opts.projectDir);
|
|
343
366
|
try {
|
|
@@ -450,6 +473,54 @@ export async function runBranchPlan(opts) {
|
|
|
450
473
|
observeNativeCompactions: opts.observeQaNativeCompactions,
|
|
451
474
|
resolveBlocked: (adapter, reason) => foreman.resolveBlocker(adapter, reason, "qa"),
|
|
452
475
|
evidence: (entry) => opts.log.write("qa-evidence", { ticket: node.ticket.id, ...entry }),
|
|
476
|
+
resumeBuilderGuidance: qaOnlyRecovery,
|
|
477
|
+
deliverBuilderFollowup: (instruction, followup) => deliverBuilderGuidanceFollowup({ projectDir: opts.projectDir, runId: opts.runId, ticketId: node.ticket.id, worktree: worktreePath }, instruction, followup, {
|
|
478
|
+
prepare: async (instruction) => {
|
|
479
|
+
if (!builder)
|
|
480
|
+
throw new Error("Builder session unavailable");
|
|
481
|
+
const strategy = opts.builderSessionStrategy ?? "compact";
|
|
482
|
+
if (opts.builderSessionBoundary)
|
|
483
|
+
builder = await opts.builderSessionBoundary(builder, instruction, strategy, worktreePath);
|
|
484
|
+
else if (strategy === "fresh") {
|
|
485
|
+
await builder.close();
|
|
486
|
+
builder = await opts.createBuilder(worktreePath);
|
|
487
|
+
}
|
|
488
|
+
else if (builder.sessionId()) {
|
|
489
|
+
const compacted = await compactWithRetry(builder);
|
|
490
|
+
if (!compacted.ok) {
|
|
491
|
+
await builder.close();
|
|
492
|
+
builder = await opts.createBuilder(worktreePath);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (opts.beforeBuilderTurn)
|
|
496
|
+
builder = await opts.beforeBuilderTurn(builder, instruction, worktreePath);
|
|
497
|
+
return builder;
|
|
498
|
+
},
|
|
499
|
+
validateRequirements: () => {
|
|
500
|
+
const current = loadTickets(resolveTicketPaths(loadTicketsConfig(opts.projectDir), opts.projectDir).tickets).find(ticket => ticket.id === node.ticket.id);
|
|
501
|
+
if (!current || qaDigest("admitted-requirements", current) !== followup.requirementsDigest)
|
|
502
|
+
throw new BuildAssignmentRejected("Ticket requirements changed during session preparation; renewed scope approval is required");
|
|
503
|
+
},
|
|
504
|
+
recordSession: adapter => {
|
|
505
|
+
const sessionId = adapter.sessionId(), sessionRef = adapter.sessionRef?.();
|
|
506
|
+
if (!sessionId)
|
|
507
|
+
return;
|
|
508
|
+
builderStream = { sessionId, ...(sessionRef ? { sessionRef } : {}), worktreePath };
|
|
509
|
+
const db = new WorkflowDb(opts.projectDir);
|
|
510
|
+
try {
|
|
511
|
+
db.recordBranchResumeSession(opts.runId, { ticket: node.ticket.id, branch: node.branch, base: node.baseBranch, worktreePath, sessionId, sessionRef, logPath: "structured-recovery", deliveryUnitId: node.deliveryUnitId });
|
|
512
|
+
}
|
|
513
|
+
finally {
|
|
514
|
+
db.close();
|
|
515
|
+
}
|
|
516
|
+
opts.recordBuilderSession?.(sessionRef ?? sessionId, node.ticket.id, worktreePath);
|
|
517
|
+
},
|
|
518
|
+
completed: async (operationId, adapter) => {
|
|
519
|
+
builderWorkSessions += 1;
|
|
520
|
+
await opts.observeBuilderNativeCompactions?.(adapter, worktreePath);
|
|
521
|
+
opts.log.write("qa-fix", { kind: "builder-guidance-followup", ticket: node.ticket.id, operationId });
|
|
522
|
+
},
|
|
523
|
+
}),
|
|
453
524
|
deliverFailure: async (request) => {
|
|
454
525
|
if (!builder)
|
|
455
526
|
return { ok: false, detail: "Builder session unavailable" };
|
|
@@ -530,6 +601,13 @@ export async function runBranchPlan(opts) {
|
|
|
530
601
|
isError: false,
|
|
531
602
|
});
|
|
532
603
|
}
|
|
604
|
+
const finalControlsDb = new WorkflowDb(opts.projectDir);
|
|
605
|
+
try {
|
|
606
|
+
finalControlsDb.assertFinalizationControls(opts.runId, node.ticket.id);
|
|
607
|
+
}
|
|
608
|
+
finally {
|
|
609
|
+
finalControlsDb.close();
|
|
610
|
+
}
|
|
533
611
|
if (opts.qaEnabled && !qaWaived && !finalizationRecovery) {
|
|
534
612
|
if (!qaPassCertificateId || !qaSourceStateDigest)
|
|
535
613
|
throw new Error("QA passed without a durable pass certificate");
|
|
@@ -760,6 +838,10 @@ export async function runBranchPlan(opts) {
|
|
|
760
838
|
await observeNode(opts, node, "cleanup", "removing ticket worktree", () => removeTicketWorktree(opts.projectDir, worktreePath));
|
|
761
839
|
}
|
|
762
840
|
catch (err) {
|
|
841
|
+
if (err instanceof BuildAssignmentRejected) {
|
|
842
|
+
summaries.push(summaryFor(node, "needs-human", err.message));
|
|
843
|
+
return summaries;
|
|
844
|
+
}
|
|
763
845
|
if (err instanceof SessionUnavailableError || err instanceof SessionUnavailableContinuityError) {
|
|
764
846
|
const message = err.message;
|
|
765
847
|
opts.log.write("branch-issue", { ticket: node.ticket.id, code: "session_unavailable", message, blocking: true });
|
|
@@ -1027,6 +1109,8 @@ export function buildBranchTicketInstruction(node, trackerPaths = {}) {
|
|
|
1027
1109
|
|
|
1028
1110
|
${node.ticket.id}: ${node.ticket.title}
|
|
1029
1111
|
|
|
1112
|
+
${renderBuildWorkContext(node.ticket, trackerPaths.validationChecklist ?? [])}
|
|
1113
|
+
|
|
1030
1114
|
Summary:
|
|
1031
1115
|
${node.ticket.summary}
|
|
1032
1116
|
|
|
@@ -1047,7 +1131,7 @@ Branch-mode rules:
|
|
|
1047
1131
|
- If another selected ticket is required before this one can be completed, stop and end with STEP_STATUS: blocked | ticket="${node.ticket.id}" branch_dependency="<ticket-id>" reason="<why>".
|
|
1048
1132
|
- End with STEP_STATUS: done | ticket="${node.ticket.id}" summary="<what changed>" when the ticket is implemented.
|
|
1049
1133
|
|
|
1050
|
-
QA
|
|
1134
|
+
Final QA runs independently in a separate disposable snapshot and fresh reviewer conversation.
|
|
1051
1135
|
|
|
1052
1136
|
${MARKER_SPEC}`;
|
|
1053
1137
|
}
|
|
@@ -1057,6 +1141,8 @@ export function buildBranchTicketResumeInstruction(node, trackerPaths = {}) {
|
|
|
1057
1141
|
|
|
1058
1142
|
${node.ticket.id}: ${node.ticket.title}
|
|
1059
1143
|
|
|
1144
|
+
${renderBuildWorkContext(node.ticket, trackerPaths.validationChecklist ?? [])}
|
|
1145
|
+
|
|
1060
1146
|
Resume from the current repository state. Inspect the worktree if needed, then finish only this ticket.
|
|
1061
1147
|
|
|
1062
1148
|
Branch-mode rules:
|
|
@@ -1067,7 +1153,7 @@ Branch-mode rules:
|
|
|
1067
1153
|
- If another selected ticket is required before this one can be completed, stop and end with STEP_STATUS: blocked | ticket="${node.ticket.id}" branch_dependency="<ticket-id>" reason="<why>".
|
|
1068
1154
|
- End with STEP_STATUS: done | ticket="${node.ticket.id}" summary="<what changed>" when the ticket is implemented.
|
|
1069
1155
|
|
|
1070
|
-
QA
|
|
1156
|
+
Final QA runs independently in a separate disposable snapshot and fresh reviewer conversation.
|
|
1071
1157
|
|
|
1072
1158
|
${MARKER_SPEC}`;
|
|
1073
1159
|
}
|
package/dist/buildAdmission.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registerHandbackWriter } from "./qaHandbackMigration.js";
|
|
1
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
4
|
import { realpathSync } from "node:fs";
|
|
@@ -56,6 +57,7 @@ export function maintainBuildAdmission(authority) {
|
|
|
56
57
|
try {
|
|
57
58
|
db = new Database(join(authority.project, ".rafi/recovery.sqlite3"), { fileMustExist: true });
|
|
58
59
|
db.function("rafi_protocol_v3", () => 1);
|
|
60
|
+
registerHandbackWriter(db);
|
|
59
61
|
const changed = db.prepare("UPDATE build_admission SET record_json=json_set(record_json,'$.heartbeatAt',?) WHERE singleton=1 AND json_extract(record_json,'$.token')=?").run(new Date().toISOString(), authority.token);
|
|
60
62
|
if (changed.changes !== 1)
|
|
61
63
|
stopBuildAdmissionHeartbeat(authority);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { BuilderAdapter } from "./adapters/types.js";
|
|
2
|
+
import type { StepStatus } from "./foreman.js";
|
|
3
|
+
import type { TurnResult } from "./adapters/types.js";
|
|
4
|
+
import { type FrozenQaSourceState } from "./qaSnapshot.js";
|
|
5
|
+
/** Work responses confirm an assignment; they never select the next ticket. */
|
|
6
|
+
export declare function validateBuildAssignment(ticketId: string, status: StepStatus, responseText?: string): string | undefined;
|
|
7
|
+
export interface BuildAssignmentCapture {
|
|
8
|
+
operationId: string;
|
|
9
|
+
runId: string;
|
|
10
|
+
ticketId: string;
|
|
11
|
+
worktree: string;
|
|
12
|
+
before: BuildSourceState;
|
|
13
|
+
instruction: string;
|
|
14
|
+
guidanceIds: string[];
|
|
15
|
+
}
|
|
16
|
+
type BuildSourceState = Pick<FrozenQaSourceState, "digest" | "head" | "contentDigest" | "capturedAt" | "pathInventory" | "stagedDiff" | "unstagedDiff" | "untracked"> & {
|
|
17
|
+
captureMode: "git" | "filesystem";
|
|
18
|
+
omissions?: string[];
|
|
19
|
+
};
|
|
20
|
+
export declare class BuildAssignmentRejected extends Error {
|
|
21
|
+
}
|
|
22
|
+
export interface BuilderGuidanceFollowup {
|
|
23
|
+
reviewAttemptId: string;
|
|
24
|
+
requirementsDigest: string;
|
|
25
|
+
maximum: number;
|
|
26
|
+
authorizationId?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function assertBuildAssignmentReconciled(projectDir: string, runId: string, worktree?: string): void;
|
|
29
|
+
/** Commit response/source bytes before interpreting any scoped outcome. */
|
|
30
|
+
export declare function beginBuildAssignment(projectDir: string, runId: string, ticketId: string, worktree: string, instruction: string, followup?: BuilderGuidanceFollowup, adapter?: BuilderAdapter): BuildAssignmentCapture;
|
|
31
|
+
export declare function finishBuildAssignment(projectDir: string, assignment: BuildAssignmentCapture, result: TurnResult, status: StepStatus): string | undefined;
|
|
32
|
+
export declare function captureBuildSource(worktree: string): BuildSourceState;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { assertBuilderContractCoverage } from "./qaBuilderCoverage.js";
|
|
2
|
+
import { resolveEffectiveQaConfiguration } from "./qaEffectiveConfig.js";
|
|
3
|
+
import { contractDigest } from "./qaVerificationContract.js";
|
|
4
|
+
import { queueGraphMaintenance } from "./graph/boundary.js";
|
|
5
|
+
import { actualContractSession, assertContractReceipt } from "./qaContractDelivery.js";
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
import { lstatSync, readFileSync, readdirSync, readlinkSync, realpathSync } from "node:fs";
|
|
8
|
+
import { join, relative, resolve } from "node:path";
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { captureFrozenQaSource } from "./qaSnapshot.js";
|
|
11
|
+
import { WorkflowDb } from "./workflowDb.js";
|
|
12
|
+
import { WorkflowReader } from "./workflowReader.js";
|
|
13
|
+
/** Work responses confirm an assignment; they never select the next ticket. */
|
|
14
|
+
export function validateBuildAssignment(ticketId, status, responseText = "") {
|
|
15
|
+
// An unmarked response may receive the existing bounded response-only repair.
|
|
16
|
+
// Explicit foreign markers must not be erased by a parser error or correction.
|
|
17
|
+
const named = [...responseText.matchAll(/\bticket="([^"]*)"/g)].map(match => match[1]);
|
|
18
|
+
if (status.kind === "unknown" && !status.ticket && named.every(id => id === ticketId))
|
|
19
|
+
return undefined;
|
|
20
|
+
if (status.ticket !== ticketId) {
|
|
21
|
+
return `Builder response named ${status.ticket ?? "no ticket"}, but this turn was assigned to ${ticketId}. Preserved work requires ownership reconciliation before further execution.`;
|
|
22
|
+
}
|
|
23
|
+
if (status.kind === "qa_pass" || status.kind === "qa_fail")
|
|
24
|
+
return `Builder emitted QA status ${status.kind} for assigned ticket ${ticketId}; independent QA is required.`;
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
export class BuildAssignmentRejected extends Error {
|
|
28
|
+
}
|
|
29
|
+
export function assertBuildAssignmentReconciled(projectDir, runId, worktree = projectDir) {
|
|
30
|
+
const reader = new WorkflowReader(projectDir);
|
|
31
|
+
try {
|
|
32
|
+
const runs = [...new Set([runId, ...reader.buildRuns(true).map(run => run.runId)])];
|
|
33
|
+
for (const operation of runs.flatMap(id => reader.operations(id, true))) {
|
|
34
|
+
if (operation.kind !== "build-assignment")
|
|
35
|
+
continue;
|
|
36
|
+
const result = operation.result;
|
|
37
|
+
if (result?.reconciliationId)
|
|
38
|
+
continue;
|
|
39
|
+
if (!result?.rejection && !result?.sourceError && operation.status !== "in_progress" && operation.status !== "uncertain")
|
|
40
|
+
continue;
|
|
41
|
+
const intent = operation.intent;
|
|
42
|
+
if (operation.runId !== runId) {
|
|
43
|
+
if (!intent.worktree)
|
|
44
|
+
continue;
|
|
45
|
+
try {
|
|
46
|
+
if (realpathSync(intent.worktree) !== realpathSync(worktree))
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
if (resolve(intent.worktree) !== resolve(worktree))
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (result?.rejection || result?.sourceError || operation.status === "in_progress" || operation.status === "uncertain") {
|
|
55
|
+
throw new BuildAssignmentRejected(`Build ${runId} requires assignment reconciliation for ${operation.idempotencyKey}: ${result?.rejection ?? result?.sourceError ?? "provider dispatch outcome is unconfirmed"}. Inspect with rafi manager --ask '/qa-conflicts ${runId}'.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
reader.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Commit response/source bytes before interpreting any scoped outcome. */
|
|
64
|
+
export function beginBuildAssignment(projectDir, runId, ticketId, worktree, instruction, followup, adapter) {
|
|
65
|
+
assertBuildAssignmentReconciled(projectDir, runId, worktree);
|
|
66
|
+
const operationId = `build-assignment:${runId}:${randomUUID()}`;
|
|
67
|
+
const db = new WorkflowDb(projectDir);
|
|
68
|
+
try {
|
|
69
|
+
const before = captureBuildSource(worktree);
|
|
70
|
+
let guidance = { text: instruction, ids: [] };
|
|
71
|
+
db.atomic(() => {
|
|
72
|
+
db.ensureRun(runId);
|
|
73
|
+
const admission = db.assertAdmittedWork(runId, ticketId);
|
|
74
|
+
let contractBinding;
|
|
75
|
+
if (db.qaPreparationStore().policy(runId)?.mode === "enforce") {
|
|
76
|
+
const contract = assertContractReceipt(db.qaPreparationStore(), runId, ticketId, admission.requirementsDigest, adapter ? actualContractSession(adapter, worktree, projectDir) : undefined, adapter);
|
|
77
|
+
const session = actualContractSession(adapter, worktree, projectDir);
|
|
78
|
+
const receipt = db.qaPreparationStore().receipts(contract.contentDigest).find(row => row.sessionId === session.sessionId && row.generation === session.generation && row.workspace === session.workspace && row.compactionSequence === session.compactionSequence);
|
|
79
|
+
contractBinding = { digest: contract.contentDigest, revision: contract.revision, receiptOperationId: receipt.operationId, session };
|
|
80
|
+
const qaMake = db.getRun(runId)?.state?.qa?.settings?.make ?? adapter.agent;
|
|
81
|
+
const current = resolveEffectiveQaConfiguration(projectDir, { make: qaMake });
|
|
82
|
+
if (contract.inputs.find(input => input.kind === "rules")?.digest !== current.digest || contract.inputs.find(input => input.kind === "checklist")?.digest !== contractDigest("input-checklist", current.checklist))
|
|
83
|
+
throw new BuildAssignmentRejected("Canonical QA rules, skills or checklist changed after delivery; reconcile the contract before implementation");
|
|
84
|
+
}
|
|
85
|
+
guidance = db.reserveGuidance(runId, ticketId, "builder", operationId, before.digest, instruction);
|
|
86
|
+
if (followup) {
|
|
87
|
+
if (admission.requirementsDigest !== followup.requirementsDigest)
|
|
88
|
+
throw new BuildAssignmentRejected("Ticket requirements changed; renewed scope approval is required before the Builder follow-up");
|
|
89
|
+
if (!guidance.ids.length)
|
|
90
|
+
throw new BuildAssignmentRejected("No eligible Builder guidance remains; no follow-up was dispatched");
|
|
91
|
+
const latest = db.qaReviewAttempts(runId, ticketId).at(-1);
|
|
92
|
+
const head = db.qaTicketHead(runId, ticketId);
|
|
93
|
+
if (latest?.attemptId !== followup.reviewAttemptId || latest.status !== "passed" || head.state !== "passed")
|
|
94
|
+
throw new BuildAssignmentRejected("Builder follow-up requires the current unfinalized QA pass");
|
|
95
|
+
db.reserveQaRemediation(runId, ticketId, latest.attemptId, operationId, followup.maximum, followup.authorizationId);
|
|
96
|
+
db.invalidateUnconsumedQaPassCertificates(runId, ticketId, "builder-guidance-followup");
|
|
97
|
+
db.transitionQa(runId, ticketId, head.revision, { type: "builder-guidance-followup-intended" });
|
|
98
|
+
}
|
|
99
|
+
const instructionDigest = db.putEvidence("qa", Buffer.from(guidance.text));
|
|
100
|
+
db.planOperation({ runId, idempotencyKey: operationId, kind: "build-assignment", intent: { ticketId, worktree, admissionId: admission.assignmentId, requirementsDigest: admission.requirementsDigest, instructionDigest, ...(contractBinding ? { contractBinding } : {}), before: retainSource(db, before), ...(followup ? { managerFollowup: followup } : {}) } });
|
|
101
|
+
db.recordWorkAssignment(runId, ticketId, operationId, { requirementsDigest: admission.requirementsDigest, worktree, sourceDigest: before.digest, ownerGeneration: db.currentLease()?.generation, instructionDigest });
|
|
102
|
+
db.updateOperation(operationId, "in_progress");
|
|
103
|
+
});
|
|
104
|
+
return { operationId, runId, ticketId, worktree, before, instruction: guidance.text, guidanceIds: guidance.ids };
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
db.close();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function finishBuildAssignment(projectDir, assignment, result, status) {
|
|
111
|
+
let rejection = result.failure?.dispatchState === "not-sent" ? undefined : validateBuildAssignment(assignment.ticketId, status, result.text);
|
|
112
|
+
// Source capture can fail after an external turn. Keep the response even then.
|
|
113
|
+
let after;
|
|
114
|
+
let sourceError;
|
|
115
|
+
try {
|
|
116
|
+
after = captureBuildSource(assignment.worktree);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
sourceError = error instanceof Error ? error.message : String(error);
|
|
120
|
+
}
|
|
121
|
+
if (result.failure?.dispatchState === "not-sent" && after && after.contentDigest !== assignment.before.contentDigest)
|
|
122
|
+
rejection = "Provider claimed an unsubmitted turn but product source changed; reconcile the captured source before continuing";
|
|
123
|
+
const db = new WorkflowDb(projectDir);
|
|
124
|
+
try {
|
|
125
|
+
db.atomic(() => {
|
|
126
|
+
if (!rejection && !result.isError && !result.failure && ["done", "plan_complete"].includes(status.kind) && after) {
|
|
127
|
+
try {
|
|
128
|
+
assertBuilderContractCoverage(db, assignment.runId, assignment.ticketId, assignment.operationId, after.digest);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
rejection = String(error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const responseDigest = db.putEvidence("qa", Buffer.from(result.rawResponse ?? result.text));
|
|
135
|
+
const cleanedResponseDigest = db.putEvidence("qa", Buffer.from(result.cleanedResponse ?? result.text));
|
|
136
|
+
const providerInstructionDigest = result.providerInstruction ? db.putEvidence("qa", Buffer.from(result.providerInstruction)) : undefined;
|
|
137
|
+
db.updateOperation(assignment.operationId, result.failure?.dispatchState === "unknown" || result.isError && !result.failure?.dispatchState && status.kind === "unknown" ? "uncertain" : "confirmed", { result: { turnId: result.turnId, responseDigest, cleanedResponseDigest, providerInstructionDigest, providerMetadata: result.providerMetadata, dispatchState: result.failure?.dispatchState, status, rejection, sourceError, after: after ? retainSource(db, after) : undefined } });
|
|
138
|
+
const admission = db.assertAdmittedWork(assignment.runId, assignment.ticketId);
|
|
139
|
+
db.qaPreparationStore().metric(assignment.runId, assignment.ticketId, admission.requirementsDigest, `metric:builder-result:${assignment.operationId}`, "phase-observation", { phase: "builder", durationMs: Math.max(0, Date.now() - Date.parse(assignment.before.capturedAt)), costUsd: result.costAuthoritative ? result.costUsd : null, inputTokens: result.inputTokens ?? null, outputTokens: result.outputTokens ?? null });
|
|
140
|
+
db.finishGuidance(assignment.guidanceIds, "builder", { submitted: result.failure?.dispatchState === "not-sent" ? false : result.turnId ? true : undefined, receipt: result.turnId ? { turnId: result.turnId, responseDigest, providerInstructionDigest, postSourceDigest: after?.digest } : undefined, applied: !rejection && status.kind === "done" && assignment.guidanceIds.every(id => result.text.includes(id)) });
|
|
141
|
+
if (rejection || sourceError) {
|
|
142
|
+
const run = db.getRun(assignment.runId);
|
|
143
|
+
db.transition(assignment.runId, { checkpoint: "build-assignment-rejected", state: { ...run.state, assignmentConflict: { operationId: assignment.operationId, ticketId: assignment.ticketId, worktree: assignment.worktree, responseDigest, rejection, sourceError } }, event: "build_assignment_rejected", payload: { operationId: assignment.operationId, ticketId: assignment.ticketId, returnedTicketId: status.ticket, responseDigest, rejection, sourceError } });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
db.close();
|
|
149
|
+
}
|
|
150
|
+
if (!rejection && !sourceError && !result.isError && status.kind === "done" && after) {
|
|
151
|
+
try {
|
|
152
|
+
queueGraphMaintenance(projectDir, assignment.worktree, assignment.operationId, result.text, after.contentDigest !== assignment.before.contentDigest);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
console.error(`Graph maintenance deferred: ${String(error)}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return rejection ?? (sourceError ? `Builder source evidence is unavailable: ${sourceError}. Reconcile before continuing.` : undefined);
|
|
159
|
+
}
|
|
160
|
+
function retainSource(db, source) {
|
|
161
|
+
const staged = retainDiff(db, source.stagedDiff);
|
|
162
|
+
const unstaged = retainDiff(db, source.unstagedDiff);
|
|
163
|
+
return { digest: source.digest, head: source.head, captureMode: source.captureMode, omissions: source.omissions, contentDigest: source.contentDigest, capturedAt: source.capturedAt, pathInventory: source.pathInventory,
|
|
164
|
+
stagedDiffDigest: staged.digest, stagedDiffChunks: staged.chunks, stagedDiffBytes: source.stagedDiff.length,
|
|
165
|
+
unstagedDiffDigest: unstaged.digest, unstagedDiffChunks: unstaged.chunks, unstagedDiffBytes: source.unstagedDiff.length,
|
|
166
|
+
untracked: source.untracked.map(file => { const evidence = retainDiff(db, file.bytes); return { path: file.path, mode: file.mode, kind: file.kind, digest: file.digest, evidenceDigest: evidence.digest, evidenceChunks: evidence.chunks, byteLength: file.bytes.length }; }) };
|
|
167
|
+
}
|
|
168
|
+
/** Preserve large product files without changing the shared per-item evidence limits. */
|
|
169
|
+
function retainDiff(db, bytes) {
|
|
170
|
+
if (bytes.length <= 16 * 1024 * 1024)
|
|
171
|
+
return { digest: db.putEvidence("diff", bytes) };
|
|
172
|
+
const chunks = [];
|
|
173
|
+
for (let offset = 0; offset < bytes.length; offset += 8 * 1024 * 1024)
|
|
174
|
+
chunks.push(db.putEvidence("diff", bytes.subarray(offset, offset + 8 * 1024 * 1024)));
|
|
175
|
+
return { chunks };
|
|
176
|
+
}
|
|
177
|
+
export function captureBuildSource(worktree) {
|
|
178
|
+
const head = spawnSync("git", ["-C", worktree, "rev-parse", "--verify", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
179
|
+
if (head.status === 0)
|
|
180
|
+
return { ...captureFrozenQaSource(worktree), captureMode: "git" };
|
|
181
|
+
if (head.error?.code !== "ENOENT" && !/not a git repository|Needed a single revision|unknown revision/.test(head.stderr ?? ""))
|
|
182
|
+
throw head.error ?? new Error(`Git source identity is unavailable: ${head.stderr}`);
|
|
183
|
+
const capture = () => {
|
|
184
|
+
const root = resolve(worktree);
|
|
185
|
+
const untracked = [];
|
|
186
|
+
const ignored = new Set([".git", ".rafi", ".foreman", "node_modules"]);
|
|
187
|
+
const visit = (directory) => {
|
|
188
|
+
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
189
|
+
if (ignored.has(entry.name))
|
|
190
|
+
continue;
|
|
191
|
+
const path = join(directory, entry.name);
|
|
192
|
+
if (entry.isDirectory())
|
|
193
|
+
visit(path);
|
|
194
|
+
else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
195
|
+
const kind = entry.isSymbolicLink() ? "symlink" : "file";
|
|
196
|
+
const bytes = kind === "symlink" ? Buffer.from(readlinkSync(path)) : readFileSync(path);
|
|
197
|
+
untracked.push({ path: relative(root, path).replace(/\\/g, "/"), kind, bytes, mode: lstatSync(path).mode, digest: createHash("sha256").update(bytes).digest("hex") });
|
|
198
|
+
}
|
|
199
|
+
else
|
|
200
|
+
throw new Error(`Unsupported source path type: ${path}`);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
visit(root);
|
|
204
|
+
const contentDigest = createHash("sha256").update(JSON.stringify(untracked.map(({ bytes: _, ...identity }) => identity))).digest("hex");
|
|
205
|
+
return { digest: contentDigest, head: "unavailable", captureMode: "filesystem", omissions: ["Git baseline/diff unavailable: project is not a repository or has no first commit"], contentDigest, capturedAt: new Date().toISOString(),
|
|
206
|
+
pathInventory: untracked.map(file => ({ path: file.path, staged: [], unstaged: [], untracked: { kind: file.kind, mode: file.mode, digest: file.digest } })), untracked, stagedDiff: Buffer.alloc(0), unstagedDiff: Buffer.alloc(0) };
|
|
207
|
+
};
|
|
208
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
209
|
+
const before = capture();
|
|
210
|
+
const after = capture();
|
|
211
|
+
if (before.digest === after.digest)
|
|
212
|
+
return after;
|
|
213
|
+
}
|
|
214
|
+
throw new Error("Product source changed during assignment evidence capture; retry at a stable boundary");
|
|
215
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import type { ManagerActionRequestV1 } from "rafi-spec";
|
|
3
|
+
export type InstructionRecipient = "builder" | "qa";
|
|
4
|
+
export type InstructionState = "queued" | "reserved" | "delivered" | "acknowledged" | "applied" | "verified" | "uncertain" | "rejected" | "superseded" | "withdrawn";
|
|
5
|
+
export interface BuildInstruction {
|
|
6
|
+
instructionId: string;
|
|
7
|
+
request: ManagerActionRequestV1;
|
|
8
|
+
sequence: number;
|
|
9
|
+
userTurn: string;
|
|
10
|
+
textDigest: string;
|
|
11
|
+
requirementsDigest: string;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
basis: {
|
|
14
|
+
assignmentId: string;
|
|
15
|
+
scopeRevision: string;
|
|
16
|
+
reviewAttemptId?: string;
|
|
17
|
+
sourceDigest?: string;
|
|
18
|
+
};
|
|
19
|
+
deliveries: Array<{
|
|
20
|
+
recipient: InstructionRecipient;
|
|
21
|
+
state: InstructionState;
|
|
22
|
+
operationId?: string;
|
|
23
|
+
sourceDigest?: string;
|
|
24
|
+
instructionDigest?: string;
|
|
25
|
+
receipt?: unknown;
|
|
26
|
+
detail?: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
export declare class BuildControlBoundary extends Error {
|
|
30
|
+
}
|
|
31
|
+
export declare function migrateBuildInterventions(db: Database.Database): void;
|
|
32
|
+
/** Questions belong to work through their ticket interruption or retained remediation stop. */
|
|
33
|
+
export declare function decisionBelongsToWork(decision: {
|
|
34
|
+
decisionId: string;
|
|
35
|
+
runId: string;
|
|
36
|
+
interruptionId: string;
|
|
37
|
+
}, runId: string, workId: string, stops: Array<{
|
|
38
|
+
runId: string;
|
|
39
|
+
workId: string;
|
|
40
|
+
operationId: string;
|
|
41
|
+
decisionId?: string;
|
|
42
|
+
}>): boolean;
|
|
43
|
+
export declare function decisionWorkId(db: Database.Database, decision: {
|
|
44
|
+
decisionId: string;
|
|
45
|
+
runId: string;
|
|
46
|
+
interruptionId: string;
|
|
47
|
+
}): string | undefined;
|
|
48
|
+
/** A proposal never authorizes itself. Only exact commands from the original user turn do. */
|
|
49
|
+
export declare function parseManagerAction(userTurn: string, requestId?: string, expectedRevision?: number): ManagerActionRequestV1 | undefined;
|
|
50
|
+
/** Narrow write capability coexists with a build lease and cannot dispatch or publish workflow state. */
|
|
51
|
+
export declare class BuildInterventionControl {
|
|
52
|
+
private readonly runId;
|
|
53
|
+
private readonly workId;
|
|
54
|
+
private readonly db;
|
|
55
|
+
constructor(projectDir: string, runId: string, workId: string);
|
|
56
|
+
close(): void;
|
|
57
|
+
revision(): number;
|
|
58
|
+
enqueue(request: ManagerActionRequestV1, userTurn: string): BuildInstruction;
|
|
59
|
+
}
|
|
60
|
+
export declare function instruction(db: Database.Database, id: string): BuildInstruction | undefined;
|
|
61
|
+
/** Restart consumes retained completion proof, never sends a replacement provider turn. */
|
|
62
|
+
export declare function reconcileGuidance(db: Database.Database, runId: string, workId: string): void;
|
|
63
|
+
export declare function queuedControls(db: Database.Database, runId: string, workId: string): BuildInstruction[];
|
|
64
|
+
export declare function hasQueuedBuilderGuidance(db: Database.Database, runId: string, workId: string): boolean;
|
|
65
|
+
export declare function completeControl(db: Database.Database, id: string, receipt: unknown, rejection?: string): void;
|
|
66
|
+
export declare function assertFinalizationControls(db: Database.Database, runId: string, workId: string): void;
|
|
67
|
+
export declare function builderVerificationContext(db: Database.Database, runId: string, workId: string, sourceDigest: string): {
|
|
68
|
+
text: string;
|
|
69
|
+
ids: string[];
|
|
70
|
+
};
|
|
71
|
+
export declare function verifyBuilderGuidance(db: Database.Database, ids: string[], certificate: {
|
|
72
|
+
certificateId: string;
|
|
73
|
+
sourceStateDigest: string;
|
|
74
|
+
reviewBasisDigest: string;
|
|
75
|
+
}, response: string, turnReceiptDigest: string): void;
|
|
76
|
+
/** Called only inside the owning process's existing dispatch transaction. */
|
|
77
|
+
export declare function reserveGuidance(db: Database.Database, runId: string, workId: string, recipient: InstructionRecipient, operationId: string, sourceDigest: string, instructionText: string, reserve?: boolean): {
|
|
78
|
+
text: string;
|
|
79
|
+
ids: string[];
|
|
80
|
+
};
|
|
81
|
+
export declare function finishGuidance(db: Database.Database, ids: string[], recipient: InstructionRecipient, result: {
|
|
82
|
+
submitted: boolean | undefined;
|
|
83
|
+
receipt?: unknown;
|
|
84
|
+
applied?: boolean;
|
|
85
|
+
}): void;
|
|
86
|
+
export declare function verifyGuidance(db: Database.Database, ids: string[], certificate: {
|
|
87
|
+
certificateId: string;
|
|
88
|
+
sourceStateDigest: string;
|
|
89
|
+
reviewBasisDigest: string;
|
|
90
|
+
}, response: string): void;
|