ai-foreman 1.7.26 → 1.7.28

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 (98) hide show
  1. package/dist/adapters/codex.d.ts +12 -0
  2. package/dist/adapters/codex.js +50 -1
  3. package/dist/adapters/recovering.d.ts +4 -0
  4. package/dist/adapters/recovering.js +10 -0
  5. package/dist/adapters/sessionFailure.d.ts +6 -0
  6. package/dist/adapters/sessionFailure.js +19 -0
  7. package/dist/adapters/types.d.ts +6 -0
  8. package/dist/branch/currentGuard.d.ts +8 -0
  9. package/dist/branch/currentGuard.js +5 -0
  10. package/dist/branch/runner.js +6 -2
  11. package/dist/buildApproval.js +2 -1
  12. package/dist/buildRuns.d.ts +12 -0
  13. package/dist/buildRuns.js +50 -0
  14. package/dist/buildWorkContext.js +1 -1
  15. package/dist/buildWorkMigration.d.ts +10 -0
  16. package/dist/buildWorkMigration.js +18 -2
  17. package/dist/cli/start.d.ts +10 -1
  18. package/dist/cli/start.js +186 -31
  19. package/dist/cli/state.js +39 -1
  20. package/dist/cli/tickets.d.ts +37 -0
  21. package/dist/cli/tickets.js +367 -12
  22. package/dist/config.js +14 -4
  23. package/dist/continuity.d.ts +1 -0
  24. package/dist/continuity.js +5 -0
  25. package/dist/foreman.js +89 -8
  26. package/dist/graph/preparation.js +6 -3
  27. package/dist/graph/semantic.js +2 -1
  28. package/dist/graph/turn.js +8 -1
  29. package/dist/handoffProtocol.js +2 -1
  30. package/dist/handoffs.js +7 -19
  31. package/dist/hostQaComparison.d.ts +50 -0
  32. package/dist/hostQaComparison.js +195 -0
  33. package/dist/log.d.ts +1 -1
  34. package/dist/managerPacket.js +1 -1
  35. package/dist/observability.d.ts +2 -0
  36. package/dist/observability.js +2 -1
  37. package/dist/permissions/policy.js +32 -0
  38. package/dist/planChanges.js +1 -1
  39. package/dist/projectDiagnostics.js +30 -0
  40. package/dist/providerQuestions.js +4 -24
  41. package/dist/qaBuildGate.d.ts +6 -1
  42. package/dist/qaBuildGate.js +76 -16
  43. package/dist/qaBuilderCoverage.d.ts +2 -1
  44. package/dist/qaBuilderCoverage.js +58 -7
  45. package/dist/qaBuilderEvidence.d.ts +18 -0
  46. package/dist/qaBuilderEvidence.js +60 -0
  47. package/dist/qaContractCoverage.d.ts +12 -1
  48. package/dist/qaContractCoverage.js +34 -2
  49. package/dist/qaContractDelivery.d.ts +2 -2
  50. package/dist/qaContractDelivery.js +88 -10
  51. package/dist/qaEffectiveConfig.js +2 -1
  52. package/dist/qaFailureDelivery.d.ts +23 -0
  53. package/dist/qaFailureDelivery.js +87 -6
  54. package/dist/qaHostFacts.d.ts +2 -0
  55. package/dist/qaHostFacts.js +11 -0
  56. package/dist/qaPreparation.d.ts +38 -1
  57. package/dist/qaPreparation.js +372 -31
  58. package/dist/qaPreparationInputs.js +5 -1
  59. package/dist/qaPreparationPolicy.d.ts +2 -1
  60. package/dist/qaPreparationPolicy.js +39 -9
  61. package/dist/qaPreparationStore.d.ts +4 -0
  62. package/dist/qaPreparationStore.js +8 -0
  63. package/dist/qaRecovery.d.ts +7 -0
  64. package/dist/qaRecovery.js +73 -1
  65. package/dist/qaReview.js +58 -9
  66. package/dist/qaReviewEvidence.d.ts +6 -0
  67. package/dist/qaReviewEvidence.js +37 -0
  68. package/dist/qaRollbackRehearsal.d.ts +2 -0
  69. package/dist/qaRollbackRehearsal.js +78 -0
  70. package/dist/qaSnapshot.d.ts +3 -1
  71. package/dist/qaSnapshot.js +3 -2
  72. package/dist/qaVerificationContract.d.ts +16 -0
  73. package/dist/qaVerificationContract.js +183 -17
  74. package/dist/sources/sourceRegistry.js +16 -5
  75. package/dist/stateTransfer.d.ts +14 -0
  76. package/dist/stateTransfer.js +51 -0
  77. package/dist/statusReporter.d.ts +8 -0
  78. package/dist/statusReporter.js +5 -0
  79. package/dist/supervisedStart.js +23 -0
  80. package/dist/ticketPlanning.d.ts +373 -0
  81. package/dist/ticketPlanning.js +34 -3
  82. package/dist/ticketPopulation.d.ts +28 -3
  83. package/dist/ticketPopulation.js +127 -15
  84. package/dist/tickets/details.d.ts +2 -1
  85. package/dist/tickets/details.js +7 -3
  86. package/dist/tickets/eligibility.d.ts +6 -0
  87. package/dist/tickets/eligibility.js +12 -1
  88. package/dist/tickets/riskWaivers.d.ts +3 -0
  89. package/dist/tickets/riskWaivers.js +12 -0
  90. package/dist/tickets/stateDb.d.ts +10 -0
  91. package/dist/tickets/stateDb.js +23 -0
  92. package/dist/tickets/ticketLoader.d.ts +9 -0
  93. package/dist/tickets/ticketLoader.js +15 -2
  94. package/dist/tickets/ticketSchema.d.ts +14 -1
  95. package/dist/tickets/ticketSchema.js +2 -1
  96. package/dist/tickets/validate.js +11 -1
  97. package/dist/workflowDb.js +24 -3
  98. package/package.json +9 -4
