ai-foreman 1.7.29 → 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 (136) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude.d.ts +23 -12
  3. package/dist/adapters/claude.js +227 -23
  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 +85 -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 +15 -0
  22. package/dist/buildApproval.js +25 -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 +22 -26
  31. package/dist/cli/tickets.d.ts +0 -7
  32. package/dist/cli/tickets.js +41 -40
  33. package/dist/continuity.d.ts +10 -14
  34. package/dist/continuity.js +198 -179
  35. package/dist/continuityOrigin.d.ts +13 -0
  36. package/dist/continuityOrigin.js +91 -0
  37. package/dist/foreman.d.ts +10 -4
  38. package/dist/foreman.js +51 -193
  39. package/dist/graph/boundary.d.ts +1 -1
  40. package/dist/graph/boundary.js +1 -1
  41. package/dist/graph/host.js +8 -19
  42. package/dist/graph/manager.d.ts +1 -1
  43. package/dist/graph/manager.js +1 -1
  44. package/dist/graph/preparation.d.ts +38 -0
  45. package/dist/graph/preparation.js +7 -6
  46. package/dist/graph/semantic.d.ts +1 -0
  47. package/dist/graph/semantic.js +4 -1
  48. package/dist/graph/session.js +7 -2
  49. package/dist/graph/turn.d.ts +2 -3
  50. package/dist/graph/turn.js +28 -40
  51. package/dist/handoffProtocol.js +2 -2
  52. package/dist/handoffs.d.ts +3 -1
  53. package/dist/handoffs.js +90 -59
  54. package/dist/hostGraphApplicability.d.ts +2 -1
  55. package/dist/hostGraphApplicability.js +17 -19
  56. package/dist/hostQaAugmentation.js +21 -4
  57. package/dist/hostQaComparison.d.ts +21 -2
  58. package/dist/hostQaComparison.js +43 -29
  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 +47 -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 +4 -9
  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/qaFailureDelivery.js +97 -85
  83. package/dist/qaHostTransformation.d.ts +26 -0
  84. package/dist/qaHostTransformation.js +100 -0
  85. package/dist/qaPreparation.d.ts +8 -20
  86. package/dist/qaPreparation.js +133 -248
  87. package/dist/qaPreparationInputs.js +19 -0
  88. package/dist/qaPrerequisites.d.ts +2 -1
  89. package/dist/qaPrerequisites.js +21 -1
  90. package/dist/qaProtocolV2.d.ts +15 -0
  91. package/dist/qaRecovery.js +21 -4
  92. package/dist/qaReview.js +220 -395
  93. package/dist/qaReviewEvidence.d.ts +7 -3
  94. package/dist/qaReviewEvidence.js +15 -4
  95. package/dist/qaRuntime.js +1 -1
  96. package/dist/qaSnapshot.js +71 -21
  97. package/dist/qaStructuredResponse.d.ts +10 -0
  98. package/dist/qaStructuredResponse.js +57 -0
  99. package/dist/qaTimeline.js +5 -3
  100. package/dist/qaVerificationContract.d.ts +1 -2
  101. package/dist/qaVerificationContract.js +24 -35
  102. package/dist/readinessGate.js +5 -4
  103. package/dist/runtimeReadiness.js +23 -15
  104. package/dist/statusReporter.d.ts +2 -0
  105. package/dist/statusReporter.js +2 -0
  106. package/dist/structuredCutover.d.ts +31 -0
  107. package/dist/structuredCutover.js +510 -0
  108. package/dist/structuredCutoverFiles.d.ts +10 -0
  109. package/dist/structuredCutoverFiles.js +181 -0
  110. package/dist/structuredDispatchAuthority.d.ts +22 -0
  111. package/dist/structuredDispatchAuthority.js +110 -0
  112. package/dist/structuredDispatchRecord.d.ts +29 -0
  113. package/dist/structuredDispatchRecord.js +36 -0
  114. package/dist/structuredLiveBudget.d.ts +2 -0
  115. package/dist/structuredLiveBudget.js +23 -0
  116. package/dist/structuredOperation.d.ts +18 -0
  117. package/dist/structuredOperation.js +257 -0
  118. package/dist/supervisedStart.js +35 -21
  119. package/dist/ticketPlanning.d.ts +1 -373
  120. package/dist/ticketPlanning.js +2 -26
  121. package/dist/ticketPopulation.d.ts +0 -3
  122. package/dist/ticketPopulation.js +4 -21
  123. package/dist/ticketPopulationPageAuthority.d.ts +13 -0
  124. package/dist/ticketPopulationPageAuthority.js +30 -0
  125. package/dist/ticketPopulationPaging.d.ts +27 -0
  126. package/dist/ticketPopulationPaging.js +105 -0
  127. package/dist/tickets/renderMarkdown.d.ts +14 -0
  128. package/dist/tickets/renderMarkdown.js +39 -5
  129. package/dist/tickets/ticketLoader.js +10 -0
  130. package/dist/tickets/ticketSchema.d.ts +107 -0
  131. package/dist/tickets/ticketSchema.js +4 -1
  132. package/dist/workflowDb.d.ts +100 -2
  133. package/dist/workflowDb.js +763 -39
  134. package/dist/workflowReader.d.ts +1 -0
  135. package/dist/workflowReader.js +9 -0
  136. 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,88 +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
