ai-foreman 1.7.28 → 2.0.0

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 (144) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude.d.ts +23 -12
  3. package/dist/adapters/claude.js +228 -24
  4. package/dist/adapters/codex.d.ts +17 -7
  5. package/dist/adapters/codex.js +195 -17
  6. package/dist/adapters/recovering.d.ts +2 -0
  7. package/dist/adapters/recovering.js +5 -1
  8. package/dist/adapters/structured.d.ts +71 -0
  9. package/dist/adapters/structured.js +213 -0
  10. package/dist/adapters/types.d.ts +86 -9
  11. package/dist/agentRun.d.ts +2 -0
  12. package/dist/agentRun.js +2 -1
  13. package/dist/boundedTurnEvidence.d.ts +18 -0
  14. package/dist/boundedTurnEvidence.js +76 -0
  15. package/dist/branch/currentGuard.d.ts +2 -0
  16. package/dist/branch/currentGuard.js +2 -0
  17. package/dist/branch/planner.js +1 -4
  18. package/dist/branch/runner.d.ts +2 -1
  19. package/dist/branch/runner.js +10 -7
  20. package/dist/buildAdmission.js +4 -0
  21. package/dist/buildApproval.d.ts +30 -0
  22. package/dist/buildApproval.js +68 -0
  23. package/dist/buildAssignment.d.ts +1 -0
  24. package/dist/buildAssignment.js +82 -23
  25. package/dist/buildInterventions.d.ts +2 -2
  26. package/dist/buildInterventions.js +11 -4
  27. package/dist/buildLaunchGate.js +3 -2
  28. package/dist/builderGuidanceFollowup.js +17 -6
  29. package/dist/cli/manager.js +22 -20
  30. package/dist/cli/start.js +32 -24
  31. package/dist/cli/tickets.js +40 -24
  32. package/dist/continuity.d.ts +10 -14
  33. package/dist/continuity.js +198 -179
  34. package/dist/continuityOrigin.d.ts +13 -0
  35. package/dist/continuityOrigin.js +91 -0
  36. package/dist/foreman.d.ts +20 -5
  37. package/dist/foreman.js +57 -187
  38. package/dist/graph/boundary.d.ts +1 -1
  39. package/dist/graph/boundary.js +1 -1
  40. package/dist/graph/host.js +8 -19
  41. package/dist/graph/manager.d.ts +1 -1
  42. package/dist/graph/manager.js +1 -1
  43. package/dist/graph/preparation.d.ts +38 -0
  44. package/dist/graph/preparation.js +7 -6
  45. package/dist/graph/semantic.d.ts +1 -0
  46. package/dist/graph/semantic.js +4 -1
  47. package/dist/graph/session.js +7 -2
  48. package/dist/graph/turn.d.ts +2 -3
  49. package/dist/graph/turn.js +28 -40
  50. package/dist/handoffProtocol.js +2 -2
  51. package/dist/handoffs.d.ts +3 -1
  52. package/dist/handoffs.js +90 -59
  53. package/dist/hostGraphApplicability.d.ts +46 -0
  54. package/dist/hostGraphApplicability.js +97 -0
  55. package/dist/hostQaAugmentation.d.ts +18 -0
  56. package/dist/hostQaAugmentation.js +81 -0
  57. package/dist/hostQaComparison.d.ts +21 -2
  58. package/dist/hostQaComparison.js +43 -28
  59. package/dist/hostSourcePreservation.d.ts +38 -0
  60. package/dist/hostSourcePreservation.js +72 -0
  61. package/dist/interviews.d.ts +7 -0
  62. package/dist/interviews.js +18 -1
  63. package/dist/legacyCommunicationDisplay.d.ts +2 -0
  64. package/dist/legacyCommunicationDisplay.js +2 -0
  65. package/dist/managerPacket.js +3 -3
  66. package/dist/markers.d.ts +4 -3
  67. package/dist/markers.js +4 -20
  68. package/dist/probeIpc.d.ts +14 -0
  69. package/dist/probeIpc.js +29 -0
  70. package/dist/qaBuildGate.d.ts +21 -0
  71. package/dist/qaBuildGate.js +48 -12
  72. package/dist/qaBuilderCoverage.js +22 -13
  73. package/dist/qaBuilderEvidence.d.ts +16 -1
  74. package/dist/qaBuilderEvidence.js +48 -8
  75. package/dist/qaContractCoverage.d.ts +2 -2
  76. package/dist/qaContractCoverage.js +11 -7
  77. package/dist/qaContractDelivery.js +34 -20
  78. package/dist/qaCoverageAssembly.d.ts +22 -0
  79. package/dist/qaCoverageAssembly.js +16 -0
  80. package/dist/qaDeliveryJournal.d.ts +2 -0
  81. package/dist/qaDeliveryJournal.js +19 -0
  82. package/dist/qaEffectiveConfig.d.ts +1 -0
  83. package/dist/qaEffectiveConfig.js +6 -1
  84. package/dist/qaFailureDelivery.js +97 -85
  85. package/dist/qaHostTransformation.d.ts +26 -0
  86. package/dist/qaHostTransformation.js +100 -0
  87. package/dist/qaPreparation.d.ts +8 -20
  88. package/dist/qaPreparation.js +133 -229
  89. package/dist/qaPreparationInputs.js +19 -0
  90. package/dist/qaPreparationStore.js +34 -6
  91. package/dist/qaPrerequisites.d.ts +2 -1
  92. package/dist/qaPrerequisites.js +21 -1
  93. package/dist/qaProtocolV2.d.ts +15 -0
  94. package/dist/qaRecovery.js +21 -4
  95. package/dist/qaReview.js +232 -394
  96. package/dist/qaReviewEvidence.d.ts +7 -3
  97. package/dist/qaReviewEvidence.js +15 -4
  98. package/dist/qaRuntime.js +1 -1
  99. package/dist/qaSnapshot.js +71 -21
  100. package/dist/qaStructuredResponse.d.ts +10 -0
  101. package/dist/qaStructuredResponse.js +57 -0
  102. package/dist/qaTimeline.js +5 -3
  103. package/dist/qaToolRecovery.d.ts +12 -0
  104. package/dist/qaToolRecovery.js +44 -0
  105. package/dist/qaVerificationContract.d.ts +3 -2
  106. package/dist/qaVerificationContract.js +58 -34
  107. package/dist/readinessGate.js +5 -4
  108. package/dist/runtimeReadiness.js +23 -15
  109. package/dist/statusReporter.d.ts +2 -0
  110. package/dist/statusReporter.js +2 -0
  111. package/dist/structuredCutover.d.ts +31 -0
  112. package/dist/structuredCutover.js +510 -0
  113. package/dist/structuredCutoverFiles.d.ts +10 -0
  114. package/dist/structuredCutoverFiles.js +181 -0
  115. package/dist/structuredDispatchAuthority.d.ts +22 -0
  116. package/dist/structuredDispatchAuthority.js +110 -0
  117. package/dist/structuredDispatchRecord.d.ts +29 -0
  118. package/dist/structuredDispatchRecord.js +36 -0
  119. package/dist/structuredLiveBudget.d.ts +2 -0
  120. package/dist/structuredLiveBudget.js +23 -0
  121. package/dist/structuredOperation.d.ts +18 -0
  122. package/dist/structuredOperation.js +257 -0
  123. package/dist/supervisedStart.d.ts +1 -1
  124. package/dist/supervisedStart.js +78 -31
  125. package/dist/supervisor.d.ts +18 -2
  126. package/dist/supervisor.js +74 -17
  127. package/dist/ticketPlanning.d.ts +1 -373
  128. package/dist/ticketPlanning.js +2 -26
  129. package/dist/ticketPopulation.d.ts +0 -3
  130. package/dist/ticketPopulation.js +4 -21
  131. package/dist/ticketPopulationPageAuthority.d.ts +13 -0
  132. package/dist/ticketPopulationPageAuthority.js +30 -0
  133. package/dist/ticketPopulationPaging.d.ts +27 -0
  134. package/dist/ticketPopulationPaging.js +105 -0
  135. package/dist/tickets/renderMarkdown.d.ts +14 -0
  136. package/dist/tickets/renderMarkdown.js +39 -5
  137. package/dist/tickets/ticketLoader.js +10 -0
  138. package/dist/tickets/ticketSchema.d.ts +107 -0
  139. package/dist/tickets/ticketSchema.js +4 -1
  140. package/dist/workflowDb.d.ts +100 -2
  141. package/dist/workflowDb.js +781 -35
  142. package/dist/workflowReader.d.ts +1 -0
  143. package/dist/workflowReader.js +9 -0
  144. package/package.json +15 -7
@@ -1,12 +1,14 @@
1
- import { QA_RISK_MINIMUMS, parseProviderContract, qaContractCandidateV1Schema, qaPreparationEscalationProposalV1Schema, qaSemanticAssessmentV1Schema, qaChallengeReceiptV1Schema, qaPreparationDepthDecisionV1Schema as qaDepthDecisionSchema } from "rafi-spec";
1
+ import { qaHostTransformation } from "./qaHostTransformation.js";
2
+ import { historicProviderWireContract, validateStructuredCandidate, QA_RISK_MINIMUMS, parseProviderContract, qaSemanticAssessmentV1Schema, qaChallengeReceiptV1Schema, qaPreparationEscalationProposalV1Schema, qaPreparationDepthDecisionV1Schema as qaDepthDecisionSchema, providerDraftSchema, depthProposalSchema, draftOutput, assessmentOutput, challengeOutput, depthOutput } from "rafi-spec";
2
3
  import { createHash, randomUUID } from "node:crypto";
