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.
Files changed (189) hide show
  1. package/README.md +12 -2
  2. package/dist/activity.d.ts +3 -0
  3. package/dist/activity.js +18 -6
  4. package/dist/adapters/claude.d.ts +16 -0
  5. package/dist/adapters/claude.js +28 -3
  6. package/dist/adapters/codex.d.ts +20 -0
  7. package/dist/adapters/codex.js +99 -5
  8. package/dist/adapters/recovering.d.ts +9 -0
  9. package/dist/adapters/recovering.js +10 -0
  10. package/dist/adapters/types.d.ts +15 -0
  11. package/dist/agentRun.d.ts +7 -0
  12. package/dist/agentRun.js +24 -13
  13. package/dist/branch/currentGuard.d.ts +1 -0
  14. package/dist/branch/currentGuard.js +1 -0
  15. package/dist/branch/finalization.js +6 -1
  16. package/dist/branch/git.d.ts +1 -0
  17. package/dist/branch/git.js +15 -1
  18. package/dist/branch/runner.d.ts +2 -0
  19. package/dist/branch/runner.js +90 -4
  20. package/dist/buildAdmission.js +2 -0
  21. package/dist/buildAssignment.d.ts +33 -0
  22. package/dist/buildAssignment.js +215 -0
  23. package/dist/buildInterventions.d.ts +90 -0
  24. package/dist/buildInterventions.js +334 -0
  25. package/dist/buildOwnershipReconciliation.d.ts +19 -0
  26. package/dist/buildOwnershipReconciliation.js +41 -0
  27. package/dist/buildOwnershipRepair.d.ts +33 -0
  28. package/dist/buildOwnershipRepair.js +111 -0
  29. package/dist/buildRuns.d.ts +6 -0
  30. package/dist/buildRuns.js +23 -17
  31. package/dist/buildSyntheticWork.d.ts +2 -0
  32. package/dist/buildSyntheticWork.js +30 -0
  33. package/dist/buildWorkAdmission.d.ts +17 -0
  34. package/dist/buildWorkAdmission.js +57 -0
  35. package/dist/buildWorkAuthorization.d.ts +2 -0
  36. package/dist/buildWorkAuthorization.js +23 -0
  37. package/dist/buildWorkContext.d.ts +3 -0
  38. package/dist/buildWorkContext.js +6 -0
  39. package/dist/buildWorkMigration.d.ts +9 -0
  40. package/dist/buildWorkMigration.js +206 -0
  41. package/dist/buildWorkReconciliation.d.ts +12 -0
  42. package/dist/buildWorkReconciliation.js +70 -0
  43. package/dist/builderGuidanceFollowup.d.ts +15 -0
  44. package/dist/builderGuidanceFollowup.js +34 -0
  45. package/dist/cli/doctor.js +3 -2
  46. package/dist/cli/graph.d.ts +2 -0
  47. package/dist/cli/graph.js +75 -0
  48. package/dist/cli/manager.d.ts +8 -0
  49. package/dist/cli/manager.js +234 -12
  50. package/dist/cli/recovery.js +3 -2
  51. package/dist/cli/runtimeAuthPrompt.js +28 -8
  52. package/dist/cli/start.js +152 -48
  53. package/dist/cli/status.js +20 -5
  54. package/dist/cli/tickets.js +9 -7
  55. package/dist/continuity.d.ts +11 -0
  56. package/dist/continuity.js +22 -6
  57. package/dist/foreman.d.ts +10 -2
  58. package/dist/foreman.js +330 -39
  59. package/dist/graph/boundary.d.ts +15 -0
  60. package/dist/graph/boundary.js +50 -0
  61. package/dist/graph/bridge.d.ts +9 -0
  62. package/dist/graph/bridge.js +146 -0
  63. package/dist/graph/bridge.py +205 -0
  64. package/dist/graph/config.d.ts +11 -0
  65. package/dist/graph/config.js +40 -0
  66. package/dist/graph/corpus.d.ts +16 -0
  67. package/dist/graph/corpus.js +188 -0
  68. package/dist/graph/derived.d.ts +17 -0
  69. package/dist/graph/derived.js +73 -0
  70. package/dist/graph/host.d.ts +4 -0
  71. package/dist/graph/host.js +160 -0
  72. package/dist/graph/install.d.ts +3 -0
  73. package/dist/graph/install.js +50 -0
  74. package/dist/graph/lifecycle.d.ts +11 -0
  75. package/dist/graph/lifecycle.js +124 -0
  76. package/dist/graph/maintenance.d.ts +43 -0
  77. package/dist/graph/maintenance.js +301 -0
  78. package/dist/graph/manager.d.ts +9 -0
  79. package/dist/graph/manager.js +92 -0
  80. package/dist/graph/preparation.d.ts +41 -0
  81. package/dist/graph/preparation.js +43 -0
  82. package/dist/graph/read.d.ts +27 -0
  83. package/dist/graph/read.js +152 -0
  84. package/dist/graph/semantic.d.ts +17 -0
  85. package/dist/graph/semantic.js +27 -0
  86. package/dist/graph/session.d.ts +8 -0
  87. package/dist/graph/session.js +92 -0
  88. package/dist/graph/sources.d.ts +4 -0
  89. package/dist/graph/sources.js +11 -0
  90. package/dist/graph/storage.d.ts +38 -0
  91. package/dist/graph/storage.js +159 -0
  92. package/dist/graph/turn.d.ts +17 -0
  93. package/dist/graph/turn.js +360 -0
  94. package/dist/graph/util.d.ts +5 -0
  95. package/dist/graph/util.js +51 -0
  96. package/dist/handoffs.d.ts +1 -0
  97. package/dist/handoffs.js +5 -1
  98. package/dist/humanDecision.d.ts +21 -2
  99. package/dist/humanDecision.js +169 -11
  100. package/dist/index.js +2 -0
  101. package/dist/installOwnership.d.ts +46 -0
  102. package/dist/installOwnership.js +440 -0
  103. package/dist/linuxProbeInventory.d.ts +12 -0
  104. package/dist/linuxProbeInventory.js +48 -0
  105. package/dist/log.d.ts +1 -1
  106. package/dist/log.js +7 -1
  107. package/dist/managerEvidence.d.ts +25 -0
  108. package/dist/managerEvidence.js +326 -0
  109. package/dist/managerEvidenceArtifacts.d.ts +38 -0
  110. package/dist/managerEvidenceArtifacts.js +118 -0
  111. package/dist/managerPacket.d.ts +2 -0
  112. package/dist/managerPacket.js +23 -0
  113. package/dist/markers.d.ts +1 -1
  114. package/dist/markers.js +2 -2
  115. package/dist/observability.js +1 -1
  116. package/dist/processIdentity.js +7 -0
  117. package/dist/providerPhase.d.ts +13 -0
  118. package/dist/providerPhase.js +26 -0
  119. package/dist/qaAuthorizedDisposition.d.ts +24 -0
  120. package/dist/qaAuthorizedDisposition.js +26 -0
  121. package/dist/qaBuildGate.d.ts +4 -0
  122. package/dist/qaBuildGate.js +216 -0
  123. package/dist/qaBuilderCoverage.d.ts +5 -0
  124. package/dist/qaBuilderCoverage.js +78 -0
  125. package/dist/qaContractCoverage.d.ts +10 -0
  126. package/dist/qaContractCoverage.js +55 -0
  127. package/dist/qaContractDelivery.d.ts +23 -0
  128. package/dist/qaContractDelivery.js +145 -0
  129. package/dist/qaContractFreshness.d.ts +15 -0
  130. package/dist/qaContractFreshness.js +36 -0
  131. package/dist/qaDeliveryJournal.d.ts +10 -0
  132. package/dist/qaDeliveryJournal.js +16 -1
  133. package/dist/qaEffectiveConfig.d.ts +20 -0
  134. package/dist/qaEffectiveConfig.js +40 -0
  135. package/dist/qaEvidenceReader.d.ts +14 -0
  136. package/dist/qaEvidenceReader.js +176 -0
  137. package/dist/qaFailureDelivery.d.ts +2 -0
  138. package/dist/qaFailureDelivery.js +107 -10
  139. package/dist/qaHandbackMigration.d.ts +1 -1
  140. package/dist/qaHandbackMigration.js +3 -3
  141. package/dist/qaPreparation.d.ts +26 -0
  142. package/dist/qaPreparation.js +150 -0
  143. package/dist/qaPreparationInputs.d.ts +8 -0
  144. package/dist/qaPreparationInputs.js +47 -0
  145. package/dist/qaPreparationMetrics.d.ts +145 -0
  146. package/dist/qaPreparationMetrics.js +120 -0
  147. package/dist/qaPreparationPolicy.d.ts +5 -0
  148. package/dist/qaPreparationPolicy.js +51 -0
  149. package/dist/qaPreparationStore.d.ts +157 -0
  150. package/dist/qaPreparationStore.js +412 -0
  151. package/dist/qaProtocolV2.d.ts +28 -0
  152. package/dist/qaProtocolV2.js +12 -0
  153. package/dist/qaReview.d.ts +6 -0
  154. package/dist/qaReview.js +234 -24
  155. package/dist/qaTimeline.d.ts +18 -0
  156. package/dist/qaTimeline.js +123 -0
  157. package/dist/qaVerificationContract.d.ts +17 -0
  158. package/dist/qaVerificationContract.js +236 -0
  159. package/dist/readinessCleanup.js +12 -5
  160. package/dist/recoveryGuidance.d.ts +9 -0
  161. package/dist/recoveryGuidance.js +13 -0
  162. package/dist/runtimeAuth.d.ts +1 -0
  163. package/dist/runtimeAuth.js +5 -1
  164. package/dist/runtimeReadiness.d.ts +1 -0
  165. package/dist/runtimeReadiness.js +13 -1
  166. package/dist/sessionIdentity.d.ts +2 -0
  167. package/dist/sessionIdentity.js +14 -0
  168. package/dist/sessionLifecycle.js +3 -3
  169. package/dist/sources/sourceRegistry.js +7 -0
  170. package/dist/stateTransfer.js +50 -1
  171. package/dist/statusReporter.d.ts +1 -0
  172. package/dist/statusReporter.js +1 -0
  173. package/dist/supervisedStart.js +18 -8
  174. package/dist/terminalStyle.d.ts +21 -0
  175. package/dist/terminalStyle.js +50 -0
  176. package/dist/ticketPopulation.d.ts +1 -0
  177. package/dist/ticketPopulation.js +5 -1
  178. package/dist/tickets/commands.js +2 -2
  179. package/dist/tickets/queue.js +1 -1
  180. package/dist/tickets/ticketSchema.d.ts +198 -0
  181. package/dist/tickets/ticketSchema.js +2 -0
  182. package/dist/workflowDb.d.ts +51 -5
  183. package/dist/workflowDb.js +338 -7
  184. package/dist/workflowReader.d.ts +109 -2
  185. package/dist/workflowReader.js +48 -4
  186. package/package.json +84 -7
  187. package/scripts/copy-graph-assets.mjs +3 -0
  188. package/scripts/graph-benchmark.mjs +20 -0
  189. package/scripts/graph-evaluation.mjs +85 -0
