ai-foreman 1.7.25 → 1.7.27

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 (113) hide show
  1. package/README.md +2 -1
  2. package/dist/adapters/claude.d.ts +15 -0
  3. package/dist/adapters/claude.js +149 -28
  4. package/dist/adapters/codex.d.ts +21 -0
  5. package/dist/adapters/codex.js +127 -9
  6. package/dist/adapters/fastMode.d.ts +4 -0
  7. package/dist/adapters/fastMode.js +9 -0
  8. package/dist/adapters/recovering.d.ts +4 -0
  9. package/dist/adapters/recovering.js +10 -0
  10. package/dist/adapters/sessionFailure.d.ts +6 -0
  11. package/dist/adapters/sessionFailure.js +19 -0
  12. package/dist/adapters/types.d.ts +8 -0
  13. package/dist/agentRun.js +1 -0
  14. package/dist/branch/currentGuard.d.ts +8 -0
  15. package/dist/branch/currentGuard.js +5 -0
  16. package/dist/branch/runner.js +52 -46
  17. package/dist/buildApproval.js +2 -1
  18. package/dist/buildAssignment.d.ts +15 -0
  19. package/dist/buildAssignment.js +142 -5
  20. package/dist/buildRuns.d.ts +18 -0
  21. package/dist/buildRuns.js +123 -7
  22. package/dist/buildWorkContext.js +1 -1
  23. package/dist/buildWorkMigration.d.ts +10 -0
  24. package/dist/buildWorkMigration.js +18 -2
  25. package/dist/cli/start.d.ts +13 -0
  26. package/dist/cli/start.js +286 -59
  27. package/dist/cli/state.js +39 -1
  28. package/dist/cli/tickets.d.ts +37 -0
  29. package/dist/cli/tickets.js +367 -12
  30. package/dist/config.js +1 -0
  31. package/dist/continuity.d.ts +1 -0
  32. package/dist/continuity.js +16 -10
  33. package/dist/foreman.d.ts +8 -0
  34. package/dist/foreman.js +158 -18
  35. package/dist/graph/preparation.js +6 -3
  36. package/dist/graph/semantic.js +2 -1
  37. package/dist/graph/turn.js +8 -1
  38. package/dist/handoffProtocol.d.ts +4 -0
  39. package/dist/handoffProtocol.js +20 -0
  40. package/dist/handoffs.d.ts +1 -1
  41. package/dist/handoffs.js +30 -23
  42. package/dist/log.d.ts +1 -1
  43. package/dist/managerPacket.js +1 -1
  44. package/dist/permissions/policy.js +39 -11
  45. package/dist/permissions/shellForms.d.ts +3 -0
  46. package/dist/permissions/shellForms.js +203 -0
  47. package/dist/planChanges.js +1 -1
  48. package/dist/projectDiagnostics.js +30 -0
  49. package/dist/providerQuestions.js +4 -24
  50. package/dist/qaBuildGate.d.ts +6 -1
  51. package/dist/qaBuildGate.js +76 -16
  52. package/dist/qaBuilderActivation.d.ts +19 -0
  53. package/dist/qaBuilderActivation.js +112 -0
  54. package/dist/qaBuilderCoverage.d.ts +2 -1
  55. package/dist/qaBuilderCoverage.js +47 -5
  56. package/dist/qaBuilderEvidence.d.ts +18 -0
  57. package/dist/qaBuilderEvidence.js +60 -0
  58. package/dist/qaContractCoverage.d.ts +12 -1
  59. package/dist/qaContractCoverage.js +34 -2
  60. package/dist/qaContractDelivery.d.ts +2 -2
  61. package/dist/qaContractDelivery.js +88 -10
  62. package/dist/qaEffectiveConfig.js +2 -1
  63. package/dist/qaFailureDelivery.d.ts +23 -0
  64. package/dist/qaFailureDelivery.js +88 -7
  65. package/dist/qaHostFacts.d.ts +2 -0
  66. package/dist/qaHostFacts.js +11 -0
  67. package/dist/qaPreparation.d.ts +38 -1
  68. package/dist/qaPreparation.js +372 -31
  69. package/dist/qaPreparationInputs.js +5 -1
  70. package/dist/qaPreparationPolicy.d.ts +2 -1
  71. package/dist/qaPreparationPolicy.js +39 -9
  72. package/dist/qaRecovery.d.ts +7 -0
  73. package/dist/qaRecovery.js +75 -2
  74. package/dist/qaReview.d.ts +4 -0
  75. package/dist/qaReview.js +72 -15
  76. package/dist/qaReviewEvidence.d.ts +6 -0
  77. package/dist/qaReviewEvidence.js +37 -0
  78. package/dist/qaRollbackRehearsal.d.ts +2 -0
  79. package/dist/qaRollbackRehearsal.js +78 -0
  80. package/dist/qaSnapshot.d.ts +3 -1
  81. package/dist/qaSnapshot.js +3 -2
  82. package/dist/qaVerificationContract.d.ts +9 -0
  83. package/dist/qaVerificationContract.js +143 -17
  84. package/dist/sessionLifecycle.js +1 -1
  85. package/dist/sources/sourceRegistry.js +16 -5
  86. package/dist/stateTransfer.d.ts +14 -0
  87. package/dist/stateTransfer.js +51 -0
  88. package/dist/statusReporter.d.ts +8 -0
  89. package/dist/statusReporter.js +5 -0
  90. package/dist/supervisedStart.js +23 -3
  91. package/dist/supervisor.js +33 -0
  92. package/dist/ticketPlanning.d.ts +373 -0
  93. package/dist/ticketPlanning.js +34 -3
  94. package/dist/ticketPopulation.d.ts +28 -3
  95. package/dist/ticketPopulation.js +125 -15
  96. package/dist/tickets/details.d.ts +2 -1
  97. package/dist/tickets/details.js +7 -3
  98. package/dist/tickets/eligibility.d.ts +6 -0
  99. package/dist/tickets/eligibility.js +12 -1
  100. package/dist/tickets/groupReset.js +100 -96
  101. package/dist/tickets/reset.js +80 -69
  102. package/dist/tickets/riskWaivers.d.ts +3 -0
  103. package/dist/tickets/riskWaivers.js +12 -0
  104. package/dist/tickets/stateDb.d.ts +10 -0
  105. package/dist/tickets/stateDb.js +23 -0
  106. package/dist/tickets/ticketLoader.d.ts +9 -0
  107. package/dist/tickets/ticketLoader.js +15 -2
  108. package/dist/tickets/ticketSchema.d.ts +14 -1
  109. package/dist/tickets/ticketSchema.js +2 -1
  110. package/dist/tickets/validate.js +11 -1
  111. package/dist/workflowDb.d.ts +16 -1
  112. package/dist/workflowDb.js +287 -10
  113. package/package.json +9 -4