3
4
  import { Ajv } from "ajv";
4
- const qaPreparationDepthDecisionV1Schema = qaSemanticAssessmentV1Schema;
5
5
  import { depthObligations, validateDepthDecision } from "./qaPreparationPolicy.js";
6
- import { canonicalContractJson, assembleContract, draftDigest, inventoryDigest, parsePreparationArtifact, validateAssessment, validateCandidate } from "./qaVerificationContract.js";
6
+ import { canonicalContractJson, assembleContract, draftDigest, inventoryDigest, validateAssessment, validateCandidate } from "./qaVerificationContract.js";
7
+ /** Compatibility re-exports; canonical contract ownership is rafi-spec. */
8
+ export { draftOutput, assessmentOutput, challengeOutput, depthOutput } from "rafi-spec";
7
9
  export const FILE_SCOPE_GUIDANCE = `File lists in plans and tickets, including likely_files and generated file-only diff expectations, are estimates of implementation, not exhaustive boundaries. Different named files or an extra relevant file do not by themselves create a material conflict or block the build. Retain and assess every original requirement; for file-list checks verify that changes serve the approved work rather than requiring an exact file list or a pre-edit snapshot proving no other files changed. Rafi owns tracker lifecycle updates. Generated instructions to mark a ticket done in a document are bookkeeping, not a Builder acceptance criterion or a material conflict; do not require the Builder to perform them, and do not require the host to amend the plan before building. Do not disregard explicit user-established security, privacy, access, or other safety restrictions, or expand the approved product functionality. Report genuine substantive or safety conflicts.`;
8
10
  export const ROLLBACK_VERIFICATION_GUIDANCE = `When an original requirement explicitly asks to verify that rollback restores behavior, specify concrete future verification evidence rather than only inspecting rollback prose. A rollback plan alone does not require a rollback execution or mutation-testing rehearsal. For an npm test project where current uncommitted product changes represent the change to rehearse, the Builder may run rafi qa-rehearse-rollback alone from the project root; it runs npm test before and after reverting a synthetic commit of all uncommitted product changes in an independent temporary repository. It also runs git diff --exit-code against the captured pre-change HEAD after the revert and tests; success proves the tracked files match that baseline. For a requirement that a particular string be absent after rollback, inspecting its absence in the captured baseline plus that successful whole-tree comparison is concrete evidence of absence in the reverted tracked file. The rehearsal cannot use installed dependencies or symbolic links and does not prove ticket isolation or that an actual existing ticket commit was reverted. Assess whether this evidence satisfies the original requirement; do not claim it does when the original explicitly requires the actual ticket commit. Do not assume copying files to external scratch, arbitrary scripts or mutation experiments are allowed. If actual rollback execution is explicitly required but no permitted method satisfies it, report the limitation. Never perform destructive rollback in the source workspace or claim a rehearsal has already happened.`;
9
- export const EXECUTABLE_CHECK_GUIDANCE = `Choose verification procedures compatible with the Builder's project permissions and workspace. Documentation is disabled unless the host-bound contract documentation.required is true. When it is false, do not invent documentation checks merely because a docs directory or API-looking path exists; an absent documentation surface is not a failure. When it is true, use only the contract's exact targets and criteria, and define an actionable absent-target outcome. Do not invent mandatory inline node -e or node --eval commands: the Builder policy may refuse them. For a JavaScript behavior or export check, prefer a project-local test file executed with the project's existing test command, or a permitted node --test invocation. Do not invent a mandatory test-first RED phase or other historical sequence merely because a plan requires tests before and after implementation: a passing baseline test is not a failing new test. When an original requirement actually requires a historical sequence, preserve it and specify concrete independently reviewable evidence paths or retained host events for each required step; a generic statement that sequencing evidence exists is not actionable, and a later passing test does not prove past events. If the project policy makes raw stdout redirection or scratch-log creation unavailable, do not turn log-file capture into a blocker: use the retained bounded tool output and host events instead, and never cite a scratch path unless that exact file was successfully created and inspected. The independent QA snapshot may have a detached HEAD even when the Builder source workspace is on a branch; do not use its branch name to infer the Builder source branch. Diff and scope checks must compare the current snapshot with the captured pre-implementation baseline, not blindly with repository HEAD: previously approved or completed ticket changes are part of that baseline and must not be reported as new work. A scope check must first enumerate every changed path outside the explicit bookkeeping/evidence exclusions, then compare that complete host-observed set with the approved product/ticket scope; do not pre-filter the list to expected filenames, because that would hide accidental extra product edits. Bounded evidence-only paths such as scratch/ are not product scope and must be excluded from product-path scope checks while their retained evidence remains subject to QA review. For a current-branch multi-ticket build, do not invent a mandatory branch-alone execution check merely to prove ticket independence; use baseline-aware source/test inspection and the permitted project tests unless the original requirement explicitly requires an actual isolated execution. For additive tickets in a current-branch multi-ticket build, “acceptable on its own” is satisfied by showing that each ticket's own tests import and exercise only its own feature, pass with the shared baseline and required project tests, and do not depend on the sibling feature; do not require removing another additive feature or perform a destructive rollback experiment unless the approved requirement explicitly requires absence testing or an isolated branch. Preserve explicitly requested procedures and substantive requirements; if a required procedure cannot be run, identify the limitation rather than claiming it was executed. Independently assess whether proposed evidence actually tests the required behavior.`;
11
+ export const EXECUTABLE_CHECK_GUIDANCE = `Choose verification procedures compatible with the Builder's project permissions and workspace. Keep applicability and verification instructions consistent: an unconditional check must always be reported applicable, including a check whose predicate confirms an absent feature, disabled documentation, or an unrequested skill. For such a check require passed evidence of the absence, never an instruction to report the check not-applicable. If execution itself depends on a condition, encode conditional applicability with a concrete predicate and evidence. The independent assessor must flag contradictory applicability instructions before readiness. Documentation is disabled unless the host-bound contract documentation.required is true. When it is false, do not invent documentation checks merely because a docs directory or API-looking path exists; an absent documentation surface is not a failure. When it is true, use only the contract's exact targets and criteria, and define an actionable absent-target outcome. Do not invent mandatory inline node -e or node --eval commands: the Builder policy may refuse them. For a JavaScript behavior or export check, prefer a project-local test file executed with the project's existing test command, or a permitted node --test invocation. Do not invent a mandatory test-first RED phase or other historical sequence merely because a plan requires tests before and after implementation: a passing baseline test is not a failing new test. When an original requirement actually requires a historical sequence, preserve it and specify concrete independently reviewable evidence paths or retained host events for each required step; a generic statement that sequencing evidence exists is not actionable, and a later passing test does not prove past events. If the project policy makes raw stdout redirection or scratch-log creation unavailable, do not turn log-file capture into a blocker: use the retained bounded tool output and host events instead, and never cite a scratch path unless that exact file was successfully created and inspected. The independent QA snapshot may have a detached HEAD even when the Builder source workspace is on a branch; do not use its branch name to infer the Builder source branch. Diff and scope checks must compare the current snapshot with the captured pre-implementation baseline, not blindly with repository HEAD: previously approved or completed ticket changes are part of that baseline and must not be reported as new work. A scope check must first enumerate every changed path outside the explicit bookkeeping/evidence exclusions, then compare that complete host-observed set with the approved product/ticket scope; do not pre-filter the list to expected filenames, because that would hide accidental extra product edits. Bounded evidence-only paths such as scratch/ are not product scope and must be excluded from product-path scope checks while their retained evidence remains subject to QA review. For a current-branch multi-ticket build, do not invent a mandatory branch-alone execution check merely to prove ticket independence; use baseline-aware source/test inspection and the permitted project tests unless the original requirement explicitly requires an actual isolated execution. For additive tickets in a current-branch multi-ticket build, “acceptable on its own” is satisfied by showing that each ticket's own tests import and exercise only its own feature, pass with the shared baseline and required project tests, and do not depend on the sibling feature; do not require removing another additive feature or perform a destructive rollback experiment unless the approved requirement explicitly requires absence testing or an isolated branch. Preserve explicitly requested procedures and substantive requirements; if a required procedure cannot be run, identify the limitation rather than claiming it was executed. Independently assess whether proposed evidence actually tests the required behavior.`;
10
12
  /** Baseline bookkeeping must not block ordinary approved implementation work. */
11
13
  export const APPROVED_PATH_BASELINE_GUIDANCE = `Baseline membership is host-owned bookkeeping, not a Builder requirement. An approved product path, export, or test file is not a material QA concern merely because the provider cannot prove whether it was present before implementation. Do not reconstruct or disclose the full baseline, and do not turn that uncertainty into an unresolved concern. Rafi performs the exact host comparison. Do not author a provider-executed check that asks for Rafi's host postimplementation path manifest, complete changed-path inventory, or unchanged-file comparison; that host-owned comparison is already performed outside the provider snapshot and must be represented by the retained host observation, not by raw manifest disclosure or a provider check. 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.`;
12
14
  /** Ignore known non-authoritative annotations; the original provider response remains retained. */
@@ -16,74 +18,28 @@ export function normalizeAssessmentMetadata(assessment) {
16
18
  delete copy.policyVersion_note;
17
19
  return copy;
18
20
  }
