ai-foreman 1.7.20 → 1.7.21

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 (189) hide show
  1. package/README.md +12 -2
  2. package/dist/activity.d.ts +3 -0
  3. package/dist/activity.js +18 -6
  4. package/dist/adapters/claude.d.ts +16 -0
  5. package/dist/adapters/claude.js +28 -3
  6. package/dist/adapters/codex.d.ts +20 -0
  7. package/dist/adapters/codex.js +99 -5
  8. package/dist/adapters/recovering.d.ts +9 -0
  9. package/dist/adapters/recovering.js +10 -0
  10. package/dist/adapters/types.d.ts +15 -0
  11. package/dist/agentRun.d.ts +7 -0
  12. package/dist/agentRun.js +24 -13
  13. package/dist/branch/currentGuard.d.ts +1 -0
  14. package/dist/branch/currentGuard.js +1 -0
  15. package/dist/branch/finalization.js +6 -1
  16. package/dist/branch/git.d.ts +1 -0
  17. package/dist/branch/git.js +15 -1
  18. package/dist/branch/runner.d.ts +2 -0
  19. package/dist/branch/runner.js +90 -4
  20. package/dist/buildAdmission.js +2 -0
  21. package/dist/buildAssignment.d.ts +33 -0
  22. package/dist/buildAssignment.js +215 -0
  23. package/dist/buildInterventions.d.ts +90 -0
  24. package/dist/buildInterventions.js +334 -0
  25. package/dist/buildOwnershipReconciliation.d.ts +19 -0
  26. package/dist/buildOwnershipReconciliation.js +41 -0
  27. package/dist/buildOwnershipRepair.d.ts +33 -0
  28. package/dist/buildOwnershipRepair.js +111 -0
  29. package/dist/buildRuns.d.ts +6 -0
  30. package/dist/buildRuns.js +23 -17
  31. package/dist/buildSyntheticWork.d.ts +2 -0
  32. package/dist/buildSyntheticWork.js +30 -0
  33. package/dist/buildWorkAdmission.d.ts +17 -0
  34. package/dist/buildWorkAdmission.js +57 -0
  35. package/dist/buildWorkAuthorization.d.ts +2 -0
  36. package/dist/buildWorkAuthorization.js +23 -0
  37. package/dist/buildWorkContext.d.ts +3 -0
  38. package/dist/buildWorkContext.js +6 -0
  39. package/dist/buildWorkMigration.d.ts +9 -0
  40. package/dist/buildWorkMigration.js +206 -0
  41. package/dist/buildWorkReconciliation.d.ts +12 -0
  42. package/dist/buildWorkReconciliation.js +70 -0
  43. package/dist/builderGuidanceFollowup.d.ts +15 -0
  44. package/dist/builderGuidanceFollowup.js +34 -0
  45. package/dist/cli/doctor.js +3 -2
  46. package/dist/cli/graph.d.ts +2 -0
  47. package/dist/cli/graph.js +75 -0
  48. package/dist/cli/manager.d.ts +8 -0
  49. package/dist/cli/manager.js +234 -12
  50. package/dist/cli/recovery.js +3 -2
  51. package/dist/cli/runtimeAuthPrompt.js +28 -8
  52. package/dist/cli/start.js +152 -48
  53. package/dist/cli/status.js +20 -5
  54. package/dist/cli/tickets.js +9 -7
  55. package/dist/continuity.d.ts +11 -0
  56. package/dist/continuity.js +22 -6
  57. package/dist/foreman.d.ts +10 -2
  58. package/dist/foreman.js +330 -39
  59. package/dist/graph/boundary.d.ts +15 -0
  60. package/dist/graph/boundary.js +50 -0
  61. package/dist/graph/bridge.d.ts +9 -0
  62. package/dist/graph/bridge.js +146 -0
  63. package/dist/graph/bridge.py +205 -0
  64. package/dist/graph/config.d.ts +11 -0
  65. package/dist/graph/config.js +40 -0
  66. package/dist/graph/corpus.d.ts +16 -0
  67. package/dist/graph/corpus.js +188 -0
  68. package/dist/graph/derived.d.ts +17 -0
  69. package/dist/graph/derived.js +73 -0
  70. package/dist/graph/host.d.ts +4 -0
  71. package/dist/graph/host.js +160 -0
  72. package/dist/graph/install.d.ts +3 -0
  73. package/dist/graph/install.js +50 -0
  74. package/dist/graph/lifecycle.d.ts +11 -0
  75. package/dist/graph/lifecycle.js +124 -0
  76. package/dist/graph/maintenance.d.ts +43 -0
  77. package/dist/graph/maintenance.js +301 -0
  78. package/dist/graph/manager.d.ts +9 -0
  79. package/dist/graph/manager.js +92 -0
  80. package/dist/graph/preparation.d.ts +41 -0
  81. package/dist/graph/preparation.js +43 -0
  82. package/dist/graph/read.d.ts +27 -0
  83. package/dist/graph/read.js +152 -0
  84. package/dist/graph/semantic.d.ts +17 -0
  85. package/dist/graph/semantic.js +27 -0
  86. package/dist/graph/session.d.ts +8 -0
  87. package/dist/graph/session.js +92 -0
  88. package/dist/graph/sources.d.ts +4 -0
  89. package/dist/graph/sources.js +11 -0
  90. package/dist/graph/storage.d.ts +38 -0
  91. package/dist/graph/storage.js +159 -0
  92. package/dist/graph/turn.d.ts +17 -0
  93. package/dist/graph/turn.js +360 -0
  94. package/dist/graph/util.d.ts +5 -0
  95. package/dist/graph/util.js +51 -0
  96. package/dist/handoffs.d.ts +1 -0
  97. package/dist/handoffs.js +5 -1
  98. package/dist/humanDecision.d.ts +21 -2
  99. package/dist/humanDecision.js +169 -11
  100. package/dist/index.js +2 -0
  101. package/dist/installOwnership.d.ts +46 -0
  102. package/dist/installOwnership.js +440 -0
  103. package/dist/linuxProbeInventory.d.ts +12 -0
  104. package/dist/linuxProbeInventory.js +48 -0
  105. package/dist/log.d.ts +1 -1
  106. package/dist/log.js +7 -1
  107. package/dist/managerEvidence.d.ts +25 -0
  108. package/dist/managerEvidence.js +326 -0
  109. package/dist/managerEvidenceArtifacts.d.ts +38 -0
  110. package/dist/managerEvidenceArtifacts.js +118 -0
  111. package/dist/managerPacket.d.ts +2 -0
  112. package/dist/managerPacket.js +23 -0
  113. package/dist/markers.d.ts +1 -1
  114. package/dist/markers.js +2 -2
  115. package/dist/observability.js +1 -1
  116. package/dist/processIdentity.js +7 -0
  117. package/dist/providerPhase.d.ts +13 -0
  118. package/dist/providerPhase.js +26 -0
  119. package/dist/qaAuthorizedDisposition.d.ts +24 -0
  120. package/dist/qaAuthorizedDisposition.js +26 -0
  121. package/dist/qaBuildGate.d.ts +4 -0
  122. package/dist/qaBuildGate.js +216 -0
  123. package/dist/qaBuilderCoverage.d.ts +5 -0
  124. package/dist/qaBuilderCoverage.js +78 -0
  125. package/dist/qaContractCoverage.d.ts +10 -0
  126. package/dist/qaContractCoverage.js +55 -0
  127. package/dist/qaContractDelivery.d.ts +23 -0
  128. package/dist/qaContractDelivery.js +145 -0
  129. package/dist/qaContractFreshness.d.ts +15 -0
  130. package/dist/qaContractFreshness.js +36 -0
  131. package/dist/qaDeliveryJournal.d.ts +10 -0
  132. package/dist/qaDeliveryJournal.js +16 -1
  133. package/dist/qaEffectiveConfig.d.ts +20 -0
  134. package/dist/qaEffectiveConfig.js +40 -0
  135. package/dist/qaEvidenceReader.d.ts +14 -0
  136. package/dist/qaEvidenceReader.js +176 -0
  137. package/dist/qaFailureDelivery.d.ts +2 -0
  138. package/dist/qaFailureDelivery.js +107 -10
  139. package/dist/qaHandbackMigration.d.ts +1 -1
  140. package/dist/qaHandbackMigration.js +3 -3
  141. package/dist/qaPreparation.d.ts +26 -0
  142. package/dist/qaPreparation.js +150 -0
  143. package/dist/qaPreparationInputs.d.ts +8 -0
  144. package/dist/qaPreparationInputs.js +47 -0
  145. package/dist/qaPreparationMetrics.d.ts +145 -0
  146. package/dist/qaPreparationMetrics.js +120 -0
  147. package/dist/qaPreparationPolicy.d.ts +5 -0
  148. package/dist/qaPreparationPolicy.js +51 -0
  149. package/dist/qaPreparationStore.d.ts +157 -0
  150. package/dist/qaPreparationStore.js +412 -0
  151. package/dist/qaProtocolV2.d.ts +28 -0
  152. package/dist/qaProtocolV2.js +12 -0
  153. package/dist/qaReview.d.ts +6 -0
  154. package/dist/qaReview.js +234 -24
  155. package/dist/qaTimeline.d.ts +18 -0
  156. package/dist/qaTimeline.js +123 -0
  157. package/dist/qaVerificationContract.d.ts +17 -0
  158. package/dist/qaVerificationContract.js +236 -0
  159. package/dist/readinessCleanup.js +12 -5
  160. package/dist/recoveryGuidance.d.ts +9 -0
  161. package/dist/recoveryGuidance.js +13 -0
  162. package/dist/runtimeAuth.d.ts +1 -0
  163. package/dist/runtimeAuth.js +5 -1
  164. package/dist/runtimeReadiness.d.ts +1 -0
  165. package/dist/runtimeReadiness.js +13 -1
  166. package/dist/sessionIdentity.d.ts +2 -0
  167. package/dist/sessionIdentity.js +14 -0
  168. package/dist/sessionLifecycle.js +3 -3
  169. package/dist/sources/sourceRegistry.js +7 -0
  170. package/dist/stateTransfer.js +50 -1
  171. package/dist/statusReporter.d.ts +1 -0
  172. package/dist/statusReporter.js +1 -0
  173. package/dist/supervisedStart.js +18 -8
  174. package/dist/terminalStyle.d.ts +21 -0
  175. package/dist/terminalStyle.js +50 -0
  176. package/dist/ticketPopulation.d.ts +1 -0
  177. package/dist/ticketPopulation.js +5 -1
  178. package/dist/tickets/commands.js +2 -2
  179. package/dist/tickets/queue.js +1 -1
  180. package/dist/tickets/ticketSchema.d.ts +198 -0
  181. package/dist/tickets/ticketSchema.js +2 -0
  182. package/dist/workflowDb.d.ts +51 -5
  183. package/dist/workflowDb.js +338 -7
  184. package/dist/workflowReader.d.ts +109 -2
  185. package/dist/workflowReader.js +48 -4
  186. package/package.json +84 -7
  187. package/scripts/copy-graph-assets.mjs +3 -0
  188. package/scripts/graph-benchmark.mjs +20 -0
  189. package/scripts/graph-evaluation.mjs +85 -0
