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
@@ -0,0 +1,181 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { invalidStructuredRun, STRUCTURED_CUTOVER_REASON } from "./structuredCutover.js";
5
+ /** Only these declared local projections can carry resumable communication state. */
6
+ export const COMMUNICATION_FILE_OWNERSHIP = Object.freeze([
7
+ { directory: ".rafi/interviews", kind: "interview", recurse: false },
8
+ { directory: ".foreman/runs", kind: "build-checkpoint", recurse: false },
9
+ { directory: ".foreman/delivery-sessions", kind: "branch-session", recurse: false },
10
+ { directory: ".tickets/delivery-sessions", kind: "branch-session", recurse: false },
11
+ { directory: ".foreman/qa-report-recovery", kind: "qa-recovery", recurse: true },
12
+ ]);
13
+ /** Unknown/oversized file state conservatively retains evidence until classified. */
14
+ export function communicationFileEvidenceReferences(root) {
15
+ const references = new Set();
16
+ const walk = (path) => {
17
+ if (!existsSync(path))
18
+ return;
19
+ const stat = lstatSync(path);
20
+ if (stat.isSymbolicLink()) {
21
+ references.add("*");
22
+ return;
23
+ }
24
+ if (stat.isDirectory()) {
25
+ for (const name of readdirSync(path))
26
+ walk(join(path, name));
27
+ return;
28
+ }
29
+ if (!stat.isFile() || stat.size > 8 * 1024 * 1024) {
30
+ references.add("*");
31
+ return;
32
+ }
33
+ for (const match of readFileSync(path, "utf8").matchAll(/\b[a-f0-9]{64}\b/g))
34
+ references.add(match[0]);
35
+ };
36
+ for (const owner of COMMUNICATION_FILE_OWNERSHIP)
37
+ walk(join(root, owner.directory));
38
+ walk(join(root, ".rafi", "communication-quarantine"));
39
+ return references;
40
+ }
41
+ export function quarantineCommunicationFiles(root, db) {
42
+ const archive = join(root, ".rafi", "communication-quarantine");
43
+ const sync = (path) => { const fd = openSync(path, "r"); try {
44
+ fsyncSync(fd);
45
+ }
46
+ finally {
47
+ closeSync(fd);
48
+ } };
49
+ for (const owner of COMMUNICATION_FILE_OWNERSHIP) {
50
+ if (owner.kind === "qa-recovery") {
51
+ const directory = join(root, owner.directory);
52
+ if (existsSync(directory) && lstatSync(directory).isSymbolicLink())
53
+ throw new Error("QA recovery storage symlink is not allowed during cutover");
54
+ if (existsSync(directory))
55
+ for (const name of readdirSync(directory)) {
56
+ const packet = join(directory, name);
57
+ if (lstatSync(packet).isSymbolicLink())
58
+ throw new Error("Cutover cannot classify symlinked QA recovery state");
59
+ if (lstatSync(packet).isDirectory()) {
60
+ const manifestPath = join(packet, "manifest.json");
61
+ if (existsSync(manifestPath)) {
62
+ if (lstatSync(manifestPath).isSymbolicLink())
63
+ throw new Error("QA recovery manifest symlink is not allowed during cutover");
64
+ let manifest = {};
65
+ try {
66
+ manifest = JSON.parse(readFileSync(manifestPath, "utf8")) ?? {};
67
+ }
68
+ catch { /* Incompatible packet remains preserved behind the sentinel. */ }
69
+ if (manifest.version === 2 && typeof manifest.runId === "string" && typeof manifest.packetDigest === "string"
70
+ && db.prepare(`SELECT 1 FROM workflow_runs r JOIN qa_packet_projections p ON p.run_id=r.run_id
71
+ WHERE r.run_id=? AND r.checkpoint<>? AND p.packet_digest=? AND p.ticket_id=?
72
+ AND CASE WHEN json_valid(p.manifest_json) THEN json_extract(p.manifest_json,'$.revision') ELSE NULL END=? AND p.path=?`)
73
+ .get(manifest.runId, STRUCTURED_CUTOVER_REASON, manifest.packetDigest, manifest.ticketId, manifest.revision, packet))
74
+ continue;
75
+ }
76
+ const marker = join(packet, ".structured-communication-v2-cutover");
77
+ if (existsSync(marker) && lstatSync(marker).isSymbolicLink())
78
+ throw new Error("Unsafe QA cutover marker");
79
+ if (!existsSync(marker))
80
+ writeFileSync(marker, STRUCTURED_CUTOVER_REASON, { mode: 0o600, flag: "wx" });
81
+ sync(marker);
82
+ sync(packet);
83
+ }
84
+ }
85
+ continue;
86
+ }
87
+ const walk = (directory) => {
88
+ if (!existsSync(directory))
89
+ return;
90
+ if (lstatSync(directory).isSymbolicLink())
91
+ throw new Error(`Cutover cannot classify symlinked communication storage: ${directory}`);
92
+ for (const name of readdirSync(directory)) {
93
+ const path = join(directory, name), stat = lstatSync(path);
94
+ if (stat.isSymbolicLink())
95
+ throw new Error(`Cutover cannot classify a symlinked communication projection: ${path}`);
96
+ if (stat.isDirectory()) {
97
+ if (owner.recurse)
98
+ walk(path);
99
+ continue;
100
+ }
101
+ if (!name.endsWith(".json") || !stat.isFile())
102
+ continue;
103
+ const bytes = readFileSync(path);
104
+ let value;
105
+ try {
106
+ value = JSON.parse(bytes.toString());
107
+ }
108
+ catch {
109
+ value = {};
110
+ }
111
+ if (!value || typeof value !== "object" || Array.isArray(value))
112
+ value = {};
113
+ if (value.structuredCommunicationCutover?.reason === STRUCTURED_CUTOVER_REASON)
114
+ continue;
115
+ if (owner.kind === "interview" && value.communicationProtocolVersion === 2 && value.version === 2
116
+ && typeof value.id === "string" && typeof value.workflow === "string" && typeof value.checkpoint === "string"
117
+ && value.answers && typeof value.answers === "object" && value.runtime && typeof value.runtime === "object"
118
+ && !value.runtime.sessionId && value.sessionIds && Object.keys(value.sessionIds).length === 0
119
+ && Array.isArray(value.outputs) && value.outputs.every(output => output && typeof output.path === "string" && (output.sha256 === null || /^[a-f0-9]{64}$/.test(output.sha256))))
120
+ continue; // Human answers, file observations and host-only preparation.
121
+ if (value.communicationProtocolVersion === 2 && typeof value.runId === "string" && Object.keys(value).every(key => ["communicationProtocolVersion", "runId", "dispatchId"].includes(key))) {
122
+ const run = db.prepare("SELECT checkpoint FROM workflow_runs WHERE run_id=?").get(value.runId);
123
+ if (run && run.checkpoint !== STRUCTURED_CUTOVER_REASON && typeof value.dispatchId === "string" && db.prepare("SELECT 1 FROM operation_journal WHERE run_id=? AND idempotency_key=? AND status='confirmed'").get(value.runId, value.dispatchId))
124
+ continue;
125
+ }
126
+ if (owner.kind === "interview" && value.communicationProtocolVersion === 2 && value.version === 2 && value.workflow === "tickets-plan") {
127
+ const reference = value.decisions?.nativeDispatch;
128
+ try {
129
+ const run = reference && db.prepare("SELECT * FROM workflow_runs WHERE run_id=?").get(reference.runId);
130
+ const operation = reference && db.prepare("SELECT intent_json FROM operation_journal WHERE run_id=? AND idempotency_key=? AND status='confirmed'").get(reference.runId, reference.dispatchId);
131
+ const tuple = operation && JSON.parse(operation.intent_json).tuple;
132
+ if (reference?.version === 2 && Object.keys(reference).length === 4 && reference.runId === `ticket-plan:${value.id}` &&
133
+ tuple?.operation === "ticket-plan" && tuple.digest === reference.tupleDigest && tuple.sessionId === value.runtime?.sessionId &&
134
+ !value.decisions.structuredResponse && !value.decisions.proposalResponse && run && !invalidStructuredRun(db, run, root))
135
+ continue;
136
+ }
137
+ catch { /* Unverifiable native references remain incompatible. */ }
138
+ }
139
+ if (owner.kind === "build-checkpoint") {
140
+ const run = db.prepare("SELECT checkpoint FROM workflow_runs WHERE run_id=?").get(value.runId);
141
+ if (run && run.checkpoint !== STRUCTURED_CUTOVER_REASON && !value.builder && !value.qa && !value.receipts && !value.sessionBindings)
142
+ continue;
143
+ }
144
+ mkdirSync(archive, { recursive: true, mode: 0o700 });
145
+ if (lstatSync(archive).isSymbolicLink())
146
+ throw new Error("Unsafe communication quarantine storage");
147
+ const digest = createHash("sha256").update(bytes).digest("hex");
148
+ const archived = join(archive, `${digest}.json`);
149
+ if (!existsSync(archived))
150
+ writeFileSync(archived, bytes, { mode: 0o600, flag: "wx" });
151
+ if (lstatSync(archived).isSymbolicLink() || createHash("sha256").update(readFileSync(archived)).digest("hex") !== digest)
152
+ throw new Error("Communication quarantine integrity failure");
153
+ sync(archived);
154
+ sync(archive);
155
+ const next = { ...value, status: owner.kind === "interview" ? "incompatible" : "superseded",
156
+ checkpoint: STRUCTURED_CUTOVER_REASON, runtime: {}, sessionIds: {}, continuityLost: true,
157
+ structuredCommunicationCutover: { version: 2, reason: STRUCTURED_CUTOVER_REASON, archiveDigest: digest, kind: owner.kind } };
158
+ // The archive and stage survive SQLite rollback. A restart can classify
159
+ // either original or incompatible bytes without losing the input or
160
+ // treating a partial filesystem rewrite as completed DB repair.
161
+ const replacement = `${JSON.stringify(next, null, 2)}\n`;
162
+ const stageDigest = createHash("sha256").update(path).update(digest).digest("hex");
163
+ const stage = join(archive, `${stageDigest}.stage.json`);
164
+ const stagedBytes = JSON.stringify({ version: 1, path, originalDigest: digest,
165
+ replacementDigest: createHash("sha256").update(replacement).digest("hex"), replacement });
166
+ if (!existsSync(stage))
167
+ writeFileSync(stage, stagedBytes, { mode: 0o600, flag: "wx" });
168
+ if (lstatSync(stage).isSymbolicLink() || readFileSync(stage, "utf8") !== stagedBytes)
169
+ throw new Error("Communication quarantine stage integrity failure");
170
+ sync(stage);
171
+ sync(archive);
172
+ const temporary = `${path}.${randomUUID()}.cutover.tmp`;
173
+ writeFileSync(temporary, replacement, { mode: 0o600, flag: "wx" });
174
+ sync(temporary);
175
+ renameSync(temporary, path);
176
+ sync(directory);
177
+ }
178
+ };
179
+ walk(join(root, owner.directory));
180
+ }
181
+ }
@@ -0,0 +1,22 @@
1
+ import { type OutputContract, type ProviderSessionRefV1, type SelectedDispatchTupleV2 } from "rafi-spec";
2
+ import type { TurnResult } from "./adapters/types.js";
3
+ declare const authorityBrand: unique symbol;
4
+ /** An in-process capability; JSON or a caller-created owner label cannot mint it. */
5
+ export interface StructuredDispatchCapability {
6
+ readonly [authorityBrand]: true;
7
+ }
8
+ /** Only the host's explicit, durable fresh-recovery decision grants this scope. */
9
+ export declare function withFreshRecoveryValidation<T>(root: string, runId: string, role: "builder" | "qa", send: (authorization: import("./workflowDb.js").FreshRecoveryValidation) => Promise<T>): Promise<T>;
10
+ export declare function currentFreshRecoveryValidation(root: string, runId: string): import("./workflowDb.js").FreshRecoveryValidation | undefined;
11
+ export declare function withSealedDispatch<T>(root: string, tuple: SelectedDispatchTupleV2, send: (capability: StructuredDispatchCapability) => Promise<T>, session?: ProviderSessionRefV1): Promise<T>;
12
+ /** Provider initialization is discarded and cannot use an authoritative operation contract. */
13
+ export declare function withEphemeralInitializationDispatch<T>(root: string, tuple: SelectedDispatchTupleV2, send: (capability: StructuredDispatchCapability) => Promise<T>, session: ProviderSessionRefV1): Promise<T>;
14
+ export declare function assertSealedDispatch(root: string, capability: StructuredDispatchCapability, tuple: SelectedDispatchTupleV2 | undefined, instruction?: string): void;
15
+ export declare function assertActiveProviderDispatch(policy: import("./adapters/types.js").TurnRequest, instruction: string, provider?: "claude" | "codex", session?: ProviderSessionRefV1): asserts policy is import("./adapters/types.js").SealedTurnPolicy;
16
+ /** Domain callbacks create their durable turn before entering the sealing gateway. */
17
+ export declare function durableOwnerDispatch<T extends (...args: any[]) => Promise<TurnResult>>(root: string, output: OutputContract, owner: {
18
+ runId: string;
19
+ actionId: string;
20
+ }, dispatch: T): T;
21
+ export declare function isDurableOwnerDispatch(dispatch: Function): boolean;
22
+ export {};
@@ -0,0 +1,110 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { structuredDigest, initializationOutput, historicProviderWireContract } from "rafi-spec";
3
+ const capabilities = new WeakMap();
4
+ const active = new AsyncLocalStorage();
5
+ const freshRecovery = new AsyncLocalStorage();
6
+ /** Only the host's explicit, durable fresh-recovery decision grants this scope. */
7
+ export async function withFreshRecoveryValidation(root, runId, role, send) {
8
+ const { WorkflowDb } = await import("./workflowDb.js");
9
+ const db = new WorkflowDb(root);
10
+ let authorization;
11
+ try {
12
+ authorization = db.freshRecoveryValidation(runId, role);
13
+ }
14
+ finally {
15
+ db.close();
16
+ }
17
+ return freshRecovery.run({ root, runId, authorization }, () => send(authorization));
18
+ }
19
+ export function currentFreshRecoveryValidation(root, runId) {
20
+ const scope = freshRecovery.getStore();
21
+ return scope?.root === root && scope.runId === runId ? scope.authorization : undefined;
22
+ }
23
+ function withCapability(root, tuple, send, session, consume) {
24
+ const capability = Object.freeze({});
25
+ capabilities.set(capability, { root, tuple, submitted: false, consume, session: session && Object.freeze({ ...session }) });
26
+ return active.run(capability, () => send(capability));
27
+ }
28
+ export async function withSealedDispatch(root, tuple, send, session) {
29
+ const { WorkflowDb } = await import("./workflowDb.js");
30
+ const db = new WorkflowDb(root);
31
+ try {
32
+ const claimId = db.claimStructuredSubmission(tuple, session);
33
+ return await withCapability(root, tuple, send, session, () => db.consumeStructuredSubmission(tuple, claimId, session));
34
+ }
35
+ finally {
36
+ db.close();
37
+ }
38
+ }
39
+ /** Provider initialization is discarded and cannot use an authoritative operation contract. */
40
+ export function withEphemeralInitializationDispatch(root, tuple, send, session) {
41
+ historicProviderWireContract(initializationOutput, tuple);
42
+ if (tuple.operation !== initializationOutput.operation || tuple.sessionId !== session.sessionId || tuple.generation !== session.generation)
43
+ throw new Error("Ephemeral initialization cannot authorize workflow content");
44
+ return withCapability(root, tuple, send, session);
45
+ }
46
+ export function assertSealedDispatch(root, capability, tuple, instruction) {
47
+ const sealed = capabilities.get(capability);
48
+ if (active.getStore() !== capability || !sealed || sealed.root !== root || sealed.tuple !== tuple ||
49
+ instruction !== undefined && structuredDigest(instruction) !== sealed.tuple.instructionDigest)
50
+ throw new Error("Structured dispatch requires the active committed sealing capability");
51
+ }
52
+ export function assertActiveProviderDispatch(policy, instruction, provider, session) {
53
+ if (!policy.output)
54
+ throw new Error("Sealed provider policy requires an output contract");
55
+ const capability = policy.dispatchCapability;
56
+ const sealed = capability && capabilities.get(capability);
57
+ if (!capability || !sealed)
58
+ throw new Error("Structured provider send requires a committed sealing capability");
59
+ assertSealedDispatch(sealed.root, capability, policy.selectedDispatch, instruction);
60
+ if (policy.output.operation !== sealed.tuple.operation || policy.output.schemaDigest !== sealed.tuple.schemaDigest || policy.output.schemaVersion !== sealed.tuple.schemaVersion)
61
+ throw new Error("Structured output contract differs from its sealed owner tuple");
62
+ if (provider && sealed.tuple.provider !== provider)
63
+ throw new Error("Sealed dispatch belongs to another provider");
64
+ if (sealed.session && (!session || ["provider", "sessionId", "role", "stream", "generation", "cwd", "configRoot", "workspaceIdentity", "ticketId", "deliveryUnitId"]
65
+ .some(field => session[field] !== sealed.session[field])))
66
+ throw new Error("Provider session scope changed after dispatch sealing");
67
+ if (!policy.wireContract || policy.renderedWireContractDigest !== policy.wireContract.descriptorDigest ||
68
+ policy.wireContract.descriptorDigest !== sealed.tuple.descriptorDigest || policy.dispatchId !== sealed.tuple.dispatchId)
69
+ throw new Error("Structured provider policy differs from its sealed descriptor");
70
+ if (sealed.submitted)
71
+ throw new Error("Sealed structured dispatch was already submitted; reconcile before a new dispatch");
72
+ sealed.consume?.();
73
+ sealed.submitted = true;
74
+ }
75
+ const ownerDispatches = new WeakSet();
76
+ /** Domain callbacks create their durable turn before entering the sealing gateway. */
77
+ export function durableOwnerDispatch(root, output, owner, dispatch) {
78
+ const owned = async (...args) => {
79
+ const { WorkflowDb } = await import("./workflowDb.js");
80
+ const preparation = new WorkflowDb(root);
81
+ try {
82
+ const record = output.operation === "qa-verdict" ? preparation.qaReviewAttempt(owner.actionId)
83
+ : output.operation === "builder-remediation" ? preparation.qaRemediationAttempt(owner.actionId) : undefined;
84
+ if (!record || record.runId !== owner.runId || record.status !== "started")
85
+ throw new Error("Domain dispatch requires its active persisted owner attempt");
86
+ }
87
+ finally {
88
+ preparation.close();
89
+ }
90
+ const result = await dispatch(...args);
91
+ if (!result.isError && !result.failure) {
92
+ if (!result.selectedDispatch)
93
+ throw new Error("Durable owner returned an unsealed provider turn");
94
+ const db = new WorkflowDb(root);
95
+ try {
96
+ if (result.selectedDispatch.actionId !== owner.actionId || db.operation(result.selectedDispatch.dispatchId)?.runId !== owner.runId ||
97
+ db.structuredOwnerDispatch(result.selectedDispatch.dispatchId).tupleDigest !== result.selectedDispatch.digest)
98
+ throw new Error("Domain callback returned a receipt for another owner");
99
+ db.replayStructuredDispatch(result.selectedDispatch.dispatchId, output);
100
+ }
101
+ finally {
102
+ db.close();
103
+ }
104
+ }
105
+ return result;
106
+ };
107
+ ownerDispatches.add(owned);
108
+ return owned;
109
+ }
110
+ export function isDurableOwnerDispatch(dispatch) { return ownerDispatches.has(dispatch); }
@@ -0,0 +1,29 @@
1
+ import { type OutputContract, type SelectedDispatchTupleV2, type StructuredResponse } from "rafi-spec";
2
+ export interface SealedDispatchRecordV2 {
3
+ version: 2;
4
+ tupleJson: string;
5
+ tupleDigest: string;
6
+ descriptorDigest: string;
7
+ renderedInstructionDigest: string;
8
+ }
9
+ export interface RetainedStructuredReceiptV2 {
10
+ version: 2;
11
+ tupleDigest: string;
12
+ hostTurnId?: string;
13
+ receipt: Omit<Extract<StructuredResponse, {
14
+ state: "candidate";
15
+ }>, "value"> | StructuredResponse | null;
16
+ valueDigest?: string;
17
+ rejectedValue?: {
18
+ representation: "json-text" | "decoded-json";
19
+ digest: string;
20
+ evidenceDigest: string;
21
+ originalBytes: number;
22
+ retainedBytes: number;
23
+ };
24
+ }
25
+ export declare function hydrateStructuredReceipt(retained: RetainedStructuredReceiptV2, readEvidence: (digest: string) => Buffer | undefined): StructuredResponse | undefined;
26
+ export declare function sealedDispatchRecord(tuple: SelectedDispatchTupleV2): SealedDispatchRecordV2;
27
+ export declare function persistedDispatchTuple(record: SealedDispatchRecordV2): SelectedDispatchTupleV2;
28
+ /** The same verifier is used before confirmation and by persisted domain readers. */
29
+ export declare function verifyPersistedDispatch<T>(contract: OutputContract<T>, record: SealedDispatchRecordV2, receipt: StructuredResponse | undefined, receivedTuple?: SelectedDispatchTupleV2): T;
@@ -0,0 +1,36 @@
1
+ import { canonicalJson, historicProviderWireContract, parseSelectedDispatchTupleV2, structuredDigest, validateStructuredCandidate } from "rafi-spec";
2
+ export function hydrateStructuredReceipt(retained, readEvidence) {
3
+ if (!retained.receipt)
4
+ return undefined;
5
+ if (retained.receipt.state !== "candidate")
6
+ return retained.receipt;
7
+ if (!retained.valueDigest)
8
+ throw new Error("Retained native receipt has no bounded value reference");
9
+ const bytes = readEvidence(retained.valueDigest);
10
+ if (!bytes)
11
+ throw new Error("Retained native receipt value is missing");
12
+ const value = retained.receipt.representation === "json-text" ? bytes.toString() : JSON.parse(bytes.toString());
13
+ return { ...retained.receipt, value };
14
+ }
15
+ export function sealedDispatchRecord(tuple) {
16
+ return Object.freeze({ version: 2, tupleJson: canonicalJson(tuple), tupleDigest: tuple.digest,
17
+ descriptorDigest: tuple.descriptorDigest, renderedInstructionDigest: tuple.instructionDigest });
18
+ }
19
+ export function persistedDispatchTuple(record) {
20
+ if (!record || record.version !== 2 || typeof record.tupleJson !== "string")
21
+ throw new Error("Missing persisted sealed owner tuple");
22
+ const tuple = parseSelectedDispatchTupleV2(JSON.parse(record.tupleJson));
23
+ const { digest, ...fields } = tuple;
24
+ if (canonicalJson(tuple) !== record.tupleJson || structuredDigest(fields) !== digest || record.tupleDigest !== digest ||
25
+ record.descriptorDigest !== tuple.descriptorDigest || record.renderedInstructionDigest !== tuple.instructionDigest)
26
+ throw new Error("Persisted owner tuple canonical bytes or identity mismatch");
27
+ return tuple;
28
+ }
29
+ /** The same verifier is used before confirmation and by persisted domain readers. */
30
+ export function verifyPersistedDispatch(contract, record, receipt, receivedTuple) {
31
+ const tuple = persistedDispatchTuple(record);
32
+ if (receivedTuple && canonicalJson(receivedTuple) !== record.tupleJson)
33
+ throw new Error("Receipt tuple differs from persisted owner intent");
34
+ const descriptor = historicProviderWireContract(contract, tuple);
35
+ return validateStructuredCandidate(contract, receipt, descriptor, tuple);
36
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProviderQueryBudget } from "./adapters/types.js";
2
+ export declare function inheritedStructuredLiveBudget(provider: "claude" | "codex", env?: NodeJS.ProcessEnv): ProviderQueryBudget | undefined;
@@ -0,0 +1,23 @@
1
+ /** Inherited supervision for public CLI journeys, including detached workers. */
2
+ import { reserveStructuredLiveQuery, structuredLiveBudgetPath } from "rafi-spec";
3
+ export function inheritedStructuredLiveBudget(provider, env = process.env) {
4
+ if (!env.RAFI_STRUCTURED_LIVE_RUNNER && !env.RAFI_STRUCTURED_LIVE_BUDGET_PATH)
5
+ return undefined;
6
+ const deadline = Date.parse(env.RAFI_LIVE_DEADLINE ?? "");
7
+ const root = env.RAFI_LIVE_RETAINED_LOG_DIRECTORY, scope = env.RAFI_LIVE_CREDENTIAL_SCOPE;
8
+ if (env.RAFI_LIVE_STRUCTURED !== "1" || !root || !scope || !env.RAFI_STRUCTURED_LIVE_RUNNER ||
9
+ !Number.isFinite(deadline) || deadline <= Date.now() || deadline > Date.now() + 30 * 60_000 ||
10
+ env.RAFI_STRUCTURED_LIVE_BUDGET_PATH !== structuredLiveBudgetPath(root, scope, deadline) ||
11
+ !(env.RAFI_LIVE_PROVIDERS ?? "claude,codex").split(",").includes(provider))
12
+ throw new Error("Invalid inherited structured live supervision");
13
+ const requireAuthoritativeCost = env.RAFI_LIVE_REQUIRE_COST_METER === "1";
14
+ if (requireAuthoritativeCost && provider === "codex")
15
+ throw new Error("Codex live structured validation is blocked: no authoritative spend meter");
16
+ const signal = AbortSignal.timeout(Math.max(1, deadline - Date.now()));
17
+ return { reserveQuery: selected => {
18
+ if (selected !== provider)
19
+ throw new Error("Inherited live provider mismatch");
20
+ return reserveStructuredLiveQuery({ path: env.RAFI_STRUCTURED_LIVE_BUDGET_PATH, runner: `${env.RAFI_STRUCTURED_LIVE_RUNNER}/cli-${process.pid}`,
21
+ provider, signal, deadline, requireAuthoritativeCost });
22
+ } };
23
+ }
@@ -0,0 +1,18 @@
1
+ import { type OperationResponse, type OutputContract } from "rafi-spec";
2
+ import type { BuilderAdapter, TurnRequest, TurnResult } from "./adapters/types.js";
3
+ import type { StepStatus } from "./foreman.js";
4
+ export declare function operationInstruction(contract: OutputContract): string;
5
+ /**
6
+ * Actionable host message for a failed structured turn. The response text of an
7
+ * otherwise valid turn is evidence, not the error; prefer retained host
8
+ * failures, then the native rejection, then provider diagnostics.
9
+ */
10
+ export declare function structuredTurnErrorMessage(turn: TurnResult, label?: string): string;
11
+ export declare function operationResponse<T = unknown>(turn: TurnResult, contract: OutputContract): OperationResponse<T>;
12
+ export declare function operationStatus(turn: TurnResult, contract: OutputContract, ticket?: string): StepStatus;
13
+ export declare function operationPayload<T>(turn: TurnResult, contract: OutputContract): T;
14
+ export declare function nativePolicy(output: OutputContract, policy?: TurnRequest): TurnRequest;
15
+ /** Reuses the workflow journal. A receipt is retained before domain interpretation. */
16
+ export declare function journalStructuredDispatch(root: string, adapter: BuilderAdapter, instruction: string, policy: TurnRequest, dispatch: (text: string, policy: TurnRequest) => Promise<TurnResult>): Promise<TurnResult>;
17
+ /** Only unsealed host request fields survive a handoff or a restart. */
18
+ export declare function continuationPolicy(policy: TurnRequest, parentDispatchId: string): TurnRequest;