19
- /** Accept only an exact known obligation label when a provider wraps it in a
20
- * descriptive object. This is a representation repair, not a host decision:
21
- * unknown or ambiguous objects remain unchanged and fail schema validation. */
22
- export function normalizeAssessmentObligations(assessment, allowed) {
23
- const copy = structuredClone(assessment);
24
- if (!Array.isArray(copy.assessedObligations))
25
- return copy;
26
- const known = new Set(allowed);
27
- copy.assessedObligations = copy.assessedObligations.map(item => {
28
- if (typeof item === "string")
29
- return item;
30
- if (!item || typeof item !== "object")
31
- return item;
32
- const matches = Object.values(item).filter(value => typeof value === "string" && known.has(value));
33
- return matches.length === 1 ? matches[0] : item;
34
- });
35
- return copy;
36
- }
37
- /** A preparation assessor must not block on the expected absence of future
38
- * Builder work. Keep the concern for auditability, but make it non-material
39
- * only when every referenced check is postimplementation and the concern is
40
- * explicitly asking the Builder to implement/rerun that check. Concerns tied
41
- * to preimplementation checks, missing procedures, or other semantic gaps
42
- * remain blocking. */
43
- export function normalizeExpectedFutureAssessment(assessment, candidate) {
44
- const copy = structuredClone(assessment);
45
- const checks = new Map(candidate.checks.map(check => [check.id, check]));
46
- copy.concerns = copy.concerns.map(concern => {
47
- if (!concern.material || !concern.checkIds.length || !concern.checkIds.every(id => checks.get(id)?.timing === "postimplementation"))
48
- return concern;
49
- const expectedFutureAbsence = /\b(absent|missing|not (?:yet )?(?:implemented|present|available)|could not find|not found|does not exist|fails? with)\b/i.test(concern.reason)
50
- && /\b(?:before|pre[- ]?builder|not yet)\b/i.test(concern.reason)
51
- && /\b(?:implement|add|create|rerun|re-?run|builder|after implementation)\b/i.test(concern.nextAction);
52
- return expectedFutureAbsence ? { ...concern, material: false } : concern;
53
- });
54
- return copy;
21
+ function classifyMaterialConcern(concern) {
22
+ // A semantic defect in the approved contract cannot be delegated to Builder
23
+ // merely because the eventual evidence is postimplementation. Resolve the
24
+ // authority first; only an otherwise valid future check belongs to Builder.
25
+ if (concern.category === "contract-gap" || concern.category === "verification-gap") {
26
+ return { route: "plan-revision", action: "Revise and approve the affected plan or ticket verification intent before Builder dispatch." };
27
+ }
28
+ if (concern.timing === "postimplementation") {
29
+ return { route: "builder", action: "Retain this as an approved postimplementation check; do not repair it during preparation." };
30
+ }
31
+ if (concern.category === "scope-conflict") {
32
+ return { route: "human", action: "Resolve the scope conflict through an explicit human decision or approved plan revision." };
33
+ }
34
+ if (concern.category === "evidence-gap") {
35
+ return { route: "draft", action: "Repair the QA draft with retained source evidence, then obtain a fresh independent assessment." };
36
+ }
37
+ return { route: "host", action: "Record the missing host fact and resume only after it is available." };
55
38
  }
56
- // The provider proposes verification work; the host owns contract identity,
57
- // approved inputs, requirement text, depth, timestamps and evidence hashes.
58
- const durableFields = qaContractCandidateV1Schema.properties;
59
- const providerEvidence = { type: "object", additionalProperties: false, required: ["obligation", "references", "analysis"], properties: {
60
- obligation: { type: "string", minLength: 1 }, analysis: { type: "string", minLength: 1 },
61
- references: { type: "array", minItems: 1, items: { type: "object", additionalProperties: false, required: ["path", "locator"], properties: { path: { type: "string", minLength: 1 }, locator: { type: "string", minLength: 1 } } } },
62
- } };
63
- const providerBaseline = { type: "object", additionalProperties: false, required: ["id", "runtime", "procedure", "status", "relevance", "expectedTransition"], properties: {
64
- id: { type: "string", minLength: 1 }, checkId: { type: "string", minLength: 1 }, applicability: { enum: ["applicable", "not-applicable", "unresolved"] }, predicateEvidence: { type: "array", items: { type: "string" } }, runtime: { type: "string", minLength: 1 }, procedure: { type: "string", minLength: 1 }, status: { enum: ["passed", "failed", "blocked", "not-run"] }, relevance: { type: "string", minLength: 1 }, expectedTransition: { type: "string", minLength: 1 },
65
- } };
66
- const providerDraftSchema = { type: "object", additionalProperties: false, required: ["checks", "coverage", "baseline", "preparationEvidence", "unresolved"], properties: {
67
- checks: durableFields.checks, coverage: durableFields.coverage, baseline: { type: "array", items: providerBaseline }, preparationEvidence: { type: "array", minItems: 1, items: providerEvidence }, unresolved: durableFields.unresolved,
68
- } };
69
39
  const contractValidator = new Ajv({ allErrors: true });
70
40
  const validateProviderDraft = contractValidator.compile(providerDraftSchema);
71
41
  const validateAssessmentContract = contractValidator.compile(qaSemanticAssessmentV1Schema);
72
42
  const validateChallengeContract = contractValidator.compile(qaChallengeReceiptV1Schema);
73
- /** Providers sometimes echo host-owned evidence metadata into the draft. */
74
- function normalizeProviderDraftMetadata(value) {
75
- const copy = structuredClone(value);
76
- for (const evidence of copy.preparationEvidence ?? []) {
77
- const metadata = evidence;
78
- delete metadata.operationId;
79
- delete metadata.sessionId;
80
- delete metadata.evidenceDigest;
81
- delete metadata.graphReceiptRefs;
82
- for (const reference of evidence.references ?? [])
83
- delete reference.digest;
84
- }
85
- return copy;
86
- }
87
43
  /**
88
44
  * A mandatory requirement must have at least one mandatory verification check.
89
45
  * Providers occasionally return a concrete check with the weaker advisory
@@ -117,31 +73,6 @@ export function enforceMandatoryCoverage(candidate) {
117
73
  }
118
74
  return copy;
119
75
  }
120
- /**
121
- * Codex occasionally appends a made-up requirement ID to an otherwise valid
122
- * check. Remove only those extraneous annotations when the check still has an
123
- * exact authoritative reference. Never invent or remap an ID, and leave a
124
- * check with no valid references untouched so normal validation fails closed.
125
- */
126
- export function removeExtraneousRequirementReferences(candidate, inventory) {
127
- const copy = structuredClone(candidate);
128
- const allowed = new Set(inventory.map(requirement => requirement.id));
129
- for (const check of copy.checks) {
130
- const valid = check.requirementRefs.filter(id => allowed.has(id));
131
- if (valid.length > 0 && valid.length !== check.requirementRefs.length)
132
- check.requirementRefs = [...new Set(valid)];
133
- }
134
- return copy;
135
- }
136
- function providerBlocker(text, tag) {
137
- // A refusal is not a malformed candidate and cannot be repaired by formatting.
138
- if (text.includes(`${tag}_START`) && text.includes(`${tag}_END`) && /\{[\s\S]*\}/.test(text.slice(text.indexOf(`${tag}_START`) + tag.length + 6, text.indexOf(`${tag}_END`))))
139
- return undefined;
140
- if (!/\b(?:cannot|can't|unable|blocked|denied|not permitted|lack(?:s|ing) access)\b/i.test(text))
141
- return undefined;
142
- const explanation = text.replace(/\s+/g, " ").trim().slice(0, 500);
143
- return `QA provider could not produce ${tag}: ${explanation}`;
144
- }
145
76
  export async function prepareVerificationContract(initial, inventory, deps) {
146
77
  const store = deps.store;
147
78
  const head = store.head(initial.runId, initial.workId, initial.admissionDigest);
@@ -170,20 +101,17 @@ export async function prepareVerificationContract(initial, inventory, deps) {
170
101
  if (retained && canonicalContractJson(candidate.inputs) !== canonicalContractJson(initial.inputs))
171
102
  return { status: "blocked", detail: "Retained draft inputs changed", nextAction: "Reconcile authoritative input changes" };
172
103
  let correctionIssues = [];
173
- const run = async (phase, kind, instruction, resultId) => {
104
+ const run = async (phase, kind, instruction, resultId, artifact) => {
105
+ const output = artifact ?? (phase === "planner" ? depthOutput : phase === "assess" ? assessmentOutput : phase === "challenge" ? challengeOutput : draftOutput);
174
106
  const effectivePhase = phase === "prepare" && correctionIssues.length > 0 ? "repair" : phase;
175
107
  if (phase === "assess")
176
- instruction = `This is QA PREPARATION semantic assessment before Builder work, not final QA. Do not emit RAFI_QA_FAILURE_REPORT, RAFI_QA_COVERAGE, QA findings, or STEP_STATUS; return only the requested RAFI_QA_ASSESSMENT envelope.\n\n${instruction}`;
177
- if (phase === "assess" && deps.provider === "codex")
178
- instruction = `Codex role boundary: you are the independent PRE-BUILD QA ASSESSOR. Do not perform or summarize final QA, do not report Builder results, and do not emit RAFI_QA_FAILURE_REPORT_START/END. Your only output is the requested RAFI_QA_ASSESSMENT_START/END object describing whether the proposed contract is complete before Builder work.\n\n${instruction}`;
179
- if (phase === "assess" && deps.provider === "claude")
180
- instruction = `Claude response contract: every concerns entry MUST be an object exactly shaped as {"reason":"...","requirementIds":["exact-id"],"checkIds":["exact-check-id"],"material":true,"owner":"qa","nextAction":"..."}. Never use a legacy summary field, string concerns, or fields such as severity, title, message, evidence, category, or resolution. If there are no concerns, use concerns: [].\n\n${instruction}`;
108
+ instruction = `This is QA PREPARATION semantic assessment before Builder work, not final QA. Do not emit RAFI_QA_FAILURE_REPORT, RAFI_QA_COVERAGE, QA findings, or STEP_STATUS; return only the requested preparation-assessment result.\n\n${instruction}`;
181
109
  if (phase === "prepare")
182
110
  instruction = `This is a fresh draft response. Use only check IDs declared in this response; do not copy allocated check IDs or baseline/coverage references from an earlier draft or correction attempt. Every check reference in prose, procedures, expected evidence, and mappings must use the exact check.id declared in this response; never use descriptive aliases such as qa-subtract-export. Rafi will allocate stable IDs after validating this response.\n\n${instruction}`;
183
111
  if (phase === "assess")
184
112
  instruction = `The authoritative scope is the exact inventory and admitted slice in this prompt. Plan-level acceptance or test criteria outside the admitted slice are context only and must not be reported as missing inventory requirements.\n\n${instruction}`;
185
113
  if (effectivePhase === "repair")
186
- instruction = `Targeted QA contract repair only. Preserve valid checks, methods, bindings, and evidence; change only what the validation issues require. Do not redo investigation, expand scope, invent evidence, or change host-owned fields. Return the complete corrected five-field draft.\n\n${instruction}`;
114
+ instruction = `Targeted QA contract repair only. Preserve valid checks, methods, bindings, and evidence; change only what the validation issues require. Do not redo investigation, expand scope, invent evidence, or change host-owned fields. Return the complete corrected artifact for the supplied operation schema.\n\n${instruction}`;
187
115
  if (["prepare", "assess", "challenge"].includes(phase) && deps.hostFacts)
188
116
  instruction = `${deps.hostFacts}\n\n${instruction}`;
189
117
  if (["prepare", "assess", "challenge"].includes(phase))
@@ -194,86 +122,46 @@ export async function prepareVerificationContract(initial, inventory, deps) {
194
122
  instruction = `${ROLLBACK_VERIFICATION_GUIDANCE}\nThe Builder must invoke the exact launching CLI as: ${process.env.RAFI_REHEARSAL_COMMAND ?? "rafi qa-rehearse-rollback"}\n\n${instruction}`;
195
123
  const operationId = `qa-preparation:${randomUUID()}`;
196
124
  store.setState(initial.runId, initial.workId, initial.admissionDigest, ["assess", "challenge"].includes(phase) ? "validating" : "preparing", `Read-only ${phase} operation intended`, store.head(initial.runId, initial.workId, initial.admissionDigest).generation);
197
- store.reserve(budget, operationId, kind, { phase, instruction, inputDigest: inventoryDigest(inventory) }, deps.now(), resultId);
125
+ store.reserve(budget, operationId, kind, { phase, instruction, inputDigest: inventoryDigest(inventory), protocolVersion: output.protocolVersion, operation: output.operation, schemaVersion: output.schemaVersion, schemaDigest: output.schemaDigest }, deps.now(), resultId);
198
126
  const current = store.budget(initial.runId, initial.workId, initial.admissionDigest);
199
127
  const dispatchedAt = deps.now();
200
- const result = await deps.dispatch(effectivePhase, instruction, operationId, Math.max(1, current.deadlineMs - deps.now()));
128
+ const result = await deps.dispatch(effectivePhase, instruction, operationId, Math.max(1, current.deadlineMs - deps.now()), output);
201
129
  store.retainResult(budget, operationId, result, result.usage);
202
- store.metric(initial.runId, initial.workId, initial.admissionDigest, `metric:preparation-operation:${operationId}`, kind === "repair" ? "format-repair" : "phase-observation", { phase: phase === "planner" ? "planning" : phase === "challenge" ? "challenge" : phase === "repair" ? "repair" : "preparation", durationMs: Math.max(0, deps.now() - dispatchedAt), costUsd: result.usage?.costUsd ?? null, inputTokens: result.usage?.inputTokens ?? null, outputTokens: result.usage?.outputTokens ?? null });
130
+ store.metric(initial.runId, initial.workId, initial.admissionDigest, `metric:preparation-operation:${operationId}`, "phase-observation", { phase: phase === "planner" ? "planning" : phase === "challenge" ? "challenge" : phase === "repair" ? "repair" : "preparation", durationMs: Math.max(0, deps.now() - dispatchedAt), costUsd: result.usage?.costUsd ?? null, inputTokens: result.usage?.inputTokens ?? null, outputTokens: result.usage?.outputTokens ?? null });
203
131
  if (deps.now() >= current.deadlineMs)
204
132
  throw new Error("Preparation deadline expired; partial evidence retained");
205
- return { ...result, operationId };
133
+ return { ...result, operationId, output };
206
134
  };
207
- const parsed = async (result, tag) => {
208
- let response = result;
209
- const phaseBoundary = tag === "RAFI_QA_ASSESSMENT"
210
- ? "This is QA PREPARATION semantic assessment before Builder work, not final QA. Do not emit RAFI_QA_FAILURE_REPORT, RAFI_QA_COVERAGE, QA findings, or STEP_STATUS. Return only the requested RAFI_QA_ASSESSMENT envelope."
211
- : tag === "RAFI_QA_CANDIDATE"
212
- ? "This is QA PREPARATION contract drafting before Builder work, not final QA. Do not emit RAFI_QA_FAILURE_REPORT, RAFI_QA_COVERAGE, or STEP_STATUS. Return only the requested RAFI_QA_CANDIDATE envelope."
213
- : "";
214
- for (let repair = 0;; repair++) {
215
- try {
216
- return parsePreparationArtifact(response.text, tag);
217
- }
218
- catch (error) {
219
- // A provider can answer the preparation assessor with a final-QA
220
- // artifact from the wrong role. That is a repairable protocol error,
221
- // not a refusal to participate in preparation.
222
- const blocker = tag === "RAFI_QA_ASSESSMENT" && response.text.includes("RAFI_QA_FAILURE_REPORT_START")
223
- ? undefined
224
- : providerBlocker(response.text, tag);
225
- if (blocker)
226
- throw new Error(blocker);
227
- // A provider can return a short progress/prose response without any
228
- // JSON at all. Treat that as a bounded format defect unless it is an
229
- // explicit refusal; the repair turn is still required to produce the
230
- // exact envelope and is subject to the same strict parser below.
231
- if (repair >= 2)
232
- throw error;
233
- response = await run("repair", "repair", `${phaseBoundary}\nBounded format repair of the retained result. You may use read-only tools to inspect the frozen QA snapshot if needed, but do not edit files, redo the investigation, expand scope, invent evidence, or change host-owned fields. Return exactly one ${tag}_START JSON ${tag}_END envelope and no prose. If this is RAFI_QA_ASSESSMENT, it is still the pre-build assessor role: never convert the response into a final QA report or RAFI_QA_FAILURE_REPORT. If the retained response contains one JSON object without markers, copy that exact object between the markers; do not summarize it, add fields, or infer missing values. If it contains multiple objects or no complete object, return a corrected artifact only when the retained response itself unambiguously identifies the one requested object.\nRetained response:\n${response.text}`, result.operationId ?? result.sessionId);
234
- }
235
- }
135
+ const parsed = async (result, _tag) => {
136
+ if (!result.output)
137
+ throw new Error("Preparation result has no selected output contract");
138
+ if (!result.selectedDispatch)
139
+ throw new Error("Preparation result has no sealed v2 dispatch tuple");
140
+ const response = validateStructuredCandidate(result.output, result.structured, historicProviderWireContract(result.output, result.selectedDispatch), result.selectedDispatch);
141
+ if (response.result.kind !== "completed")
142
+ throw new Error(response.result.kind === "blocked" ? response.result.reason : `Preparation returned ${response.result.kind}`);
143
+ return response.result.payload;
236
144
  };
237
- /**
238
- * Validate provider-owned artifacts at their response boundary. A provider
239
- * sometimes copies the preparation draft into an independent assessment;
240
- * that is not an assessment contract and must be repaired as a bounded
241
- * schema error rather than being allowed to fail much later in assembly.
242
- */
243
145
  const parsedContract = async (result, tag, validate, name, normalize) => {
244
- let response = result;
245
- for (let repair = 0;; repair++) {
246
- try {
247
- const value = normalize ? normalize(await parsed(response, tag), response) : await parsed(response, tag);
248
- if (!validate(value))
249
- throw new Error(`${name} contract validation failed: ${(validate.errors ?? []).map(error => JSON.stringify(error)).join("; ")}`);
250
- return { value, response };
251
- }
252
- catch (error) {
253
- if (repair >= 2)
254
- throw error;
255
- const assessmentRepairContract = name === "QA semantic assessment"
256
- ? `Every concerns item must be an object with exactly these six fields and no others: {"reason":"...","requirementIds":[],"checkIds":[],"material":true,"owner":"qa","nextAction":"..."}. Replace legacy fields such as description, summary, severity, title, message, evidence, category, and resolution; do not merely wrap or rename an incomplete object. Use exact authoritative IDs only, and use concerns: [] when no supported concern remains.`
257
- : "";
258
- response = await run("repair", "repair", `Bounded schema repair for ${name}. You may use read-only tools to inspect the frozen QA snapshot if needed, but preserve the substantive independent assessment and return ONLY the exact ${tag}_START/END artifact matching the supplied schema. Do not edit files, add evidence, requirements, checks, scope, or host-owned identity. For RAFI_QA_ASSESSMENT, remain in the pre-build assessment role and never return a final QA failure report. ${assessmentRepairContract} Validation error: ${error instanceof Error ? error.message : String(error)}\nRetained response:\n${response.text}`, response.operationId ?? response.sessionId);
259
- }
146
+ const payload = await parsed(result, tag);
147
+ const value = normalize ? normalize(payload, result) : payload;
148
+ if (normalize) {
149
+ const record = qaHostTransformation({ rule: "assessment-host-bindings-v1", operation: result.output.operation,
150
+ tupleDigest: result.selectedDispatch.digest, before: payload, after: value });
151
+ store.putArtifact("host-normalization", { ...record,
152
+ inputRef: store.putArtifact("host-transformation-input", payload), outputRef: store.putArtifact("host-transformation-output", value) });
260
153
  }
154
+ if (!validate(value))
155
+ throw new Error(`${name} contract validation failed: ${JSON.stringify(validate.errors)}`);
156
+ return { value, response: result };
261
157
  };
262
158
  try {
263
159
  if (!retained && !retainedDepth && decisionIssues.length) {
264
160
  let plannerIssues = decisionIssues;
265
161
  for (let attempt = 0; attempt < 3; attempt++) {
266
- const response = await run("planner", attempt ? "repair" : "planning", `Read-only depth assessment of admitted scope. Select Standard or higher unless explicit Focused low-risk evidence is established. Return RAFI_QA_DEPTH_START/END with QaPreparationDepthDecisionV1. Schema: ${JSON.stringify(qaDepthDecisionSchema)}. Policy qa-preparation-v1. Host minimums: interactions/compatibility/state/external dependencies=3; authorization/security/data/migration/concurrency/recovery/deployment=4; unresolved architectural uncertainty=5. Do not change the admitted scope, inputs, requirements, or risk evidence. Every riskFactors.category must appear verbatim in minimumReasons. ${attempt ? `Correct these validation issues from the previous response: ${JSON.stringify(plannerIssues)}` : ""}\n${JSON.stringify(initial)}`);
267
- let proposed;
268
- try {
269
- proposed = await parsed(response, "RAFI_QA_DEPTH");
270
- }
271
- catch (error) {
272
- plannerIssues = [`QA planner response was not a valid RAFI_QA_DEPTH envelope: ${error instanceof Error ? error.message : String(error)}`];
273
- if (attempt === 2)
274
- throw new Error(`depth-decision-incomplete: ${plannerIssues.join("; ")}`);
275
- continue;
276
- }
162
+ const response = await run("planner", attempt ? "repair" : "planning", `Read-only depth assessment of admitted scope. Select Standard or higher unless explicit Focused low-risk evidence is established. Return the native depth payload with QaPreparationDepthDecisionV1. Schema: ${JSON.stringify(depthProposalSchema)}. Policy qa-preparation-v1. Host minimums: interactions/compatibility/state/external dependencies=3; authorization/security/data/migration/concurrency/recovery/deployment=4; unresolved architectural uncertainty=5. Do not change the admitted scope, inputs, requirements, or risk evidence. Every riskFactors.category must appear verbatim in minimumReasons. ${attempt ? `Correct these validation issues from the previous response: ${JSON.stringify(plannerIssues)}` : ""}\n${JSON.stringify(initial)}`);
163
+ const proposed = await parsed(response, "RAFI_QA_DEPTH");
164
+ const depthBefore = structuredClone(proposed);
277
165
  proposed.plannerOperationId = response.operationId;
278
166
  proposed.plannerIdentity = response.sessionId;
279
167
  // minimumReasons is a host-validated restatement of the planner's
@@ -281,6 +169,9 @@ export async function prepareVerificationContract(initial, inventory, deps) {
281
169
  // the selected level, risk evidence, or rationale.
282
170
  const requiredReasons = proposed.riskFactors.map(risk => risk.category);
283
171
  proposed.minimumReasons = [...proposed.minimumReasons, ...requiredReasons.filter(category => !proposed.minimumReasons.includes(category))];
172
+ store.putArtifact("host-normalization", { ...qaHostTransformation({ rule: "depth-host-bindings-v1", operation: response.output.operation,
173
+ tupleDigest: response.selectedDispatch.digest, before: depthBefore, after: proposed }),
174
+ inputRef: store.putArtifact("host-transformation-input", depthBefore), outputRef: store.putArtifact("host-transformation-output", proposed) });
284
175
  plannerIssues = validateDepthDecision(proposed);
285
176
  if (!plannerIssues.length) {
286
177
  candidate.depthDecision = proposed;
@@ -296,44 +187,38 @@ export async function prepareVerificationContract(initial, inventory, deps) {
296
187
  store.retainProgress(budget, "depth-decision", candidate.depthDecision);
297
188
  let authorSessionId = retained?.authorSessionId ?? "";
298
189
  for (let pass = 0; pass < (candidate.depthDecision.level <= 2 ? 2 : 3) && (!retained || targetedConcerns.length > 0 || validateCandidate(candidate, inventory, store.equivalentResolver()).length > 0); pass++) {
299
- const response = await run("prepare", "investigation", `QA preparation only. Inspect relevant source/tests; do not implement, install dependencies, or expand admitted product scope. Return RAFI_QA_CANDIDATE_START/END with the provider draft schema: ${JSON.stringify(providerDraftSchema)}. Return ONLY the five draft fields in that schema: do not return contract identity, depth, inputs or requirements. Rafi owns and calculates timestamps, source and evidence hashes, operation/session identities and requirement text; you do not need shell access or hash calculations. Cite existing workspace-relative file paths and locations that you inspected. Use only the exact authoritative requirement IDs listed in the inventory below. Never invent, shorten, rename, derive, or copy a requirement ID from a filename, check ID, plan text, or sibling ticket. Every check.requirementRefs entry must be an exact ID from the inventory, and every coverage.requirementId must be an exact ID from the inventory; if a check has no applicable requirement, do not emit that check. Use the supplied requirement IDs and cover each original requirement, including rules and skills, with at least one mandatory check for each mandatory requirement. Each coverage row's checkIds must name checks whose requirementRefs array includes that exact row requirementId; every check must have at least one valid requirementRef. Verification cwd must be relative to the workspace; never copy an absolute disposable-snapshot path into cwd, commands, evidence, or prose. Use the stable relative location scratch/<filename> for bounded temporary logs. dependsOnChecks references other check IDs; prerequisiteRefs references only IDs of observations in baseline, not check IDs or host-preflight labels. Use empty arrays when none apply. Checks for future Builder work must use postimplementation timing; only checks already verified before Builder starts may use preimplementation or both timing, and every mandatory preimplementation/both check requires a matching passed baseline observation bound by checkId. Conditional observations also need predicate evidence. In this schema preimplementation means already verified before Builder starts, not a step Builder must perform before editing. For test-first or other Builder-executed sequencing, use postimplementation timing and require retained evidence proving the prescribed order; do not claim a future step has already passed. Future implementation and verification evidence is expected to be produced by Builder and final QA; specify it in the checks rather than treating its ordinary pre-build absence as a material unresolved concern. Do report genuine current blockers or uncertainties as unresolved. Scope checks must filter to approved product paths or ticket files; never require an exact whole-worktree status because Rafi-owned bookkeeping paths such as .tickets, .rafi, docs, configuration, and agent instructions are expected host changes. Supply one preparationEvidence entry for EACH required obligation, using its exact obligation label separately, with its own analysis and cited existing files; do not combine labels into a single entry. Supply concrete check methods/evidence and complete coverage. No disposition or waiver can be agent-authored. If risks exceed selected depth, return a separate RAFI_QA_ESCALATION_START/END JSON block {"version":1,"rationale":"...","riskFactors":[{"category":"...","rationale":"...","references":["..."]}]}; do not change the depth yourself.\nValidation issues to correct from the previous draft: ${JSON.stringify(correctionIssues)}\nPreviously retained independent concerns requiring targeted investigation and concrete resolution: ${JSON.stringify(targetedConcerns)}\nRequired obligations: ${depthObligations(candidate.depthDecision.level).join(", ")}\nThe authoritative candidate/input inventory below contains the exact allowed requirement ID set.\nAuthoritative candidate/input inventory:\n${JSON.stringify(candidate)}`);
300
- const rawProposal = await parsed(response, "RAFI_QA_CANDIDATE");
301
- const legacy = Object.hasOwn(rawProposal, "runId");
302
- const proposal = legacy ? rawProposal : normalizeProviderDraftMetadata(rawProposal);
303
- if (!proposal || typeof proposal !== "object" || Array.isArray(proposal))
304
- throw new Error("QA preparation draft must be an object");
305
- if (!legacy && !validateProviderDraft(proposal)) {
306
- correctionIssues = (validateProviderDraft.errors ?? []).map(error => `${error.instancePath || "/"} ${error.message}`);
307
- if (pass + 1 >= (candidate.depthDecision.level <= 2 ? 2 : 3))
308
- throw new Error(`QA preparation draft correction exhausted: ${correctionIssues.join("; ")}`);
190
+ const response = await run("prepare", "investigation", `QA preparation only. Inspect relevant source/tests; do not implement, install dependencies, or expand admitted product scope. Return the native preparation-draft payload matching the supplied native schema. Do not return contract identity, depth, inputs or requirements. Rafi owns and calculates timestamps, source and evidence hashes, operation/session identities and requirement text; you do not need shell access or hash calculations. Your completed operation result has only kind, summary, and payload: never add any field such as escalation beside result.payload. Choose exactly one payload shape: (1) a normal {"kind":"draft","checks":[],"coverage":[],"baseline":[],"preparationEvidence":[],"unresolved":[]} payload, or (2) {"kind":"escalation","escalation":{"version":1,"rationale":"...","riskFactors":[{"category":"...","rationale":"...","references":["..."]}]}} when risk exceeds selected depth. Do not combine a draft and an escalation. Cite existing workspace-relative file paths and locations that you inspected. Use only the exact authoritative requirement IDs listed in the inventory below. Never invent, shorten, rename, derive, or copy a requirement ID from a filename, check ID, plan text, or sibling ticket. Every check.requirementRefs entry must be an exact ID from the inventory, and every coverage.requirementId must be an exact ID from the inventory; if a check has no applicable requirement, do not emit that check. Use the supplied requirement IDs and cover each original requirement, including rules and skills, with at least one mandatory check for each mandatory requirement. Each coverage row's checkIds must name checks whose requirementRefs array includes that exact row requirementId; every check must have at least one valid requirementRef. Verification cwd must be relative to the workspace; never copy an absolute disposable-snapshot path into cwd, commands, evidence, or prose. Use the stable relative location scratch/<filename> for bounded temporary logs. dependsOnChecks references other check IDs; prerequisiteRefs references only IDs of observations in baseline, not check IDs or host-preflight labels. Use empty arrays when none apply. Checks for future Builder work must use postimplementation timing; only checks already verified before Builder starts may use preimplementation or both timing, and every mandatory preimplementation/both check requires a matching passed baseline observation bound by checkId. Conditional observations also need predicate evidence. In this schema preimplementation means already verified before Builder starts, not a step Builder must perform before editing. For test-first or other Builder-executed sequencing, use postimplementation timing and require retained evidence proving the prescribed order; do not claim a future step has already passed. Future implementation and verification evidence is expected to be produced by Builder and final QA; specify it in the checks rather than treating its ordinary pre-build absence as a material unresolved concern. Do report genuine current blockers or uncertainties as unresolved. Scope checks must filter to approved product paths or ticket files; never require an exact whole-worktree status because Rafi-owned bookkeeping paths such as .tickets, .rafi, docs, configuration, and agent instructions are expected host changes. Supply one preparationEvidence entry for EACH required obligation, using its exact obligation label separately, with its own analysis and cited existing files; do not combine labels into a single entry. Supply concrete check methods/evidence and complete coverage. No disposition or waiver can be agent-authored.\nValidation issues to correct from the previous draft: ${JSON.stringify(correctionIssues)}\nPreviously retained independent concerns requiring targeted investigation and concrete resolution: ${JSON.stringify(targetedConcerns)}\nRequired obligations: ${depthObligations(candidate.depthDecision.level).join(", ")}\nThe authoritative candidate/input inventory below contains the exact allowed requirement ID set.\nAuthoritative candidate/input inventory:\n${JSON.stringify(candidate)}`);
191
+ const payload = await parsed(response, "RAFI_QA_CANDIDATE");
192
+ if (payload.kind === "escalation") {
193
+ const request = parseProviderContract(qaPreparationEscalationProposalV1Schema, payload.escalation, "QA preparation escalation");
194
+ const minimum = Math.max(candidate.depthDecision.level, ...request.riskFactors.map(risk => QA_RISK_MINIMUMS[risk.category]));
195
+ const revision = await run("planner", "escalation", `Revise only the QA depth for this admitted scope. Do not expand product scope. Host minimum ${minimum}; keep predecessorDecisionId ${candidate.depthDecision.decisionId}. Return the native depth payload following ${JSON.stringify(qaDepthDecisionSchema)}. Previous decision: ${JSON.stringify(candidate.depthDecision)}; cited risk evidence: ${JSON.stringify(request)}`);
196
+ const decision = await parsed(revision, "RAFI_QA_DEPTH");
197
+ const before = structuredClone(decision);
198
+ decision.plannerIdentity = revision.sessionId;
199
+ decision.plannerOperationId = revision.operationId;
200
+ store.putArtifact("host-normalization", { ...qaHostTransformation({ rule: "depth-host-bindings-v1", operation: revision.output.operation,
201
+ tupleDigest: revision.selectedDispatch.digest, before, after: decision }),
202
+ inputRef: store.putArtifact("host-transformation-input", before), outputRef: store.putArtifact("host-transformation-output", decision) });
203
+ const errors = validateDepthDecision(decision, candidate.depthDecision);
204
+ if (decision.level < minimum || request.riskFactors.some(risk => !decision.riskFactors.some(actual => actual.category === risk.category)))
205
+ errors.push("Planner revision does not address evidenced risk minimum");
206
+ if (errors.length)
207
+ throw new Error(`depth-decision-incomplete: ${errors.join("; ")}`);
208
+ candidate.depthDecision = decision;
209
+ store.reviseDepth(budget, decision);
210
+ store.retainProgress(budget, "depth-decision", decision);
309
211
  continue;
310
212
  }
311
- if (legacy) {
312
- const depth = proposal.depthDecision;
313
- const inputs = proposal.inputs;
314
- // Legacy models sometimes remove Markdown code ticks from descriptive
315
- // text. Compare those fields narrowly; never normalize references or
316
- // risk categories and never publish the provider's copy.
317
- const prose = (value) => value.replace(/`/g, "");
318
- const sameDepthProse = !!depth && typeof depth.rationale === "string" && Array.isArray(depth.minimumReasons) && depth.minimumReasons.every(item => typeof item === "string") && Array.isArray(depth.riskFactors) && depth.riskFactors.every(item => item && typeof item.rationale === "string") && (!depth.lowRisk || [depth.lowRisk.narrowBehavior, depth.lowRisk.understoodDependencies, depth.lowRisk.adequateVerification].every(item => typeof item === "string")) && prose(depth.rationale) === prose(candidate.depthDecision.rationale)
319
- && canonicalContractJson(depth.minimumReasons.map(prose)) === canonicalContractJson(candidate.depthDecision.minimumReasons.map(prose))
320
- && canonicalContractJson(depth.riskFactors.map(risk => ({ ...risk, rationale: prose(risk.rationale) }))) === canonicalContractJson(candidate.depthDecision.riskFactors.map(risk => ({ ...risk, rationale: prose(risk.rationale) })))
321
- && canonicalContractJson(depth.lowRisk ? { narrowBehavior: prose(depth.lowRisk.narrowBehavior), understoodDependencies: prose(depth.lowRisk.understoodDependencies), adequateVerification: prose(depth.lowRisk.adequateVerification) } : null) === canonicalContractJson(candidate.depthDecision.lowRisk ? { narrowBehavior: prose(candidate.depthDecision.lowRisk.narrowBehavior), understoodDependencies: prose(candidate.depthDecision.lowRisk.understoodDependencies), adequateVerification: prose(candidate.depthDecision.lowRisk.adequateVerification) } : null);
322
- const matchingReference = (actual, expected) => actual === expected || (!actual.startsWith("/") && !actual.split(/[\\/]/).includes("..") && expected.endsWith(`/${actual}`));
323
- const matchingInputs = Array.isArray(inputs) && inputs.length === candidate.inputs.length && inputs.every((input, index) => {
324
- const expected = candidate.inputs[index];
325
- return expected && input && typeof input.reference === "string" && matchingReference(input.reference, expected.reference) && canonicalContractJson({ ...input, reference: expected.reference }) === canonicalContractJson(expected);
326
- });
327
- if (proposal.runId !== candidate.runId || proposal.workId !== candidate.workId || proposal.admissionDigest !== candidate.admissionDigest || proposal.contractId !== candidate.contractId || proposal.revision !== candidate.revision || !depth || depth.level !== candidate.depthDecision.level || depth.minimumLevel !== candidate.depthDecision.minimumLevel || depth.policyVersion !== candidate.depthDecision.policyVersion || depth.decisionId !== candidate.depthDecision.decisionId || !sameDepthProse || !matchingInputs)
328
- throw new Error("Preparation changed host-owned identity/depth/input authority");
329
- }
330
- else if (Object.keys(proposal).some(key => !["checks", "coverage", "baseline", "preparationEvidence", "unresolved"].includes(key)))
331
- throw new Error("QA preparation draft contains unexpected host-owned or unsupported fields");
332
- if (![proposal.checks, proposal.coverage, proposal.baseline, proposal.preparationEvidence, proposal.unresolved].every(Array.isArray))
333
- throw new Error("QA preparation draft is missing required proposal arrays");
334
- if (!legacy && !deps.normalizeDraft)
213
+ const { kind: _kind, ...rawProposal } = payload;
214
+ // Only provider-owned fields can cross the native contract. Historical
215
+ // full-candidate envelopes are read by historical readers, never here.
216
+ const proposal = rawProposal;
217
+ if (!validateProviderDraft(proposal))
218
+ throw new Error(`Invalid native draft: ${JSON.stringify(validateProviderDraft.errors)}`);
219
+ if (!deps.normalizeDraft)
335
220
  throw new Error("QA preparation host evidence normalization is unavailable");
336
- const draft = { ...structuredClone(candidate), checks: proposal.checks, coverage: proposal.coverage, baseline: proposal.baseline, preparationEvidence: proposal.preparationEvidence, unresolved: proposal.unresolved, ...(legacy && proposal.requirements ? { requirements: proposal.requirements } : {}) };
221
+ const draft = { ...structuredClone(candidate), checks: proposal.checks, coverage: proposal.coverage, baseline: proposal.baseline, preparationEvidence: proposal.preparationEvidence, unresolved: proposal.unresolved };
337
222
  draft.createdAt = candidate.createdAt;
338
223
  if (candidate.predecessorDigest === undefined)
339
224
  delete draft.predecessorDigest;
@@ -349,7 +234,13 @@ export async function prepareVerificationContract(initial, inventory, deps) {
349
234
  draft.requirements.push(...inventory.filter(item => !present.has(item.id)).map(item => structuredClone(item)));
350
235
  }
351
236
  try {
352
- candidate = removeExtraneousRequirementReferences(enforceMandatoryCoverage(deps.normalizeDraft ? deps.normalizeDraft(draft, response) : draft), inventory);
237
+ const before = structuredClone(draft);
238
+ const normalized = enforceMandatoryCoverage(deps.normalizeDraft ? deps.normalizeDraft(draft, response) : draft);
239
+ const record = qaHostTransformation({ rule: "draft-host-bindings-v1", operation: response.output.operation,
240
+ tupleDigest: response.selectedDispatch.digest, before, after: normalized, workspacePaths: response.workspacePaths });
241
+ store.putArtifact("host-normalization", { ...record, workspacePaths: response.workspacePaths,
242
+ inputRef: store.putArtifact("host-transformation-input", before), outputRef: store.putArtifact("host-transformation-output", normalized) });
243
+ candidate = normalized;
353
244
  }
354
245
  catch (error) {
355
246
  const detail = error instanceof Error ? error.message : String(error);
@@ -363,23 +254,6 @@ export async function prepareVerificationContract(initial, inventory, deps) {
363
254
  evidence.operationId = response.operationId;
364
255
  evidence.sessionId = response.sessionId;
365
256
  }
366
- if (response.text.includes("RAFI_QA_ESCALATION_START")) {
367
- const request = parseProviderContract(qaPreparationEscalationProposalV1Schema, parsePreparationArtifact(response.text, "RAFI_QA_ESCALATION"), "QA preparation escalation");
368
- const minimum = Math.max(candidate.depthDecision.level, ...request.riskFactors.map(risk => QA_RISK_MINIMUMS[risk.category]));
369
- const revision = await run("planner", "escalation", `Revise only the QA depth for this admitted scope. Do not expand product scope. Host minimum ${minimum}; keep predecessorDecisionId ${candidate.depthDecision.decisionId}. Return RAFI_QA_DEPTH_START/END following ${JSON.stringify(qaDepthDecisionSchema)}. Previous decision: ${JSON.stringify(candidate.depthDecision)}; cited risk evidence: ${JSON.stringify(request)}`);
370
- const decision = await parsed(revision, "RAFI_QA_DEPTH");
371
- decision.plannerIdentity = revision.sessionId;
372
- decision.plannerOperationId = revision.operationId;
373
- const errors = validateDepthDecision(decision, candidate.depthDecision);
374
- if (decision.level < minimum || request.riskFactors.some(risk => !decision.riskFactors.some(actual => actual.category === risk.category)))
375
- errors.push("Planner revision does not address evidenced risk minimum");
376
- if (errors.length)
377
- throw new Error(`depth-decision-incomplete: ${errors.join("; ")}`);
378
- candidate.depthDecision = decision;
379
- store.reviseDepth(budget, decision);
380
- store.retainProgress(budget, "depth-decision", decision);
381
- continue;
382
- }
383
257
  const structural = validateCandidate(candidate, inventory, store.equivalentResolver());
384
258
  if (!structural.length)
385
259
  break;
@@ -391,8 +265,8 @@ export async function prepareVerificationContract(initial, inventory, deps) {
391
265
  throw new Error(`Contract incomplete: ${structural.join("; ")}`);
392
266
  const binding = { draftPayloadDigest: draftDigest(candidate), inventoryDigest: inventoryDigest(candidate.requirements), policyVersion: candidate.depthDecision.policyVersion, authorSessionId };
393
267
  const retainedAssessment = store.progress(budget, "assessment");
394
- const normalizeAssessment = (value, response, fallback) => {
395
- const normalized = normalizeAssessmentObligations(normalizeAssessmentMetadata(value), depthObligations(candidate.depthDecision.level));
268
+ const normalizeAssessment = (value, response) => {
269
+ const normalized = structuredClone(value);
396
270
  // These bindings are host-owned. A repair may omit them or emit a
397
271
  // placeholder; restore only the exact current binding and retained
398
272
  // obligation labels, never provider-invented content.
@@ -405,13 +279,9 @@ export async function prepareVerificationContract(initial, inventory, deps) {
405
279
  normalized.sessionId = response.sessionId;
406
280
  if (response.operationId)
407
281
  normalized.operationId = response.operationId;
408
- if ((!normalized.assessedObligations || normalized.assessedObligations.length === 0) && fallback?.assessedObligations?.length)
409
- normalized.assessedObligations = [...fallback.assessedObligations];
410
- if ((!normalized.assessedRequirementIds || normalized.assessedRequirementIds.length === 0) && fallback?.assessedRequirementIds?.length)
411
- normalized.assessedRequirementIds = [...fallback.assessedRequirementIds];
412
282
  return normalized;
413
283
  };
414
- let assessmentResponse = retainedAssessment?.draftPayloadDigest === draftDigest(candidate) ? undefined : await run("assess", "assessment", `Independent semantic assessment in a fresh conversation. Assess each original requirement and selected-depth obligation; identify omissions, conflicts, practical verification gaps, accidental scope, and unsupported dispositions. Treat Rafi-owned bookkeeping paths (.tickets, .rafi, generated docs/configuration, and agent instructions) as expected host changes. Scope concerns must be evaluated against approved product paths and ticket requirements; do not require an exact full-worktree status listing. Future Builder actions are not preparation failures: when an approved requirement specifies test-first ordering, a migration, rollback rehearsal, or other implementation-time sequence, represent it as a mandatory postimplementation check requiring retained Builder evidence. Do not mark the absence of that future evidence before Builder starts as a material concern, and do not demand that preparation reproduce historical order. Only report a material concern when the contract cannot state an actionable postimplementation check or when an already-required preimplementation fact is missing. Return RAFI_QA_ASSESSMENT_START/END with QaSemanticAssessmentV1 (schema: ${JSON.stringify(qaPreparationDepthDecisionV1Schema)}). The concerns field is an array of objects, never strings; every concern object must have exactly these six fields and no others: {reason:string, requirementIds:string[], checkIds:string[], material:boolean, owner:string, nextAction:string}. Do not use legacy fields such as severity, evidence, title, message, category, or resolution. Use concerns: [] only when all issues are resolved. assessedRequirementIds MUST contain every ID in this exact list, even when a requirement is host-owned or has no concern: ${JSON.stringify(candidate.requirements.map(requirement => requirement.id))}. Put every individually assessed required obligation label in assessedObligations, using the exact labels below; do not combine labels or claim to have assessed anything you did not assess. Required obligation labels: ${JSON.stringify(depthObligations(candidate.depthDecision.level))}. Bind exactly ${JSON.stringify(binding)}.\nPrior independent concerns requiring concrete resolution evidence: ${JSON.stringify(targetedConcerns)}\nOriginal inventory:\n${JSON.stringify(inventory)}\nDraft:\n${JSON.stringify(candidate)}`);
284
+ let assessmentResponse = retainedAssessment?.draftPayloadDigest === draftDigest(candidate) ? undefined : await run("assess", "assessment", `Independent semantic assessment in a fresh conversation. Assess each original requirement and selected-depth obligation; identify omissions, conflicts, practical verification gaps, accidental scope, and unsupported dispositions. Treat Rafi-owned bookkeeping paths (.tickets, .rafi, generated docs/configuration, and agent instructions) as expected host changes. Scope concerns must be evaluated against approved product paths and ticket requirements; do not require an exact full-worktree status listing. Future Builder actions are not preparation failures: when an approved requirement specifies test-first ordering, a migration, rollback rehearsal, or other implementation-time sequence, represent it as a mandatory postimplementation check requiring retained Builder evidence. Do not mark the absence of that future evidence before Builder starts as a material concern, and do not demand that preparation reproduce historical order. If a concern's only next action is to inspect, compare, run, or otherwise produce evidence for a check that is already declared with postimplementation timing, set that concern material:false; it is an actionable future check, not a preparation blocker. Only report a material concern when the contract cannot state an actionable postimplementation check or when an already-required preimplementation fact is missing. Return the native preparation-assessment payload matching the supplied native preparation-assessment schema. The concerns field is an array of objects, never strings; every concern object must have exactly these eight fields and no others: {reason:string, requirementIds:string[], checkIds:string[], material:boolean, owner:string, nextAction:string, category:"evidence-gap|contract-gap|scope-conflict|verification-gap", timing:"preimplementation|postimplementation|both"}. Do not use legacy fields such as severity, evidence, title, message, or resolution. Use concerns: [] only when all issues are resolved. assessedRequirementIds MUST contain every ID in this exact list, even when a requirement is host-owned or has no concern: ${JSON.stringify(candidate.requirements.map(requirement => requirement.id))}. Put every individually assessed required obligation label in assessedObligations, using the exact labels below; do not combine labels or claim to have assessed anything you did not assess. Required obligation labels: ${JSON.stringify(depthObligations(candidate.depthDecision.level))}. The host binds dispatch, source, policy and author identities; do not echo them in the payload.\nPrior independent concerns requiring concrete resolution evidence: ${JSON.stringify(targetedConcerns)}\nOriginal inventory:\n${JSON.stringify(inventory)}\nDraft:\n${JSON.stringify(candidate)}`);
415
285
  let assessed = assessmentResponse
416
286
  ? await parsedContract(assessmentResponse, "RAFI_QA_ASSESSMENT", validateAssessmentContract, "QA semantic assessment", (value, response) => normalizeAssessment(value, response))
417
287
  : { value: retainedAssessment, response: undefined };
@@ -419,9 +289,43 @@ export async function prepareVerificationContract(initial, inventory, deps) {
419
289
  // session identity, but it cannot prove or safely author the builder's ID.
420
290
  assessed.value.authorSessionId = binding.authorSessionId;
421
291
  for (let repair = 0; assessmentResponse && repair < 3; repair++) {
292
+ // Native concerns retain their declared materiality; prose cannot downgrade it.
422
293
  const semanticIssues = validateAssessment(candidate, assessed.value, authorSessionId);
423
294
  if (!semanticIssues.length)
424
295
  break;
296
+ const materialConcerns = assessed.value.concerns.filter(concern => concern.material);
297
+ if (materialConcerns.length) {
298
+ const routes = materialConcerns.map(concern => ({
299
+ concern: {
300
+ category: concern.category ?? "unknown",
301
+ timing: concern.timing ?? "unknown",
302
+ requirementIds: [...concern.requirementIds],
303
+ checkIds: [...concern.checkIds],
304
+ reason: concern.reason,
305
+ },
306
+ ...classifyMaterialConcern(concern),
307
+ }));
308
+ const classificationRef = store.putArtifact("preparation-concern-routing-v1", {
309
+ version: 1,
310
+ runId: initial.runId,
311
+ workId: initial.workId,
312
+ admissionDigest: initial.admissionDigest,
313
+ targetArtifactDigest: draftDigest(candidate),
314
+ originatingAssessmentReceipt: assessed.response?.operationId ?? assessed.value.operationId,
315
+ routes,
316
+ });
317
+ store.retainProgress(budget, "assessment", assessed.value);
318
+ const nextAction = routes.map(route => route.action).join(" ");
319
+ const detail = `Material concern requires host-owned routing (${routes.map(route => route.route).join(", ")}); assessment repair is not authorized: ${materialConcerns.map(concern => concern.reason).join(" | ").slice(0, 1600)}`;
320
+ store.setState(initial.runId, initial.workId, initial.admissionDigest, "blocked", detail, store.head(initial.runId, initial.workId, initial.admissionDigest).generation);
321
+ store.event(initial.runId, initial.workId, `preparation-concern-routing:${classificationRef}`, "preparation-concern-routing", {
322
+ classificationRef,
323
+ routes,
324
+ targetArtifactDigest: draftDigest(candidate),
325
+ originatingAssessmentReceipt: assessed.response?.operationId ?? assessed.value.operationId,
326
+ });
327
+ return { status: "blocked", detail, nextAction };
328
+ }
425
329
  if (repair === 2)
426
330
  throw new Error(semanticIssues.join("; "));
427
331
  const preservedAuthorSessionId = assessed.value.authorSessionId;
@@ -435,8 +339,8 @@ export async function prepareVerificationContract(initial, inventory, deps) {
435
339
  authorSessionId: assessed.value.authorSessionId,
436
340
  };
437
341
  const freshAssessment = semanticIssues.includes("Assessment lacks independent authorship");
438
- assessmentResponse = await run(freshAssessment ? "assess" : "repair", freshAssessment ? "assessment" : "repair", `${freshAssessment ? "Fresh independent assessment required: use a new QA provider session, distinct from the preparation author session. Do not reuse or claim the preparation session." : "Bounded semantic repair for QA semantic assessment."} Preserve the independent review, but correct only these host validation issues: ${JSON.stringify(semanticIssues)}. Use only requirement IDs from the authoritative inventory and only check IDs declared in the supplied draft; never invent aliases such as qa-c1 or qa-c6, and never cite undefined retained evidence. If a concern is not supported by an actual requirement or declared check, remove or rewrite that concern. Return only RAFI_QA_ASSESSMENT_START/END with the complete corrected assessment. Binding: ${JSON.stringify(binding)}\nAuthoritative requirement IDs: ${JSON.stringify(candidate.requirements.map(requirement => requirement.id))}\nDeclared check IDs: ${JSON.stringify(candidate.checks.map(check => check.id))}\nBounded retained assessment fields to preserve or correct:\n${JSON.stringify(boundedRetainedAssessment)}`, assessed.response?.operationId ?? assessed.response?.sessionId);
439
- const repaired = await parsedContract(assessmentResponse, "RAFI_QA_ASSESSMENT", validateAssessmentContract, "QA semantic assessment", (value, response) => normalizeAssessment(value, response, assessed.value));
342
+ assessmentResponse = await run(freshAssessment ? "assess" : "repair", freshAssessment ? "assessment" : "repair", `${freshAssessment ? "Fresh independent assessment required: use a new QA provider session, distinct from the preparation author session. Do not reuse or claim the preparation session." : "Bounded semantic repair for QA semantic assessment."} Preserve the independent review, but correct only these host validation issues: ${JSON.stringify(semanticIssues)}. Use only requirement IDs from the authoritative inventory and only check IDs declared in the supplied draft; never invent aliases such as qa-c1 or qa-c6, and never cite undefined retained evidence. If a concern is not supported by an actual requirement or declared check, remove or rewrite that concern. Return only the native preparation-assessment payload with the complete corrected assessment. Binding: ${JSON.stringify(binding)}\nAuthoritative requirement IDs: ${JSON.stringify(candidate.requirements.map(requirement => requirement.id))}\nDeclared check IDs: ${JSON.stringify(candidate.checks.map(check => check.id))}\nBounded retained assessment fields to preserve or correct:\n${JSON.stringify(boundedRetainedAssessment)}`, assessed.response?.operationId ?? assessed.response?.sessionId, assessmentOutput);
343
+ const repaired = await parsedContract(assessmentResponse, "RAFI_QA_ASSESSMENT", validateAssessmentContract, "QA semantic assessment", (value, response) => normalizeAssessment(value, response));
440
344
  // Repairs may omit this immutable binding while rewriting concerns. Preserve
441
345
  // it only when the prior parsed assessment supplied it; a conflicting value
442
346
  // remains visible to validateAssessment and still fails closed.
@@ -445,7 +349,7 @@ export async function prepareVerificationContract(initial, inventory, deps) {
445
349
  repaired.value.authorSessionId = binding.authorSessionId;
446
350
  assessed = repaired;
447
351
  }
448
- const assessment = normalizeExpectedFutureAssessment(normalizeAssessmentMetadata(assessed.value), candidate);
352
+ const assessment = assessed.value;
449
353
  // These binding fields are host-owned. Providers frequently copy or
450
354
  // truncate the inventory digest while describing their assessment; that
451
355
  // must not invalidate an otherwise substantive independent review.
@@ -461,7 +365,7 @@ export async function prepareVerificationContract(initial, inventory, deps) {
461
365
  if (challenge?.draftPayloadDigest !== draftDigest(candidate))
462
366
  challenge = undefined;
463
367
  if (candidate.depthDecision.level === 5 && !challenge) {
464
- const response = await run("challenge", "challenge", `Independent approach challenge, fresh assessor conversation. Planning only. Return RAFI_QA_CHALLENGE_START/END with QaChallengeReceiptV1 (schema: ${JSON.stringify(qaChallengeReceiptV1Schema)}): independently critique the author's retained approach-proposal evidence (do not author a replacement), independent contract conclusions and separate approachConcerns for scope, invariants, failure-boundaries, verification. Unresolved material concerns prevent readiness. Binding: ${JSON.stringify(binding)}. Previously retained concerns requiring concrete resolution: ${JSON.stringify(targetedConcerns)}.\n${JSON.stringify(candidate)}`);
368
+ const response = await run("challenge", "challenge", `Independent approach challenge, fresh assessor conversation. Planning only. Return the native preparation-challenge payload matching the supplied native preparation-challenge schema: independently critique the author's retained approach-proposal evidence (do not author a replacement), independent contract conclusions and separate approachConcerns for scope, invariants, failure-boundaries, verification. Unresolved material concerns prevent readiness. Binding: ${JSON.stringify(binding)}. Previously retained concerns requiring concrete resolution: ${JSON.stringify(targetedConcerns)}.\n${JSON.stringify(candidate)}`);
465
369
  const challenged = await parsedContract(response, "RAFI_QA_CHALLENGE", validateChallengeContract, "QA approach challenge", (value, response) => normalizeAssessment(value, response));
466
370
  challenge = normalizeAssessmentMetadata(challenged.value);
467
371
  challenge.sessionId = challenged.response.sessionId;