package/README.md CHANGED
@@ -116,7 +116,7 @@ Provider sessions are historical after import. Resume with `rafi build:resume <p
116
116
  | `-a, --agent <agent>` | `claude` / `codex` | single `rafi-config.yaml` target, otherwise `claude` | Builder agent. Explicit `--agent` always wins. |
117
117
  | `-m, --model <model>` | `gpt-5.5` / any supported agent model | agent default | Overrides the builder model. |
118
118
  | `--effort <level>` | Claude Code: `low` / `medium` / `high` / `xhigh`<br>Codex: `low` / `medium` / `high` / `xhigh` | agent default | Reasoning level. |
119
- | `--fast` | flag | off | Lower latency. For Codex, maps to `effort=low` when `--effort` is not set. |
119
+ | `--fast` | flag | off | Use native fast mode when the selected model supports it. Otherwise continue at standard speed with the same model and reasoning settings. |
120
120
  | `-t, --tickets <path>` | `.md` / `.txt` / `.yaml` | auto-detects standard tracker files | Sends a task file to the builder during preflight planning. |
121
121
  | `-y, --yes` | flag | off | Skips preflight confirmation. |
122
122
  | `--no-qa` | flag | QA on | Disables per-ticket QA review. |
@@ -640,6 +640,7 @@ For Claude:
640
640
  - Bash is denied if it contains an always-escalate substring.
641
641
  - Bash is otherwise allowed only for configured prefixes.
642
642
  - Every chained segment must start with an `allowBash` prefix.
643
+ - Supported project test forms include literal in-worktree `cd directory &&`, constrained `PYTHONPATH` or test environment assignments, existing project-local virtualenv Python/pytest executables, and `2>&1`. `uv run --offline --no-sync` may wrap an already allowed test command. Every pipeline executable must be allowed. Configure additional test commands under `permissions.allowBash` in `foreman.yaml`; PATH/loader overrides, substitutions, file redirection, and dependency acquisition remain separate decisions.
643
644
  - Unknown tools are denied.
644
645
  - Unknown Bash commands are denied.
645
646
 
@@ -95,9 +95,22 @@ export declare class ClaudeAdapter implements BuilderAdapter {
95
95
  private terminalResult?;
96
96
  private streamEnded;
97
97
  private closed;
98
+ private closePromise?;
99
+ private launchScopeObserved;
100
+ private providerIdentityObserved;
101
+ private toolCwd?;
102
+ private readonly toolCwds;
103
+ private fastModePrepared?;
104
+ private fastModeEnabled;
105
+ private fastModeUnavailable;
106
+ private turnSawWork;
107
+ private prepareFastMode;
108
+ private disableFastModeNotice;
109
+ private disableFastMode;
98
110
  static create(opts: BuilderAdapterOptions): Promise<ClaudeAdapter>;
99
111
  private constructor();
100
112
  /** Background loop: consume the SDK message stream until it ends. */
113
+ private providerInputs;
101
114
  private pump;
102
115
  private handle;
103
116
  sendTurn(text: string, policy?: {
@@ -107,6 +120,7 @@ export declare class ClaudeAdapter implements BuilderAdapter {
107
120
  purpose?: ProviderTurnPurpose;
108
121
  }): Promise<TurnResult>;
109
122
  private sendTurnInternal;
123
+ private sendTurnAttempt;
110
124
  private captureStderr;
111
125
  private observationContext;
112
126
  private observeToolResults;
@@ -128,6 +142,7 @@ export declare class ClaudeAdapter implements BuilderAdapter {
128
142
  observeEvents(listener: (event: BuilderEvent) => void): () => void;
129
143
  events(): AsyncIterable<BuilderEvent>;
130
144
  close(): Promise<void>;
145
+ private closeInternal;
131
146
  private failSessionIdentity;
132
147
  private observeSession;
133
148
  private settlePending;
@@ -1,7 +1,7 @@
1
1
  import { ProviderPhaseBarrier } from "../providerPhase.js";
2
2
  import { OperationDeadline } from "../util/deadline.js";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
- import { dirname, join } from "node:path";
4
+ import { dirname, join, relative, isAbsolute } from "node:path";
5
5
  /** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
6
6
  export async function requireClaudeSDK() {
7
7
  try {
@@ -20,6 +20,8 @@ import { normalizeRuntimeErrorText } from "../runtimeAuth.js";
20
20
  import { classifyClaudeSdkFailure, resolveExecutablePath, sanitizeDiagnostics, } from "../runtimeReadiness.js";
21
21
  import { createProviderSessionRef, validateProviderSessionScope, canonicalSessionPath } from "../sessionIdentity.js";
22
22
  import { SessionUnavailableError, sessionUnavailableResult } from "./sessionFailure.js";
23
+ import { isFastModeUnavailable, reportFastModeUnavailable } from "./fastMode.js";
24
+ import { handoffSystemInstructions } from "../handoffProtocol.js";
23
25
  const DEFAULT_PROVIDER_IDLE_TIMEOUT_MS = 30 * 60_000;
24
26
  /**
25
27
  * Pure function: build the `options` object passed to `query()`.
@@ -37,7 +39,9 @@ export function buildClaudeQueryOptions(opts) {
37
39
  resume: opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId,
38
40
  permissionMode: qaReadOnly ? "default" : "acceptEdits",
39
41
  effort: opts.effort,
40
- extraArgs: opts.fast ? { fast: null } : undefined,
42
+ // Start at standard speed even when user/project settings enable fast mode.
43
+ // Enabling it before checking capabilities can silently switch the model.
44
+ settings: { fastMode: false },
41
45
  // Read-only roles still need the user's Claude authentication settings (for
42
46
  // example, an enterprise apiKeyHelper). Do not load repository-controlled
43
47
  // settings in those roles.
@@ -60,7 +64,7 @@ export function buildClaudeQueryOptions(opts) {
60
64
  const exactSkills = opts.preloadedSkillContent?.length
61
65
  ? ["# Preloaded Skills", "Use the following skills for this run.", ...opts.preloadedSkillContent.map((skill) => skill.content)].join("\n\n")
62
66
  : undefined;
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");
67
+ const systemAppend = [handoffSystemInstructions(opts.sessionRole, 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");
64
68
  if (systemAppend) {
65
69
  base.systemPrompt = { type: "preset", preset: "claude_code", append: systemAppend };
66
70
  }
@@ -216,6 +220,58 @@ export class ClaudeAdapter {
216
220
  terminalResult;
217
221
  streamEnded = false;
218
222
  closed = false;
223
+ closePromise;
224
+ launchScopeObserved = false;
225
+ providerIdentityObserved = false;
226
+ toolCwd;
227
+ toolCwds = new Map();
228
+ fastModePrepared;
229
+ fastModeEnabled = false;
230
+ fastModeUnavailable = false;
231
+ turnSawWork = false;
232
+ async prepareFastMode() {
233
+ if (!this.opts.fast || this.fastModeUnavailable)
234
+ return;
235
+ this.fastModePrepared ??= (async () => {
236
+ // A missing capability is conservative: standard speed, same model.
237
+ let models;
238
+ try {
239
+ const init = await new OperationDeadline("Claude model capabilities", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.initializationResult());
240
+ models = init.models ?? [];
241
+ }
242
+ catch (error) {
243
+ if (!isFastModeUnavailable(String(error)))
244
+ throw error;
245
+ this.disableFastModeNotice();
246
+ return;
247
+ }
248
+ const model = models.find(item => item.value === (this.opts.model ?? "default"));
249
+ if (model?.supportsFastMode !== true || typeof this.query.applyFlagSettings !== "function") {
250
+ this.disableFastModeNotice();
251
+ return;
252
+ }
253
+ try {
254
+ await new OperationDeadline("Claude fast mode", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ fastMode: true }));
255
+ this.fastModeEnabled = true;
256
+ }
257
+ catch (error) {
258
+ if (!isFastModeUnavailable(String(error)))
259
+ throw error;
260
+ await this.disableFastMode();
261
+ }
262
+ })();
263
+ await this.fastModePrepared;
264
+ }
265
+ disableFastModeNotice() {
266
+ if (!this.fastModeUnavailable)
267
+ reportFastModeUnavailable(this.opts.model);
268
+ this.fastModeUnavailable = true;
269
+ this.fastModeEnabled = false;
270
+ }
271
+ async disableFastMode() {
272
+ await new OperationDeadline("Claude standard speed", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ fastMode: false }));
273
+ this.disableFastModeNotice();
274
+ }
219
275
  static async create(opts) {
220
276
  try {
221
277
  const runtimeExecutable = opts.runtimeExecutable ?? resolveExecutablePath("claude");
@@ -269,7 +325,7 @@ export class ClaudeAdapter {
269
325
  finish: (spanId, outcome) => opts.observer.store.finishSpan(spanId, { outcome }),
270
326
  } : undefined);
271
327
  this.query = query({
272
- prompt: this.inbox,
328
+ prompt: this.providerInputs(),
273
329
  options: {
274
330
  ...buildClaudeQueryOptions(opts),
275
331
  abortController: this.abort,
@@ -277,6 +333,10 @@ export class ClaudeAdapter {
277
333
  PreToolUse: [{ hooks: [async (input) => {
278
334
  if (input.hook_event_name !== "PreToolUse")
279
335
  return {};
336
+ this.toolCwd = input.cwd;
337
+ this.toolCwds.set(input.tool_use_id, input.cwd);
338
+ if (this.toolCwds.size > 1000)
339
+ this.toolCwds.delete(this.toolCwds.keys().next().value);
280
340
  const reason = this.phaseBarrier.denial(input.tool_name);
281
341
  return reason ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } } : {};
282
342
  }] }],
@@ -306,6 +366,7 @@ export class ClaudeAdapter {
306
366
  const decision = await opts.permission({
307
367
  toolName,
308
368
  input,
369
+ executionCwd: (requestOptions.toolUseID ? this.toolCwds.get(requestOptions.toolUseID) : undefined) ?? this.toolCwd ?? opts.cwd,
309
370
  signal: requestOptions.signal,
310
371
  title: requestOptions.title,
311
372
  displayName: requestOptions.displayName,
@@ -334,6 +395,17 @@ export class ClaudeAdapter {
334
395
  this.pumpDone = this.pump();
335
396
  }
336
397
  /** Background loop: consume the SDK message stream until it ends. */
398
+ async *providerInputs() {
399
+ for await (const message of this.inbox) {
400
+ // A closed queue can still contain buffered input. Never let the SDK
401
+ // submit it after we recorded a proven pre-submit cancellation/failure.
402
+ if (this.closed || this.terminalResult || this.streamEnded)
403
+ return;
404
+ if (this.pending?.instruction === message.message.content)
405
+ this.pending.submitted = true;
406
+ yield message;
407
+ }
408
+ }
337
409
  async pump() {
338
410
  try {
339
411
  for await (const msg of this.query) {
@@ -394,10 +466,14 @@ export class ClaudeAdapter {
394
466
  for (const block of msg.message.content) {
395
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() });
396
468
  if (block.type === "text" && block.text) {
469
+ if (msg.error && this.fastModeEnabled && isFastModeUnavailable(block.text) && !this.turnSawWork)
470
+ continue;
471
+ this.turnSawWork = true;
397
472
  this.eventQueue.push({ kind: "text", text: block.text, byteCount: Buffer.byteLength(block.text), digest: createHash("sha256").update(block.text).digest("hex"), eventId: randomUUID(), observedAt: new Date().toISOString() });
398
473
  this.opts.observer?.signal(true);
399
474
  }
400
475
  else if (block.type === "tool_use") {
476
+ this.turnSawWork = true;
401
477
  const callId = block.id;
402
478
  this.toolCalls.set(callId, { name: block.name, input: block.input });
403
479
  if (this.toolCalls.size > 1000)
@@ -530,7 +606,8 @@ export class ClaudeAdapter {
530
606
  result.outputTokens = mergedUsage.outputTokens;
531
607
  result.usage = { scope: "session-cumulative", inputTokens: mergedUsage.sample.inputTokens, outputTokens: mergedUsage.sample.outputTokens, totalTokens: mergedUsage.sample.totalTokens, costUsd: mergedUsage.sample.authoritativeCostUsd };
532
608
  this.cumulativeUsage = mergedUsage.sample;
533
- this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
609
+ if (!(result.isError && this.fastModeEnabled && !this.turnSawWork && isFastModeUnavailable(text)))
610
+ this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
534
611
  this.settlePending(result, false);
535
612
  this.turnSignals = [];
536
613
  this.structuredError = undefined;
@@ -582,7 +659,17 @@ export class ClaudeAdapter {
582
659
  this.responseOnlyTurn = false;
583
660
  }
584
661
  }
585
- sendTurnInternal(text) {
662
+ async sendTurnInternal(text) {
663
+ if (this.opts.fast)
664
+ await this.prepareFastMode();
665
+ const result = await this.sendTurnAttempt(text);
666
+ if (result.isError && this.fastModeEnabled && !this.turnSawWork && isFastModeUnavailable(result.rawResponse ?? result.text) && !this.streamEnded) {
667
+ await this.disableFastMode();
668
+ return this.sendTurnAttempt(text);
669
+ }
670
+ return result;
671
+ }
672
+ sendTurnAttempt(text) {
586
673
  if (this.closed)
587
674
  return Promise.reject(new Error("builder is closed"));
588
675
  if (this.terminalResult || this.streamEnded)
@@ -593,9 +680,10 @@ export class ClaudeAdapter {
593
680
  return new Promise((resolve, reject) => {
594
681
  this.eventQueue.push({ kind: "activity", state: "starting Claude turn", provider: "claude", model: this.opts.model });
595
682
  this.turnSignals = [];
683
+ this.turnSawWork = false;
596
684
  this.structuredError = undefined;
597
685
  this.apiErrorStatus = undefined;
598
- const pending = this.pending = { resolve, reject, instruction: text, turnId: this.activeProviderTurnId ?? randomUUID() };
686
+ const pending = this.pending = { resolve, reject, instruction: text, submitted: false, turnId: this.activeProviderTurnId ?? randomUUID() };
599
687
  this.armPendingTurn(pending);
600
688
  this.inbox.push({
601
689
  type: "user",
@@ -642,15 +730,16 @@ export class ClaudeAdapter {
642
730
  // continue the same turn. Preserve that state as unknown until a
643
731
  // correlated, explicit terminal result arrives.
644
732
  const resultReason = elapsedOnly ? "elapsed_placeholder"
645
- : failed && /timed?\s*out|timeout/i.test(text) ? "timeout"
646
- : failed && /cancel(?:led|ed)/i.test(text) ? "cancelled"
647
- : failed && /permission denied/i.test(text) ? "permission_denied"
648
- : failed && /not found/i.test(text) ? "not_found"
649
- : failed && /\b(?:exit(?:ed)?|status)\s+[1-9]\d*\b/i.test(text) ? "nonzero_exit"
650
- : failed && /\b(?:failed|error|exception)\b/i.test(text) ? "explicit_error"
651
- : failed ? "unexplained_error"
652
- : "completed";
653
- const status = resultReason === "timeout" ? "timed_out" : resultReason === "cancelled" ? "cancelled" : ["permission_denied", "not_found", "nonzero_exit", "explicit_error"].includes(resultReason) ? "failed" : resultReason === "completed" ? "completed" : "unknown";
733
+ : failed && /Foreman policy: this action needs human approval\b[\s\S]*\band was NOT performed\./.test(text) ? "host_policy_denied"
734
+ : failed && /timed?\s*out|timeout/i.test(text) ? "timeout"
735
+ : failed && /cancel(?:led|ed)/i.test(text) ? "cancelled"
736
+ : failed && /permission denied/i.test(text) ? "permission_denied"
737
+ : 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";
654
743
  if (status !== "unknown") {
655
744
  this.completedToolCalls.add(callId);
656
745
  if (this.completedToolCalls.size > 1000)
@@ -699,7 +788,8 @@ export class ClaudeAdapter {
699
788
  });
700
789
  const prepare = async () => {
701
790
  await this.query.initializationResult();
702
- if (!this._sessionRef?.validatedAt && typeof this.query.supportedCommands === "function") {
791
+ await this.prepareFastMode();
792
+ if (!this.providerIdentityObserved && typeof this.query.supportedCommands === "function") {
703
793
  const commands = await this.query.supportedCommands();
704
794
  // Some SDK/CLI pairs expose init identity only after their first input.
705
795
  // Require an advertised built-in local command; never substitute a
@@ -708,7 +798,10 @@ export class ClaudeAdapter {
708
798
  this.opts.onLifecycleTrace?.({ phase: "local-context-initialization-start", at: new Date().toISOString() });
709
799
  const result = await this.sendTurn("/context", { responseOnly: true });
710
800
  this.opts.onLifecycleTrace?.({ phase: "local-context-initialization-complete", at: new Date().toISOString() });
711
- if (result.isError || result.failure || result.costUsd > 0)
801
+ // SDK total_cost_usd includes earlier conversation cost on resume.
802
+ // The advertised built-in command must report zero model turns;
803
+ // historical cumulative cost does not make this a paid warm-up.
804
+ if (result.isError || result.failure || result.numTurns !== 0 || (result.costUsd > 0 && result.usage?.scope !== "session-cumulative"))
712
805
  throw new Error("Claude local context initialization did not complete without model work");
713
806
  }
714
807
  }
@@ -803,7 +896,7 @@ export class ClaudeAdapter {
803
896
  // The SDK can acknowledge initialization while its CLI is still waiting
804
897
  // for first input. Sending getContextUsage then deadlocks the control
805
898
  // queue, including a later local command. Establish identity first.
806
- if (!this._sessionRef?.validatedAt && typeof this.query.supportedCommands === "function")
899
+ if (!this.providerIdentityObserved && typeof this.query.supportedCommands === "function")
807
900
  await this.prepareSession();
808
901
  await new OperationDeadline("Claude settings", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ autoCompactEnabled: true }));
809
902
  const baseline = await new OperationDeadline("Claude context usage", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.getContextUsage());
@@ -825,10 +918,10 @@ export class ClaudeAdapter {
825
918
  || !Number.isFinite(installed.autoCompactThreshold) || installed.autoCompactThreshold === undefined) {
826
919
  throw new Error("Claude did not expose an enabled native automatic-compaction threshold after configuration");
827
920
  }
828
- // `applyFlagSettings` can legitimately clamp autoCompactWindow. The
829
- // provider's reported trigger is authoritative; rejecting the entire run
830
- // merely because a configured value is below that minimum made defaults
831
- // unusable. Lifecycle code consumes this verified effective ceiling.
921
+ // A control acknowledgment may disagree with the SDK's subsequent report.
922
+ // Use that reported threshold as the available lifecycle telemetry rather
923
+ // than rejecting the build. This does not prove clamping or independently
924
+ // verify the native trigger's actual enforcement.
832
925
  const effectiveThresholdPercent = Math.max(1, Math.min(99, Math.floor(installed.autoCompactThreshold / installed.maxTokens * 100)));
833
926
  this.preparedAutoCompactionPolicy = {
834
927
  requestedThresholdPercent: threshold,
@@ -866,20 +959,32 @@ export class ClaudeAdapter {
866
959
  return { ok: false, error: "Claude SDK cannot change reasoning/fast controls on an existing transport" };
867
960
  if (settings.model === this.opts.model)
868
961
  return { ok: true };
962
+ if (this.opts.fast)
963
+ await this.prepareFastMode();
964
+ if (this.fastModeEnabled)
965
+ await new OperationDeadline("Claude standard speed", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ fastMode: false }));
966
+ this.fastModeEnabled = false;
869
967
  const result = await new OperationDeadline("Claude model settings", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.sendTurn(`/model ${settings.model ?? "default"}`), () => this.abort.abort());
870
968
  if (result.isError)
871
969
  return { ok: false, error: result.text, ...(result.failure ? { failure: result.failure } : {}) };
872
970
  this.opts.model = settings.model;
971
+ this.fastModePrepared = undefined;
972
+ this.fastModeUnavailable = false;
973
+ if (this.opts.fast)
974
+ await this.prepareFastMode();
873
975
  return { ok: true };
874
976
  }
875
977
  observeEvents(listener) { return this.eventQueue.observe(listener); }
876
978
  events() {
877
979
  return this.eventQueue;
878
980
  }
879
- async close() {
981
+ close() { return this.closePromise ??= this.closeInternal(); }
982
+ async closeInternal() {
880
983
  if (this.closed)
881
984
  return;
882
985
  this.closed = true;
986
+ if (this.pending)
987
+ this.settlePending(this.streamFailureResult("Claude turn interrupted; submitted work requires recovery", false));
883
988
  this.questionTrace?.finish("closed");
884
989
  this.rejectSessionIdentity(new Error("Claude session closed during initialization"));
885
990
  this.abort.abort();
@@ -896,7 +1001,7 @@ export class ClaudeAdapter {
896
1001
  failSessionIdentity(error) {
897
1002
  this.rejectSessionIdentity(error);
898
1003
  const result = sessionUnavailableResult(new SessionUnavailableError({
899
- runtime: "claude", phase: this.pending ? "turn" : "preflight", dispatchState: this.pending ? "unknown" : "not-sent",
1004
+ runtime: "claude", phase: this.pending ? "turn" : "preflight", dispatchState: this.pending?.submitted ? "unknown" : "not-sent",
900
1005
  executable: this.opts.runtimeExecutable ?? "claude", cwd: this.opts.cwd, diagnostics: error.message,
901
1006
  availability: { version: 1, status: "unavailable", checkedAt: new Date().toISOString(), reason: "attach-failed", detail: error.message, ...(this._sessionRef ? { sessionRef: this._sessionRef } : {}) },
902
1007
  }));
@@ -912,11 +1017,26 @@ export class ClaudeAdapter {
912
1017
  this.failSessionIdentity(error);
913
1018
  throw error;
914
1019
  }
915
- if (observedCwd !== undefined && canonicalSessionPath(observedCwd) !== canonicalSessionPath(this.opts.cwd)) {
1020
+ const launchCwd = canonicalSessionPath(this.opts.cwd);
1021
+ const currentCwd = observedCwd === undefined ? undefined : canonicalSessionPath(observedCwd);
1022
+ const withinLaunchScope = currentCwd === undefined ? false : (() => {
1023
+ const path = relative(launchCwd, currentCwd);
1024
+ return path === "" || (!isAbsolute(path) && path !== ".." && !path.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`));
1025
+ })();
1026
+ // Establish the launch scope before trusting mutable Bash cwd. Subsequent
1027
+ // messages for the same identity may report a directory inside that scope;
1028
+ // they must not replace the immutable cwd in the durable session reference.
1029
+ const trustedResumeScope = Boolean(this.opts.resumeSessionRef?.validatedAt && canonicalSessionPath(this.opts.resumeSessionRef.cwd) === launchCwd);
1030
+ if (currentCwd !== undefined && (this.launchScopeObserved || trustedResumeScope ? !withinLaunchScope : currentCwd !== launchCwd)) {
916
1031
  const error = new Error(`Claude session cwd ${observedCwd} does not match ${this.opts.cwd}`);
917
1032
  this.failSessionIdentity(error);
918
1033
  throw error;
919
1034
  }