- // A provider can report a stale check identity copied into descriptive
48
- // prose from an earlier draft. The host has already validated the
49
- // structured check/coverage/dependency references; when the assessor also
50
- // says the referenced acceptance item is already covered, this is a
51
- // non-material labeling defect rather than an unresolved contract gap.
52
- // Keep the concern in the retained assessment, but do not block Builder.
53
- const staleCoveredReference = /not in the declared check IDs or its dependsOnChecks/i.test(concern.reason)
54
- && /reference can be removed|already covered/i.test(`${concern.reason} ${concern.nextAction}`)
55
- && concern.checkIds.length > 0
56
- && concern.checkIds.every(id => checks.has(id));
57
- if (concern.material && staleCoveredReference)
58
- return { ...concern, material: false };
59
- if (!concern.material || !concern.checkIds.length || !concern.checkIds.every(id => checks.get(id)?.timing === "postimplementation"))
60
- return concern;
61
- 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)
62
- && /\b(?:before|pre[- ]?builder|not yet)\b/i.test(concern.reason)
63
- && /\b(?:implement|add|create|rerun|re-?run|builder|after implementation)\b/i.test(concern.nextAction);
64
- const inventedSiblingAbsence = /\b(?:no|without|absen[ct]|missing)\s+(?:the\s+)?[a-z0-9_-]+\s+export\b|\b(?:sibling|other)\s+(?:feature|slice)\s+(?:absen[ct]|removed|present)\b/i.test(concern.reason)
65
- && /\b(?:own|imports? only|no [a-z0-9_-]+ symbol|drop|remove)\b/i.test(concern.nextAction);
66
- return expectedFutureAbsence || inventedSiblingAbsence ? { ...concern, material: false } : concern;
67
- });
68
- 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." };
69
38
  }
