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
package/dist/markers.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
- * STEP_STATUS protocol strings. Extracted from foreman.ts so tests can import
2
+ * Native operation instructions. Extracted from foreman.ts so tests can import
3
3
  * them without pulling in @clack/prompts (which requires Node >=20 for styleText).
4
4
  */
5
- export declare const MARKER_SPEC = "End EVERY turn with exactly one marker line as the LAST line, nothing after it:\n STEP_STATUS: done | ticket=\"T001\" summary=\"what you just did\" next=\"the next ticket or step\"\n STEP_STATUS: blocked | ticket=\"T001\" reason=\"why you cannot proceed\"\n STEP_STATUS: plan_complete | ticket=\"T001\" summary=\"what you just did\"\n STEP_STATUS: needs_input | ticket=\"T001\" question=\"your question for the user\" choices=\"Option A|Option B|Option C\"\nUse \"done\" after finishing a ticket or step when more remain, \"plan_complete\" after\nthe final ticket or step, \"blocked\" if you cannot proceed without help, and \"needs_input\"\nif you need the user to make a decision before continuing.\nAlways include the assigned ticket=\"<ticket-id>\" in every work response marker, including needs_input, when working from a ticket queue. Never substitute another ticket.\nAfter \"done\" or \"plan_complete\", foreman may run a QA pass on your work \u2014 expect a\nfollow-up instruction asking you to triple-check accuracy, tests, and ticket satisfaction.";
6
- export declare const QA_MARKER_SPEC = "End EVERY turn with exactly one marker line as the LAST line, nothing after it:\n STEP_STATUS: qa_pass | summary=\"confirmed everything checks out\"\n RAFI_QA_FAILURE_REPORT_START\n {\"version\":1,\"summary\":\"...\",\"checks_run\":[...],\"findings\":[...],\"observations\":[...]}\n RAFI_QA_FAILURE_REPORT_END\n STEP_STATUS: qa_fail | issues=\"short plain-text synopsis\"\n STEP_STATUS: blocked | reason=\"why QA itself cannot proceed\"\n STEP_STATUS: needs_input | question=\"...\" choices=\"...\"";
5
+ /** Active work protocol; legacy status parsers remain historical readers. */
6
+ export declare const MARKER_SPEC = "Return one work-report result through the configured native JSON schema. result.kind completed is your claim that the assigned work is finished; do not add a duplicate status field. The payload contains disposition modified or already-satisfied, truthful evidence, coverage when a verification contract is supplied (otherwise null), and graphMaintenance (null unless a meaningful indexed change occurred). Blocked work and questions use their own result alternatives. Never claim completion merely because a turn ended. Include bounded continuity when requested. The host still requires independent QA before completing a ticket.";
7
+ export declare const QA_MARKER_SPEC = "Return one qa-verdict result through the configured native JSON schema. A completed verdict payload is either passed with independent coverage, or failed with the complete structured failure report and coverage. Report blocked work or a question explicitly. A pass cannot contain blocking findings; preserve all failures and uncertainties. Include required continuity in the same object.";
package/dist/markers.js CHANGED
@@ -1,23 +1,7 @@
1
1
  /**
2
- * STEP_STATUS protocol strings. Extracted from foreman.ts so tests can import
2
+ * Native operation instructions. Extracted from foreman.ts so tests can import
3
3
  * them without pulling in @clack/prompts (which requires Node >=20 for styleText).
4
4
  */
5
- export const MARKER_SPEC = `End EVERY turn with exactly one marker line as the LAST line, nothing after it:
6
- STEP_STATUS: done | ticket="T001" summary="what you just did" next="the next ticket or step"
7
- STEP_STATUS: blocked | ticket="T001" reason="why you cannot proceed"
8
- STEP_STATUS: plan_complete | ticket="T001" summary="what you just did"
9
- STEP_STATUS: needs_input | ticket="T001" question="your question for the user" choices="Option A|Option B|Option C"
10
- Use "done" after finishing a ticket or step when more remain, "plan_complete" after
11
- the final ticket or step, "blocked" if you cannot proceed without help, and "needs_input"
12
- if you need the user to make a decision before continuing.
13
- Always include the assigned ticket="<ticket-id>" in every work response marker, including needs_input, when working from a ticket queue. Never substitute another ticket.
14
- After "done" or "plan_complete", foreman may run a QA pass on your work — expect a
15
- follow-up instruction asking you to triple-check accuracy, tests, and ticket satisfaction.`;
16
- export const QA_MARKER_SPEC = `End EVERY turn with exactly one marker line as the LAST line, nothing after it:
17
- STEP_STATUS: qa_pass | summary="confirmed everything checks out"
18
- RAFI_QA_FAILURE_REPORT_START
19
- {"version":1,"summary":"...","checks_run":[...],"findings":[...],"observations":[...]}
20
- RAFI_QA_FAILURE_REPORT_END
21
- STEP_STATUS: qa_fail | issues="short plain-text synopsis"
22
- STEP_STATUS: blocked | reason="why QA itself cannot proceed"
23
- STEP_STATUS: needs_input | question="..." choices="..."`;
5
+ /** Active work protocol; legacy status parsers remain historical readers. */
6
+ export const MARKER_SPEC = `Return one work-report result through the configured native JSON schema. result.kind completed is your claim that the assigned work is finished; do not add a duplicate status field. The payload contains disposition modified or already-satisfied, truthful evidence, coverage when a verification contract is supplied (otherwise null), and graphMaintenance (null unless a meaningful indexed change occurred). Blocked work and questions use their own result alternatives. Never claim completion merely because a turn ended. Include bounded continuity when requested. The host still requires independent QA before completing a ticket.`;
7
+ export const QA_MARKER_SPEC = `Return one qa-verdict result through the configured native JSON schema. A completed verdict payload is either passed with independent coverage, or failed with the complete structured failure report and coverage. Report blocked work or a question explicitly. A pass cannot contain blocking findings; preserve all failures and uncertainties. Include required continuity in the same object.`;
@@ -0,0 +1,14 @@
1
+ import { type HostIpc } from "rafi-spec";
2
+ type ProbeTerminal = Extract<HostIpc, {
3
+ kind: "rafi-probe-result" | "rafi-probe-spawn-error";
4
+ }>;
5
+ /** One registered helper may publish one terminal result. Identical delivery is idempotent. */
6
+ export declare class ProbeIpcTracker {
7
+ private readonly id;
8
+ private readonly pid;
9
+ private registered;
10
+ terminal?: ProbeTerminal;
11
+ constructor(id: string, pid: number | undefined);
12
+ accept(raw: unknown): HostIpc | "conflict" | undefined;
13
+ }
14
+ export {};
@@ -0,0 +1,29 @@
1
+ import { parseHostIpc } from "rafi-spec";
2
+ /** One registered helper may publish one terminal result. Identical delivery is idempotent. */
3
+ export class ProbeIpcTracker {
4
+ id;
5
+ pid;
6
+ registered = false;
7
+ terminal;
8
+ constructor(id, pid) {
9
+ this.id = id;
10
+ this.pid = pid;
11
+ }
12
+ accept(raw) {
13
+ const message = parseHostIpc(raw);
14
+ if (!message || !("id" in message) || message.id !== this.id || message.pid !== this.pid)
15
+ return;
16
+ if (message.kind === "rafi-probe-registered") {
17
+ if (this.registered)
18
+ return;
19
+ this.registered = true;
20
+ return message;
21
+ }
22
+ if (!this.registered || (message.kind !== "rafi-probe-result" && message.kind !== "rafi-probe-spawn-error"))
23
+ return;
24
+ if (this.terminal)
25
+ return JSON.stringify(this.terminal) === JSON.stringify(message) ? undefined : "conflict";
26
+ this.terminal = message;
27
+ return message;
28
+ }
29
+ }
@@ -1,9 +1,30 @@
1
1
  import type { BuilderAdapter, TurnResult } from "./adapters/types.js";
