@claudexor/orchestrator 3.1.2 → 3.3.7-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/dist/attemptFinalize.d.ts +14 -11
  2. package/dist/attemptFinalize.d.ts.map +1 -1
  3. package/dist/attemptFinalize.js +41 -14
  4. package/dist/attemptFinalize.js.map +1 -1
  5. package/dist/attemptTelemetry.d.ts +7 -0
  6. package/dist/attemptTelemetry.d.ts.map +1 -1
  7. package/dist/attemptTelemetry.js +2 -1
  8. package/dist/attemptTelemetry.js.map +1 -1
  9. package/dist/attemptUsageCost.d.ts +24 -2
  10. package/dist/attemptUsageCost.d.ts.map +1 -1
  11. package/dist/attemptUsageCost.js +18 -2
  12. package/dist/attemptUsageCost.js.map +1 -1
  13. package/dist/budgetFailure.d.ts +1 -1
  14. package/dist/budgetFailure.d.ts.map +1 -1
  15. package/dist/budgetFailure.js.map +1 -1
  16. package/dist/candidateEvidence.d.ts +24 -0
  17. package/dist/candidateEvidence.d.ts.map +1 -1
  18. package/dist/candidateEvidence.js +26 -0
  19. package/dist/candidateEvidence.js.map +1 -1
  20. package/dist/credential-profile-rotation.d.ts +145 -0
  21. package/dist/credential-profile-rotation.d.ts.map +1 -0
  22. package/dist/credential-profile-rotation.js +335 -0
  23. package/dist/credential-profile-rotation.js.map +1 -0
  24. package/dist/credential-profiles.d.ts +90 -130
  25. package/dist/credential-profiles.d.ts.map +1 -1
  26. package/dist/credential-profiles.js +156 -290
  27. package/dist/credential-profiles.js.map +1 -1
  28. package/dist/decisionBudget.d.ts +2 -2
  29. package/dist/decisionBudget.d.ts.map +1 -1
  30. package/dist/deepScanReducer.d.ts +14 -44
  31. package/dist/deepScanReducer.d.ts.map +1 -1
  32. package/dist/deepScanReducer.js +240 -128
  33. package/dist/deepScanReducer.js.map +1 -1
  34. package/dist/delegatedHome.d.ts +156 -0
  35. package/dist/delegatedHome.d.ts.map +1 -0
  36. package/dist/delegatedHome.js +179 -0
  37. package/dist/delegatedHome.js.map +1 -0
  38. package/dist/delegationBudgetAuthority.d.ts.map +1 -1
  39. package/dist/delegationBudgetAuthority.js.map +1 -1
  40. package/dist/delegationFailure.d.ts.map +1 -1
  41. package/dist/git-precondition.d.ts +11 -0
  42. package/dist/git-precondition.d.ts.map +1 -0
  43. package/dist/git-precondition.js +69 -0
  44. package/dist/git-precondition.js.map +1 -0
  45. package/dist/index.d.ts +1 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/interaction.d.ts +14 -6
  50. package/dist/interaction.d.ts.map +1 -1
  51. package/dist/interaction.js +106 -19
  52. package/dist/interaction.js.map +1 -1
  53. package/dist/orchestrator.d.ts +64 -83
  54. package/dist/orchestrator.d.ts.map +1 -1
  55. package/dist/orchestrator.js +578 -691
  56. package/dist/orchestrator.js.map +1 -1
  57. package/dist/planBrief.d.ts +27 -0
  58. package/dist/planBrief.d.ts.map +1 -0
  59. package/dist/planBrief.js +42 -0
  60. package/dist/planBrief.js.map +1 -0
  61. package/dist/planQuestions.js +4 -1
  62. package/dist/planQuestions.js.map +1 -1
  63. package/dist/planRun.d.ts +4 -3
  64. package/dist/planRun.d.ts.map +1 -1
  65. package/dist/planRun.js +33 -33
  66. package/dist/planRun.js.map +1 -1
  67. package/dist/plannerAttempt.d.ts +64 -0
  68. package/dist/plannerAttempt.d.ts.map +1 -0
  69. package/dist/plannerAttempt.js +235 -0
  70. package/dist/plannerAttempt.js.map +1 -0
  71. package/dist/policyFindings.js.map +1 -1
  72. package/dist/requestRequirements.d.ts +43 -3
  73. package/dist/requestRequirements.d.ts.map +1 -1
  74. package/dist/requestRequirements.js +106 -13
  75. package/dist/requestRequirements.js.map +1 -1
  76. package/dist/reviewerPanel.js.map +1 -1
  77. package/dist/root-ledger.d.ts.map +1 -1
  78. package/dist/root-ledger.js +2 -0
  79. package/dist/root-ledger.js.map +1 -1
  80. package/dist/run-bounded.d.ts.map +1 -1
  81. package/dist/run-bounded.js +7 -1
  82. package/dist/run-bounded.js.map +1 -1
  83. package/dist/run-input-resolution.d.ts.map +1 -1
  84. package/dist/run-input-resolution.js +26 -8
  85. package/dist/run-input-resolution.js.map +1 -1
  86. package/dist/runEventLog.d.ts +48 -0
  87. package/dist/runEventLog.d.ts.map +1 -0
  88. package/dist/runEventLog.js +63 -0
  89. package/dist/runEventLog.js.map +1 -0
  90. package/dist/runFacts.d.ts +23 -0
  91. package/dist/runFacts.d.ts.map +1 -0
  92. package/dist/runFacts.js +179 -0
  93. package/dist/runFacts.js.map +1 -0
  94. package/dist/runFactsBuilder.d.ts +5 -0
  95. package/dist/runFactsBuilder.d.ts.map +1 -0
  96. package/dist/runFactsBuilder.js +412 -0
  97. package/dist/runFactsBuilder.js.map +1 -0
  98. package/dist/runFactsDeliverable.d.ts +13 -0
  99. package/dist/runFactsDeliverable.d.ts.map +1 -0
  100. package/dist/runFactsDeliverable.js +94 -0
  101. package/dist/runFactsDeliverable.js.map +1 -0
  102. package/dist/runFactsPresentation.d.ts +7 -0
  103. package/dist/runFactsPresentation.d.ts.map +1 -0
  104. package/dist/runFactsPresentation.js +113 -0
  105. package/dist/runFactsPresentation.js.map +1 -0
  106. package/dist/runFactsReview.d.ts +7 -0
  107. package/dist/runFactsReview.d.ts.map +1 -0
  108. package/dist/runFactsReview.js +63 -0
  109. package/dist/runFactsReview.js.map +1 -0
  110. package/dist/runSupport.d.ts +17 -15
  111. package/dist/runSupport.d.ts.map +1 -1
  112. package/dist/runSupport.js +16 -0
  113. package/dist/runSupport.js.map +1 -1
  114. package/dist/runTerminalContext.d.ts +31 -0
  115. package/dist/runTerminalContext.d.ts.map +1 -0
  116. package/dist/runTerminalContext.js +12 -0
  117. package/dist/runTerminalContext.js.map +1 -0
  118. package/dist/runTerminalResults.d.ts +80 -0
  119. package/dist/runTerminalResults.d.ts.map +1 -0
  120. package/dist/runTerminalResults.js +164 -0
  121. package/dist/runTerminalResults.js.map +1 -0
  122. package/dist/runTerminals.d.ts +4 -80
  123. package/dist/runTerminals.d.ts.map +1 -1
  124. package/dist/runTerminals.js +357 -260
  125. package/dist/runTerminals.js.map +1 -1
  126. package/dist/secretDiff.d.ts +3 -0
  127. package/dist/secretDiff.d.ts.map +1 -1
  128. package/dist/secretDiff.js +7 -0
  129. package/dist/secretDiff.js.map +1 -1
  130. package/dist/structuredOutput.d.ts.map +1 -1
  131. package/dist/structuredOutput.js.map +1 -1
  132. package/dist/task-contract-builder.d.ts +35 -0
  133. package/dist/task-contract-builder.d.ts.map +1 -0
  134. package/dist/task-contract-builder.js +114 -0
  135. package/dist/task-contract-builder.js.map +1 -0
  136. package/dist/transientClassify.js.map +1 -1
  137. package/package.json +17 -17
@@ -1,17 +1,19 @@
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, vendorVerifiedProfileStatus, } 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";
6
+ import { appliedAttemptFacts, assertDelegatedEvidence, confinementNotice, externallyConfinedLane, scopedHarnessHome, } from "./delegatedHome.js";
6
7
  import * as AC from "./attemptUsageCost.js";
7
- import { candidateRoster, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, } from "./candidateEvidence.js";
8
+ import { candidateRoster, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, unanimousDeclaredFailure, } from "./candidateEvidence.js";
8
9
  import { capabilityIntents } from "@claudexor/gateway";
9
10
  import { policyFindings } from "./policyFindings.js";
10
11
  import { join } from "node:path";
11
12
  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";
13
+ import { runPlannerAttempt as executePlannerAttempt, } from "./plannerAttempt.js";
14
+ import { HarnessRunSpec, ModeKind as ModeKindSchema, QuotaSnapshot as QuotaSnapshotSchema, isBlocking, makeOutcomeFacts, normalizeUserOutputSchema, strictifyOutputSchema, estimateEffectiveAuthRoute, } from "@claudexor/schema";
15
+ import { globalConfigDir, loadConfig } from "@claudexor/config";
16
+ import { AnswerAssembly, CLAUDEXOR_ARTIFACT_DIR, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, countsAsAgentProgress, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
15
17
  import { assertRouteModelsAllowed } from "./modelGovernance.js";
16
18
  import { governRouteEffort } from "./effortGovernance.js";
17
19
  import { isFullAccess, RequestRequirementsResolver } from "./requestRequirements.js";
@@ -20,16 +22,18 @@ import { routingFailureClassification } from "./routing-failure.js";
20
22
  export { routingFailureClassification } from "./routing-failure.js";
21
23
  import { runBounded } from "./run-bounded.js";
22
24
  import { planPrompt } from "./plan-prompt.js";
25
+ import { verifiedPlanBrief, withPlanBrief } from "./planBrief.js";
23
26
  import { resolveRunInputDefaults } from "./run-input-resolution.js";
24
- import { createRootLedger } from "./root-ledger.js";
27
+ import { beginAnnouncedRun } from "./runEventLog.js";
25
28
  import { arbitrationBudgetOptions, decisionBudgetSummary } from "./decisionBudget.js";
26
29
  import { buildRevisePrompt } from "./revisePrompt.js";
27
- import { announcedRunContext, cancelledResult, failTerminally, guardAnnouncedRun, writeFailure, } from "./runTerminals.js";
30
+ import { cancelledResult, declaredFailure, failTerminally, guardAnnouncedRun, writeFailure, } from "./runTerminals.js";
28
31
  import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
29
32
  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";
33
+ 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
34
  import { candidateStatusInRouteContext, resolveReadOnlyRouteContext, } from "./routeContext.js";
32
35
  import { resolveAutoReviewerPanel, resolveExplicitReviewerPanel } from "./reviewerPanel.js";
36
+ import { ensureWriteModeGitBoundary } from "./git-precondition.js";
33
37
  import { buildContinuation, } from "./continuity.js";
34
38
  import { activePlanPointer, resolveContinuitySummary, workspaceAnchor, } from "./continuity-facts.js";
35
39
  import { runDiffReview } from "./diffReview.js";
@@ -41,18 +45,18 @@ import { dominantHarnessFailureCategory, harnessFailureNextActions } from "./har
41
45
  import { finalizeAttempt, readOnlyNoSuccessTerminal, resolveWorkReportEnvelope, unrecoveredToolErrorFailure, unwrapWorkReportEnvelope, webEvidenceFailure, } from "./attemptFinalize.js";
42
46
  import { buildContinuationPacket, decideContinuation, synthesizeContinuationRequest, } from "./continuation.js";
43
47
  import { interactionChannelFor } from "./interaction.js";
44
- import { gateSpecsFromContract, renderTestsEvidence, resolveContractGates, } from "./contract-gates.js";
48
+ import { gateSpecsFromContract, renderTestsEvidence } from "./contract-gates.js";
49
+ import { buildTaskContract } from "./task-contract-builder.js";
45
50
  import { ArtifactStore } from "@claudexor/artifact-store";
46
- import { EventLog } from "@claudexor/event-log";
47
51
  import { assertMandatoryContext, buildContextPack, rawContextForEnvelope, preflightEvidence, writeEvidencePacket, } from "@claudexor/context";
48
- import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, ensureClaudeBridge, ensureGitRepository, consumeRawPatchEnvelope, snapshotTree, } from "@claudexor/workspace";
52
+ import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, ensureClaudeBridge, consumeRawPatchEnvelope, snapshotTree, } from "@claudexor/workspace";
49
53
  import { blockedDecisionOverride, finalVerifyBlocks, finalVerifyPatch, verifyAndDeliver, } from "@claudexor/delivery";
