@claudexor/orchestrator 3.1.2 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +3 -1
  2. package/dist/attemptFinalize.d.ts +14 -11
  3. package/dist/attemptFinalize.d.ts.map +1 -1
  4. package/dist/attemptFinalize.js +41 -14
  5. package/dist/attemptFinalize.js.map +1 -1
  6. package/dist/attemptTelemetry.d.ts +7 -0
  7. package/dist/attemptTelemetry.d.ts.map +1 -1
  8. package/dist/attemptTelemetry.js +2 -1
  9. package/dist/attemptTelemetry.js.map +1 -1
  10. package/dist/attemptUsageCost.d.ts +17 -1
  11. package/dist/attemptUsageCost.d.ts.map +1 -1
  12. package/dist/attemptUsageCost.js +26 -2
  13. package/dist/attemptUsageCost.js.map +1 -1
  14. package/dist/budgetFailure.js.map +1 -1
  15. package/dist/credential-profile-rotation.d.ts +148 -0
  16. package/dist/credential-profile-rotation.d.ts.map +1 -0
  17. package/dist/credential-profile-rotation.js +322 -0
  18. package/dist/credential-profile-rotation.js.map +1 -0
  19. package/dist/credential-profiles.d.ts +38 -134
  20. package/dist/credential-profiles.d.ts.map +1 -1
  21. package/dist/credential-profiles.js +74 -296
  22. package/dist/credential-profiles.js.map +1 -1
  23. package/dist/decisionBudget.d.ts +2 -2
  24. package/dist/decisionBudget.d.ts.map +1 -1
  25. package/dist/deepScanReducer.d.ts +14 -44
  26. package/dist/deepScanReducer.d.ts.map +1 -1
  27. package/dist/deepScanReducer.js +240 -128
  28. package/dist/deepScanReducer.js.map +1 -1
  29. package/dist/delegationBudgetAuthority.d.ts.map +1 -1
  30. package/dist/delegationBudgetAuthority.js.map +1 -1
  31. package/dist/delegationFailure.d.ts.map +1 -1
  32. package/dist/git-precondition.d.ts +11 -0
  33. package/dist/git-precondition.d.ts.map +1 -0
  34. package/dist/git-precondition.js +69 -0
  35. package/dist/git-precondition.js.map +1 -0
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/interaction.d.ts +14 -6
  41. package/dist/interaction.d.ts.map +1 -1
  42. package/dist/interaction.js +106 -19
  43. package/dist/interaction.js.map +1 -1
  44. package/dist/orchestrator.d.ts +51 -83
  45. package/dist/orchestrator.d.ts.map +1 -1
  46. package/dist/orchestrator.js +558 -704
  47. package/dist/orchestrator.js.map +1 -1
  48. package/dist/planBrief.d.ts +27 -0
  49. package/dist/planBrief.d.ts.map +1 -0
  50. package/dist/planBrief.js +42 -0
  51. package/dist/planBrief.js.map +1 -0
  52. package/dist/planQuestions.js +4 -1
  53. package/dist/planQuestions.js.map +1 -1
  54. package/dist/planRun.d.ts +4 -3
  55. package/dist/planRun.d.ts.map +1 -1
  56. package/dist/planRun.js +33 -33
  57. package/dist/planRun.js.map +1 -1
  58. package/dist/plannerAttempt.d.ts +64 -0
  59. package/dist/plannerAttempt.d.ts.map +1 -0
  60. package/dist/plannerAttempt.js +235 -0
  61. package/dist/plannerAttempt.js.map +1 -0
  62. package/dist/policyFindings.js.map +1 -1
  63. package/dist/requestRequirements.d.ts +28 -1
  64. package/dist/requestRequirements.d.ts.map +1 -1
  65. package/dist/requestRequirements.js +89 -10
  66. package/dist/requestRequirements.js.map +1 -1
  67. package/dist/reviewerPanel.js.map +1 -1
  68. package/dist/root-ledger.d.ts.map +1 -1
  69. package/dist/root-ledger.js +2 -0
  70. package/dist/root-ledger.js.map +1 -1
  71. package/dist/run-bounded.d.ts.map +1 -1
  72. package/dist/run-bounded.js +7 -1
  73. package/dist/run-bounded.js.map +1 -1
  74. package/dist/run-input-resolution.d.ts.map +1 -1
  75. package/dist/run-input-resolution.js +26 -8
  76. package/dist/run-input-resolution.js.map +1 -1
  77. package/dist/runEventLog.d.ts +48 -0
  78. package/dist/runEventLog.d.ts.map +1 -0
  79. package/dist/runEventLog.js +63 -0
  80. package/dist/runEventLog.js.map +1 -0
  81. package/dist/runFacts.d.ts +23 -0
  82. package/dist/runFacts.d.ts.map +1 -0
  83. package/dist/runFacts.js +179 -0
  84. package/dist/runFacts.js.map +1 -0
  85. package/dist/runFactsBuilder.d.ts +5 -0
  86. package/dist/runFactsBuilder.d.ts.map +1 -0
  87. package/dist/runFactsBuilder.js +412 -0
  88. package/dist/runFactsBuilder.js.map +1 -0
  89. package/dist/runFactsDeliverable.d.ts +13 -0
  90. package/dist/runFactsDeliverable.d.ts.map +1 -0
  91. package/dist/runFactsDeliverable.js +94 -0
  92. package/dist/runFactsDeliverable.js.map +1 -0
  93. package/dist/runFactsPresentation.d.ts +7 -0
  94. package/dist/runFactsPresentation.d.ts.map +1 -0
  95. package/dist/runFactsPresentation.js +113 -0
  96. package/dist/runFactsPresentation.js.map +1 -0
  97. package/dist/runFactsReview.d.ts +7 -0
  98. package/dist/runFactsReview.d.ts.map +1 -0
  99. package/dist/runFactsReview.js +63 -0
  100. package/dist/runFactsReview.js.map +1 -0
  101. package/dist/runSupport.d.ts +17 -15
  102. package/dist/runSupport.d.ts.map +1 -1
  103. package/dist/runSupport.js +16 -0
  104. package/dist/runSupport.js.map +1 -1
  105. package/dist/runTerminalContext.d.ts +31 -0
  106. package/dist/runTerminalContext.d.ts.map +1 -0
  107. package/dist/runTerminalContext.js +12 -0
  108. package/dist/runTerminalContext.js.map +1 -0
  109. package/dist/runTerminalResults.d.ts +55 -0
  110. package/dist/runTerminalResults.d.ts.map +1 -0
  111. package/dist/runTerminalResults.js +134 -0
  112. package/dist/runTerminalResults.js.map +1 -0
  113. package/dist/runTerminals.d.ts +4 -80
  114. package/dist/runTerminals.d.ts.map +1 -1
  115. package/dist/runTerminals.js +357 -260
  116. package/dist/runTerminals.js.map +1 -1
  117. package/dist/secretDiff.d.ts +3 -0
  118. package/dist/secretDiff.d.ts.map +1 -1
  119. package/dist/secretDiff.js +7 -0
  120. package/dist/secretDiff.js.map +1 -1
  121. package/dist/structuredOutput.d.ts.map +1 -1
  122. package/dist/structuredOutput.js.map +1 -1
  123. package/dist/task-contract-builder.d.ts +35 -0
  124. package/dist/task-contract-builder.d.ts.map +1 -0
  125. package/dist/task-contract-builder.js +114 -0
  126. package/dist/task-contract-builder.js.map +1 -0
  127. package/dist/transientClassify.js.map +1 -1
  128. package/package.json +17 -17
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
- import { observeNativeSessionEvent, preflightCredentialProfile, preflightDefaultSubject, resolveCredentialProfile, resumeSessionForProfile, rotateSpecOnTypedLimit, selectedProfileAvailability, } from "./credential-profiles.js";
2
+ import { effectiveAuthPreference, observeNativeSessionEvent, preflightCredentialProfile, preflightDefaultSubject, probeCredentialProfileStatus, profileHeadroomBreach, profileStatusAdmits, resolveCredentialProfile, resumeSessionForProfile, rotateSpecOnTypedLimit, selectedProfileAvailability, staticRotationCandidates, } from "./credential-profiles.js";
3
3
  import { writeRunTelemetryArtifact } from "./runTelemetryWriter.js";
4
4
  import { buildFileBackedSynthesisInput, materializeWinnerOutputs, stageFileBackedContext, writeCandidateAttemptArtifacts, } from "./candidateOutputs.js";
5
5
  import { processAttemptUsage } from "./attemptUsage.js";
@@ -9,9 +9,10 @@ import { capabilityIntents } from "@claudexor/gateway";
9
9
  import { policyFindings } from "./policyFindings.js";
10
10
  import { join } from "node:path";
11
11
  import { finalizePlanRun, runCouncilPlan, writePlanHarnessFailure, } from "./planRun.js";
12
- import { HarnessRunSpec, ModeKind as ModeKindSchema, SCHEMA_VERSION, TRUST_FULL_ACCESS_CODE, FrozenTaskContractArtifact as TaskContractSchema, isBlocking, makeOutcomeFacts, normalizeUserOutputSchema, strictifyOutputSchema, estimateEffectiveAuthRoute, } from "@claudexor/schema";
13
- import { globalConfigDir, loadConfig, trustConfigPath } from "@claudexor/config";
14
- import { AnswerAssembly, CLAUDEXOR_ARTIFACT_DIR, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
12
+ import { runPlannerAttempt as executePlannerAttempt, } from "./plannerAttempt.js";
13
+ import { HarnessRunSpec, ModeKind as ModeKindSchema, QuotaSnapshot as QuotaSnapshotSchema, isBlocking, makeOutcomeFacts, normalizeUserOutputSchema, strictifyOutputSchema, estimateEffectiveAuthRoute, } from "@claudexor/schema";
14
+ import { globalConfigDir, loadConfig } from "@claudexor/config";
15
+ import { AnswerAssembly, CLAUDEXOR_ARTIFACT_DIR, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, countsAsAgentProgress, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
15
16
  import { assertRouteModelsAllowed } from "./modelGovernance.js";
16
17
  import { governRouteEffort } from "./effortGovernance.js";
17
18
  import { isFullAccess, RequestRequirementsResolver } from "./requestRequirements.js";
@@ -20,16 +21,18 @@ import { routingFailureClassification } from "./routing-failure.js";
20
21
  export { routingFailureClassification } from "./routing-failure.js";
21
22
  import { runBounded } from "./run-bounded.js";
22
23
  import { planPrompt } from "./plan-prompt.js";
24
+ import { verifiedPlanBrief, withPlanBrief } from "./planBrief.js";
23
25
  import { resolveRunInputDefaults } from "./run-input-resolution.js";
24
- import { createRootLedger } from "./root-ledger.js";
26
+ import { beginAnnouncedRun } from "./runEventLog.js";
25
27
  import { arbitrationBudgetOptions, decisionBudgetSummary } from "./decisionBudget.js";
26
28
  import { buildRevisePrompt } from "./revisePrompt.js";
27
- import { announcedRunContext, cancelledResult, failTerminally, guardAnnouncedRun, writeFailure, } from "./runTerminals.js";
29
+ import { cancelledResult, failTerminally, guardAnnouncedRun, writeFailure, } from "./runTerminals.js";
28
30
  import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
29
31
  import { assertOutputSchemaCompiles, finalizeStructuredOutput } from "./structuredOutput.js";
30
- import { transientRetryDelayMs, promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, safeErrorMessage, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, writeRaceDeliveryDecision, } from "./runSupport.js";
32
+ import { transientRetryDelayMs, promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, safeErrorMessage, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, winnerNeedsHuman, writeRaceDeliveryDecision, } from "./runSupport.js";
31
33
  import { candidateStatusInRouteContext, resolveReadOnlyRouteContext, } from "./routeContext.js";
32
34
  import { resolveAutoReviewerPanel, resolveExplicitReviewerPanel } from "./reviewerPanel.js";
35
+ import { ensureWriteModeGitBoundary } from "./git-precondition.js";
33
36
  import { buildContinuation, } from "./continuity.js";
34
37
  import { activePlanPointer, resolveContinuitySummary, workspaceAnchor, } from "./continuity-facts.js";
35
38
  import { runDiffReview } from "./diffReview.js";
@@ -41,18 +44,18 @@ import { dominantHarnessFailureCategory, harnessFailureNextActions } from "./har
41
44
  import { finalizeAttempt, readOnlyNoSuccessTerminal, resolveWorkReportEnvelope, unrecoveredToolErrorFailure, unwrapWorkReportEnvelope, webEvidenceFailure, } from "./attemptFinalize.js";
42
45
  import { buildContinuationPacket, decideContinuation, synthesizeContinuationRequest, } from "./continuation.js";
43
46
  import { interactionChannelFor } from "./interaction.js";
44
- import { gateSpecsFromContract, renderTestsEvidence, resolveContractGates, } from "./contract-gates.js";
47
+ import { gateSpecsFromContract, renderTestsEvidence } from "./contract-gates.js";
48
+ import { buildTaskContract } from "./task-contract-builder.js";
45
49
  import { ArtifactStore } from "@claudexor/artifact-store";
46
- import { EventLog } from "@claudexor/event-log";
47
50
  import { assertMandatoryContext, buildContextPack, rawContextForEnvelope, preflightEvidence, writeEvidencePacket, } from "@claudexor/context";
48
- import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, ensureClaudeBridge, ensureGitRepository, consumeRawPatchEnvelope, snapshotTree, } from "@claudexor/workspace";
51
+ import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, ensureClaudeBridge, consumeRawPatchEnvelope, snapshotTree, } from "@claudexor/workspace";
49
52
  import { blockedDecisionOverride, finalVerifyBlocks, finalVerifyPatch, verifyAndDeliver, } from "@claudexor/delivery";
50
53
  import { HarnessGateway } from "@claudexor/gateway";
51
54
  import { ReadinessLedger, evaluateConvergence, failureSignature, gatesPassed, reviewCandidate, revalidateFindings, runGates, } from "@claudexor/review";
52
55
  import { arbitrate } from "@claudexor/arbitration";
53
56
  import { buildSynthesisPlan, decideSynthesis } from "@claudexor/synthesis";