@@ -1,4 +1,4 @@
1
- const MATERIAL_KEYS = new Set(["scope", "summary", "acceptance", "acceptance_criteria", "depends_on", "dependencies", "delivery", "delivery_strategy", "branch", "branch_mode", "completion", "completion_mode"]);
1
+ const MATERIAL_KEYS = new Set(["scope", "summary", "acceptance", "acceptance_criteria", "documentation", "depends_on", "dependencies", "delivery", "delivery_strategy", "branch", "branch_mode", "completion", "completion_mode"]);
2
2
  const BOOKKEEPING_KEYS = new Set(["status", "evidence", "updated_at", "updatedAt", "timestamp", "notes"]);
3
3
  /** Structural classification; agent prose is intentionally ignored. */
4
4
  export function classifyPlanChange(before, after) {
@@ -5,6 +5,8 @@ import { execFileSync } from "node:child_process";
5
5
  import { readInterviewRecords } from "./interviews.js";
6
6
  import { cmdQueue, cmdValidate } from "./tickets/commands.js";
7
7
  import { getAllTicketDetails } from "./tickets/details.js";
8
+ import { loadRafiConfigObject } from "./tickets/setupConfig.js";
9
+ import { resolveQaPreparationConfig } from "./qaPreparationPolicy.js";
8
10
  function collectProjectState(root) {
9
11
  const state = { configPresent: existsSync(join(root, "rafi-config.yaml")), dirtyPaths: [], planningSessions: [], errors: [] };
10
12
  const sessions = readInterviewRecords(root);
@@ -22,6 +24,21 @@ function collectProjectState(root) {
22
24
  state.errors.push(String(error));
23
25
  }
24
26
  }
27
+ if (state.configPresent) {
28
+ try {
29
+ const config = loadRafiConfigObject(root);
30
+ const legacy = Boolean(config && config.qa_preparation && typeof config.qa_preparation === "object" && !Array.isArray(config.qa_preparation));
31
+ const policy = resolveQaPreparationConfig(config?.qa_preparation, config?.qa_preparation_shadow === true, config?.qa_preparation_wall_time_ms);
32
+ state.qaPreparation = {
33
+ mode: policy.mode,
34
+ configuration: legacy ? "legacy" : "current",
35
+ detail: legacy ? "Legacy QA preparation configuration will be rewritten to public booleans by `rafi compile <project>`." : policy.mode === "legacy" ? "QA preparation is disabled; enable qa_preparation: true to require QA requirements before Builder work." : policy.mode === "shadow" ? "QA preparation is collecting non-blocking rollout evidence." : "QA preparation is enabled and requires a ticket contract before Builder work.",
36
+ };
37
+ }
38
+ catch (error) {
39
+ state.qaPreparation = { mode: "legacy", configuration: "invalid", detail: `QA preparation configuration is invalid: ${error instanceof Error ? error.message : String(error)}` };
40
+ }
41
+ }
25
42
  return state;
26
43
  }
27
44
  import { BUILD_LEASE_STALE_MS, readBuildRuns } from "./buildRuns.js";
@@ -67,6 +84,19 @@ export function collectManagerProjectDiagnostics(projectDir, options = {}) {
67
84
  const currentFocus = options.currentFocusRunId ?? initialFocus;
68
85
  if (currentFocus && !ids.has(currentFocus))
69
86
  throw new Error(`build run not found: ${currentFocus}`);
87
+ if (projectState.qaPreparation && currentFocus) {
88
+ const preparation = workflow.qaPreparation(currentFocus);
89
+ const states = preparation.works.reduce((counts, work) => {
90
+ if (work.state === "ready")
91
+ counts.ready++;
92
+ else if (["blocked", "incomplete", "uncertain"].includes(work.state))
93
+ counts.blocked++;
94
+ else
95
+ counts.pending++;
96
+ return counts;
97
+ }, { ready: 0, pending: 0, blocked: 0 });
98
+ projectState.qaPreparation = { ...projectState.qaPreparation, mode: preparation.mode, runId: currentFocus, contracts: states };
99
+ }
70
100
  const summaries = [...ids].map(runId => mergeSummary({ runId, build: buildById.get(runId), stored: storedById.get(runId), rollup: rollupById.get(runId), workflow: workflowById.get(runId), activeRunId, leaseRunId: lease?.runId, now, schemaVersion: observability.schemaVersion() }))
71
101
  .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || b.createdAt.localeCompare(a.createdAt) || a.runId.localeCompare(b.runId));
72
102
  const detailIds = [...new Set([currentFocus, activeRunId, ...(options.referencedRunIds ?? [])].filter((id) => Boolean(id && buildById.has(id))))].slice(0, 5);
@@ -2,6 +2,7 @@ import { buildScopeRevision } from "./buildApproval.js";
2
2
  import { durableHumanDecision, HumanDecisionRequired } from "./humanDecision.js";
3
3
  import { isCancel, multiselect, select, text } from "@clack/prompts";
4
4
  import { currentActivity, pauseActivityForInput } from "./activity.js";
5
+ import { providerQuestionV1Schema } from "rafi-spec";
5
6
  const ASK_USER_QUESTION_TOOL = "AskUserQuestion";
6
7
  const CUSTOM_VALUE = "__rafi_custom_response__";
7
8
  export const GRILL_ME_STOP_CHOICE = "Stop questions and make the plan now";
@@ -101,31 +102,10 @@ function parseQuestions(input) {
101
102
  if (!Array.isArray(rawQuestions))
102
103
  return [];
103
104
  return rawQuestions.flatMap((raw) => {
104
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
105
+ const result = providerQuestionV1Schema.safeParse(raw);
106
+ if (!result.success)
105
107
  return [];
106
- const record = raw;
107
- const question = typeof record.question === "string" ? record.question.trim() : "";
108
- if (!question)
109
- return [];
110
- const header = typeof record.header === "string" && record.header.trim()
111
- ? record.header.trim()
112
- : undefined;
113
- const options = Array.isArray(record.options)
114
- ? record.options.flatMap((option) => {
115
- if (!option || typeof option !== "object" || Array.isArray(option))
116
- return [];
117
- const optionRecord = option;
118
- const label = typeof optionRecord.label === "string" ? optionRecord.label.trim() : "";
119
- if (!label)
120
- return [];
121
- return [{
122
- label,
123
- description: typeof optionRecord.description === "string" ? optionRecord.description : undefined,
124
- preview: typeof optionRecord.preview === "string" ? optionRecord.preview : undefined,
125
- }];
126
- })
127
- : [];
128
- return [{ question, header, options, multiSelect: record.multiSelect === true }];
108
+ return [result.data];
129
109
  });
130
110
  }