70
- // The provider proposes verification work; the host owns contract identity,
71
- // approved inputs, requirement text, depth, timestamps and evidence hashes.
72
- const durableFields = qaContractCandidateV1Schema.properties;
73
- const providerEvidence = { type: "object", additionalProperties: false, required: ["obligation", "references", "analysis"], properties: {
74
- obligation: { type: "string", minLength: 1 }, analysis: { type: "string", minLength: 1 },
75
- references: { type: "array", minItems: 1, items: { type: "object", additionalProperties: false, required: ["path", "locator"], properties: { path: { type: "string", minLength: 1 }, locator: { type: "string", minLength: 1 } } } },
76
- } };
77
- const providerBaseline = { type: "object", additionalProperties: false, required: ["id", "runtime", "procedure", "status", "relevance", "expectedTransition"], properties: {
78
- 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 },
79
- } };
80
- const providerDraftSchema = { type: "object", additionalProperties: false, required: ["checks", "coverage", "baseline", "preparationEvidence", "unresolved"], properties: {
81
- checks: durableFields.checks, coverage: durableFields.coverage, baseline: { type: "array", items: providerBaseline }, preparationEvidence: { type: "array", minItems: 1, items: providerEvidence }, unresolved: durableFields.unresolved,
82
- } };
83
39
  const contractValidator = new Ajv({ allErrors: true });
84
40
  const validateProviderDraft = contractValidator.compile(providerDraftSchema);
85
41
  const validateAssessmentContract = contractValidator.compile(qaSemanticAssessmentV1Schema);
86
42
  const validateChallengeContract = contractValidator.compile(qaChallengeReceiptV1Schema);
87
- /** Providers sometimes echo host-owned evidence metadata into the draft. */
88
- function normalizeProviderDraftMetadata(value) {
89
- const copy = structuredClone(value);
90
- for (const evidence of copy.preparationEvidence ?? []) {
91
- const metadata = evidence;
92
- delete metadata.operationId;
93
- delete metadata.sessionId;
94
- delete metadata.evidenceDigest;
95
- delete metadata.graphReceiptRefs;
96
- for (const reference of evidence.references ?? [])
97
- delete reference.digest;
98
- }
99
- return copy;
100
- }
101
43
  /**
102
44
  * A mandatory requirement must have at least one mandatory verification check.
103
45
  * Providers occasionally return a concrete check with the weaker advisory
@@ -131,31 +73,6 @@ export function enforceMandatoryCoverage(candidate) {
131
73
  }
132
74
  return copy;
133
75
  }
134
- /**
135
- * Codex occasionally appends a made-up requirement ID to an otherwise valid
136
- * check. Remove only those extraneous annotations when the check still has an
137
- * exact authoritative reference. Never invent or remap an ID, and leave a
138
- * check with no valid references untouched so normal validation fails closed.
139
- */
140
- export function removeExtraneousRequirementReferences(candidate, inventory) {
141
- const copy = structuredClone(candidate);
142
- const allowed = new Set(inventory.map(requirement => requirement.id));
143
- for (const check of copy.checks) {
144
- const valid = check.requirementRefs.filter(id => allowed.has(id));
145
- if (valid.length > 0 && valid.length !== check.requirementRefs.length)
146
- check.requirementRefs = [...new Set(valid)];
147
- }
148
- return copy;
149
- }
150
- function providerBlocker(text, tag) {
151
- // A refusal is not a malformed candidate and cannot be repaired by formatting.
152
- 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`))))
153
- return undefined;
154
- if (!/\b(?:cannot|can't|unable|blocked|denied|not permitted|lack(?:s|ing) access)\b/i.test(text))
155
- return undefined;
156
- const explanation = text.replace(/\s+/g, " ").trim().slice(0, 500);
157
- return `QA provider could not produce ${tag}: ${explanation}`;
158
- }
159
76
  export async function prepareVerificationContract(initial, inventory, deps) {
160
77
  const store = deps.store;
161
78
  const head = store.head(initial.runId, initial.workId, initial.admissionDigest);
@@ -184,20 +101,17 @@ export async function prepareVerificationContract(initial, inventory, deps) {
184
101
  if (retained && canonicalContractJson(candidate.inputs) !== canonicalContractJson(initial.inputs))
185
102
  return { status: "blocked", detail: "Retained draft inputs changed", nextAction: "Reconcile authoritative input changes" };
186
103
  let correctionIssues = [];
187
- 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);
188
106
  const effectivePhase = phase === "prepare" && correctionIssues.length > 0 ? "repair" : phase;
189
107
  if (phase === "assess")
190
- 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}`;
191
- if (phase === "assess" && deps.provider === "codex")
192
- 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}`;
193
- if (phase === "assess" && deps.provider === "claude")
194
- 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}`;
195
109
  if (phase === "prepare")