54
57
  import { attemptCostEvidence, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, explainRanking, loadHarnessMetrics, promptFingerprint, rankHarnesses, reviewUsageCostSettlement, } from "@claudexor/budget";
55
- import { readTextSafe, appendLine, assertNoInlineSecretValues, containsSecretLikeToken, DELEGATION_ENV, hashJson, newId, noProjectRepoRoot, nowIso, redactSecrets, safeInvoke, sha256, userConfigDir, writeText, } from "@claudexor/util";
58
+ import { readTextSafe, appendLine, assertNoInlineSecretValues, DELEGATION_ENV, hashJson, newId, noProjectRepoRoot, redactSecrets, safeInvoke, sha256, userConfigDir, writeText, } from "@claudexor/util";
56
59
  import { assertWriteIsolation } from "./write-isolation.js";
57
60
  const LABELS = "ABCDEFGHIJ".split("");
58
61
  const NO_PROJECT_ROOT = noProjectRepoRoot();
@@ -110,10 +113,14 @@ export class Orchestrator {
110
113
  throw new Error(`Delegate is an agent-only strategy (got mode=${mode})`);
111
114
  }
112
115
  const runId = resolved.runId ?? newId("run");
116
+ let delegatedLedgerAttached = false;
113
117
  resolved = {
114
118
  ...resolved,
115
119
  runId,
116
120
  taskId: resolved.taskId ?? newId("task"),
121
+ onDelegatedLedgerAttached: () => {
122
+ delegatedLedgerAttached = true;
123
+ },
117
124
  };
