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,213 @@
1
+ import { boundedJson, structuredDigest, validateStructuredCandidate } from "rafi-spec";
2
+ export function structuredReceipt(contract, provider, value, identity, representation, selected, tuple) {
3
+ if (value === undefined)
4
+ return { state: "rejected", reason: "missing-result", detail: "Successful provider terminal omitted its structured result", dispatchId: identity.dispatchId };
5
+ let observedValue;
6
+ try {
7
+ // Retain bounded invalid JSON text too, before parsing can reject it.
8
+ if (representation === "json-text" && typeof value === "string" && Buffer.byteLength(value) <= selected.maximumResponseBytes)
9
+ observedValue = { representation, value, digest: structuredDigest(value) };
10
+ else if (representation === "decoded-json")
11
+ observedValue = { representation, value: boundedJson(value, representation, selected.maximumResponseBytes), digest: structuredDigest(value) };
12
+ if (!identity.sessionId || !identity.nativeTurnId)
13
+ throw new Error("Missing native receipt identity");
14
+ boundedJson(value, representation, selected.maximumResponseBytes);
15
+ if (tuple.provider !== provider || tuple.operation !== contract.operation || tuple.schemaDigest !== contract.schemaDigest || tuple.descriptorDigest !== selected.descriptorDigest ||
16
+ tuple.dispatchId !== identity.dispatchId || tuple.sessionId !== identity.sessionId)
17
+ throw new Error("Receipt identity does not match selected dispatch tuple v2");
18
+ const candidate = { state: "candidate", protocolVersion: 2, operation: contract.operation,
19
+ schemaVersion: contract.schemaVersion, schemaDigest: contract.schemaDigest, wireSchemaDigest: selected.wireSchemaDigest, profile: selected.profile,
20
+ wireContractDigest: selected.descriptorDigest, wireVariant: selected.variant,
21
+ selectedDispatchDigest: tuple.digest,
22
+ ...identity, sessionId: identity.sessionId, nativeTurnId: identity.nativeTurnId, terminal: "completed", representation, value };
23
+ const normalized = validateStructuredCandidate(contract, candidate, selected, tuple);
24
+ candidate.normalization = { version: 1, rawDecodedDigest: structuredDigest(boundedJson(value, representation, selected.maximumResponseBytes)), canonicalDigest: structuredDigest(normalized) };
25
+ return candidate;
26
+ }
27
+ catch (error) {
28
+ return { state: "rejected", reason: "invalid-contract", detail: String(error), dispatchId: identity.dispatchId, ...(observedValue ? { observedValue } : {}) };
29
+ }
30
+ }
31
+ const STREAM_HEAD_BYTES = 65_536;
32
+ const STREAM_TAIL_BYTES = 8_192;
33
+ const STREAM_TEXT_ITEMS = 8;
34
+ /** Pretty-printed JSON never needs this much consecutive whitespace. */
35
+ export const CODEX_RUNAWAY_WHITESPACE_BYTES = 2_048;
36
+ /** The provider is emitting a runaway whitespace sequence; its output can never be accepted. */
37
+ export class CodexRunawayOutputError extends Error {
38
+ itemId;
39
+ whitespaceBytes;
40
+ constructor(itemId, whitespaceBytes) {
41
+ super(`Codex streamed ${whitespaceBytes} consecutive whitespace bytes in one message; runaway output was stopped before completion`);
42
+ this.itemId = itemId;
43
+ this.whitespaceBytes = whitespaceBytes;
44
+ this.name = "CodexRunawayOutputError";
45
+ }
46
+ }
47
+ function headUtf8(text, maximum) {
48
+ const bytes = Buffer.from(text);
49
+ if (bytes.length <= maximum)
50
+ return text;
51
+ let end = maximum;
52
+ while (end > 0 && (bytes[end] & 0xc0) === 0x80)
53
+ end--;
54
+ return bytes.subarray(0, end).toString();
55
+ }
56
+ function tailUtf8(text, maximum) {
57
+ const bytes = Buffer.from(text);
58
+ if (bytes.length <= maximum)
59
+ return text;
60
+ let start = bytes.length - maximum;
61
+ while (start < bytes.length && (bytes[start] & 0xc0) === 0x80)
62
+ start++;
63
+ return bytes.subarray(start).toString();
64
+ }
65
+ /** Holds early notifications until turn/start supplies the exact turn identity. */
66
+ export class CodexTurnCollector {
67
+ threadId;
68
+ requirePhase;
69
+ onProgress;
70
+ maximumResponseBytes;
71
+ turnId;
72
+ early = [];
73
+ bytes = 0;
74
+ finals = new Map();
75
+ streams = new Map();
76
+ streamedBytes = 0;
77
+ streamedDeltas = 0;
78
+ earlyDeltaBytes = 0;
79
+ terminal;
80
+ error;
81
+ tools = false;
82
+ resolve;
83
+ reject;
84
+ completion = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; });
85
+ constructor(threadId, requirePhase, onProgress, maximumResponseBytes = 2_000_000) {
86
+ this.threadId = threadId;
87
+ this.requirePhase = requirePhase;
88
+ this.onProgress = onProgress;
89
+ this.maximumResponseBytes = maximumResponseBytes;
90
+ void this.completion.catch(() => { });
91
+ }
92
+ bind(turnId) {
93
+ if (!turnId || this.turnId)
94
+ throw new Error("Missing or duplicate native turn binding");
95
+ this.turnId = turnId;
96
+ this.onProgress?.();
97
+ const early = this.early;
98
+ this.early = [];
99
+ this.bytes = 0; // Early transport was already bounded; count result bytes once while draining.
100
+ for (const event of early)
101
+ this.ingest(event.method, event.params);
102
+ this.settle();
103
+ }
104
+ ingest(method, params) {
105
+ if (params.threadId !== this.threadId)
106
+ return;
107
+ if (!this.terminal && this.turnId && params.turnId === this.turnId && method.startsWith("item/") && method.endsWith("/delta"))
108
+ this.onProgress?.();
109
+ if (!["item/started", "item/completed", "turn/completed", "item/agentMessage/delta"].includes(method))
110
+ return;
111
+ if (!this.turnId) {
112
+ // Early streamed text has its own budget so a final item is never charged twice.
113
+ // Event count bounds overhead; content bytes bound early streamed text.
114
+ if (method === "item/agentMessage/delta")
115
+ this.earlyDeltaBytes += typeof params.delta === "string" ? Buffer.byteLength(params.delta) : 0;
116
+ else
117
+ this.bytes += Buffer.byteLength(JSON.stringify(params));
118
+ if (this.early.length >= 10_000 || this.bytes > this.maximumResponseBytes || this.earlyDeltaBytes > this.maximumResponseBytes)
119
+ return this.fail(new Error("Early Codex notification limit exceeded"));
120
+ this.early.push({ method, params });
121
+ return;
122
+ }
123
+ const terminal = params.turn;
124
+ if ((method === "turn/completed" ? terminal?.id : params.turnId) !== this.turnId)
125
+ return;
126
+ if (method === "item/agentMessage/delta")
127
+ return this.ingestDelta(params);
128
+ if (!this.terminal)
129
+ this.onProgress?.();
130
+ const item = params.item;
131
+ if (item && ["commandExecution", "fileChange", "mcpToolCall", "dynamicToolCall", "webSearch", "imageGeneration", "collabAgentToolCall"].includes(String(item.type)))
132
+ this.tools = true;
133
+ if (method === "item/completed" && item?.type === "agentMessage" && typeof item.text === "string" && (item.phase === "final_answer" || !this.requirePhase && item.phase === undefined)) {
134
+ if (typeof item.id !== "string" || !item.id)
135
+ return this.invalidate(new Error("Final result has no native item identity"));
136
+ const previous = this.finals.get(item.id);
137
+ if (previous !== undefined && previous !== item.text)
138
+ return this.invalidate(new Error("Conflicting redelivery of native result item"));
139
+ if (previous === undefined) {
140
+ this.bytes += Buffer.byteLength(item.text);
141
+ if (this.bytes > this.maximumResponseBytes)
142
+ return this.fail(new Error("Codex result limit exceeded"));
143
+ this.finals.set(item.id, item.text);
144
+ }
145
+ }
146
+ if (method === "item/completed" && item?.type === "agentMessage" && typeof item.id === "string") {
147
+ const stream = this.streams.get(item.id);
148
+ if (stream)
149
+ stream.completed = true;
150
+ }
151
+ if (method === "turn/completed") {
152
+ if (this.terminal && JSON.stringify(this.terminal) !== JSON.stringify(terminal))
153
+ return this.invalidate(new Error("Conflicting native terminal events"));
154
+ this.terminal = terminal;
155
+ this.settle();
156
+ }
157
+ }
158
+ /**
159
+ * Streamed text is bounded before completion, but never becomes a result:
160
+ * only a completed final item is accepted and charged against the budget.
161
+ */
162
+ ingestDelta(params) {
163
+ if (this.terminal || this.error)
164
+ return;
165
+ const itemId = params.itemId, delta = params.delta;
166
+ if (typeof itemId !== "string" || !itemId || typeof delta !== "string" || this.finals.has(itemId))
167
+ return;
168
+ const now = new Date().toISOString();
169
+ let stream = this.streams.get(itemId);
170
+ if (!stream) {
171
+ stream = { itemId, bytes: 0, deltas: 0, completed: false, whitespaceRun: 0, head: "", tail: "", textRetained: true, startedAt: now, lastDeltaAt: now };
172
+ this.streams.set(itemId, stream);
173
+ const retained = [...this.streams.values()].filter(item => item.textRetained);
174
+ if (retained.length > STREAM_TEXT_ITEMS) {
175
+ const oldest = retained[0];
176
+ oldest.head = "";
177
+ oldest.tail = "";
178
+ oldest.textRetained = false;
179
+ }
180
+ }
181
+ const bytes = Buffer.byteLength(delta);
182
+ stream.bytes += bytes;
183
+ stream.deltas += 1;
184
+ stream.lastDeltaAt = now;
185
+ this.streamedBytes += bytes;
186
+ this.streamedDeltas += 1;
187
+ const headBytes = Buffer.byteLength(stream.head);
188
+ if (headBytes < STREAM_HEAD_BYTES)
189
+ stream.head += headUtf8(delta, STREAM_HEAD_BYTES - headBytes);
190
+ stream.tail = tailUtf8(stream.tail + delta, STREAM_TAIL_BYTES);
191
+ const trailing = delta.length - delta.trimEnd().length;
192
+ stream.whitespaceRun = trailing === delta.length ? stream.whitespaceRun + bytes : Buffer.byteLength(delta.slice(delta.length - trailing));
193
+ if (stream.whitespaceRun >= CODEX_RUNAWAY_WHITESPACE_BYTES)
194
+ return this.fail(new CodexRunawayOutputError(itemId, stream.whitespaceRun));
195
+ if (stream.bytes > this.maximumResponseBytes)
196
+ this.fail(new Error(`Codex streamed message exceeded the ${this.maximumResponseBytes}-byte result limit before completion`));
197
+ }
198
+ streamDiagnostic() {
199
+ return { authoritative: false, streamedBytes: this.streamedBytes, deltas: this.streamedDeltas, items: [...this.streams.values()].map(item => ({ ...item })) };
200
+ }
201
+ fail(error) { this.error = error; this.reject(error); }
202
+ invalidate(error) { this.error = error; }
203
+ settle() { if (this.terminal)
204
+ this.resolve(this.terminal); }
205
+ final() {
206
+ if (this.error)
207
+ throw this.error;
208
+ if (this.finals.size > 1)
209
+ throw new Error("Multiple distinct final result items");
210
+ const entry = [...this.finals][0];
211
+ return { text: entry?.[1] ?? "", itemId: entry?.[0] };
212
+ }
213
+ }
@@ -52,8 +52,17 @@ export interface RuntimeFailure {
52
52
  dispatchState?: TurnDispatchState;
53
53
  availability?: SessionAvailabilityV1;
54
54
  }
