ai-foreman 1.7.28 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude.d.ts +23 -12
  3. package/dist/adapters/claude.js +228 -24
  4. package/dist/adapters/codex.d.ts +17 -7
  5. package/dist/adapters/codex.js +195 -17
  6. package/dist/adapters/recovering.d.ts +2 -0
  7. package/dist/adapters/recovering.js +5 -1
  8. package/dist/adapters/structured.d.ts +71 -0
  9. package/dist/adapters/structured.js +213 -0
  10. package/dist/adapters/types.d.ts +86 -9
  11. package/dist/agentRun.d.ts +2 -0
  12. package/dist/agentRun.js +2 -1
  13. package/dist/boundedTurnEvidence.d.ts +18 -0
  14. package/dist/boundedTurnEvidence.js +76 -0
  15. package/dist/branch/currentGuard.d.ts +2 -0
  16. package/dist/branch/currentGuard.js +2 -0
  17. package/dist/branch/planner.js +1 -4
  18. package/dist/branch/runner.d.ts +2 -1
  19. package/dist/branch/runner.js +10 -7
  20. package/dist/buildAdmission.js +4 -0
  21. package/dist/buildApproval.d.ts +30 -0
  22. package/dist/buildApproval.js +68 -0
  23. package/dist/buildAssignment.d.ts +1 -0
  24. package/dist/buildAssignment.js +82 -23
  25. package/dist/buildInterventions.d.ts +2 -2
  26. package/dist/buildInterventions.js +11 -4
  27. package/dist/buildLaunchGate.js +3 -2
  28. package/dist/builderGuidanceFollowup.js +17 -6
  29. package/dist/cli/manager.js +22 -20
  30. package/dist/cli/start.js +32 -24
  31. package/dist/cli/tickets.js +40 -24
  32. package/dist/continuity.d.ts +10 -14
  33. package/dist/continuity.js +198 -179
  34. package/dist/continuityOrigin.d.ts +13 -0
  35. package/dist/continuityOrigin.js +91 -0
  36. package/dist/foreman.d.ts +20 -5
  37. package/dist/foreman.js +57 -187
  38. package/dist/graph/boundary.d.ts +1 -1
  39. package/dist/graph/boundary.js +1 -1
  40. package/dist/graph/host.js +8 -19
  41. package/dist/graph/manager.d.ts +1 -1
  42. package/dist/graph/manager.js +1 -1
  43. package/dist/graph/preparation.d.ts +38 -0
  44. package/dist/graph/preparation.js +7 -6
  45. package/dist/graph/semantic.d.ts +1 -0
  46. package/dist/graph/semantic.js +4 -1
  47. package/dist/graph/session.js +7 -2
  48. package/dist/graph/turn.d.ts +2 -3
  49. package/dist/graph/turn.js +28 -40
  50. package/dist/handoffProtocol.js +2 -2
  51. package/dist/handoffs.d.ts +3 -1
  52. package/dist/handoffs.js +90 -59
  53. package/dist/hostGraphApplicability.d.ts +46 -0
  54. package/dist/hostGraphApplicability.js +97 -0
  55. package/dist/hostQaAugmentation.d.ts +18 -0
  56. package/dist/hostQaAugmentation.js +81 -0
  57. package/dist/hostQaComparison.d.ts +21 -2
  58. package/dist/hostQaComparison.js +43 -28
  59. package/dist/hostSourcePreservation.d.ts +38 -0
  60. package/dist/hostSourcePreservation.js +72 -0
  61. package/dist/interviews.d.ts +7 -0
  62. package/dist/interviews.js +18 -1
  63. package/dist/legacyCommunicationDisplay.d.ts +2 -0
  64. package/dist/legacyCommunicationDisplay.js +2 -0
  65. package/dist/managerPacket.js +3 -3
  66. package/dist/markers.d.ts +4 -3
  67. package/dist/markers.js +4 -20
  68. package/dist/probeIpc.d.ts +14 -0
  69. package/dist/probeIpc.js +29 -0
  70. package/dist/qaBuildGate.d.ts +21 -0
  71. package/dist/qaBuildGate.js +48 -12
  72. package/dist/qaBuilderCoverage.js +22 -13
  73. package/dist/qaBuilderEvidence.d.ts +16 -1
  74. package/dist/qaBuilderEvidence.js +48 -8
  75. package/dist/qaContractCoverage.d.ts +2 -2
  76. package/dist/qaContractCoverage.js +11 -7
  77. package/dist/qaContractDelivery.js +34 -20
  78. package/dist/qaCoverageAssembly.d.ts +22 -0
  79. package/dist/qaCoverageAssembly.js +16 -0
  80. package/dist/qaDeliveryJournal.d.ts +2 -0
  81. package/dist/qaDeliveryJournal.js +19 -0
  82. package/dist/qaEffectiveConfig.d.ts +1 -0
  83. package/dist/qaEffectiveConfig.js +6 -1
  84. package/dist/qaFailureDelivery.js +97 -85
  85. package/dist/qaHostTransformation.d.ts +26 -0
  86. package/dist/qaHostTransformation.js +100 -0
  87. package/dist/qaPreparation.d.ts +8 -20
  88. package/dist/qaPreparation.js +133 -229
  89. package/dist/qaPreparationInputs.js +19 -0
  90. package/dist/qaPreparationStore.js +34 -6
  91. package/dist/qaPrerequisites.d.ts +2 -1
  92. package/dist/qaPrerequisites.js +21 -1
  93. package/dist/qaProtocolV2.d.ts +15 -0
  94. package/dist/qaRecovery.js +21 -4
  95. package/dist/qaReview.js +232 -394
  96. package/dist/qaReviewEvidence.d.ts +7 -3
  97. package/dist/qaReviewEvidence.js +15 -4
  98. package/dist/qaRuntime.js +1 -1
  99. package/dist/qaSnapshot.js +71 -21
  100. package/dist/qaStructuredResponse.d.ts +10 -0
  101. package/dist/qaStructuredResponse.js +57 -0
  102. package/dist/qaTimeline.js +5 -3
  103. package/dist/qaToolRecovery.d.ts +12 -0
  104. package/dist/qaToolRecovery.js +44 -0
  105. package/dist/qaVerificationContract.d.ts +3 -2
  106. package/dist/qaVerificationContract.js +58 -34
  107. package/dist/readinessGate.js +5 -4
  108. package/dist/runtimeReadiness.js +23 -15
  109. package/dist/statusReporter.d.ts +2 -0
  110. package/dist/statusReporter.js +2 -0
  111. package/dist/structuredCutover.d.ts +31 -0
  112. package/dist/structuredCutover.js +510 -0
  113. package/dist/structuredCutoverFiles.d.ts +10 -0
  114. package/dist/structuredCutoverFiles.js +181 -0
  115. package/dist/structuredDispatchAuthority.d.ts +22 -0
  116. package/dist/structuredDispatchAuthority.js +110 -0
  117. package/dist/structuredDispatchRecord.d.ts +29 -0
  118. package/dist/structuredDispatchRecord.js +36 -0
  119. package/dist/structuredLiveBudget.d.ts +2 -0
  120. package/dist/structuredLiveBudget.js +23 -0
  121. package/dist/structuredOperation.d.ts +18 -0
  122. package/dist/structuredOperation.js +257 -0
  123. package/dist/supervisedStart.d.ts +1 -1
  124. package/dist/supervisedStart.js +78 -31
  125. package/dist/supervisor.d.ts +18 -2
  126. package/dist/supervisor.js +74 -17
  127. package/dist/ticketPlanning.d.ts +1 -373
  128. package/dist/ticketPlanning.js +2 -26
  129. package/dist/ticketPopulation.d.ts +0 -3
  130. package/dist/ticketPopulation.js +4 -21
  131. package/dist/ticketPopulationPageAuthority.d.ts +13 -0
  132. package/dist/ticketPopulationPageAuthority.js +30 -0
  133. package/dist/ticketPopulationPaging.d.ts +27 -0
  134. package/dist/ticketPopulationPaging.js +105 -0
  135. package/dist/tickets/renderMarkdown.d.ts +14 -0
  136. package/dist/tickets/renderMarkdown.js +39 -5
  137. package/dist/tickets/ticketLoader.js +10 -0
  138. package/dist/tickets/ticketSchema.d.ts +107 -0
  139. package/dist/tickets/ticketSchema.js +4 -1
  140. package/dist/workflowDb.d.ts +100 -2
  141. package/dist/workflowDb.js +781 -35
  142. package/dist/workflowReader.d.ts +1 -0
  143. package/dist/workflowReader.js +9 -0
  144. package/package.json +15 -7