118
125
  if (resolved.delegate === true) {
119
126
  resolved = {
@@ -157,50 +164,63 @@ export class Orchestrator {
157
164
  if (resolved.repoRoot !== NO_PROJECT_ROOT) {
158
165
  assertMandatoryContext(resolved.repoRoot, this.projectConfig(resolved.repoRoot).context.mandatory_files);
159
166
  }
160
- // Reviewer panels are validated only inside the strategies that actually
161
- // review (race/convergence under agent, and plan) — AFTER run-dir
167
+ // Reviewer panels are validated only inside Agent strategies that actually
168
+ // review (race/convergence; Plan Council is the plan critique path) — AFTER run-dir
162
169
  // creation, so a doomed explicit panel yields typed failure ARTIFACTS
163
170
  // (failure.yaml naming the refusal) instead of a bare pre-run throw.
164
- // ask/audit never spawn reviewers, so a panel there never spends doctor/
171
+ // Ask and Plan never spawn code reviewers, so a panel there never spends doctor/
165
172
  // model probes and never fails a run that would not use it.
166
173
  // Whole-strategy terminal net: once a strategy ANNOUNCES its
167
174
  // run, any escaped throw still stamps failure.yaml + summary + run.failed
168
175
  // instead of orphaning events.jsonl.
169
- return guardAnnouncedRun(resolved.signal, (announce) => {
170
- switch (mode) {
171
- case "ask":
172
- // `--deep-scan` widens the answer into the bounded multi-scout
173
- // research sweep with synthesis (the old `audit --swarm`/`explore`).
174
- return resolved.deepScan
175
- ? this.runDeepScan(resolved, announce)
176
- : this.runAsk(resolved, announce);
177
- case "agent":
178
- // Engine strategies are FLAGS on agent (v0.9 collapse): `--until-clean`
179
- // and `--attempts` select the convergence loop; `--n` selects the race
180
- // width; `--create` switches the candidate intent to create_from_scratch.
181
- if (resolved.untilClean)
182
- return this.runConvergence(resolved, mode, null, announce);
183
- if (resolved.attempts !== undefined && resolved.attempts !== null) {
184
- return this.runConvergence(resolved, mode, resolved.attempts, announce);
185
- }
186
- return this.runRace({ ...resolved, n: resolved.n ?? 1 }, mode, announce);
187
- case "plan":
188
- return this.runPlan(resolved, announce);
176
+ const releaseRunState = (settledRunId) => {
177
+ this.routingRationaleByRun.delete(settledRunId);
178
+ this.deps.delegationBudgetAuthority?.releaseRun(settledRunId);
179
+ };
180
+ try {
181
+ return await guardAnnouncedRun(resolved.signal, (announce) => {
182
+ switch (mode) {
183
+ case "ask":
184
+ // `--deep-scan` widens the answer into the bounded multi-scout
185
+ // research sweep with synthesis (the old `audit --swarm`/`explore`).
186
+ return resolved.deepScan
187
+ ? this.runDeepScan(resolved, announce)
188
+ : this.runAsk(resolved, announce);
189
+ case "agent":
190
+ // Engine strategies are FLAGS on agent (v0.9 collapse): `--until-clean`
191
+ // and `--attempts` select the convergence loop; `--n` selects the race
192
+ // width; `--create` switches the candidate intent to create_from_scratch.
193
+ if (resolved.untilClean)
194
+ return this.runConvergence(resolved, mode, null, announce);
195
+ if (resolved.attempts !== undefined && resolved.attempts !== null) {
196
+ return this.runConvergence(resolved, mode, resolved.attempts, announce);
197
+ }
198
+ return this.runRace({ ...resolved, n: resolved.n ?? 1 }, mode, announce);
199
+ case "plan":
200
+ return this.runPlan(resolved, announce);
201
+ }
202
+ }, async ({ runId }) => {
203
+ const authority = this.deps.delegationBudgetAuthority;
204
+ if (!authority?.hasParent(runId))
205
+ return;
206
+ authority.beginParentClose(runId);
207
+ await authority.waitForChildren(runId);
208
+ },
209
+ // Single per-run terminalization hook: release the routing-rationale map
210
+ // entry on EVERY terminal (incl. a run that died before its telemetry
211
+ // writer ran, which is the leak this closes).
212
+ releaseRunState);
213
+ }
214
+ catch (error) {
215
+ // A durable startup sink may refuse `run.created` before the strategy can
216
+ // announce its context. Release only a delegated child ledger acquired by
217
+ // THIS invocation; a caller-supplied run-id collision must never release
218
+ // another live parent/child authority or its routing state.
219
+ if (delegatedLedgerAttached) {
220
+ this.deps.delegationBudgetAuthority?.releaseRun(runId);
189
221
  }
190
- }, async ({ runId }) => {
191
- const authority = this.deps.delegationBudgetAuthority;
192
- if (!authority?.hasParent(runId))
193
- return;
194
- authority.beginParentClose(runId);
195
- await authority.waitForChildren(runId);
196
- },
197
- // Single per-run terminalization hook: release the routing-rationale map
198
- // entry on EVERY terminal (incl. a run that died before its telemetry
199
- // writer ran, which is the leak this closes).
200
- (runId) => {
201
- this.routingRationaleByRun.delete(runId);
202
- this.deps.delegationBudgetAuthority?.releaseRun(runId);
203
- });
222
+ throw error;
223
+ }
204
224
  }
205
225
  async resolveReviewers(cwd, runAuthPreference, onIgnoredSetting) {
206
226
  if (this.deps.reviewers)
@@ -273,11 +293,7 @@ export class Orchestrator {
273
293
  }
274
294
  authPreferenceForHarness(repoRoot, harnessId, runAuthPreference) {
275
295
  const cfg = this.config(repoRoot)?.global;
276
- const explicit = (v) => v && v !== "auto" ? v : undefined;
277
- return (explicit(runAuthPreference) ??
278
- explicit(cfg?.harnesses?.[harnessId]?.auth_preference) ??
279
- explicit(cfg?.routing?.auth_preference) ??
280
- "auto");
296
+ return effectiveAuthPreference(runAuthPreference, cfg?.harnesses?.[harnessId]?.auth_preference, cfg?.routing?.auth_preference);
281
297
  }
282
298
  artifactStore(input) {
283
299
  if (input.mode === "ask" && input.contextMode === "off" && input.repoRoot === NO_PROJECT_ROOT) {
@@ -300,20 +316,25 @@ export class Orchestrator {
300
316
  estimateUsdFloor(repoRoot) {
301
317
  return this.config(repoRoot)?.global.budget.estimate_usd_floor ?? 0.05;
302
318
  }
319
+ /** Delegate children overlap their still-running parent, so child-side paid
320
+ * units use the same bounded floor as later slots in a parallel wave. */
321
+ reservationEstimateUsd(input, parallel = false) {
322
+ return parallel || Boolean(input.delegatedFromRunId)
323
+ ? this.estimateUsdFloor(input.repoRoot)
324
+ : undefined;
325
+ }
303
326
  execRootOf(input) {
304
327
  return input.executionRoot ?? input.repoRoot;
305
328
  }
306
- sessionSpecFields(input, harnessId, log) {
329
+ async sessionSpecFields(input, harnessId, model, log, defaultRoute = null, quotaAdmission) {
307
330
  const cfg = this.config(input.repoRoot)?.global;
308
- const profile = this.preflightProfile(input, harnessId, log);
309
- const explicit = (v) => (v && v !== "auto" ? v : undefined);
331
+ const profile = quotaAdmission?.model === model
332
+ ? quotaAdmission.profile
333
+ : await this.preflightProfile(input, harnessId, model, log, defaultRoute);
310
334
  return {
311
335
  // "auto" at ANY level falls through (thread turns send the thread default
312
336
  // "auto" as a per-run value; it must not shadow a configured preference).
313
- auth_preference: explicit(input.authPreference) ??
314
- explicit(cfg?.harnesses?.[harnessId]?.auth_preference) ??
315
- explicit(cfg?.routing?.auth_preference) ??
316
- "auto",
337
+ auth_preference: effectiveAuthPreference(input.authPreference, cfg?.harnesses?.[harnessId]?.auth_preference, cfg?.routing?.auth_preference),
317
338
  resume_session_id: resumeSessionForProfile(input.resumeSessions?.[harnessId], profile),
318
339
  credential_profile: profile,
319
340
  };
@@ -388,19 +409,62 @@ export class Orchestrator {
388
409
  const policy = this.config(repoRoot)?.global.harnesses?.[harnessId]?.profile_policy;
389
410
  return policy ?? { limit_action: "fail", rotation_eligible: [], headroom_threshold: 0.9 };
390
411
  }
391
- preflightProfile(input, harnessId, log) {
412
+ /** Fresh profile readiness for one rotation decision epoch. Accounts uses
413
+ * the same probe wrapper + admission predicate when projecting next_up. */
414
+ async readyProfileIdsForRotation(input, harnessId, current, excluded = new Set()) {
415
+ const profiles = staticRotationCandidates({
416
+ registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
417
+ harnessId,
418
+ policy: this.profilePolicy(input.repoRoot, harnessId),
419
+ current,
420
+ excluded,
421
+ });
422
+ const adapter = this.deps.registry.get(harnessId);
423
+ const entries = await Promise.all(profiles.map(async (profile) => ({
424
+ profile,
425
+ status: await probeCredentialProfileStatus(profile, adapter?.probeCredentialProfile?.bind(adapter)),
426
+ })));
427
+ return new Set(entries
428
+ .filter(({ profile, status }) => profileStatusAdmits(profile, status))
429
+ .map(({ profile }) => profile.profile_id));
430
+ }
431
+ async preflightProfile(input, harnessId, model, log, defaultRoute) {
392
432
  const profile = this.resolveCredentialProfile(input, harnessId);
393
433
  const policy = this.profilePolicy(input.repoRoot, harnessId);
394
434
  const registry = this.config(input.repoRoot)?.global.credential_profiles ?? [];
395
435
  const snapshots = this.deps.quotaSnapshots?.() ?? [];
396
436
  const emit = (type, payload) => log?.emit(type, payload);
437
+ const breach = profileHeadroomBreach(snapshots, harnessId, profile?.profile_id ?? null, policy.headroom_threshold, model);
438
+ const readyProfileIds = policy.limit_action === "rotate" &&
439
+ breach !== null &&
440
+ (profile !== null || defaultRoute === "local_session")
441
+ ? await this.readyProfileIdsForRotation(input, harnessId, profile)
442
+ : new Set();
397
443
  if (!profile) {
398
444
  // Unpinned runs (INV-135 auto-balance): under `rotate`, a fresh
399
445
  // default-subject headroom breach starts on the next eligible
400
446
  // subscription profile instead; `fail`/`ask` change nothing.
401
- return preflightDefaultSubject({ harnessId, policy, registry, snapshots, emit });
447
+ return preflightDefaultSubject({
448
+ harnessId,
449
+ policy,
450
+ registry,
451
+ snapshots,
452
+ readyProfileIds,
453
+ defaultRoute,
454
+ model,
455
+ emit,
456
+ });
402
457
  }
403
- return preflightCredentialProfile({ profile, harnessId, policy, registry, snapshots, emit });
458
+ return preflightCredentialProfile({
459
+ profile,
460
+ harnessId,
461
+ policy,
462
+ registry,
463
+ snapshots,
464
+ readyProfileIds,
465
+ model,
466
+ emit,
467
+ });
404
468
  }
405
469
  /**
406
470
  * Resolve candidate adapters: explicit `--harness`, else available real harnesses, then
@@ -477,6 +541,27 @@ export class Orchestrator {
477
541
  }
478
542
  }
479
543
  }
544
+ const attachments = input.attachments ?? [];
545
+ // Resolve the complete attachment pool once from manifest truth. If any
546
+ // lane still lacks usable discovery truth, preserve the existing per-lane
547
+ // discovery/doctor precedence and resolve that lane inside the route loop.
548
+ const canResolveAttachmentPool = attachments.length > 0 &&
549
+ ids.every((id) => {
550
+ const status = statusById.get(id);
551
+ return status?.manifest != null && status.status !== "unavailable";
552
+ });
553
+ const attachmentPoolAdmission = canResolveAttachmentPool
554
+ ? this.requestRequirements.resolveAttachmentPool(explicitPool ? "explicit" : "auto", attachments, ids.map((id) => ({
555
+ harnessId: id,
556
+ declarations: statusById.get(id)?.manifest?.capability_profile.attachment_inputs ?? null,
557
+ available: true,
558
+ })))
559
+ : null;
560
+ if (attachmentPoolAdmission?.outcome === "refused") {
561
+ throw new HarnessUnavailableError(attachmentPoolAdmission.message ??
562
+ "no available harness lane can receive the selected attachments");
563
+ }
564
+ const attachmentRejectionById = new Map(attachmentPoolAdmission?.rejected.map((admission) => [admission.harnessId, admission]) ?? []);
480
565
  const policy = input.web ?? input.externalContextPolicy ?? "auto";
481
566
  const pool = [];
482
567
  const dropped = [];
@@ -610,9 +695,10 @@ export class Orchestrator {
610
695
  dropLane(id, "web", why);
611
696
  continue;
612
697
  }
613
- const attachmentRefusal = this.requestRequirements.attachmentRefusal(id, input.attachments ?? [], manifest.capability_profile.attachment_inputs);
614
- if (attachmentRefusal) {
615
- dropLane(id, "attachment", attachmentRefusal);
698
+ const attachmentAdmission = attachmentRejectionById.get(id) ??
699
+ this.requestRequirements.resolveAttachmentLane(id, attachments, manifest.capability_profile.attachment_inputs);
700
+ if (!attachmentAdmission.admitted) {
701
+ dropLane(id, "attachment", attachmentAdmission.message ?? `${id} rejects attachments`);
616
702
  continue;
617
703
  }
618
704
  const reason = status.reasons.length > 0 ? `: ${status.reasons.join("; ")}` : "";
@@ -646,6 +732,7 @@ export class Orchestrator {
646
732
  // the default store's sources apply only to profile-less runs.
647
733
  authRouteEstimate: this.profileAuthRoute(input, id) ??
648
734
  estimateEffectiveAuthRoute(this.authPreferenceForHarness(input.repoRoot, id, input.authPreference), status.authSources),
735
+ quotaAdmission: { model: null, profile: null, route: null },
649
736
  supportsSynthesize: manifest.capabilities.synthesize,
650
737
  supportsInteractive: manifest.capabilities.interactive,
651
738
  supportsJsonSchemaOutput: manifest.capabilities.json_schema_output,
@@ -680,11 +767,29 @@ export class Orchestrator {
680
767
  if (pool.length === 0) {
681
768
  throw new HarnessUnavailableError(`no harness can perform '${intent}' for this mode${dropped.length ? ` (skipped: ${dropped.join(", ")})` : ""}`);
682
769
  }
683
- const ordered = this.orderPool(pool, input, intent, statusById, ledger, runId);
770
+ // Quota admission must use the account that will actually spawn. In
771
+ // particular, an opt-in default-subject rotation has to select its ready
772
+ // profile before the budget router filters the exhausted default away.
773
+ // The same resolved profile is reused by the first spec build below.
774
+ const quotaPreparedPool = await Promise.all(pool.map(async (routed) => {
775
+ const model = input.models?.[routed.adapter.id] ?? routed.settings?.defaultModel ?? null;
776
+ const profile = await this.preflightProfile(input, routed.adapter.id, model, log, routed.authRouteEstimate);
777
+ const route = profile
778
+ ? profile.credential_kind === "api_key"
779
+ ? "managed_api_key"
780
+ : "vendor_native"
781
+ : routed.authRouteEstimate === "api_key"
782
+ ? "managed_api_key"
783
+ : routed.authRouteEstimate === "local_session"
784
+ ? "vendor_native"
785
+ : null;
786
+ return { ...routed, quotaAdmission: { model, profile, route } };
787
+ }));
788
+ const ordered = this.orderPool(quotaPreparedPool, input, intent, statusById, ledger, runId);
684
789
  if (ordered.length === 0) {
685
790
  throw new HarnessUnavailableError(`no harness remains eligible for '${intent}' after budget and quota routing`);
686
791
  }
687
- emitPrimaryDivergence(log, input.primaryHarness, ordered, pool, dropped);
792
+ emitPrimaryDivergence(log, input.primaryHarness, ordered, quotaPreparedPool, dropped);
688
793
  const n = input.n ?? ordered.length;
689
794
  const selectionOrder = ordered;
690
795
  const out = [];
@@ -791,28 +896,25 @@ export class Orchestrator {
791
896
  : authModes.includes("api_key")
792
897
  ? "api_key"
793
898
  : "unknown";
794
- // A selected profile's credential_kind decides the route outright
795
- // (round-18 #2): an api_key profile must never inherit a
899
+ // The quota-preflight profile's credential_kind decides the route
900
+ // outright (round-18 #2). This includes an unpinned default subject
901
+ // that rotated before ranking; an api_key profile must never inherit a
796
902
  // subscription classification from the default store's metric.
797
- const authMode = this.profileAuthRoute(input, r.adapter.id) ??
903
+ const admittedProfileRoute = r.quotaAdmission.profile
904
+ ? r.quotaAdmission.profile.credential_kind === "api_key"
905
+ ? "api_key"
906
+ : "local_session"
907
+ : null;
908
+ const authMode = admittedProfileRoute ??
798
909
  (input.authPreference === "api_key"
799
910
  ? "api_key"
800
911
  : input.authPreference === "subscription"
801
912
  ? "local_session"
802
913
  : (metric?.last_auth_mode ?? guessedAuthMode));
803
- // The quota subject this candidate would actually run as (release
804
- // wave round-16 #2): the resolved profile id, or null for the engine
805
- // default so profile A's cooldown never excludes profile B or the
806
- // default. A profile that does not resolve for this harness routes
807
- // as unknown (undefined) and stays conservatively any-subject.
808
- let credentialSubjectId;
809
- try {
810
- credentialSubjectId =
811
- this.resolveCredentialProfile(input, r.adapter.id)?.profile_id ?? null;
812
- }
813
- catch {
814
- credentialSubjectId = undefined;
815
- }
914
+ // The exact quota subject selected before ranking: profile id or the
915
+ // engine default. Profile A's cooldown never excludes profile B or the
916
+ // default on the same harness and route.
917
+ const credentialSubjectId = r.quotaAdmission.profile?.profile_id ?? null;
816
918
  // QA-034: the typed auth-route evidence (doctor source verification x the
817
919
  // resolved route) is AUTHORITATIVE for billing knowledge in the router —
818
920
  // a VERIFIED native route proves subscription_entitlement, so it survives
@@ -823,20 +925,19 @@ export class Orchestrator {
823
925
  return {
824
926
  harnessId: r.adapter.id,
825
927
  available: true,
826
- model: input.models?.[r.adapter.id] ??
827
- config.harnesses[r.adapter.id]?.default_model ??
828
- undefined,
928
+ model: r.quotaAdmission.model,
829
929
  effort: input.efforts?.[r.adapter.id] ??
830
930
  input.effort ??
831
931
  config.harnesses[r.adapter.id]?.effort ??
832
932
  undefined,
833
933
  billingKnowledge: authMode === "api_key" ? "metered" : "unknown",
834
934
  incrementalCostUsd: authMode === "api_key" ? (metric?.avg_cost_usd ?? null) : null,
835
- credentialRoute: authMode === "api_key"
836
- ? "managed_api_key"
837
- : authMode === "local_session"
838
- ? "vendor_native"
839
- : undefined,
935
+ credentialRoute: r.quotaAdmission.route ??
936
+ (authMode === "api_key"
937
+ ? "managed_api_key"
938
+ : authMode === "local_session"
939
+ ? "vendor_native"
940
+ : undefined),
840
941
  ...(authRoute ? { authRoute } : {}),
841
942
  credentialSubjectId,
842
943
  };
@@ -884,6 +985,36 @@ export class Orchestrator {
884
985
  }
885
986
  return ordered;
886
987
  }
988
+ /** Harness-only convergence helpers still need the exact quota identity
989
+ * selected before ranking. This facade keeps their small interface while
990
+ * preventing a scoped limit on one model/account from cooling another. */
991
+ quotaLedgerView(ledger, routes) {
992
+ const byId = new Map(routes.map((route) => [route.adapter.id, route]));
993
+ const identity = (id) => {
994
+ const route = byId.get(id);
995
+ return route
996
+ ? {
997
+ credentialRoute: route.quotaAdmission.route ?? undefined,
998
+ credentialSubjectId: route.quotaAdmission.profile?.profile_id ?? null,
999
+ model: route.quotaAdmission.model,
1000
+ }
1001
+ : null;
1002
+ };
1003
+ return {
1004
+ bindingPaceSlack: (id) => {
1005
+ const selected = identity(id);
1006
+ return selected
1007
+ ? ledger.bindingPaceSlack(id, selected.credentialRoute, selected.credentialSubjectId, Date.now(), selected.model)
1008
+ : ledger.bindingPaceSlack(id);
1009
+ },
1010
+ cooldownActive: (id) => {
1011
+ const selected = identity(id);
1012
+ return selected
1013
+ ? ledger.cooldownActive(id, selected.credentialRoute, selected.credentialSubjectId, Date.now(), selected.model)
1014
+ : ledger.cooldownActive(id);
1015
+ },
1016
+ };
1017
+ }
887
1018
  /**
888
1019
  * Typed auth-route evidence for one candidate (QA-034): the concrete
889
1020
  * credential route the resolved auth mode maps to, plus the doctor's
@@ -981,119 +1112,9 @@ export class Orchestrator {
981
1112
  return this.config(repoRoot).project;
982
1113
  }
983
1114
  buildContract(input, taskId, mode) {
984
- const resolvedCfg = this.config(input.repoRoot);
985
- const cfg = resolvedCfg.project;
986
- const readOnlyMode = mode === "ask" || mode === "plan";
987
- const requestedAccess = input.access ?? (readOnlyMode ? "readonly" : resolvedCfg.trust.access_default);
988
- // Effective access is COMPUTED by the engine, never echoed from a client:
989
- // read-only modes clamp to readonly regardless of the request.
990
- const effectiveAccess = readOnlyMode ? "readonly" : requestedAccess;
991
- // TrustConfig is USER-LEVEL only (versioned repo config must never
992
- // self-grant sensitive powers): unsandboxed full access requires an
993
- // explicit allow in ~/.claudexor trust settings — loud error, no downgrade.
994
- // The gate applies to the EFFECTIVE profile: a read-only run clamped to
995
- // readonly never runs unsandboxed and needs no trust allow.
996
- if (effectiveAccess === "full" && !resolvedCfg.trust.allow_full_access) {
997
- // Typed refusal: the `code` rides the daemon job record onto the thread
998
- // turn (TurnEnqueueError.code), so surfaces key remedies on the CODE —
999
- // never on substring-matching this human message.
1000
- throw Object.assign(new Error(`access profile 'full' requires allow_full_access: true in the user-level trust file for this repo ` +
1001
- `(${trustConfigPath(input.repoRoot)}); enable it with \`claudexor trust --allow-full-access\` — refusing to run unsandboxed`),
1002
- // Refusal semantics are born at the throw (W24): the one-time grant is
1003
- // a 403, and the daemon persists this status onto the job record.
1004
- { code: TRUST_FULL_ACCESS_CODE, status: 403 });
1005
- }
1006
- const externalContextPolicy = input.web ?? input.externalContextPolicy ?? "auto";
1007
- // Deterministic gate commands come from explicit run input, then versioned
1008
- // project config. Without these, gateSpecs is empty and convergence is
1009
- // review-only; with them, convergence is test-driven.
1010
- const resolvedGates = resolveContractGates({
1011
- repoRoot: input.repoRoot,
1012
- effectiveAccess,
1013
- config: cfg,
1014
- trustGrants: resolvedCfg.trust.test_command_grants,
1015
- operatorCommands: input.tests ?? [],
1016
- projectCommands: cfg.tests?.commands ?? [],
1017
- });
1018
- const commands = resolvedGates.commands;
1019
- const protectedPaths = [...new Set(cfg.constraints.protected_paths)];
1020
- const autoProtectedPaths = resolvedGates.autoProtectedPaths;
1021
- const protectedPathApprovals = [
1022
- ...new Map([...(input.protectedPathApprovals ?? [])].map((approval) => [approval.path, approval])).values(),
1023
- ];
1024
- return TaskContractSchema.parse({
1025
- schema_version: SCHEMA_VERSION,
1026
- task_id: taskId,
1027
- created_at: nowIso(),
1028
- repo: { root: input.repoRoot, base_ref: input.baseRef ?? "HEAD", dirty_policy: "snapshot" },
1029
- mode: { kind: mode },
1030
- delegation_requested: input.delegate === true,
1031
- run_lineage: {
1032
- parent_run_id: input.parentRunId ?? null,
1033
- delegated_from_run_id: input.delegatedFromRunId ?? null,
1034
- },
1035
- user_intent: { raw: redactSecrets(input.prompt) },
1036
- // Redacted for symmetry with user_intent.raw — a no-op on fenced input
1037
- // (the inline-secret fence already blocked any secret-like value at every
1038
- // ingress incl. this engine boundary), so task-producing lanes read back
1039
- // the real instructions via harnessSpecKnobs().
1040
- instructions: input.instructions === undefined ? undefined : redactSecrets(input.instructions),
1041
- // Already normalized/strictified at the engine boundary (run() refuses
1042
- // unsupported shapes before any run dir exists).
1043
- output_schema: input.outputSchema ?? null,
1044
- auth_preference: input.authPreference ?? "auto",
1045
- credential_profile_id: input.credentialProfileId ?? null,
1046
- max_turns: input.maxTurns ?? null,
1047
- constraints: {
1048
- protected_paths: protectedPaths,
1049
- deny_paths: [...new Set(input.denyPaths ?? [])],
1050
- auto_protected_paths: autoProtectedPaths,
1051
- protected_path_approvals: protectedPathApprovals,
1052
- },
1053
- tests: { commands },
1054
- access: {
1055
- requested_profile: requestedAccess,
1056
- effective_profile: effectiveAccess,
1057
- },
1058
- external_context: {
1059
- policy: externalContextPolicy,
1060
- web_required: externalContextPolicy === "cached" || externalContextPolicy === "live",
1061
- // Per-route upgrades (e.g. claude cached->live) are disclosed in events
1062
- // and telemetry.yaml; the immutable contract records the requested policy.
1063
- effective_mode: externalContextPolicy,
1064
- },
1065
- // Harness-native tool names are adapter knowledge; the neutral contract
1066
- // carries only the policy plus user-configured allow/deny lists (wired
1067
- // from per-harness settings).
1068
- tool_permission_policy: {
1069
- web: externalContextPolicy,
1070
- allow: [],
1071
- deny: [],
1072
- },
1073
- budget: {
1074
- routing_goal: input.routingGoal ?? this.deps.routingGoal ?? cfg?.budget?.routing_goal ?? "auto",
1075
- paid_budget: this.resolvePaidBudget(input.paidBudget, resolvedCfg),
1076
- },
1077
- // The resolved harness-scoped model map (scalar already expanded to the
1078
- // primary by resolveRunInput). The contract is what route spec building
1079
- // reads — there is no run-global model (INV-103).
1080
- routing_models: input.models ?? {},
1081
- // QA-035: freeze the RESOLVED reasoning-effort per known lane so Exact
1082
- // Retry replays it instead of re-resolving current settings. Precedence
1083
- // (specific beats general): the harness-scoped `efforts` map entry, then a
1084
- // per-turn scalar `input.effort`, then the harness settings default — the
1085
- // same map that Exact Retry replays so a NON-PRIMARY lane keeps its own
1086
- // frozen effort (QA-035 completeness). Only known-pool lanes are frozen
1087
- // here (a pure auto pool's lanes resolve later — documented seam).
1088
- routing_efforts: Object.fromEntries([...new Set([...(input.harnesses ?? []), ...Object.keys(input.efforts ?? {})])]
1089
- .map((hid) => [
1090
- hid,
1091
- input.efforts?.[hid] ??
1092
- input.effort ??
1093
- resolvedCfg.global.harnesses?.[hid]?.effort ??
1094
- null,
1095
- ])
1096
- .filter((entry) => entry[1] !== null)),
1115
+ return buildTaskContract(input, taskId, mode, {
1116
+ paidBudget: this.deps.paidBudget,
1117
+ routingGoal: this.deps.routingGoal,
1097
1118
  });
1098
1119
  }
1099
1120
  /**
@@ -1172,9 +1193,9 @@ export class Orchestrator {
1172
1193
  /**
1173
1194
  * D-16: the WorkReport transport envelope for one route. Called at every
1174
1195
  * task-producing spec-build site AFTER harnessSpecKnobs so it OVERRIDES the
1175
- * plain caller-schema transport with the compiled `{work_report, output}`
1176
- * envelope on capable routes. The returned `mode` is retained by the caller
1177
- * and handed to `unwrapWorkReportEnvelope` when the answer is finalized.
1196
+ * plain caller-schema transport with the resolved WorkReport channel on
1197
+ * capable routes. The returned `mode` is retained by the caller and handed
1198
+ * to `unwrapWorkReportEnvelope` when the answer is finalized.
1178
1199
  */
1179
1200
  workReportEnvelopeFor(routed, contract, interactive) {
1180
1201
  return resolveWorkReportEnvelope({
@@ -1188,7 +1209,7 @@ export class Orchestrator {
1188
1209
  /**
1189
1210
  * D-16: apply the resolved WorkReport transport to a built spec — set the
1190
1211
  * envelope output_schema (constrained/side_tool routes) and APPEND the fenced
1191
- * envelope instruction (validated routes, e.g. cursor). Mutates the spec in
1212
+ * metadata instruction (validated routes, e.g. cursor). Mutates the spec in
1192
1213
  * place and returns the mode the answer unwrap consumes. Called at every
1193
1214
  * task-producing spec-build site so the transport is never wired one-off.
1194
1215
  */
@@ -1265,7 +1286,7 @@ export class Orchestrator {
1265
1286
  * Returns null (no packet, no pointer) for native resume, a fresh thread, or
1266
1287
  * a non-thread run. Never throws: continuity failure degrades to no packet.
1267
1288
  */
1268
- async resolveContinuity(runInput, harnessId, resolvedProfileId, nativeResumeAvailable, store, paths, repoRoot, log) {
1289
+ async resolveContinuity(runInput, harnessId, resolvedProfileId, nativeResumeAvailable, sessionFields, store, paths, repoRoot, log) {
1269
1290
  const ctx = runInput.threadContinuity;
1270
1291
  if (!runInput.threadId || !ctx)
1271
1292
  return null;
@@ -1303,7 +1324,6 @@ export class Orchestrator {
1303
1324
  // summary. Same credential route + scoped lane home a real read-only
1304
1325
  // thread turn uses (INV-034/135). Best-effort in its OWN guard — a summary
1305
1326
  // failure keeps the full mechanical packet, never drops it.
1306
- const sessionFields = this.sessionSpecFields(runInput, harnessId);
1307
1327
  req.cachedSummary = await resolveContinuitySummary({
1308
1328
  req,
1309
1329
  threadId: runInput.threadId,
@@ -1371,13 +1391,15 @@ export class Orchestrator {
1371
1391
  // Isolated scoped-home sessions are never retained after disposal.
1372
1392
  const inPlaceEnvelope = envelope.worktree_path === envelope.repo_root;
1373
1393
  const rawContextPacket = await rawContextForEnvelope(routed.implementationTransport, envelope);
1374
- const sessionFields = runInput ? this.sessionSpecFields(runInput, adapter.id, log) : undefined;
1394
+ const sessionFields = runInput
1395
+ ? await this.sessionSpecFields(runInput, adapter.id, knobs.model, log, routed.authRouteEstimate, routed.quotaAdmission)
1396
+ : undefined;
1375
1397
  // Continuity (INV-137): once the lane (harness + resolved profile) is known,
1376
1398
  // build the continuation packet, materialize context/THREAD.md, and point
1377
1399
  // the prompt at it — never embed the packet body in the prompt. Replaces the
1378
1400
  // old static session.rebound "not_portable" phrase with a real disclosure.
1379
1401
  const laneContinuity = runInput
1380
- ? await this.resolveContinuity(runInput, adapter.id, sessionFields?.credential_profile?.profile_id ?? runInput.credentialProfileId ?? null, inPlaceEnvelope && !!sessionFields?.resume_session_id, store, paths, envelope.repo_root, log)
1402
+ ? await this.resolveContinuity(runInput, adapter.id, sessionFields?.credential_profile?.profile_id ?? runInput.credentialProfileId ?? null, inPlaceEnvelope && !!sessionFields?.resume_session_id, sessionFields, store, paths, envelope.repo_root, log)
1381
1403
  : null;
1382
1404
  let spec = HarnessRunSpec.parse({
1383
1405
  session_id: newId("ses"),
@@ -1480,13 +1502,16 @@ export class Orchestrator {
1480
1502
  try {
1481
1503
  const watched = withInactivityWatchdog(adapter.run(runSpec), {
1482
1504
  timeoutMs: inactivityMs,
1505
+ countsAsProgress: countsAsAgentProgress,
1483
1506
  onTimeout: () => {
1484
1507
  attemptAbort.abort();
1485
1508
  void adapter.cancel?.(activeSessionId)?.catch(() => { });
1486
1509
  },
1487
- // Waiting on the USER (pending interaction) is legitimate
1488
- // silence the interaction channel enforces its own wait budget.
1510
+ // Waiting on the USER is legitimate silence. The interaction policy
1511
+ // either enforces a finite deadline or waits until answer/cancel/
1512
+ // terminal/restart when the timeout is disabled.
1489
1513
  isSuspended: () => (interaction?.pendingCount?.() ?? 0) > 0,
1514
+ suspensionVersion: () => interaction?.suspensionVersion?.() ?? 0,
1490
1515
  });
1491
1516
  for await (const ev of watched) {
1492
1517
  if (signal?.aborted)
@@ -1616,13 +1641,18 @@ export class Orchestrator {
1616
1641
  // W5.4 failover: a typed-limit hit rebuilds the spec on a NEW vendor
1617
1642
  // session under the next profile with provenance (vendor_limit_rejected).
1618
1643
  if (harnessErrored && runInput && !signal?.aborted) {
1644
+ const rotationPolicy = this.profilePolicy(contract.repo.root, adapter.id);
1645
+ const readyProfileIds = sawTypedLimit && deliverableEmpty && rotationPolicy.limit_action === "rotate"
1646
+ ? await this.readyProfileIdsForRotation(runInput, adapter.id, spec.credential_profile ?? null, triedProfiles)
1647
+ : new Set();
1619
1648
  const rotated = rotateSpecOnTypedLimit({
1620
1649
  spec,
1621
1650
  harnessId: adapter.id,
1622
1651
  attemptId,
1623
- policy: this.profilePolicy(contract.repo.root, adapter.id),
1652
+ policy: rotationPolicy,
1624
1653
  registry: this.config(contract.repo.root)?.global.credential_profiles ?? [],
1625
1654
  snapshots: this.deps.quotaSnapshots?.() ?? [],
1655
+ readyProfileIds,
1626
1656
  triedProfiles,
1627
1657
  sawTypedLimit,
1628
1658
  deliverableEmpty,
@@ -1679,7 +1709,7 @@ export class Orchestrator {
1679
1709
  if (webUnsatisfied(telemetry)) {
1680
1710
  errors.push(webEvidenceFailure(telemetry.web));
1681
1711
  }
1682
- // D-16: un-nest {work_report, output} so answer.md persists the OUTPUT, not the envelope.
1712
+ // D-16: remove the WorkReport transport so answer.md persists only the deliverable.
1683
1713
  const unwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", workReportMode, {
1684
1714
  sideToolReport: telemetry.sideToolWorkReport ?? undefined,
1685
1715
  });
@@ -1814,66 +1844,17 @@ export class Orchestrator {
1814
1844
  // Thin delegate — the channel mechanics live in interaction.ts.
1815
1845
  return interactionChannelFor(input, log, runId, taskId, attemptId, harnessId, supportsInteractive, DEFAULT_INTERACTION_TIMEOUT_MS);
1816
1846
  }
1817
- /**
1818
- * Guarantee a git boundary for write-mode runs. Non-git project folders are
1819
- * initialized in place (`git init`, deterministic baseline commit) without
1820
- * creating or editing `.gitignore`, and the action is announced via a
1821
- * `project.git.initialized` event. Returns the failure message when the
1822
- * boundary cannot be established (the terminal failure events are already
1823
- * emitted); null on success.
1824
- */
1825
- async ensureWriteModeGitBoundary(repoRoot, log, store, paths, runId, mode) {
1826
- if (repoRoot === NO_PROJECT_ROOT)
1827
- return null;
1828
- try {
1829
- const result = await ensureGitRepository(repoRoot);
1830
- if (result.initialized || result.baselineCommitted) {
1831
- log.emit("project.git.initialized", {
1832
- repo_root: repoRoot,
1833
- initialized: result.initialized,
1834
- baseline_committed: result.baselineCommitted,
1835
- gitignore_seeded: result.gitignoreSeeded,
1836
- head_sha: result.headSha,
1837
- });
1838
- }
1839
- return null;
1840
- }
1841
- catch (err) {
1842
- const message = safeErrorMessage(err);
1843
- writeFailure(store, paths, {
1844
- phase: "workspace",
1845
- category: "project",
1846
- safeMessage: message,
1847
- runDir: paths.root,
1848
- nextActions: [
1849
- "Check the project folder permissions",
1850
- "Initialize git manually (git init)",
1851
- "Retry the run",
1852
- ],
1853
- });
1854
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: failed\n- Phase: workspace\n\n${message}\n`);
1855
- log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
1856
- log.emit("run.failed", {
1857
- lifecycle: "failed",
1858
- facts: makeOutcomeFacts("failed", { reason: "harness_failed" }),
1859
- reason: "harness_failed",
1860
- phase: "workspace",
1861
- error: message,
1862
- failure_ref: "final/failure.yaml",
1863
- });
1864
- return message;
1865
- }
1866
- }
1867
1847
  /**
1868
1848
  * D-14 layer 3 (AGENTS.md unification, INV-113): the ONE new live-tree write.
1869
1849
  * When the PROJECT root has `AGENTS.md` and no `CLAUDE.md`, drop a thin
1870
1850
  * `CLAUDE.md` (`@AGENTS.md` import + Claudexor ownership marker) so a Claude
1871
1851
  * Code route reads the same instruction file codex/cursor read natively.
1872
1852
  *
1873
- * Fenced exactly where the automatic git-init boundary is: read-only modes
1874
- * never reach this run-prep stage; `--in-place` stateful targets are left
1875
- * untouched; the write targets the PROJECT root (`repoRoot`), never a worktree
1876
- * envelope. The workspace helper adds exclusive-create + no-follow +
1853
+ * The project-root bridge has its own narrower fence: read-only modes never
1854
+ * reach this run-prep stage and `--in-place` stateful targets are left
1855
+ * untouched. Git admission is independently owned by `runStartRequiresGit`.
1856
+ * The write targets the PROJECT root (`repoRoot`), never a worktree envelope.
1857
+ * The workspace helper adds exclusive-create + no-follow +
1877
1858
  * idempotency, so a hand-written or symlinked `CLAUDE.md` is never overwritten
1878
1859
  * and a concurrent/second prep is a no-op. Announced via a typed
1879
1860
  * `project.claude_bridge.created` event on an actual create only — the git-init
@@ -1898,37 +1879,6 @@ export class Orchestrator {
1898
1879
  });
1899
1880
  }
1900
1881
  }
1901
- /**
1902
- * Freeze-on-implement delivery (D17/D27): verify the frozen plan's hash and
1903
- * materialize it as context/PLAN.md in the run artifact tree — OUTSIDE every
1904
- * worktree, so it can never dirty a diff — then point the prompt at the
1905
- * absolute path. A mismatched or unreadable plan fails LOUDLY before any
1906
- * harness spawns (the tamper fence; retry replays planRef verbatim, so a
1907
- * retried implement can never silently run without its plan).
1908
- */
1909
- withPlanBrief(input, store, paths, log) {
1910
- if (!input.planRef)
1911
- return input;
1912
- const text = readTextSafe(input.planRef.path);
1913
- if (!text || !text.trim()) {
1914
- throw new Error(`implement plan: the frozen plan at ${input.planRef.path} is missing or unreadable`);
1915
- }
1916
- const digest = sha256(text).replace(/^sha256:/, "");
1917
- if (digest !== input.planRef.sha256) {
1918
- throw new Error(`implement plan: plan hash mismatch (expected ${input.planRef.sha256}, got ${digest}) — the plan was modified after freeze; re-run Implement from the plan turn`);
1919
- }
1920
- const briefPath = join(paths.contextDir, "PLAN.md");
1921
- store.writeText(briefPath, text);
1922
- log.emit("plan.brief.materialized", {
1923
- plan_run_id: input.planRef.runId,
1924
- sha256: input.planRef.sha256,
1925
- path: "context/PLAN.md",
1926
- });
1927
- return {
1928
- ...input,
1929
- prompt: `${input.prompt}\n\nThe approved plan is at: ${briefPath} — read it before starting and re-read it as needed.`,
1930
- };
1931
- }
1932
1882
  async runRace(input, mode, announce) {
1933
1883
  const taskId = input.taskId ?? newId("task");
1934
1884
  const runId = input.runId ?? newId("run");
@@ -1936,20 +1886,28 @@ export class Orchestrator {
1936
1886
  // announced: a refused run must fail the request loudly, not 200 a runId
1937
1887
  // and leave an orphaned run dir without a terminal event.
1938
1888
  const contract = this.buildContract(input, taskId, mode);
1939
- const store = this.artifactStore(input);
1940
- const paths = store.createRun(runId);
1941
- const log = new EventLog(paths.eventsPath, runId, taskId, input.onEvent, input.threadId);
1942
- input = this.withPlanBrief(input, store, paths, log);
1943
- // The execution root is the tree the harness mutates: the project itself for
1944
- // in-place threads/ordinary runs, or the thread's persistent worktree for an
1945
- // isolated thread. Config/artifacts/contract stay anchored to repoRoot. Both
1946
- // the WorkspaceManager and the git boundary resolve against this SINGLE root.
1889
+ const planBrief = verifiedPlanBrief(input);
1890
+ const quotaSnapshots = this.quotaSnapshotPreflight();
1891
+ const { store, paths, log, ledger } = beginAnnouncedRun({
1892
+ input,
1893
+ contract,
1894
+ quotaSnapshots,
1895
+ store: this.artifactStore(input),
1896
+ authority: this.deps.delegationBudgetAuthority,
1897
+ runId,
1898
+ taskId,
1899
+ mode,
1900
+ phase: "race",
1901
+ prompt: input.prompt,
1902
+ }, announce);
1903
+ input = withPlanBrief(input, store, paths, log, planBrief);
1904
+ // The execution root is the tree the harness mutates: the project itself
1905
+ // for in-place threads/ordinary runs, or the thread's persistent worktree
1906
+ // for an isolated thread. Config/artifacts/contract stay anchored to
1907
+ // repoRoot. Both the WorkspaceManager and the git boundary resolve against
1908
+ // this SINGLE root.
1947
1909
  const execRoot = this.execRootOf(input);
1948
1910
  const wsm = new WorkspaceManager(execRoot);
1949
- safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
1950
- log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
1951
- const ledger = this.rootLedger(input, contract, log);
1952
- announce?.(announcedRunContext({ log, store, paths, runId, taskId, mode, phase: "race" }, ledger, () => this.deps.delegationBudgetAuthority?.hasParent(runId) === true));
1953
1911
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
1954
1912
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
1955
1913
  // Write modes need a git boundary for worktree isolation and honest diffs.
@@ -1958,17 +1916,17 @@ export class Orchestrator {
1958
1916
  // silent mutation (user-locked decision, comparator: Codex requires git).
1959
1917
  // For an isolated thread the execution root is already a git worktree, so
1960
1918
  // this is a no-op there; for in-place it ensures the live project is git.
1961
- const gitPreconditionError = await this.ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
1919
+ const gitPreconditionError = await ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
1962
1920
  if (gitPreconditionError) {
1963
1921
  return {
1964
1922
  runId,
1965
1923
  taskId,
1966
1924
  mode,
1967
1925
  lifecycle: "failed",
1968
- facts: makeOutcomeFacts("failed", { reason: "harness_failed" }),
1926
+ facts: makeOutcomeFacts("failed", { reason: gitPreconditionError.reason }),
1969
1927
  winner: null,
1970
1928
  runDir: paths.root,
1971
- summary: gitPreconditionError,
1929
+ summary: gitPreconditionError.message,
1972
1930
  candidates: [],
1973
1931
  };
1974
1932
  }
@@ -2066,7 +2024,7 @@ export class Orchestrator {
2066
2024
  attemptId,
2067
2025
  intent: this.candidateIntent(input),
2068
2026
  harnessId: routed.adapter.id,
2069
- cost: attemptCostEvidence(routed.adapter.id, attemptId, i > 0 ? this.estimateUsdFloor(input.repoRoot) : undefined, this.routeBillingKnowledge(input, routed.adapter.id)),
2027
+ cost: attemptCostEvidence(routed.adapter.id, attemptId, this.reservationEstimateUsd(input, i > 0), this.routeBillingKnowledge(input, routed.adapter.id)),
2070
2028
  });
2071
2029
  log.emit("budget.lease.created", {
2072
2030
  granted: lease.granted,
@@ -2232,6 +2190,13 @@ export class Orchestrator {
2232
2190
  continuation_count: candidateContinuationCount,
2233
2191
  packet_turns: packet.continuity.disclosure.packetTurns,
2234
2192
  });
2193
+ log.emit("harness.started", {
2194
+ harness_id: adapter.id,
2195
+ attempt_id: contAttemptId,
2196
+ external_context_policy: knobs.webPolicy,
2197
+ ...(knobs.ignored.length > 0 ? { ignored_settings: knobs.ignored } : {}),
2198
+ continuation_of: run.attemptId,
2199
+ });
2235
2200
  const contLeaseId = contLease.lease?.lease_id ?? "";
2236
2201
  try {
2237
2202
  const contRun = await this.runCandidateInEnvelope(slot.routed, envelope, contAttemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
@@ -2519,7 +2484,7 @@ export class Orchestrator {
2519
2484
  // candidates (a trivial greeting in agent mode no longer burns two reviewers on
2520
2485
  // "(empty diff)"). Candidates still flow through arbitration/gates so the
2521
2486
  // no_op/answer outcome and gate failures are unchanged.
2522
- evidences = await this.reviewRuns(workingRuns, reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal);
2487
+ evidences = await this.reviewRuns(workingRuns, reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input));
2523
2488
  }
2524
2489
  catch (err) {
2525
2490
  // Review preflight/evidence failures end TERMINALLY with artifacts —
@@ -2544,7 +2509,7 @@ export class Orchestrator {
2544
2509
  attemptId: "synth",
2545
2510
  intent: "synthesize",
2546
2511
  harnessId: synthRouted.adapter.id,
2547
- cost: attemptCostEvidence(synthRouted.adapter.id, "synth", undefined, this.routeBillingKnowledge(input, synthRouted.adapter.id)),
2512
+ cost: attemptCostEvidence(synthRouted.adapter.id, "synth", this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, synthRouted.adapter.id)),
2548
2513
  });
2549
2514
  if (lease.granted) {
2550
2515
  let envelope;
@@ -2580,7 +2545,7 @@ export class Orchestrator {
2580
2545
  runs.push(run);
2581
2546
  try {
2582
2547
  if (isWorkingCandidate(run)) {
2583
- const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal);
2548
+ const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input));
2584
2549
  evidences.push(...synthEvidence);
2585
2550
  workingRuns.push(run);
2586
2551
  }
@@ -2629,8 +2594,12 @@ export class Orchestrator {
2629
2594
  // Winner can only be a candidate that actually produced work; corpses are
2630
2595
  // excluded from arbitration upstream and from the fallback here.
2631
2596
  const winnerRun = workingRuns.find((r) => r.attemptId === result.decision.winner) ?? workingRuns[0];
2632
- // A reviewer escalation to a human is a BLOCKED terminal, not a silent risk note.
2633
- const needsHuman = evidences.some((e) => e.findings.some((f) => f.severity === "NEEDS_HUMAN" && isBlocking(f)));
2597
+ const winnerEvidence = winnerRun
2598
+ ? evidences.find((e) => e.attemptId === winnerRun.attemptId)
2599
+ : undefined;
2600
+ // D9 winner-only NEEDS_HUMAN gate, fail-closed on a winner with no review
2601
+ // evidence record (see winnerNeedsHuman).
2602
+ const needsHuman = winnerNeedsHuman(winnerRun?.attemptId ?? null, evidences);
2634
2603
  // Run-level review_verified is the WINNER's verification: an
2635
2604
  // empty-diff loser's unverified route must not drag the shipped result's
2636
2605
  // flag false. No winner -> fall back to the all-candidates view.
@@ -2693,12 +2662,11 @@ export class Orchestrator {
2693
2662
  })) {
2694
2663
  log.emit("output.ready", { kind: "artifact", path });
2695
2664
  }
2696
- assertNoSecretLikeTokens("final patch diff", winnerRun.diff);
2665
+ secretDiff.assertNoSecretLikeTokens("final patch diff", winnerRun.diff);
2697
2666
  const patchSha256 = sha256(winnerRun.diff);
2698
2667
  store.writeText(join(paths.finalDir, "patch.diff"), winnerRun.diff);
2699
2668
  const wstats = diffStats(winnerRun.diff);
2700
2669
  const hasDiff = winnerRun.diff.trim().length > 0;
2701
- const winnerEvidence = evidences.find((e) => e.attemptId === winnerRun.attemptId);
2702
2670
  const blockers = winnerEvidence
2703
2671
  ? winnerEvidence.findings.filter((f) => isBlocking(f)).length
2704
2672
  : 0;
@@ -3011,7 +2979,7 @@ export class Orchestrator {
3011
2979
  env: reviewHome.env,
3012
2980
  }).finally(() => reviewHome.dispose());
3013
2981
  }
3014
- async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal) {
2982
+ async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal, reservationEstimateUsd) {
3015
2983
  const evidences = [];
3016
2984
  for (const run of runs) {
3017
2985
  const candidateCwd = run.reviewCwd ?? cwd;
@@ -3030,7 +2998,7 @@ export class Orchestrator {
3030
2998
  attemptId: run.attemptId,
3031
2999
  intent: "review",
3032
3000
  harnessId: "review-panel",
3033
- cost: attemptCostEvidence("review-panel", run.attemptId),
3001
+ cost: attemptCostEvidence("review-panel", run.attemptId, reservationEstimateUsd),
3034
3002
  })
3035
3003
  : undefined;
3036
3004
  const result = hasDiff && reviewers.length > 0 && (reviewLease?.granted ?? true)
@@ -3174,10 +3142,21 @@ export class Orchestrator {
3174
3142
  const runId = input.runId ?? newId("run");
3175
3143
  // Contract validation BEFORE the run is announced (see runRace).
3176
3144
  const contract = this.buildContract(input, taskId, mode);
3177
- const store = this.artifactStore(input);
3178
- const paths = store.createRun(runId);
3179
- const log = new EventLog(paths.eventsPath, runId, taskId, input.onEvent, input.threadId);
3180
- input = this.withPlanBrief(input, store, paths, log);
3145
+ const planBrief = verifiedPlanBrief(input);
3146
+ const quotaSnapshots = this.quotaSnapshotPreflight();
3147
+ const { store, paths, log, ledger } = beginAnnouncedRun({
3148
+ input,
3149
+ contract,
3150
+ quotaSnapshots,
3151
+ store: this.artifactStore(input),
3152
+ authority: this.deps.delegationBudgetAuthority,
3153
+ runId,
3154
+ taskId,
3155
+ mode,
3156
+ phase: "convergence",
3157
+ prompt: input.prompt,
3158
+ }, announce);
3159
+ input = withPlanBrief(input, store, paths, log, planBrief);
3181
3160
  // The execution root is the tree the harness mutates (thread worktree for an
3182
3161
  // isolated thread, else the project). The WorkspaceManager AND the git
3183
3162
  // boundary must resolve against the SAME root — the race path does so via the
@@ -3186,27 +3165,11 @@ export class Orchestrator {
3186
3165
  const execRoot = this.execRootOf(input);
3187
3166
  const wsm = new WorkspaceManager(execRoot);
3188
3167
  const readiness = new ReadinessLedger();
3189
- let ledger;
3190
- try {
3191
- ledger = this.rootLedger(input, contract, log);
3192
- store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
3193
- safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
3194
- log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
3195
- announce?.(announcedRunContext({ log, store, paths, runId, taskId, mode, phase: "convergence" }, ledger, () => this.deps.delegationBudgetAuthority?.hasParent(runId) === true));
3196
- }
3197
- catch (error) {
3198
- // A delegated child attaches its scoped financial view before the run is
3199
- // announced. If any fallible artifact/start callback in that narrow gap
3200
- // throws, the terminal net has no run context, so detach here explicitly.
3201
- if (input.delegatedFromRunId) {
3202
- this.deps.delegationBudgetAuthority?.releaseRun(runId);
3203
- }
3204
- throw error;
3205
- }
3168
+ store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
3206
3169
  // Live (in-place) isolation deliberately tolerates non-git stateful
3207
3170
  // environments; only envelope isolation needs the git boundary.
3208
3171
  if (!input.inPlace) {
3209
- const gitPreconditionError = await this.ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
3172
+ const gitPreconditionError = await ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
3210
3173
  if (gitPreconditionError) {
3211
3174
  return {
3212
3175
  spendUsd: ledger.spend(),
@@ -3214,10 +3177,10 @@ export class Orchestrator {
3214
3177
  taskId,
3215
3178
  mode,
3216
3179
  lifecycle: "failed",
3217
- facts: makeOutcomeFacts("failed", { reason: "harness_failed" }),
3180
+ facts: makeOutcomeFacts("failed", { reason: gitPreconditionError.reason }),
3218
3181
  winner: null,
3219
3182
  runDir: paths.root,
3220
- summary: gitPreconditionError,
3183
+ summary: gitPreconditionError.message,
3221
3184
  candidates: [],
3222
3185
  };
3223
3186
  }
@@ -3349,7 +3312,8 @@ export class Orchestrator {
3349
3312
  // observed quota cooldown across all harnesses, or genuine no-progress (a stall on the same
3350
3313
  // failure signature after every available harness has tried it).
3351
3314
  const stallThreshold = input.untilClean === true ? 4 : 2;
3352
- const allCooledDown = () => adapterPool.every((a) => ledger.cooldownActive(a.adapter.id));
3315
+ const convergenceQuotaLedger = this.quotaLedgerView(ledger, adapterPool);
3316
+ const allCooledDown = () => adapterPool.every((a) => convergenceQuotaLedger.cooldownActive(a.adapter.id));
3353
3317
  const attemptTelemetries = [];
3354
3318
  let lastDiffStable = true;
3355
3319
  try {
@@ -3406,7 +3370,7 @@ export class Orchestrator {
3406
3370
  attemptId,
3407
3371
  intent: "repair",
3408
3372
  harnessId: adapter.id,
3409
- cost: attemptCostEvidence(adapter.id, attemptId, undefined, this.routeBillingKnowledge(input, adapter.id)),
3373
+ cost: attemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, adapter.id)),
3410
3374
  });
3411
3375
  if (!lease.granted) {
3412
3376
  exhausted = true;
@@ -3518,7 +3482,7 @@ export class Orchestrator {
3518
3482
  attemptId,
3519
3483
  intent: "review",
3520
3484
  harnessId: "review-panel",
3521
- cost: attemptCostEvidence("review-panel", attemptId),
3485
+ cost: attemptCostEvidence("review-panel", attemptId, this.reservationEstimateUsd(input)),
3522
3486
  })
3523
3487
  : null;
3524
3488
  const reviewResult = reviewers.length > 0 && (reviewLease?.granted ?? false)
@@ -3700,7 +3664,7 @@ export class Orchestrator {
3700
3664
  if (adapterPool.length > 1 && triedSinceProgress.size < adapterPool.length) {
3701
3665
  // Quota-headroom consumer (mid-run, where quota observations EXIST);
3702
3666
  // pick + honest route event owned by runSupport.rotateOnStall.
3703
- adapterIdx = rotateOnStall(adapterPool.map((a) => a.adapter.id), adapterIdx, ledger, triedSinceProgress, log, lastRun?.harnessId ?? null);
3667
+ adapterIdx = rotateOnStall(adapterPool.map((a) => a.adapter.id), adapterIdx, convergenceQuotaLedger, triedSinceProgress, log, lastRun?.harnessId ?? null);
3704
3668
  routed = adapterPool[adapterIdx];
3705
3669
  adapter = routed.adapter;
3706
3670
  }
@@ -3808,7 +3772,7 @@ export class Orchestrator {
3808
3772
  // work_product (its partial patch.diff stays diagnostic via attempts/);
3809
3773
  // in-place keeps the product so the honest Revert offer survives.
3810
3774
  if (lastRun && (!interrupted || input.inPlace === true)) {
3811
- assertNoSecretLikeTokens("final patch diff", lastRun.diff);
3775
+ secretDiff.assertNoSecretLikeTokens("final patch diff", lastRun.diff);
3812
3776
  const patchSha256 = sha256(lastRun.diff);
3813
3777
  store.writeText(join(paths.finalDir, "patch.diff"), lastRun.diff);
3814
3778
  // Honest apply-state (parity with runRace single-candidate in-place): a
@@ -3978,265 +3942,77 @@ export class Orchestrator {
3978
3942
  reviewVerified: actualReviewVerified,
3979
3943
  };
3980
3944
  }
3981
- /** One read-only planner spawn shared by solo fallback, Council drafts, and merge. */
3982
- async runPlannerAttempt(args) {
3983
- const { input, contract, taskId, runId, log, store, paths, ledger, routed, attemptId } = args;
3984
- const adapter = routed.adapter;
3985
- const lease = ledger.reserve({
3986
- taskId,
3987
- attemptId,
3988
- intent: args.intent,
3989
- harnessId: adapter.id,
3990
- cost: attemptCostEvidence(adapter.id, attemptId, undefined, this.routeBillingKnowledge(input, adapter.id)),
3991
- });
3992
- if (!lease.granted) {
3993
- log.emit("budget.lease.created", {
3994
- granted: false,
3995
- reason: lease.reason,
3996
- denied: lease.denied,
3997
- attempt_id: attemptId,
3998
- harness_id: adapter.id,
3999
- });
4000
- return {
4001
- attemptId,
4002
- harnessId: adapter.id,
4003
- status: "failed",
4004
- outcomeClass: "clean", // never spawned: refused pre-flight by the budget gate
4005
- error: lease.reason ?? "budget lease denied",
4006
- text: null,
4007
- telemetry: null,
4008
- budgetDenied: true,
4009
- budgetDenial: {
4010
- code: lease.denied ?? "hard_cap",
4011
- reason: lease.reason ?? "budget lease denied",
4012
- harnessId: adapter.id,
4013
- attemptId,
4014
- },
4015
- };
4016
- }
4017
- const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
4018
- const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
4019
- const planSessionFields = this.sessionSpecFields(input, adapter.id, log);
4020
- // Continuity (INV-137): a thread PLAN turn is a chat turn — hydrate a
4021
- // lane switch/gap with a packet and disclose it.
4022
- const laneContinuity = args.laneRun
4023
- ? await this.resolveContinuity(input, adapter.id, planSessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null, planSessionFields.resume_session_id !== null, store, paths, this.execRootOf(input), log)
4024
- : null;
4025
- const spec = HarnessRunSpec.parse({
4026
- session_id: newId("ses"),
4027
- intent: args.intent,
4028
- prompt: laneContinuity?.pointerLine
4029
- ? `${args.promptBody}\n\n${laneContinuity.pointerLine}`
4030
- : args.promptBody,
4031
- cwd: this.execRootOf(input),
4032
- access: "readonly",
4033
- // Planners must SEE any image/file the user attached (e.g. "plan a fix for
4034
- // what's in this screenshot"), not just agent/race runs.
4035
- attachments: input.attachments ?? [],
4036
- ...planSessionFields,
4037
- ...this.harnessSpecKnobs(contract, knobs, args.intent),
4038
- env_inheritance: envInheritance(this.config(input.repoRoot)),
4039
- // A thread plan turn spawns in its DURABLE per-lane home so its native
4040
- // session is reachable for resume next turn (INV-034); a non-thread
4041
- // plan keeps the disposable route-context home.
4042
- env: (args.laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? args.fallbackHome,
4043
- });
4044
- const plannerAbort = new AbortController();
4045
- spec.extra["abortSignal"] = input.signal
4046
- ? AbortSignal.any([input.signal, plannerAbort.signal])
4047
- : plannerAbort.signal;
4048
- const planInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
4049
- if (planInteraction)
4050
- spec.extra["interactionChannel"] = planInteraction;
4051
- // D-16: compile the WorkReport envelope for the plan lane (require plan text
4052
- // below folds the deliverable; the veto rides work_state).
4053
- const planWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(planInteraction));
4054
- const planWorkMode = this.applyWorkEnvelope(spec, planWorkEnvelope);
4055
- const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
4056
- const answer = new AnswerAssembly();
4057
- const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
4058
- knobs.webPolicy === "cached" ||
4059
- knobs.webPolicy === "live", effectiveWeb, [],
4060
- // Requested-model capture: a plan lane silently downgraded to another
4061
- // model surfaces the mismatch in its route receipt, just like agent.
4062
- knobs.model);
4063
- const onAbort = () => {
4064
- void adapter.cancel?.(spec.session_id)?.catch(() => { });
4065
- };
4066
- if (input.signal) {
4067
- if (input.signal.aborted)
4068
- onAbort();
4069
- else
4070
- input.signal.addEventListener("abort", onAbort, { once: true });
4071
- }
4072
- let cost = 0;
4073
- let costEstimated = false;
4074
- let harnessError = null;
4075
- const budgetSignalState = { quotaPressureDisclosed: false };
4076
- try {
4077
- log.emit("harness.started", {
4078
- harness_id: adapter.id,
4079
- attempt_id: attemptId,
4080
- external_context_policy: knobs.webPolicy,
4081
- ...(knobs.ignored.length > 0 ? { ignored_settings: knobs.ignored } : {}),
4082
- });
4083
- if (!input.signal?.aborted) {
4084
- const watchedPlan = withInactivityWatchdog(adapter.run(spec), {
4085
- timeoutMs: harnessInactivityTimeoutMs(this.config(input.repoRoot)),
4086
- onTimeout: () => {
4087
- plannerAbort.abort();
4088
- void adapter.cancel?.(spec.session_id)?.catch(() => { });
4089
- },
4090
- isSuspended: () => (planInteraction?.pendingCount?.() ?? 0) > 0,
4091
- });
4092
- for await (const ev of watchedPlan) {
4093
- if (input.signal?.aborted)
4094
- break;
4095
- const safeEv = redactHarnessEvent(ev);
4096
- safeInvoke(input.onHarnessEvent, safeEv);
4097
- // A thread PLAN turn IS a chat turn now (INV-034): its native
4098
- // session lives in the DURABLE per-lane home, so record it for the
4099
- // next lane turn's resume. Council members are distinct lanes.
4100
- if (args.laneRun)
4101
- observeNativeSessionEvent(input, adapter.id, safeEv);
4102
- observeAuthSwitch(log, adapter.id, attemptId, safeEv);
4103
- log.emit("harness.event", harnessEventPayload(adapter.id, attemptId, safeEv));
4104
- appendLine(attemptEventsPath, JSON.stringify(safeEv));
4105
- observeAttemptTelemetry(telemetry, safeEv);
4106
- if (safeEv.plan_progress) {
4107
- log.emit("plan.progress", {
4108
- attempt_id: attemptId,
4109
- harness_id: adapter.id,
4110
- items: safeEv.plan_progress.items,
4111
- });
4112
- }
4113
- // read-only routes burn quota too — same single owner as the agent loop.
4114
- observeBudgetSignals(ledger, log, adapter.id, attemptId, safeEv, budgetSignalState);
4115
- this.deps.quotaEventSink?.(adapter.id, safeEv);
4116
- if (safeEv.type === "usage" && safeEv.usage?.cost_usd) {
4117
- cost += safeEv.usage.cost_usd;
4118
- if (safeEv.usage.estimated)
4119
- costEstimated = true;
4120
- log.emit("budget.observation", {
4121
- harness_id: adapter.id,
4122
- attempt_id: attemptId,
4123
- kind: "spend",
4124
- usd: safeEv.usage.cost_usd,
4125
- estimated: safeEv.usage.estimated === true,
4126
- });
4127
- }
4128
- // A TYPED final message wins verbatim over joined narration.
4129
- answer.observe(safeEv);
4130
- if (safeEv.type === "error")
4131
- harnessError = safeEv.error ? redactSecrets(safeEv.error) : "harness emitted an error";
4132
- }
4133
- }
4134
- }
4135
- catch (err) {
4136
- harnessError = safeErrorMessage(err);
4137
- }
4138
- finally {
4139
- input.signal?.removeEventListener("abort", onAbort);
4140
- ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost));
4141
- }
4142
- // D-16: unwrap and require PLAN TEXT — a plan with no text is not delivered.
4143
- // The unwrap runs BEFORE the error axes: the deliverable it yields is what
4144
- // decides whether an unrecovered tool error is fatal (explorer parity).
4145
- const planUnwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", planWorkMode, {
4146
- sideToolReport: telemetry.sideToolWorkReport ?? undefined,
4147
- });
4148
- const planText = redactSecrets(planUnwrapped.deliverable).trim();
4149
- const unrecovered = unrecoveredToolErrors(telemetry);
4150
- const webBlocked = webUnsatisfied(telemetry);
4151
- if (!harnessError && webBlocked) {
4152
- harnessError = webEvidenceFailure(telemetry.web);
4153
- }
4154
- // INV-043/INV-044, explorer parity: a DELIVERED plan keeps an unrecovered
4155
- // non-web tool error as warning evidence instead of discarding the plan (see
4156
- // the helper). Web keeps its hard gate above; the finalizer outranks both.
4157
- harnessError ??= unrecoveredToolErrorFailure(unrecovered, planText.length > 0);
4158
- const planFinalized = finalizeAttempt({
4159
- deliverableEvidence: planText.length > 0,
4160
- harnessErrored: harnessError !== null && !webBlocked,
4161
- workReport: planUnwrapped.workReport,
4162
- workReportSource: planUnwrapped.source,
4163
- workReportViolation: planUnwrapped.contractViolation,
4164
- contextTerminalExhausted: telemetry.contextExhausted,
4165
- });
4166
- // A broken WorkReport contract is a hard failure only when the finalizer
4167
- // ranked it so (a terminal context exhaustion outranks it).
4168
- if (!harnessError && planFinalized.outcomeClass === "contract_failure") {
4169
- harnessError = `work_report contract: ${planUnwrapped.contractViolation}`;
4170
- }
4171
- // D-16 r9: an interrupted (context-exhausted) planner is NEVER a clean
4172
- // plan — partial text must not become final/plan.md as success. A VETO
4173
- // (needs_input/incomplete work_state) is DIFFERENT by the sealed D-16
4174
- // contract (X35, INV-116 canaries): the plan still delivers, lifecycle
4175
- // succeeded, and the work_state veto rides the OUTCOME (non-zero exit) —
4176
- // it must not be laundered into a harness failure either direction.
4177
- if (!harnessError && planFinalized.outcomeClass === "interrupted") {
4178
- harnessError = "context capacity exhausted before the plan completed";
4179
- }
4180
- const attemptError = harnessError ??
4181
- (planFinalized.deliverablePresent ? null : "planner produced no plan text") ??
4182
- (input.signal?.aborted ? "planner cancelled" : null);
4183
- setAttemptOutcome(telemetry, {
4184
- deliverablePresent: planFinalized.deliverablePresent,
4185
- gatesPassed: null,
4186
- harnessErrored: (harnessError !== null && !webBlocked) || planFinalized.harnessErrored,
4187
- webRequiredUnsatisfied: webBlocked,
4188
- workState: planFinalized.workState,
4189
- });
4190
- if (attemptError) {
4191
- log.emit("harness.completed", {
4192
- harness_id: adapter.id,
4193
- attempt_id: attemptId,
4194
- status: webBlocked ? "blocked" : "failed",
4195
- error: attemptError,
4196
- ...telemetrySummary(telemetry),
4197
- });
4198
- return {
4199
- attemptId,
4200
- harnessId: adapter.id,
4201
- status: webBlocked ? "blocked" : "failed",
4202
- outcomeClass: planFinalized.outcomeClass,
4203
- error: attemptError,
4204
- text: null,
4205
- telemetry,
4206
- budgetDenied: false,
4207
- };
4208
- }
4209
- const text = planText || "(no output)";
4210
- log.emit("harness.completed", {
4211
- harness_id: adapter.id,
4212
- attempt_id: attemptId,
4213
- status: "success",
4214
- ...telemetrySummary(telemetry),
4215
- });
3945
+ /** Bind private route/session preparation to the planner-attempt owner. */
3946
+ plannerAttemptDeps() {
4216
3947
  return {
4217
- attemptId,
4218
- harnessId: adapter.id,
4219
- status: "success",
4220
- outcomeClass: planFinalized.outcomeClass,
4221
- error: null,
4222
- text,
4223
- telemetry,
4224
- budgetDenied: false,
3948
+ billingKnowledge: (input, harnessId) => this.routeBillingKnowledge(input, harnessId),
3949
+ inactivityTimeoutMs: (repoRoot) => harnessInactivityTimeoutMs(this.config(repoRoot)),
3950
+ quotaEventSink: this.deps.quotaEventSink,
3951
+ prepare: async (args) => {
3952
+ const { input, contract, taskId, runId, log, store, paths, routed, attemptId } = args;
3953
+ const adapter = routed.adapter;
3954
+ const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
3955
+ const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
3956
+ const sessionFields = await this.sessionSpecFields(input, adapter.id, knobs.model, log, routed.authRouteEstimate, routed.quotaAdmission);
3957
+ const laneContinuity = args.laneRun
3958
+ ? await this.resolveContinuity(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null, sessionFields.resume_session_id !== null, sessionFields, store, paths, this.execRootOf(input), log)
3959
+ : null;
3960
+ const spec = HarnessRunSpec.parse({
3961
+ session_id: newId("ses"),
3962
+ intent: args.intent,
3963
+ prompt: laneContinuity?.pointerLine
3964
+ ? `${args.promptBody}\n\n${laneContinuity.pointerLine}`
3965
+ : args.promptBody,
3966
+ cwd: this.execRootOf(input),
3967
+ access: "readonly",
3968
+ attachments: input.attachments ?? [],
3969
+ ...sessionFields,
3970
+ ...this.harnessSpecKnobs(contract, knobs, args.intent),
3971
+ env_inheritance: envInheritance(this.config(input.repoRoot)),
3972
+ env: (args.laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? args.fallbackHome,
3973
+ });
3974
+ const plannerAbort = new AbortController();
3975
+ spec.extra["abortSignal"] = input.signal
3976
+ ? AbortSignal.any([input.signal, plannerAbort.signal])
3977
+ : plannerAbort.signal;
3978
+ const planInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
3979
+ if (planInteraction)
3980
+ spec.extra["interactionChannel"] = planInteraction;
3981
+ const planWorkMode = this.applyWorkEnvelope(spec, this.workReportEnvelopeFor(routed, contract, Boolean(planInteraction)));
3982
+ return {
3983
+ knobs,
3984
+ effectiveWeb,
3985
+ spec,
3986
+ plannerAbort,
3987
+ planInteraction,
3988
+ planWorkMode,
3989
+ };
3990
+ },
4225
3991
  };
4226
3992
  }
3993
+ /** One read-only planner spawn shared by solo fallback, Council drafts, and merge. */
3994
+ async runPlannerAttempt(args) {
3995
+ return executePlannerAttempt(this.plannerAttemptDeps(), args);
3996
+ }
4227
3997
  async runPlan(input, announce) {
4228
3998
  const taskId = input.taskId ?? newId("task");
4229
3999
  const runId = input.runId ?? newId("run");
4230
4000
  // Plan runs get the same immutable contract truth as every other mode;
4231
4001
  // contract validation runs BEFORE the run is announced (see runRace).
4232
4002
  const contract = this.buildContract(input, taskId, "plan");
4233
- const store = this.artifactStore(input);
4234
- const paths = store.createRun(runId);
4235
- const log = new EventLog(paths.eventsPath, runId, taskId, input.onEvent, input.threadId);
4236
- safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
4237
- log.emit("run.created", { mode: "plan", prompt: redactSecrets(input.prompt) });
4238
- const ledger = this.rootLedger(input, contract, log);
4239
- announce?.(announcedRunContext({ log, store, paths, runId, taskId, mode: "plan", phase: "plan" }, ledger, () => this.deps.delegationBudgetAuthority?.hasParent(runId) === true));
4003
+ const quotaSnapshots = this.quotaSnapshotPreflight();
4004
+ const { store, paths, log, ledger } = beginAnnouncedRun({
4005
+ input,
4006
+ contract,
4007
+ quotaSnapshots,
4008
+ store: this.artifactStore(input),
4009
+ authority: this.deps.delegationBudgetAuthority,
4010
+ runId,
4011
+ taskId,
4012
+ mode: "plan",
4013
+ phase: "plan",
4014
+ prompt: input.prompt,
4015
+ }, announce);
4240
4016
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
4241
4017
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
4242
4018
  // W3.3: ONE resolved read-only context — the routing point-probe and every
@@ -4344,6 +4120,7 @@ export class Orchestrator {
4344
4120
  roHome,
4345
4121
  contextSection,
4346
4122
  laneRun,
4123
+ estimateUsdFloor: this.estimateUsdFloor(input.repoRoot),
4347
4124
  });
4348
4125
  }
4349
4126
  const plans = [];
@@ -4373,6 +4150,7 @@ export class Orchestrator {
4373
4150
  fallbackHome: roHome.env,
4374
4151
  promptBody: planPrompt(input.prompt) + contextSection,
4375
4152
  intent: "plan",
4153
+ reservationEstimateUsd: this.reservationEstimateUsd(input),
4376
4154
  });
4377
4155
  if (outcome.budgetDenied) {
4378
4156
  // QA-050: retain the denied planner slot before breaking so the
@@ -4451,7 +4229,6 @@ export class Orchestrator {
4451
4229
  }
4452
4230
  if (plans.length === 0) {
4453
4231
  return writePlanHarnessFailure(this.planRunDeps(), {
4454
- input,
4455
4232
  contract,
4456
4233
  taskId,
4457
4234
  runId,
@@ -4520,17 +4297,8 @@ export class Orchestrator {
4520
4297
  defaultPrompt: "Explore this repository and synthesize evidence-cited findings, omissions, and follow-up questions.",
4521
4298
  }, announce);
4522
4299
  }
4523
- resolvePaidBudget(inputBudget, cfg) {
4524
- return inputBudget ?? this.deps.paidBudget ?? cfg.global.budget.paid_budget_per_run;
4525
- }
4526
- rootLedger(input, contract, log) {
4527
- return createRootLedger({
4528
- input,
4529
- contract,
4530
- log,
4531
- authority: this.deps.delegationBudgetAuthority,
4532
- quotaSnapshots: this.deps.quotaSnapshots?.() ?? [],
4533
- });
4300
+ quotaSnapshotPreflight() {
4301
+ return [...(this.deps.quotaSnapshots?.() ?? [])].map((snapshot) => QuotaSnapshotSchema.parse(snapshot));
4534
4302
  }
4535
4303
  routeBillingKnowledge(input, harnessId) {
4536
4304
  // A selected profile's credential_kind decides billing (round-18 #2).
@@ -4560,10 +4328,10 @@ export class Orchestrator {
4560
4328
  // scout reserve) so a subscription route is not refused for lacking a
4561
4329
  // cash quote.
4562
4330
  attemptCostEvidence(harnessId, attemptId, this.estimateUsdFloor(input.repoRoot), this.routeBillingKnowledge(input, harnessId)),
4563
- buildSpec: (routed, homeEnv, prompt, attemptId) => {
4331
+ buildSpec: async (routed, homeEnv, prompt, attemptId) => {
4564
4332
  const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
4565
4333
  const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
4566
- const sessionFields = this.sessionSpecFields(input, routed.adapter.id, log);
4334
+ const sessionFields = await this.sessionSpecFields(input, routed.adapter.id, knobs.model, log, routed.authRouteEstimate, routed.quotaAdmission);
4567
4335
  const spec = HarnessRunSpec.parse({
4568
4336
  session_id: newId("ses"),
4569
4337
  intent: "synthesize",
@@ -4604,13 +4372,19 @@ export class Orchestrator {
4604
4372
  // Contract validation BEFORE the run is announced (see runRace). The
4605
4373
  // recorded user intent is the CALLER's goal.
4606
4374
  const contract = this.buildContract({ ...input, prompt: opts.contractIntent ?? prompt }, taskId, opts.mode);
4607
- const store = this.artifactStore(input);
4608
- const paths = store.createRun(runId);
4609
- const log = new EventLog(paths.eventsPath, runId, taskId, input.onEvent, input.threadId);
4610
- safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
4611
- log.emit("run.created", { mode: opts.mode, prompt: redactSecrets(prompt) });
4612
- const ledger = this.rootLedger(input, contract, log);
4613
- announce?.(announcedRunContext({ log, store, paths, runId, taskId, mode: opts.mode, phase: "report" }, ledger, () => this.deps.delegationBudgetAuthority?.hasParent(runId) === true));
4375
+ const quotaSnapshots = this.quotaSnapshotPreflight();
4376
+ const { store, paths, log, ledger } = beginAnnouncedRun({
4377
+ input,
4378
+ contract,
4379
+ quotaSnapshots,
4380
+ store: this.artifactStore(input),
4381
+ authority: this.deps.delegationBudgetAuthority,
4382
+ runId,
4383
+ taskId,
4384
+ mode: opts.mode,
4385
+ phase: "report",
4386
+ prompt,
4387
+ }, announce);
4614
4388
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
4615
4389
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
4616
4390
  // The ask/deep-scan report stays bare (its scouts read the tree themselves);
@@ -4752,10 +4526,10 @@ export class Orchestrator {
4752
4526
  intent: opts.intent,
4753
4527
  harnessId: adapter.id,
4754
4528
  // QA-019: an n>1 deep-scan scout admits under a FINITE estimate floor
4755
- // (mirror of the candidate loop): the first scout reserves without a
4756
- // floor, but later scouts pass the repo's usd floor so a subscription
4757
- // swarm is not refused for lacking a per-attempt cash quote under a cap.
4758
- cost: attemptCostEvidence(adapter.id, attemptId, opts.deepScan && idx > 0 ? this.estimateUsdFloor(input.repoRoot) : undefined, this.routeBillingKnowledge(input, adapter.id)),
4529
+ // (mirror of the candidate loop): the first top-level scout reserves
4530
+ // without a floor; later scouts and every real Delegate child pass the
4531
+ // repo floor because they overlap an existing family unit.
4532
+ cost: attemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input, opts.deepScan && idx > 0), this.routeBillingKnowledge(input, adapter.id)),
4759
4533
  });
4760
4534
  if (!lease.granted) {
4761
4535
  log.emit("budget.lease.created", {
@@ -4784,67 +4558,120 @@ export class Orchestrator {
4784
4558
  }
4785
4559
  return { status: "budget_denied", reason: lease.reason ?? "budget lease denied" };
4786
4560
  }
4787
- // Lease granted: the attempt is now committed to run — disclose the launch.
4788
- onLaunch?.();
4789
- const knobs = this.routeSpecKnobs(routed, contract, modelOverride, input.effort);
4790
- const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
4791
- const explorerPrompt = (opts.deepScan
4792
- ? `${prompt}\n\nExplorer ${idx + 1}/${adapters.length}: focus on a distinct slice. Emit evidence-cited findings, explicit unknowns/omissions, and follow-up questions. Do not edit files.`
4793
- : prompt) + contextSection;
4794
- const sessionFields = this.sessionSpecFields(input, adapter.id, log);
4795
- const grantResume = sessionFields.resume_session_id !== null && !resumeGranted.has(adapter.id);
4796
- if (grantResume)
4797
- resumeGranted.add(adapter.id);
4798
- // Continuity (INV-137): a thread ASK turn is a chat turn — hydrate a lane
4799
- // switch/gap with a packet and disclose it. Gated on laneRun (deep-scan
4800
- // scouts are excluded from laneRun); native resume is available only when
4801
- // this slot was granted the lane's recorded session.
4802
- const laneContinuity = laneRun
4803
- ? await this.resolveContinuity(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null, grantResume, store, paths, this.execRootOf(input), log)
4804
- : null;
4805
- // D-16d: the continuation packet pointer rides after the lane pointer so
4806
- // the fresh session is re-grounded in the exhausted attempt's work.
4807
- const promptWithPointers = [explorerPrompt, laneContinuity?.pointerLine, continuationPointer]
4808
- .filter((p) => Boolean(p))
4809
- .join("\n\n");
4810
- let spec = HarnessRunSpec.parse({
4811
- session_id: newId("ses"),
4812
- intent: opts.intent,
4813
- prompt: promptWithPointers,
4814
- cwd: this.execRootOf(input),
4815
- access: "readonly",
4816
- // ASK/EXPLORE/AUDIT read-only runs must forward the user's attachments —
4817
- // a live "describe this image" turn sent an image that was being dropped here, so
4818
- // the model honestly reported it saw nothing (the v0.13 attachment bug).
4819
- attachments: input.attachments ?? [],
4820
- auth_preference: sessionFields.auth_preference,
4821
- credential_profile: sessionFields.credential_profile,
4822
- resume_session_id: grantResume ? sessionFields.resume_session_id : null,
4823
- ...this.harnessSpecKnobs(contract, knobs, opts.intent),
4824
- env_inheritance: envInheritance(this.config(input.repoRoot)),
4825
- // A thread lane turn spawns in its DURABLE per-lane home so the native
4826
- // session it records is reachable for resume next turn; everything else
4827
- // uses the disposable route-context home.
4828
- env: (laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? roHome.env,
4829
- });
4830
- const reportAbort = new AbortController();
4831
- spec.extra["abortSignal"] = input.signal
4832
- ? AbortSignal.any([input.signal, reportAbort.signal])
4833
- : reportAbort.signal;
4834
- const reportInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
4835
- if (reportInteraction)
4836
- spec.extra["interactionChannel"] = reportInteraction;
4837
- // D-16: compile the WorkReport envelope for the read-only lane.
4838
- const readonlyWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(reportInteraction));
4839
- const readonlyWorkMode = this.applyWorkEnvelope(spec, readonlyWorkEnvelope);
4840
- const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
4841
- const answer = new AnswerAssembly();
4842
- const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
4843
- knobs.webPolicy === "cached" ||
4844
- knobs.webPolicy === "live", effectiveWeb, [],
4845
- // Requested-model capture so ask/audit route receipts detect a silent
4846
- // model downgrade (typed model_mismatch), not just agent runs.
4847
- knobs.model);
4561
+ // As with planners, the granted lease owns profile/continuity/spec
4562
+ // preparation. Contain a pre-stream rejection as this attempt's failure;
4563
+ // parallel siblings can then finish before the shared HOME is disposed.
4564
+ const preparation = await (async () => {
4565
+ // Lease granted: the attempt is now committed to run — disclose the launch.
4566
+ onLaunch?.();
4567
+ const knobs = this.routeSpecKnobs(routed, contract, modelOverride, input.effort);
4568
+ const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
4569
+ const explorerPrompt = (opts.deepScan
4570
+ ? `${prompt}\n\nExplorer ${idx + 1}/${adapters.length}: focus on a distinct slice. Emit evidence-cited findings, explicit unknowns/omissions, and follow-up questions. Do not edit files.`
4571
+ : prompt) + contextSection;
4572
+ const sessionFields = await this.sessionSpecFields(input, adapter.id, knobs.model, log, routed.authRouteEstimate, routed.quotaAdmission);
4573
+ const grantResume = sessionFields.resume_session_id !== null && !resumeGranted.has(adapter.id);
4574
+ if (grantResume)
4575
+ resumeGranted.add(adapter.id);
4576
+ // Continuity (INV-137): a thread ASK turn is a chat turn — hydrate a lane
4577
+ // switch/gap with a packet and disclose it. Gated on laneRun (deep-scan
4578
+ // scouts are excluded from laneRun); native resume is available only when
4579
+ // this slot was granted the lane's recorded session.
4580
+ const laneContinuity = laneRun
4581
+ ? await this.resolveContinuity(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null, grantResume, sessionFields, store, paths, this.execRootOf(input), log)
4582
+ : null;
4583
+ // D-16d: the continuation packet pointer rides after the lane pointer so
4584
+ // the fresh session is re-grounded in the exhausted attempt's work.
4585
+ const promptWithPointers = [
4586
+ explorerPrompt,
4587
+ laneContinuity?.pointerLine,
4588
+ continuationPointer,
4589
+ ]
4590
+ .filter((p) => Boolean(p))
4591
+ .join("\n\n");
4592
+ const spec = HarnessRunSpec.parse({
4593
+ session_id: newId("ses"),
4594
+ intent: opts.intent,
4595
+ prompt: promptWithPointers,
4596
+ cwd: this.execRootOf(input),
4597
+ access: "readonly",
4598
+ // ASK/EXPLORE/AUDIT read-only runs must forward the user's attachments —
4599
+ // a live "describe this image" turn sent an image that was being dropped here, so
4600
+ // the model honestly reported it saw nothing (the v0.13 attachment bug).
4601
+ attachments: input.attachments ?? [],
4602
+ auth_preference: sessionFields.auth_preference,
4603
+ credential_profile: sessionFields.credential_profile,
4604
+ resume_session_id: grantResume ? sessionFields.resume_session_id : null,
4605
+ ...this.harnessSpecKnobs(contract, knobs, opts.intent),
4606
+ env_inheritance: envInheritance(this.config(input.repoRoot)),
4607
+ // A thread lane turn spawns in its DURABLE per-lane home so the native
4608
+ // session it records is reachable for resume next turn; everything else
4609
+ // uses the disposable route-context home.
4610
+ env: (laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? roHome.env,
4611
+ });
4612
+ const reportAbort = new AbortController();
4613
+ spec.extra["abortSignal"] = input.signal
4614
+ ? AbortSignal.any([input.signal, reportAbort.signal])
4615
+ : reportAbort.signal;
4616
+ const reportInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
4617
+ if (reportInteraction)
4618
+ spec.extra["interactionChannel"] = reportInteraction;
4619
+ // D-16: compile the WorkReport envelope for the read-only lane.
4620
+ const readonlyWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(reportInteraction));
4621
+ const readonlyWorkMode = this.applyWorkEnvelope(spec, readonlyWorkEnvelope);
4622
+ const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
4623
+ const answer = new AnswerAssembly();
4624
+ const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
4625
+ knobs.webPolicy === "cached" ||
4626
+ knobs.webPolicy === "live", effectiveWeb, [],
4627
+ // Requested-model capture so ask/audit route receipts detect a silent
4628
+ // model downgrade (typed model_mismatch), not just agent runs.
4629
+ knobs.model);
4630
+ return {
4631
+ knobs,
4632
+ spec,
4633
+ reportAbort,
4634
+ reportInteraction,
4635
+ readonlyWorkMode,
4636
+ attemptEventsPath,
4637
+ answer,
4638
+ telemetry,
4639
+ };
4640
+ })().then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
4641
+ if (!preparation.ok) {
4642
+ const message = `read-only attempt setup failed: ${safeErrorMessage(preparation.error)}`;
4643
+ AC.settleGrantedAttemptLease({
4644
+ ledger,
4645
+ leaseId: lease.lease?.lease_id ?? "",
4646
+ attemptId,
4647
+ harnessId: adapter.id,
4648
+ costUsd: 0,
4649
+ costEstimated: false,
4650
+ preStreamFailureSource: "readonly-pre-stream",
4651
+ });
4652
+ const telemetry = createAttemptTelemetry(contract.external_context.policy, contract.external_context.web_required, contract.external_context.effective_mode);
4653
+ setAttemptOutcome(telemetry, {
4654
+ deliverablePresent: false,
4655
+ gatesPassed: null,
4656
+ harnessErrored: true,
4657
+ webRequiredUnsatisfied: false,
4658
+ });
4659
+ attempts.push({
4660
+ attemptId,
4661
+ harnessId: adapter.id,
4662
+ status: "failed",
4663
+ report: "",
4664
+ error: message,
4665
+ telemetry,
4666
+ });
4667
+ attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
4668
+ if (opts.deepScan) {
4669
+ store.writeText(join(paths.findingsDir, `${attemptId}-error.md`), `# Explorer ${attemptId} failed\n\n${message}\n`);
4670
+ }
4671
+ return { status: "launched" };
4672
+ }
4673
+ const { knobs, spec: preparedSpec, reportAbort, reportInteraction, readonlyWorkMode, attemptEventsPath, answer, telemetry, } = preparation.value;
4674
+ let spec = preparedSpec;
4848
4675
  const retryPolicy = transientRetryPolicy(this.config(input.repoRoot));
4849
4676
  let activeSessionId = spec.session_id;
4850
4677
  const onAbort = () => {
@@ -4884,11 +4711,13 @@ export class Orchestrator {
4884
4711
  try {
4885
4712
  const watchedReport = withInactivityWatchdog(adapter.run(runSpec), {
4886
4713
  timeoutMs: harnessInactivityTimeoutMs(this.config(input.repoRoot)),
4714
+ countsAsProgress: countsAsAgentProgress,
4887
4715
  onTimeout: () => {
4888
4716
  reportAbort.abort();
4889
4717
  void adapter.cancel?.(activeSessionId)?.catch(() => { });
4890
4718
  },
4891
4719
  isSuspended: () => (reportInteraction?.pendingCount?.() ?? 0) > 0,
4720
+ suspensionVersion: () => reportInteraction?.suspensionVersion?.() ?? 0,
4892
4721
  });
4893
4722
  for await (const ev of watchedReport) {
4894
4723
  if (input.signal?.aborted)
@@ -4952,13 +4781,18 @@ export class Orchestrator {
4952
4781
  // W5.4 reactive failover, READ-ONLY lane (same contract as the
4953
4782
  // candidate lane; typed limits only, never plain transients).
4954
4783
  if (harnessError && !input.signal?.aborted) {
4784
+ const rotationPolicy = this.profilePolicy(input.repoRoot, adapter.id);
4785
+ const readyProfileIds = sawTypedLimit && reportSoFar.length === 0 && rotationPolicy.limit_action === "rotate"
4786
+ ? await this.readyProfileIdsForRotation(input, adapter.id, spec.credential_profile ?? null, triedProfiles)
4787
+ : new Set();
4955
4788
  const rotated = rotateSpecOnTypedLimit({
4956
4789
  spec,
4957
4790
  harnessId: adapter.id,
4958
4791
  attemptId,
4959
- policy: this.profilePolicy(input.repoRoot, adapter.id),
4792
+ policy: rotationPolicy,
4960
4793
  registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
4961
4794
  snapshots: this.deps.quotaSnapshots?.() ?? [],
4795
+ readyProfileIds,
4962
4796
  triedProfiles,
4963
4797
  sawTypedLimit,
4964
4798
  deliverableEmpty: reportSoFar.length === 0,
@@ -5000,7 +4834,17 @@ export class Orchestrator {
5000
4834
  }
5001
4835
  finally {
5002
4836
  input.signal?.removeEventListener("abort", onAbort);
5003
- ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost));
4837
+ AC.settleGrantedAttemptLease({
4838
+ ledger,
4839
+ leaseId: lease.lease?.lease_id ?? "",
4840
+ attemptId,
4841
+ harnessId: adapter.id,
4842
+ costUsd: cost,
4843
+ costEstimated,
4844
+ authMode: telemetry.authMode,
4845
+ usageCost: telemetry.usageCost,
4846
+ preStreamFailureSource: "readonly-pre-stream",
4847
+ });
5004
4848
  }
5005
4849
  if (harnessError && telemetry.transientFailures.length > 0) {
5006
4850
  log.emit("route.transient.exhausted", {
@@ -5011,11 +4855,14 @@ export class Orchestrator {
5011
4855
  });
5012
4856
  }
5013
4857
  attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
5014
- // D-16: un-nest the {work_report, output} envelope; the OUTPUT is the report.
4858
+ // D-16: remove the WorkReport transport; the deliverable is the report.
5015
4859
  const roUnwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", readonlyWorkMode, {
5016
4860
  sideToolReport: telemetry.sideToolWorkReport ?? undefined,
5017
4861
  });
5018
- const report = redactSecrets(roUnwrapped.deliverable);
4862
+ // Trim symmetrically with the plan path: a whitespace-only answer is not
4863
+ // a delivered report (the final-artifact wrapper heading would otherwise
4864
+ // make it read as present content by construction).
4865
+ const report = redactSecrets(roUnwrapped.deliverable).trim();
5019
4866
  const unrecovered = unrecoveredToolErrors(telemetry);
5020
4867
  const webBlocked = webUnsatisfied(telemetry);
5021
4868
  const reportPresent = report.length > 0;
@@ -5043,6 +4890,13 @@ export class Orchestrator {
5043
4890
  harnessErrored: harnessError !== null && !webBlocked,
5044
4891
  webRequiredUnsatisfied: webBlocked,
5045
4892
  workState: roFinalized.workState,
4893
+ // A read-only attempt that completed CLEANLY with an honestly empty
4894
+ // answer is a success with deliverable_present=false ("(no output)"),
4895
+ // never a fake contract failure — the trim above must not convert a
4896
+ // phantom deliverable into a phantom harness failure. Only the clean
4897
+ // finalizer class qualifies: contract failures, vetoes, and context
4898
+ // interruptions keep the strict deliverable requirement.
4899
+ emptyDeliverableAllowed: roFinalized.outcomeClass === "clean",
5046
4900
  });
5047
4901
  if (harnessError) {
5048
4902
  log.emit("harness.completed", {
@@ -5473,7 +5327,7 @@ export class Orchestrator {
5473
5327
  }
5474
5328
  this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, opts.deepScan ? null : (succeeded[0]?.attemptId ?? null), deepScanSynthesis);
5475
5329
  log.emit("output.ready", {
5476
- kind: opts.mode === "ask" ? "answer" : "report",
5330
+ kind: opts.deepScan ? "report" : "answer",
5477
5331
  path: `final/${opts.artifactName}`,
5478
5332
  });
5479
5333
  if (opts.deepScan) {
@@ -5531,11 +5385,16 @@ export class Orchestrator {
5531
5385
  .map((a) => `${a.attemptId}:${a.harnessId}:${a.status}`)
5532
5386
  .join(", ");
5533
5387
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${opts.mode})\n\n- Harnesses: ${harnessLabel}\n- Lifecycle: ${terminalFacts.lifecycle}${terminalFacts.reason ? ` (${terminalFacts.reason})` : ""}\n\n${report}\n`);
5388
+ const reportProducerAttemptId = opts.deepScan &&
5389
+ deepScanSynthesis?.status === "succeeded" &&
5390
+ deepScanSynthesis.reducer_attempt_id
5391
+ ? deepScanSynthesis.reducer_attempt_id
5392
+ : (succeeded[0]?.attemptId ?? "a01");
5534
5393
  store.writeYaml(join(paths.finalDir, "work_product.yaml"), {
5535
5394
  id: newId("wp"),
5536
5395
  kind: "report",
5537
5396
  source_task_id: taskId,
5538
- producer_attempt_id: succeeded[0]?.attemptId ?? "a01",
5397
+ producer_attempt_id: reportProducerAttemptId,
5539
5398
  files: Object.fromEntries([[opts.artifactName, join(paths.finalDir, opts.artifactName)]]),
5540
5399
  meta: {
5541
5400
  harnesses: attempts.map((a) => a.harnessId),
@@ -5544,7 +5403,7 @@ export class Orchestrator {
5544
5403
  read_only: true,
5545
5404
  },
5546
5405
  });
5547
- log.emit("work_product.emitted", { kind: "report", winner: succeeded[0]?.attemptId ?? null });
5406
+ log.emit("work_product.emitted", { kind: "report", winner: reportProducerAttemptId });
5548
5407
  const workVetoed = terminalFacts.work_state?.state === "needs_input" ||
5549
5408
  terminalFacts.work_state?.state === "incomplete";
5550
5409
  if (terminalFacts.lifecycle !== "succeeded") {
@@ -5596,9 +5455,4 @@ export class Orchestrator {
5596
5455
  };
5597
5456
  }
5598
5457
  }
5599
- function assertNoSecretLikeTokens(label, text) {
5600
- if (containsSecretLikeToken(text)) {
5601
- throw new Error(`${label} contains secret-like token; refusing to persist artifact`);
5602
- }
5603
- }
5604
5458
  //# sourceMappingURL=orchestrator.js.map