package/README.md CHANGED
@@ -131,7 +131,7 @@ Provider sessions are historical after import. Resume with `rafi build:resume <p
131
131
  | `--branch-prefix <prefix>` | `feature` / `rafi` / another valid Git prefix | `feature` | Prefix for generated ticket branches. Explicit prefixes are preserved verbatim. |
132
132
  | `--max-branch-depth <n>` | positive integer | `5` | Maximum selected branch/PR stack depth (root PR is depth 1). |
133
133
  | `--show-session-cost` / `--hide-session-cost` | flags | saved role preferences | Show authoritative provider cost or trustworthy cumulative tokens for Builder and QA; does not change context occupancy display. |
134
- | `--auto-compact-threshold <percent>` | integer `1`–`99` | saved Builder value, then `50` | Initial run threshold override. A newer persistent settings revision wins at the next safe boundary. |
134
+ | `--auto-compact-threshold <percent>` | integer `1`–`99` | saved Builder value, then `65` | Initial run threshold override. A newer persistent settings revision wins at the next safe boundary. |
135
135
 
136
136
  Auto-detected tracker files:
137
137
 
@@ -182,7 +182,7 @@ Owner-only recovery packets under `.foreman/qa-report-recovery/` bind the review
182
182
 
183
183
  ### `ai-foreman manager`
184
184
 
185
- Manager is a project-wide, read-only diagnostic conversation over every retained build run. It starts with a verified live run when one exists, otherwise the most recently updated run; `--run <id>` changes only that initial focus. Each question refreshes the catalog, active state, deterministic aggregates, and relevant run details.
185
+ Manager provides project-wide diagnostic conversations and explicitly authorized, scoped controls over retained build runs. It starts with a verified live run when one exists, otherwise the most recently updated run; `--run <id>` changes only that initial focus. Each question refreshes the catalog, active state, deterministic aggregates, and relevant run details.
186
186
 
187
187
  Permanent sanitized run summaries keep timing, counts, provider/model configuration, usage coverage, failure category, and terminal Git facts useful after detailed spans expire. Detailed spans retain the configured 30-day policy. Missing historical facts are reported as partial rather than reconstructed or treated as zero.
188
188
 
@@ -193,6 +193,16 @@ ai-foreman manager ./my-project
193
193
  ai-foreman manager ./my-project --run <run-id> --ask "Compare this run with the last five successful runs"
194
194
  ```