196
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}`;
197
111
  if (phase === "assess")
198
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}`;
199
113
  if (effectivePhase === "repair")
200
- 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}`;
201
115
  if (["prepare", "assess", "challenge"].includes(phase) && deps.hostFacts)
202
116
  instruction = `${deps.hostFacts}\n\n${instruction}`;
203
117
  if (["prepare", "assess", "challenge"].includes(phase))
@@ -208,86 +122,46 @@ export async function prepareVerificationContract(initial, inventory, deps) {
208
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}`;
209
123
  const operationId = `qa-preparation:${randomUUID()}`;
210
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);
211
- 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);
212
126
  const current = store.budget(initial.runId, initial.workId, initial.admissionDigest);
213
127
  const dispatchedAt = deps.now();
214
- 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);
215
129
  store.retainResult(budget, operationId, result, result.usage);
216
- 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 });
217
131
  if (deps.now() >= current.deadlineMs)
218
132
  throw new Error("Preparation deadline expired; partial evidence retained");
219
- return { ...result, operationId };
133
+ return { ...result, operationId, output };
220
134
  };
221
- const parsed = async (result, tag) => {
222
- let response = result;
223
- const phaseBoundary = tag === "RAFI_QA_ASSESSMENT"
224
- ? "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."
225
- : tag === "RAFI_QA_CANDIDATE"
226
- ? "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."
227
- : "";
228
- for (let repair = 0;; repair++) {
229
- try {
230
- return parsePreparationArtifact(response.text, tag);
231
- }
232
- catch (error) {
233
- // A provider can answer the preparation assessor with a final-QA
234
- // artifact from the wrong role. That is a repairable protocol error,
235
- // not a refusal to participate in preparation.
236
- const blocker = tag === "RAFI_QA_ASSESSMENT" && response.text.includes("RAFI_QA_FAILURE_REPORT_START")
237
- ? undefined
238
- : providerBlocker(response.text, tag);
239
- if (blocker)
240
- throw new Error(blocker);
241
- // A provider can return a short progress/prose response without any
242
- // JSON at all. Treat that as a bounded format defect unless it is an
243
- // explicit refusal; the repair turn is still required to produce the
244
- // exact envelope and is subject to the same strict parser below.
245
- if (repair >= 2)
246
- throw error;
247
- 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);
248
- }
249
- }
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;
250
144
  };
251
- /**
252
- * Validate provider-owned artifacts at their response boundary. A provider
253
- * sometimes copies the preparation draft into an independent assessment;
254
- * that is not an assessment contract and must be repaired as a bounded
255
- * schema error rather than being allowed to fail much later in assembly.
256
- */
257
145
  const parsedContract = async (result, tag, validate, name, normalize) => {
258
- let response = result;
259
- for (let repair = 0;; repair++) {
260
- try {
261
- const value = normalize ? normalize(await parsed(response, tag), response) : await parsed(response, tag);
262
- if (!validate(value))
263
- throw new Error(`${name} contract validation failed: ${(validate.errors ?? []).map(error => JSON.stringify(error)).join("; ")}`);
264
- return { value, response };
265
- }
266
- catch (error) {
267
- if (repair >= 2)
268
- throw error;
269
- const assessmentRepairContract = name === "QA semantic assessment"
270
- ? `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.`
271
- : "";
272
- 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);
273
- }
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) });
274
153
  }
154
+ if (!validate(value))
155
+ throw new Error(`${name} contract validation failed: ${JSON.stringify(validate.errors)}`);
156
+ return { value, response: result };
275
157
  };
