ai-foreman 1.7.29 → 2.0.1

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 (156) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude.d.ts +23 -12
  3. package/dist/adapters/claude.js +231 -26
  4. package/dist/adapters/codex.d.ts +19 -7
  5. package/dist/adapters/codex.js +202 -19
  6. package/dist/adapters/recovering.d.ts +2 -0
  7. package/dist/adapters/recovering.js +5 -1
  8. package/dist/adapters/structured.d.ts +71 -0
  9. package/dist/adapters/structured.js +213 -0
  10. package/dist/adapters/types.d.ts +85 -9
  11. package/dist/agentRun.d.ts +2 -0
  12. package/dist/agentRun.js +2 -1
  13. package/dist/boundedTurnEvidence.d.ts +18 -0
  14. package/dist/boundedTurnEvidence.js +76 -0
  15. package/dist/branch/currentGuard.d.ts +2 -0
  16. package/dist/branch/currentGuard.js +2 -0
  17. package/dist/branch/git.js +3 -8
  18. package/dist/branch/planner.js +1 -4
  19. package/dist/branch/runner.d.ts +2 -1
  20. package/dist/branch/runner.js +10 -7
  21. package/dist/buildAdmission.js +4 -0
  22. package/dist/buildApproval.d.ts +15 -0
  23. package/dist/buildApproval.js +28 -9
  24. package/dist/buildAssignment.d.ts +1 -0
  25. package/dist/buildAssignment.js +82 -23
  26. package/dist/buildInterventions.d.ts +2 -2
  27. package/dist/buildInterventions.js +11 -4
  28. package/dist/buildLaunchGate.js +3 -2
  29. package/dist/buildRuns.d.ts +1 -0
  30. package/dist/buildRuns.js +6 -1
  31. package/dist/builderGuidanceFollowup.js +17 -6
  32. package/dist/cli/doctor.js +3 -1
  33. package/dist/cli/manager.js +22 -20
  34. package/dist/cli/start.js +85 -39
  35. package/dist/cli/tickets.d.ts +0 -8
  36. package/dist/cli/tickets.js +50 -60
  37. package/dist/continuity.d.ts +10 -16
  38. package/dist/continuity.js +223 -208
  39. package/dist/continuityOrigin.d.ts +13 -0
  40. package/dist/continuityOrigin.js +91 -0
  41. package/dist/dispatchNotSent.d.ts +4 -0
  42. package/dist/dispatchNotSent.js +10 -0
  43. package/dist/foreman.d.ts +15 -16
  44. package/dist/foreman.js +70 -278
  45. package/dist/gitExclude.d.ts +16 -0
  46. package/dist/gitExclude.js +46 -0
  47. package/dist/graph/boundary.d.ts +1 -1
  48. package/dist/graph/boundary.js +1 -1
  49. package/dist/graph/host.js +8 -19
  50. package/dist/graph/manager.d.ts +1 -1
  51. package/dist/graph/manager.js +1 -1
  52. package/dist/graph/preparation.d.ts +38 -0
  53. package/dist/graph/preparation.js +7 -6
  54. package/dist/graph/semantic.d.ts +1 -0
  55. package/dist/graph/semantic.js +4 -1
  56. package/dist/graph/session.js +7 -2
  57. package/dist/graph/turn.d.ts +3 -4
  58. package/dist/graph/turn.js +29 -41
  59. package/dist/handoffProtocol.js +2 -2
  60. package/dist/handoffs.d.ts +5 -3
  61. package/dist/handoffs.js +92 -88
  62. package/dist/hostGraphApplicability.d.ts +2 -1
  63. package/dist/hostGraphApplicability.js +17 -19
  64. package/dist/hostQaAugmentation.js +21 -4
  65. package/dist/hostQaComparison.d.ts +21 -2
  66. package/dist/hostQaComparison.js +43 -29
  67. package/dist/hostSourcePreservation.d.ts +38 -0
  68. package/dist/hostSourcePreservation.js +72 -0
  69. package/dist/humanDecision.d.ts +7 -0
  70. package/dist/humanDecision.js +11 -0
  71. package/dist/interviews.d.ts +7 -0
  72. package/dist/interviews.js +18 -1
  73. package/dist/legacyCommunicationDisplay.d.ts +2 -0
  74. package/dist/legacyCommunicationDisplay.js +2 -0
  75. package/dist/managerPacket.js +3 -3
  76. package/dist/markers.d.ts +4 -3
  77. package/dist/markers.js +4 -20
  78. package/dist/observability.js +3 -8
  79. package/dist/probeIpc.d.ts +14 -0
  80. package/dist/probeIpc.js +29 -0
  81. package/dist/qaBuildGate.d.ts +21 -0
  82. package/dist/qaBuildGate.js +60 -13
  83. package/dist/qaBuilderCoverage.js +22 -13
  84. package/dist/qaBuilderEvidence.d.ts +16 -1
  85. package/dist/qaBuilderEvidence.js +48 -8
  86. package/dist/qaContractCoverage.d.ts +2 -2
  87. package/dist/qaContractCoverage.js +4 -9
  88. package/dist/qaContractDelivery.js +34 -20
  89. package/dist/qaCoverageAssembly.d.ts +22 -0
  90. package/dist/qaCoverageAssembly.js +16 -0
  91. package/dist/qaDeliveryJournal.d.ts +2 -0
  92. package/dist/qaDeliveryJournal.js +19 -0
  93. package/dist/qaFailureDelivery.js +97 -85
  94. package/dist/qaHostTransformation.d.ts +26 -0
  95. package/dist/qaHostTransformation.js +100 -0
  96. package/dist/qaPreparation.d.ts +8 -20
  97. package/dist/qaPreparation.js +153 -254
  98. package/dist/qaPreparationInputs.js +19 -0
  99. package/dist/qaPreparationStore.d.ts +11 -0
  100. package/dist/qaPreparationStore.js +44 -4
  101. package/dist/qaPrerequisites.d.ts +2 -1
  102. package/dist/qaPrerequisites.js +21 -1
  103. package/dist/qaProtocolV2.d.ts +15 -0
  104. package/dist/qaRecovery.d.ts +0 -3
  105. package/dist/qaRecovery.js +21 -27
  106. package/dist/qaReview.js +241 -406
  107. package/dist/qaReviewEvidence.d.ts +7 -3
  108. package/dist/qaReviewEvidence.js +15 -4
  109. package/dist/qaRuntime.js +1 -1
  110. package/dist/qaSnapshot.js +76 -21
  111. package/dist/qaStructuredResponse.d.ts +18 -0
  112. package/dist/qaStructuredResponse.js +81 -0
  113. package/dist/qaTimeline.js +2 -2
  114. package/dist/qaVerificationContract.d.ts +1 -2
  115. package/dist/qaVerificationContract.js +24 -35
  116. package/dist/readinessGate.js +5 -4
  117. package/dist/recoveryOwners.d.ts +32 -0
  118. package/dist/recoveryOwners.js +87 -0
  119. package/dist/runtimePreflight.d.ts +11 -0
  120. package/dist/runtimePreflight.js +77 -0
  121. package/dist/runtimeReadiness.js +23 -15
  122. package/dist/sources/sourceRegistry.d.ts +0 -3
  123. package/dist/sources/sourceRegistry.js +0 -42
  124. package/dist/statusReporter.d.ts +2 -0
  125. package/dist/statusReporter.js +2 -0
  126. package/dist/structuredCutover.d.ts +31 -0
  127. package/dist/structuredCutover.js +510 -0
  128. package/dist/structuredCutoverFiles.d.ts +10 -0
  129. package/dist/structuredCutoverFiles.js +181 -0
  130. package/dist/structuredDispatchAuthority.d.ts +22 -0
  131. package/dist/structuredDispatchAuthority.js +110 -0
  132. package/dist/structuredDispatchRecord.d.ts +29 -0
  133. package/dist/structuredDispatchRecord.js +36 -0
  134. package/dist/structuredLiveBudget.d.ts +2 -0
  135. package/dist/structuredLiveBudget.js +23 -0
  136. package/dist/structuredOperation.d.ts +18 -0
  137. package/dist/structuredOperation.js +279 -0
  138. package/dist/supervisedStart.js +35 -21
  139. package/dist/ticketPlanning.d.ts +1 -376
  140. package/dist/ticketPlanning.js +2 -49
  141. package/dist/ticketPopulation.d.ts +0 -3
  142. package/dist/ticketPopulation.js +4 -21
  143. package/dist/ticketPopulationPageAuthority.d.ts +13 -0
  144. package/dist/ticketPopulationPageAuthority.js +30 -0
  145. package/dist/ticketPopulationPaging.d.ts +27 -0
  146. package/dist/ticketPopulationPaging.js +127 -0
  147. package/dist/tickets/renderMarkdown.d.ts +14 -0
  148. package/dist/tickets/renderMarkdown.js +39 -5
  149. package/dist/tickets/ticketLoader.js +10 -0
  150. package/dist/tickets/ticketSchema.d.ts +107 -0
  151. package/dist/tickets/ticketSchema.js +4 -1
  152. package/dist/workflowDb.d.ts +116 -3
  153. package/dist/workflowDb.js +852 -61
  154. package/dist/workflowReader.d.ts +1 -0
  155. package/dist/workflowReader.js +9 -0
  156. package/package.json +23 -7