131
111
  async function askOneQuestion(question, prompts, signal) {
@@ -1,4 +1,9 @@
1
- import type { BuilderAdapter } from "./adapters/types.js";
1
+ import type { BuilderAdapter, TurnResult } from "./adapters/types.js";
2
2
  import type { QaSessionHandle } from "./qaReview.js";
3
+ /** A received response missing only continuity metadata is a bounded format defect. */
4
+ export declare function preparationTurnPayload(turn: TurnResult): {
5
+ text: string;
6
+ formatErrors: string[];
7
+ };
3
8
  /** Shared by ordinary, branch, synthetic, remediation and Manager followups. */
4
9
  export declare function ensureBuilderContract(projectDir: string, runId: string, workId: string, workspace: string, builder: BuilderAdapter, qaFactory?: (cwd: string) => Promise<QaSessionHandle>): Promise<string>;
@@ -14,13 +14,23 @@ import { WorkflowDb } from "./workflowDb.js";
14
14
  import { createRoleBuilder, readOnlyPermissionConfig } from "./agentRun.js";
15
15
  import { captureBuildSource, assertBuildAssignmentReconciled } from "./buildAssignment.js";
16
16
  import { createDisposableQaSnapshotAsync } from "./qaSnapshot.js";
17
- import { allocateStableCheckIds, authoritativeInventory, contractDigest, renderVerificationContract } from "./qaVerificationContract.js";
17
+ import { allocateStableCheckIds, authoritativeInventory, bindUniqueBaselineCheckIds, contractDigest, mapSnapshotWorkingDirectories, normalizeSnapshotPaths, renderVerificationContract, rewriteProviderHostScopeChecks } from "./qaVerificationContract.js";
18
18
  import { resolveEffectiveQaConfiguration } from "./qaEffectiveConfig.js";
19
19
  import { prepareVerificationContract } from "./qaPreparation.js";
20
+ import { qaHostGraphFact } from "./qaHostFacts.js";
20
21
  import { actualContractSession, assertContractReceipt, deliverVerificationContract } from "./qaContractDelivery.js";
21
- import { COVERAGE_INSTRUCTIONS } from "./qaContractCoverage.js";
22
+ import { BUILDER_WORK_COVERAGE_INSTRUCTIONS } from "./qaContractCoverage.js";
22
23
  import { renderBuildWorkContext } from "./buildWorkContext.js";
23
24
  import { compareContractInputs } from "./qaContractFreshness.js";
25
+ import { readBuildRuns } from "./buildRuns.js";
26
+ /** A received response missing only continuity metadata is a bounded format defect. */
27
+ export function preparationTurnPayload(turn) {
28
+ const formatErrors = turn.continuityErrors ?? [];
29
+ return {
30
+ text: formatErrors.length ? turn.rawResponse ?? turn.cleanedResponse ?? turn.text : turn.text,
31
+ formatErrors,
32
+ };
33
+ }
24
34
  /** Shared by ordinary, branch, synthetic, remediation and Manager followups. */
25
35
  export async function ensureBuilderContract(projectDir, runId, workId, workspace, builder, qaFactory) {
26
36
  return withGraphDerivedAccess([], () => ensureBuilderContractWithProvenance(projectDir, runId, workId, workspace, builder, qaFactory));
@@ -33,8 +43,6 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
33
43
  const policy = store.policy(runId) ?? store.freezePolicy(runId, resolveQaPreparationConfig(undefined));
34
44
  if (policy.mode === "legacy")
35
45
  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
46
  assertBuildAssignmentReconciled(projectDir, runId, workspace);
39
47
  const admission = db.assertAdmittedWork(runId, workId);
40
48
  const ticket = db.workDefinitions(runId).find(work => work.workId === workId)?.definition;
@@ -66,6 +74,24 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
66
74
  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
75
  }
68
76
  const source = captureBuildSource(workspace);
77
+ const preimplementationPathManifestDigest = contractDigest("preimplementation-path-manifest", source.pathInventory.map(path => ({
78
+ path: path.path,
79
+ staged: path.staged,
80
+ unstaged: path.unstaged,
81
+ ...(path.headObject ? { headObject: path.headObject } : {}),
82
+ ...(path.indexObject ? { indexObject: path.indexObject } : {}),
83
+ ...(path.worktreeObject ? { worktreeObject: path.worktreeObject } : {}),
84
+ ...(path.untracked ? { untracked: path.untracked } : {}),
85
+ })));
86
+ const baselineEvidence = store.putArtifact("baseline-observation", {
87
+ kind: "host-captured-preimplementation-source",
88
+ sourceDigest: source.digest,
89
+ contentDigest: source.contentDigest,
90
+ head: source.head,
91
+ captureMode: source.captureMode,
92
+ capturedAt: source.capturedAt,
93
+ });
94
+ inputs.push({ id: "preimplementation-source", kind: "baseline", reference: "builder.preimplementation-source", digest: source.digest, authority: "inspected-context", revision: source.head, availability: "available" });
69
95
  const head = store.head(runId, workId, admission.requirementsDigest);
70
96
  const priorBudget = store.budget(runId, workId, admission.requirementsDigest);
71
97
  if (priorBudget)
@@ -87,14 +113,30 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
87
113
  amendmentPredecessor = old.contentDigest;
88
114
  }
89
115
  }