1035
+ if (currentCwd === launchCwd)
1036
+ this.launchScopeObserved = true;
1037
+ this.providerIdentityObserved = true;
1038
+ if (currentCwd !== undefined)
1039
+ this.toolCwd = currentCwd;
920
1040
  this._sessionId = sessionId;
921
1041
  if (this._sessionRef?.validatedAt) {
922
1042
  this.resolveSessionIdentity(this._sessionRef);
@@ -1021,19 +1141,20 @@ export class ClaudeAdapter {
1021
1141
  return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_PROVIDER_IDLE_TIMEOUT_MS;
1022
1142
  }
1023
1143
  streamFailureResult(message, sessionUnavailable) {
1144
+ const dispatchState = this.pending?.submitted === false ? "not-sent" : "unknown";
1024
1145
  if (sessionUnavailable) {
1025
1146
  const availability = {
1026
1147
  version: 1, status: "unavailable", checkedAt: new Date().toISOString(), reason: "not-found",
1027
1148
  detail: message, ...(this._sessionRef ? { sessionRef: this._sessionRef } : {}),
1028
1149
  };
1029
1150
  return sessionUnavailableResult(new SessionUnavailableError({
1030
- runtime: "claude", phase: "turn", dispatchState: "unknown", executable: this.opts.runtimeExecutable ?? "claude",
1151
+ runtime: "claude", phase: "turn", dispatchState, executable: this.opts.runtimeExecutable ?? "claude",
1031
1152
  cwd: this.opts.cwd, diagnostics: message, availability,
1032
1153
  }));
1033
1154
  }
1034
1155
  return {
1035
1156
  text: message, isError: true, numTurns: 0, costUsd: 0, costAuthoritative: false,
1036
- failure: { runtime: "claude", phase: this.opts.runtimePhase ?? "builder", category: "agent-stream", executable: this.opts.runtimeExecutable ?? "claude", cwd: this.opts.cwd, diagnostics: message, dispatchState: "unknown" },
1157
+ failure: { runtime: "claude", phase: this.opts.runtimePhase ?? "builder", category: "agent-stream", executable: this.opts.runtimeExecutable ?? "claude", cwd: this.opts.cwd, diagnostics: message, dispatchState },
1037
1158
  };
1038
1159
  }
1039
1160
  }