276
158
  try {
277
159
  if (!retained && !retainedDepth && decisionIssues.length) {
278
160
  let plannerIssues = decisionIssues;
279
161
  for (let attempt = 0; attempt < 3; attempt++) {
280
- 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)}`);
281
- let proposed;
282
- try {
283
- proposed = await parsed(response, "RAFI_QA_DEPTH");
284
- }
285
- catch (error) {
286
- plannerIssues = [`QA planner response was not a valid RAFI_QA_DEPTH envelope: ${error instanceof Error ? error.message : String(error)}`];
287
- if (attempt === 2)
288
- throw new Error(`depth-decision-incomplete: ${plannerIssues.join("; ")}`);
289
- continue;
290
- }
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);
291
165
  proposed.plannerOperationId = response.operationId;
292
166
  proposed.plannerIdentity = response.sessionId;
293
167
  // minimumReasons is a host-validated restatement of the planner's
@@ -295,6 +169,9 @@ export async function prepareVerificationContract(initial, inventory, deps) {
295
169
  // the selected level, risk evidence, or rationale.
296
170
  const requiredReasons = proposed.riskFactors.map(risk => risk.category);
297
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) });
298
175
  plannerIssues = validateDepthDecision(proposed);
299
176
  if (!plannerIssues.length) {
300
177
  candidate.depthDecision = proposed;
@@ -310,44 +187,38 @@ export async function prepareVerificationContract(initial, inventory, deps) {
310
187
  store.retainProgress(budget, "depth-decision", candidate.depthDecision);
311
188
  let authorSessionId = retained?.authorSessionId ?? "";
312
189
  for (let pass = 0; pass < (candidate.depthDecision.level <= 2 ? 2 : 3) && (!retained || targetedConcerns.length > 0 || validateCandidate(candidate, inventory, store.equivalentResolver()).length > 0); pass++) {
313
- 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)}`);
314
- const rawProposal = await parsed(response, "RAFI_QA_CANDIDATE");
315
- const legacy = Object.hasOwn(rawProposal, "runId");
316
- const proposal = legacy ? rawProposal : normalizeProviderDraftMetadata(rawProposal);
317
- if (!proposal || typeof proposal !== "object" || Array.isArray(proposal))
318
- throw new Error("QA preparation draft must be an object");
319
- if (!legacy && !validateProviderDraft(proposal)) {
320
- correctionIssues = (validateProviderDraft.errors ?? []).map(error => `${error.instancePath || "/"} ${error.message}`);
321
- if (pass + 1 >= (candidate.depthDecision.level <= 2 ? 2 : 3))
322
- 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);
323
211
  continue;
324
212
  }