116
+ const hostBaseline = { id: "host-preimplementation-source", sourceDigest: source.digest, phase: "preimplementation-host-capture", runtime: "Rafi host", procedure: "Host-captured source identity and per-file path manifest before Builder edits", observedAt: source.capturedAt, status: "passed", evidenceDigest: baselineEvidence, relevance: `Authoritative comparison basis for unchanged-file and scope checks; exact host-owned path-manifest digest is ${preimplementationPathManifestDigest}`, expectedTransition: "Rafi compares the complete postimplementation path inventory with this host-owned baseline; providers do not need the raw manifest" };
90
117
  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
118
  ...(amendmentPredecessor ? { predecessorDigest: amendmentPredecessor } : {}),
92
119
  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() };
120
+ documentation: structuredClone(ticket.documentation ?? { required: false, targets: [], criteria: [] }),
121
+ inputs, baseline: [hostBaseline], requirements: inventory, checks: [], coverage: [], preparationEvidence: [], unresolved: [], createdAt: new Date(priorBudget?.startMs ?? Date.now()).toISOString() };
122
+ const buildRun = readBuildRuns(projectDir).find(run => run.runId === runId);
123
+ const deliveryFact = buildRun?.branchMode === "current"
124
+ ? "Host-owned delivery-mode fact: this admitted build uses current-branch semantics. Do not require an isolated ticket branch, a multiply-only branch, branch identity, or a branch-alone test merely to prove additive ticket independence. HEAD may predate already completed sibling tickets, so never use git status, git diff HEAD, or a HEAD-based whole-tree comparison as the ticket-scope proof. Evaluate independence with the host-captured preimplementation baseline observation, approved product-path scope, ticket-owned tests, and permitted project tests. An isolated execution remains required only if the original approved requirement explicitly says so."
125
+ : buildRun?.branchMode === "per-ticket"
126
+ ? "Host-owned delivery-mode fact: this admitted build uses isolated branch-per-ticket semantics. Verify branch/worktree identity when the contract requires it; do not infer it from the independent QA snapshot."
127
+ : "Host-owned delivery-mode fact: delivery mode was not available from the authoritative build record; do not invent branch-isolation requirements without an explicit approved requirement.";
94
128
  const result = await prepareVerificationContract(initial, inventory, {
95
- store, now: Date.now,
129
+ provider: builder.agent,
130
+ store, now: Date.now, hostFacts: `${qaHostGraphFact(projectDir)}\n${deliveryFact}\nHost-owned baseline fact: Rafi captured the complete preimplementation per-file path manifest and will perform the exact unchanged-file and scope comparison. Its digest is ${preimplementationPathManifestDigest}. Do not require the provider to receive or reproduce the raw manifest; cite the retained host-preimplementation-source observation and assess the host comparison result instead. Do not author a provider-executed check for Rafi's host postimplementation path manifest, complete changed-path inventory, or unchanged-file comparison; Rafi performs that comparison outside the provider snapshot. Ordinary approved product paths, exports, and tests are not a material QA concern merely because the provider cannot prove whether they were present before implementation; Rafi owns that comparison. Do not reconstruct or disclose the full baseline. For current-branch additive tickets, a provider-authored or inferred request to test the branch with an already-completed sibling feature removed is not an applicable requirement when the approved scope says not to remove or reconstruct that sibling; verify the ticket's own behavior and tests against the shared baseline instead. Treat sibling-absence testing as binding only when the user explicitly approved that absence requirement. Escalate only an actual host-observed out-of-scope, destructive, protected-file, or requirement-relevant change.`,
96
131
  normalizeDraft: (draft, response) => {
97
- const normalized = allocateStableCheckIds(draft);
132
+ const normalized = rewriteProviderHostScopeChecks(bindUniqueBaselineCheckIds(allocateStableCheckIds(draft)), hostBaseline.id);
133
+ if (!normalized.baseline.some(observation => observation.id === hostBaseline.id))
134
+ normalized.baseline.unshift(structuredClone(hostBaseline));
135
+ for (const check of normalized.checks)
136
+ for (const method of check.verification) {
137
+ if (response.workspacePaths && Object.hasOwn(response.workspacePaths, method.cwd))
138
+ method.cwd = response.workspacePaths[method.cwd];
139
+ }
98
140
  for (const observation of normalized.baseline) {
99
141
  const original = structuredClone(observation);
100
142
  observation.sourceDigest = source.digest;
@@ -110,10 +152,22 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
110
152
  evidence.graphReceiptRefs = [...response.graphReceiptRefs];
111
153
  evidence.evidenceDigest = store.putArtifact("investigation-evidence", response);
112
154
  for (const reference of evidence.references) {
113
- const path = realpathSync(resolve(workspace, reference.path)), root = realpathSync(workspace);
155
+ let path;
156
+ try {
157
+ path = realpathSync(resolve(workspace, reference.path));
158
+ }
159
+ catch {
160
+ throw new Error(`Inspected source reference ${JSON.stringify(reference.path)} is missing or inaccessible; cite an existing file actually inspected in the frozen workspace`);
161
+ }
162
+ const root = realpathSync(workspace);
114
163
  if (!path.startsWith(root + sep))
115
164
  throw new Error("Inspected source reference escapes actual workspace");
116
- reference.digest = createHash("sha256").update(readFileSync(path)).digest("hex");
165
+ try {
166
+ reference.digest = createHash("sha256").update(readFileSync(path)).digest("hex");
167
+ }
168
+ catch {
169
+ throw new Error(`Inspected source reference ${JSON.stringify(reference.path)} is not a readable file`);
170
+ }
117
171
  }
118
172
  }
119
173
  return normalized;
@@ -121,8 +175,11 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
121
175
  inputsCurrent: candidate => {
122
176
  if (db.assertAdmittedWork(runId, workId).requirementsDigest !== admission.requirementsDigest)
123
177
  return false;
178
+ const currentSource = captureBuildSource(workspace);
179
+ if (currentSource.contentDigest !== source.contentDigest)
180
+ return false;
124
181
  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));
182
+ 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.id === "preimplementation-source" ? { ...input, digest: currentSource.digest, revision: currentSource.head } : input));
126
183
  return !compared.changed.length && !compared.unavailable.length;
127
184
  },