@@ -28,6 +28,7 @@ export declare class CodexAdapter implements BuilderAdapter {
28
28
  private sessionAvailability?;
29
29
  private initialized;
30
30
  private closed;
31
+ private closePromise?;
31
32
  private activeText;
32
33
  private usage?;
33
34
  private usageRevision;
@@ -53,11 +54,30 @@ export declare class CodexAdapter implements BuilderAdapter {
53
54
  private readonly answeredApprovalRequests;
54
55
  private activeProviderTurnSpanId?;
55
56
  private readonly toolSpans;
57
+ private effectiveModel?;
58
+ private fastPreparedModel?;
59
+ private fastTier?;
60
+ private readonly fastUnavailableModels;
61
+ private turnSawTool;
62
+ /** Native compaction is non-blocking. The next host turn gets this reminder. */
63
+ private qaContractReminder?;
64
+ private qaContractReminderPending;
65
+ private qaContractReminderEvent?;
66
+ private disableFastMode;
67
+ private prepareFastMode;
56
68
  constructor(opts: BuilderAdapterOptions);
57
69
  contractCapabilities(): {
58
70
  sameSessionAcceptance: boolean;
59
71
  nativeCompactionBarrier: boolean;
60
72
  };
73
+ contractCompactionSequence(): number;
74
+ configureQaContractReminder(reminder: string, onEvent?: (event: {
75
+ kind: "installed" | "compaction-observed" | "reminder-delivered" | "reminder-retained";
76
+ at: string;
77
+ sessionId?: string;
78
+ }) => void): void;
79
+ private qaReminderEvent;
80
+ private instructionWithPendingQaReminder;
61
81
  buildInstruction(instruction: string): string;
62
82
  buildAppServerArgs(): string[];
63
83
  /** Deprecated fixture helper. Runtime execution uses only app-server. */
@@ -89,6 +109,7 @@ export declare class CodexAdapter implements BuilderAdapter {
89
109
  observeEvents(listener: (event: BuilderEvent) => void): () => void;
90
110
  events(): AsyncIterable<BuilderEvent>;
91
111
  close(): Promise<void>;
112
+ private closeInternal;
92
113
  private ensureThread;
93
114
  private ensureThreadInternal;
94
115
  private threadAttached;