50
54
  import { HarnessGateway } from "@claudexor/gateway";
51
55
  import { ReadinessLedger, evaluateConvergence, failureSignature, gatesPassed, reviewCandidate, revalidateFindings, runGates, } from "@claudexor/review";
52
56
  import { arbitrate } from "@claudexor/arbitration";
53
57
  import { buildSynthesisPlan, decideSynthesis } from "@claudexor/synthesis";
54
58
  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";
59
+ import { readTextSafe, appendLine, assertNoInlineSecretValues, DELEGATION_ENV, hashJson, newId, noProjectRepoRoot, redactSecrets, safeInvoke, sha256, userConfigDir, writeText, } from "@claudexor/util";
56
60
  import { assertWriteIsolation } from "./write-isolation.js";
57
61
  const LABELS = "ABCDEFGHIJ".split("");
58
62
  const NO_PROJECT_ROOT = noProjectRepoRoot();
@@ -110,10 +114,14 @@ export class Orchestrator {
110
114
  throw new Error(`Delegate is an agent-only strategy (got mode=${mode})`);
111
115
  }
112
116
  const runId = resolved.runId ?? newId("run");
117
+ let delegatedLedgerAttached = false;
113
118
  resolved = {
114
119
  ...resolved,
115
120
  runId,
116
121
  taskId: resolved.taskId ?? newId("task"),
122
+ onDelegatedLedgerAttached: () => {
123
+ delegatedLedgerAttached = true;
124
+ },
117
125
  };
118
126
  if (resolved.delegate === true) {
119
127
  resolved = {
@@ -157,50 +165,63 @@ export class Orchestrator {
157
165
  if (resolved.repoRoot !== NO_PROJECT_ROOT) {
158
166
  assertMandatoryContext(resolved.repoRoot, this.projectConfig(resolved.repoRoot).context.mandatory_files);
159
167
  }
160
- // Reviewer panels are validated only inside the strategies that actually
161
- // review (race/convergence under agent, and plan) — AFTER run-dir
168
+ // Reviewer panels are validated only inside Agent strategies that actually
169
+ // review (race/convergence; Plan Council is the plan critique path) — AFTER run-dir
162
170
  // creation, so a doomed explicit panel yields typed failure ARTIFACTS
163
171
  // (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/
172
+ // Ask and Plan never spawn code reviewers, so a panel there never spends doctor/
165
173
  // model probes and never fails a run that would not use it.
166
174
  // Whole-strategy terminal net: once a strategy ANNOUNCES its
167
175
  // run, any escaped throw still stamps failure.yaml + summary + run.failed
168
176
  // 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);
177
+ const releaseRunState = (settledRunId) => {
178
+ this.routingRationaleByRun.delete(settledRunId);
179
+ this.deps.delegationBudgetAuthority?.releaseRun(settledRunId);
180
+ };
181
+ try {
182
+ return await guardAnnouncedRun(resolved.signal, (announce) => {
183
+ switch (mode) {
184
+ case "ask":
185
+ // `--deep-scan` widens the answer into the bounded multi-scout
186
+ // research sweep with synthesis (the old `audit --swarm`/`explore`).
187
+ return resolved.deepScan
188
+ ? this.runDeepScan(resolved, announce)
189
+ : this.runAsk(resolved, announce);
190
+ case "agent":
191
+ // Engine strategies are FLAGS on agent (v0.9 collapse): `--until-clean`
192
+ // and `--attempts` select the convergence loop; `--n` selects the race
193
+ // width; `--create` switches the candidate intent to create_from_scratch.
194
+ if (resolved.untilClean)
195
+ return this.runConvergence(resolved, mode, null, announce);
196
+ if (resolved.attempts !== undefined && resolved.attempts !== null) {
197
+ return this.runConvergence(resolved, mode, resolved.attempts, announce);
198
+ }
199
+ return this.runRace({ ...resolved, n: resolved.n ?? 1 }, mode, announce);
200
+ case "plan":
201
+ return this.runPlan(resolved, announce);
202
+ }
203
+ }, async ({ runId }) => {
204
+ const authority = this.deps.delegationBudgetAuthority;
205
+ if (!authority?.hasParent(runId))
206
+ return;
207
+ authority.beginParentClose(runId);
208
+ await authority.waitForChildren(runId);
209
+ },
210
+ // Single per-run terminalization hook: release the routing-rationale map
211
+ // entry on EVERY terminal (incl. a run that died before its telemetry
212
+ // writer ran, which is the leak this closes).
213
+ releaseRunState);
214
+ }
215
+ catch (error) {
216
+ // A durable startup sink may refuse `run.created` before the strategy can
217
+ // announce its context. Release only a delegated child ledger acquired by
218
+ // THIS invocation; a caller-supplied run-id collision must never release
219
+ // another live parent/child authority or its routing state.
220
+ if (delegatedLedgerAttached) {
221
+ this.deps.delegationBudgetAuthority?.releaseRun(runId);
189
222
  }
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
- });
223
+ throw error;
224
+ }
204
225
  }