325
- if (legacy) {
326
- const depth = proposal.depthDecision;
327
- const inputs = proposal.inputs;
328
- // Legacy models sometimes remove Markdown code ticks from descriptive
329
- // text. Compare those fields narrowly; never normalize references or
330
- // risk categories and never publish the provider's copy.
331
- const prose = (value) => value.replace(/`/g, "");
332
- 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)
333
- && canonicalContractJson(depth.minimumReasons.map(prose)) === canonicalContractJson(candidate.depthDecision.minimumReasons.map(prose))
334
- && canonicalContractJson(depth.riskFactors.map(risk => ({ ...risk, rationale: prose(risk.rationale) }))) === canonicalContractJson(candidate.depthDecision.riskFactors.map(risk => ({ ...risk, rationale: prose(risk.rationale) })))
335
- && 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);
336
- const matchingReference = (actual, expected) => actual === expected || (!actual.startsWith("/") && !actual.split(/[\\/]/).includes("..") && expected.endsWith(`/${actual}`));
337
- const matchingInputs = Array.isArray(inputs) && inputs.length === candidate.inputs.length && inputs.every((input, index) => {
338
- const expected = candidate.inputs[index];
339
- return expected && input && typeof input.reference === "string" && matchingReference(input.reference, expected.reference) && canonicalContractJson({ ...input, reference: expected.reference }) === canonicalContractJson(expected);
340
- });
341
- 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)
342
- throw new Error("Preparation changed host-owned identity/depth/input authority");
343
- }
344
- else if (Object.keys(proposal).some(key => !["checks", "coverage", "baseline", "preparationEvidence", "unresolved"].includes(key)))
345
- throw new Error("QA preparation draft contains unexpected host-owned or unsupported fields");
346
- if (![proposal.checks, proposal.coverage, proposal.baseline, proposal.preparationEvidence, proposal.unresolved].every(Array.isArray))
347
- throw new Error("QA preparation draft is missing required proposal arrays");
348
- 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)
349
220
  throw new Error("QA preparation host evidence normalization is unavailable");
350
- 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 };
351
222
  draft.createdAt = candidate.createdAt;
352
223
  if (candidate.predecessorDigest === undefined)
353
224
  delete draft.predecessorDigest;
@@ -363,7 +234,13 @@ export async function prepareVerificationContract(initial, inventory, deps) {
363
234
  draft.requirements.push(...inventory.filter(item => !present.has(item.id)).map(item => structuredClone(item)));
364
235
  }
365
236
  try {
366
- 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;
367
244
  }
368
245
  catch (error) {
369
246
  const detail = error instanceof Error ? error.message : String(error);
@@ -377,23 +254,6 @@ export async function prepareVerificationContract(initial, inventory, deps) {
377
254
  evidence.operationId = response.operationId;
378
255
  evidence.sessionId = response.sessionId;
379
256
  }
380
- if (response.text.includes("RAFI_QA_ESCALATION_START")) {
381
- const request = parseProviderContract(qaPreparationEscalationProposalV1Schema, parsePreparationArtifact(response.text, "RAFI_QA_ESCALATION"), "QA preparation escalation");
382
- const minimum = Math.max(candidate.depthDecision.level, ...request.riskFactors.map(risk => QA_RISK_MINIMUMS[risk.category]));
383
- 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)}`);
384
- const decision = await parsed(revision, "RAFI_QA_DEPTH");
385
- decision.plannerIdentity = revision.sessionId;
386
- decision.plannerOperationId = revision.operationId;
387
- const errors = validateDepthDecision(decision, candidate.depthDecision);
388
- if (decision.level < minimum || request.riskFactors.some(risk => !decision.riskFactors.some(actual => actual.category === risk.category)))
389
- errors.push("Planner revision does not address evidenced risk minimum");
390
- if (errors.length)
391
- throw new Error(`depth-decision-incomplete: ${errors.join("; ")}`);
392
- candidate.depthDecision = decision;
393
- store.reviseDepth(budget, decision);
394
- store.retainProgress(budget, "depth-decision", decision);
395
- continue;
396
- }
397
257
  const structural = validateCandidate(candidate, inventory, store.equivalentResolver());
398
258
  if (!structural.length)
399
259
  break;