195
195
 
196
+ Full QA history is available without a provider through `--ask '/qa-work <run>'`, `/qa-attempts <run> <work>`, `/qa-report <run> <work> <attempt> [occurrence]`, and `/qa-timeline <run> <work>`. One-shot output expands all pinned pages and artifacts before exiting, independently of bounded model packets. `/qa-export <run> <work> <attempt> [occurrence]` exports exact protected report bytes with their identity and digest. Interactive `/more <cursor>` and `/artifact <handle>` continuations belong to that session; a later invocation retrieves fresh evidence. Reads do not migrate workflow state.
197
+
198
+ Explicit `/guide-builder`, `/guide-qa`, and `/guide-both` commands take a run, work ID, and exact advice. The owning process delivers one-use guidance at its next safe boundary. `/pause <run> <work> [run]` waits for the active turn to finish; withdraw the pause to allow continuation. `/request-attempt <run> <work> <reason>` grants one existing-ledger attempt authorization for the current failed review or waiting Builder guidance after an unfinalized pass. Guidance alone never grants attempts. Inspect truthful delivery and verification receipts with `/qa-instruction <run> <work> <instruction>`.
199
+
200
+ If QA passes while Builder guidance is waiting, explicit resume applies that guidance within the existing approved ticket scope and remaining attempt allowance, then performs a complete fresh QA review. This works on the current branch and in isolated ticket worktrees. The follow-up consumes the same durable allowance as QA remediation; neither a fresh review nor a restart resets it. When the allowance is exhausted, authorize one scoped follow-up with `/request-attempt`, then resume. Changed requirements need renewed approval, and uncertain submissions require reconciliation before another Builder turn. Saving guidance alone does not start work.
201
+
202
+ Ownership schema 4 requires admitted work before Builder/QA dispatch. Legacy QA records are retained in quarantine when authorization cannot be proved. Both resume aliases assess conflicts before selecting recovery mode. Use `/qa-conflicts <run>` and `/qa-repair-plan <run> <work>` to inspect the source mapping, then submit an explicit `/qa-repair <JSON>` decision with the displayed revision, source digest, approved scope, complete path mapping, and operator attestation. Stop the existing owner first. Repairs preserve source and historical packets and require fresh verification; uncertain provider dispatch needs separate reconciliation.
203
+
204
+ The upgrade stores a coherent database backup and recovery packet copy under `.rafi/backups/work-admission-v4/<database digest>/` before committing its guards. Rollback means pausing execution and preserving newer evidence for forward repair. Older writers are rejected against upgraded databases; do not down-migrate or restore a backup without accounting for subsequent provider work.
205
+
196
206
  ### GitHub PR Failure Recovery
197
207
 
198
208
  When `--create-pr` cannot prepare GitHub, push a branch, or create a PR, Foreman records a structured failure and blocks the affected ticket cleanly. Blocked worktrees are retained automatically so you can repair the environment and retry without losing the ticket session.
@@ -14,6 +14,8 @@ export interface ActivityReporterOptions {
14
14
  usefulProgressWarningMs?: number;
15
15
  /** Override the safe automatic TTY rendering mode for tests or embedding hosts. */
16
16
  ttyMode?: ActivityTtyMode;
17
+ /** Environment used for color policy; useful for isolated hosts and tests. */
18
+ colorEnvironment?: NodeJS.ProcessEnv;
17
19
  }
18
20
  export type ActivityTtyMode = "auto" | "cursor" | "records";
19
21
  /** One process-local live view shared by nested RAFI operations. */