205
226
  async resolveReviewers(cwd, runAuthPreference, onIgnoredSetting) {
206
227
  if (this.deps.reviewers)
@@ -273,11 +294,7 @@ export class Orchestrator {
273
294
  }
274
295
  authPreferenceForHarness(repoRoot, harnessId, runAuthPreference) {
275
296
  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");
297
+ return effectiveAuthPreference(runAuthPreference, cfg?.harnesses?.[harnessId]?.auth_preference, cfg?.routing?.auth_preference);
281
298
  }
282
299
  artifactStore(input) {
283
300
  if (input.mode === "ask" && input.contextMode === "off" && input.repoRoot === NO_PROJECT_ROOT) {
@@ -300,20 +317,23 @@ export class Orchestrator {
300
317
  estimateUsdFloor(repoRoot) {
301
318
  return this.config(repoRoot)?.global.budget.estimate_usd_floor ?? 0.05;
302
319
  }
320
+ /** Delegate children overlap their still-running parent, so child-side paid
321
+ * units use the same bounded floor as later slots in a parallel wave. */
322
+ reservationEstimateUsd(input, parallel = false) {
323
+ return parallel || Boolean(input.delegatedFromRunId)
324
+ ? this.estimateUsdFloor(input.repoRoot)
325
+ : undefined;
326
+ }
303
327
  execRootOf(input) {
304
328
  return input.executionRoot ?? input.repoRoot;
305
329
  }
306
- sessionSpecFields(input, harnessId, log) {
330
+ async sessionSpecFields(input, harnessId, log, defaultRoute = null) {
307
331
  const cfg = this.config(input.repoRoot)?.global;
308
- const profile = this.preflightProfile(input, harnessId, log);
309
- const explicit = (v) => (v && v !== "auto" ? v : undefined);
332
+ const profile = await this.preflightProfile(input, harnessId, log, defaultRoute);
310
333
  return {
311
334
  // "auto" at ANY level falls through (thread turns send the thread default
312
335
  // "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",
336
+ auth_preference: effectiveAuthPreference(input.authPreference, cfg?.harnesses?.[harnessId]?.auth_preference, cfg?.routing?.auth_preference),
317
337
  resume_session_id: resumeSessionForProfile(input.resumeSessions?.[harnessId], profile),
318
338
  credential_profile: profile,
319
339
  };
@@ -388,19 +408,74 @@ export class Orchestrator {
388
408
  const policy = this.config(repoRoot)?.global.harnesses?.[harnessId]?.profile_policy;
389
409
  return policy ?? { limit_action: "fail", rotation_eligible: [], headroom_threshold: 0.9 };
390
410
  }
391
- preflightProfile(input, harnessId, log) {
411
+ /** The quota poller's authenticated vendor evidence for THIS decision epoch,
412
+ * read once so every profile in the epoch is judged against one observation
413
+ * set. This is what makes profile readiness at admission mean the same thing
414
+ * it means on the Accounts card (INV-135 honesty). */
415
+ vendorQuotaObservations() {
416
+ return {
417
+ snapshots: this.deps.quotaSnapshots?.() ?? [],
418
+ absences: this.deps.quotaAbsences?.() ?? [],
419
+ };
420
+ }
421
+ /** Fresh profile readiness for one rotation decision epoch. Accounts uses
422
+ * the same probe wrapper + vendor overlay + admission predicate when
423
+ * projecting next_up. */
424
+ async readyProfileIdsForRotation(input, harnessId, current, excluded = new Set()) {
425
+ const profiles = staticRotationCandidates({
426
+ registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
427
+ harnessId,
428
+ policy: this.profilePolicy(input.repoRoot, harnessId),
429
+ current,
430
+ excluded,
431
+ });
432
+ const adapter = this.deps.registry.get(harnessId);
433
+ const quota = this.vendorQuotaObservations();
434
+ const entries = await Promise.all(profiles.map(async (profile) => ({
435
+ profile,
436
+ // Rotating INTO a profile the vendor has already rejected would spend a
437
+ // whole attempt to rediscover the 401 the poller reported a minute ago.
438
+ status: vendorVerifiedProfileStatus(await probeCredentialProfileStatus(profile, adapter?.probeCredentialProfile?.bind(adapter)), quota),
439
+ })));
440
+ return new Set(entries
441
+ .filter(({ profile, status }) => profileStatusAdmits(profile, status))
442
+ .map(({ profile }) => profile.profile_id));
443
+ }
444
+ async preflightProfile(input, harnessId, log, defaultRoute) {
392
445
  const profile = this.resolveCredentialProfile(input, harnessId);
393
446
  const policy = this.profilePolicy(input.repoRoot, harnessId);
394
447
  const registry = this.config(input.repoRoot)?.global.credential_profiles ?? [];
395
448
  const snapshots = this.deps.quotaSnapshots?.() ?? [];
396
449
  const emit = (type, payload) => log?.emit(type, payload);
450
+ const breach = profileHeadroomBreach(snapshots, harnessId, profile?.profile_id ?? null, policy.headroom_threshold);
451
+ const readyProfileIds = policy.limit_action === "rotate" &&
452
+ breach !== null &&
453
+ (profile !== null || defaultRoute === "local_session")
454
+ ? await this.readyProfileIdsForRotation(input, harnessId, profile)
455
+ : new Set();
397
456
  if (!profile) {
398
457
  // Unpinned runs (INV-135 auto-balance): under `rotate`, a fresh
399
458
  // default-subject headroom breach starts on the next eligible
400
459
  // subscription profile instead; `fail`/`ask` change nothing.
401
- return preflightDefaultSubject({ harnessId, policy, registry, snapshots, emit });
460
+ return preflightDefaultSubject({
461
+ harnessId,
462
+ policy,
463
+ registry,
464
+ snapshots,
465
+ readyProfileIds,
466
+ defaultRoute,
467
+ emit,
468
+ });
402
469
  }
403
- return preflightCredentialProfile({ profile, harnessId, policy, registry, snapshots, emit });
470
+ return preflightCredentialProfile({
471
+ profile,
472
+ harnessId,
473
+ policy,
474
+ registry,
475
+ snapshots,
476
+ readyProfileIds,
477
+ emit,
478
+ });
404
479
  }
405
480
  /**
406
481
  * Resolve candidate adapters: explicit `--harness`, else available real harnesses, then
@@ -477,6 +552,27 @@ export class Orchestrator {
477
552
  }
478
553
  }
479
554
  }
555
+ const attachments = input.attachments ?? [];
556
+ // Resolve the complete attachment pool once from manifest truth. If any
557
+ // lane still lacks usable discovery truth, preserve the existing per-lane
558
+ // discovery/doctor precedence and resolve that lane inside the route loop.
559
+ const canResolveAttachmentPool = attachments.length > 0 &&
560
+ ids.every((id) => {
561
+ const status = statusById.get(id);
562
+ return status?.manifest != null && status.status !== "unavailable";
563
+ });
564
+ const attachmentPoolAdmission = canResolveAttachmentPool
565
+ ? this.requestRequirements.resolveAttachmentPool(explicitPool ? "explicit" : "auto", attachments, ids.map((id) => ({
566
+ harnessId: id,
567
+ declarations: statusById.get(id)?.manifest?.capability_profile.attachment_inputs ?? null,
568
+ available: true,
569
+ })))
570
+ : null;
571
+ if (attachmentPoolAdmission?.outcome === "refused") {
572
+ throw new HarnessUnavailableError(attachmentPoolAdmission.message ??
573
+ "no available harness lane can receive the selected attachments");
574
+ }
575
+ const attachmentRejectionById = new Map(attachmentPoolAdmission?.rejected.map((admission) => [admission.harnessId, admission]) ?? []);
480
576
  const policy = input.web ?? input.externalContextPolicy ?? "auto";
481
577
  const pool = [];
482
578
  const dropped = [];
@@ -495,6 +591,9 @@ export class Orchestrator {
495
591
  dropped.push(detail);
496
592
  droppedLanes.push({ harnessId, stage, detail });
497
593
  };
594
+ // One observation set for the whole admission pass, so two lanes cannot be
595
+ // judged against different vendor epochs.
596
+ const vendorQuota = this.vendorQuotaObservations();
498
597
  for (const id of ids) {
499
598
  const adapter = this.deps.registry.get(id);
500
599
  if (!adapter) {
@@ -558,6 +657,11 @@ export class Orchestrator {
558
657
  profileId: this.effectiveProfileId(input, id),
559
658
  harnessId: id,
560
659
  probe: profileAdapter?.probeCredentialProfile?.bind(profileAdapter),
660
+ // `verification: passed` from the local store only means a login file
661
+ // is present. The poller's authenticated vendor call is the only
662
+ // liveness evidence we have, and admission is the surface that must
663
+ // act on it — otherwise the run dispatches into a revoked token.
664
+ quota: vendorQuota,
561
665
  });
562
666
  if (profileVerdict !== null) {
563
667
  if (profileVerdict === "available") {
@@ -590,7 +694,7 @@ export class Orchestrator {
590
694
  const readOnlyIntent = intent === "plan" || intent === "spec" || intent === "explain" || intent === "audit";
591
695
  const requiredAccess = this.requestRequirements.adapterAccess(intent, manifest.capabilities.implementation_transport, readOnlyIntent
592
696
  ? "readonly"
593
- : (input.access ?? this.config(input.repoRoot).trust.access_default));
697
+ : (input.access ?? this.config(input.repoRoot).trust.access_default), externallyConfinedLane(input.delegated === true));
594
698
  const accessSupported = !requiredAccess || manifest.access_profiles_supported.includes(requiredAccess);
595
699
  const webSupport = manifest.capabilities.web_policy;
596
700
  // Match routeSpecKnobs: a per-harness web default upgrades run-level auto,
@@ -610,9 +714,10 @@ export class Orchestrator {
610
714
  dropLane(id, "web", why);
611
715
  continue;
612
716
  }
613
- const attachmentRefusal = this.requestRequirements.attachmentRefusal(id, input.attachments ?? [], manifest.capability_profile.attachment_inputs);
614
- if (attachmentRefusal) {
615
- dropLane(id, "attachment", attachmentRefusal);
717
+ const attachmentAdmission = attachmentRejectionById.get(id) ??
718
+ this.requestRequirements.resolveAttachmentLane(id, attachments, manifest.capability_profile.attachment_inputs);
719
+ if (!attachmentAdmission.admitted) {
720
+ dropLane(id, "attachment", attachmentAdmission.message ?? `${id} rejects attachments`);
616
721
  continue;
617
722
  }
618
723
  const reason = status.reasons.length > 0 ? `: ${status.reasons.join("; ")}` : "";
@@ -981,119 +1086,9 @@ export class Orchestrator {
981
1086
  return this.config(repoRoot).project;
982
1087
  }
983
1088
  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)),
1089
+ return buildTaskContract(input, taskId, mode, {
1090
+ paidBudget: this.deps.paidBudget,
1091
+ routingGoal: this.deps.routingGoal,
1097
1092
  });
1098
1093
  }
1099
1094
  /**
@@ -1172,9 +1167,9 @@ export class Orchestrator {
1172
1167
  /**
1173
1168
  * D-16: the WorkReport transport envelope for one route. Called at every
1174
1169
  * 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.
1170
+ * plain caller-schema transport with the resolved WorkReport channel on
1171
+ * capable routes. The returned `mode` is retained by the caller and handed
1172
+ * to `unwrapWorkReportEnvelope` when the answer is finalized.
1178
1173
  */
1179
1174
  workReportEnvelopeFor(routed, contract, interactive) {
1180
1175
  return resolveWorkReportEnvelope({
@@ -1188,7 +1183,7 @@ export class Orchestrator {
1188
1183
  /**
1189
1184
  * D-16: apply the resolved WorkReport transport to a built spec — set the
1190
1185
  * envelope output_schema (constrained/side_tool routes) and APPEND the fenced
1191
- * envelope instruction (validated routes, e.g. cursor). Mutates the spec in
1186
+ * metadata instruction (validated routes, e.g. cursor). Mutates the spec in
1192
1187
  * place and returns the mode the answer unwrap consumes. Called at every
1193
1188
  * task-producing spec-build site so the transport is never wired one-off.
1194
1189
  */
@@ -1265,7 +1260,7 @@ export class Orchestrator {
1265
1260
  * Returns null (no packet, no pointer) for native resume, a fresh thread, or
1266
1261
  * a non-thread run. Never throws: continuity failure degrades to no packet.
1267
1262
  */
1268
- async resolveContinuity(runInput, harnessId, resolvedProfileId, nativeResumeAvailable, store, paths, repoRoot, log) {
1263
+ async resolveContinuity(runInput, harnessId, resolvedProfileId, nativeResumeAvailable, sessionFields, store, paths, repoRoot, log) {
1269
1264
  const ctx = runInput.threadContinuity;
1270
1265
  if (!runInput.threadId || !ctx)
1271
1266
  return null;
@@ -1303,7 +1298,6 @@ export class Orchestrator {
1303
1298
  // summary. Same credential route + scoped lane home a real read-only
1304
1299
  // thread turn uses (INV-034/135). Best-effort in its OWN guard — a summary
1305
1300
  // failure keeps the full mechanical packet, never drops it.
1306
- const sessionFields = this.sessionSpecFields(runInput, harnessId);
1307
1301
  req.cachedSummary = await resolveContinuitySummary({
1308
1302
  req,
1309
1303
  threadId: runInput.threadId,
@@ -1361,30 +1355,51 @@ export class Orchestrator {
1361
1355
  return { pointerLine: null };
1362
1356
  }
1363
1357
  }
1364
- async runCandidateInEnvelope(routed, envelope, attemptId, label, contract, prompt, store, paths, wsm, ledger, access = "workspace_write", onHarnessEvent, signal, modelHint, effortHint, intent = "implement", log, effectiveWebMode, interaction, budgetGuard, runInput, streamDeltas = false, fileBackedContext,
1358
+ /**
1359
+ * The ONE place an attempt's HOME and OS boundary are decided.
1360
+ *
1361
+ * Called by every caller of `runCandidateInEnvelope` INSIDE that caller's
1362
+ * try, so a delegated refusal is caught by the same catch that writes the
1363
+ * attempt record, and so the decided home is in scope there.
1364
+ */
1365
+ harnessHomeFor(wsm, envelope, routed, runInput) {
1366
+ // Refuses (never silently degrades) when a delegated run cannot be confined.
1367
+ return scopedHarnessHome(wsm, envelope, envelope.worktree_path === envelope.repo_root, runInput?.delegated === true, routed.adapterAccess);
1368
+ }
1369
+ async runCandidateInEnvelope(routed, envelope, attemptId, label, contract, prompt, store, paths, wsm, ledger, access, onHarnessEvent, signal, modelHint, effortHint, intent, log, effectiveWebMode, interaction, budgetGuard, runInput, streamDeltas, fileBackedContext,
1365
1370
  /** D-16d: when set, the mechanical continuation checkpoint pointer for a
1366
1371
  * one-shot fresh-session continuation — appended to the prompt so the model
1367
1372
  * (and the offline fake) re-grounds in the exhausted attempt's partial work. */
1368
- continuationPointer) {
1373
+ continuationPointer,
1374
+ /** Decided by `harnessHomeFor` in the CALLER, so the per-attempt applied
1375
+ * facts exist in the caller's catch too: an attempt that ran and then threw
1376
+ * must record what it ran under, not just why it stopped. REQUIRED (no
1377
+ * default): a silent fallback here would spawn a delegated child on the
1378
+ * operator's real home while the record still claimed confinement. */
1379
+ harnessHome) {
1369
1380
  const adapter = routed.adapter;
1370
1381
  const knobs = this.routeSpecKnobs(routed, contract, modelHint, effortHint);
1371
1382
  // Isolated scoped-home sessions are never retained after disposal.
1372
1383
  const inPlaceEnvelope = envelope.worktree_path === envelope.repo_root;
1373
1384
  const rawContextPacket = await rawContextForEnvelope(routed.implementationTransport, envelope);
1374
- const sessionFields = runInput ? this.sessionSpecFields(runInput, adapter.id, log) : undefined;
1385
+ const sessionFields = runInput
1386
+ ? await this.sessionSpecFields(runInput, adapter.id, log, routed.authRouteEstimate)
1387
+ : undefined;
1375
1388
  // Continuity (INV-137): once the lane (harness + resolved profile) is known,
1376
1389
  // build the continuation packet, materialize context/THREAD.md, and point
1377
1390
  // the prompt at it — never embed the packet body in the prompt. Replaces the
1378
1391
  // old static session.rebound "not_portable" phrase with a real disclosure.
1379
1392
  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)
1393
+ ? 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
1394
  : null;
1382
1395
  let spec = HarnessRunSpec.parse({
1383
1396
  session_id: newId("ses"),
1384
1397
  intent,
1385
1398
  // Engine-derived read-only prompt constraints: protected/auto-protected
1386
1399
  // paths PLUS the exact typed gate argv the run will execute (QA-022 FIX B).
1387
- prompt: promptWithEngineConstraints([prompt, laneContinuity?.pointerLine, continuationPointer]
1400
+ prompt: promptWithEngineConstraints(
1401
+ // A child running without an OS boundary is TOLD so (disclosure 2 of 3).
1402
+ [prompt, confinementNotice(harnessHome), laneContinuity?.pointerLine, continuationPointer]
1388
1403
  .filter((s) => typeof s === "string" && s.length > 0)
1389
1404
  .join("\n\n"), contract.constraints.protected_paths, contract.constraints.auto_protected_paths, contract.constraints.protected_path_approvals, contract.tests.commands),
1390
1405
  attachments: runInput?.attachments ?? [],
@@ -1406,9 +1421,14 @@ export class Orchestrator {
1406
1421
  ...(inPlaceEnvelope && sessionFields?.resume_session_id
1407
1422
  ? { resume_session_id: sessionFields.resume_session_id }
1408
1423
  : {}),
1409
- // Scoped harness home only for isolated envelopes; in-place runs use the
1410
- // native environment so the resumed vendor session is actually reachable.
1411
- ...(inPlaceEnvelope ? {} : { env: wsm.envFor(envelope) }),
1424
+ // Scoped harness home for isolated envelopes AND for every delegated run;
1425
+ // an ordinary in-place run keeps the native environment so the resumed
1426
+ // vendor session is reachable. See scopedHarnessHome for the cost a
1427
+ // delegated in-place attempt pays for that confinement.
1428
+ ...(harnessHome.env ? { env: harnessHome.env } : {}),
1429
+ // The OS boundary itself. The shared CLI run loop wraps the argv with it;
1430
+ // no adapter opts in, so no adapter can forget.
1431
+ confinement: harnessHome.confinement,
1412
1432
  raw_context_packet: rawContextPacket,
1413
1433
  stream_deltas: streamDeltas,
1414
1434
  });
@@ -1419,6 +1439,9 @@ export class Orchestrator {
1419
1439
  const workEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(interaction));
1420
1440
  const workReportMode = this.applyWorkEnvelope(spec, workEnvelope);
1421
1441
  const inactivityMs = harnessInactivityTimeoutMs(this.config(contract.repo.root));
1442
+ // Named once: the attempt record, the CandidateRun and the terminal gate all
1443
+ // read the SAME applied facts rather than three re-derivations.
1444
+ const applied = appliedAttemptFacts(harnessHome, spec.access, sessionFields?.credential_profile?.profile_id ?? runInput?.credentialProfileId ?? null);
1422
1445
  const attemptStartedMs = Date.now();
1423
1446
  const budgetSignalState = { quotaPressureDisclosed: false };
1424
1447
  const triedProfiles = new Set(); // W5.4 failover: each profile at most once
@@ -1480,13 +1503,16 @@ export class Orchestrator {
1480
1503
  try {
1481
1504
  const watched = withInactivityWatchdog(adapter.run(runSpec), {
1482
1505
  timeoutMs: inactivityMs,
1506
+ countsAsProgress: countsAsAgentProgress,
1483
1507
  onTimeout: () => {
1484
1508
  attemptAbort.abort();
1485
1509
  void adapter.cancel?.(activeSessionId)?.catch(() => { });
1486
1510
  },
1487
- // Waiting on the USER (pending interaction) is legitimate
1488
- // silence the interaction channel enforces its own wait budget.
1511
+ // Waiting on the USER is legitimate silence. The interaction policy
1512
+ // either enforces a finite deadline or waits until answer/cancel/
1513
+ // terminal/restart when the timeout is disabled.
1489
1514
  isSuspended: () => (interaction?.pendingCount?.() ?? 0) > 0,
1515
+ suspensionVersion: () => interaction?.suspensionVersion?.() ?? 0,
1490
1516
  });
1491
1517
  for await (const ev of watched) {
1492
1518
  if (signal?.aborted)
@@ -1616,13 +1642,18 @@ export class Orchestrator {
1616
1642
  // W5.4 failover: a typed-limit hit rebuilds the spec on a NEW vendor
1617
1643
  // session under the next profile with provenance (vendor_limit_rejected).
1618
1644
  if (harnessErrored && runInput && !signal?.aborted) {
1645
+ const rotationPolicy = this.profilePolicy(contract.repo.root, adapter.id);
1646
+ const readyProfileIds = sawTypedLimit && deliverableEmpty && rotationPolicy.limit_action === "rotate"
1647
+ ? await this.readyProfileIdsForRotation(runInput, adapter.id, spec.credential_profile ?? null, triedProfiles)
1648
+ : new Set();
1619
1649
  const rotated = rotateSpecOnTypedLimit({
1620
1650
  spec,
1621
1651
  harnessId: adapter.id,
1622
1652
  attemptId,
1623
- policy: this.profilePolicy(contract.repo.root, adapter.id),
1653
+ policy: rotationPolicy,
1624
1654
  registry: this.config(contract.repo.root)?.global.credential_profiles ?? [],
1625
1655
  snapshots: this.deps.quotaSnapshots?.() ?? [],
1656
+ readyProfileIds,
1626
1657
  triedProfiles,
1627
1658
  sawTypedLimit,
1628
1659
  deliverableEmpty,
@@ -1679,7 +1710,7 @@ export class Orchestrator {
1679
1710
  if (webUnsatisfied(telemetry)) {
1680
1711
  errors.push(webEvidenceFailure(telemetry.web));
1681
1712
  }
1682
- // D-16: un-nest {work_report, output} so answer.md persists the OUTPUT, not the envelope.
1713
+ // D-16: remove the WorkReport transport so answer.md persists only the deliverable.
1683
1714
  const unwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", workReportMode, {
1684
1715
  sideToolReport: telemetry.sideToolWorkReport ?? undefined,
1685
1716
  });
@@ -1782,6 +1813,10 @@ export class Orchestrator {
1782
1813
  ...(secretDiffRefusal ? { secret_diff_refusal: secretDiffRefusal } : {}),
1783
1814
  gates: gates.map((g) => ({ id: g.id, status: g.status })),
1784
1815
  branch: envelope.branch_name,
1816
+ // Applied facts, not promises: the caller of a delegated run
1817
+ // verifies the confinement it asked for instead of trusting it.
1818
+ // Built by the SAME shape the failure path writes.
1819
+ ...applied,
1785
1820
  },
1786
1821
  }), {
1787
1822
  totalUsd: cost,
@@ -1805,6 +1840,7 @@ export class Orchestrator {
1805
1840
  telemetry,
1806
1841
  ...(secretDiffRefusal ? { secretDiffRefusal } : {}),
1807
1842
  outcomeClass: finalized.outcomeClass,
1843
+ applied,
1808
1844
  };
1809
1845
  }
1810
1846
  interactionChannelFor(input, log, runId, taskId, attemptId, harnessId,
@@ -1814,66 +1850,17 @@ export class Orchestrator {
1814
1850
  // Thin delegate — the channel mechanics live in interaction.ts.
1815
1851
  return interactionChannelFor(input, log, runId, taskId, attemptId, harnessId, supportsInteractive, DEFAULT_INTERACTION_TIMEOUT_MS);
1816
1852
  }
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
1853
  /**
1868
1854
  * D-14 layer 3 (AGENTS.md unification, INV-113): the ONE new live-tree write.
1869
1855
  * When the PROJECT root has `AGENTS.md` and no `CLAUDE.md`, drop a thin
1870
1856
  * `CLAUDE.md` (`@AGENTS.md` import + Claudexor ownership marker) so a Claude
1871
1857
  * Code route reads the same instruction file codex/cursor read natively.
1872
1858
  *
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 +
1859
+ * The project-root bridge has its own narrower fence: read-only modes never
1860
+ * reach this run-prep stage and `--in-place` stateful targets are left
1861
+ * untouched. Git admission is independently owned by `runStartRequiresGit`.
1862
+ * The write targets the PROJECT root (`repoRoot`), never a worktree envelope.
1863
+ * The workspace helper adds exclusive-create + no-follow +
1877
1864
  * idempotency, so a hand-written or symlinked `CLAUDE.md` is never overwritten
1878
1865
  * and a concurrent/second prep is a no-op. Announced via a typed
1879
1866
  * `project.claude_bridge.created` event on an actual create only — the git-init
@@ -1898,37 +1885,6 @@ export class Orchestrator {
1898
1885
  });
1899
1886
  }
1900
1887
  }
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
1888
  async runRace(input, mode, announce) {
1933
1889
  const taskId = input.taskId ?? newId("task");
1934
1890
  const runId = input.runId ?? newId("run");
@@ -1936,20 +1892,28 @@ export class Orchestrator {
1936
1892
  // announced: a refused run must fail the request loudly, not 200 a runId
1937
1893
  // and leave an orphaned run dir without a terminal event.
1938
1894
  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.
1895
+ const planBrief = verifiedPlanBrief(input);
1896
+ const quotaSnapshots = this.quotaSnapshotPreflight();
1897
+ const { store, paths, log, ledger } = beginAnnouncedRun({
1898
+ input,
1899
+ contract,
1900
+ quotaSnapshots,
1901
+ store: this.artifactStore(input),
1902
+ authority: this.deps.delegationBudgetAuthority,
1903
+ runId,
1904
+ taskId,
1905
+ mode,
1906
+ phase: "race",
1907
+ prompt: input.prompt,
1908
+ }, announce);
1909
+ input = withPlanBrief(input, store, paths, log, planBrief);
1910
+ // The execution root is the tree the harness mutates: the project itself
1911
+ // for in-place threads/ordinary runs, or the thread's persistent worktree
1912
+ // for an isolated thread. Config/artifacts/contract stay anchored to
1913
+ // repoRoot. Both the WorkspaceManager and the git boundary resolve against
1914
+ // this SINGLE root.
1947
1915
  const execRoot = this.execRootOf(input);
1948
1916
  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
1917
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
1954
1918
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
1955
1919
  // Write modes need a git boundary for worktree isolation and honest diffs.
@@ -1958,17 +1922,17 @@ export class Orchestrator {
1958
1922
  // silent mutation (user-locked decision, comparator: Codex requires git).
1959
1923
  // For an isolated thread the execution root is already a git worktree, so
1960
1924
  // 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);
1925
+ const gitPreconditionError = await ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
1962
1926
  if (gitPreconditionError) {
1963
1927
  return {
1964
1928
  runId,
1965
1929
  taskId,
1966
1930
  mode,
1967
1931
  lifecycle: "failed",
1968
- facts: makeOutcomeFacts("failed", { reason: "harness_failed" }),
1932
+ facts: makeOutcomeFacts("failed", { reason: gitPreconditionError.reason }),
1969
1933
  winner: null,
1970
1934
  runDir: paths.root,
1971
- summary: gitPreconditionError,
1935
+ summary: gitPreconditionError.message,
1972
1936
  candidates: [],
1973
1937
  };
1974
1938
  }
@@ -2066,7 +2030,7 @@ export class Orchestrator {
2066
2030
  attemptId,
2067
2031
  intent: this.candidateIntent(input),
2068
2032
  harnessId: routed.adapter.id,
2069
- cost: attemptCostEvidence(routed.adapter.id, attemptId, i > 0 ? this.estimateUsdFloor(input.repoRoot) : undefined, this.routeBillingKnowledge(input, routed.adapter.id)),
2033
+ cost: attemptCostEvidence(routed.adapter.id, attemptId, this.reservationEstimateUsd(input, i > 0), this.routeBillingKnowledge(input, routed.adapter.id)),
2070
2034
  });
2071
2035
  log.emit("budget.lease.created", {
2072
2036
  granted: lease.granted,
@@ -2146,6 +2110,9 @@ export class Orchestrator {
2146
2110
  const knobs = this.routeSpecKnobs(slot.routed, contract, downgradeModel ?? undefined, input.effort);
2147
2111
  const effectiveWeb = this.discloseWebUpgrade(log, slot.routed, knobs.webPolicy, slot.attemptId);
2148
2112
  let envelope;
2113
+ // Declared OUTSIDE the try so the catch below can state what the attempt
2114
+ // actually ran under, not merely that it stopped.
2115
+ let harnessHome;
2149
2116
  try {
2150
2117
  log.emit("harness.started", {
2151
2118
  harness_id: adapter.id,
@@ -2165,6 +2132,7 @@ export class Orchestrator {
2165
2132
  requestedSingleCandidate &&
2166
2133
  slot.routed.implementationTransport !== "git_patch_envelope",
2167
2134
  });
2135
+ harnessHome = this.harnessHomeFor(wsm, envelope, slot.routed, input);
2168
2136
  const run = await this.runCandidateInEnvelope(slot.routed, envelope, slot.attemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
2169
2137
  const safeEv = redactHarnessEvent(ev);
2170
2138
  safeInvoke(input.onHarnessEvent, safeEv);
@@ -2175,7 +2143,8 @@ export class Orchestrator {
2175
2143
  return false;
2176
2144
  budgetStopped = true;
2177
2145
  return true;
2178
- }, input, requestedSingleCandidate);
2146
+ }, input, requestedSingleCandidate, // W-C4 deltas: single-candidate chat lane only (racing = noise x N)
2147
+ undefined, undefined, harnessHome);
2179
2148
  ledger.settle(slot.leaseId, attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
2180
2149
  log.emit("harness.completed", {
2181
2150
  harness_id: adapter.id,
@@ -2232,6 +2201,13 @@ export class Orchestrator {
2232
2201
  continuation_count: candidateContinuationCount,
2233
2202
  packet_turns: packet.continuity.disclosure.packetTurns,
2234
2203
  });
2204
+ log.emit("harness.started", {
2205
+ harness_id: adapter.id,
2206
+ attempt_id: contAttemptId,
2207
+ external_context_policy: knobs.webPolicy,
2208
+ ...(knobs.ignored.length > 0 ? { ignored_settings: knobs.ignored } : {}),
2209
+ continuation_of: run.attemptId,
2210
+ });
2235
2211
  const contLeaseId = contLease.lease?.lease_id ?? "";
2236
2212
  try {
2237
2213
  const contRun = await this.runCandidateInEnvelope(slot.routed, envelope, contAttemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
@@ -2244,7 +2220,7 @@ export class Orchestrator {
2244
2220
  return false;
2245
2221
  budgetStopped = true;
2246
2222
  return true;
2247
- }, input, requestedSingleCandidate, undefined, packet.pointerLine ?? undefined);
2223
+ }, input, requestedSingleCandidate, undefined, packet.pointerLine ?? undefined, harnessHome);
2248
2224
  ledger.settle(contLeaseId, attemptUsageCostSettlement(contRun.cost, contRun.costEstimated, contRun.attemptId, contRun.harnessId, contRun.telemetry.authMode, contRun.telemetry.usageCost));
2249
2225
  log.emit("harness.completed", {
2250
2226
  harness_id: adapter.id,
@@ -2290,6 +2266,7 @@ export class Orchestrator {
2290
2266
  const failureCost = AC.attemptFailureCost(err, "post-stream-error", 0);
2291
2267
  ledger.settle(slot.leaseId, failureCost.settlement);
2292
2268
  const message = safeErrorMessage(err);
2269
+ const declared = declaredFailure(err);
2293
2270
  const infraPhase = envelope === undefined ? "workspace" : "harness";
2294
2271
  log.emit("harness.completed", {
2295
2272
  harness_id: adapter.id,
@@ -2298,7 +2275,7 @@ export class Orchestrator {
2298
2275
  error: message,
2299
2276
  phase: infraPhase,
2300
2277
  });
2301
- store.writeYaml(join(paths.attemptsDir, slot.attemptId, "attempt.yaml"), AC.attemptFailureRecord(slot.attemptId, adapter.id, failureCost, infraPhase, message));
2278
+ store.writeYaml(join(paths.attemptsDir, slot.attemptId, "attempt.yaml"), AC.attemptFailureRecord(slot.attemptId, adapter.id, failureCost, infraPhase, message, appliedAttemptFacts(harnessHome, slot.routed.adapterAccess, this.effectiveProfileId(input, adapter.id))));
2302
2279
  runsBySlot[slotIdx] = {
2303
2280
  attemptId: slot.attemptId,
2304
2281
  harnessId: adapter.id,
@@ -2315,6 +2292,11 @@ export class Orchestrator {
2315
2292
  slot.routed.delegationRequirement,
2316
2293
  ], knobs.model),
2317
2294
  infraPhase,
2295
+ // Keep a TYPED pre-spawn refusal (spent quota window + its reset)
2296
+ // alive past this catch; `message` alone would force the terminal to
2297
+ // read prose back out.
2298
+ ...(declared.code ? { declaredFailure: declared } : {}),
2299
+ applied: appliedAttemptFacts(harnessHome, slot.routed.adapterAccess, this.effectiveProfileId(input, adapter.id)),
2318
2300
  };
2319
2301
  }
2320
2302
  finally {
@@ -2324,6 +2306,9 @@ export class Orchestrator {
2324
2306
  };
2325
2307
  await runBounded(slots, Math.min(slots.length, MAX_PARALLEL_CANDIDATES), runSlot);
2326
2308
  const runs = runsBySlot.filter((r) => r !== undefined);
2309
+ // Fail-closed terminal: a delegated mutating run whose attempts cannot state
2310
+ // their applied confinement refuses instead of passing.
2311
+ assertDelegatedEvidence(input.delegated === true, candidateAccess, runs);
2327
2312
  const cancelledCandidates = () => runs.map((r) => ({
2328
2313
  attemptId: r.attemptId,
2329
2314
  harnessId: r.harnessId,
@@ -2447,15 +2432,21 @@ export class Orchestrator {
2447
2432
  // harness cause (timeout, rate limit, crash, config) gets remediation that
2448
2433
  // fits it, instead of a doomed "Check harness authentication".
2449
2434
  const harnessCategory = dominantHarnessFailureCategory(first.telemetry.transientFailures);
2435
+ // A run speaks with a candidate's TYPED refusal only when EVERY candidate
2436
+ // died of the same one (candidateEvidence owns that rule); mixed causes
2437
+ // keep the honest harness terminal.
2438
+ const unanimous = unanimousDeclaredFailure(runs);
2450
2439
  writeFailure(store, paths, {
2451
2440
  phase,
2452
- category: phase === "workspace" ? "project" : "harness_error",
2441
+ category: unanimous?.category ?? (phase === "workspace" ? "project" : "harness_error"),
2442
+ code: unanimous?.code ?? null,
2453
2443
  harnessId: first.harnessId,
2454
2444
  attemptId: first.attemptId,
2455
2445
  safeMessage: rootCause,
2456
2446
  rawDetailRef: `attempts/${first.attemptId}/attempt.yaml`,
2457
2447
  eventRefs: existingEventRefs,
2458
2448
  runDir: paths.root,
2449
+ resetsAt: unanimous?.resetsAt ?? null,
2459
2450
  nextActions: first.secretDiffRefusal
2460
2451
  ? secretDiff.secretDiffNextActions(first.secretDiffRefusal)
2461
2452
  : phase === "workspace"
@@ -2519,7 +2510,7 @@ export class Orchestrator {
2519
2510
  // candidates (a trivial greeting in agent mode no longer burns two reviewers on
2520
2511
  // "(empty diff)"). Candidates still flow through arbitration/gates so the
2521
2512
  // 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);
2513
+ evidences = await this.reviewRuns(workingRuns, reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input));
2523
2514
  }
2524
2515
  catch (err) {
2525
2516
  // Review preflight/evidence failures end TERMINALLY with artifacts —
@@ -2544,7 +2535,7 @@ export class Orchestrator {
2544
2535
  attemptId: "synth",
2545
2536
  intent: "synthesize",
2546
2537
  harnessId: synthRouted.adapter.id,
2547
- cost: attemptCostEvidence(synthRouted.adapter.id, "synth", undefined, this.routeBillingKnowledge(input, synthRouted.adapter.id)),
2538
+ cost: attemptCostEvidence(synthRouted.adapter.id, "synth", this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, synthRouted.adapter.id)),
2548
2539
  });
2549
2540
  if (lease.granted) {
2550
2541
  let envelope;
@@ -2567,11 +2558,12 @@ export class Orchestrator {
2567
2558
  dirtyPolicy: "snapshot",
2568
2559
  accessProfile: candidateAccess,
2569
2560
  });
2561
+ const synthHome = this.harnessHomeFor(wsm, envelope, synthRouted, input);
2570
2562
  const run = await this.runCandidateInEnvelope(synthRouted, envelope, "synth", "Synthesis", contract, synthesisInput.prompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
2571
2563
  const safeEv = redactHarnessEvent(ev);
2572
2564
  safeInvoke(input.onHarnessEvent, safeEv);
2573
2565
  log.emit("harness.event", harnessEventPayload(synthAdapter.id, "synth", safeEv));
2574
- }, input.signal, undefined, input.effort, "synthesize", log, effectiveWeb, this.interactionChannelFor(input, log, runId, taskId, "synth", synthAdapter.id, synthRouted.supportsInteractive), undefined, input, false, synthesisInput.content);
2566
+ }, input.signal, undefined, input.effort, "synthesize", log, effectiveWeb, this.interactionChannelFor(input, log, runId, taskId, "synth", synthAdapter.id, synthRouted.supportsInteractive), undefined, input, false, synthesisInput.content, undefined, synthHome);
2575
2567
  ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
2576
2568
  reviewEnvelopes.push(envelope);
2577
2569
  envelope = undefined;
@@ -2580,7 +2572,7 @@ export class Orchestrator {
2580
2572
  runs.push(run);
2581
2573
  try {
2582
2574
  if (isWorkingCandidate(run)) {
2583
- const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal);
2575
+ const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input));
2584
2576
  evidences.push(...synthEvidence);
2585
2577
  workingRuns.push(run);
2586
2578
  }
@@ -2629,8 +2621,12 @@ export class Orchestrator {
2629
2621
  // Winner can only be a candidate that actually produced work; corpses are
2630
2622
  // excluded from arbitration upstream and from the fallback here.
2631
2623
  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)));
2624
+ const winnerEvidence = winnerRun
2625
+ ? evidences.find((e) => e.attemptId === winnerRun.attemptId)
2626
+ : undefined;
2627
+ // D9 winner-only NEEDS_HUMAN gate, fail-closed on a winner with no review
2628
+ // evidence record (see winnerNeedsHuman).
2629
+ const needsHuman = winnerNeedsHuman(winnerRun?.attemptId ?? null, evidences);
2634
2630
  // Run-level review_verified is the WINNER's verification: an
2635
2631
  // empty-diff loser's unverified route must not drag the shipped result's
2636
2632
  // flag false. No winner -> fall back to the all-candidates view.
@@ -2693,12 +2689,11 @@ export class Orchestrator {
2693
2689
  })) {
2694
2690
  log.emit("output.ready", { kind: "artifact", path });
2695
2691
  }
2696
- assertNoSecretLikeTokens("final patch diff", winnerRun.diff);
2692
+ secretDiff.assertNoSecretLikeTokens("final patch diff", winnerRun.diff);
2697
2693
  const patchSha256 = sha256(winnerRun.diff);
2698
2694
  store.writeText(join(paths.finalDir, "patch.diff"), winnerRun.diff);
2699
2695
  const wstats = diffStats(winnerRun.diff);
2700
2696
  const hasDiff = winnerRun.diff.trim().length > 0;
2701
- const winnerEvidence = evidences.find((e) => e.attemptId === winnerRun.attemptId);
2702
2697
  const blockers = winnerEvidence
2703
2698
  ? winnerEvidence.findings.filter((f) => isBlocking(f)).length
2704
2699
  : 0;
@@ -3011,7 +3006,7 @@ export class Orchestrator {
3011
3006
  env: reviewHome.env,
3012
3007
  }).finally(() => reviewHome.dispose());
3013
3008
  }
3014
- async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal) {
3009
+ async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal, reservationEstimateUsd) {
3015
3010
  const evidences = [];
3016
3011
  for (const run of runs) {
3017
3012
  const candidateCwd = run.reviewCwd ?? cwd;
@@ -3030,7 +3025,7 @@ export class Orchestrator {
3030
3025
  attemptId: run.attemptId,
3031
3026
  intent: "review",
3032
3027
  harnessId: "review-panel",
3033
- cost: attemptCostEvidence("review-panel", run.attemptId),
3028
+ cost: attemptCostEvidence("review-panel", run.attemptId, reservationEstimateUsd),
3034
3029
  })
3035
3030
  : undefined;
3036
3031
  const result = hasDiff && reviewers.length > 0 && (reviewLease?.granted ?? true)
@@ -3174,10 +3169,21 @@ export class Orchestrator {
3174
3169
  const runId = input.runId ?? newId("run");
3175
3170
  // Contract validation BEFORE the run is announced (see runRace).
3176
3171
  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);
3172
+ const planBrief = verifiedPlanBrief(input);
3173
+ const quotaSnapshots = this.quotaSnapshotPreflight();
3174
+ const { store, paths, log, ledger } = beginAnnouncedRun({
3175
+ input,
3176
+ contract,
3177
+ quotaSnapshots,
3178
+ store: this.artifactStore(input),
3179
+ authority: this.deps.delegationBudgetAuthority,
3180
+ runId,
3181
+ taskId,
3182
+ mode,
3183
+ phase: "convergence",
3184
+ prompt: input.prompt,
3185
+ }, announce);
3186
+ input = withPlanBrief(input, store, paths, log, planBrief);
3181
3187
  // The execution root is the tree the harness mutates (thread worktree for an
3182
3188
  // isolated thread, else the project). The WorkspaceManager AND the git
3183
3189
  // boundary must resolve against the SAME root — the race path does so via the
@@ -3186,27 +3192,11 @@ export class Orchestrator {
3186
3192
  const execRoot = this.execRootOf(input);
3187
3193
  const wsm = new WorkspaceManager(execRoot);
3188
3194
  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
- }
3195
+ store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
3206
3196
  // Live (in-place) isolation deliberately tolerates non-git stateful
3207
3197
  // environments; only envelope isolation needs the git boundary.
3208
3198
  if (!input.inPlace) {
3209
- const gitPreconditionError = await this.ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
3199
+ const gitPreconditionError = await ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
3210
3200
  if (gitPreconditionError) {
3211
3201
  return {
3212
3202
  spendUsd: ledger.spend(),
@@ -3214,10 +3204,10 @@ export class Orchestrator {
3214
3204
  taskId,
3215
3205
  mode,
3216
3206
  lifecycle: "failed",
3217
- facts: makeOutcomeFacts("failed", { reason: "harness_failed" }),
3207
+ facts: makeOutcomeFacts("failed", { reason: gitPreconditionError.reason }),
3218
3208
  winner: null,
3219
3209
  runDir: paths.root,
3220
- summary: gitPreconditionError,
3210
+ summary: gitPreconditionError.message,
3221
3211
  candidates: [],
3222
3212
  };
3223
3213
  }
@@ -3406,7 +3396,7 @@ export class Orchestrator {
3406
3396
  attemptId,
3407
3397
  intent: "repair",
3408
3398
  harnessId: adapter.id,
3409
- cost: attemptCostEvidence(adapter.id, attemptId, undefined, this.routeBillingKnowledge(input, adapter.id)),
3399
+ cost: attemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, adapter.id)),
3410
3400
  });
3411
3401
  if (!lease.granted) {
3412
3402
  exhausted = true;
@@ -3415,7 +3405,9 @@ export class Orchestrator {
3415
3405
  const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
3416
3406
  const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
3417
3407
  let run;
3408
+ let harnessHome;
3418
3409
  try {
3410
+ harnessHome = this.harnessHomeFor(wsm, envelope, routed, input);
3419
3411
  log.emit("harness.started", {
3420
3412
  harness_id: adapter.id,
3421
3413
  attempt_id: attemptId,
@@ -3429,7 +3421,8 @@ export class Orchestrator {
3429
3421
  }, input.signal, undefined, input.effort, "repair", log, effectiveWeb, this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive), (streamedUsd) => {
3430
3422
  ledger.updateHold(lease.lease?.lease_id ?? "", streamedUsd);
3431
3423
  return ledger.tier() === "hard";
3432
- }, input, true);
3424
+ }, input, true, // convergence runs one candidate: live deltas on (W-C4)
3425
+ undefined, undefined, harnessHome);
3433
3426
  ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
3434
3427
  log.emit("harness.completed", {
3435
3428
  harness_id: adapter.id,
@@ -3452,8 +3445,9 @@ export class Orchestrator {
3452
3445
  status: "failed",
3453
3446
  error: message,
3454
3447
  });
3455
- store.writeYaml(join(paths.attemptsDir, attemptId, "attempt.yaml"), AC.attemptFailureRecord(attemptId, adapter.id, failureCost, "harness", message));
3448
+ store.writeYaml(join(paths.attemptsDir, attemptId, "attempt.yaml"), AC.attemptFailureRecord(attemptId, adapter.id, failureCost, "harness", message, appliedAttemptFacts(harnessHome, routed.adapterAccess, this.effectiveProfileId(input, adapter.id))));
3456
3449
  run = {
3450
+ applied: appliedAttemptFacts(harnessHome, routed.adapterAccess, this.effectiveProfileId(input, adapter.id)),
3457
3451
  attemptId,
3458
3452
  harnessId: adapter.id,
3459
3453
  label: `Attempt ${attempt}`,
@@ -3467,6 +3461,9 @@ export class Orchestrator {
3467
3461
  };
3468
3462
  }
3469
3463
  lastRun = run;
3464
+ // Fail-closed twin of the candidate lane's gate: this loop terminalizes
3465
+ // per attempt, so the proof is spent per attempt.
3466
+ assertDelegatedEvidence(input.delegated === true, convergenceAccess, [run]);
3470
3467
  attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry: run.telemetry });
3471
3468
  // Cancellation/deadline keeps priority over a belt failure finalized concurrently.
3472
3469
  if (input.signal?.aborted)
@@ -3518,7 +3515,7 @@ export class Orchestrator {
3518
3515
  attemptId,
3519
3516
  intent: "review",
3520
3517
  harnessId: "review-panel",
3521
- cost: attemptCostEvidence("review-panel", attemptId),
3518
+ cost: attemptCostEvidence("review-panel", attemptId, this.reservationEstimateUsd(input)),
3522
3519
  })
3523
3520
  : null;
3524
3521
  const reviewResult = reviewers.length > 0 && (reviewLease?.granted ?? false)
@@ -3808,7 +3805,7 @@ export class Orchestrator {
3808
3805
  // work_product (its partial patch.diff stays diagnostic via attempts/);
3809
3806
  // in-place keeps the product so the honest Revert offer survives.
3810
3807
  if (lastRun && (!interrupted || input.inPlace === true)) {
3811
- assertNoSecretLikeTokens("final patch diff", lastRun.diff);
3808
+ secretDiff.assertNoSecretLikeTokens("final patch diff", lastRun.diff);
3812
3809
  const patchSha256 = sha256(lastRun.diff);
3813
3810
  store.writeText(join(paths.finalDir, "patch.diff"), lastRun.diff);
3814
3811
  // Honest apply-state (parity with runRace single-candidate in-place): a
@@ -3978,265 +3975,77 @@ export class Orchestrator {
3978
3975
  reviewVerified: actualReviewVerified,
3979
3976
  };
3980
3977
  }
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
- });
3978
+ /** Bind private route/session preparation to the planner-attempt owner. */
3979
+ plannerAttemptDeps() {
4216
3980
  return {
4217
- attemptId,
4218
- harnessId: adapter.id,
4219
- status: "success",
4220
- outcomeClass: planFinalized.outcomeClass,
4221
- error: null,
4222
- text,
4223
- telemetry,
4224
- budgetDenied: false,
3981
+ billingKnowledge: (input, harnessId) => this.routeBillingKnowledge(input, harnessId),
3982
+ inactivityTimeoutMs: (repoRoot) => harnessInactivityTimeoutMs(this.config(repoRoot)),
3983
+ quotaEventSink: this.deps.quotaEventSink,
3984
+ prepare: async (args) => {
3985
+ const { input, contract, taskId, runId, log, store, paths, routed, attemptId } = args;
3986
+ const adapter = routed.adapter;
3987
+ const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
3988
+ const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
3989
+ const sessionFields = await this.sessionSpecFields(input, adapter.id, log, routed.authRouteEstimate);
3990
+ const laneContinuity = args.laneRun
3991
+ ? 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)
3992
+ : null;
3993
+ const spec = HarnessRunSpec.parse({
3994
+ session_id: newId("ses"),
3995
+ intent: args.intent,
3996
+ prompt: laneContinuity?.pointerLine
3997
+ ? `${args.promptBody}\n\n${laneContinuity.pointerLine}`
3998
+ : args.promptBody,
3999
+ cwd: this.execRootOf(input),
4000
+ access: "readonly",
4001
+ attachments: input.attachments ?? [],
4002
+ ...sessionFields,
4003
+ ...this.harnessSpecKnobs(contract, knobs, args.intent),
4004
+ env_inheritance: envInheritance(this.config(input.repoRoot)),
4005
+ env: (args.laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? args.fallbackHome,
4006
+ });
4007
+ const plannerAbort = new AbortController();
4008
+ spec.extra["abortSignal"] = input.signal
4009
+ ? AbortSignal.any([input.signal, plannerAbort.signal])
4010
+ : plannerAbort.signal;
4011
+ const planInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
4012
+ if (planInteraction)
4013
+ spec.extra["interactionChannel"] = planInteraction;
4014
+ const planWorkMode = this.applyWorkEnvelope(spec, this.workReportEnvelopeFor(routed, contract, Boolean(planInteraction)));
4015
+ return {
4016
+ knobs,
4017
+ effectiveWeb,
4018
+ spec,
4019
+ plannerAbort,
4020
+ planInteraction,
4021
+ planWorkMode,
4022
+ };
4023
+ },
4225
4024
  };
4226
4025
  }
4026
+ /** One read-only planner spawn shared by solo fallback, Council drafts, and merge. */
4027
+ async runPlannerAttempt(args) {
4028
+ return executePlannerAttempt(this.plannerAttemptDeps(), args);
4029
+ }
4227
4030
  async runPlan(input, announce) {
4228
4031
  const taskId = input.taskId ?? newId("task");
4229
4032
  const runId = input.runId ?? newId("run");
4230
4033
  // Plan runs get the same immutable contract truth as every other mode;
4231
4034
  // contract validation runs BEFORE the run is announced (see runRace).
4232
4035
  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));
4036
+ const quotaSnapshots = this.quotaSnapshotPreflight();
4037
+ const { store, paths, log, ledger } = beginAnnouncedRun({
4038
+ input,
4039
+ contract,
4040
+ quotaSnapshots,
4041
+ store: this.artifactStore(input),
4042
+ authority: this.deps.delegationBudgetAuthority,
4043
+ runId,
4044
+ taskId,
4045
+ mode: "plan",
4046
+ phase: "plan",
4047
+ prompt: input.prompt,
4048
+ }, announce);
4240
4049
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
4241
4050
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
4242
4051
  // W3.3: ONE resolved read-only context — the routing point-probe and every
@@ -4344,6 +4153,7 @@ export class Orchestrator {
4344
4153
  roHome,
4345
4154
  contextSection,
4346
4155
  laneRun,
4156
+ estimateUsdFloor: this.estimateUsdFloor(input.repoRoot),
4347
4157
  });
4348
4158
  }
4349
4159
  const plans = [];
@@ -4373,6 +4183,7 @@ export class Orchestrator {
4373
4183
  fallbackHome: roHome.env,
4374
4184
  promptBody: planPrompt(input.prompt) + contextSection,
4375
4185
  intent: "plan",
4186
+ reservationEstimateUsd: this.reservationEstimateUsd(input),
4376
4187
  });
4377
4188
  if (outcome.budgetDenied) {
4378
4189
  // QA-050: retain the denied planner slot before breaking so the
@@ -4451,7 +4262,6 @@ export class Orchestrator {
4451
4262
  }
4452
4263
  if (plans.length === 0) {
4453
4264
  return writePlanHarnessFailure(this.planRunDeps(), {
4454
- input,
4455
4265
  contract,
4456
4266
  taskId,
4457
4267
  runId,
@@ -4520,17 +4330,8 @@ export class Orchestrator {
4520
4330
  defaultPrompt: "Explore this repository and synthesize evidence-cited findings, omissions, and follow-up questions.",
4521
4331
  }, announce);
4522
4332
  }
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
- });
4333
+ quotaSnapshotPreflight() {
4334
+ return [...(this.deps.quotaSnapshots?.() ?? [])].map((snapshot) => QuotaSnapshotSchema.parse(snapshot));
4534
4335
  }
4535
4336
  routeBillingKnowledge(input, harnessId) {
4536
4337
  // A selected profile's credential_kind decides billing (round-18 #2).
@@ -4560,10 +4361,10 @@ export class Orchestrator {
4560
4361
  // scout reserve) so a subscription route is not refused for lacking a
4561
4362
  // cash quote.
4562
4363
  attemptCostEvidence(harnessId, attemptId, this.estimateUsdFloor(input.repoRoot), this.routeBillingKnowledge(input, harnessId)),
4563
- buildSpec: (routed, homeEnv, prompt, attemptId) => {
4364
+ buildSpec: async (routed, homeEnv, prompt, attemptId) => {
4564
4365
  const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
4565
4366
  const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
4566
- const sessionFields = this.sessionSpecFields(input, routed.adapter.id, log);
4367
+ const sessionFields = await this.sessionSpecFields(input, routed.adapter.id, log, routed.authRouteEstimate);
4567
4368
  const spec = HarnessRunSpec.parse({
4568
4369
  session_id: newId("ses"),
4569
4370
  intent: "synthesize",
@@ -4604,13 +4405,19 @@ export class Orchestrator {
4604
4405
  // Contract validation BEFORE the run is announced (see runRace). The
4605
4406
  // recorded user intent is the CALLER's goal.
4606
4407
  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));
4408
+ const quotaSnapshots = this.quotaSnapshotPreflight();
4409
+ const { store, paths, log, ledger } = beginAnnouncedRun({
4410
+ input,
4411
+ contract,
4412
+ quotaSnapshots,
4413
+ store: this.artifactStore(input),
4414
+ authority: this.deps.delegationBudgetAuthority,
4415
+ runId,
4416
+ taskId,
4417
+ mode: opts.mode,
4418
+ phase: "report",
4419
+ prompt,
4420
+ }, announce);
4614
4421
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
4615
4422
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
4616
4423
  // The ask/deep-scan report stays bare (its scouts read the tree themselves);
@@ -4752,10 +4559,10 @@ export class Orchestrator {
4752
4559
  intent: opts.intent,
4753
4560
  harnessId: adapter.id,
4754
4561
  // 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)),
4562
+ // (mirror of the candidate loop): the first top-level scout reserves
4563
+ // without a floor; later scouts and every real Delegate child pass the
4564
+ // repo floor because they overlap an existing family unit.
4565
+ cost: attemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input, opts.deepScan && idx > 0), this.routeBillingKnowledge(input, adapter.id)),
4759
4566
  });
4760
4567
  if (!lease.granted) {
4761
4568
  log.emit("budget.lease.created", {
@@ -4784,67 +4591,120 @@ export class Orchestrator {
4784
4591
  }
4785
4592
  return { status: "budget_denied", reason: lease.reason ?? "budget lease denied" };
4786
4593
  }
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);
4594
+ // As with planners, the granted lease owns profile/continuity/spec
4595
+ // preparation. Contain a pre-stream rejection as this attempt's failure;
4596
+ // parallel siblings can then finish before the shared HOME is disposed.
4597
+ const preparation = await (async () => {
4598
+ // Lease granted: the attempt is now committed to run — disclose the launch.
4599
+ onLaunch?.();
4600
+ const knobs = this.routeSpecKnobs(routed, contract, modelOverride, input.effort);
4601
+ const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
4602
+ const explorerPrompt = (opts.deepScan
4603
+ ? `${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.`
4604
+ : prompt) + contextSection;
4605
+ const sessionFields = await this.sessionSpecFields(input, adapter.id, log, routed.authRouteEstimate);
4606
+ const grantResume = sessionFields.resume_session_id !== null && !resumeGranted.has(adapter.id);
4607
+ if (grantResume)
4608
+ resumeGranted.add(adapter.id);
4609
+ // Continuity (INV-137): a thread ASK turn is a chat turn — hydrate a lane
4610
+ // switch/gap with a packet and disclose it. Gated on laneRun (deep-scan
4611
+ // scouts are excluded from laneRun); native resume is available only when
4612
+ // this slot was granted the lane's recorded session.
4613
+ const laneContinuity = laneRun
4614
+ ? await this.resolveContinuity(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null, grantResume, sessionFields, store, paths, this.execRootOf(input), log)
4615
+ : null;
4616
+ // D-16d: the continuation packet pointer rides after the lane pointer so
4617
+ // the fresh session is re-grounded in the exhausted attempt's work.
4618
+ const promptWithPointers = [
4619
+ explorerPrompt,
4620
+ laneContinuity?.pointerLine,
4621
+ continuationPointer,
4622
+ ]
4623
+ .filter((p) => Boolean(p))
4624
+ .join("\n\n");
4625
+ const spec = HarnessRunSpec.parse({
4626
+ session_id: newId("ses"),
4627
+ intent: opts.intent,
4628
+ prompt: promptWithPointers,
4629
+ cwd: this.execRootOf(input),
4630
+ access: "readonly",
4631
+ // ASK/EXPLORE/AUDIT read-only runs must forward the user's attachments —
4632
+ // a live "describe this image" turn sent an image that was being dropped here, so
4633
+ // the model honestly reported it saw nothing (the v0.13 attachment bug).
4634
+ attachments: input.attachments ?? [],
4635
+ auth_preference: sessionFields.auth_preference,
4636
+ credential_profile: sessionFields.credential_profile,
4637
+ resume_session_id: grantResume ? sessionFields.resume_session_id : null,
4638
+ ...this.harnessSpecKnobs(contract, knobs, opts.intent),
4639
+ env_inheritance: envInheritance(this.config(input.repoRoot)),
4640
+ // A thread lane turn spawns in its DURABLE per-lane home so the native
4641
+ // session it records is reachable for resume next turn; everything else
4642
+ // uses the disposable route-context home.
4643
+ env: (laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? roHome.env,
4644
+ });
4645
+ const reportAbort = new AbortController();
4646
+ spec.extra["abortSignal"] = input.signal
4647
+ ? AbortSignal.any([input.signal, reportAbort.signal])
4648
+ : reportAbort.signal;
4649
+ const reportInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
4650
+ if (reportInteraction)
4651
+ spec.extra["interactionChannel"] = reportInteraction;
4652
+ // D-16: compile the WorkReport envelope for the read-only lane.
4653
+ const readonlyWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(reportInteraction));
4654
+ const readonlyWorkMode = this.applyWorkEnvelope(spec, readonlyWorkEnvelope);
4655
+ const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
4656
+ const answer = new AnswerAssembly();
4657
+ const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
4658
+ knobs.webPolicy === "cached" ||
4659
+ knobs.webPolicy === "live", effectiveWeb, [],
4660
+ // Requested-model capture so ask/audit route receipts detect a silent
4661
+ // model downgrade (typed model_mismatch), not just agent runs.
4662
+ knobs.model);
4663
+ return {
4664
+ knobs,
4665
+ spec,
4666
+ reportAbort,
4667
+ reportInteraction,
4668
+ readonlyWorkMode,
4669
+ attemptEventsPath,
4670
+ answer,
4671
+ telemetry,
4672
+ };
4673
+ })().then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
4674
+ if (!preparation.ok) {
4675
+ const message = `read-only attempt setup failed: ${safeErrorMessage(preparation.error)}`;
4676
+ AC.settleGrantedAttemptLease({
4677
+ ledger,
4678
+ leaseId: lease.lease?.lease_id ?? "",
4679
+ attemptId,
4680
+ harnessId: adapter.id,
4681
+ costUsd: 0,
4682
+ costEstimated: false,
4683
+ preStreamFailureSource: "readonly-pre-stream",
4684
+ });
4685
+ const telemetry = createAttemptTelemetry(contract.external_context.policy, contract.external_context.web_required, contract.external_context.effective_mode);
4686
+ setAttemptOutcome(telemetry, {
4687
+ deliverablePresent: false,
4688
+ gatesPassed: null,
4689
+ harnessErrored: true,
4690
+ webRequiredUnsatisfied: false,
4691
+ });
4692
+ attempts.push({
4693
+ attemptId,
4694
+ harnessId: adapter.id,
4695
+ status: "failed",
4696
+ report: "",
4697
+ error: message,
4698
+ telemetry,
4699
+ });
4700
+ attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
4701
+ if (opts.deepScan) {
4702
+ store.writeText(join(paths.findingsDir, `${attemptId}-error.md`), `# Explorer ${attemptId} failed\n\n${message}\n`);
4703
+ }
4704
+ return { status: "launched" };
4705
+ }
4706
+ const { knobs, spec: preparedSpec, reportAbort, reportInteraction, readonlyWorkMode, attemptEventsPath, answer, telemetry, } = preparation.value;
4707
+ let spec = preparedSpec;
4848
4708
  const retryPolicy = transientRetryPolicy(this.config(input.repoRoot));
4849
4709
  let activeSessionId = spec.session_id;
4850
4710
  const onAbort = () => {
@@ -4884,11 +4744,13 @@ export class Orchestrator {
4884
4744
  try {
4885
4745
  const watchedReport = withInactivityWatchdog(adapter.run(runSpec), {
4886
4746
  timeoutMs: harnessInactivityTimeoutMs(this.config(input.repoRoot)),
4747
+ countsAsProgress: countsAsAgentProgress,
4887
4748
  onTimeout: () => {
4888
4749
  reportAbort.abort();
4889
4750
  void adapter.cancel?.(activeSessionId)?.catch(() => { });
4890
4751
  },
4891
4752
  isSuspended: () => (reportInteraction?.pendingCount?.() ?? 0) > 0,
4753
+ suspensionVersion: () => reportInteraction?.suspensionVersion?.() ?? 0,
4892
4754
  });
4893
4755
  for await (const ev of watchedReport) {
4894
4756
  if (input.signal?.aborted)
@@ -4952,13 +4814,18 @@ export class Orchestrator {
4952
4814
  // W5.4 reactive failover, READ-ONLY lane (same contract as the
4953
4815
  // candidate lane; typed limits only, never plain transients).
4954
4816
  if (harnessError && !input.signal?.aborted) {
4817
+ const rotationPolicy = this.profilePolicy(input.repoRoot, adapter.id);
4818
+ const readyProfileIds = sawTypedLimit && reportSoFar.length === 0 && rotationPolicy.limit_action === "rotate"
4819
+ ? await this.readyProfileIdsForRotation(input, adapter.id, spec.credential_profile ?? null, triedProfiles)
4820
+ : new Set();
4955
4821
  const rotated = rotateSpecOnTypedLimit({
4956
4822
  spec,
4957
4823
  harnessId: adapter.id,
4958
4824
  attemptId,
4959
- policy: this.profilePolicy(input.repoRoot, adapter.id),
4825
+ policy: rotationPolicy,
4960
4826
  registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
4961
4827
  snapshots: this.deps.quotaSnapshots?.() ?? [],
4828
+ readyProfileIds,
4962
4829
  triedProfiles,
4963
4830
  sawTypedLimit,
4964
4831
  deliverableEmpty: reportSoFar.length === 0,
@@ -5000,7 +4867,17 @@ export class Orchestrator {
5000
4867
  }
5001
4868
  finally {
5002
4869
  input.signal?.removeEventListener("abort", onAbort);
5003
- ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost));
4870
+ AC.settleGrantedAttemptLease({
4871
+ ledger,
4872
+ leaseId: lease.lease?.lease_id ?? "",
4873
+ attemptId,
4874
+ harnessId: adapter.id,
4875
+ costUsd: cost,
4876
+ costEstimated,
4877
+ authMode: telemetry.authMode,
4878
+ usageCost: telemetry.usageCost,
4879
+ preStreamFailureSource: "readonly-pre-stream",
4880
+ });
5004
4881
  }
5005
4882
  if (harnessError && telemetry.transientFailures.length > 0) {
5006
4883
  log.emit("route.transient.exhausted", {
@@ -5011,11 +4888,14 @@ export class Orchestrator {
5011
4888
  });
5012
4889
  }
5013
4890
  attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
5014
- // D-16: un-nest the {work_report, output} envelope; the OUTPUT is the report.
4891
+ // D-16: remove the WorkReport transport; the deliverable is the report.
5015
4892
  const roUnwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", readonlyWorkMode, {
5016
4893
  sideToolReport: telemetry.sideToolWorkReport ?? undefined,
5017
4894
  });
5018
- const report = redactSecrets(roUnwrapped.deliverable);
4895
+ // Trim symmetrically with the plan path: a whitespace-only answer is not
4896
+ // a delivered report (the final-artifact wrapper heading would otherwise
4897
+ // make it read as present content by construction).
4898
+ const report = redactSecrets(roUnwrapped.deliverable).trim();
5019
4899
  const unrecovered = unrecoveredToolErrors(telemetry);
5020
4900
  const webBlocked = webUnsatisfied(telemetry);
5021
4901
  const reportPresent = report.length > 0;
@@ -5043,6 +4923,13 @@ export class Orchestrator {
5043
4923
  harnessErrored: harnessError !== null && !webBlocked,
5044
4924
  webRequiredUnsatisfied: webBlocked,
5045
4925
  workState: roFinalized.workState,
4926
+ // A read-only attempt that completed CLEANLY with an honestly empty
4927
+ // answer is a success with deliverable_present=false ("(no output)"),
4928
+ // never a fake contract failure — the trim above must not convert a
4929
+ // phantom deliverable into a phantom harness failure. Only the clean
4930
+ // finalizer class qualifies: contract failures, vetoes, and context
4931
+ // interruptions keep the strict deliverable requirement.
4932
+ emptyDeliverableAllowed: roFinalized.outcomeClass === "clean",
5046
4933
  });
5047
4934
  if (harnessError) {
5048
4935
  log.emit("harness.completed", {
@@ -5473,7 +5360,7 @@ export class Orchestrator {
5473
5360
  }
5474
5361
  this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, opts.deepScan ? null : (succeeded[0]?.attemptId ?? null), deepScanSynthesis);
5475
5362
  log.emit("output.ready", {
5476
- kind: opts.mode === "ask" ? "answer" : "report",
5363
+ kind: opts.deepScan ? "report" : "answer",
5477
5364
  path: `final/${opts.artifactName}`,
5478
5365
  });
5479
5366
  if (opts.deepScan) {
@@ -5531,11 +5418,16 @@ export class Orchestrator {
5531
5418
  .map((a) => `${a.attemptId}:${a.harnessId}:${a.status}`)
5532
5419
  .join(", ");
5533
5420
  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`);
5421
+ const reportProducerAttemptId = opts.deepScan &&
5422
+ deepScanSynthesis?.status === "succeeded" &&
5423
+ deepScanSynthesis.reducer_attempt_id
5424
+ ? deepScanSynthesis.reducer_attempt_id
5425
+ : (succeeded[0]?.attemptId ?? "a01");
5534
5426
  store.writeYaml(join(paths.finalDir, "work_product.yaml"), {
5535
5427
  id: newId("wp"),
5536
5428
  kind: "report",
5537
5429
  source_task_id: taskId,
5538
- producer_attempt_id: succeeded[0]?.attemptId ?? "a01",
5430
+ producer_attempt_id: reportProducerAttemptId,
5539
5431
  files: Object.fromEntries([[opts.artifactName, join(paths.finalDir, opts.artifactName)]]),
5540
5432
  meta: {
5541
5433
  harnesses: attempts.map((a) => a.harnessId),
@@ -5544,7 +5436,7 @@ export class Orchestrator {
5544
5436
  read_only: true,
5545
5437
  },
5546
5438
  });
5547
- log.emit("work_product.emitted", { kind: "report", winner: succeeded[0]?.attemptId ?? null });
5439
+ log.emit("work_product.emitted", { kind: "report", winner: reportProducerAttemptId });
5548
5440
  const workVetoed = terminalFacts.work_state?.state === "needs_input" ||
5549
5441
  terminalFacts.work_state?.state === "incomplete";
5550
5442
  if (terminalFacts.lifecycle !== "succeeded") {
@@ -5596,9 +5488,4 @@ export class Orchestrator {
5596
5488
  };
5597
5489
  }
5598
5490
  }
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
5491
  //# sourceMappingURL=orchestrator.js.map