@@ -405,8 +265,8 @@ export async function prepareVerificationContract(initial, inventory, deps) {
405
265
  throw new Error(`Contract incomplete: ${structural.join("; ")}`);
406
266
  const binding = { draftPayloadDigest: draftDigest(candidate), inventoryDigest: inventoryDigest(candidate.requirements), policyVersion: candidate.depthDecision.policyVersion, authorSessionId };
407
267
  const retainedAssessment = store.progress(budget, "assessment");
408
- const normalizeAssessment = (value, response, fallback) => {
409
- const normalized = normalizeAssessmentObligations(normalizeAssessmentMetadata(value), depthObligations(candidate.depthDecision.level));
268
+ const normalizeAssessment = (value, response) => {
269
+ const normalized = structuredClone(value);
410
270
  // These bindings are host-owned. A repair may omit them or emit a
411
271
  // placeholder; restore only the exact current binding and retained
412
272
  // obligation labels, never provider-invented content.
@@ -419,13 +279,9 @@ export async function prepareVerificationContract(initial, inventory, deps) {
419
279
  normalized.sessionId = response.sessionId;
420
280
  if (response.operationId)
421
281
  normalized.operationId = response.operationId;
422
- if ((!normalized.assessedObligations || normalized.assessedObligations.length === 0) && fallback?.assessedObligations?.length)
423
- normalized.assessedObligations = [...fallback.assessedObligations];
424
- if ((!normalized.assessedRequirementIds || normalized.assessedRequirementIds.length === 0) && fallback?.assessedRequirementIds?.length)
425
- normalized.assessedRequirementIds = [...fallback.assessedRequirementIds];
426
282
  return normalized;
427
283
  };
428
- 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 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)}`);
429
285
  let assessed = assessmentResponse
430
286
  ? await parsedContract(assessmentResponse, "RAFI_QA_ASSESSMENT", validateAssessmentContract, "QA semantic assessment", (value, response) => normalizeAssessment(value, response))
431
287
  : { value: retainedAssessment, response: undefined };
@@ -433,14 +289,43 @@ export async function prepareVerificationContract(initial, inventory, deps) {
433
289
  // session identity, but it cannot prove or safely author the builder's ID.
434
290
  assessed.value.authorSessionId = binding.authorSessionId;
435
291
  for (let repair = 0; assessmentResponse && repair < 3; repair++) {
436
- // Apply host-owned current-branch applicability normalization before
437
- // semantic validation. Otherwise an assessor's material sibling-absence
438
- // concern is rejected before the host can downgrade that non-binding
439
- // concern to an auditable, non-material observation.
440
- assessed.value = normalizeExpectedFutureAssessment(assessed.value, candidate);
292
+ // Native concerns retain their declared materiality; prose cannot downgrade it.
441
293
  const semanticIssues = validateAssessment(candidate, assessed.value, authorSessionId);
442
294
  if (!semanticIssues.length)
443
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
+ }
444
329
  if (repair === 2)
445
330
  throw new Error(semanticIssues.join("; "));
446
331
  const preservedAuthorSessionId = assessed.value.authorSessionId;
@@ -454,8 +339,8 @@ export async function prepareVerificationContract(initial, inventory, deps) {
454
339
  authorSessionId: assessed.value.authorSessionId,
455
340
  };
456
341
  const freshAssessment = semanticIssues.includes("Assessment lacks independent authorship");
457
- 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);
458
- 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));
459
344
  // Repairs may omit this immutable binding while rewriting concerns. Preserve
460
345
  // it only when the prior parsed assessment supplied it; a conflicting value
461
346
  // remains visible to validateAssessment and still fails closed.
@@ -464,7 +349,7 @@ export async function prepareVerificationContract(initial, inventory, deps) {
464
349
  repaired.value.authorSessionId = binding.authorSessionId;
465
350
  assessed = repaired;
466
351
  }
467
- const assessment = normalizeExpectedFutureAssessment(normalizeAssessmentMetadata(assessed.value), candidate);
352
+ const assessment = assessed.value;
468
353
  // These binding fields are host-owned. Providers frequently copy or
469
354
  // truncate the inventory digest while describing their assessment; that
470
355
  // must not invalidate an otherwise substantive independent review.
@@ -480,7 +365,7 @@ export async function prepareVerificationContract(initial, inventory, deps) {
480
365
  if (challenge?.draftPayloadDigest !== draftDigest(candidate))
481
366
  challenge = undefined;
482
367
  if (candidate.depthDecision.level === 5 && !challenge) {
483
- 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)}`);
484
369
  const challenged = await parsedContract(response, "RAFI_QA_CHALLENGE", validateChallengeContract, "QA approach challenge", (value, response) => normalizeAssessment(value, response));
485
370
  challenge = normalizeAssessmentMetadata(challenged.value);
486
371
  challenge.sessionId = challenged.response.sessionId;