package/README.md CHANGED
@@ -48,7 +48,7 @@ Helpful when:
48
48
 
49
49
  Requires:
50
50
 
51
- - Node.js 20 or newer.
51
+ - Node.js 24.
52
52
 
53
53
  ### Claude runtime and enterprise authentication
54
54
 
@@ -1,4 +1,4 @@
1
- import { ProviderPhaseBarrier, type ProviderTurnPurpose } from "../providerPhase.js";
1
+ import { ProviderPhaseBarrier } from "../providerPhase.js";
2
2
  import type { SDKSessionInfo, PermissionResult } from "@anthropic-ai/claude-agent-sdk";
3
3
  /** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
4
4
  export declare function requireClaudeSDK(): Promise<typeof import("@anthropic-ai/claude-agent-sdk")>;
@@ -9,7 +9,7 @@ import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
9
9
  * Extracted so tests can assert on the shape without making a live SDK call.
10
10
  * `canUseTool` is omitted here — it's a closure that the constructor adds.
11
11
  */
12
- export declare function buildClaudeQueryOptions(opts: Omit<BuilderAdapterOptions, "permission">): Record<string, unknown>;
12
+ export declare function buildClaudeQueryOptions(opts: Omit<BuilderAdapterOptions, "permission">, scratchDirectory?: string): Record<string, unknown>;
13
13
  export declare function probeClaudeSession(ref: ProviderSessionRefV1, input: {
14
14
  cwd: string;
15
15
  configRoot?: string;
@@ -43,6 +43,7 @@ export declare function claudeApiRetryEvent(message: {
43
43
  */
44
44
  export declare class ClaudeAdapter implements BuilderAdapter {
45
45
  private readonly opts;
46
+ private readonly queryFactory;
46
47
  readonly agent: "claude";
47
48
  graphRuntimeSettings(): {
48
49
  model: string | undefined;
@@ -50,11 +51,16 @@ export declare class ClaudeAdapter implements BuilderAdapter {
50
51
  fast: boolean | undefined;
51
52
  runtimeExecutable: string | undefined;
52
53
  };
53
- private readonly inbox;
54
+ private inbox;
54
55
  private readonly eventQueue;
55
- private readonly query;
56
- private readonly abort;
57
- private readonly pumpDone;
56
+ private query;
57
+ private abort;
58
+ private pumpDone;
59
+ private rotatingQuery;
60
+ private configuringTurn;
61
+ private output?;
62
+ private outputWireContract?;
63
+ private responseOnlyViolation;
58
64
  private _sessionId?;
59
65
  private _sessionRef?;
60
66
  private readonly sessionIdentityReady;
@@ -76,8 +82,10 @@ export declare class ClaudeAdapter implements BuilderAdapter {
76
82
  private cumulativeUsage;
77
83
  private priorTurnUsage;
78
84
  private activeProviderTurnId?;
85
+ private activeTurnPolicy?;
79
86
  private activeProviderTurnSpanId?;
80
87
  private readonly toolSpans;
88
+ private readonly structuredSubmissionIds;
81
89
  private readonly toolCalls;
82
90
  private readonly completedToolCalls;
83
91
  private readonly unknownToolResults;
@@ -96,6 +104,7 @@ export declare class ClaudeAdapter implements BuilderAdapter {
96
104
  private streamEnded;
97
105
  private closed;
98
106
  private closePromise?;
107
+ private readonly scratchDirectory?;
99
108
  private launchScopeObserved;
100
109
  private providerIdentityObserved;
101
110
  private toolCwd?;
@@ -109,16 +118,18 @@ export declare class ClaudeAdapter implements BuilderAdapter {
109
118
  private disableFastMode;
110
119
  static create(opts: BuilderAdapterOptions): Promise<ClaudeAdapter>;
111
120
  private constructor();
121
+ private queryBudget?;
122
+ private queryBudgetAbort?;
123
+ private finishQueryBudget;
124
+ private startQuery;
125
+ private isResultSubmission;
126
+ private resultSubmissionError;
127
+ private configureOutput;
112
128
  /** Background loop: consume the SDK message stream until it ends. */
113
129
  private providerInputs;
114
130
  private pump;
115
131
  private handle;
116
- sendTurn(text: string, policy?: {
117
- handback?: boolean;
118
- responseOnly?: boolean;
119
- logicalActionId?: string;
120
- purpose?: ProviderTurnPurpose;
121
- }): Promise<TurnResult>;
132
+ sendTurn(text: string, policy?: import("./types.js").ProviderTurnPolicy): Promise<TurnResult>;
122
133
  private sendTurnInternal;
123
134
  private sendTurnAttempt;
124
135
  private captureStderr;
@@ -1,7 +1,11 @@
1
+ import { inheritedStructuredLiveBudget } from "../structuredLiveBudget.js";
1
2
  import { ProviderPhaseBarrier } from "../providerPhase.js";
2
3
  import { OperationDeadline } from "../util/deadline.js";
4
+ import { assertActiveProviderDispatch } from "../structuredDispatchAuthority.js";
3
5
  import { createHash, randomUUID } from "node:crypto";
4
6
  import { dirname, join, relative, isAbsolute } from "node:path";
7
+ import { mkdtempSync, rmSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
5
9
  /** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
6
10
  export async function requireClaudeSDK() {
7
11
  try {
@@ -22,19 +26,21 @@ import { createProviderSessionRef, validateProviderSessionScope, canonicalSessio
22
26
  import { SessionUnavailableError, sessionUnavailableResult } from "./sessionFailure.js";
23
27
  import { isFastModeUnavailable, reportFastModeUnavailable } from "./fastMode.js";
24
28
  import { handoffSystemInstructions } from "../handoffProtocol.js";
29
+ import { providerControlOperationV2, providerWireContract, renderProviderWireInstruction, validateProviderWireValue } from "rafi-spec";
30
+ import { structuredReceipt } from "./structured.js";
25
31
  const DEFAULT_PROVIDER_IDLE_TIMEOUT_MS = 30 * 60_000;
26
32
  /**
27
33
  * Pure function: build the `options` object passed to `query()`.
28
34
  * Extracted so tests can assert on the shape without making a live SDK call.
29
35
  * `canUseTool` is omitted here — it's a closure that the constructor adds.
30
36
  */
31
- export function buildClaudeQueryOptions(opts) {
37
+ export function buildClaudeQueryOptions(opts, scratchDirectory = join(dirname(opts.cwd), "scratch")) {
32
38
  const qaReadOnly = opts.sessionRole === "qa" || opts.sandboxMode === "read-only";
33
39
  const confinedBuilder = opts.sessionRole === "builder";
34
40
  const base = {
35
41
  cwd: opts.cwd,
36
42
  pathToClaudeCodeExecutable: opts.runtimeExecutable,
37
- env: qaReadOnly ? qaEnvironment(process.env, opts.cwd) : { ...process.env },
43
+ env: qaReadOnly ? qaEnvironment(process.env, scratchDirectory) : { ...process.env },
38
44
  model: opts.model,
39
45
  resume: opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId,
40
46
  permissionMode: qaReadOnly ? "default" : "acceptEdits",
@@ -58,7 +64,7 @@ export function buildClaudeQueryOptions(opts) {
58
64
  autoAllowBashIfSandboxed: true,
59
65
  allowUnsandboxedCommands: false,
60
66
  network: { allowedDomains: [], deniedDomains: ["*"] },
61
- filesystem: { allowWrite: [join(dirname(opts.cwd), "scratch")], denyWrite: [opts.cwd, ...(opts.configRoot ? [opts.configRoot] : [])] },
67
+ filesystem: { allowWrite: [scratchDirectory], denyWrite: [opts.cwd, ...(opts.configRoot ? [opts.configRoot] : [])] },
62
68
  } } : {}),
63
69
  };
64
70
  const exactSkills = opts.preloadedSkillContent?.length
@@ -73,9 +79,9 @@ export function buildClaudeQueryOptions(opts) {
73
79
  }
74
80
  return base;
75
81
  }
76
- function qaEnvironment(source, cwd) {
82
+ function qaEnvironment(source, scratchDirectory) {
77
83
  const allowed = ["PATH", "HOME", "USER", "LOGNAME", "SHELL", "TMPDIR", "LANG", "LC_ALL", "TERM", "NO_COLOR", "FORCE_COLOR", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL", "CLAUDE_CODE_API_KEY_HELPER_TTL_MS", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "SSL_CERT_FILE", "SSL_CERT_DIR", "NODE_EXTRA_CA_CERTS"];
78
- return { ...Object.fromEntries(allowed.flatMap((key) => source[key] === undefined ? [] : [[key, source[key]]])), TMPDIR: join(dirname(cwd), "scratch") };
84
+ return { ...Object.fromEntries(allowed.flatMap((key) => source[key] === undefined ? [] : [[key, source[key]]])), TMPDIR: scratchDirectory };
79
85
  }
80
86
  export async function probeClaudeSession(ref, input) {
81
87
  const now = input.now ?? new Date();
@@ -176,6 +182,7 @@ export function claudeApiRetryEvent(message) {
176
182
  */
177
183
  export class ClaudeAdapter {
178
184
  opts;
185
+ queryFactory;
179
186
  agent = "claude";
180
187
  graphRuntimeSettings() { return { model: this.opts.model, effort: this.opts.effort, fast: this.opts.fast, runtimeExecutable: this.opts.runtimeExecutable }; }
181
188
  inbox = new AsyncQueue();
@@ -183,6 +190,11 @@ export class ClaudeAdapter {
183
190
  query;
184
191
  abort = new AbortController();
185
192
  pumpDone;
193
+ rotatingQuery = false;
194
+ configuringTurn = false;
195
+ output;
196
+ outputWireContract;
197
+ responseOnlyViolation = false;
186
198
  _sessionId;
187
199
  _sessionRef;
188
200
  sessionIdentityReady;
@@ -204,8 +216,10 @@ export class ClaudeAdapter {
204
216
  cumulativeUsage = { observedAt: new Date(0).toISOString(), source: "provider" };
205
217
  priorTurnUsage = {};
206
218
  activeProviderTurnId;
219
+ activeTurnPolicy;
207
220
  activeProviderTurnSpanId;
208
221
  toolSpans = new Map();
222
+ structuredSubmissionIds = new Set();
209
223
  toolCalls = new Map();
210
224
  completedToolCalls = new Set();
211
225
  unknownToolResults = new Map();
@@ -221,6 +235,7 @@ export class ClaudeAdapter {
221
235
  streamEnded = false;
222
236
  closed = false;
223
237
  closePromise;
238
+ scratchDirectory;
224
239
  launchScopeObserved = false;
225
240
  providerIdentityObserved = false;
226
241
  toolCwd;
@@ -251,6 +266,7 @@ export class ClaudeAdapter {
251
266
  return;
252
267
  }
253
268
  try {
269
+ providerControlOperationV2("claude", "applyFlagSettings");
254
270
  await new OperationDeadline("Claude fast mode", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ fastMode: true }));
255
271
  this.fastModeEnabled = true;
256
272
  }
@@ -269,10 +285,13 @@ export class ClaudeAdapter {
269
285
  this.fastModeEnabled = false;
270
286
  }
271
287
  async disableFastMode() {
288
+ providerControlOperationV2("claude", "applyFlagSettings");
272
289
  await new OperationDeadline("Claude standard speed", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ fastMode: false }));
273
290
  this.disableFastModeNotice();
274
291
  }
275
292
  static async create(opts) {
293
+ const inheritedBudget = inheritedStructuredLiveBudget("claude");
294
+ opts = { ...opts, queryBudget: opts.queryBudget ?? inheritedBudget };
276
295
  try {
277
296
  const runtimeExecutable = opts.runtimeExecutable ?? resolveExecutablePath("claude");
278
297
  if (!runtimeExecutable) {
@@ -310,8 +329,9 @@ export class ClaudeAdapter {
310
329
  }
311
330
  }
312
331
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
313
- constructor(opts, query) {
332
+ constructor(opts, queryFactory) {
314
333
  this.opts = opts;
334
+ this.queryFactory = queryFactory;
315
335
  this.phaseBarrier.begin("initialization");
316
336
  this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
317
337
  this._sessionRef = opts.resumeSessionRef;
@@ -324,15 +344,61 @@ export class ClaudeAdapter {
324
344
  start: (attemptId) => opts.observer.store.startSpan(this.observationContext(), { kind: "provider_wait", name: "Waiting for Claude after question answer", attributes: { attemptId } }),
325
345
  finish: (spanId, outcome) => opts.observer.store.finishSpan(spanId, { outcome }),
326
346
  } : undefined);
327
- this.query = query({
347
+ // Planning and interpretation also use read-only sessions, without a QA
348
+ // snapshot's sibling scratch directory. Own a real, private directory for
349
+ // the complete query lifecycle, including exact-session schema rotations.
350
+ // macOS's default TMPDIR is too long for the SDK's Unix socket paths.
351
+ if (opts.sessionRole === "qa" || opts.sandboxMode === "read-only")
352
+ this.scratchDirectory = mkdtempSync(join(process.platform === "darwin" ? "/tmp" : tmpdir(), "rf-c-"));
353
+ try {
354
+ this.startQuery();
355
+ }
356
+ catch (error) {
357
+ this.finishQueryBudget();
358
+ if (this.scratchDirectory)
359
+ rmSync(this.scratchDirectory, { recursive: true, force: true });
360
+ throw error;
361
+ }
362
+ }
363
+ queryBudget;
364
+ queryBudgetAbort;
365
+ finishQueryBudget() {
366
+ if (this.queryBudgetAbort)
367
+ this.queryBudget?.signal.removeEventListener("abort", this.queryBudgetAbort);
368
+ this.queryBudgetAbort = undefined;
369
+ this.queryBudget?.close();
370
+ this.queryBudget = undefined;
371
+ }
372
+ startQuery() {
373
+ const opts = this.opts;
374
+ this.queryBudget = opts.queryBudget?.reserveQuery("claude");
375
+ if (this.queryBudget) {
376
+ const lease = this.queryBudget;
377
+ this.queryBudgetAbort = () => { this.abort.abort(); void this.close(); };
378
+ lease.signal.addEventListener("abort", this.queryBudgetAbort, { once: true });
379
+ if (lease.signal.aborted) {
380
+ this.finishQueryBudget();
381
+ throw new Error("Live query deadline expired before launch");
382
+ }
383
+ }
384
+ this.query = this.queryFactory({
328
385
  prompt: this.providerInputs(),
329
386
  options: {
330
- ...buildClaudeQueryOptions(opts),
387
+ ...buildClaudeQueryOptions(opts, this.scratchDirectory),
388
+ ...(this.queryBudget ? { maxBudgetUsd: this.queryBudget.maximumSpendUsd } : {}),
389
+ ...(this._sessionId ? { resume: this._sessionId } : {}),
390
+ ...(this.outputWireContract ? { outputFormat: { type: "json_schema", schema: this.outputWireContract.wireSchema } } : {}),
331
391
  abortController: this.abort,
332
392
  hooks: {
333
393
  PreToolUse: [{ hooks: [async (input) => {
334
394
  if (input.hook_event_name !== "PreToolUse")
335
395
  return {};
396
+ if (this.isResultSubmission(input.tool_name, input.tool_input)) {
397
+ const reason = this.resultSubmissionError(input.tool_input);
398
+ return reason ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } } : {};
399
+ }
400
+ if (this.output && this.responseOnlyTurn)
401
+ this.responseOnlyViolation = true;
336
402
  this.toolCwd = input.cwd;
337
403
  this.toolCwds.set(input.tool_use_id, input.cwd);
338
404
  if (this.toolCwds.size > 1000)
@@ -356,6 +422,12 @@ export class ClaudeAdapter {
356
422
  },
357
423
  stderr: (data) => this.captureStderr(data),
358
424
  canUseTool: async (toolName, input, requestOptions = {}) => {
425
+ if (this.isResultSubmission(toolName, input)) {
426
+ const reason = this.resultSubmissionError(input);
427
+ return reason ? { behavior: "deny", message: reason } : { behavior: "allow", updatedInput: input };
428
+ }
429
+ if (this.output && this.responseOnlyTurn)
430
+ this.responseOnlyViolation = true;
359
431
  if (this.responseOnlyTurn)
360
432
  return { behavior: "deny", message: "QA response correction forbids all tools, including read-only tools", interrupt: true };
361
433
  const isProviderQuestion = toolName === "AskUserQuestion";
@@ -394,6 +466,60 @@ export class ClaudeAdapter {
394
466
  });
395
467
  this.pumpDone = this.pump();
396
468
  }
469
+ isResultSubmission(name, input) {
470
+ // Only the SDK's built-in submission in a host-configured structured query.
471
+ // An MCP name or arbitrary tool cannot acquire this exception.
472
+ return Boolean(this.pending && this.output && name === "StructuredOutput");
473
+ }
474
+ resultSubmissionError(input) {
475
+ try {
476
+ // This SDK hook sees provider wire data, not the canonical artifact.
477
+ // Canonical validation is deliberately deferred until the selected
478
+ // normalizer runs in structuredReceipt.
479
+ validateProviderWireValue(this.outputWireContract, input);
480
+ return undefined;
481
+ }
482
+ catch (error) {
483
+ // The SDK submission is data, including when malformed. Give its bounded
484
+ // native retry the actual wire-contract violations, never a mutation-policy
485
+ // diagnosis. Only a valid wire payload may invoke the SDK submission tool.
486
+ return `Native structured result violates selected ${this.outputWireContract.profile} wire contract for ${this.output.operation}: ${String(error).slice(0, 2400)}. Return every required field with the declared type.`;
487
+ }
488
+ }
489
+ async configureOutput(output, wireContract) {
490
+ const selected = output ? wireContract ?? providerWireContract(output, "claude", "default", { requireRegistered: true }) : undefined;
491
+ if (selected && (selected.provider !== "claude" || selected.canonical.schemaDigest !== output.schemaDigest))
492
+ throw new Error("Claude received a mismatched provider wire contract");
493
+ if (output?.schemaDigest === this.output?.schemaDigest && output?.operation === this.output?.operation && selected?.descriptorDigest === this.outputWireContract?.descriptorDigest)
494
+ return;
495
+ if (this.pending || this.closed || this.streamEnded)
496
+ throw new Error("Cannot change output schema on an active or unavailable Claude query");
497
+ this.rotatingQuery = true;
498
+ try {
499
+ this.inbox.close();
500
+ this.abort.abort();
501
+ this.query.close?.();
502
+ await new OperationDeadline("Claude schema transition", this.opts.shutdownTimeoutMs ?? 10_000).run(() => this.pumpDone);
503
+ this.finishQueryBudget();
504
+ }
505
+ catch (error) {
506
+ this.streamEnded = true;
507
+ throw error;
508
+ }
509
+ finally {
510
+ this.rotatingQuery = false;
511
+ }
512
+ this.inbox = new AsyncQueue();
513
+ this.abort = new AbortController();
514
+ this.output = output;
515
+ this.outputWireContract = selected;
516
+ this.fastModePrepared = undefined;
517
+ this.fastModeEnabled = false;
518
+ this.providerIdentityObserved = false;
519
+ this.startQuery();
520
+ if (this._sessionId)
521
+ this.eventQueue.push({ kind: "session-transition", transition: "resumed", detail: "Exact session resumed for output schema change" });
522
+ }
397
523
  /** Background loop: consume the SDK message stream until it ends. */
398
524
  async *providerInputs() {
399
525
  for await (const message of this.inbox) {
@@ -413,6 +539,8 @@ export class ClaudeAdapter {
413
539
  }
414
540
  }
415
541
  catch (err) {
542
+ if (this.rotatingQuery)
543
+ return;
416
544
  this.questionTrace?.finish(this.closed ? "closed" : "stream-error");
417
545
  // Suppress the AbortError that fires when close() aborts the stream.
418
546
  const isShutdownAbort = this.closed &&
@@ -429,6 +557,8 @@ export class ClaudeAdapter {
429
557
  }
430
558
  }
431
559
  finally {
560
+ if (this.rotatingQuery)
561
+ return;
432
562
  this.questionTrace?.finish(this.closed ? "closed" : "stream-ended");
433
563
  this.streamEnded = true;
434
564
  if (!this.closed && this.pending) {
@@ -464,7 +594,7 @@ export class ClaudeAdapter {
464
594
  this.turnSignals.push(`assistant error: ${msg.error}`);
465
595
  }
466
596
  for (const block of msg.message.content) {
467
- this.eventQueue.push({ kind: "provider-item", provider: "claude", lifecycle: "completed", itemType: String(block.type ?? "unknown"), payload: structuredClone(block), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
597
+ this.eventQueue.push({ kind: "provider-item", provider: "claude", lifecycle: "completed", itemType: block.type === "tool_use" && this.isResultSubmission(block.name, block.input) ? "structuredOutput" : String(block.type ?? "unknown"), payload: structuredClone(block), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
468
598
  if (block.type === "text" && block.text) {
469
599
  if (msg.error && this.fastModeEnabled && isFastModeUnavailable(block.text) && !this.turnSawWork)
470
600
  continue;
@@ -473,6 +603,16 @@ export class ClaudeAdapter {
473
603
  this.opts.observer?.signal(true);
474
604
  }
475
605
  else if (block.type === "tool_use") {
606
+ // The SDK result submission is transport, not a domain tool. Retain its
607
+ // provider-item evidence without falsely reporting an acceptance violation.
608
+ if (this.isResultSubmission(block.name, block.input)) {
609
+ this.structuredSubmissionIds.add(block.id);
610
+ if (this.structuredSubmissionIds.size > 1000)
611
+ this.structuredSubmissionIds.delete(this.structuredSubmissionIds.values().next().value);
612
+ continue;
613
+ }
614
+ if (this.output && this.responseOnlyTurn && !this.isResultSubmission(block.name, block.input))
615
+ this.responseOnlyViolation = true;
476
616
  this.turnSawWork = true;
477
617
  const callId = block.id;
478
618
  this.toolCalls.set(callId, { name: block.name, input: block.input });
@@ -537,6 +677,8 @@ export class ClaudeAdapter {
537
677
  this.eventQueue.push({ kind: "activity", provider: "claude", state: "running tool", detail: `${msg.tool_name} (${Math.floor(msg.elapsed_time_seconds)}s)`, transient: true });
538
678
  const progress = msg;
539
679
  const callId = typeof progress.tool_use_id === "string" ? progress.tool_use_id : typeof progress.toolUseID === "string" ? progress.toolUseID : undefined;
680
+ if (callId && this.structuredSubmissionIds.has(callId))
681
+ return;
540
682
  this.eventQueue.push({ kind: "tool", name: msg.tool_name, input: {}, lifecycle: "progress", callId, durationMs: Math.max(0, msg.elapsed_time_seconds * 1000), completionKnown: false, eventId: randomUUID(), observedAt: new Date().toISOString() });
541
683
  this.opts.observer?.signal(false);
542
684
  }
@@ -586,7 +728,8 @@ export class ClaudeAdapter {
586
728
  cwd: this.opts.cwd,
587
729
  diagnostics: rawDiagnostics,
588
730
  } : undefined;
589
- const result = {
731
+ this.queryBudget?.record(msg.total_cost_usd);
732
+ let result = {
590
733
  text: failure ? formatClaudeFailure(failure) : text,
591
734
  isError: msg.is_error,
592
735
  numTurns: msg.num_turns,
@@ -599,8 +742,21 @@ export class ClaudeAdapter {
599
742
  rawResponse: text,
600
743
  cleanedResponse: failure ? formatClaudeFailure(failure) : text,
601
744
  providerMetadata: { provider: "claude", sessionId: this._sessionId, sessionRef: this._sessionRef },
745
+ ...(this.activeTurnPolicy?.selectedDispatch ? { selectedDispatch: this.activeTurnPolicy.selectedDispatch } : {}),
602
746
  };
603
747
  const rawResult = msg;
748
+ if (this.output) {
749
+ const selectedDispatch = this.activeTurnPolicy?.selectedDispatch;
750
+ if (!selectedDispatch || !this._sessionId)
751
+ throw new Error("Structured terminal lost its sealed dispatch tuple or session");
752
+ result = { ...result, selectedDispatch, structured: msg.subtype !== "success" || msg.is_error || this.responseOnlyViolation
753
+ ? { state: "rejected", reason: this.responseOnlyViolation ? "tool-policy-violation" : "execution-not-completed", detail: `Claude terminal ${msg.subtype}`, dispatchId: result.turnId }
754
+ : !this.outputWireContract || !this.activeTurnPolicy?.selectedDispatch
755
+ ? { state: "rejected", reason: "invalid-contract", detail: "Claude structured turn has no sealed descriptor or tuple", dispatchId: result.turnId }
756
+ : structuredReceipt(this.output, "claude", rawResult.structured_output, { dispatchId: result.turnId, sessionId: this._sessionId, nativeTurnId: typeof rawResult.uuid === "string" ? rawResult.uuid : undefined }, "decoded-json", this.outputWireContract, this.activeTurnPolicy.selectedDispatch) };
757
+ if (result.structured.state === "rejected")
758
+ result.isError = true;
759
+ }
604
760
  const mergedUsage = mergeClaudeProviderSessionUsage(this.cumulativeUsage, rawResult);
605
761
  result.inputTokens = mergedUsage.inputTokens;
606
762
  result.outputTokens = mergedUsage.outputTokens;
@@ -616,11 +772,30 @@ export class ClaudeAdapter {
616
772
  }
617
773
  }
618
774
  async sendTurn(text, policy) {
619
- if (this.pending)
775
+ if (this.pending || this.configuringTurn)
620
776
  throw new Error("a turn is already in progress");
777
+ if (policy?.output)
778
+ assertActiveProviderDispatch(policy, text, "claude", this.sessionRef());
779
+ this.configuringTurn = true;
780
+ try {
781
+ const selected = policy?.output ? policy.wireContract ?? providerWireContract(policy.output, "claude", "default", { requireRegistered: true }) : undefined;
782
+ if (selected && policy?.renderedWireContractDigest && policy.renderedWireContractDigest !== selected.descriptorDigest)
783
+ throw new Error("Claude received mismatched rendered wire guidance");
784
+ if (selected) {
785
+ text = renderProviderWireInstruction(text, selected);
786
+ policy = { ...policy, wireContract: selected, renderedWireContractDigest: selected.descriptorDigest };
787
+ }
788
+ if (policy?.output?.schemaDigest !== this.output?.schemaDigest || policy?.output?.operation !== this.output?.operation || selected?.descriptorDigest !== this.outputWireContract?.descriptorDigest)
789
+ await this.configureOutput(policy?.output, selected);
790
+ }
791
+ finally {
792
+ this.configuringTurn = false;
793
+ }
794
+ this.responseOnlyViolation = false;
795
+ this.activeTurnPolicy = policy;
621
796
  this.responseOnlyTurn = Boolean(policy?.responseOnly);
622
797
  this.phaseBarrier.begin(policy?.purpose ?? (policy?.responseOnly ? "response-repair" : "implementation"));
623
- const turnId = randomUUID();
798
+ const turnId = policy?.dispatchId ?? randomUUID();
624
799
  this.activeProviderTurnId = turnId;
625
800
  const observer = this.opts.observer;
626
801
  if (!observer) {
@@ -663,7 +838,7 @@ export class ClaudeAdapter {
663
838
  if (this.opts.fast)
664
839
  await this.prepareFastMode();
665
840
  const result = await this.sendTurnAttempt(text);
666
- if (result.isError && this.fastModeEnabled && !this.turnSawWork && isFastModeUnavailable(result.rawResponse ?? result.text) && !this.streamEnded) {
841
+ if (!this.output && result.isError && this.fastModeEnabled && !this.turnSawWork && isFastModeUnavailable(result.rawResponse ?? result.text) && !this.streamEnded) {
667
842
  await this.disableFastMode();
668
843
  return this.sendTurnAttempt(text);
669
844
  }
@@ -712,9 +887,9 @@ export class ClaudeAdapter {
712
887
  for (const block of body.content) {
713
888
  if (block.type !== "tool_result")
714
889
  continue;
715
- this.eventQueue.push({ kind: "provider-item", provider: "claude", lifecycle: "completed", itemType: "tool_result", payload: structuredClone(block), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
890
+ this.eventQueue.push({ kind: "provider-item", provider: "claude", lifecycle: "completed", itemType: typeof block.tool_use_id === "string" && this.structuredSubmissionIds.has(block.tool_use_id) ? "structuredOutput" : "tool_result", payload: structuredClone(block), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
716
891
  const callId = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
717
- if (!callId)
892
+ if (!callId || this.structuredSubmissionIds.has(callId))
718
893
  continue;
719
894
  if (this.completedToolCalls.has(callId))
720
895
  continue;
@@ -735,11 +910,15 @@ export class ClaudeAdapter {
735
910
  : failed && /cancel(?:led|ed)/i.test(text) ? "cancelled"
736
911
  : failed && /permission denied/i.test(text) ? "permission_denied"
737
912
  : failed && /not found/i.test(text) ? "not_found"
738
- : failed && /\b(?:exit(?:ed)?|status)\s+[1-9]\d*\b/i.test(text) ? "nonzero_exit"
739
- : failed && /\b(?:failed|error|exception)\b/i.test(text) ? "explicit_error"
740
- : failed ? "unexplained_error"
741
- : "completed";
742
- const status = resultReason === "host_policy_denied" ? "denied" : resultReason === "timeout" ? "timed_out" : resultReason === "cancelled" ? "cancelled" : ["permission_denied", "not_found", "nonzero_exit", "explicit_error"].includes(resultReason) ? "failed" : resultReason === "completed" ? "completed" : "unknown";
913
+ // Claude's Read tool explicitly reports its size guard this
914
+ // way. It is a terminal, observed rejection, not a
915
+ // provisional transport error that might later complete.
916
+ : failed && /\b(?:content|file)\b[\s\S]{0,160}\bexceeds\s+(?:the\s+)?maximum\s+allowed\s+tokens\b/i.test(text) ? "output_limit"
917
+ : failed && /\b(?:exit(?:ed)?|status)\s+[1-9]\d*\b/i.test(text) ? "nonzero_exit"
918
+ : failed && /\b(?:failed|error|exception)\b/i.test(text) ? "explicit_error"
919
+ : failed ? "unexplained_error"
920
+ : "completed";
921
+ const status = resultReason === "host_policy_denied" ? "denied" : resultReason === "timeout" ? "timed_out" : resultReason === "cancelled" ? "cancelled" : ["permission_denied", "not_found", "output_limit", "nonzero_exit", "explicit_error"].includes(resultReason) ? "failed" : resultReason === "completed" ? "completed" : "unknown";
743
922
  if (status !== "unknown") {
744
923
  this.completedToolCalls.add(callId);
745
924
  if (this.completedToolCalls.size > 1000)
@@ -749,7 +928,7 @@ export class ClaudeAdapter {
749
928
  const outputDigest = createHash("sha256").update(text).digest("hex");
750
929
  // Persist only correlation metadata. The result text can contain source
751
930
  // paths, command output, or credentials and must not enter lifecycle logs.
752
- this.opts.onLifecycleTrace?.({ phase: "provider-tool-result", at: new Date().toISOString(), sessionId: this._sessionId, providerTurnId: this.activeProviderTurnId, toolCallId: callId, toolName: call?.name ?? "unidentified tool", classification: status, resultReason, isError: failed, outputDigest, rawOutputBytes: Buffer.byteLength(text) });
931
+ this.opts.onLifecycleTrace?.({ phase: "provider-tool-result", at: new Date().toISOString(), sessionId: this._sessionId, providerTurnId: this.activeProviderTurnId, toolCallId: callId, toolName: call?.name ?? "unidentified tool", classification: status, resultReason, isError: failed, outputDigest, rawOutputBytes: Buffer.byteLength(text), completionKnown: status !== "unknown" });
753
932
  if (status === "unknown") {
754
933
  if (this.unknownToolResults.get(callId) === outputDigest)
755
934
  continue;
@@ -795,6 +974,7 @@ export class ClaudeAdapter {
795
974
  // Require an advertised built-in local command; never substitute a
796
975
  // model-generated warm-up for QA's pre-dispatch identity validation.
797
976
  if (commands.some(command => command.name === "context" && "builtin" in command && command.builtin === true)) {
977
+ providerControlOperationV2("claude", "/context");
798
978
  this.opts.onLifecycleTrace?.({ phase: "local-context-initialization-start", at: new Date().toISOString() });
799
979
  const result = await this.sendTurn("/context", { responseOnly: true });
800
980
  this.opts.onLifecycleTrace?.({ phase: "local-context-initialization-complete", at: new Date().toISOString() });
@@ -807,7 +987,11 @@ export class ClaudeAdapter {
807
987
  }
808
988
  return this.sessionIdentityReady;
809
989
  };
810
- const ref = await Promise.race([prepare(), interrupted]);
990
+ const ready = await Promise.race([prepare(), interrupted]);
991
+ // The identity promise resolves once. A host may later adopt a new scope
992
+ // for the same conversation (for example a handoff generation), so the
993
+ // prepared session is the adapter's current reference, as for Codex.
994
+ const ref = this._sessionRef?.sessionId === ready.sessionId ? this._sessionRef : ready;
811
995
  if (!ref.validatedAt)
812
996
  throw new Error("Claude session identity was not provider-validated during initialization");
813
997
  return ref;
@@ -857,6 +1041,7 @@ export class ClaudeAdapter {
857
1041
  }
858
1042
  }
859
1043
  async compactInternal() {
1044
+ providerControlOperationV2("claude", "/compact");
860
1045
  this.compactResult = undefined;
861
1046
  this.manualCompactionInFlight = true;
862
1047
  const normalMs = Math.min(180_000, Math.max(1, this.opts.compactionTimeoutMs ?? 120_000));
@@ -898,7 +1083,9 @@ export class ClaudeAdapter {
898
1083
  // queue, including a later local command. Establish identity first.
899
1084
  if (!this.providerIdentityObserved && typeof this.query.supportedCommands === "function")
900
1085
  await this.prepareSession();
1086
+ providerControlOperationV2("claude", "applyFlagSettings");
901
1087
  await new OperationDeadline("Claude settings", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ autoCompactEnabled: true }));
1088
+ providerControlOperationV2("claude", "getContextUsage");
902
1089
  const baseline = await new OperationDeadline("Claude context usage", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.getContextUsage());
903
1090
  if (!Number.isFinite(baseline.maxTokens) || baseline.maxTokens <= 0
904
1091
  || !Number.isFinite(baseline.autoCompactThreshold) || baseline.autoCompactThreshold === undefined
@@ -912,7 +1099,9 @@ export class ClaudeAdapter {
912
1099
  if (reserve < 0)
913
1100
  throw new Error("Claude reported an invalid automatic-compaction reserve");
914
1101
  const requestedWindow = tokenLimit(baseline.maxTokens, threshold) + reserve;
1102
+ providerControlOperationV2("claude", "applyFlagSettings");
915
1103
  await new OperationDeadline("Claude settings", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ autoCompactEnabled: true, autoCompactWindow: requestedWindow }));
1104
+ providerControlOperationV2("claude", "getContextUsage");
916
1105
  const installed = await new OperationDeadline("Claude context usage", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.getContextUsage());
917
1106
  if (!installed.isAutoCompactEnabled || !Number.isFinite(installed.maxTokens) || installed.maxTokens <= 0
918
1107
  || !Number.isFinite(installed.autoCompactThreshold) || installed.autoCompactThreshold === undefined) {
@@ -942,6 +1131,7 @@ export class ClaudeAdapter {
942
1131
  restoreNativeCompactions(compactions) { this.nativeCompactions.unshift(...compactions); }
943
1132
  async contextUsage() {
944
1133
  try {
1134
+ providerControlOperationV2("claude", "getContextUsage");
945
1135
  const usage = await new OperationDeadline("Claude context usage", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.getContextUsage());
946
1136
  const result = { used: usage.totalTokens, maximum: usage.maxTokens, percentage: usage.percentage, observedAt: new Date().toISOString(), source: "provider-query" };
947
1137
  this.eventQueue.push({ kind: "context-usage", ...result });
@@ -959,14 +1149,21 @@ export class ClaudeAdapter {
959
1149
  return { ok: false, error: "Claude SDK cannot change reasoning/fast controls on an existing transport" };
960
1150
  if (settings.model === this.opts.model)
961
1151
  return { ok: true };
1152
+ providerControlOperationV2("claude", "/model");
1153
+ if (settings.model && /[\r\n]/.test(settings.model))
1154
+ return { ok: false, error: "Model control requires one model identifier" };
962
1155
  if (this.opts.fast)
963
1156
  await this.prepareFastMode();
1157
+ if (this.fastModeEnabled)
1158
+ providerControlOperationV2("claude", "applyFlagSettings");
964
1159
  if (this.fastModeEnabled)
965
1160
  await new OperationDeadline("Claude standard speed", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ fastMode: false }));
966
1161
  this.fastModeEnabled = false;
967
1162
  const result = await new OperationDeadline("Claude model settings", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.sendTurn(`/model ${settings.model ?? "default"}`), () => this.abort.abort());
968
1163
  if (result.isError)
969
1164
  return { ok: false, error: result.text, ...(result.failure ? { failure: result.failure } : {}) };
1165
+ if (result.numTurns !== 0 || result.structured)
1166
+ return { ok: false, error: "Model control did not complete as a non-authoritative provider acknowledgement" };
970
1167
  this.opts.model = settings.model;
971
1168
  this.fastModePrepared = undefined;
972
1169
  this.fastModeUnavailable = false;
@@ -995,8 +1192,15 @@ export class ClaudeAdapter {
995
1192
  catch {
996
1193
  // interrupt is best-effort — ignore if no turn is active
997
1194
  }
998
- this.query.close?.();
999
- await new OperationDeadline("Claude shutdown", this.opts.shutdownTimeoutMs ?? 10_000).run(() => this.pumpDone).catch(() => { });
1195
+ try {
1196
+ this.query.close?.();
1197
+ await new OperationDeadline("Claude shutdown", this.opts.shutdownTimeoutMs ?? 10_000).run(() => this.pumpDone).catch(() => { });
1198
+ }
1199
+ finally {
1200
+ this.finishQueryBudget();
1201
+ if (this.scratchDirectory)
1202
+ rmSync(this.scratchDirectory, { recursive: true, force: true });
1203
+ }
1000
1204
  }
1001
1205
  failSessionIdentity(error) {
1002
1206
  this.rejectSessionIdentity(error);