@@ -0,0 +1,26 @@
1
+ export class ProviderPhaseBarrier {
2
+ purpose = "implementation";
3
+ needsRenewal = false;
4
+ enforcing = false;
5
+ compactionSequence = 0;
6
+ begin(purpose) { this.purpose = purpose; }
7
+ enableEnforcement() { if (!this.enforcing)
8
+ this.needsRenewal = true; this.enforcing = true; }
9
+ compact() { this.compactionSequence++; if (this.enforcing)
10
+ this.needsRenewal = true; }
11
+ accept(sequence) { if (sequence !== this.compactionSequence)
12
+ throw new Error("Compaction occurred during contract delivery"); this.needsRenewal = false; }
13
+ denial(tool) {
14
+ if (this.enforcing && ["Agent", "Task", "TaskOutput"].includes(tool))
15
+ return "Enforcing Builder cannot delegate mutation outside the contract tool barrier";
16
+ if (this.needsRenewal)
17
+ return "Contract delivery renewal required after native compaction; implementation tools paused";
18
+ if (this.purpose === "implementation")
19
+ return undefined;
20
+ if (["contract-acceptance", "initialization", "response-repair"].includes(this.purpose))
21
+ return "Host response-only phase forbids all tools";
22
+ if (!["Read", "Glob", "Grep", "ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool))
23
+ return "Host planning phase forbids mutation, shell commands, installs, and child agents";
24
+ return undefined;
25
+ }
26
+ }
@@ -0,0 +1,24 @@
1
+ import type Database from "better-sqlite3";
2
+ import type { QaContractCandidateV1, VerificationCheckV1, VerificationMethod } from "rafi-spec";
3
+ export interface AuthorizedEquivalentV1 {
4
+ version: 1;
5
+ kind: "qa-equivalent-verification";
6
+ runId: string;
7
+ workId: string;
8
+ admissionDigest: string;
9
+ contractId: string;
10
+ revision: number;
11
+ checkId: string;
12
+ expectationDigest: string;
13
+ method: Omit<VerificationMethod, "equivalentAuthorityId">;
14
+ reason: string;
15
+ validity: {
16
+ runtime: string;
17
+ expiresAt: string;
18
+ sourceDigest?: string;
19
+ };
20
+ }
21
+ export type EquivalentAuthorityResolver = (reference: string, candidate: QaContractCandidateV1, check: VerificationCheckV1, method: VerificationMethod, sourceDigest?: string, nowMs?: number) => boolean;
22
+ export declare function expectationDigest(check: VerificationCheckV1): string;
23
+ /** Only a human answer to an exactly scoped host decision supplies approval. Agent strings never do. */
24
+ export declare function persistedEquivalentResolver(db: Database.Database): EquivalentAuthorityResolver;
@@ -0,0 +1,26 @@
1
+ import { canonicalJson } from "./qaProtocolV2.js";
2
+ import { contractDigest } from "./qaVerificationContract.js";
3
+ export function expectationDigest(check) {
4
+ const { verification: _methods, dispositionRef: _disposition, ...expectation } = check;
5
+ return contractDigest("equivalent-expectation", expectation);
6
+ }
7
+ /** Only a human answer to an exactly scoped host decision supplies approval. Agent strings never do. */
8
+ export function persistedEquivalentResolver(db) {
9
+ return (reference, candidate, check, method, sourceDigest, nowMs = Date.now()) => {
10
+ const row = db.prepare("SELECT decision_json FROM human_decisions WHERE decision_id=? AND run_id=?").get(reference, candidate.runId);
11
+ if (!row)
12
+ return false;
13
+ const decision = JSON.parse(row.decision_json);
14
+ if (decision.status !== "answered" || decision.selectedChoiceId !== "custom" || !decision.answeredAt || decision.interruptionId !== `qa-equivalent:${candidate.contractId}:${candidate.revision}:${check.id}` || !decision.answer)
15
+ return false;
16
+ let authority;
17
+ try {
18
+ authority = JSON.parse(decision.answer);
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ const { equivalentAuthorityId: _reference, ...concreteMethod } = method;
24
+ return authority.version === 1 && authority.kind === "qa-equivalent-verification" && authority.runId === candidate.runId && authority.workId === candidate.workId && authority.admissionDigest === candidate.admissionDigest && authority.contractId === candidate.contractId && authority.revision === candidate.revision && authority.checkId === check.id && authority.expectationDigest === expectationDigest(check) && Boolean(authority.reason?.trim()) && canonicalJson(authority.method) === canonicalJson(concreteMethod) && authority.validity?.runtime === method.runtime && Date.parse(authority.validity.expiresAt) > nowMs && (!authority.validity.sourceDigest || authority.validity.sourceDigest === sourceDigest);
25
+ };
26
+ }
@@ -0,0 +1,4 @@
1
+ import type { BuilderAdapter } from "./adapters/types.js";
2
+ import type { QaSessionHandle } from "./qaReview.js";
3
+ /** Shared by ordinary, branch, synthetic, remediation and Manager followups. */
4
+ export declare function ensureBuilderContract(projectDir: string, runId: string, workId: string, workspace: string, builder: BuilderAdapter, qaFactory?: (cwd: string) => Promise<QaSessionHandle>): Promise<string>;
@@ -0,0 +1,216 @@
1
+ import { resolveQaPreparationConfig } from "./qaPreparationPolicy.js";
2
+ import { createHash } from "node:crypto";
3
+ import { readFileSync, realpathSync } from "node:fs";
4
+ import { join, resolve, sep } from "node:path";
5
+ import { isTicketsInitialized, loadTicketsConfig } from "./tickets/config.js";
6
+ import { loadTickets } from "./tickets/ticketLoader.js";
7
+ import { qaDigest } from "./qaProtocolV2.js";
8
+ import { admittedPlanInput } from "./qaPreparationInputs.js";
9
+ import { checkQaPrerequisites } from "./qaPrerequisites.js";
10
+ import { OperationDeadline } from "./util/deadline.js";
11
+ import { sendPreparationGraphTurn } from "./graph/preparation.js";
12
+ import { withGraphDerivedAccess } from "./graph/derived.js";
13
+ import { WorkflowDb } from "./workflowDb.js";
14
+ import { createRoleBuilder, readOnlyPermissionConfig } from "./agentRun.js";
15
+ import { captureBuildSource, assertBuildAssignmentReconciled } from "./buildAssignment.js";
16
+ import { createDisposableQaSnapshotAsync } from "./qaSnapshot.js";
17
+ import { allocateStableCheckIds, authoritativeInventory, contractDigest, renderVerificationContract } from "./qaVerificationContract.js";
18
+ import { resolveEffectiveQaConfiguration } from "./qaEffectiveConfig.js";
19
+ import { prepareVerificationContract } from "./qaPreparation.js";
20
+ import { actualContractSession, assertContractReceipt, deliverVerificationContract } from "./qaContractDelivery.js";
21
+ import { COVERAGE_INSTRUCTIONS } from "./qaContractCoverage.js";
22
+ import { renderBuildWorkContext } from "./buildWorkContext.js";
23
+ import { compareContractInputs } from "./qaContractFreshness.js";
24
+ /** Shared by ordinary, branch, synthetic, remediation and Manager followups. */
25
+ export async function ensureBuilderContract(projectDir, runId, workId, workspace, builder, qaFactory) {
26
+ return withGraphDerivedAccess([], () => ensureBuilderContractWithProvenance(projectDir, runId, workId, workspace, builder, qaFactory));
27
+ }
28
+ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, workspace, builder, qaFactory) {
29
+ const db = new WorkflowDb(projectDir);
30
+ try {
31
+ db.ensureRun(runId);
32
+ const store = db.qaPreparationStore();
33
+ const policy = store.policy(runId) ?? store.freezePolicy(runId, resolveQaPreparationConfig(undefined));
34
+ if (policy.mode === "legacy")
35
+ return "";
36
+ if (policy.mode === "enforce" && !builder.contractCapabilities?.().nativeCompactionBarrier)
37
+ throw new Error(`unsupported-capability: ${builder.agent} enforcement requires proven native-compaction delivery continuity`);
38
+ assertBuildAssignmentReconciled(projectDir, runId, workspace);
39
+ const admission = db.assertAdmittedWork(runId, workId);
40
+ const ticket = db.workDefinitions(runId).find(work => work.workId === workId)?.definition;
41
+ if (!ticket)
42
+ throw new Error("Preparation requires the frozen admitted work definition");
43
+ if (admission.kind === "ticket" && isTicketsInitialized(projectDir)) {
44
+ const current = loadTickets(join(projectDir, loadTicketsConfig(projectDir).paths.tickets)).find(ticket => ticket.id === workId);
45
+ if (!current || qaDigest("admitted-requirements", current) !== admission.requirementsDigest)
46
+ throw new Error("Ticket changed from frozen admitted scope; renewed approval is required before preparation or implementation");
47
+ }
48
+ const state = db.getRun(runId)?.state;
49
+ const effective = resolveEffectiveQaConfiguration(projectDir, { make: state?.qa?.settings?.make ?? builder.agent });
50
+ const configurationEvent = `preparation-configuration:${runId}:${workId}:${effective.digest}`;
51
+ if (!store.eventRecord(configurationEvent))
52
+ store.event(runId, workId, configurationEvent, "effective-preparation-configuration", { policy, effectiveDigest: effective.digest, configRoot: effective.configRoot, roleSource: effective.roleSource, provider: builder.agent, capabilities: builder.contractCapabilities?.() ?? null, finalReviewEnabled: db.getRun(runId)?.state?.qaEnabled ?? null });
53
+ const inputs = [
54
+ { id: "admitted-ticket", kind: "ticket", reference: admission.assignmentId, digest: contractDigest("input-ticket", ticket), authority: "approved-scope", revision: admission.scopeRevision, availability: "available" },
55
+ { id: "project-checklist", kind: "checklist", reference: "tickets.build.validation_checklist", digest: contractDigest("input-checklist", effective.checklist), authority: "project-rule", revision: "current", availability: "available" },
56
+ { id: "effective-rules", kind: "rules", reference: effective.configRoot, digest: effective.digest, authority: "project-rule", revision: "current", availability: "available" },
57
+ { id: "preparation-policy", kind: "policy", reference: policy.policyVersion, digest: contractDigest("input-policy", policy), authority: "planner-policy", revision: policy.policyVersion, availability: "available" },
58
+ ];
59
+ const planInput = admittedPlanInput(projectDir, ticket);
60
+ if (planInput)
61
+ inputs.push(planInput.input);
62
+ const inventory = [...authoritativeInventory(ticket, effective.checklist, inputs), ...(planInput?.requirements ?? [])];
63
+ for (const [locator, statement] of [["qa-rules", effective.qaRules], ["builder-rules", effective.builderRules], ...effective.skills.map(skill => [`skill:${skill.name}`, skill.content])]) {
64
+ if (!statement.trim())
65
+ continue;
66
+ inventory.push({ id: `req-rules-${contractDigest("rule-identity", { locator, statement }).slice(0, 24)}`, inputRef: "effective-rules", locator, statement, digest: contractDigest("statement", statement), obligation: "mandatory", origin: "explicit", authority: "approved" });
67
+ }
68
+ const source = captureBuildSource(workspace);
69
+ const head = store.head(runId, workId, admission.requirementsDigest);
70
+ const priorBudget = store.budget(runId, workId, admission.requirementsDigest);
71
+ if (priorBudget)
72
+ store.applyAnsweredExtensions(priorBudget);
73
+ let amendmentPredecessor;
74
+ if (head.digest) {
75
+ const old = store.contract(head.digest);
76
+ const comparison = compareContractInputs(old, inputs);
77
+ const inputDigest = contractDigest("amended-inputs", inputs);
78
+ const retainedIntent = priorBudget ? store.progress(priorBudget, "amendment-inputs") : undefined;
79
+ const changedDuringAmendment = head.state !== "ready" && retainedIntent?.predecessorDigest === old.contentDigest && retainedIntent.inputDigest !== inputDigest;
80
+ if (comparison.changed.length || comparison.unavailable.length || changedDuringAmendment) {
81
+ const qaHead = db.qaTicketHead(runId, workId);
82
+ if (["completed", "waived", "finalizing", "turn-intended", "turn-uncertain", "remediation-intended", "remediation-uncertain"].includes(qaHead.state))
83
+ throw new Error("Active or finalized QA authority requires explicit recovery/reopen before contract amendment");
84
+ const budget = priorBudget;
85
+ if (retainedIntent?.inputDigest !== inputDigest || retainedIntent.predecessorDigest !== old.contentDigest)
86
+ store.beginAmendment(budget, inventory, { reason: `Canonical authorized inputs changed: ${[...comparison.changed, ...comparison.unavailable].join(", ") || "inputs reverted during amendment"}`, inputDigest, predecessorDigest: old.contentDigest });
87
+ amendmentPredecessor = old.contentDigest;
88
+ }
89
+ }
90
+ const initial = { version: 1, contractId: head.digest ? store.contract(head.digest).contractId : `contract:${contractDigest("contract-family", { runId, workId, admission: admission.requirementsDigest })}`, revision: head.digest ? store.contract(head.digest).revision + (amendmentPredecessor ? 1 : 0) : 1,
91
+ ...(amendmentPredecessor ? { predecessorDigest: amendmentPredecessor } : {}),
92
+ runId, workId, admissionDigest: admission.requirementsDigest, depthDecision: ticket.qa_preparation,
93
+ inputs, baseline: [], requirements: inventory, checks: [], coverage: [], preparationEvidence: [], unresolved: [], createdAt: new Date(priorBudget?.startMs ?? Date.now()).toISOString() };
94
+ const result = await prepareVerificationContract(initial, inventory, {
95
+ store, now: Date.now,
96
+ normalizeDraft: (draft, response) => {
97
+ const normalized = allocateStableCheckIds(draft);
98
+ for (const observation of normalized.baseline) {
99
+ const original = structuredClone(observation);
100
+ observation.sourceDigest = source.digest;
101
+ observation.observedAt = new Date().toISOString();
102
+ observation.phase = "preimplementation-investigation";
103
+ observation.evidenceDigest = store.putArtifact("baseline-observation", { observation: original, sourceDigest: source.digest, operationId: response.operationId, sessionId: response.sessionId, providerReceipt: store.putArtifact("investigation-evidence", response) });
104
+ }
105
+ for (const evidence of normalized.preparationEvidence) {
106
+ evidence.operationId = response.operationId;
107
+ evidence.sessionId = response.sessionId;
108
+ delete evidence.graphReceiptRefs;
109
+ if (response.graphReceiptRefs?.length)
110
+ evidence.graphReceiptRefs = [...response.graphReceiptRefs];
111
+ evidence.evidenceDigest = store.putArtifact("investigation-evidence", response);
112
+ for (const reference of evidence.references) {
113
+ const path = realpathSync(resolve(workspace, reference.path)), root = realpathSync(workspace);
114
+ if (!path.startsWith(root + sep))
115
+ throw new Error("Inspected source reference escapes actual workspace");
116
+ reference.digest = createHash("sha256").update(readFileSync(path)).digest("hex");
117
+ }
118
+ }
119
+ return normalized;
120
+ },
121
+ inputsCurrent: candidate => {
122
+ if (db.assertAdmittedWork(runId, workId).requirementsDigest !== admission.requirementsDigest)
123
+ return false;
124
+ const current = resolveEffectiveQaConfiguration(projectDir, { make: state?.qa?.settings?.make ?? builder.agent });
125
+ const compared = compareContractInputs(candidate, inputs.map(input => input.id === "effective-rules" ? { ...input, digest: current.digest } : input.id === "project-checklist" ? { ...input, digest: contractDigest("input-checklist", current.checklist) } : input));
126
+ return !compared.changed.length && !compared.unavailable.length;
127
+ },
128
+ dispatch: async (phase, instruction, operationId, remainingMs) => new OperationDeadline("QA preparation operation", remainingMs).run(async () => {
129
+ const started = performance.now();
130
+ const remaining = () => {
131
+ const value = remainingMs - (performance.now() - started);
132
+ if (value <= 0)
133
+ throw new Error("QA preparation deadline expired during session setup; completion is unknown");
134
+ return value;
135
+ };
136
+ const before = captureBuildSource(workspace);
137
+ if (phase === "planner") {
138
+ const role = await createRoleBuilder({ projectDir: workspace, configRoot: projectDir, role: "planner", label: "QA depth assessment", sandboxMode: "read-only", permissionConfig: readOnlyPermissionConfig(), yes: true, allowSwitch: false });
139
+ try {
140
+ const turn = await sendPreparationGraphTurn(role.builder, `${instruction}\nFrozen admitted work definition: ${JSON.stringify(ticket)}\nOriginal approved plan context: ${JSON.stringify(planInput?.plan ?? null)}\nFrozen effective configuration: ${JSON.stringify(effective)}`, { phase, projectDir, workspace, sourceRef: before.digest, runId, workId, operationId, remainingMs: remaining() });
141
+ store.event(runId, workId, `${operationId}:raw`, "preparation-provider-return", turn);
142
+ if (captureBuildSource(workspace).contentDigest !== before.contentDigest)
143
+ throw new Error("Product source changed during confined planning; reconcile retained dispatch");
144
+ if (turn.isError || turn.failure || !role.builder.sessionId())
145
+ throw new Error(`planner-unavailable: ${turn.text}`);
146
+ return { text: turn.text, graphReceiptRefs: turn.graphReceiptRefs, sessionId: role.builder.sessionId(), usage: { inputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.inputTokens : turn.inputTokens, outputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.outputTokens : turn.outputTokens, costUsd: turn.costAuthoritative ? turn.costUsd : undefined } };
147
+ }
148
+ finally {
149
+ await role.builder.close();
150
+ }
151
+ }
152
+ const snapshot = await createDisposableQaSnapshotAsync(workspace);
153
+ let adapter;
154
+ try {
155
+ remaining();
156
+ const prerequisites = await checkQaPrerequisites({ snapshotPath: snapshot.path, sourceDigest: snapshot.frozenState.digest, ticket });
157
+ const prerequisiteDigest = db.putEvidence("qa", JSON.stringify(prerequisites));
158
+ remaining();
159
+ if (qaFactory)
160
+ adapter = (await qaFactory(snapshot.path)).adapter;
161
+ else
162
+ adapter = (await createRoleBuilder({ projectDir: snapshot.path, configRoot: projectDir, role: "qa", agent: state?.qa?.settings?.make ?? builder.agent, preloadedSkillContent: effective.skills.map(skill => ({ name: skill.name, content: skill.content })), label: "QA preparation", sandboxMode: "read-only", permissionConfig: readOnlyPermissionConfig(), yes: true, allowSwitch: false })).builder;
163
+ const turn = await sendPreparationGraphTurn(adapter, `${instruction}\nOriginal approved plan context: ${JSON.stringify(planInput?.plan ?? null)}\nFrozen admitted work definition: ${JSON.stringify(ticket)}\nHost early prerequisite observations (host availability is not provider access; classify readiness versus final-only dependencies): ${JSON.stringify({ ...prerequisites, evidenceDigest: prerequisiteDigest })}\nFrozen effective configuration: ${JSON.stringify(effective)}\nOperation: ${operationId}; remaining wall time: ${remainingMs}ms; source: ${source.digest}`, { phase, projectDir, workspace: snapshot.path, accessWorkspace: workspace, sourceRef: snapshot.frozenState.digest, runId, workId, operationId, remainingMs: remaining() });
164
+ store.event(runId, workId, `${operationId}:raw`, "preparation-provider-return", turn);
165
+ if (turn.isError || turn.failure || !adapter.sessionId())
166
+ throw new Error(`Preparation provider unavailable: ${turn.text}`);
167
+ if (captureBuildSource(workspace).contentDigest !== before.contentDigest)
168
+ throw new Error("Source changed during preparation; retained dispatch requires reconciliation");
169
+ return { text: turn.text, graphReceiptRefs: turn.graphReceiptRefs, sessionId: adapter.sessionId(), usage: { inputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.inputTokens : turn.inputTokens, outputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.outputTokens : turn.outputTokens, costUsd: turn.costAuthoritative ? turn.costUsd : undefined } };
170
+ }
171
+ finally {
172
+ try {
173
+ await adapter?.close();
174
+ }
175
+ finally {
176
+ await snapshot.remove();
177
+ }
178
+ }
179
+ }),
180
+ });
181
+ if (result.status !== "ready") {
182
+ if (policy.mode === "shadow")
183
+ return "";
184
+ const budget = store.budget(runId, workId, admission.requirementsDigest);
185
+ if (budget && /budget exhausted|allowance exhausted|deadline expired/i.test(result.detail)) {
186
+ const decision = db.ensureHumanDecision({ runId, decisionKey: `qa-preparation-extension:${budget.id}:${budget.deadlineMs}:${budget.extensions.length}`, interruptionId: `qa-preparation:${budget.id}`, prompt: `Preparation allowance exhausted. Additional time/rounds require an operational extension, preserving depth, scope, original start and all consumed/uncertain dispatches. Supply JSON {"budgetId":"${budget.id}","reason":"operator reason","deadlineMs":<absolute epoch milliseconds>,"caps":{"investigation":<total round ceiling>,"assessment":<total round ceiling>}}. Current budget: ${JSON.stringify(budget)}`, choices: [{ id: "custom", label: "Provide a scoped operational extension using --answer" }] });
187
+ throw new Error(`QA preparation ${result.status}: ${result.detail}. Answer ${decision.decisionId} with rafi build:decide --run ${runId} --decision ${decision.decisionId} --choice custom --answer '<scoped JSON>', then resume. Uncertain dispatches still require reconciliation.`);
188
+ }
189
+ throw new Error(`QA preparation ${result.status}: ${result.detail}. ${result.nextAction}`);
190
+ }
191
+ amendmentPredecessor = result.contract.predecessorDigest;
192
+ if (amendmentPredecessor && !store.eventRecord(`amendment:${result.contract.contentDigest}`)) {
193
+ db.atomic(() => {
194
+ db.invalidateUnconsumedQaPassCertificates(runId, workId, "contract-amended");
195
+ db.markQaReportsRecheckRequired(runId, workId, "contract-amended");
196
+ const qaHead = db.qaTicketHead(runId, workId);
197
+ db.transitionQa(runId, workId, qaHead.revision, { type: "contract-amended", predecessorDigest: amendmentPredecessor, contractDigest: result.contract.contentDigest, reason: "Canonical authorized input reconciliation" });
198
+ store.metric(runId, workId, admission.requirementsDigest, `metric:amendment:${result.contract.contentDigest}`, "amendment");
199
+ store.event(runId, workId, `amendment:${result.contract.contentDigest}`, "contract-amendment", { predecessorDigest: amendmentPredecessor, contractDigest: result.contract.contentDigest, evidenceCarryForward: [], disposition: "All final checks require independent current-source review; no evidence silently carried" });
200
+ });
201
+ }
202
+ if (policy.mode === "shadow")
203
+ return "";
204
+ await builder.prepareSession?.();
205
+ try {
206
+ assertContractReceipt(store, runId, workId, admission.requirementsDigest, actualContractSession(builder, workspace, projectDir), builder);
207
+ }
208
+ catch {
209
+ await deliverVerificationContract(result.contract, store, builder, workspace, projectDir, renderBuildWorkContext(ticket, effective.checklist));
210
+ }
211
+ return `${renderVerificationContract(result.contract)}\n${COVERAGE_INSTRUCTIONS}\nCoverage binding: ${JSON.stringify({ phase: "builder", runId, workId, revision: result.contract.revision, contractDigest: result.contract.contentDigest })}`;
212
+ }
213
+ finally {
214
+ db.close();
215
+ }
216
+ }
@@ -0,0 +1,5 @@
1
+ import type { BuilderAdapter, TurnResult } from "./adapters/types.js";
2
+ import { WorkflowDb } from "./workflowDb.js";
3
+ /** Claims are collected after source capture so the provider can name the actual completed source. */
4
+ export declare function collectBuilderContractCoverage(projectDir: string, runId: string, workId: string, workspace: string, adapter: BuilderAdapter, operationId: string, instruction: string, implementation: TurnResult): Promise<void>;
5
+ export declare function assertBuilderContractCoverage(db: WorkflowDb, runId: string, workId: string, operationId: string, sourceDigest: string): void;
@@ -0,0 +1,78 @@
1
+ import { dispatchWithGraphAccess, withGraphSessionAccess } from "./graph/session.js";
2
+ import { WorkflowDb } from "./workflowDb.js";
3
+ import { actualContractSession, assertContractReceipt } from "./qaContractDelivery.js";
4
+ import { captureBuildSource } from "./buildAssignment.js";
5
+ import { COVERAGE_INSTRUCTIONS, parseContractCoverage, validateContractCoverage } from "./qaContractCoverage.js";
6
+ import { contractDigest } from "./qaVerificationContract.js";
7
+ import { OperationDeadline } from "./util/deadline.js";
8
+ /** Claims are collected after source capture so the provider can name the actual completed source. */
9
+ export async function collectBuilderContractCoverage(projectDir, runId, workId, workspace, adapter, operationId, instruction, implementation) {
10
+ return withGraphSessionAccess(projectDir, adapter, () => collectBuilderContractCoverageOwned(projectDir, runId, workId, workspace, adapter, operationId, instruction, implementation));
11
+ }
12
+ async function collectBuilderContractCoverageOwned(projectDir, runId, workId, workspace, adapter, operationId, instruction, implementation) {
13
+ const db = new WorkflowDb(projectDir);
14
+ try {
15
+ const store = db.qaPreparationStore();
16
+ if (store.policy(runId)?.mode !== "enforce")
17
+ return;
18
+ const admission = db.assertAdmittedWork(runId, workId);
19
+ const session = actualContractSession(adapter, workspace, projectDir);
20
+ const contract = assertContractReceipt(store, runId, workId, admission.requirementsDigest, session, adapter);
21
+ const source = captureBuildSource(workspace);
22
+ const binding = { phase: "builder", sourceDigest: source.digest, inputBasisDigest: contractDigest("builder-input-basis", { operationId, instruction, contractDigest: contract.contentDigest }), attemptId: operationId, sessionId: session.sessionId };
23
+ const eventId = `builder-coverage:${operationId}`;
24
+ const existing = db.operation(eventId);
25
+ if (existing) {
26
+ if (existing.status !== "confirmed")
27
+ throw new Error("Uncertain Builder coverage dispatch must be reconciled without redispatch");
28
+ const retained = existing.result;
29
+ const errors = validateContractCoverage(contract, store.artifact(retained.coverageDigest, "builder-coverage"), binding, false);
30
+ if (errors.length)
31
+ throw new Error(errors.join("; "));
32
+ return;
33
+ }
34
+ // Preserve the implementation response before a failed acknowledgment or a crash.
35
+ const implementationDigest = db.putEvidence("qa", implementation.rawResponse ?? implementation.text);
36
+ db.planOperation({ runId, idempotencyKey: eventId, kind: "builder-coverage", intent: { workId, contractDigest: contract.contentDigest, binding, implementationDigest, session } });
37
+ db.updateOperation(eventId, "in_progress");
38
+ try {
39
+ const prompt = `Response-only evidence collection for completed implementation ${operationId}. Do not run tools, modify files, repeat work, or invent verification. Record failed, blocked or not-run truthfully. The implementation response is retained as ${implementationDigest}.\n${COVERAGE_INSTRUCTIONS}\nExact coverage binding: ${JSON.stringify({ version: 1, runId, workId, revision: contract.revision, contractDigest: contract.contentDigest, ...binding })}\nAll checks: ${JSON.stringify(contract.checks)}`;
40
+ const response = await new OperationDeadline("Builder coverage acknowledgment", 120_000).run(() => dispatchWithGraphAccess(projectDir, adapter, prompt, { purpose: "response-repair", responseOnly: true }, (text, policy) => adapter.sendTurn(text, policy)), () => { void adapter.close(); });
41
+ const responseDigest = db.putEvidence("qa", response.rawResponse ?? response.text);
42
+ db.updateOperation(eventId, "uncertain", { result: { responseDigest } });
43
+ if (response.isError || response.failure)
44
+ throw new Error("Builder coverage provider failed; retained response requires reconciliation");
45
+ if (JSON.stringify(actualContractSession(adapter, workspace, projectDir)) !== JSON.stringify(session) || captureBuildSource(workspace).digest !== source.digest)
46
+ throw new Error("Builder coverage changed session or product source");
47
+ const coverage = parseContractCoverage(response.text);
48
+ const errors = validateContractCoverage(contract, coverage, binding, false);
49
+ if (errors.length)
50
+ throw new Error(`Builder coverage invalid: ${errors.join("; ")}`);
51
+ const coverageDigest = store.putArtifact("builder-coverage", coverage);
52
+ db.updateOperation(eventId, "confirmed", { result: { responseDigest, coverageDigest, binding, implementationDigest } });
53
+ }
54
+ catch (error) {
55
+ throw new Error(`Builder evidence collection blocked: ${String(error)}`);
56
+ }
57
+ }
58
+ finally {
59
+ db.close();
60
+ }
61
+ }
62
+ export function assertBuilderContractCoverage(db, runId, workId, operationId, sourceDigest) {
63
+ const store = db.qaPreparationStore();
64
+ if (store.policy(runId)?.mode !== "enforce")
65
+ return;
66
+ const admission = db.assertAdmittedWork(runId, workId), head = store.head(runId, workId, admission.requirementsDigest);
67
+ if (head.state !== "ready" || !head.digest)
68
+ throw new Error("Completion requires the ready contract");
69
+ const operation = db.operation(`builder-coverage:${operationId}`);
70
+ if (operation?.status !== "confirmed")
71
+ throw new Error("Completion requires retained, source-bound Builder check coverage");
72
+ const result = operation.result;
73
+ if (result.binding.sourceDigest !== sourceDigest || result.binding.attemptId !== operationId)
74
+ throw new Error("Builder completion coverage is stale");
75
+ const errors = validateContractCoverage(store.contract(head.digest), store.artifact(result.coverageDigest, "builder-coverage"), result.binding, false);
76
+ if (errors.length)
77
+ throw new Error(errors.join("; "));
78
+ }
@@ -0,0 +1,10 @@
1
+ import type { QaContractCoverageV1, QaVerificationContractV1 } from "rafi-spec";
2
+ export declare const COVERAGE_INSTRUCTIONS = "Return RAFI_QA_COVERAGE_START, one QaContractCoverageV1 JSON object, RAFI_QA_COVERAGE_END before the final status marker. Include every check ID with independently evaluated applicability, predicate evidence, outcome (passed/failed/blocked/not-run), and concrete procedure/cwd/runtime/sourceDigest/reference evidence. Missing, blocked, not-run, unresolved applicability, or failed mandatory coverage cannot authorize normal completion. Builder coverage is a claim; final QA independently verifies it.";
3
+ export declare function parseContractCoverage(text: string): QaContractCoverageV1;
4
+ export declare function validateContractCoverage(contract: QaVerificationContractV1, coverage: QaContractCoverageV1, binding: {
5
+ phase: "builder" | "qa";
6
+ sourceDigest: string;
7
+ inputBasisDigest: string;
8
+ attemptId: string;
9
+ sessionId: string;
10
+ }, requirePass?: boolean): string[];
@@ -0,0 +1,55 @@
1
+ import { Ajv } from "ajv";
2
+ import { qaContractCoverageV1Schema } from "rafi-spec";
3
+ const coverageSchema = new Ajv({ allErrors: true }).compile(qaContractCoverageV1Schema);
4
+ import { parsePreparationArtifact } from "./qaVerificationContract.js";
5
+ export const COVERAGE_INSTRUCTIONS = `Return RAFI_QA_COVERAGE_START, one QaContractCoverageV1 JSON object, RAFI_QA_COVERAGE_END before the final status marker. Include every check ID with independently evaluated applicability, predicate evidence, outcome (passed/failed/blocked/not-run), and concrete procedure/cwd/runtime/sourceDigest/reference evidence. Missing, blocked, not-run, unresolved applicability, or failed mandatory coverage cannot authorize normal completion. Builder coverage is a claim; final QA independently verifies it.`;
6
+ export function parseContractCoverage(text) { return parsePreparationArtifact(text, "RAFI_QA_COVERAGE"); }
7
+ export function validateContractCoverage(contract, coverage, binding, requirePass = true) {
8
+ if (!coverageSchema(coverage))
9
+ return (coverageSchema.errors ?? []).map(error => `${error.instancePath} ${error.message}`);
10
+ const issues = [];
11
+ if (coverage.version !== 1 || coverage.runId !== contract.runId || coverage.workId !== contract.workId || coverage.revision !== contract.revision || coverage.contractDigest !== contract.contentDigest)
12
+ issues.push("Coverage contract/work binding mismatch");
13
+ for (const key of ["phase", "sourceDigest", "inputBasisDigest", "attemptId", "sessionId"])
14
+ if (coverage[key] !== binding[key])
15
+ issues.push(`Coverage ${key} mismatch`);
16
+ if (!Array.isArray(coverage.checks))
17
+ return [...issues, "Missing check coverage"];
18
+ const seen = new Set();
19
+ for (const row of coverage.checks) {
20
+ const check = contract.checks.find(check => check.id === row.checkId);
21
+ if (!check || seen.has(row.checkId)) {
22
+ issues.push(`Unknown/duplicate coverage ${row.checkId}`);
23
+ continue;
24
+ }
25
+ seen.add(row.checkId);
26
+ if (!["applicable", "not-applicable", "unresolved"].includes(row.applicability) || !["passed", "failed", "blocked", "not-run"].includes(row.outcome))
27
+ issues.push(`Illegal coverage state ${row.checkId}`);
28
+ if (check.applicability.kind === "unconditional" && row.applicability !== "applicable")
29
+ issues.push(`Unconditional applicability cannot be waived ${row.checkId}`);
30
+ if (row.applicability === "not-applicable" && !row.predicateEvidence?.length)
31
+ issues.push(`Not-applicable requires predicate evidence ${row.checkId}`);
32
+ if (check.applicability.kind === "conditional" && row.applicability === "applicable" && !row.predicateEvidence?.length)
33
+ issues.push(`Conditional applicability requires predicate evidence ${row.checkId}`);
34
+ if (requirePass && check.obligation === "mandatory" && (row.applicability === "unresolved" || row.applicability === "applicable" && row.outcome !== "passed"))
35
+ issues.push(`Mandatory check incomplete ${row.checkId}`);
36
+ if (!Array.isArray(row.evidence))
37
+ issues.push(`Missing evidence array ${row.checkId}`);
38
+ if (row.outcome === "passed" && row.applicability === "applicable") {
39
+ if (binding.phase === "qa" && check.timing === "preimplementation") {
40
+ const baseline = contract.baseline.find(observation => observation.checkId === check.id && observation.status === "passed");
41
+ if (!baseline || !row.evidence?.some(evidence => evidence.reference === baseline.evidenceDigest))
42
+ issues.push(`Preimplementation coverage lacks retained baseline authority ${row.checkId}`);
43
+ }
44
+ if (!row.evidence?.length)
45
+ issues.push(`Passing check requires independent evidence ${row.checkId}`);
46
+ for (const evidence of row.evidence ?? [])
47
+ if (!evidence.reference?.trim() || !evidence.procedure?.trim() || !evidence.cwd?.trim() || !evidence.runtime?.trim() || check.timing !== "preimplementation" && evidence.sourceDigest !== binding.sourceDigest)
48
+ issues.push(`Missing/stale source evidence ${row.checkId}`);
49
+ }
50
+ }
51
+ for (const check of contract.checks)
52
+ if (!seen.has(check.id))
53
+ issues.push(`Missing check ${check.id}`);
54
+ return issues;
55
+ }
@@ -0,0 +1,23 @@
1
+ import type { QaVerificationContractV1, QaContractDeliveryReceiptV1 } from "rafi-spec";
2
+ import type { BuilderAdapter } from "./adapters/types.js";
3
+ import type { QaPreparationStore } from "./qaPreparationStore.js";
4
+ export interface ContractSessionBinding {
5
+ provider: string;
6
+ sessionId: string;
7
+ generation: number;
8
+ workspace: string;
9
+ configRoot: string;
10
+ compactionSequence: number;
11
+ }
12
+ export declare function actualContractSession(adapter: BuilderAdapter, workspace: string, configRoot: string): ContractSessionBinding;
13
+ export declare function materializeContract(contract: QaVerificationContractV1, workspace: string): {
14
+ json: string;
15
+ markdown: string;
16
+ resourceDigest: string;
17
+ };
18
+ /** Complete retained review inputs, accessible inside the confined QA snapshot. */
19
+ export declare function finalReviewContractContext(contract: QaVerificationContractV1, store: QaPreparationStore, workspace: string, builderClaims?: unknown[]): string;
20
+ export declare function deliverVerificationContract(contract: QaVerificationContractV1, store: QaPreparationStore, adapter: BuilderAdapter, workspace: string, configRoot: string, context: string): Promise<QaContractDeliveryReceiptV1>;
21
+ /** Byte bounded, lossless Unicode transport. Concatenation reconstructs the exact original input. */
22
+ export declare function segmentContractTransport(content: string, maximumBytes?: number): string[];
23
+ export declare function assertContractReceipt(store: QaPreparationStore, runId: string, workId: string, admissionDigest: string, session?: ContractSessionBinding, adapter?: BuilderAdapter): QaVerificationContractV1;