@@ -1,6 +1,6 @@
1
- import type { ProviderTurnPurpose } from "../providerPhase.js";
2
1
  import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, CompactResult, ContextUsage, NativeAutoCompactionPolicy, NativeCompaction, ProviderSessionUsage, ProviderSettingSwitch, TurnResult } from "./types.js";
3
2
  import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
3
+ import type { ProviderTurnPolicy } from "./types.js";
4
4
  export interface CodexLineResult {
5
5
  events: BuilderEvent[];
6
6
  sessionId?: string;
@@ -30,6 +30,12 @@ export declare class CodexAdapter implements BuilderAdapter {
30
30
  private closed;
31
31
  private closePromise?;
32
32
  private activeText;
33
+ private structuredCollector?;
34
+ private turnStreamedBytes;
35
+ private streamStartedAt;
36
+ private streamActivityAt;
37
+ private streamDiagnosticAt;
38
+ private turnInFlight;
33
39
  private usage?;
34
40
  private usageRevision;
35
41
  private providerSessionUsage?;
@@ -82,12 +88,7 @@ export declare class CodexAdapter implements BuilderAdapter {
82
88
  buildAppServerArgs(): string[];
83
89
  /** Deprecated fixture helper. Runtime execution uses only app-server. */
84
90
  buildArgs(instruction: string): string[];
85
- sendTurn(instruction: string, policy?: {
86
- handback?: boolean;
87
- responseOnly?: boolean;
88
- logicalActionId?: string;
89
- purpose?: ProviderTurnPurpose;
90
- }): Promise<TurnResult>;
91
+ sendTurn(instruction: string, policy?: ProviderTurnPolicy): Promise<TurnResult>;
91
92
  private sendTurnInternal;
92
93
  compact(): Promise<CompactResult>;
93
94
  private compactInternal;
@@ -119,9 +120,20 @@ export declare class CodexAdapter implements BuilderAdapter {
119
120
  private handle;
120
121
  private observationContext;
121
122
  private waitFor;
123
+ /** Ask the app-server to stop a native turn; true only once its terminal is observed. */
124
+ private interruptNativeTurn;
125
+ /** Streamed output is visible progress; it is never treated as a result. */
126
+ private observeAgentMessageDelta;
127
+ /**
128
+ * Bounded interruption evidence for structured turns under .foreman (Rafi's
129
+ * git-excluded log directory). Diagnostic only: never read back as output.
130
+ */
131
+ private writeStreamDiagnostic;
122
132
  private traceLifecycle;
123
133
  private touchWaiters;
124
134
  private providerIdleTimeoutMs;
135
+ /** Read-only QA may reach the network only with the operator's --qa-network grant. */
136
+ private qaNetworkAccess;
125
137
  private effectiveNetworkAccess;
126
138
  private effectiveApprovalPolicy;
127
139
  /** Fail closed; approval is for this operation only, never a session cache or policy amendment. */
@@ -1,7 +1,8 @@
1
+ import { inheritedStructuredLiveBudget } from "../structuredLiveBudget.js";
1
2
  import { OperationDeadline } from "../util/deadline.js";
2
3
  import { spawn } from "node:child_process";
3
4
  import { createHash, randomUUID } from "node:crypto";
4
- import { existsSync, readFileSync } from "node:fs";
5
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
5
6
  import { join } from "node:path";
6
7
  import { createInterface } from "node:readline";
7
8
  import { loadSkill } from "special-agents";
@@ -11,6 +12,9 @@ import { canonicalSessionPath, createProviderSessionRef, validateProviderSession
11
12
  import { SessionUnavailableError, sessionUnavailableResult } from "./sessionFailure.js";
12
13
  import { isFastModeUnavailable, reportFastModeUnavailable } from "./fastMode.js";
13
14
  import { handoffSystemInstructions } from "../handoffProtocol.js";
15
+ import { providerControlOperationV2, providerWireContract, renderProviderWireInstruction, initializationOutput, validateStructuredCandidate, selectedDispatchTupleV2, structuredDigest } from "rafi-spec";
16
+ import { assertActiveProviderDispatch, withEphemeralInitializationDispatch } from "../structuredDispatchAuthority.js";
17
+ import { CodexRunawayOutputError, CodexTurnCollector, structuredReceipt } from "./structured.js";
14
18
  /** Compatibility parser for recorded pre-app-server JSONL fixtures. */
15
19
  export function parseCodexLine(raw) {
16
20
  const type = raw.type;
@@ -46,6 +50,12 @@ export class CodexAdapter {
46
50
  closed = false;
47
51
  closePromise;
48
52
  activeText = [];
53
+ structuredCollector;
54
+ turnStreamedBytes = 0;
55
+ streamStartedAt = 0;
56
+ streamActivityAt = 0;
57
+ streamDiagnosticAt = 0;
58
+ turnInFlight = false;
49
59
  usage;
50
60
  usageRevision = 0;
51
61
  providerSessionUsage;
@@ -117,6 +127,7 @@ export class CodexAdapter {
117
127
  }
118
128
  constructor(opts) {
119
129
  this.opts = opts;
130
+ inheritedStructuredLiveBudget("codex");
120
131
  this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
121
132
  this._sessionRef = opts.resumeSessionRef;
122
133
  }
@@ -140,6 +151,7 @@ export class CodexAdapter {
140
151
  buildInstruction(instruction) {
141
152
  return [this.opts.systemPromptAppend,
142
153
  this.opts.sessionRole === "builder" ? `Builder runtime contract: shell network ${this.effectiveNetworkAccess() ? "explicitly approved for this build" : "disabled"}; sandbox escalation ${this.effectiveApprovalPolicy() === "on-request" ? "requires separate approval for each exact operation" : "disabled"}. Web tools do not establish shell network access. If acquisition needs unavailable capabilities, request input before running acquisition, or use a local source/dependency bundle with verified provenance. A ticket answer does not change runtime permissions.` : undefined,
154
+ this.qaNetworkAccess() ? "QA runtime contract: shell network explicitly approved for this review (--qa-network); you may run live checks against operator-provided local or remote services. Workspace stays read-only." : undefined,
143
155
  this.buildSkillsAppendix(), instruction]
144
156
  .filter((part) => Boolean(part)).join("\n\n");
145
157
  }
@@ -163,9 +175,23 @@ export class CodexAdapter {
163
175
  return args;
164
176
  }
165
177
  async sendTurn(instruction, policy) {
178
+ if (this.turnInFlight)
179
+ throw new Error("a turn is already in progress");
180
+ if (policy?.output)
181
+ assertActiveProviderDispatch(policy, instruction, "codex", this.sessionRef());
182
+ if (policy?.output) {
183
+ const selected = policy.wireContract ?? providerWireContract(policy.output, "codex", "default", { requireRegistered: true });
184
+ if (selected.provider !== "codex" || selected.canonical.schemaDigest !== policy.output.schemaDigest)
185
+ throw new Error("Codex received a mismatched provider wire contract");
186
+ if (policy.renderedWireContractDigest && policy.renderedWireContractDigest !== selected.descriptorDigest)
187
+ throw new Error("Codex received mismatched rendered wire guidance");
188
+ instruction = renderProviderWireInstruction(instruction, selected);
189
+ policy = { ...policy, wireContract: selected, renderedWireContractDigest: selected.descriptorDigest };
190
+ }
191
+ this.turnInFlight = true;
166
192
  const prepared = this.instructionWithPendingQaReminder(instruction);
167
193
  instruction = prepared.instruction;
168
- const turnId = randomUUID();
194
+ const turnId = policy?.dispatchId ?? randomUUID();
169
195
  this.activeProviderTurnId = turnId;
170
196
  const observer = this.opts.observer;
171
197
  if (!observer) {
@@ -187,6 +213,8 @@ export class CodexAdapter {
187
213
  finally {
188
214
  this.activeProviderTurnId = undefined;
189
215
  this.activeNativeTurnId = undefined;
216
+ this.structuredCollector = undefined;
217
+ this.turnInFlight = false;
190
218
  }
191
219
  }
192
220
  const context = this.observationContext();
@@ -218,9 +246,11 @@ export class CodexAdapter {
218
246
  this.activeProviderTurnId = undefined;
219
247
  this.activeNativeTurnId = undefined;
220
248
  this.activeProviderTurnSpanId = undefined;
249
+ this.structuredCollector = undefined;
250
+ this.turnInFlight = false;
221
251
  }
222
252
  }
223
- async sendTurnInternal(instruction, policy) {
253
+ async sendTurnInternal(instruction, policy, runawayRetries = 0) {
224
254
  if (this.closed)
225
255
  throw new Error("builder is closed");
226
256
  this.eventQueue.push({ kind: "activity", state: "starting Codex turn", provider: "codex", model: this.opts.model });
@@ -233,12 +263,29 @@ export class CodexAdapter {
233
263
  const toolsBefore = this.observedToolCalls;
234
264
  this.turnSawTool = false;
235
265
  this.activeText = [];
266
+ this.turnStreamedBytes = 0;
267
+ this.streamStartedAt = 0;
268
+ this.streamActivityAt = 0;
269
+ this.streamDiagnosticAt = 0;
236
270
  this.lastTurnTokens = undefined;
237
271
  this.turnUsageBaseline = this.providerSessionUsage ? { ...this.providerSessionUsage } : undefined;
238
- const completion = this.waitFor("turn/completed", (params) => params.threadId === this._sessionId, this.providerIdleTimeoutMs(), true);
239
- void completion.catch(() => { });
272
+ const deadline = new OperationDeadline("Codex active turn", this.opts.turnDeadlineMs ?? 3_600_000);
273
+ let idleTimer;
274
+ const clearIdle = () => { if (idleTimer)
275
+ clearTimeout(idleTimer); idleTimer = undefined; };
276
+ const resetIdle = () => {
277
+ clearIdle();
278
+ idleTimer = setTimeout(() => collector?.fail(new Error("Codex native turn idle deadline expired; execution outcome requires reconciliation")), this.providerIdleTimeoutMs());
279
+ idleTimer.unref();
280
+ };
281
+ const wire = policy?.output ? policy.wireContract ?? providerWireContract(policy.output, "codex", "default", { requireRegistered: true }) : undefined;
282
+ const collector = policy?.output ? this.structuredCollector = new CodexTurnCollector(this._sessionId, true, resetIdle, wire.maximumResponseBytes) : undefined;
283
+ const completion = collector ? collector.completion : this.waitFor("turn/completed", (params) => params.threadId === this._sessionId, this.providerIdleTimeoutMs(), true);
284
+ void completion.then(clearIdle, clearIdle);
240
285
  turnStartDispatched = true;
286
+ const readOnlyTurn = Boolean(policy?.responseOnly || policy?.purpose && policy.purpose !== "implementation" || this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa");
241
287
  const startParams = {
288
+ ...(wire ? { outputSchema: wire.wireSchema } : {}),
242
289
  threadId: this._sessionId,
243
290
  input: [{ type: "text", text: providerInstruction, text_elements: [] }],
244
291
  cwd: this.opts.cwd,
@@ -246,41 +293,82 @@ export class CodexAdapter {
246
293
  effort: this.opts.effort ?? null,
247
294
  ...(this.opts.fast ? { serviceTier: this.fastTier ?? "default" } : {}),
248
295
  approvalPolicy: policy?.responseOnly || policy?.purpose && policy.purpose !== "implementation" ? "never" : this.effectiveApprovalPolicy(),
249
- sandboxPolicy: policy?.responseOnly || policy?.purpose && policy.purpose !== "implementation" || this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa"
250
- ? { type: "readOnly", networkAccess: false }
296
+ sandboxPolicy: readOnlyTurn
297
+ ? { type: "readOnly", networkAccess: this.qaNetworkAccess() }
251
298
  : { type: "workspaceWrite", writableRoots: [this.opts.cwd], networkAccess: this.effectiveNetworkAccess() },
252
299
  };
253
300
  let started;
254
301
  try {
255
- started = await this.request("turn/start", startParams);
302
+ started = await deadline.run(() => this.request("turn/start", startParams));
256
303
  }
257
304
  catch (error) {
258
- if (!this.fastTier || !isFastModeUnavailable(String(error)) || this.turnSawTool || this.activeText.length)
305
+ if (policy?.output || !this.fastTier || !isFastModeUnavailable(String(error)) || this.turnSawTool || this.activeText.length)
259
306
  throw error;
260
307
  this.disableFastMode();
261
308
  started = await this.request("turn/start", { ...startParams, serviceTier: "default" });
262
309
  }
263
310
  this.activeNativeTurnId = String(started.turn?.id ?? "") || undefined;
264
- const params = await new OperationDeadline("Codex active turn", this.opts.turnDeadlineMs ?? 3_600_000).run(() => completion);
265
- const turn = params.turn;
266
- const responseOnlyViolation = Boolean(policy?.responseOnly && this.observedToolCalls !== toolsBefore);
267
- const failed = turn?.status === "failed" || responseOnlyViolation;
311
+ collector?.bind(this.activeNativeTurnId ?? "");
312
+ let params;
313
+ try {
314
+ params = await deadline.run(() => completion);
315
+ }
316
+ catch (error) {
317
+ // Runaway output is a definite provider rejection, not an unknown
318
+ // outcome. A read-only turn cannot change the workspace, so after the
319
+ // native turn is confirmed stopped it may be asked once more, inside
320
+ // the same sealed host dispatch. Writable turns are never retried.
321
+ const nativeTurnId = this.activeNativeTurnId;
322
+ if (error instanceof CodexRunawayOutputError && runawayRetries < 1 && readOnlyTurn && nativeTurnId && await this.interruptNativeTurn(nativeTurnId)) {
323
+ this.writeStreamDiagnostic("interrupted", `${error.message}; retrying once`);
324
+ this.traceLifecycle("runaway-output-retry", { elapsedMs: 0 });
325
+ this.eventQueue.push({ kind: "retry", provider: "codex", reason: error.message, attempt: 1, maximum: 1, managedBy: "rafi", retryId: nativeTurnId });
326
+ return this.sendTurnInternal(instruction, policy, runawayRetries + 1);
327
+ }
328
+ throw error;
329
+ }
330
+ const turn = collector ? params : params.turn;
331
+ const responseOnlyViolation = Boolean(policy?.responseOnly && (collector ? collector.tools : this.observedToolCalls !== toolsBefore));
332
+ const failed = turn?.status !== "completed" || responseOnlyViolation;
268
333
  const error = turn?.error;
269
- const text = this.activeText.join("\n");
270
- if (failed && !responseOnlyViolation && this.fastTier && !this.turnSawTool && !text && isFastModeUnavailable(String(error?.message ?? ""))) {
334
+ let final;
335
+ let invalidFinal;
336
+ try {
337
+ final = collector?.final();
338
+ }
339
+ catch (error) {
340
+ invalidFinal = String(error);
341
+ }
342
+ if (this.streamDiagnosticAt)
343
+ this.writeStreamDiagnostic(failed || invalidFinal ? "interrupted" : "completed", failed ? `Codex terminal ${String(turn?.status)}` : invalidFinal);
344
+ const text = final?.text ?? this.activeText.join("\n");
345
+ if (!policy?.output && failed && !responseOnlyViolation && this.fastTier && !this.turnSawTool && !text && isFastModeUnavailable(String(error?.message ?? ""))) {
271
346
  this.disableFastMode();
272
347
  return this.sendTurnInternal(instruction, policy);
273
348
  }
274
- const result = {
349
+ let result = {
275
350
  text: failed ? normalizeRuntimeErrorText("codex", String(responseOnlyViolation ? "Response-only correction used tools; its result cannot authorize completion" : error?.message ?? text), null, "app-server turn") : text,
276
351
  isError: failed, numTurns: 1, costUsd: 0, costAuthoritative: false,
277
352
  turnId: this.activeProviderTurnId, hostInstruction: instruction, providerInstruction,
278
353
  rawResponse: text, cleanedResponse: failed ? normalizeRuntimeErrorText("codex", String(responseOnlyViolation ? "Response-only correction used tools; its result cannot authorize completion" : error?.message ?? text), null, "app-server turn") : text,
279
354
  providerMetadata: { provider: "codex", sessionId: this._sessionId, sessionRef: this._sessionRef },
355
+ ...(policy?.selectedDispatch ? { selectedDispatch: policy.selectedDispatch } : {}),
280
356
  ...(this.lastTurnTokens ?? {}),
281
357
  };
282
358
  result.usage = { scope: "turn-delta", inputTokens: result.inputTokens, outputTokens: result.outputTokens,
283
359
  ...(result.inputTokens !== undefined || result.outputTokens !== undefined ? { totalTokens: (result.inputTokens ?? 0) + (result.outputTokens ?? 0) } : {}) };
360
+ if (policy?.output) {
361
+ const selectedDispatch = policy.selectedDispatch;
362
+ if (!selectedDispatch || !this._sessionId)
363
+ throw new Error("Structured terminal lost its sealed dispatch tuple or session");
364
+ result = { ...result, selectedDispatch, structured: failed ? { state: "rejected", reason: responseOnlyViolation ? "tool-policy-violation" : "execution-not-completed", detail: `Codex terminal ${String(turn?.status)}`, dispatchId: result.turnId }
365
+ : invalidFinal ? { state: "rejected", reason: "ambiguous-result", detail: invalidFinal, dispatchId: result.turnId }
366
+ : !wire || !policy.selectedDispatch
367
+ ? { state: "rejected", reason: "invalid-contract", detail: "Codex structured turn has no sealed descriptor or tuple", dispatchId: result.turnId }
368
+ : structuredReceipt(policy.output, "codex", final?.itemId ? text : undefined, { dispatchId: result.turnId, sessionId: this._sessionId, nativeTurnId: this.activeNativeTurnId, nativeItemId: final?.itemId }, "json-text", wire, policy.selectedDispatch) };
369
+ if (result.structured.state === "rejected")
370
+ result.isError = true;
371
+ }
284
372
  this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
285
373
  return result;
286
374
  }
@@ -298,6 +386,8 @@ export class CodexAdapter {
298
386
  return result;
299
387
  }
300
388
  const detail = [error instanceof Error ? error.message : String(error), this.stderr].filter(Boolean).join("\n");
389
+ if (this.turnStreamedBytes)
390
+ this.writeStreamDiagnostic("interrupted", error instanceof Error ? error.message : String(error));
301
391
  const text = normalizeRuntimeErrorText("codex", detail, this.process?.exitCode ?? null, "builder turn");
302
392
  this.disconnect(new Error(text));
303
393
  const resumed = Boolean(this.opts.resumeSessionRef);
@@ -379,6 +469,7 @@ export class CodexAdapter {
379
469
  }, hardTimeoutMs, false, controller.signal);
380
470
  this.manualCompactionInFlight = true;
381
471
  try {
472
+ providerControlOperationV2("codex", "thread/compact/start");
382
473
  const acknowledgementFailure = this.request("thread/compact/start", { threadId: this._sessionId }).then(() => { this.traceLifecycle("compaction-acknowledged"); return new Promise(() => { }); }, error => {
383
474
  // A missing acknowledgement is not failure evidence. Explicit
384
475
  // completion plus fresh occupancy is sufficient to reconcile it.
@@ -391,6 +482,13 @@ export class CodexAdapter {
391
482
  finally {
392
483
  this.manualCompactionInFlight = false;
393
484
  }
485
+ // Manual completion must invalidate delivery receipts just like automatic
486
+ // compaction. Continuity accounts for this requested compaction separately.
487
+ this.nativeCompactionSequence++;
488
+ if (this.qaContractReminder) {
489
+ this.qaContractReminderPending = true;
490
+ this.qaReminderEvent("compaction-observed");
491
+ }
394
492
  this.eventQueue.push({ kind: "session-transition", transition: "compacted" });
395
493
  this.traceLifecycle("compaction-completed", { elapsedMs: performance.now() - began });
396
494
  return { ok: true };
@@ -437,11 +535,28 @@ export class CodexAdapter {
437
535
  if (this.opts.allowAutoCompactionSetupTurn === false)
438
536
  return;
439
537
  const toolsBefore = this.observedToolCalls;
440
- const setup = await this.sendTurnInternal("Rafi internal initialization only. Do not call tools or modify files. Reply briefly that the context is ready.", { purpose: "initialization", responseOnly: true });
538
+ const descriptor = providerWireContract(initializationOutput, "codex");
539
+ const instruction = renderProviderWireInstruction("Rafi internal initialization only. Do not call tools or modify files. Submit the native session-initialization result with completed payload {ready:true} and null continuity.", descriptor);
540
+ const session = this.sessionRef();
541
+ if (!session)
542
+ throw new Error("Ephemeral Codex initialization requires a prepared session");
543
+ const id = randomUUID();
544
+ const tuple = selectedDispatchTupleV2({ operation: initializationOutput.operation, schemaVersion: initializationOutput.schemaVersion, schemaDigest: initializationOutput.schemaDigest,
545
+ provider: "codex", profile: descriptor.profile, variant: descriptor.variant, descriptorDigest: descriptor.descriptorDigest,
546
+ wireSchemaDigest: descriptor.wireSchemaDigest, guidanceDigest: descriptor.guidanceDigest, normalizerId: descriptor.normalizerId,
547
+ normalizerVersion: descriptor.normalizerVersion, maximumSchemaBytes: descriptor.maximumSchemaBytes, maximumResponseBytes: descriptor.maximumResponseBytes,
548
+ maximumPageBytes: descriptor.maximumPageBytes ?? null, instructionDigest: structuredDigest(instruction), actionId: id, dispatchId: id, sessionId: session.sessionId, generation: session.generation });
549
+ const setup = await withEphemeralInitializationDispatch(this.opts.configRoot ?? this.opts.cwd, tuple, dispatchCapability => this.sendTurn(instruction, { purpose: "initialization", responseOnly: true, output: initializationOutput, dispatchId: id, logicalActionId: id, wireContract: descriptor,
550
+ renderedWireContractDigest: descriptor.descriptorDigest, selectedDispatch: tuple, dispatchCapability }), session);
441
551
  if (setup.isError)
442
552
  throw new Error(`Codex automatic-compaction setup failed: ${setup.text.slice(0, 240)}`);
443
553
  if (this.observedToolCalls !== toolsBefore)
444
554
  throw new Error("Codex automatic-compaction setup unexpectedly called a tool");
555
+ if (!setup.selectedDispatch)
556
+ throw new Error("Codex initialization returned no sealed dispatch tuple");
557
+ const accepted = validateStructuredCandidate(initializationOutput, setup.structured, providerWireContract(initializationOutput, "codex"), setup.selectedDispatch);
558
+ if (accepted.result.kind !== "completed" || !accepted.result.payload.ready)
559
+ throw new Error("Codex initialization did not confirm readiness");
445
560
  }
446
561
  const maximum = this.usage?.maximum;
447
562
  if (!maximum || !Number.isFinite(maximum) || maximum <= 0) {
@@ -527,6 +642,8 @@ export class CodexAdapter {
527
642
  if (this.closed)
528
643
  return;
529
644
  this.closed = true;
645
+ if (this.turnInFlight && this.turnStreamedBytes)
646
+ this.writeStreamDiagnostic("interrupted", "adapter closed during an active turn");
530
647
  const child = this.process;
531
648
  let onExit;
532
649
  const exited = child && child.exitCode === null ? new Promise(resolve => { onExit = resolve; child.once("exit", resolve); }) : Promise.resolve();
@@ -716,7 +833,17 @@ export class CodexAdapter {
716
833
  if (!message.method)
717
834
  return;
718
835
  const params = message.params ?? {};
719
- if (message.method === "item/started") {
836
+ this.structuredCollector?.ingest(message.method, params);
837
+ // Native receipts and tool observations must belong to the same dispatch.
838
+ // Foreign notifications remain unable to affect completion or evidence.
839
+ if (this.structuredCollector && (params.threadId && params.threadId !== this._sessionId || this.activeNativeTurnId && params.turnId && params.turnId !== this.activeNativeTurnId))
840
+ return;
841
+ if (message.method === "item/agentMessage/delta") {
842
+ if (!this.turnInFlight || params.threadId && this._sessionId && params.threadId !== this._sessionId)
843
+ return;
844
+ this.observeAgentMessageDelta(params);
845
+ }
846
+ else if (message.method === "item/started") {
720
847
  const item = params.item;
721
848
  if (item)
722
849
  this.eventQueue.push({ kind: "provider-item", provider: "codex", lifecycle: "started", itemType: String(item.type ?? "unknown"), payload: exactProviderItem(item), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
@@ -904,6 +1031,54 @@ export class CodexAdapter {
904
1031
  }
905
1032
  });
906
1033
  }
1034
+ /** Ask the app-server to stop a native turn; true only once its terminal is observed. */
1035
+ async interruptNativeTurn(nativeTurnId) {
1036
+ const stopped = this.waitFor("turn/completed", params => params.threadId === this._sessionId && params.turn?.id === nativeTurnId, 15_000)
1037
+ .then(() => true, () => false);
1038
+ try {
1039
+ await new OperationDeadline("Codex turn interrupt", 10_000).run(() => this.request("turn/interrupt", { threadId: this._sessionId, turnId: nativeTurnId }));
1040
+ }
1041
+ catch (error) {
1042
+ this.traceLifecycle("turn-interrupt-failed", { method: error instanceof Error ? error.message.slice(0, 200) : "unknown" });
1043
+ }
1044
+ return stopped;
1045
+ }
1046
+ /** Streamed output is visible progress; it is never treated as a result. */
1047
+ observeAgentMessageDelta(params) {
1048
+ if (typeof params.delta !== "string")
1049
+ return;
1050
+ this.turnStreamedBytes += Buffer.byteLength(params.delta);
1051
+ const now = Date.now();
1052
+ if (!this.streamStartedAt)
1053
+ this.streamStartedAt = now;
1054
+ if (now - this.streamActivityAt >= 1_000) {
1055
+ this.streamActivityAt = now;
1056
+ this.eventQueue.push({ kind: "activity", provider: "codex", state: "writing response", detail: `${formatStreamBytes(this.turnStreamedBytes)} streamed`, transient: true });
1057
+ }
1058
+ if (now - (this.streamDiagnosticAt || this.streamStartedAt) >= 2_000)
1059
+ this.writeStreamDiagnostic("streaming");
1060
+ }
1061
+ /**
1062
+ * Bounded interruption evidence for structured turns under .foreman (Rafi's
1063
+ * git-excluded log directory). Diagnostic only: never read back as output.
1064
+ */
1065
+ writeStreamDiagnostic(state, detail) {
1066
+ const collector = this.structuredCollector, nativeTurnId = this.activeNativeTurnId, threadId = this._sessionId;
1067
+ if (!collector || !nativeTurnId || !threadId)
1068
+ return;
1069
+ this.streamDiagnosticAt = Date.now();
1070
+ try {
1071
+ const safe = (value) => value.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 120);
1072
+ const dir = join(this.opts.configRoot ?? this.opts.cwd, ".foreman", "provider-streams");
1073
+ mkdirSync(dir, { recursive: true });
1074
+ const path = join(dir, `codex-${safe(threadId)}-${safe(nativeTurnId)}.json`);
1075
+ const body = { version: 1, provider: "codex", state, threadId, nativeTurnId, dispatchId: this.activeProviderTurnId, updatedAt: new Date().toISOString(),
1076
+ ...(detail ? { detail: Buffer.from(detail).subarray(0, 2_000).toString() } : {}), ...collector.streamDiagnostic() };
1077
+ writeFileSync(`${path}.tmp`, JSON.stringify(body, null, 2));
1078
+ renameSync(`${path}.tmp`, path);
1079
+ }
1080
+ catch { /* diagnostics cannot change execution */ }
1081
+ }
907
1082
  traceLifecycle(phase, data = {}) {
908
1083
  try {
909
1084
  this.opts.onLifecycleTrace?.({ phase, at: new Date().toISOString(), sessionId: this._sessionId, ...data });
@@ -916,9 +1091,13 @@ export class CodexAdapter {
916
1091
  waiter.touch?.();
917
1092
  }
918
1093
  providerIdleTimeoutMs() {
919
- const configured = this.opts.providerIdleTimeoutMs;
1094
+ const configured = this.opts.providerIdleTimeoutMs ?? Number(process.env.RAFI_PROVIDER_IDLE_TIMEOUT_MS);
920
1095
  return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_PROVIDER_IDLE_TIMEOUT_MS;
921
1096
  }
1097
+ /** Read-only QA may reach the network only with the operator's --qa-network grant. */
1098
+ qaNetworkAccess() {
1099
+ return this.opts.sessionRole === "qa" && this.opts.networkAccess === true;
1100
+ }
922
1101
  effectiveNetworkAccess() {
923
1102
  return this.opts.sandboxMode !== "read-only" && this.opts.sessionRole !== "qa" && this.opts.networkAccess === true;
924
1103
  }
@@ -973,6 +1152,7 @@ export class CodexAdapter {
973
1152
  }
974
1153
  }
975
1154
  disconnect(error) {
1155
+ this.structuredCollector?.fail(error);
976
1156
  this.connectionGeneration++;
977
1157
  for (const controller of this.approvalRequests.values())
978
1158
  controller.abort();
@@ -1088,3 +1268,6 @@ function loadSkillMarkdown(cwd, skill) {
1088
1268
  function cumulativeDelta(before, after) {
1089
1269
  return before !== undefined && after !== undefined && after >= before ? after - before : undefined;
1090
1270
  }
1271
+ function formatStreamBytes(bytes) {
1272
+ return bytes < 1024 ? `${bytes} B` : bytes < 1_048_576 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1_048_576).toFixed(1)} MB`;
1273
+ }
@@ -18,6 +18,8 @@ export interface RecoveringAdapterOptions {
18
18
  /** Adds interactive recovery around structured runtime turn failures. */
19
19
  export declare class RecoveringAdapter implements BuilderAdapter {
20
20
  private readonly opts;
21
+ postConfirmedDispatch(reference: import("../adapters/types.js").ConfirmedDispatchReference): Promise<TurnResult | void>;
22
+ structuredDispatchRequirement(): import("../adapters/types.js").StructuredDispatchRequirement | undefined;
21
23
  private adapter;
22
24
  private runtime;
23
25
  private readonly eventQueue;
@@ -5,6 +5,8 @@ import { randomUUID } from "node:crypto";
5
5
  /** Adds interactive recovery around structured runtime turn failures. */
6
6
  export class RecoveringAdapter {
7
7
  opts;
8
+ postConfirmedDispatch(reference) { return this.adapter.postConfirmedDispatch?.(reference) ?? Promise.resolve(); }
9
+ structuredDispatchRequirement() { return this.adapter.structuredDispatchRequirement?.(); }
8
10
  adapter;
9
11
  runtime;
10
12
  eventQueue = new AsyncQueue();
@@ -45,7 +47,9 @@ export class RecoveringAdapter {
45
47
  this.eventQueue.push({ kind: "error", message: `failed to persist scoped provider session binding: ${error instanceof Error ? error.message : String(error)}` });
46
48
  }
47
49
  }
48
- if (policy?.handback || !this.opts.enabled || !result.isError || !result.failure)
50
+ // Structured operations have one recovery owner above the adapter. Never
51
+ // hide a second actual dispatch behind the same request/receipt identity.
52
+ if (policy?.output || policy?.handback || !this.opts.enabled || !result.isError || !result.failure)
49
53
  return result;
50
54
  // The host cannot know whether a missing exact session received this
51
55
  // instruction. Never replay it or silently switch providers.
@@ -0,0 +1,71 @@
1
+ import { providerWireContract, type OutputContract, type SelectedDispatchTupleV2, type StructuredResponse } from "rafi-spec";
2
+ export declare function structuredReceipt(contract: OutputContract, provider: "claude" | "codex", value: unknown, identity: {
3
+ dispatchId: string;
4
+ sessionId: string;
5
+ nativeTurnId?: string;
6
+ nativeItemId?: string;
7
+ }, representation: "json-text" | "decoded-json", selected: ReturnType<typeof providerWireContract>, tuple: SelectedDispatchTupleV2): StructuredResponse;
8
+ /** Pretty-printed JSON never needs this much consecutive whitespace. */
9
+ export declare const CODEX_RUNAWAY_WHITESPACE_BYTES = 2048;
10
+ /** The provider is emitting a runaway whitespace sequence; its output can never be accepted. */
11
+ export declare class CodexRunawayOutputError extends Error {
12
+ readonly itemId: string;
13
+ readonly whitespaceBytes: number;
14
+ constructor(itemId: string, whitespaceBytes: number);
15
+ }
16
+ /** Bounded, non-authoritative view of one streamed agent message. */
17
+ export interface CodexStreamItemDiagnostic {
18
+ itemId: string;
19
+ bytes: number;
20
+ deltas: number;
21
+ completed: boolean;
22
+ whitespaceRun: number;
23
+ head: string;
24
+ tail: string;
25
+ textRetained: boolean;
26
+ startedAt: string;
27
+ lastDeltaAt: string;
28
+ }
29
+ export interface CodexStreamDiagnostic {
30
+ authoritative: false;
31
+ streamedBytes: number;
32
+ deltas: number;
33
+ items: CodexStreamItemDiagnostic[];
34
+ }
35
+ /** Holds early notifications until turn/start supplies the exact turn identity. */
36
+ export declare class CodexTurnCollector {
37
+ private readonly threadId;
38
+ private readonly requirePhase;
39
+ private readonly onProgress?;
40
+ private readonly maximumResponseBytes;
41
+ private turnId?;
42
+ private early;
43
+ private bytes;
44
+ private finals;
45
+ private streams;
46
+ private streamedBytes;
47
+ private streamedDeltas;
48
+ private earlyDeltaBytes;
49
+ private terminal?;
50
+ private error?;
51
+ tools: boolean;
52
+ private resolve;
53
+ private reject;
54
+ readonly completion: Promise<Record<string, unknown>>;
55
+ constructor(threadId: string, requirePhase: boolean, onProgress?: (() => void) | undefined, maximumResponseBytes?: number);
56
+ bind(turnId: string): void;
57
+ ingest(method: string, params: Record<string, unknown>): void;
58
+ /**
59
+ * Streamed text is bounded before completion, but never becomes a result:
60
+ * only a completed final item is accepted and charged against the budget.
61
+ */
62
+ private ingestDelta;
63
+ streamDiagnostic(): CodexStreamDiagnostic;
64
+ fail(error: Error): void;
65
+ private invalidate;
66
+ private settle;
67
+ final(): {
68
+ text: string;
69
+ itemId?: string;
70
+ };
71
+ }