2
2
  import type { QaSessionHandle } from "./qaReview.js";
3
+ import { resolveEffectiveQaConfiguration } from "./qaEffectiveConfig.js";
3
4
  /** A received response missing only continuity metadata is a bounded format defect. */
4
5
  export declare function preparationTurnPayload(turn: TurnResult): {
5
6
  text: string;
6
7
  formatErrors: string[];
7
8
  };
9
+ /**
10
+ * The selected QA bundle is already injected as immutable system context for
11
+ * the isolated provider session. Repeat only its identity here: the exact
12
+ * requirements remain in the authoritative inventory, while repeating full
13
+ * rules and skill bodies in every operation wastes context and makes native
14
+ * structured submission needlessly fragile.
15
+ */
16
+ export declare function preparationConfigurationReference(effective: ReturnType<typeof resolveEffectiveQaConfiguration>): {
17
+ digest: string;
18
+ configRoot: string;
19
+ roleSource: "compiled" | "library" | "fallback";
20
+ checklistDigest: string;
21
+ qaRulesDigest: string;
22
+ builderRulesDigest: string;
23
+ skills: {
24
+ name: string;
25
+ path: string;
26
+ digest: string;
27
+ }[];
28
+ };
8
29
  /** Shared by ordinary, branch, synthetic, remediation and Manager followups. */
9
30
  export declare function ensureBuilderContract(projectDir: string, runId: string, workId: string, workspace: string, builder: BuilderAdapter, qaFactory?: (cwd: string) => Promise<QaSessionHandle>): Promise<string>;
@@ -1,3 +1,5 @@
1
+ import { operationPayload } from "./structuredOperation.js";
2
+ import { loadGraphConfig } from "./graph/config.js";
1
3
  import { resolveQaPreparationConfig } from "./qaPreparationPolicy.js";
2
4
  import { createHash } from "node:crypto";
3
5
  import { readFileSync, realpathSync } from "node:fs";
@@ -10,11 +12,12 @@ import { checkQaPrerequisites } from "./qaPrerequisites.js";
10
12
  import { OperationDeadline } from "./util/deadline.js";
11
13
  import { sendPreparationGraphTurn } from "./graph/preparation.js";
12
14
  import { withGraphDerivedAccess } from "./graph/derived.js";
15
+ import { assessmentOutput, challengeOutput, depthOutput, draftOutput } from "rafi-spec";
13
16
  import { WorkflowDb } from "./workflowDb.js";
14
17
  import { createRoleBuilder, readOnlyPermissionConfig } from "./agentRun.js";
15
18
  import { captureBuildSource, assertBuildAssignmentReconciled } from "./buildAssignment.js";
16
19
  import { createDisposableQaSnapshotAsync } from "./qaSnapshot.js";
17
- import { allocateStableCheckIds, authoritativeInventory, bindUniqueBaselineCheckIds, contractDigest, mapSnapshotWorkingDirectories, normalizeSnapshotPaths, renderVerificationContract, rewriteProviderHostScopeChecks } from "./qaVerificationContract.js";
20
+ import { allocateStableCheckIds, authoritativeInventory, bindUniqueBaselineCheckIds, contractDigest, ensureHostScopeCheck, mapSnapshotWorkingDirectories, normalizeSnapshotPaths, renderVerificationContract } from "./qaVerificationContract.js";
18
21
  import { resolveEffectiveQaConfiguration } from "./qaEffectiveConfig.js";
19
22
  import { prepareVerificationContract } from "./qaPreparation.js";
20
23
  import { qaHostGraphFact } from "./qaHostFacts.js";
@@ -23,6 +26,7 @@ import { BUILDER_WORK_COVERAGE_INSTRUCTIONS } from "./qaContractCoverage.js";
23
26
  import { renderBuildWorkContext } from "./buildWorkContext.js";
24
27
  import { compareContractInputs } from "./qaContractFreshness.js";
25
28
  import { readBuildRuns } from "./buildRuns.js";
29
+ import { normalizeGraphQaChecks } from "./hostGraphApplicability.js";
26
30
  /** A received response missing only continuity metadata is a bounded format defect. */
27
31
  export function preparationTurnPayload(turn) {
28
32
  const formatErrors = turn.continuityErrors ?? [];
@@ -31,6 +35,24 @@ export function preparationTurnPayload(turn) {
31
35
  formatErrors,
32
36
  };
33
37
  }
38
+ /**
39
+ * The selected QA bundle is already injected as immutable system context for
40
+ * the isolated provider session. Repeat only its identity here: the exact
41
+ * requirements remain in the authoritative inventory, while repeating full
42
+ * rules and skill bodies in every operation wastes context and makes native
43
+ * structured submission needlessly fragile.
44
+ */
45
+ export function preparationConfigurationReference(effective) {
46
+ return {
47
+ digest: effective.digest,
48
+ configRoot: effective.configRoot,
49
+ roleSource: effective.roleSource,
50
+ checklistDigest: contractDigest("input-checklist", effective.checklist),
51
+ qaRulesDigest: contractDigest("qa-rules", effective.qaRules),
52
+ builderRulesDigest: contractDigest("builder-rules", effective.builderRules),
53
+ skills: effective.skills.map(skill => ({ name: skill.name, path: skill.path, digest: skill.digest })),
54
+ };
55
+ }
34
56
  /** Shared by ordinary, branch, synthetic, remediation and Manager followups. */