55
- export interface TurnResult {
56
- /** Final assistant message text — where the STEP_STATUS marker lives. */
55
+ export interface RetainedTransportEvidence {
56
+ rawDigest: string;
57
+ responseDigest: string;
58
+ rawBytes: number;
59
+ retainedRawBytes: number;
60
+ valueDigest?: string;
61
+ rejectedValueDigest?: string;
62
+ }
63
+ export interface TurnResultBase {
64
+ transportEvidence?: RetainedTransportEvidence;
65
+ /** Human display / historical transport only. Never parse for a native operation. */
57
66
  text: string;
58
67
  isError: boolean;
59
68
  numTurns: number;
@@ -75,6 +84,8 @@ export interface TurnResult {
75
84
  turnId?: string;
76
85
  /** Deferred handback continuity validation; no hidden provider repair. */
77
86
  continuityErrors?: string[];
87
+ /** Validated composed continuity, never parsed from presentation text. */
88
+ continuity?: import("rafi-spec").ContinuityDelta;
78
89
  /** Exact instruction requested by the immediate host caller. */
79
90
  hostInstruction?: string;
80
91
  /** Exact instruction dispatched to the provider after all wrappers. */
@@ -89,6 +100,14 @@ export interface TurnResult {
89
100
  sessionRef?: ProviderSessionRefV1;
90
101
  };
91
102
  }
103
+ /** Structured transport observations always carry the tuple that selected them. */
104
+ export type TurnResult = TurnResultBase & ({
105
+ readonly structured: import("rafi-spec").StructuredResponse;
106
+ readonly selectedDispatch: import("rafi-spec").SelectedDispatchTupleV2;
107
+ } | {
108
+ readonly structured?: undefined;
109
+ readonly selectedDispatch?: import("rafi-spec").SelectedDispatchTupleV2;
110
+ });
92
111
  /** Observability events emitted while a builder works. */
93
112
  export type BuilderEvent = ({
94
113
  kind: "text";
@@ -117,7 +136,7 @@ export type BuilderEvent = ({
117
136
  lifecycle: "started" | "completed";
118
137
  itemType: string;
119
138
  payload: Record<string, unknown>;
120
- payloadCompleteness: "complete";
139
+ payloadCompleteness: "complete" | "truncated";
121
140
  providerTurnId?: string;
122
141
  } | {
123
142
  kind: "activity";
@@ -205,7 +224,17 @@ export interface ProviderSettingSwitch {
205
224
  fast?: boolean;
206
225
  }
207
226
  export type EffortLevel = "low" | "medium" | "high" | "xhigh";
227
+ export interface ProviderQueryBudget {
228
+ reserveQuery(provider: "claude" | "codex"): {
229
+ maximumSpendUsd: number;
230
+ signal: AbortSignal;
231
+ record(costUsd: number): void;
232
+ close(): void;
233
+ };
234
+ }
208
235
  export interface BuilderAdapterOptions {
236
+ /** Live validation supplies the shared supervisor; production has no synthetic dollar meter. */
237
+ queryBudget?: ProviderQueryBudget;
209
238
  /** Working directory the builder operates in. */
210
239
  cwd: string;
211
240
  /** Absolute runtime executable path verified by the readiness probe. */
@@ -287,9 +316,28 @@ export interface BuilderAdapterOptions {
287
316
  isError?: boolean;
288
317
  outputDigest?: string;
289
318
  rawOutputBytes?: number;
319
+ completionKnown?: boolean;
290
320
  }) => void;
291
321
  }
322
+ export interface ConfirmedDispatchReference {
323
+ dispatchId: string;
324
+ runId: string;
325
+ tupleDigest: string;
326
+ }
327
+ /**
328
+ * Host-owned response requirement contributed by an adapter decorator. The
329
+ * instruction is applied before the dispatch is sealed; validation runs on the
330
+ * native-validated response before the dispatch can be confirmed.
331
+ */
332
+ export interface StructuredDispatchRequirement {
333
+ readonly instruction: string;
334
+ validate(response: import("rafi-spec").OperationResponse): void;
335
+ }
292
336
  export interface BuilderAdapter {
337
+ /** Receives only durable verified transport identity, after confirmation. */
338
+ postConfirmedDispatch?(reference: ConfirmedDispatchReference): Promise<TurnResult | void>;
339
+ /** Requirements every structured dispatch through this adapter must satisfy. */
340
+ structuredDispatchRequirement?(): StructuredDispatchRequirement | undefined;
293
341
  graphRuntimeSettings?(): Pick<BuilderAdapterOptions, "model" | "effort" | "fast" | "runtimeExecutable"> | undefined;
294
342
  readonly agent: "claude" | "codex";
295
343
  contractCapabilities?(): {
@@ -306,12 +354,7 @@ export interface BuilderAdapter {
306
354
  sessionId?: string;
307
355
  }) => void): void;
308
356
  /** Send one instruction; resolves when that turn completes. */
309
- sendTurn(text: string, policy?: {
310
- handback?: boolean;
311
- responseOnly?: boolean;
312
- logicalActionId?: string;
313
- purpose?: ProviderTurnPurpose;
314
- }): Promise<TurnResult>;
357
+ sendTurn(text: string, policy?: TurnRequest): Promise<TurnResult>;
315
358
  /** Synchronous observation through the event owner. Terminal precedes sendTurn resolution. */
316
359
  observeEvents?(listener: (event: BuilderEvent) => void): () => void;
317
360
  /** Commit a deferred handback checkpoint only after the host validates the turn. */
@@ -358,3 +401,37 @@ export interface BuilderAdapter {
358
401
  /** Shut the builder down and release resources. */
359
402
  close(): Promise<void>;
360
403
  }
404
+ /** Host request only; the durable gateway must seal this before provider contact. */
405
+ export interface TurnRequest {
406
+ journalRunId?: string;
407
+ /** Minted only inside the committed structured gateway; never serialized. */
408
+ dispatchCapability?: import("../structuredDispatchAuthority.js").StructuredDispatchCapability;
409
+ handback?: boolean;
410
+ responseOnly?: boolean;
411
+ logicalActionId?: string;
412
+ dispatchId?: string;
413
+ parentDispatchId?: string;
414
+ purpose?: ProviderTurnPurpose;
415
+ output?: import("rafi-spec").OutputContract;
416
+ /** Immutable selected provider boundary; no adapter may substitute a schema. */
417
+ wireContract?: import("rafi-spec").ProviderWireContract<any>;
418
+ /** Digest of the descriptor that rendered the exact provider instruction. */
419
+ renderedWireContractDigest?: string;
420
+ /** Immutable authority sealed before the provider receives the instruction. */
421
+ selectedDispatch?: import("rafi-spec").SelectedDispatchTupleV2;
422
+ }
423
+ /** Terminal provider boundary. Requests acquire this shape only after sealing. */
424
+ export interface SealedTurnPolicy extends TurnRequest {
425
+ output: import("rafi-spec").OutputContract;
426
+ wireContract: import("rafi-spec").ProviderWireContract<any>;
427
+ renderedWireContractDigest: string;
428
+ selectedDispatch: import("rafi-spec").SelectedDispatchTupleV2;
429
+ dispatchCapability: import("../structuredDispatchAuthority.js").StructuredDispatchCapability;
430
+ dispatchId: string;
431
+ }
432
+ export type TurnPolicy = SealedTurnPolicy | (TurnRequest & {
433
+ output?: never;
434
+ selectedDispatch?: never;
435
+ dispatchCapability?: never;
436
+ });
437
+ export type ProviderTurnPolicy = TurnPolicy;
@@ -1,4 +1,5 @@
1
1
  export { withGraphDerivedAccess } from "./graph/derived.js";
2
+ export { operationInstruction, operationResponse, operationStatus, operationPayload, nativePolicy } from "./structuredOperation.js";
2
3
  import { type PermissionConfig } from "./config.js";
3
4
  import { Log } from "./log.js";
4
5
  import { type StepStatus } from "./foreman.js";
@@ -48,6 +49,7 @@ export interface RoleBuilder {
48
49
  log: Log;
49
50
  }
50
51
  export interface RoleInstructionRunOptions extends Omit<RoleBuilderOptions, "log"> {
52
+ output: import("rafi-spec").OutputContract;
51
53
  instruction: string;
52
54
  logPath?: string;
53
55
  logEvent?: "rafi-plan" | "ticket-populate" | "preflight" | "uninstall-proposal";
package/dist/agentRun.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { withGraphDerivedAccess } from "./graph/derived.js";
2
+ export { operationInstruction, operationResponse, operationStatus, operationPayload, nativePolicy } from "./structuredOperation.js";
2
3
  import { existsSync, readFileSync, statSync } from "node:fs";
3
4
  import { join } from "node:path";
4
5
  import { loadConfig, DEFAULT_CONFIG } from "./config.js";
@@ -335,7 +336,7 @@ export async function runRoleInstruction(opts) {
335
336
  const foreman = new Foreman(builder, log, { desktop: config.notifications.enabled, terminalBell: config.notifications.terminal_bell }, false, 3, opts.projectDir);
336
337
  foreman.setGraphPurpose(opts.graphPurpose ?? { planner: "planning", discovery: "discovery", "ticket-maker": "ticket-population", uninstaller: "uninstall-analysis" }[opts.role] ?? "implementation");
337
338
  try {
338
- const turn = await foreman.runInstruction(opts.instruction);
339
+ const turn = await foreman.runInstruction(opts.instruction, { output: opts.output });
339
340
  await builder.close();
340
341
  await viewer;
341
342
  if (opts.logEvent) {
@@ -0,0 +1,18 @@
1
+ import type { BuilderEvent, TurnResult } from "./adapters/types.js";
2
+ export declare function boundedUtf8(text: string, maximum: number): Buffer;
3
+ export declare function receiptFingerprint(turn: TurnResult): string;
4
+ export declare function rawFingerprint(turn: TurnResult): string;
5
+ /** Retains at most one descriptor budget; omitted events cannot establish acceptance. */
6
+ export declare class BoundedTurnEvents extends Array<BuilderEvent> {
7
+ readonly maximumBytes: number;
8
+ static get [Symbol.species](): ArrayConstructor;
9
+ retainedBytes: number;
10
+ totalCount: number;
11
+ toolCount: number;
12
+ terminalCount: number;
13
+ omittedCount: number;
14
+ terminalOverflow: boolean;
15
+ constructor(maximumBytes: number);
16
+ push(...events: BuilderEvent[]): number;
17
+ metadata(): unknown;
18
+ }
@@ -0,0 +1,76 @@
1
+ import { createHash } from "node:crypto";
2
+ import { canonicalJson } from "rafi-spec";
3
+ export function boundedUtf8(text, maximum) {
4
+ const bytes = Buffer.from(text);
5
+ if (bytes.length <= maximum)
6
+ return bytes;
7
+ let end = maximum;
8
+ while (end > 0 && (bytes[end] & 0xc0) === 0x80)
9
+ end--;
10
+ return bytes.subarray(0, end);
11
+ }
12
+ const fingerprints = new WeakMap();
13
+ export function receiptFingerprint(turn) { return fingerprints.get(turn)?.receipt ?? digest(canonicalJson(JSON.parse(JSON.stringify(turn.structured ?? null)))); }
14
+ export function rawFingerprint(turn) { return fingerprints.get(turn)?.raw ?? digest(turn.rawResponse ?? turn.text); }
15
+ function digest(text) { return createHash("sha256").update(text).digest("hex"); }
16
+ /** Retains at most one descriptor budget; omitted events cannot establish acceptance. */
17
+ export class BoundedTurnEvents extends Array {
18
+ maximumBytes;
19
+ static get [Symbol.species]() { return Array; }
20
+ retainedBytes = 2;
21
+ totalCount = 0;
22
+ toolCount = 0;
23
+ terminalCount = 0;
24
+ omittedCount = 0;
25
+ terminalOverflow = false;
26
+ constructor(maximumBytes) {
27
+ super();
28
+ this.maximumBytes = maximumBytes;
29
+ }
30
+ push(...events) {
31
+ for (const event of events) {
32
+ this.totalCount++;
33
+ // Claude reports reasoning as thinking/redacted_thinking content blocks.
34
+ if (event.kind === "tool" || event.kind === "provider-item" && !["text", "agentMessage", "reasoning", "thinking", "redacted_thinking", "userMessage", "contextCompaction", "structuredOutput"].includes(event.itemType))
35
+ this.toolCount++;
36
+ let projected;
37
+ if (event.kind === "turn-complete") {
38
+ this.terminalCount++;
39
+ const turn = event.result;
40
+ const result = { text: "", isError: turn.isError, costUsd: 0, numTurns: turn.numTurns, turnId: turn.turnId,
41
+ providerMetadata: turn.providerMetadata && JSON.parse(JSON.stringify(turn.providerMetadata)), failure: turn.failure && { ...turn.failure, diagnostics: "omitted" } };
42
+ fingerprints.set(result, { receipt: receiptFingerprint(turn), raw: rawFingerprint(turn) });
43
+ projected = { kind: "turn-complete", turnId: event.turnId, result };
44
+ }
45
+ else if (event.kind === "tool")
46
+ projected = { ...event, input: Buffer.byteLength(JSON.stringify(event.input) ?? "") <= 1024 ? JSON.parse(JSON.stringify(event.input ?? null)) : {}, outputSummary: undefined, inputCompleteness: Buffer.byteLength(JSON.stringify(event.input) ?? "") <= 1024 ? event.inputCompleteness : "truncated" };
47
+ else if (event.kind === "provider-item")
48
+ projected = { ...event, payload: {}, payloadCompleteness: "truncated" };
49
+ else if (event.kind === "text")
50
+ projected = { kind: "text", text: boundedUtf8(event.text, 512).toString(), byteCount: Buffer.byteLength(event.text), digest: digest(event.text) };
51
+ else if (event.kind === "error")
52
+ projected = { kind: "error", message: boundedUtf8(event.message, 512).toString() };
53
+ else
54
+ projected = event;
55
+ if (projected.kind !== "turn-complete")
56
+ projected = JSON.parse(JSON.stringify(projected));
57
+ const size = Buffer.byteLength(JSON.stringify(projected)) + 1;
58
+ const limit = event.kind === "turn-complete" ? this.maximumBytes - 2048 : Math.floor(this.maximumBytes / 2);
59
+ if (this.retainedBytes + size > limit) {
60
+ this.omittedCount++;
61
+ if (event.kind === "turn-complete")
62
+ this.terminalOverflow = true;
63
+ continue;
64
+ }
65
+ this.retainedBytes += size;
66
+ super.push(projected);
67
+ }
68
+ return this.length;
69
+ }
70
+ metadata() {
71
+ return JSON.parse(JSON.stringify({ totalCount: this.totalCount, toolCount: this.toolCount, terminalCount: this.terminalCount, omittedCount: this.omittedCount,
72
+ terminalOverflow: this.terminalOverflow, events: this.map(event => event.kind === "turn-complete"
73
+ ? { kind: event.kind, turnId: event.turnId, receiptDigest: receiptFingerprint(event.result), rawDigest: rawFingerprint(event.result), isError: event.result.isError, provider: event.result.providerMetadata }
74
+ : event) }));
75
+ }
76
+ }
@@ -18,6 +18,8 @@ export declare class CurrentWorkflowGuardAdapter implements BuilderAdapter {
18
18
  private readonly adapter;
19
19
  private readonly cwd;
20
20
  private readonly expected;
21
+ postConfirmedDispatch(reference: import("../adapters/types.js").ConfirmedDispatchReference): Promise<TurnResult | void>;
22
+ structuredDispatchRequirement(): import("../adapters/types.js").StructuredDispatchRequirement | undefined;
21
23
  readonly agent: BuilderAdapter["agent"];
22
24
  constructor(adapter: BuilderAdapter, cwd: string, expected?: CurrentWorkflowIdentity);
23
25
  sendTurn(text: string, policy?: Parameters<BuilderAdapter["sendTurn"]>[1]): Promise<TurnResult>;
@@ -30,6 +30,8 @@ export class CurrentWorkflowGuardAdapter {
30
30
  adapter;
31
31
  cwd;
32
32
  expected;
33
+ postConfirmedDispatch(reference) { return this.adapter.postConfirmedDispatch?.(reference) ?? Promise.resolve(); }
34
+ structuredDispatchRequirement() { return this.adapter.structuredDispatchRequirement?.(); }
33
35
  agent;
34
36
  constructor(adapter, cwd, expected = captureCurrentWorkflowIdentity(cwd)) {
35
37
  this.adapter = adapter;
@@ -170,10 +170,7 @@ export function buildBranchAuditInstruction(tickets) {
170
170
  ].join("\n")).join("\n\n");
171
171
  return `Audit this selected ticket batch for undeclared code dependencies. Do not implement anything.
172
172
 
173
- Return one line per required selected-ticket dependency using:
174
- <ticket> depends_on <ticket> - <short reason>
175
-
176
- Only name dependencies where both tickets are in this selected batch. If there are none, say "None".
173
+ Submit the native branch-dependency-audit payload with dependencies containing ticket, dependsOn, and reason. Only name dependencies where both tickets are in this selected batch. If there are none, return an empty dependencies array.
177
174
 
178
175
  ${body}`;
179
176
  }
@@ -1,6 +1,6 @@
1
1
  import type { BuilderAdapter, EffortLevel } from "../adapters/types.js";
2
2
  import type { RunObserver } from "../observability.js";
3
- import type { ProviderSessionRefV1, SessionStrategy } from "rafi-spec";
3
+ import { type ProviderSessionRefV1, type SessionStrategy } from "rafi-spec";
4
4
  import { type QaNonconvergenceContext, type QaNonconvergenceDecision, type QaReportRecoveryHandler, type QaSessionBoundaryRecovery, type QaSessionHandle } from "../qaReview.js";
5
5
  import type { Log } from "../log.js";
6
6
  import type { BranchPlan, BranchPlanNode, BranchRunSummary, CompletionMode, MergeMethod, ReviewProvider } from "./types.js";
@@ -8,6 +8,7 @@ import { SessionUnavailableError } from "../adapters/sessionFailure.js";
8
8
  import { SessionUnavailableContinuityError } from "../continuity.js";
9
9
  import type { QaRecoveryPacket } from "../qaRecovery.js";
10
10
  export interface DeliveryUnitSession {
11
+ communicationProtocolVersion?: 2;
11
12
  runId?: string;
12
13
  unitId: string;
13
14
  branch: string;
@@ -6,6 +6,7 @@ import { loadTickets } from "../tickets/ticketLoader.js";
6
6
  import { formatRecoveryCommand } from "../recoveryGuidance.js";
7
7
  import { WorkflowReader } from "../workflowReader.js";
8
8
  import { buildScopeRevision } from "../buildApproval.js";
9
+ import { qaVerdictOutput } from "rafi-spec";
9
10
  import { beginQaFinalization, completeQaFinalization, verifyPendingQaFinalizationSource, compactWithRetry, runIsolatedQa } from "../qaReview.js";
10
11
  import { QaFailureDeliveryService } from "../qaFailureDelivery.js";
11
12
  import { Foreman, MARKER_SPEC } from "../foreman.js";
@@ -30,6 +31,8 @@ export function readDeliveryUnitSession(projectDir, unitId) {
30
31
  return undefined;
31
32
  try {
32
33
  const cached = JSON.parse(readFileSync(path, "utf8"));
34
+ if (cached.communicationProtocolVersion !== 2)
35
+ return undefined;
33
36
  const reader = new WorkflowReader(projectDir);
34
37
  try {
35
38
  const known = reader.branchResumeSessions(false);
@@ -376,7 +379,7 @@ export async function runBranchPlan(opts) {
376
379
  db.updateOperation(key, "in_progress");
377
380
  }
378
381
  });
379
- const response = await foreman.runInstruction(ticketInstruction);
382
+ const response = await foreman.runBuilderWork(ticketInstruction);
380
383
  for (const decision of continuations)
381
384
  db.updateOperation(`decision-continuation:${decision.decisionId}`, response.result.failure?.dispatchState === "unknown" ? "uncertain" : "confirmed", { result: { isError: response.result.isError } });
382
385
  return response;
@@ -435,7 +438,7 @@ export async function runBranchPlan(opts) {
435
438
  if (node.deliveryUnitId) {
436
439
  const path = deliverySessionPath(opts.projectDir, node.deliveryUnitId);
437
440
  mkdirSync(join(opts.projectDir, ".foreman", "delivery-sessions"), { recursive: true });
438
- writeFileSync(path, `${JSON.stringify({ runId: opts.runId, unitId: node.deliveryUnitId, branch: node.branch, worktreePath, sessionId, sessionRef: builder.sessionRef?.(), ticket: node.ticket.id }, null, 2)}\n`, "utf8");
441
+ writeFileSync(path, `${JSON.stringify({ communicationProtocolVersion: 2, runId: opts.runId, unitId: node.deliveryUnitId, branch: node.branch, worktreePath, sessionId, sessionRef: builder.sessionRef?.(), ticket: node.ticket.id }, null, 2)}\n`, "utf8");
439
442
  }
440
443
  }
441
444
  }
@@ -482,7 +485,7 @@ export async function runBranchPlan(opts) {
482
485
  resumedRecovery: qaResumedRecovery,
483
486
  sessionBoundary: opts.qaSessionBoundary ?? (async () => { throw new Error("QA recovery requires a validated durable session boundary"); }),
484
487
  observeNativeCompactions: opts.observeQaNativeCompactions,
485
- resolveBlocked: (adapter, reason) => foreman.resolveBlocker(adapter, reason, "qa"),
488
+ resolveBlocked: (adapter, reason) => foreman.resolveBlocker(adapter, reason, "qa", qaVerdictOutput),
486
489
  evidence: (entry) => opts.log.write("qa-evidence", { ticket: node.ticket.id, ...entry }),
487
490
  resumeBuilderGuidance: qaOnlyRecovery,
488
491
  deliverBuilderFollowup: (instruction, followup) => deliverBuilderGuidanceFollowup({ projectDir: opts.projectDir, runId: opts.runId, ticketId: node.ticket.id, worktree: worktreePath }, instruction, followup, {
@@ -1145,8 +1148,8 @@ Branch-mode rules:
1145
1148
  - Do not push, create PRs, or run git commit.
1146
1149
  - Do not edit .tickets/, ${progressDoc}, or other tracker state.
1147
1150
  - If a required test is denied only because you added a pipe, redirection, substitution, or wrapper, retry the exact required-test command without that extra shell syntax. Do not retry other denied actions.
1148
- - If another selected ticket is required before this one can be completed, stop and end with STEP_STATUS: blocked | ticket="${node.ticket.id}" branch_dependency="<ticket-id>" reason="<why>".
1149
- - End with STEP_STATUS: done | ticket="${node.ticket.id}" summary="<what changed>" when the ticket is implemented.
1151
+ - If another selected ticket is required before this one can be completed, return a native blocked result with branchDependency set to that ticket ID, reason explaining the dependency, and nextAction describing the needed resolution. Do not execute that other ticket.
1152
+ - When this ticket is implemented, return the native completed work-report with its summary, evidence, coverage, and continuity. Rafi binds the assigned ticket ID and requires independent QA before acceptance.
1150
1153
 
1151
1154
  Final QA runs independently in a separate disposable snapshot and fresh reviewer conversation.
1152
1155
 
@@ -1167,8 +1170,8 @@ Branch-mode rules:
1167
1170
  - Do not switch branches.
1168
1171
  - Do not push, create PRs, or run git commit.
1169
1172
  - Do not edit .tickets/, ${progressDoc}, or other tracker state.
1170
- - If another selected ticket is required before this one can be completed, stop and end with STEP_STATUS: blocked | ticket="${node.ticket.id}" branch_dependency="<ticket-id>" reason="<why>".
1171
- - End with STEP_STATUS: done | ticket="${node.ticket.id}" summary="<what changed>" when the ticket is implemented.
1173
+ - If another selected ticket is required before this one can be completed, return a native blocked result with branchDependency set to that ticket ID, reason explaining the dependency, and nextAction describing the needed resolution. Do not execute that other ticket.
1174
+ - When this ticket is implemented, return the native completed work-report with its summary, evidence, coverage, and continuity. Rafi binds the assigned ticket ID and requires independent QA before acceptance.
1172
1175
 
1173
1176
  Final QA runs independently in a separate disposable snapshot and fresh reviewer conversation.
1174
1177
 
@@ -1,4 +1,6 @@
1
1
  import { registerHandbackWriter } from "./qaHandbackMigration.js";
2
+ import { registerQaGraphJournalWriter } from "./qaDeliveryJournal.js";
3
+ import { assertStructuredCutoverComplete } from "./structuredCutover.js";
2
4
  import { AsyncLocalStorage } from "node:async_hooks";
3
5
  import { createHash, randomUUID } from "node:crypto";
4
6
  import { realpathSync } from "node:fs";
@@ -58,6 +60,8 @@ export function maintainBuildAdmission(authority) {
58
60
  db = new Database(join(authority.project, ".rafi/recovery.sqlite3"), { fileMustExist: true });
59
61
  db.function("rafi_protocol_v3", () => 1);
60
62
  registerHandbackWriter(db);
63
+ registerQaGraphJournalWriter(db);
64
+ assertStructuredCutoverComplete(db);
61
65
  const changed = db.prepare("UPDATE build_admission SET record_json=json_set(record_json,'$.heartbeatAt',?) WHERE singleton=1 AND json_extract(record_json,'$.token')=?").run(new Date().toISOString(), authority.token);
62
66
  if (changed.changes !== 1)
63
67
  stopBuildAdmissionHeartbeat(authority);