128
185
  dispatch: async (phase, instruction, operationId, remainingMs) => new OperationDeadline("QA preparation operation", remainingMs).run(async () => {
@@ -137,13 +194,14 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
137
194
  if (phase === "planner") {
138
195
  const role = await createRoleBuilder({ projectDir: workspace, configRoot: projectDir, role: "planner", label: "QA depth assessment", sandboxMode: "read-only", permissionConfig: readOnlyPermissionConfig(), yes: true, allowSwitch: false });
139
196
  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() });
197
+ const turn = await sendPreparationGraphTurn(role.builder, `This is a read-only QA planning subturn, not Builder work. Do not implement anything, edit files, run project tests, or describe a TDD implementation plan. Your response must begin with RAFI_QA_DEPTH_START and contain only the requested depth JSON, ending with RAFI_QA_DEPTH_END.\n${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
198
  store.event(runId, workId, `${operationId}:raw`, "preparation-provider-return", turn);
142
199
  if (captureBuildSource(workspace).contentDigest !== before.contentDigest)
143
200
  throw new Error("Product source changed during confined planning; reconcile retained dispatch");
144
- if (turn.isError || turn.failure || !role.builder.sessionId())
201
+ const payload = preparationTurnPayload(turn);
202
+ if ((turn.isError && !payload.formatErrors.length) || turn.failure || !role.builder.sessionId())
145
203
  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 } };
204
+ return { text: payload.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
205
  }
148
206
  finally {
149
207
  await role.builder.close();
@@ -162,11 +220,13 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
162
220
  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
221
  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
222
  store.event(runId, workId, `${operationId}:raw`, "preparation-provider-return", turn);
165
- if (turn.isError || turn.failure || !adapter.sessionId())
223
+ const payload = preparationTurnPayload(turn);
224
+ payload.text = normalizeSnapshotPaths(payload.text);
225
+ if ((turn.isError && !payload.formatErrors.length) || turn.failure || !adapter.sessionId())
166
226
  throw new Error(`Preparation provider unavailable: ${turn.text}`);
167
227
  if (captureBuildSource(workspace).contentDigest !== before.contentDigest)
168
228
  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 } };
229
+ return { text: payload.text, graphReceiptRefs: turn.graphReceiptRefs, sessionId: adapter.sessionId(), workspacePaths: mapSnapshotWorkingDirectories(payload.text, snapshot.path), 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
230
  }
171
231
  finally {
172
232
  try {
@@ -208,7 +268,7 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
208
268
  catch {
209
269
  await deliverVerificationContract(result.contract, store, builder, workspace, projectDir, renderBuildWorkContext(ticket, effective.checklist));
210
270
  }
211
- return `${renderVerificationContract(result.contract)}\n${COVERAGE_INSTRUCTIONS}\nCoverage binding: ${JSON.stringify({ phase: "builder", runId, workId, revision: result.contract.revision, contractDigest: result.contract.contentDigest })}`;
271
+ return `${renderVerificationContract(result.contract)}\n${BUILDER_WORK_COVERAGE_INSTRUCTIONS}`;
212
272
  }
213
273
  finally {
214
274
  db.close();
@@ -1,5 +1,6 @@
1
1
  import type { BuilderAdapter, TurnResult } from "./adapters/types.js";
2
2
  import { WorkflowDb } from "./workflowDb.js";
3
+ import { type HostBuilderToolObservation } from "./qaBuilderEvidence.js";
3
4
  /** 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 collectBuilderContractCoverage(projectDir: string, runId: string, workId: string, workspace: string, adapter: BuilderAdapter, operationId: string, instruction: string, implementation: TurnResult, hostObservations?: HostBuilderToolObservation[], initialSourceDigest?: string): Promise<void>;
5
6
  export declare function assertBuilderContractCoverage(db: WorkflowDb, runId: string, workId: string, operationId: string, sourceDigest: string): void;
@@ -2,14 +2,16 @@ import { dispatchWithGraphAccess, withGraphSessionAccess } from "./graph/session
2
2
  import { WorkflowDb } from "./workflowDb.js";
3
3
  import { actualContractSession, assertContractReceipt } from "./qaContractDelivery.js";
4
4
  import { captureBuildSource } from "./buildAssignment.js";
5
- import { COVERAGE_INSTRUCTIONS, parseContractCoverage, validateContractCoverage } from "./qaContractCoverage.js";
5
+ import { COVERAGE_INSTRUCTIONS, bindCoverageHostFields, parseContractCoverage, validateContractCoverage } from "./qaContractCoverage.js";
6
6
  import { contractDigest } from "./qaVerificationContract.js";
7
7
  import { OperationDeadline } from "./util/deadline.js";
8
+ import { hostTestSequenceEvidence } from "./qaBuilderEvidence.js";
9
+ import { qaHostGraphFact } from "./qaHostFacts.js";
8
10
  /** 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
+ export async function collectBuilderContractCoverage(projectDir, runId, workId, workspace, adapter, operationId, instruction, implementation, hostObservations = [], initialSourceDigest) {
12
+ return withGraphSessionAccess(projectDir, adapter, () => collectBuilderContractCoverageOwned(projectDir, runId, workId, workspace, adapter, operationId, instruction, implementation, hostObservations, initialSourceDigest));
11
13
  }
12
- async function collectBuilderContractCoverageOwned(projectDir, runId, workId, workspace, adapter, operationId, instruction, implementation) {
14
+ async function collectBuilderContractCoverageOwned(projectDir, runId, workId, workspace, adapter, operationId, instruction, implementation, hostObservations, initialSourceDigest) {
13
15
  const db = new WorkflowDb(projectDir);
14
16
  try {
15
17
  const store = db.qaPreparationStore();
@@ -19,7 +21,7 @@ async function collectBuilderContractCoverageOwned(projectDir, runId, workId, wo
19
21
  const session = actualContractSession(adapter, workspace, projectDir);
20
22
  const contract = assertContractReceipt(store, runId, workId, admission.requirementsDigest, session, adapter);
21
23
  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 };
24
+ let binding = { phase: "builder", sourceDigest: source.digest, inputBasisDigest: contractDigest("builder-input-basis", { operationId, instruction, contractDigest: contract.contentDigest }), attemptId: operationId, sessionId: session.sessionId };
23
25
  const eventId = `builder-coverage:${operationId}`;
24
26
  const existing = db.operation(eventId);
25
27
  if (existing) {
@@ -42,9 +44,19 @@ async function collectBuilderContractCoverageOwned(projectDir, runId, workId, wo
42
44
  db.updateOperation(eventId, "uncertain", { result: { responseDigest } });
43
45
  if (response.isError || response.failure)
44
46
  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)
47
+ const afterSession = actualContractSession(adapter, workspace, projectDir);
48
+ const sameScopedSession = afterSession.provider === session.provider
49
+ && afterSession.workspace === session.workspace
50
+ && afterSession.configRoot === session.configRoot
51
+ && (afterSession.sessionId === session.sessionId || afterSession.compactionSequence > session.compactionSequence);
52
+ if (!sameScopedSession || captureBuildSource(workspace).digest !== source.digest)
46
53
  throw new Error("Builder coverage changed session or product source");
47
- const coverage = parseContractCoverage(response.text);
54
+ // A provider may compact while producing this response-only evidence.
55
+ // Bind the retained coverage to the host-observed post-compaction session
56
+ // while continuing to reject unscoped session changes and source edits.
57
+ binding = { ...binding, sessionId: afterSession.sessionId };
58
+ const coverage = bindCoverageHostFields(parseContractCoverage(response.text), contract, binding);
59
+ bindHostOwnedBuilderFacts(db, coverage, contract, hostObservations, initialSourceDigest, source.digest, workId, projectDir);
48
60
  const errors = validateContractCoverage(contract, coverage, binding, false);
49
61
  if (errors.length)
50
62
  throw new Error(`Builder coverage invalid: ${errors.join("; ")}`);
@@ -59,6 +71,45 @@ async function collectBuilderContractCoverageOwned(projectDir, runId, workId, wo
59
71
  db.close();
60
72
  }
61
73
  }
74
+ function checkText(check) {
75
+ return JSON.stringify(check);
76
+ }
77
+ function bindHostOwnedBuilderFacts(db, coverage, contract, observations, _initialSourceDigest, finalSourceDigest, workId, projectDir) {
78
+ const graphFact = qaHostGraphFact(projectDir);
79
+ const graphDisabled = /"enabled":false/.test(graphFact);
80
+ for (const row of coverage.checks ?? []) {
81
+ const check = contract.checks.find(candidate => candidate.id === row.checkId);
82
+ if (!check)
83
+ continue;
84
+ const text = checkText(check);
85
+ if (check.timing === "preimplementation" && row.outcome === "not-run") {
86
+ const baseline = contract.baseline.find(observation => observation.checkId === check.id && observation.status === "passed");
87
+ if (baseline) {
88
+ row.outcome = "passed";
89
+ row.evidence = [...(row.evidence ?? []), { procedure: "Rafi retained preimplementation baseline observation", cwd: ".", runtime: "rafi host", sourceDigest: baseline.sourceDigest, reference: baseline.evidenceDigest }];
90
+ }
91
+ }
92
+ if (graphDisabled && /graph/i.test(text) && /disabled|not.?adopt|no graph/i.test(text)) {
93
+ const factDigest = db.putEvidence("qa", graphFact);
94
+ if (row.outcome === "not-run")
95
+ row.outcome = "passed";
96
+ if (!row.evidence?.some(evidence => evidence.reference === `host-graph-fact:${factDigest}`))
97
+ row.evidence = [...(row.evidence ?? []), { procedure: "Rafi host-owned graph configuration observation", cwd: ".", runtime: "rafi host", sourceDigest: finalSourceDigest, reference: `host-graph-fact:${factDigest}` }];
98
+ }
99
+ if (row.outcome !== "passed")
100
+ continue;
101
+ const historical = /red|test.?first|ordering|historical/i.test(text);
102
+ if (!historical)
103
+ continue;
104
+ const testPath = check.verification.find(method => method.kind === "command" && method.argv?.includes("--test"))?.argv?.at(-1);
105
+ const sequence = hostTestSequenceEvidence(observations, finalSourceDigest, testPath);
106
+ if (!sequence)
107
+ continue;
108
+ const evidence = JSON.stringify({ version: 1, kind: "host-builder-test-sequence", workId, checkId: row.checkId, sequence });
109
+ const evidenceDigest = db.putEvidence("qa", evidence);
110
+ row.evidence = [...(row.evidence ?? []), { procedure: "Rafi host-observed test sequence", cwd: ".", runtime: "provider tool lifecycle plus host source capture", sourceDigest: finalSourceDigest, reference: `host-test-sequence:${evidenceDigest}` }];
111
+ }
112
+ }
62
113
  export function assertBuilderContractCoverage(db, runId, workId, operationId, sourceDigest) {
63
114
  const store = db.qaPreparationStore();
64
115
  if (store.policy(runId)?.mode !== "enforce")
@@ -0,0 +1,18 @@
1
+ import type { BuilderAdapter } from "./adapters/types.js";
2
+ export interface HostBuilderToolObservation {
3
+ kind: "node-test" | "npm-test";
4
+ startedAt: string;
5
+ completedAt: string;
6
+ outcome: string;
7
+ exitCode?: number;
8
+ outputDigest?: string;
9
+ sourceDigest: string;
10
+ testPath?: string;
11
+ }
12
+ /**
13
+ * Observe only bounded test-command metadata while the provider is working.
14
+ * The provider remains subject to its normal shell policy; this is a host-side
15
+ * observation of what actually completed, not an execution or permission grant.
16
+ */
17
+ export declare function observeHostBuilderTests(adapter: BuilderAdapter, workspace: string, observations: HostBuilderToolObservation[]): () => void;
18
+ export declare function hostTestSequenceEvidence(observations: HostBuilderToolObservation[], finalSourceDigest: string, testPath?: string): HostBuilderToolObservation[] | undefined;
@@ -0,0 +1,60 @@
1
+ import { captureBuildSource } from "./buildAssignment.js";
2
+ function commandDetails(input) {
3
+ const command = typeof input?.command === "string"
4
+ ? input.command
5
+ : undefined;
6
+ if (!command)
7
+ return undefined;
8
+ const nodeTest = /\bnode\s+--test(?:\s+([^\s;&|>]+))?/.exec(command);
9
+ if (nodeTest)
10
+ return { kind: "node-test", ...(nodeTest[1] ? { testPath: nodeTest[1] } : {}) };
11
+ if (/\bnpm\s+test\b/.test(command))
12
+ return { kind: "npm-test" };
13
+ return undefined;
14
+ }
15
+ /**
16
+ * Observe only bounded test-command metadata while the provider is working.
17
+ * The provider remains subject to its normal shell policy; this is a host-side
18
+ * observation of what actually completed, not an execution or permission grant.
19
+ */
20
+ export function observeHostBuilderTests(adapter, workspace, observations) {
21
+ const open = new Map();
22
+ const stop = adapter.observeEvents?.((event) => {
23
+ if (event.kind !== "tool")
24
+ return;
25
+ const details = commandDetails(event.input);
26
+ if (event.lifecycle === "started" && details && event.callId) {
27
+ open.set(event.callId, { ...details, startedAt: event.observedAt ?? new Date().toISOString() });
28
+ return;
29
+ }
30
+ if (event.lifecycle !== "completed" || !event.callId || !event.completionKnown)
31
+ return;
32
+ const prior = open.get(event.callId);
33
+ if (!prior)
34
+ return;
35
+ open.delete(event.callId);
36
+ if (observations.length >= 40)
37
+ return;
38
+ observations.push({
39
+ kind: prior.kind,
40
+ startedAt: prior.startedAt,
41
+ completedAt: event.observedAt ?? new Date().toISOString(),
42
+ // Some adapters label every finished tool call "completed" even when
43
+ // the process exited non-zero. Exit status is the host-owned authority
44
+ // for the red/green distinction whenever it is available.
45
+ outcome: event.exitCode !== undefined ? event.exitCode === 0 ? "completed" : "failed" : event.status ?? "completed",
46
+ ...(event.exitCode === undefined ? {} : { exitCode: event.exitCode }),
47
+ ...(event.outputDigest && /^[a-f0-9]{64}$/.test(event.outputDigest) ? { outputDigest: event.outputDigest } : {}),
48
+ sourceDigest: captureBuildSource(workspace).digest,
49
+ ...(prior.testPath ? { testPath: prior.testPath } : {}),
50
+ });
51
+ }) ?? (() => { });
52
+ return () => { stop(); open.clear(); };
53
+ }
54
+ export function hostTestSequenceEvidence(observations, finalSourceDigest, testPath) {
55
+ const relevant = observations.filter(observation => (observation.kind === "node-test" || observation.kind === "npm-test") && (!testPath || observation.testPath === testPath));
56
+ const red = relevant.find(observation => observation.outcome !== "completed" && observation.outcome !== "denied");
57
+ const redIndex = red ? relevant.indexOf(red) : -1;
58
+ const green = red ? relevant.slice(redIndex + 1).find(observation => observation.sourceDigest === finalSourceDigest && observation.outcome === "completed" && observation.sourceDigest !== red.sourceDigest) : undefined;
59
+ return red && green ? [red, green] : undefined;
60
+ }
@@ -1,6 +1,17 @@
1
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.";
2
+ export declare const COVERAGE_INSTRUCTIONS: string;
3
+ export declare const BUILDER_WORK_COVERAGE_INSTRUCTIONS = "Carry out the applicable contract checks and report your work and evidence honestly in your normal final response. When a check requires historical ordering, retain bounded output for each required step and an explicit ordering record while performing it; a later passing test cannot prove an earlier failing step. Do not emit RAFI_QA_COVERAGE JSON during this implementation turn or stop for coverage binding values. After the implementation turn, Rafi will capture the completed source and request a separate response-only coverage record with the exact host-owned bindings.";
3
4
  export declare function parseContractCoverage(text: string): QaContractCoverageV1;
5
+ /** Source identity is host-owned; providers may copy a stale Builder digest. */
6
+ export declare function bindCoverageSourceDigest(coverage: QaContractCoverageV1, sourceDigest: string): QaContractCoverageV1;
7
+ /** All coverage identity fields are host-owned; provider output supplies only claims and evidence text. */
8
+ export declare function bindCoverageHostFields(coverage: QaContractCoverageV1, contract: QaVerificationContractV1, binding: {
9
+ phase: "builder" | "qa";
10
+ sourceDigest: string;
11
+ inputBasisDigest: string;
12
+ attemptId: string;
13
+ sessionId: string;
14
+ }): QaContractCoverageV1;
4
15
  export declare function validateContractCoverage(contract: QaVerificationContractV1, coverage: QaContractCoverageV1, binding: {
5
16
  phase: "builder" | "qa";
6
17
  sourceDigest: string;
@@ -2,8 +2,40 @@ import { Ajv } from "ajv";
2
2
  import { qaContractCoverageV1Schema } from "rafi-spec";
3
3
  const coverageSchema = new Ajv({ allErrors: true }).compile(qaContractCoverageV1Schema);
4
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.`;
5
+ export const COVERAGE_INSTRUCTIONS = `Return RAFI_QA_COVERAGE_START, one QaContractCoverageV1 JSON object, RAFI_QA_COVERAGE_END before the final status marker. The exact schema is ${JSON.stringify(qaContractCoverageV1Schema)}. Put every binding field at the TOP LEVEL, not inside a binding object, and copy the host-supplied binding values exactly. For each check use checkId (not id), applicability as applicable/not-applicable/unresolved, predicateEvidence as an array of strings, outcome as passed/failed/blocked/not-run, and evidence as an array of objects with procedure, cwd, runtime, sourceDigest and reference. For postimplementation evidence sourceDigest is the exact host-supplied sourceDigest, not an individual file hash. Unconditional checks cannot be marked not-applicable or unresolved; for an absence or non-impact check, mark it applicable and provide bounded inspection evidence showing that the absent change or impact was assessed. For independent QA coverage of an applicable passed preimplementation-only check, inspect its matching passed contract.baseline observation and set at least one evidence reference to that observation's exact evidenceDigest; the historical observation is bound to its original source, not the current source. Assess its relevance rather than inventing or claiming a new baseline. Host-owned checks whose applicability decisionOwner is host are the exception to provider execution: include the check truthfully as applicable/not-run with evidence that the host-owned comparison is pending, and do not block or fail QA over it; Rafi will execute the comparison and replace that row before accepting the pass. Before emitting qa_pass, run every other applicable mandatory check that is not backed by a passed retained baseline; do not leave such a provider-owned check as not-run merely because an earlier turn omitted it. If an exact required command is blocked by the provider shell policy, use a semantically equivalent permitted read-only procedure, record the substitution and limitation truthfully, and assess the same predicate; do not mark the check blocked merely because the literal command syntax was denied. If a required command is permitted and read-only, run it now and retain its bounded result. Do not add unsupported fields. Include every check with truthful applicability and concrete evidence. Missing, blocked, not-run, unresolved applicability, or failed provider-owned mandatory coverage cannot authorize normal completion. Builder coverage is a claim; final QA independently verifies it.`;
6
+ // The source and attempt binding do not exist until the implementation turn has finished.
7
+ export const BUILDER_WORK_COVERAGE_INSTRUCTIONS = `Carry out the applicable contract checks and report your work and evidence honestly in your normal final response. When a check requires historical ordering, retain bounded output for each required step and an explicit ordering record while performing it; a later passing test cannot prove an earlier failing step. Do not emit RAFI_QA_COVERAGE JSON during this implementation turn or stop for coverage binding values. After the implementation turn, Rafi will capture the completed source and request a separate response-only coverage record with the exact host-owned bindings.`;
6
8
  export function parseContractCoverage(text) { return parsePreparationArtifact(text, "RAFI_QA_COVERAGE"); }
9
+ /** Source identity is host-owned; providers may copy a stale Builder digest. */
10
+ export function bindCoverageSourceDigest(coverage, sourceDigest) {
11
+ const copy = structuredClone(coverage);
12
+ for (const check of copy.checks ?? [])
13
+ for (const evidence of check.evidence ?? [])
14
+ evidence.sourceDigest = sourceDigest;
15
+ return copy;
16
+ }
17
+ /** All coverage identity fields are host-owned; provider output supplies only claims and evidence text. */
18
+ export function bindCoverageHostFields(coverage, contract, binding) {
19
+ const copy = bindCoverageSourceDigest(coverage, binding.sourceDigest);
20
+ // Optional schema fields may be emitted as JSON null by providers. Null is
21
+ // not a meaningful limitation, so omit only that representation variant;
22
+ // non-string values elsewhere remain validation errors.
23
+ for (const check of copy.checks ?? [])
24
+ for (const evidence of check.evidence ?? [])
25
+ if (evidence.limitation === null)
26
+ delete evidence.limitation;
27
+ copy.version = 1;
28
+ copy.runId = contract.runId;
29
+ copy.workId = contract.workId;
30
+ copy.revision = contract.revision;
31
+ copy.contractDigest = contract.contentDigest;
32
+ copy.phase = binding.phase;
33
+ copy.sourceDigest = binding.sourceDigest;
34
+ copy.inputBasisDigest = binding.inputBasisDigest;
35
+ copy.attemptId = binding.attemptId;
36
+ copy.sessionId = binding.sessionId;
37
+ return copy;
38
+ }
7
39
  export function validateContractCoverage(contract, coverage, binding, requirePass = true) {
8
40
  if (!coverageSchema(coverage))
9
41
  return (coverageSchema.errors ?? []).map(error => `${error.instancePath} ${error.message}`);
@@ -32,7 +64,7 @@ export function validateContractCoverage(contract, coverage, binding, requirePas
32
64
  if (check.applicability.kind === "conditional" && row.applicability === "applicable" && !row.predicateEvidence?.length)
33
65
  issues.push(`Conditional applicability requires predicate evidence ${row.checkId}`);
34
66
  if (requirePass && check.obligation === "mandatory" && (row.applicability === "unresolved" || row.applicability === "applicable" && row.outcome !== "passed"))
35
- issues.push(`Mandatory check incomplete ${row.checkId}`);
67
+ issues.push(`Mandatory check incomplete ${row.checkId}: run its permitted verification now and resubmit passed evidence, or report a truthful blocked result for host recovery`);
36
68
  if (!Array.isArray(row.evidence))
37
69
  issues.push(`Missing evidence array ${row.checkId}`);
38
70
  if (row.outcome === "passed" && row.applicability === "applicable") {
@@ -1,4 +1,4 @@
1
- import type { QaVerificationContractV1, QaContractDeliveryReceiptV1 } from "rafi-spec";
1
+ import { type QaVerificationContractV1, type QaContractDeliveryReceiptV1 } from "rafi-spec";
2
2
  import type { BuilderAdapter } from "./adapters/types.js";
3
3
  import type { QaPreparationStore } from "./qaPreparationStore.js";
4
4
  export interface ContractSessionBinding {
@@ -16,7 +16,7 @@ export declare function materializeContract(contract: QaVerificationContractV1,
16
16
  resourceDigest: string;
17
17
  };
18
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;
19
+ export declare function finalReviewContractContext(contract: QaVerificationContractV1, store: QaPreparationStore, workspace: string, builderClaims?: unknown[], commandObservations?: unknown[]): string;
20
20
  export declare function deliverVerificationContract(contract: QaVerificationContractV1, store: QaPreparationStore, adapter: BuilderAdapter, workspace: string, configRoot: string, context: string): Promise<QaContractDeliveryReceiptV1>;
21
21
  /** Byte bounded, lossless Unicode transport. Concatenation reconstructs the exact original input. */
22
22
  export declare function segmentContractTransport(content: string, maximumBytes?: number): string[];