35
57
  export async function ensureBuilderContract(projectDir, runId, workId, workspace, builder, qaFactory) {
36
58
  return withGraphDerivedAccess([], () => ensureBuilderContractWithProvenance(projectDir, runId, workId, workspace, builder, qaFactory));
@@ -59,6 +81,9 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
59
81
  if (!store.eventRecord(configurationEvent))
60
82
  store.event(runId, workId, configurationEvent, "effective-preparation-configuration", { policy, effectiveDigest: effective.digest, configRoot: effective.configRoot, roleSource: effective.roleSource, provider: builder.agent, capabilities: builder.contractCapabilities?.() ?? null, finalReviewEnabled: db.getRun(runId)?.state?.qaEnabled ?? null });
61
83
  const inputs = [
84
+ { id: "communication-contract", kind: "policy", reference: "structured-provider-output-v1", digest: contractDigest("communication-contract", {
85
+ protocolVersion: 1, hostValidation: "typed-invariants-v1", schemas: [draftOutput, depthOutput, assessmentOutput, challengeOutput].map(output => ({ operation: output.operation, version: output.schemaVersion, digest: output.schemaDigest })),
86
+ }), authority: "planner-policy", revision: "1", availability: "available" },
62
87
  { id: "admitted-ticket", kind: "ticket", reference: admission.assignmentId, digest: contractDigest("input-ticket", ticket), authority: "approved-scope", revision: admission.scopeRevision, availability: "available" },
63
88
  { id: "project-checklist", kind: "checklist", reference: "tickets.build.validation_checklist", digest: contractDigest("input-checklist", effective.checklist), authority: "project-rule", revision: "current", availability: "available" },
64
89
  { id: "effective-rules", kind: "rules", reference: effective.configRoot, digest: effective.digest, authority: "project-rule", revision: "current", availability: "available" },
@@ -107,6 +132,8 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
107
132
  const qaHead = db.qaTicketHead(runId, workId);
108
133
  if (["completed", "waived", "finalizing", "turn-intended", "turn-uncertain", "remediation-intended", "remediation-uncertain"].includes(qaHead.state))
109
134
  throw new Error("Active or finalized QA authority requires explicit recovery/reopen before contract amendment");
135
+ if (!priorBudget)
136
+ throw new Error("Retained contract lacks its preparation budget; explicit recovery is required before amendment");
110
137
  const budget = priorBudget;
111
138
  if (retainedIntent?.inputDigest !== inputDigest || retainedIntent.predecessorDigest !== old.contentDigest)
112
139
  store.beginAmendment(budget, inventory, { reason: `Canonical authorized inputs changed: ${[...comparison.changed, ...comparison.unavailable].join(", ") || "inputs reverted during amendment"}`, inputDigest, predecessorDigest: old.contentDigest });
@@ -129,7 +156,14 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
129
156
  provider: builder.agent,
130
157
  store, now: Date.now, hostFacts: `${qaHostGraphFact(projectDir)}\n${deliveryFact}\nHost-owned baseline fact: Rafi captured the complete preimplementation per-file path manifest and will perform the exact unchanged-file and scope comparison. Its digest is ${preimplementationPathManifestDigest}. Do not require the provider to receive or reproduce the raw manifest; cite the retained host-preimplementation-source observation and assess the host comparison result instead. Do not author a provider-executed check for Rafi's host postimplementation path manifest, complete changed-path inventory, or unchanged-file comparison; Rafi performs that comparison outside the provider snapshot. Ordinary approved product paths, exports, and tests are not a material QA concern merely because the provider cannot prove whether they were present before implementation; Rafi owns that comparison. Do not reconstruct or disclose the full baseline. For current-branch additive tickets, a provider-authored or inferred request to test the branch with an already-completed sibling feature removed is not an applicable requirement when the approved scope says not to remove or reconstruct that sibling; verify the ticket's own behavior and tests against the shared baseline instead. Treat sibling-absence testing as binding only when the user explicitly approved that absence requirement. Escalate only an actual host-observed out-of-scope, destructive, protected-file, or requirement-relevant change.`,
131
158
  normalizeDraft: (draft, response) => {
132
- const normalized = rewriteProviderHostScopeChecks(bindUniqueBaselineCheckIds(allocateStableCheckIds(draft)), hostBaseline.id);
159
+ // The dispatch already retains the selected contract digest. Do not
160
+ // recursively embed its validator/schema in every evidence summary.
161
+ const { output, ...providerReceipt } = response;
162
+ const investigationEvidence = store.putArtifact("investigation-evidence", {
163
+ ...providerReceipt,
164
+ ...(output ? { selectedContract: { operation: output.operation, schemaVersion: output.schemaVersion, schemaDigest: output.schemaDigest } } : {}),
165
+ });
166
+ const normalized = allocateStableCheckIds(bindUniqueBaselineCheckIds(normalizeGraphQaChecks(ensureHostScopeCheck(draft, hostBaseline.id), loadGraphConfig(projectDir).enabled)));
133
167
  if (!normalized.baseline.some(observation => observation.id === hostBaseline.id))
134
168
  normalized.baseline.unshift(structuredClone(hostBaseline));
135
169
  for (const check of normalized.checks)
@@ -142,7 +176,7 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
142
176
  observation.sourceDigest = source.digest;
143
177
  observation.observedAt = new Date().toISOString();
144
178
  observation.phase = "preimplementation-investigation";
145
- observation.evidenceDigest = store.putArtifact("baseline-observation", { observation: original, sourceDigest: source.digest, operationId: response.operationId, sessionId: response.sessionId, providerReceipt: store.putArtifact("investigation-evidence", response) });
179
+ observation.evidenceDigest = store.putArtifact("baseline-observation", { observation: original, sourceDigest: source.digest, operationId: response.operationId, sessionId: response.sessionId, providerReceipt: investigationEvidence });
146
180
  }
147
181
  for (const evidence of normalized.preparationEvidence) {
148
182
  evidence.operationId = response.operationId;
@@ -150,8 +184,10 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
150
184
  delete evidence.graphReceiptRefs;
151
185
  if (response.graphReceiptRefs?.length)
152
186
  evidence.graphReceiptRefs = [...response.graphReceiptRefs];
153
- evidence.evidenceDigest = store.putArtifact("investigation-evidence", response);
187
+ evidence.evidenceDigest = investigationEvidence;
154
188
  for (const reference of evidence.references) {
189
+ if (response.workspacePaths && Object.hasOwn(response.workspacePaths, reference.path))
190
+ reference.path = response.workspacePaths[reference.path];
155
191
  let path;
156
192
  try {
157
193
  path = realpathSync(resolve(workspace, reference.path));
@@ -182,7 +218,7 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
182
218
  const compared = compareContractInputs(candidate, inputs.map(input => input.id === "effective-rules" ? { ...input, digest: current.digest } : input.id === "project-checklist" ? { ...input, digest: contractDigest("input-checklist", current.checklist) } : input.id === "preimplementation-source" ? { ...input, digest: currentSource.digest, revision: currentSource.head } : input));
183
219
  return !compared.changed.length && !compared.unavailable.length;
184
220
  },
185
- dispatch: async (phase, instruction, operationId, remainingMs) => new OperationDeadline("QA preparation operation", remainingMs).run(async () => {
221
+ dispatch: async (phase, instruction, operationId, remainingMs, output) => new OperationDeadline("QA preparation operation", remainingMs).run(async () => {
186
222
  const started = performance.now();
187
223
  const remaining = () => {
188
224
  const value = remainingMs - (performance.now() - started);
@@ -194,14 +230,14 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
194
230
  if (phase === "planner") {
195
231
  const role = await createRoleBuilder({ projectDir: workspace, configRoot: projectDir, role: "planner", label: "QA depth assessment", sandboxMode: "read-only", permissionConfig: readOnlyPermissionConfig(), yes: true, allowSwitch: false });
196
232
  try {
197
- const turn = await sendPreparationGraphTurn(role.builder, `This is a read-only QA planning subturn, not Builder work. Do not implement anything, edit files, run project tests, or describe a TDD implementation plan. Your response must begin with RAFI_QA_DEPTH_START and contain only the requested depth JSON, ending with RAFI_QA_DEPTH_END.\n${instruction}\nFrozen admitted work definition: ${JSON.stringify(ticket)}\nOriginal approved plan context: ${JSON.stringify(planInput?.plan ?? null)}\nFrozen effective configuration: ${JSON.stringify(effective)}`, { phase, projectDir, workspace, sourceRef: before.digest, runId, workId, operationId, remainingMs: remaining() });
233
+ const turn = await sendPreparationGraphTurn(role.builder, `This is a read-only QA planning subturn, not Builder work. Do not implement anything, edit files, run project tests, or describe a TDD implementation plan. Return the requested native depth result.\n${instruction}\nFrozen admitted work definition: ${JSON.stringify(ticket)}\nOriginal approved plan context: ${JSON.stringify(planInput?.plan ?? null)}\nFrozen effective configuration reference (authoritative requirements are in the candidate): ${JSON.stringify(preparationConfigurationReference(effective))}`, { phase, projectDir, workspace, sourceRef: before.digest, runId, workId, operationId, remainingMs: remaining(), output });
198
234
  store.event(runId, workId, `${operationId}:raw`, "preparation-provider-return", turn);
199
235
  if (captureBuildSource(workspace).contentDigest !== before.contentDigest)
200
236
  throw new Error("Product source changed during confined planning; reconcile retained dispatch");
201
237
  const payload = preparationTurnPayload(turn);
202
- if ((turn.isError && !payload.formatErrors.length) || turn.failure || !role.builder.sessionId())
238
+ if (turn.isError || turn.failure || !role.builder.sessionId())
203
239
  throw new Error(`planner-unavailable: ${turn.text}`);
204
- return { text: payload.text, graphReceiptRefs: turn.graphReceiptRefs, sessionId: role.builder.sessionId(), usage: { inputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.inputTokens : turn.inputTokens, outputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.outputTokens : turn.outputTokens, costUsd: turn.costAuthoritative ? turn.costUsd : undefined } };
240
+ return { text: payload.text, structured: turn.structured, selectedDispatch: turn.selectedDispatch, output, graphReceiptRefs: turn.graphReceiptRefs, sessionId: role.builder.sessionId(), usage: { inputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.inputTokens : turn.inputTokens, outputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.outputTokens : turn.outputTokens, costUsd: turn.costAuthoritative ? turn.costUsd : undefined } };
205
241
  }
206
242
  finally {
207
243
  await role.builder.close();
@@ -212,21 +248,21 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
212
248
  try {
213
249
  remaining();
214
250
  const prerequisites = await checkQaPrerequisites({ snapshotPath: snapshot.path, sourceDigest: snapshot.frozenState.digest, ticket });
215
- const prerequisiteDigest = db.putEvidence("qa", JSON.stringify(prerequisites));
251
+ const prerequisiteDigest = db.putEvidence("qa", Buffer.from(JSON.stringify(prerequisites)));
216
252
  remaining();
217
253
  if (qaFactory)
218
254
  adapter = (await qaFactory(snapshot.path)).adapter;
219
255
  else
220
256
  adapter = (await createRoleBuilder({ projectDir: snapshot.path, configRoot: projectDir, role: "qa", agent: state?.qa?.settings?.make ?? builder.agent, preloadedSkillContent: effective.skills.map(skill => ({ name: skill.name, content: skill.content })), label: "QA preparation", sandboxMode: "read-only", permissionConfig: readOnlyPermissionConfig(), yes: true, allowSwitch: false })).builder;
221
- const turn = await sendPreparationGraphTurn(adapter, `${instruction}\nOriginal approved plan context: ${JSON.stringify(planInput?.plan ?? null)}\nFrozen admitted work definition: ${JSON.stringify(ticket)}\nHost early prerequisite observations (host availability is not provider access; classify readiness versus final-only dependencies): ${JSON.stringify({ ...prerequisites, evidenceDigest: prerequisiteDigest })}\nFrozen effective configuration: ${JSON.stringify(effective)}\nOperation: ${operationId}; remaining wall time: ${remainingMs}ms; source: ${source.digest}`, { phase, projectDir, workspace: snapshot.path, accessWorkspace: workspace, sourceRef: snapshot.frozenState.digest, runId, workId, operationId, remainingMs: remaining() });
257
+ const turn = await sendPreparationGraphTurn(adapter, `${instruction}\nOriginal approved plan context: ${JSON.stringify(planInput?.plan ?? null)}\nFrozen admitted work definition: ${JSON.stringify(ticket)}\nHost early prerequisite observations (host availability is not provider access; classify readiness versus final-only dependencies): ${JSON.stringify({ ...prerequisites, evidenceDigest: prerequisiteDigest })}\nFrozen effective configuration reference (the exact QA rules and skills are already loaded as immutable system context; authoritative requirements are in the inventory): ${JSON.stringify(preparationConfigurationReference(effective))}\nOperation: ${operationId}; remaining wall time: ${remainingMs}ms; source: ${source.digest}`, { phase, projectDir, workspace: snapshot.path, accessWorkspace: workspace, sourceRef: snapshot.frozenState.digest, runId, workId, operationId, remainingMs: remaining(), output });
222
258
  store.event(runId, workId, `${operationId}:raw`, "preparation-provider-return", turn);
223
259
  const payload = preparationTurnPayload(turn);
224
260
  payload.text = normalizeSnapshotPaths(payload.text);
225
- if ((turn.isError && !payload.formatErrors.length) || turn.failure || !adapter.sessionId())
261
+ if (turn.isError || turn.failure || !adapter.sessionId())
226
262
  throw new Error(`Preparation provider unavailable: ${turn.text}`);
227
263
  if (captureBuildSource(workspace).contentDigest !== before.contentDigest)
228
264
  throw new Error("Source changed during preparation; retained dispatch requires reconciliation");
229
- return { text: payload.text, graphReceiptRefs: turn.graphReceiptRefs, sessionId: adapter.sessionId(), workspacePaths: mapSnapshotWorkingDirectories(payload.text, snapshot.path), usage: { inputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.inputTokens : turn.inputTokens, outputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.outputTokens : turn.outputTokens, costUsd: turn.costAuthoritative ? turn.costUsd : undefined } };
265
+ return { text: payload.text, structured: turn.structured, selectedDispatch: turn.selectedDispatch, output, graphReceiptRefs: turn.graphReceiptRefs, sessionId: adapter.sessionId(), workspacePaths: mapSnapshotWorkingDirectories(operationPayload(turn, output), snapshot.path), usage: { inputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.inputTokens : turn.inputTokens, outputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.outputTokens : turn.outputTokens, costUsd: turn.costAuthoritative ? turn.costUsd : undefined } };
230
266
  }
231
267
  finally {
232
268
  try {
@@ -2,11 +2,13 @@ import { dispatchWithGraphAccess, withGraphSessionAccess } from "./graph/session
2
2
  import { WorkflowDb } from "./workflowDb.js";
3
3
  import { actualContractSession, assertContractReceipt } from "./qaContractDelivery.js";
4
4
  import { captureBuildSource } from "./buildAssignment.js";
5
- import { COVERAGE_INSTRUCTIONS, bindCoverageHostFields, parseContractCoverage, validateContractCoverage } from "./qaContractCoverage.js";
5
+ import { COVERAGE_INSTRUCTIONS, bindCoverageHostFields, validateContractCoverage } from "./qaContractCoverage.js";
6
6
  import { contractDigest } from "./qaVerificationContract.js";
7
7
  import { OperationDeadline } from "./util/deadline.js";
8
8
  import { hostTestSequenceEvidence } from "./qaBuilderEvidence.js";
9
9
  import { qaHostGraphFact } from "./qaHostFacts.js";
10
+ import { builderCoverageOutput, workReportOutput, remediationOutput } from "rafi-spec";
11
+ import { operationPayload, operationInstruction, nativePolicy } from "./structuredOperation.js";
10
12
  /** Claims are collected after source capture so the provider can name the actual completed source. */
11
13
  export async function collectBuilderContractCoverage(projectDir, runId, workId, workspace, adapter, operationId, instruction, implementation, hostObservations = [], initialSourceDigest) {
12
14
  return withGraphSessionAccess(projectDir, adapter, () => collectBuilderContractCoverageOwned(projectDir, runId, workId, workspace, adapter, operationId, instruction, implementation, hostObservations, initialSourceDigest));
@@ -34,13 +36,19 @@ async function collectBuilderContractCoverageOwned(projectDir, runId, workId, wo
34
36
  return;
35
37
  }
36
38
  // Preserve the implementation response before a failed acknowledgment or a crash.
37
- const implementationDigest = db.putEvidence("qa", implementation.rawResponse ?? implementation.text);
38
- db.planOperation({ runId, idempotencyKey: eventId, kind: "builder-coverage", intent: { workId, contractDigest: contract.contentDigest, binding, implementationDigest, session } });
39
- db.updateOperation(eventId, "in_progress");
39
+ const implementationDigest = db.putEvidence("qa", Buffer.from(JSON.stringify({ structured: implementation.structured, rawResponse: implementation.rawResponse ?? implementation.text })));
40
+ // Persist host-owned command metadata before dispatching the response-only
41
+ // acknowledgement. This survives a process restart and does not retain
42
+ // commands or output, only bounded sequence/status/digest facts.
43
+ const hostObservationDigest = db.putEvidence("qa", Buffer.from(JSON.stringify({ version: 1, kind: "host-builder-tool-observations", workId, operationId, observations: hostObservations })));
44
+ db.planOperation({ runId, idempotencyKey: eventId, kind: "builder-coverage", intent: { workId, contractDigest: contract.contentDigest, binding, implementationDigest, hostObservationDigest, session } });
45
+ db.updateOperation(eventId, "in_progress", { result: { hostObservationDigest } });
40
46
  try {
41
47
  const prompt = `Response-only evidence collection for completed implementation ${operationId}. Do not run tools, modify files, repeat work, or invent verification. Record failed, blocked or not-run truthfully. The implementation response is retained as ${implementationDigest}.\n${COVERAGE_INSTRUCTIONS}\nExact coverage binding: ${JSON.stringify({ version: 1, runId, workId, revision: contract.revision, contractDigest: contract.contentDigest, ...binding })}\nAll checks: ${JSON.stringify(contract.checks)}`;
42
- const response = await new OperationDeadline("Builder coverage acknowledgment", 120_000).run(() => dispatchWithGraphAccess(projectDir, adapter, prompt, { purpose: "response-repair", responseOnly: true }, (text, policy) => adapter.sendTurn(text, policy)), () => { void adapter.close(); });
43
- const responseDigest = db.putEvidence("qa", response.rawResponse ?? response.text);
48
+ const composed = implementation.structured?.state === "candidate" ? implementation.structured.operation === "builder-remediation"
49
+ ? operationPayload(implementation, remediationOutput).coverage : operationPayload(implementation, workReportOutput).coverage : undefined;
50
+ const response = composed ? implementation : await new OperationDeadline("Builder coverage acknowledgment", 120_000).run(() => dispatchWithGraphAccess(projectDir, adapter, `${prompt}\n\n${operationInstruction(builderCoverageOutput)}`, nativePolicy(builderCoverageOutput, { purpose: "response-repair", responseOnly: true, logicalActionId: operationId, journalRunId: runId }), (text, policy) => adapter.sendTurn(text, policy)), () => { void adapter.close(); });
51
+ const responseDigest = db.putEvidence("qa", Buffer.from(JSON.stringify({ structured: response.structured, rawResponse: response.rawResponse ?? response.text })));
44
52
  db.updateOperation(eventId, "uncertain", { result: { responseDigest } });
45
53
  if (response.isError || response.failure)
46
54
  throw new Error("Builder coverage provider failed; retained response requires reconciliation");
@@ -55,13 +63,14 @@ async function collectBuilderContractCoverageOwned(projectDir, runId, workId, wo
55
63
  // Bind the retained coverage to the host-observed post-compaction session
56
64
  // while continuing to reject unscoped session changes and source edits.
57
65
  binding = { ...binding, sessionId: afterSession.sessionId };
58
- const coverage = bindCoverageHostFields(parseContractCoverage(response.text), contract, binding);
66
+ const claims = composed ?? operationPayload(response, builderCoverageOutput);
67
+ const coverage = bindCoverageHostFields(claims, contract, binding);
59
68
  bindHostOwnedBuilderFacts(db, coverage, contract, hostObservations, initialSourceDigest, source.digest, workId, projectDir);
60
69
  const errors = validateContractCoverage(contract, coverage, binding, false);
61
70
  if (errors.length)
62
71
  throw new Error(`Builder coverage invalid: ${errors.join("; ")}`);
63
72
  const coverageDigest = store.putArtifact("builder-coverage", coverage);
64
- db.updateOperation(eventId, "confirmed", { result: { responseDigest, coverageDigest, binding, implementationDigest } });
73
+ db.updateOperation(eventId, "confirmed", { result: { responseDigest, coverageDigest, binding, implementationDigest, hostObservationDigest } });
65
74
  }
66
75
  catch (error) {
67
76
  throw new Error(`Builder evidence collection blocked: ${String(error)}`);
@@ -74,7 +83,7 @@ async function collectBuilderContractCoverageOwned(projectDir, runId, workId, wo
74
83
  function checkText(check) {
75
84
  return JSON.stringify(check);
76
85
  }
77
- function bindHostOwnedBuilderFacts(db, coverage, contract, observations, _initialSourceDigest, finalSourceDigest, workId, projectDir) {
86
+ function bindHostOwnedBuilderFacts(db, coverage, contract, observations, initialSourceDigest, finalSourceDigest, workId, projectDir) {
78
87
  const graphFact = qaHostGraphFact(projectDir);
79
88
  const graphDisabled = /"enabled":false/.test(graphFact);
80
89
  for (const row of coverage.checks ?? []) {
@@ -89,8 +98,8 @@ function bindHostOwnedBuilderFacts(db, coverage, contract, observations, _initia
89
98
  row.evidence = [...(row.evidence ?? []), { procedure: "Rafi retained preimplementation baseline observation", cwd: ".", runtime: "rafi host", sourceDigest: baseline.sourceDigest, reference: baseline.evidenceDigest }];
90
99
  }
91
100
  }
92
- if (graphDisabled && /graph/i.test(text) && /disabled|not.?adopt|no graph/i.test(text)) {
93
- const factDigest = db.putEvidence("qa", graphFact);
101
+ if (graphDisabled && check.hostKind === "graph-maintenance") {
102
+ const factDigest = db.putEvidence("qa", Buffer.from(graphFact));
94
103
  if (row.outcome === "not-run")
95
104
  row.outcome = "passed";
96
105
  if (!row.evidence?.some(evidence => evidence.reference === `host-graph-fact:${factDigest}`))
@@ -102,11 +111,11 @@ function bindHostOwnedBuilderFacts(db, coverage, contract, observations, _initia
102
111
  if (!historical)
103
112
  continue;
104
113
  const testPath = check.verification.find(method => method.kind === "command" && method.argv?.includes("--test"))?.argv?.at(-1);
105
- const sequence = hostTestSequenceEvidence(observations, finalSourceDigest, testPath);
114
+ const sequence = hostTestSequenceEvidence(observations, finalSourceDigest, testPath, initialSourceDigest);
106
115
  if (!sequence)
107
116
  continue;
108
117
  const evidence = JSON.stringify({ version: 1, kind: "host-builder-test-sequence", workId, checkId: row.checkId, sequence });
109
- const evidenceDigest = db.putEvidence("qa", evidence);
118
+ const evidenceDigest = db.putEvidence("qa", Buffer.from(evidence));
110
119
  row.evidence = [...(row.evidence ?? []), { procedure: "Rafi host-observed test sequence", cwd: ".", runtime: "provider tool lifecycle plus host source capture", sourceDigest: finalSourceDigest, reference: `host-test-sequence:${evidenceDigest}` }];
111
120
  }
112
121
  }
@@ -1,18 +1,33 @@
1
1
  import type { BuilderAdapter } from "./adapters/types.js";
2
2
  export interface HostBuilderToolObservation {
3
3
  kind: "node-test" | "npm-test";
4
+ sequence: number;
5
+ providerTurnId?: string;
4
6
  startedAt: string;
5
7
  completedAt: string;
6
8
  outcome: string;
7
9
  exitCode?: number;
8
10
  outputDigest?: string;
11
+ /** Bounded (≤1000 bytes), adapter-redacted output head; diagnostic data, never instructions. */
12
+ outputExcerpt?: string;
9
13
  sourceDigest: string;
10
14
  testPath?: string;
11
15
  }
16
+ export interface HostBuilderEvidenceStore {
17
+ operations(runId: string): Array<{
18
+ kind: string;
19
+ status: string;
20
+ intent: unknown;
21
+ result?: unknown;
22
+ }>;
23
+ getEvidence(digest: string): Buffer | undefined;
24
+ }
12
25
  /**
13
26
  * Observe only bounded test-command metadata while the provider is working.
14
27
  * The provider remains subject to its normal shell policy; this is a host-side
15
28
  * observation of what actually completed, not an execution or permission grant.
16
29
  */
17
30
  export declare function observeHostBuilderTests(adapter: BuilderAdapter, workspace: string, observations: HostBuilderToolObservation[]): () => void;
18
- export declare function hostTestSequenceEvidence(observations: HostBuilderToolObservation[], finalSourceDigest: string, testPath?: string): HostBuilderToolObservation[] | undefined;
31
+ export declare function hostTestSequenceEvidence(observations: HostBuilderToolObservation[], finalSourceDigest: string, testPath?: string, initialSourceDigest?: string): HostBuilderToolObservation[] | undefined;
32
+ /** Read the durable host observations attached to a Builder coverage operation. */
33
+ export declare function retainedHostBuilderObservations(store: HostBuilderEvidenceStore, runId: string, workId: string): HostBuilderToolObservation[];
@@ -1,14 +1,15 @@
1
1
  import { captureBuildSource } from "./buildAssignment.js";
2
+ import { boundedUtf8 } from "./boundedTurnEvidence.js";
2
3
  function commandDetails(input) {
3
4
  const command = typeof input?.command === "string"
4
5
  ? input.command
5
6
  : undefined;
6
7
  if (!command)
7
8
  return undefined;
8
- const nodeTest = /\bnode\s+--test(?:\s+([^\s;&|>]+))?/.exec(command);
9
+ const nodeTest = /\bnode(?:js)?\s+--test(?:\s+([^\s;&|>]+))?/.exec(command);
9
10
  if (nodeTest)
10
11
  return { kind: "node-test", ...(nodeTest[1] ? { testPath: nodeTest[1] } : {}) };
11
- if (/\bnpm\s+test\b/.test(command))
12
+ if (/\b(?:npm|pnpm|yarn)\s+(?:run\s+)?test\b/.test(command))
12
13
  return { kind: "npm-test" };
13
14
  return undefined;
14
15
  }
@@ -19,12 +20,13 @@ function commandDetails(input) {
19
20
  */
20
21
  export function observeHostBuilderTests(adapter, workspace, observations) {
21
22
  const open = new Map();
22
- const stop = adapter.observeEvents?.((event) => {
23
+ let sequence = observations.length;
24
+ const listener = (event) => {
23
25
  if (event.kind !== "tool")
24
26
  return;
25
27
  const details = commandDetails(event.input);
26
28
  if (event.lifecycle === "started" && details && event.callId) {
27
- open.set(event.callId, { ...details, startedAt: event.observedAt ?? new Date().toISOString() });
29
+ open.set(event.callId, { ...details, startedAt: event.observedAt ?? new Date().toISOString(), providerTurnId: event.providerTurnId });
28
30
  return;
29
31
  }
30
32
  if (event.lifecycle !== "completed" || !event.callId || !event.completionKnown)
@@ -37,6 +39,8 @@ export function observeHostBuilderTests(adapter, workspace, observations) {
37
39
  return;
38
40
  observations.push({
39
41
  kind: prior.kind,
42
+ sequence: sequence++,
43
+ ...(prior.providerTurnId ? { providerTurnId: prior.providerTurnId } : {}),
40
44
  startedAt: prior.startedAt,
41
45
  completedAt: event.observedAt ?? new Date().toISOString(),
42
46
  // Some adapters label every finished tool call "completed" even when
@@ -45,16 +49,52 @@ export function observeHostBuilderTests(adapter, workspace, observations) {
45
49
  outcome: event.exitCode !== undefined ? event.exitCode === 0 ? "completed" : "failed" : event.status ?? "completed",
46
50
  ...(event.exitCode === undefined ? {} : { exitCode: event.exitCode }),
47
51
  ...(event.outputDigest && /^[a-f0-9]{64}$/.test(event.outputDigest) ? { outputDigest: event.outputDigest } : {}),
52
+ ...(typeof event.outputSummary === "string" && event.outputSummary ? { outputExcerpt: boundedUtf8(event.outputSummary, 1000).toString() } : {}),
48
53
  sourceDigest: captureBuildSource(workspace).digest,
49
54
  ...(prior.testPath ? { testPath: prior.testPath } : {}),
50
55
  });
51
- }) ?? (() => { });
56
+ };
57
+ // This supplementary telemetry must not preempt the delivery journal's own
58
+ // correlated-terminal check. Wrapper adapters deliberately throw when a
59
+ // provider lacks observation; dispatchOwned records that as
60
+ // delivery-uncertain without sending a turn. Treating this optional
61
+ // subscription as fatal here would bypass that durable fail-closed path.
62
+ let stop = () => { };
63
+ try {
64
+ stop = adapter.observeEvents?.(listener) ?? stop;
65
+ }
66
+ catch {
67
+ open.clear();
68
+ }
52
69
  return () => { stop(); open.clear(); };
53
70
  }
54
- export function hostTestSequenceEvidence(observations, finalSourceDigest, testPath) {
55
- const relevant = observations.filter(observation => (observation.kind === "node-test" || observation.kind === "npm-test") && (!testPath || observation.testPath === testPath));
56
- const red = relevant.find(observation => observation.outcome !== "completed" && observation.outcome !== "denied");
71
+ export function hostTestSequenceEvidence(observations, finalSourceDigest, testPath, initialSourceDigest) {
72
+ const relevant = observations
73
+ .filter(observation => (observation.kind === "node-test" || observation.kind === "npm-test") && (!testPath || observation.testPath === testPath))
74
+ .sort((a, b) => a.sequence - b.sequence || a.startedAt.localeCompare(b.startedAt));
75
+ const red = relevant.find(observation => observation.outcome !== "completed" && observation.outcome !== "denied" && (!initialSourceDigest || observation.sourceDigest === initialSourceDigest));
57
76
  const redIndex = red ? relevant.indexOf(red) : -1;
58
77
  const green = red ? relevant.slice(redIndex + 1).find(observation => observation.sourceDigest === finalSourceDigest && observation.outcome === "completed" && observation.sourceDigest !== red.sourceDigest) : undefined;
59
78
  return red && green ? [red, green] : undefined;
60
79
  }
80
+ /** Read the durable host observations attached to a Builder coverage operation. */
81
+ export function retainedHostBuilderObservations(store, runId, workId) {
82
+ for (const operation of store.operations(runId).filter(item => item.kind === "builder-coverage").reverse()) {
83
+ const intent = operation.intent;
84
+ if (intent.workId !== workId)
85
+ continue;
86
+ const result = operation.result;
87
+ const digest = typeof result?.hostObservationDigest === "string" ? result.hostObservationDigest
88
+ : typeof intent.hostObservationDigest === "string" ? intent.hostObservationDigest : undefined;
89
+ if (!digest)
90
+ continue;
91
+ try {
92
+ const parsed = JSON.parse(store.getEvidence(digest)?.toString("utf8") ?? "null");
93
+ if (!Array.isArray(parsed?.observations))
94
+ continue;
95
+ return parsed.observations.filter((item) => Boolean(item && typeof item === "object" && typeof item.kind === "string" && Number.isSafeInteger(item.sequence)));
96
+ }
97
+ catch { /* malformed optional evidence is not a QA pass */ }
98
+ }
99
+ return [];
100
+ }
@@ -1,7 +1,7 @@
1
1
  import type { QaContractCoverageV1, QaVerificationContractV1 } from "rafi-spec";
2
2
  export declare const COVERAGE_INSTRUCTIONS: string;
3
- export declare const BUILDER_WORK_COVERAGE_INSTRUCTIONS = "Carry out the applicable contract checks and report your work and evidence honestly in your normal final response. When a check requires historical ordering, retain bounded output for each required step and an explicit ordering record while performing it; a later passing test cannot prove an earlier failing step. Do not emit RAFI_QA_COVERAGE JSON during this implementation turn or stop for coverage binding values. After the implementation turn, Rafi will capture the completed source and request a separate response-only coverage record with the exact host-owned bindings.";
4
- export declare function parseContractCoverage(text: string): QaContractCoverageV1;
3
+ export declare const BUILDER_WORK_COVERAGE_INSTRUCTIONS = "Carry out the applicable contract checks and report your work and evidence honestly in your normal final response. When a check requires historical ordering, retain bounded output for each required step and an explicit ordering record while performing it; a later passing test cannot prove an earlier failing step. Include the coverage claims in the completed work-report payload when a contract is supplied. Follow the contract applicability.kind exactly: an unconditional check is always applicable, including a check that verifies documentation is disabled or a skill is not requested. Such a check can pass with concrete evidence of that absence; do not mark the check itself not-applicable. Conditional checks need observed predicate evidence. Report a genuine conflict as blocked instead of silently waiving an unconditional check. Do not wait for or invent host binding values: Rafi captures and attaches the completed source after your result.";
4
+ export declare function parseContractCoverage(_text: string): never;
5
5
  /** Source identity is host-owned; providers may copy a stale Builder digest. */
6
6
  export declare function bindCoverageSourceDigest(coverage: QaContractCoverageV1, sourceDigest: string): QaContractCoverageV1;
7
7
  /** All coverage identity fields are host-owned; provider output supplies only claims and evidence text. */
@@ -1,11 +1,10 @@
1
1
  import { Ajv } from "ajv";
2
- import { qaContractCoverageV1Schema } from "rafi-spec";
2
+ import { qaContractCoverageV1Schema, coverageClaimsSchema } from "rafi-spec";
3
3
  const coverageSchema = new Ajv({ allErrors: true }).compile(qaContractCoverageV1Schema);
4
- import { parsePreparationArtifact } from "./qaVerificationContract.js";
5
- export const COVERAGE_INSTRUCTIONS = `Return RAFI_QA_COVERAGE_START, one QaContractCoverageV1 JSON object, RAFI_QA_COVERAGE_END before the final status marker. The exact schema is ${JSON.stringify(qaContractCoverageV1Schema)}. Put every binding field at the TOP LEVEL, not inside a binding object, and copy the host-supplied binding values exactly. For each check use checkId (not id), applicability as applicable/not-applicable/unresolved, predicateEvidence as an array of strings, outcome as passed/failed/blocked/not-run, and evidence as an array of objects with procedure, cwd, runtime, sourceDigest and reference. For postimplementation evidence sourceDigest is the exact host-supplied sourceDigest, not an individual file hash. Unconditional checks cannot be marked not-applicable or unresolved; for an absence or non-impact check, mark it applicable and provide bounded inspection evidence showing that the absent change or impact was assessed. For independent QA coverage of an applicable passed preimplementation-only check, inspect its matching passed contract.baseline observation and set at least one evidence reference to that observation's exact evidenceDigest; the historical observation is bound to its original source, not the current source. Assess its relevance rather than inventing or claiming a new baseline. Host-owned checks whose applicability decisionOwner is host are the exception to provider execution: include the check truthfully as applicable/not-run with evidence that the host-owned comparison is pending, and do not block or fail QA over it; Rafi will execute the comparison and replace that row before accepting the pass. Before emitting qa_pass, run every other applicable mandatory check that is not backed by a passed retained baseline; do not leave such a provider-owned check as not-run merely because an earlier turn omitted it. If an exact required command is blocked by the provider shell policy, use a semantically equivalent permitted read-only procedure, record the substitution and limitation truthfully, and assess the same predicate; do not mark the check blocked merely because the literal command syntax was denied. If a required command is permitted and read-only, run it now and retain its bounded result. Do not add unsupported fields. Include every check with truthful applicability and concrete evidence. Missing, blocked, not-run, unresolved applicability, or failed provider-owned mandatory coverage cannot authorize normal completion. Builder coverage is a claim; final QA independently verifies it.`;
4
+ export const COVERAGE_INSTRUCTIONS = `Include coverage claims in the native operation payload. The claims schema is ${JSON.stringify(coverageClaimsSchema)}. Do not invent or echo host-owned run, source, attempt, revision or session bindings; Rafi attaches them after source capture. For each check use checkId (not id), applicability as applicable/not-applicable/unresolved, predicateEvidence as an array of strings, outcome as passed/failed/blocked/not-run, and evidence as an array of objects with procedure, cwd, runtime and reference. For postimplementation evidence, cite what you actually observed; Rafi binds the source digest. Unconditional checks cannot be marked not-applicable or unresolved; for an absence or non-impact check, mark it applicable and provide bounded inspection evidence showing that the absent change or impact was assessed. For independent QA coverage of an applicable passed preimplementation-only check, inspect its matching passed contract.baseline observation and set at least one evidence reference to that observation's exact evidenceDigest; the historical observation is bound to its original source, not the current source. Assess its relevance rather than inventing or claiming a new baseline. Graphify is optional evidence: do not install, refresh, scan, or claim Graphify execution unless the host says Rafi-managed Graphify is adopted and enabled. Host-owned Graphify applicability is authoritative; disabled, unadopted, or unavailable Graphify is not a QA blocker, while adopted-enabled Graphify is applicable and cannot be waived by provider output. Other host-owned checks whose applicability decisionOwner is host are the exception to provider execution: include the check truthfully as applicable/not-run with evidence that the host-owned comparison is pending, and do not block or fail QA over it; Rafi will execute the comparison and replace that row before accepting the pass. Before reporting a passed verdict, run every other applicable mandatory check that is not backed by a passed retained baseline; do not leave such a provider-owned check as not-run merely because an earlier turn omitted it. If an exact required command is blocked by the provider shell policy, use a semantically equivalent permitted read-only procedure, record the substitution and limitation truthfully, and assess the same predicate; do not mark the check blocked merely because the literal command syntax was denied. If a required command is permitted and read-only, run it now and retain its bounded result. For command exit status, use the host-retained tool event's exitCode and bounded output when available; that is authoritative evidence, and you must not create an echo wrapper, compound command, or other shell scaffold solely to print a numeric status. Do not add unsupported fields. Include every check with truthful applicability and concrete evidence. Missing, blocked, not-run, unresolved applicability, or failed provider-owned mandatory coverage cannot authorize normal completion. Builder coverage is a claim; final QA independently verifies it.`;
6
5
  // The source and attempt binding do not exist until the implementation turn has finished.
7
- export const BUILDER_WORK_COVERAGE_INSTRUCTIONS = `Carry out the applicable contract checks and report your work and evidence honestly in your normal final response. When a check requires historical ordering, retain bounded output for each required step and an explicit ordering record while performing it; a later passing test cannot prove an earlier failing step. Do not emit RAFI_QA_COVERAGE JSON during this implementation turn or stop for coverage binding values. After the implementation turn, Rafi will capture the completed source and request a separate response-only coverage record with the exact host-owned bindings.`;
8
- export function parseContractCoverage(text) { return parsePreparationArtifact(text, "RAFI_QA_COVERAGE"); }
6
+ export const BUILDER_WORK_COVERAGE_INSTRUCTIONS = `Carry out the applicable contract checks and report your work and evidence honestly in your normal final response. When a check requires historical ordering, retain bounded output for each required step and an explicit ordering record while performing it; a later passing test cannot prove an earlier failing step. Include the coverage claims in the completed work-report payload when a contract is supplied. Follow the contract applicability.kind exactly: an unconditional check is always applicable, including a check that verifies documentation is disabled or a skill is not requested. Such a check can pass with concrete evidence of that absence; do not mark the check itself not-applicable. Conditional checks need observed predicate evidence. Report a genuine conflict as blocked instead of silently waiving an unconditional check. Do not wait for or invent host binding values: Rafi captures and attaches the completed source after your result.`;
7
+ export function parseContractCoverage(_text) { throw new Error("Legacy coverage markers cannot provide workflow authority; use the verified native payload"); }
9
8
  /** Source identity is host-owned; providers may copy a stale Builder digest. */
10
9
  export function bindCoverageSourceDigest(coverage, sourceDigest) {
11
10
  const copy = structuredClone(coverage);
@@ -20,10 +19,11 @@ export function bindCoverageHostFields(coverage, contract, binding) {
20
19
  // Optional schema fields may be emitted as JSON null by providers. Null is
21
20
  // not a meaningful limitation, so omit only that representation variant;
22
21
  // non-string values elsewhere remain validation errors.
23
- for (const check of copy.checks ?? [])
22
+ for (const check of copy.checks ?? []) {
24
23
  for (const evidence of check.evidence ?? [])
25
24
  if (evidence.limitation === null)
26
25
  delete evidence.limitation;
26
+ }
27
27
  copy.version = 1;
28
28
  copy.runId = contract.runId;
29
29
  copy.workId = contract.workId;
@@ -61,7 +61,11 @@ export function validateContractCoverage(contract, coverage, binding, requirePas
61
61
  issues.push(`Unconditional applicability cannot be waived ${row.checkId}`);
62
62
  if (row.applicability === "not-applicable" && !row.predicateEvidence?.length)
63
63
  issues.push(`Not-applicable requires predicate evidence ${row.checkId}`);
64
- if (check.applicability.kind === "conditional" && row.applicability === "applicable" && !row.predicateEvidence?.length)
64
+ // Host-owned conditional predicates are resolved by the host augmentation
65
+ // after provider coverage is collected (for example Graphify adoption).
66
+ // Requiring provider predicate evidence here would make the host-owned,
67
+ // fail-open path block before the authoritative host observation exists.
68
+ if (check.applicability.kind === "conditional" && check.applicability.decisionOwner !== "host" && row.applicability === "applicable" && !row.predicateEvidence?.length)
65
69
  issues.push(`Conditional applicability requires predicate evidence ${row.checkId}`);
66
70
  if (requirePass && check.obligation === "mandatory" && (row.applicability === "unresolved" || row.applicability === "applicable" && row.outcome !== "passed"))
67
71
  issues.push(`Mandatory check incomplete ${row.checkId}: run its permitted verification now and resubmit passed evidence, or report a truthful blocked result for host recovery`);