@@ -30,6 +32,7 @@ export declare class ActivityReporter {
30
32
  private usefulProgressWarningPrinted;
31
33
  private pauseStartedAt;
32
34
  private readonly ttyMode;
35
+ private readonly colorEnvironment;
33
36
  private readonly commandStartedAt;
34
37
  private readonly phases;
35
38
  private nextPhaseId;
package/dist/activity.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { AsyncQueue } from "./util/asyncQueue.js";
3
+ import { accent, error, info, sanitizeTerminalText, success, warning } from "./terminalStyle.js";
3
4
  const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
4
5
  /** One process-local live view shared by nested RAFI operations. */
5
6
  export class ActivityReporter {
@@ -15,6 +16,7 @@ export class ActivityReporter {
15
16
  usefulProgressWarningPrinted = false;
16
17
  pauseStartedAt = 0;
17
18
  ttyMode;
19
+ colorEnvironment;
18
20
  commandStartedAt;
19
21
  phases = new Map();
20
22
  nextPhaseId = 1;
@@ -43,6 +45,7 @@ export class ActivityReporter {
43
45
  this.quietWarningMs = options.quietWarningMs ?? 60_000;
44
46
  this.usefulProgressWarningMs = options.usefulProgressWarningMs ?? 300_000;
45
47
  this.ttyMode = resolveTtyMode(options.ttyMode);
48
+ this.colorEnvironment = options.colorEnvironment ?? process.env;
46
49
  this.commandStartedAt = this.now();
47
50
  }
48
51
  begin(label) {
@@ -94,15 +97,16 @@ export class ActivityReporter {
94
97
  }
95
98
  note(message) {
96
99
  this.clearLine();
97
- this.output.write(`${clean(message, 500)}\n`);
100
+ this.output.write(`${sanitizeTerminalText(message).replace(/[\r\t]+/g, " ").slice(0, 500)}\n`);
98
101
  this.render(true);
99
102
  }
100
103
  writePersistent(text) {
101
104
  if (!text)
102
105
  return;
103
106
  this.clearLine();
104
- this.output.write(text);
105
- if (!text.endsWith("\n"))
107
+ const safeText = sanitizeTerminalText(text);
108
+ this.output.write(safeText);
109
+ if (!safeText.endsWith("\n"))
106
110
  this.output.write("\n");
107
111
  this.render(true);
108
112
  }
@@ -197,7 +201,14 @@ export class ActivityReporter {
197
201
  const quiet = quietFor >= this.quietWarningMs ? `provider quiet ${formatDuration(quietFor)}; RAFI is responsive` : this.detail;
198
202
  const body = [phase?.label, provider || undefined, phase ? quiet : undefined, this.agentStatusLine].filter(Boolean).join(" — ");
199
203
  if (this.output.isTTY) {
200
- const line = `${FRAMES[this.frame++ % FRAMES.length]} RAFI working: ${body} (${formatDuration(now - this.commandStartedAt)})`;
204
+ const color = { stream: this.output, env: this.colorEnvironment };
205
+ const status = phase?.label.toLowerCase() ?? "";
206
+ const marker = status.includes("fail") || status.includes("error") ? error("!", color)
207
+ : status.includes("complete") || status.includes("success") ? success("✓", color)
208
+ : status.includes("warn") || status.includes("quiet") ? warning("!", color)
209
+ : info(FRAMES[this.frame++ % FRAMES.length], color);
210
+ const safeBody = sanitizeTerminalText(body);
211
+ const line = `${marker} ${accent("RAFI", color)} working: ${safeBody} (${formatDuration(now - this.commandStartedAt)})`;
201
212
  const semanticKey = semanticStatusKey(body);
202
213
  if (this.ttyMode === "records") {
203
214
  if (semanticKey === this.lastSemanticKey)
@@ -223,7 +234,8 @@ export class ActivityReporter {
223
234
  return;
224
235
  if (force || now - this.lastHeartbeatAt >= this.heartbeatMs) {
225
236
  this.lastHeartbeatAt = now;
226
- this.output.write(`[${new Date(now).toISOString()}] rafi working: ${body} (${formatDuration(now - this.commandStartedAt)})\n`);
237
+ const color = { stream: this.output, env: this.colorEnvironment };
238
+ this.output.write(`[${new Date(now).toISOString()}] ${accent("rafi", color)} working: ${sanitizeTerminalText(body)} (${formatDuration(now - this.commandStartedAt)})\n`);
227
239
  this.lastSemanticKey = semanticKey;
228
240
  }
229
241
  }
@@ -341,7 +353,7 @@ function isFailedToolCompletion(event) {
341
353
  || (typeof event.exitCode === "number" && Number.isFinite(event.exitCode) && event.exitCode !== 0);
342
354
  }
343
355
  function clean(value, maximum) {
344
- return value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maximum);
356
+ return sanitizeTerminalText(value).replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maximum);
345
357
  }
346
358
  function semanticStatusKey(body) {
347
359
  return body
@@ -1,3 +1,4 @@
1
+ import { ProviderPhaseBarrier, type ProviderTurnPurpose } from "../providerPhase.js";
1
2
  import type { SDKSessionInfo, PermissionResult } from "@anthropic-ai/claude-agent-sdk";
2
3
  /** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
3
4
  export declare function requireClaudeSDK(): Promise<typeof import("@anthropic-ai/claude-agent-sdk")>;
@@ -43,6 +44,12 @@ export declare function claudeApiRetryEvent(message: {
43
44
  export declare class ClaudeAdapter implements BuilderAdapter {
44
45
  private readonly opts;
45
46
  readonly agent: "claude";
47
+ graphRuntimeSettings(): {
48
+ model: string | undefined;
49
+ effort: import("./types.js").EffortLevel | undefined;
50
+ fast: boolean | undefined;
51
+ runtimeExecutable: string | undefined;
52
+ };
46
53
  private readonly inbox;
47
54
  private readonly eventQueue;
48
55
  private readonly query;
@@ -74,6 +81,14 @@ export declare class ClaudeAdapter implements BuilderAdapter {
74
81
  private pending?;
75
82
  private questionTrace?;
76
83
  private responseOnlyTurn;
84
+ readonly phaseBarrier: ProviderPhaseBarrier;
85
+ contractCapabilities(): {
86
+ sameSessionAcceptance: boolean;
87
+ nativeCompactionBarrier: boolean;
88
+ };
89
+ enableContractEnforcement(): void;
90
+ contractCompactionSequence(): number;
91
+ acceptContractDelivery(sequence: number): void;
77
92
  private terminalResult?;
78
93
  private streamEnded;
79
94
  private closed;
@@ -86,6 +101,7 @@ export declare class ClaudeAdapter implements BuilderAdapter {
86
101
  handback?: boolean;
87
102
  responseOnly?: boolean;
88
103
  logicalActionId?: string;
104
+ purpose?: ProviderTurnPurpose;
89
105
  }): Promise<TurnResult>;
90
106
  private sendTurnInternal;
91
107
  private captureStderr;
@@ -1,3 +1,4 @@
1
+ import { ProviderPhaseBarrier } from "../providerPhase.js";
1
2
  import { OperationDeadline } from "../util/deadline.js";
2
3
  import { createHash, randomUUID } from "node:crypto";
3
4
  import { dirname, join } from "node:path";
@@ -27,6 +28,7 @@ const DEFAULT_PROVIDER_IDLE_TIMEOUT_MS = 30 * 60_000;
27
28
  */
28
29
  export function buildClaudeQueryOptions(opts) {
29
30
  const qaReadOnly = opts.sessionRole === "qa" || opts.sandboxMode === "read-only";
31
+ const confinedBuilder = opts.sessionRole === "builder";
30
32
  const base = {
31
33
  cwd: opts.cwd,
32
34
  pathToClaudeCodeExecutable: opts.runtimeExecutable,
@@ -41,18 +43,24 @@ export function buildClaudeQueryOptions(opts) {
41
43
  // settings in those roles.
42
44
  settingSources: qaReadOnly ? ["user"] : ["user", "project", "local"],
43
45
  ...(qaReadOnly ? { disallowedTools: ["Write", "Edit", "NotebookEdit"] } : {}),
46
+ ...(confinedBuilder && !qaReadOnly ? { sandbox: {
47
+ enabled: true, failIfUnavailable: true, autoAllowBashIfSandboxed: false, allowUnsandboxedCommands: false,
48
+ network: opts.networkAccess === true ? { allowedDomains: ["*"] } : { allowedDomains: [], deniedDomains: ["*"] },
49
+ filesystem: { allowWrite: [opts.cwd] },
50
+ } } : {}),
44
51
  ...(qaReadOnly ? { sandbox: {
45
52
  enabled: true,
46
53
  failIfUnavailable: true,
47
54
  autoAllowBashIfSandboxed: true,
48
55
  allowUnsandboxedCommands: false,
56
+ network: { allowedDomains: [], deniedDomains: ["*"] },
49
57
  filesystem: { allowWrite: [join(dirname(opts.cwd), "scratch")], denyWrite: [opts.cwd, ...(opts.configRoot ? [opts.configRoot] : [])] },
50
58
  } } : {}),
51
59
  };
52
60
  const exactSkills = opts.preloadedSkillContent?.length
53
61
  ? ["# Preloaded Skills", "Use the following skills for this run.", ...opts.preloadedSkillContent.map((skill) => skill.content)].join("\n\n")
54
62
  : undefined;
55
- const systemAppend = [opts.systemPromptAppend, exactSkills].filter((part) => Boolean(part)).join("\n\n");
63
+ const systemAppend = [opts.systemPromptAppend, opts.sessionRole === "builder" ? `Builder runtime contract: shell network ${opts.networkAccess === true ? "explicitly approved for this build" : "disabled"}; sandbox fallback disabled. If acquisition requires unavailable access, request input first or use a provenance-verified local bundle. A ticket answer does not grant runtime permissions.` : undefined, exactSkills].filter((part) => Boolean(part)).join("\n\n");
56
64
  if (systemAppend) {
57
65
  base.systemPrompt = { type: "preset", preset: "claude_code", append: systemAppend };
58
66
  }
@@ -165,6 +173,7 @@ export function claudeApiRetryEvent(message) {
165
173
  export class ClaudeAdapter {
166
174
  opts;
167
175
  agent = "claude";
176
+ graphRuntimeSettings() { return { model: this.opts.model, effort: this.opts.effort, fast: this.opts.fast, runtimeExecutable: this.opts.runtimeExecutable }; }
168
177
  inbox = new AsyncQueue();
169
178
  eventQueue = new BuilderEventQueue();
170
179
  query;
@@ -196,6 +205,11 @@ export class ClaudeAdapter {
196
205
  pending;
197
206
  questionTrace;
198
207
  responseOnlyTurn = false;
208
+ phaseBarrier = new ProviderPhaseBarrier();
209
+ contractCapabilities() { return { sameSessionAcceptance: true, nativeCompactionBarrier: true }; }
210
+ enableContractEnforcement() { this.phaseBarrier.enableEnforcement(); }
211
+ contractCompactionSequence() { return this.phaseBarrier.compactionSequence; }
212
+ acceptContractDelivery(sequence) { this.phaseBarrier.accept(sequence); }
199
213
  terminalResult;
200
214
  streamEnded = false;
201
215
  closed = false;
@@ -239,6 +253,7 @@ export class ClaudeAdapter {
239
253
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
240
254
  constructor(opts, query) {
241
255
  this.opts = opts;
256
+ this.phaseBarrier.begin("initialization");
242
257
  this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
243
258
  this._sessionRef = opts.resumeSessionRef;
244
259
  this.sessionIdentityReady = new Promise((resolve, reject) => {
@@ -255,7 +270,15 @@ export class ClaudeAdapter {
255
270
  options: {
256
271
  ...buildClaudeQueryOptions(opts),
257
272
  abortController: this.abort,
258
- hooks: { SessionStart: [{ hooks: [async (input) => {
273
+ hooks: {
274
+ PreToolUse: [{ hooks: [async (input) => {
275
+ if (input.hook_event_name !== "PreToolUse")
276
+ return {};
277
+ const reason = this.phaseBarrier.denial(input.tool_name);
278
+ return reason ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } } : {};
279
+ }] }],
280
+ PreCompact: [{ hooks: [async () => { this.phaseBarrier.compact(); return {}; }] }],
281
+ SessionStart: [{ hooks: [async (input) => {
259
282
  if (input.hook_event_name !== "SessionStart" || input.agent_id)
260
283
  return {};
261
284
  try {
@@ -266,7 +289,8 @@ export class ClaudeAdapter {
266
289
  throw error;
267
290
  }
268
291
  return {};
269
- }] }] },
292
+ }] }]
293
+ },
270
294
  stderr: (data) => this.captureStderr(data),
271
295
  canUseTool: async (toolName, input, requestOptions = {}) => {
272
296
  if (this.responseOnlyTurn)
@@ -512,6 +536,7 @@ export class ClaudeAdapter {
512
536
  if (this.pending)
513
537
  throw new Error("a turn is already in progress");
514
538
  this.responseOnlyTurn = Boolean(policy?.responseOnly);
539
+ this.phaseBarrier.begin(policy?.purpose ?? (policy?.responseOnly ? "response-repair" : "implementation"));
515
540
  const turnId = randomUUID();
516
541
  this.activeProviderTurnId = turnId;
517
542
  const observer = this.opts.observer;
@@ -1,3 +1,4 @@
1
+ import type { ProviderTurnPurpose } from "../providerPhase.js";
1
2
  import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, CompactResult, ContextUsage, NativeAutoCompactionPolicy, NativeCompaction, ProviderSessionUsage, ProviderSettingSwitch, TurnResult } from "./types.js";
2
3
  import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
3
4
  export interface CodexLineResult {
@@ -11,6 +12,12 @@ export declare function parseCodexLine(raw: Record<string, unknown>): CodexLineR
11
12
  export declare class CodexAdapter implements BuilderAdapter {
12
13
  private readonly opts;
13
14
  readonly agent: "codex";
15
+ graphRuntimeSettings(): {
16
+ model: string | undefined;
17
+ effort: import("./types.js").EffortLevel | undefined;
18
+ fast: boolean | undefined;
19
+ runtimeExecutable: string | undefined;
20
+ };
14
21
  private readonly eventQueue;
15
22
  private process?;
16
23
  private nextId;
@@ -40,9 +47,17 @@ export declare class CodexAdapter implements BuilderAdapter {
40
47
  private compactionUncertain;
41
48
  private observedToolCalls;
42
49
  private activeProviderTurnId?;
50
+ private activeNativeTurnId?;
51
+ private connectionGeneration;
52
+ private readonly approvalRequests;
53
+ private readonly answeredApprovalRequests;
43
54
  private activeProviderTurnSpanId?;
44
55
  private readonly toolSpans;
45
56
  constructor(opts: BuilderAdapterOptions);
57
+ contractCapabilities(): {
58
+ sameSessionAcceptance: boolean;
59
+ nativeCompactionBarrier: boolean;
60
+ };
46
61
  buildInstruction(instruction: string): string;
47
62
  buildAppServerArgs(): string[];
48
63
  /** Deprecated fixture helper. Runtime execution uses only app-server. */
@@ -51,6 +66,7 @@ export declare class CodexAdapter implements BuilderAdapter {
51
66
  handback?: boolean;
52
67
  responseOnly?: boolean;
53
68
  logicalActionId?: string;
69
+ purpose?: ProviderTurnPurpose;
54
70
  }): Promise<TurnResult>;
55
71
  private sendTurnInternal;
56
72
  compact(): Promise<CompactResult>;
@@ -85,6 +101,10 @@ export declare class CodexAdapter implements BuilderAdapter {
85
101
  private traceLifecycle;
86
102
  private touchWaiters;
87
103
  private providerIdleTimeoutMs;
104
+ private effectiveNetworkAccess;
105
+ private effectiveApprovalPolicy;
106
+ /** Fail closed; approval is for this operation only, never a session cache or policy amendment. */
107
+ private handleServerRequest;
88
108
  private disconnect;
89
109
  private restartForAutoCompaction;
90
110
  private buildSkillsAppendix;
@@ -31,6 +31,7 @@ const DEFAULT_PROVIDER_IDLE_TIMEOUT_MS = 30 * 60_000;
31
31
  export class CodexAdapter {
32
32
  opts;
33
33
  agent = "codex";
34
+ graphRuntimeSettings() { return { model: this.opts.model, effort: this.opts.effort, fast: this.opts.fast, runtimeExecutable: this.opts.runtimeExecutable }; }
34
35
  eventQueue = new BuilderEventQueue();
35
36
  process;
36
37
  nextId = 1;
@@ -60,6 +61,10 @@ export class CodexAdapter {
60
61
  compactionUncertain = false;
61
62
  observedToolCalls = 0;
62
63
  activeProviderTurnId;
64
+ activeNativeTurnId;
65
+ connectionGeneration = 0;
66
+ approvalRequests = new Map();
67
+ answeredApprovalRequests = new Set();
63
68
  activeProviderTurnSpanId;
64
69
  toolSpans = new Map();
65
70
  constructor(opts) {
@@ -67,8 +72,11 @@ export class CodexAdapter {
67
72
  this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
68
73
  this._sessionRef = opts.resumeSessionRef;
69
74
  }
75
+ contractCapabilities() { return { sameSessionAcceptance: true, nativeCompactionBarrier: false }; }
70
76
  buildInstruction(instruction) {
71
- return [this.opts.systemPromptAppend, this.buildSkillsAppendix(), instruction]
77
+ return [this.opts.systemPromptAppend,
78
+ 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,
79
+ this.buildSkillsAppendix(), instruction]
72
80
  .filter((part) => Boolean(part)).join("\n\n");
73
81
  }
74
82
  buildAppServerArgs() {
@@ -102,6 +110,7 @@ export class CodexAdapter {
102
110
  }
103
111
  finally {
104
112
  this.activeProviderTurnId = undefined;
113
+ this.activeNativeTurnId = undefined;
105
114
  }
106
115
  }
107
116
  const context = this.observationContext();
@@ -123,6 +132,7 @@ export class CodexAdapter {
123
132
  this.toolSpans.delete(callId);
124
133
  }
125
134
  this.activeProviderTurnId = undefined;
135
+ this.activeNativeTurnId = undefined;
126
136
  this.activeProviderTurnSpanId = undefined;
127
137
  }
128
138
  }
@@ -140,13 +150,18 @@ export class CodexAdapter {
140
150
  this.turnUsageBaseline = this.providerSessionUsage ? { ...this.providerSessionUsage } : undefined;
141
151
  const completion = this.waitFor("turn/completed", (params) => params.threadId === this._sessionId, this.providerIdleTimeoutMs(), true);
142
152
  turnStartDispatched = true;
143
- await this.request("turn/start", {
153
+ const started = await this.request("turn/start", {
144
154
  threadId: this._sessionId,
145
155
  input: [{ type: "text", text: providerInstruction, text_elements: [] }],
146
156
  cwd: this.opts.cwd,
147
157
  model: this.opts.model ?? null,
148
158
  effort: this.opts.effort ?? (this.opts.fast ? "low" : null),
159
+ approvalPolicy: policy?.responseOnly || policy?.purpose && policy.purpose !== "implementation" ? "never" : this.effectiveApprovalPolicy(),
160
+ sandboxPolicy: policy?.responseOnly || policy?.purpose && policy.purpose !== "implementation" || this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa"
161
+ ? { type: "readOnly", networkAccess: false }
162
+ : { type: "workspaceWrite", writableRoots: [this.opts.cwd], networkAccess: this.effectiveNetworkAccess() },
149
163
  });
164
+ this.activeNativeTurnId = String(started.turn?.id ?? "") || undefined;
150
165
  const params = await new OperationDeadline("Codex active turn", this.opts.turnDeadlineMs ?? 3_600_000).run(() => completion);
151
166
  const turn = params.turn;
152
167
  const responseOnlyViolation = Boolean(policy?.responseOnly && this.observedToolCalls !== toolsBefore);
@@ -319,7 +334,7 @@ export class CodexAdapter {
319
334
  if (this.opts.allowAutoCompactionSetupTurn === false)
320
335
  return;
321
336
  const toolsBefore = this.observedToolCalls;
322
- const setup = await this.sendTurnInternal("Rafi internal initialization only. Do not call tools or modify files. Reply briefly that the context is ready.");
337
+ 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 });
323
338
  if (setup.isError)
324
339
  throw new Error(`Codex automatic-compaction setup failed: ${setup.text.slice(0, 240)}`);
325
340
  if (this.observedToolCalls !== toolsBefore)
@@ -457,8 +472,9 @@ export class CodexAdapter {
457
472
  try {
458
473
  result = await this.request(method, {
459
474
  ...(this._sessionId ? { threadId: this._sessionId } : {}), cwd: this.opts.cwd,
460
- model: this.opts.model ?? null, approvalPolicy: "never",
461
- sandbox: this.opts.sandboxMode === "read-only" ? "read-only" : "workspace-write",
475
+ model: this.opts.model ?? null, approvalPolicy: this.effectiveApprovalPolicy(),
476
+ config: { sandbox_workspace_write: { network_access: this.effectiveNetworkAccess() } },
477
+ sandbox: this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa" ? "read-only" : "workspace-write",
462
478
  developerInstructions: this.opts.systemPromptAppend ?? null,
463
479
  });
464
480
  }
@@ -474,6 +490,17 @@ export class CodexAdapter {
474
490
  }
475
491
  throw error;
476
492
  }
493
+ const effective = result.sandbox;
494
+ if (this.effectiveNetworkAccess() && !effective)
495
+ throw new Error("Codex did not report an effective sandbox policy for the requested network grant; no turn dispatched");
496
+ const requestedSandbox = this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa" ? "readOnly" : "workspaceWrite";
497
+ if (effective && (effective.type !== requestedSandbox || effective.networkAccess === true && !this.effectiveNetworkAccess()))
498
+ throw new Error("Codex effective sandbox exceeds the authorized role policy; no turn dispatched");
499
+ if (effective && this.effectiveNetworkAccess() && effective.networkAccess !== true)
500
+ throw new Error("Codex did not enable the explicitly approved builder network; use an offline bundle or correct provider policy before implementation");
501
+ if (result.approvalPolicy !== undefined && result.approvalPolicy !== this.effectiveApprovalPolicy())
502
+ throw new Error("Codex effective approval policy differs from the requested role policy; no turn dispatched");
503
+ this.eventQueue.push({ kind: "activity", state: "Codex role capabilities", provider: "codex", detail: JSON.stringify({ requested: { sandbox: requestedSandbox, networkAccess: this.effectiveNetworkAccess(), approvalPolicy: this.effectiveApprovalPolicy() }, effective: effective ?? "unknown", approvalPolicy: result.approvalPolicy ?? "unknown" }) });
477
504
  const thread = result.thread;
478
505
  const returnedSessionId = String(thread?.id ?? this._sessionId ?? "") || undefined;
479
506
  if (this._sessionRef && returnedSessionId !== this._sessionRef.sessionId) {
@@ -567,6 +594,10 @@ export class CodexAdapter {
567
594
  }
568
595
  handle(message) {
569
596
  this.touchWaiters();
597
+ if (message.id !== undefined && message.method) {
598
+ void this.handleServerRequest(message);
599
+ return;
600
+ }
570
601
  if (message.id !== undefined) {
571
602
  const pending = this.pending.get(message.id);
572
603
  if (pending) {
@@ -737,6 +768,11 @@ export class CodexAdapter {
737
768
  if (timeoutMs !== undefined) {
738
769
  const arm = () => {
739
770
  timeout = setTimeout(() => {
771
+ // Human approval has its own bounded deadline; it is not provider idleness.
772
+ if (resetOnProviderActivity && this.approvalRequests.size > 0) {
773
+ arm();
774
+ return;
775
+ }
740
776
  const active = this.notificationWaiters.get(method) ?? [];
741
777
  this.notificationWaiters.set(method, active.filter((candidate) => candidate !== waiter));
742
778
  clear();
@@ -765,7 +801,65 @@ export class CodexAdapter {
765
801
  const configured = this.opts.providerIdleTimeoutMs;
766
802
  return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_PROVIDER_IDLE_TIMEOUT_MS;
767
803
  }
804
+ effectiveNetworkAccess() {
805
+ return this.opts.sandboxMode !== "read-only" && this.opts.sessionRole !== "qa" && this.opts.networkAccess === true;
806
+ }
807
+ effectiveApprovalPolicy() {
808
+ return this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa" ? "never" : this.opts.approvalPolicy ?? "never";
809
+ }
810
+ /** Fail closed; approval is for this operation only, never a session cache or policy amendment. */
811
+ async handleServerRequest(message) {
812
+ await Promise.resolve();
813
+ const id = message.id;
814
+ const identity = `${typeof id}:${id}`;
815
+ if (this.approvalRequests.has(identity) || this.answeredApprovalRequests.has(identity))
816
+ return;
817
+ const params = message.params ?? {};
818
+ const command = message.method === "item/commandExecution/requestApproval";
819
+ const fileChange = message.method === "item/fileChange/requestApproval";
820
+ if (!command && !fileChange) {
821
+ this.write({ id, error: { code: -32601, message: `Unsupported server request: ${message.method}` } });
822
+ return;
823
+ }
824
+ const connection = this.process;
825
+ const generation = this.connectionGeneration;
826
+ const hostTurnId = this.activeProviderTurnId;
827
+ const controller = new AbortController();
828
+ this.approvalRequests.set(identity, controller);
829
+ let decision = "decline";
830
+ try {
831
+ if (this.effectiveApprovalPolicy() === "on-request" && params.threadId === this._sessionId && this.activeProviderTurnId && typeof params.turnId === "string" && typeof params.itemId === "string" && params.turnId === this.activeNativeTurnId && (params.cwd === undefined || canonicalSessionPath(String(params.cwd)) === canonicalSessionPath(this.opts.cwd))) {
832
+ const verdict = await new OperationDeadline("Codex approval", this.opts.approvalTimeoutMs ?? 300_000).run(() => this.opts.permission({
833
+ toolName: command ? "Bash" : "Write",
834
+ input: { ...params, providerSandboxEscalation: true, requestedCommand: params.command },
835
+ signal: controller.signal, toolUseID: String(params.itemId),
836
+ title: "Approve this exact sandbox escalation?", description: String(params.reason ?? "Provider requests access beyond the offline workspace sandbox"),
837
+ }), () => controller.abort());
838
+ if (!controller.signal.aborted && verdict.behavior === "allow" && params.threadId === this._sessionId && this.activeProviderTurnId === hostTurnId && params.turnId === this.activeNativeTurnId)
839
+ decision = "accept";
840
+ }
841
+ }
842
+ catch {
843
+ controller.abort();
844
+ }
845
+ finally {
846
+ this.approvalRequests.delete(identity);
847
+ this.touchWaiters();
848
+ this.answeredApprovalRequests.add(identity);
849
+ if (generation === this.connectionGeneration && connection === this.process && !this.closed) {
850
+ try {
851
+ this.write({ id, result: { decision } });
852
+ }
853
+ catch { /* disconnected: no stale reply on another connection */ }
854
+ }
855
+ }
856
+ }
768
857
  disconnect(error) {
858
+ this.connectionGeneration++;
859
+ for (const controller of this.approvalRequests.values())
860
+ controller.abort();
861
+ this.approvalRequests.clear();
862
+ this.answeredApprovalRequests.clear();
769
863
  const child = this.process;
770
864
  this.process = undefined;
771
865
  this.initialized = false;
@@ -23,8 +23,17 @@ export declare class RecoveringAdapter implements BuilderAdapter {
23
23
  private readonly eventQueue;
24
24
  private eventPump;
25
25
  private closed;
26
+ private contractEnforcing;
26
27
  constructor(opts: RecoveringAdapterOptions);
28
+ graphRuntimeSettings(): Pick<import("./types.js").BuilderAdapterOptions, "model" | "effort" | "fast" | "runtimeExecutable"> | undefined;
27
29
  get agent(): "claude" | "codex";
30
+ contractCapabilities(): {
31
+ sameSessionAcceptance: boolean;
32
+ nativeCompactionBarrier: boolean;
33
+ };
34
+ enableContractEnforcement(): void;
35
+ contractCompactionSequence(): number;
36
+ acceptContractDelivery(sequence: number): void;
28
37
  sendTurn(text: string, policy?: Parameters<BuilderAdapter["sendTurn"]>[1]): Promise<TurnResult>;
29
38
  sessionId(): string | undefined;
30
39
  sessionRef(): ProviderSessionRefV1 | undefined;
@@ -10,15 +10,21 @@ export class RecoveringAdapter {
10
10
  eventQueue = new AsyncQueue();
11
11
  eventPump;
12
12
  closed = false;
13
+ contractEnforcing = false;
13
14
  constructor(opts) {
14
15
  this.opts = opts;
15
16
  this.adapter = opts.initial;
16
17
  this.runtime = opts.runtime;
17
18
  this.eventPump = this.forwardEvents(this.adapter);
18
19
  }
20
+ graphRuntimeSettings() { return this.adapter.graphRuntimeSettings?.(); }
19
21
  get agent() {
20
22
  return this.adapter.agent;
21
23
  }
24
+ contractCapabilities() { return this.adapter.contractCapabilities?.() ?? { sameSessionAcceptance: false, nativeCompactionBarrier: false }; }
25
+ enableContractEnforcement() { this.contractEnforcing = true; this.adapter.enableContractEnforcement?.(); }
26
+ contractCompactionSequence() { return this.adapter.contractCompactionSequence?.() ?? 0; }
27
+ acceptContractDelivery(sequence) { this.adapter.acceptContractDelivery?.(sequence); }
22
28
  async sendTurn(text, policy) {
23
29
  while (true) {
24
30
  const result = await this.adapter.sendTurn(text, policy);
@@ -37,6 +43,10 @@ export class RecoveringAdapter {
37
43
  // instruction. Never replay it or silently switch providers.
38
44
  if (result.failure.category === "session-unavailable" || result.failure.dispatchState === "unknown")
39
45
  return result;
46
+ // A replacement has new host-side barriers, even when it resumes the
47
+ // same native session. Only the shared build gate can renew acceptance.
48
+ if (this.contractEnforcing)
49
+ return { ...result, text: `${result.text}\nContract-enforcing runtime recovery requires an explicit host resume through the shared preparation/delivery gate; this turn was not replayed.` };
40
50
  const otherRuntime = this.runtime === "claude" ? "codex" : "claude";
41
51
  const choice = this.opts.choose
42
52
  ? await this.opts.choose(result, this.runtime, otherRuntime)
@@ -1,3 +1,4 @@
1
+ import type { ProviderTurnPurpose } from "../providerPhase.js";
1
2
  /**
2
3
  * Agent-agnostic interface over a coding agent ("builder").
3
4
  */
@@ -233,6 +234,11 @@ export interface BuilderAdapterOptions {
233
234
  fast?: boolean;
234
235
  /** Codex sandbox mode. Defaults to workspace-write for implementation runs. */
235
236
  sandboxMode?: "workspace-write" | "read-only";
237
+ /** Explicit opt-in to per-command sandbox escalation. QA remains offline/never. */
238
+ approvalPolicy?: "never" | "on-request";
239
+ approvalTimeoutMs?: number;
240
+ /** Build-scoped explicit grant; ignored for read-only/QA roles. */
241
+ networkAccess?: boolean;
236
242
  /** Role system text to append to the harness system prompt (from .rafi/compiled or library). */
237
243
  systemPromptAppend?: string;
238
244
  /** Skill names to preload for this session (Claude: lazy-loaded; Codex: flattened). */
@@ -273,12 +279,21 @@ export interface BuilderAdapterOptions {
273
279
  }) => void;
274
280
  }
275
281
  export interface BuilderAdapter {
282
+ graphRuntimeSettings?(): Pick<BuilderAdapterOptions, "model" | "effort" | "fast" | "runtimeExecutable"> | undefined;
276
283
  readonly agent: "claude" | "codex";
284
+ contractCapabilities?(): {
285
+ sameSessionAcceptance: boolean;
286
+ nativeCompactionBarrier: boolean;
287
+ };
288
+ enableContractEnforcement?(): void;
289
+ contractCompactionSequence?(): number;
290
+ acceptContractDelivery?(sequence: number): void;
277
291
  /** Send one instruction; resolves when that turn completes. */
278
292
  sendTurn(text: string, policy?: {
279
293
  handback?: boolean;
280
294
  responseOnly?: boolean;
281
295
  logicalActionId?: string;
296
+ purpose?: ProviderTurnPurpose;
282
297
  }): Promise<TurnResult>;
283
298
  /** Synchronous observation through the event owner. Terminal precedes sendTurn resolution. */
284
299
  observeEvents?(listener: (event: BuilderEvent) => void): () => void;