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
|
@@ -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;
|