@claudexor/orchestrator 3.1.1 → 3.2.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 (173) 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 +16 -20
  6. package/dist/attemptTelemetry.d.ts.map +1 -1
  7. package/dist/attemptTelemetry.js +38 -70
  8. package/dist/attemptTelemetry.js.map +1 -1
  9. package/dist/attemptUsageCost.d.ts +47 -0
  10. package/dist/attemptUsageCost.d.ts.map +1 -0
  11. package/dist/attemptUsageCost.js +191 -0
  12. package/dist/attemptUsageCost.js.map +1 -0
  13. package/dist/budgetFailure.d.ts +3 -1
  14. package/dist/budgetFailure.d.ts.map +1 -1
  15. package/dist/budgetFailure.js.map +1 -1
  16. package/dist/candidateEvidence.d.ts +5 -0
  17. package/dist/candidateEvidence.d.ts.map +1 -1
  18. package/dist/candidateEvidence.js +3 -1
  19. package/dist/candidateEvidence.js.map +1 -1
  20. package/dist/candidateOutputs.d.ts +19 -0
  21. package/dist/candidateOutputs.d.ts.map +1 -1
  22. package/dist/candidateOutputs.js +201 -67
  23. package/dist/candidateOutputs.js.map +1 -1
  24. package/dist/credential-profile-rotation.d.ts +145 -0
  25. package/dist/credential-profile-rotation.d.ts.map +1 -0
  26. package/dist/credential-profile-rotation.js +315 -0
  27. package/dist/credential-profile-rotation.js.map +1 -0
  28. package/dist/credential-profiles.d.ts +35 -134
  29. package/dist/credential-profiles.d.ts.map +1 -1
  30. package/dist/credential-profiles.js +73 -295
  31. package/dist/credential-profiles.js.map +1 -1
  32. package/dist/decisionBudget.d.ts +43 -0
  33. package/dist/decisionBudget.d.ts.map +1 -0
  34. package/dist/decisionBudget.js +84 -0
  35. package/dist/decisionBudget.js.map +1 -0
  36. package/dist/decisionTerminalReconciliation.d.ts +18 -0
  37. package/dist/decisionTerminalReconciliation.d.ts.map +1 -0
  38. package/dist/decisionTerminalReconciliation.js +26 -0
  39. package/dist/decisionTerminalReconciliation.js.map +1 -0
  40. package/dist/deepScanReducer.d.ts +14 -44
  41. package/dist/deepScanReducer.d.ts.map +1 -1
  42. package/dist/deepScanReducer.js +240 -128
  43. package/dist/deepScanReducer.js.map +1 -1
  44. package/dist/delegation-parent-activation.d.ts +6 -0
  45. package/dist/delegation-parent-activation.d.ts.map +1 -0
  46. package/dist/delegation-parent-activation.js +13 -0
  47. package/dist/delegation-parent-activation.js.map +1 -0
  48. package/dist/delegationBudgetAuthority.d.ts +30 -0
  49. package/dist/delegationBudgetAuthority.d.ts.map +1 -0
  50. package/dist/delegationBudgetAuthority.js +157 -0
  51. package/dist/delegationBudgetAuthority.js.map +1 -0
  52. package/dist/delegationFailure.d.ts +58 -0
  53. package/dist/delegationFailure.d.ts.map +1 -0
  54. package/dist/delegationFailure.js +184 -0
  55. package/dist/delegationFailure.js.map +1 -0
  56. package/dist/delegationToolEvidence.d.ts +20 -0
  57. package/dist/delegationToolEvidence.d.ts.map +1 -0
  58. package/dist/delegationToolEvidence.js +46 -0
  59. package/dist/delegationToolEvidence.js.map +1 -0
  60. package/dist/git-precondition.d.ts +11 -0
  61. package/dist/git-precondition.d.ts.map +1 -0
  62. package/dist/git-precondition.js +69 -0
  63. package/dist/git-precondition.js.map +1 -0
  64. package/dist/index.d.ts +3 -1
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +3 -1
  67. package/dist/index.js.map +1 -1
  68. package/dist/interaction.d.ts +14 -6
  69. package/dist/interaction.d.ts.map +1 -1
  70. package/dist/interaction.js +106 -19
  71. package/dist/interaction.js.map +1 -1
  72. package/dist/orchestrator.d.ts +62 -115
  73. package/dist/orchestrator.d.ts.map +1 -1
  74. package/dist/orchestrator.js +671 -965
  75. package/dist/orchestrator.js.map +1 -1
  76. package/dist/plan-prompt.d.ts +3 -0
  77. package/dist/plan-prompt.d.ts.map +1 -0
  78. package/dist/plan-prompt.js +28 -0
  79. package/dist/plan-prompt.js.map +1 -0
  80. package/dist/planBrief.d.ts +27 -0
  81. package/dist/planBrief.d.ts.map +1 -0
  82. package/dist/planBrief.js +42 -0
  83. package/dist/planBrief.js.map +1 -0
  84. package/dist/planRun.d.ts +4 -3
  85. package/dist/planRun.d.ts.map +1 -1
  86. package/dist/planRun.js +33 -33
  87. package/dist/planRun.js.map +1 -1
  88. package/dist/plannerAttempt.d.ts +64 -0
  89. package/dist/plannerAttempt.d.ts.map +1 -0
  90. package/dist/plannerAttempt.js +235 -0
  91. package/dist/plannerAttempt.js.map +1 -0
  92. package/dist/policyFindings.js.map +1 -1
  93. package/dist/requestRequirements.d.ts +45 -1
  94. package/dist/requestRequirements.d.ts.map +1 -1
  95. package/dist/requestRequirements.js +158 -11
  96. package/dist/requestRequirements.js.map +1 -1
  97. package/dist/reviewerPanel.js.map +1 -1
  98. package/dist/root-ledger.d.ts +13 -0
  99. package/dist/root-ledger.d.ts.map +1 -0
  100. package/dist/root-ledger.js +21 -0
  101. package/dist/root-ledger.js.map +1 -0
  102. package/dist/routing-failure.d.ts +6 -0
  103. package/dist/routing-failure.d.ts.map +1 -0
  104. package/dist/routing-failure.js +12 -0
  105. package/dist/routing-failure.js.map +1 -0
  106. package/dist/run-bounded.d.ts +3 -0
  107. package/dist/run-bounded.d.ts.map +1 -0
  108. package/dist/run-bounded.js +23 -0
  109. package/dist/run-bounded.js.map +1 -0
  110. package/dist/run-input-resolution.d.ts +10 -0
  111. package/dist/run-input-resolution.d.ts.map +1 -0
  112. package/dist/run-input-resolution.js +88 -0
  113. package/dist/run-input-resolution.js.map +1 -0
  114. package/dist/runEventLog.d.ts +48 -0
  115. package/dist/runEventLog.d.ts.map +1 -0
  116. package/dist/runEventLog.js +63 -0
  117. package/dist/runEventLog.js.map +1 -0
  118. package/dist/runFacts.d.ts +23 -0
  119. package/dist/runFacts.d.ts.map +1 -0
  120. package/dist/runFacts.js +179 -0
  121. package/dist/runFacts.js.map +1 -0
  122. package/dist/runFactsBuilder.d.ts +5 -0
  123. package/dist/runFactsBuilder.d.ts.map +1 -0
  124. package/dist/runFactsBuilder.js +412 -0
  125. package/dist/runFactsBuilder.js.map +1 -0
  126. package/dist/runFactsDeliverable.d.ts +13 -0
  127. package/dist/runFactsDeliverable.d.ts.map +1 -0
  128. package/dist/runFactsDeliverable.js +94 -0
  129. package/dist/runFactsDeliverable.js.map +1 -0
  130. package/dist/runFactsPresentation.d.ts +7 -0
  131. package/dist/runFactsPresentation.d.ts.map +1 -0
  132. package/dist/runFactsPresentation.js +113 -0
  133. package/dist/runFactsPresentation.js.map +1 -0
  134. package/dist/runFactsReview.d.ts +7 -0
  135. package/dist/runFactsReview.d.ts.map +1 -0
  136. package/dist/runFactsReview.js +63 -0
  137. package/dist/runFactsReview.js.map +1 -0
  138. package/dist/runSupport.d.ts +17 -15
  139. package/dist/runSupport.d.ts.map +1 -1
  140. package/dist/runSupport.js +16 -0
  141. package/dist/runSupport.js.map +1 -1
  142. package/dist/runTelemetryWriter.d.ts +5 -2
  143. package/dist/runTelemetryWriter.d.ts.map +1 -1
  144. package/dist/runTelemetryWriter.js +72 -1
  145. package/dist/runTelemetryWriter.js.map +1 -1
  146. package/dist/runTerminalContext.d.ts +31 -0
  147. package/dist/runTerminalContext.d.ts.map +1 -0
  148. package/dist/runTerminalContext.js +12 -0
  149. package/dist/runTerminalContext.js.map +1 -0
  150. package/dist/runTerminalResults.d.ts +55 -0
  151. package/dist/runTerminalResults.d.ts.map +1 -0
  152. package/dist/runTerminalResults.js +134 -0
  153. package/dist/runTerminalResults.js.map +1 -0
  154. package/dist/runTerminals.d.ts +8 -57
  155. package/dist/runTerminals.d.ts.map +1 -1
  156. package/dist/runTerminals.js +423 -135
  157. package/dist/runTerminals.js.map +1 -1
  158. package/dist/secretDiff.d.ts +32 -0
  159. package/dist/secretDiff.d.ts.map +1 -0
  160. package/dist/secretDiff.js +140 -0
  161. package/dist/secretDiff.js.map +1 -0
  162. package/dist/structuredOutput.d.ts.map +1 -1
  163. package/dist/structuredOutput.js.map +1 -1
  164. package/dist/task-contract-builder.d.ts +35 -0
  165. package/dist/task-contract-builder.d.ts.map +1 -0
  166. package/dist/task-contract-builder.js +114 -0
  167. package/dist/task-contract-builder.js.map +1 -0
  168. package/dist/terminalOutcome.d.ts +22 -0
  169. package/dist/terminalOutcome.d.ts.map +1 -0
  170. package/dist/terminalOutcome.js +39 -0
  171. package/dist/terminalOutcome.js.map +1 -0
  172. package/dist/transientClassify.js.map +1 -1
  173. package/package.json +17 -17
@@ -1,96 +1,68 @@
1
1
  import { existsSync } from "node:fs";
2
- import { observeNativeSessionEvent, preflightCredentialProfile, preflightDefaultSubject, resolveCredentialProfile, resumeSessionForProfile, rotateSpecOnTypedLimit, selectedProfileAvailability, } from "./credential-profiles.js";
2
+ import { effectiveAuthPreference, observeNativeSessionEvent, preflightCredentialProfile, preflightDefaultSubject, probeCredentialProfileStatus, profileHeadroomBreach, profileStatusAdmits, resolveCredentialProfile, resumeSessionForProfile, rotateSpecOnTypedLimit, selectedProfileAvailability, staticRotationCandidates, } from "./credential-profiles.js";
3
3
  import { writeRunTelemetryArtifact } from "./runTelemetryWriter.js";
4
4
  import { buildFileBackedSynthesisInput, materializeWinnerOutputs, stageFileBackedContext, writeCandidateAttemptArtifacts, } from "./candidateOutputs.js";
5
5
  import { processAttemptUsage } from "./attemptUsage.js";
6
+ import * as AC from "./attemptUsageCost.js";
6
7
  import { candidateRoster, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, } from "./candidateEvidence.js";
7
8
  import { capabilityIntents } from "@claudexor/gateway";
8
9
  import { policyFindings } from "./policyFindings.js";
9
10
  import { join } from "node:path";
10
11
  import { finalizePlanRun, runCouncilPlan, writePlanHarnessFailure, } from "./planRun.js";
11
- import { HarnessRunSpec, ModeKind as ModeKindSchema, SCHEMA_VERSION, TRUST_FULL_ACCESS_CODE, FrozenTaskContractArtifact as TaskContractSchema, isBlocking, makeOutcomeFacts, normalizeUserOutputSchema, strictifyOutputSchema, estimateEffectiveAuthRoute, } from "@claudexor/schema";
12
- import { globalConfigDir, loadConfig, trustConfigPath } from "@claudexor/config";
13
- import { AnswerAssembly, CLAUDEXOR_ARTIFACT_DIR, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
12
+ import { runPlannerAttempt as executePlannerAttempt, } from "./plannerAttempt.js";
13
+ import { HarnessRunSpec, ModeKind as ModeKindSchema, QuotaSnapshot as QuotaSnapshotSchema, isBlocking, makeOutcomeFacts, normalizeUserOutputSchema, strictifyOutputSchema, estimateEffectiveAuthRoute, } from "@claudexor/schema";
14
+ import { globalConfigDir, loadConfig } from "@claudexor/config";
15
+ import { AnswerAssembly, CLAUDEXOR_ARTIFACT_DIR, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, countsAsAgentProgress, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
14
16
  import { assertRouteModelsAllowed } from "./modelGovernance.js";
15
17
  import { governRouteEffort } from "./effortGovernance.js";
16
- import { RequestRequirementsResolver } from "./requestRequirements.js";
18
+ import { isFullAccess, RequestRequirementsResolver } from "./requestRequirements.js";
19
+ import { activateDelegationParent } from "./delegation-parent-activation.js";
20
+ import { routingFailureClassification } from "./routing-failure.js";
21
+ export { routingFailureClassification } from "./routing-failure.js";
22
+ import { runBounded } from "./run-bounded.js";
23
+ import { planPrompt } from "./plan-prompt.js";
24
+ import { verifiedPlanBrief, withPlanBrief } from "./planBrief.js";
25
+ import { resolveRunInputDefaults } from "./run-input-resolution.js";
26
+ import { beginAnnouncedRun } from "./runEventLog.js";
27
+ import { arbitrationBudgetOptions, decisionBudgetSummary } from "./decisionBudget.js";
17
28
  import { buildRevisePrompt } from "./revisePrompt.js";
18
29
  import { cancelledResult, failTerminally, guardAnnouncedRun, writeFailure, } from "./runTerminals.js";
19
30
  import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
20
31
  import { assertOutputSchemaCompiles, finalizeStructuredOutput } from "./structuredOutput.js";
21
- import { transientRetryDelayMs, promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, safeErrorMessage, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, writeRaceDeliveryDecision, } from "./runSupport.js";
32
+ import { transientRetryDelayMs, promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, safeErrorMessage, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, winnerNeedsHuman, writeRaceDeliveryDecision, } from "./runSupport.js";
22
33
  import { candidateStatusInRouteContext, resolveReadOnlyRouteContext, } from "./routeContext.js";
23
34
  import { resolveAutoReviewerPanel, resolveExplicitReviewerPanel } from "./reviewerPanel.js";
35
+ import { ensureWriteModeGitBoundary } from "./git-precondition.js";
24
36
  import { buildContinuation, } from "./continuity.js";
25
37
  import { activePlanPointer, resolveContinuitySummary, workspaceAnchor, } from "./continuity-facts.js";
26
38
  import { runDiffReview } from "./diffReview.js";
27
39
  import { rawScoutBundle, resolveDeepScanSynthesis, } from "./deepScanReducer.js";
28
40
  import { classifyAdapterThrow, createAttemptTelemetry, observeAttemptTelemetry, setAttemptOutcome, telemetrySummary, toolWarnings, unrecoveredToolErrors, webUnsatisfied, } from "./attemptTelemetry.js";
41
+ import * as delegateFailure from "./delegationFailure.js";
42
+ import * as secretDiff from "./secretDiff.js";
29
43
  import { dominantHarnessFailureCategory, harnessFailureNextActions } from "./harnessFailure.js";
30
44
  import { finalizeAttempt, readOnlyNoSuccessTerminal, resolveWorkReportEnvelope, unrecoveredToolErrorFailure, unwrapWorkReportEnvelope, webEvidenceFailure, } from "./attemptFinalize.js";
31
45
  import { buildContinuationPacket, decideContinuation, synthesizeContinuationRequest, } from "./continuation.js";
32
46
  import { interactionChannelFor } from "./interaction.js";
33
- import { gateSpecsFromContract, renderTestsEvidence, resolveContractGates, } from "./contract-gates.js";
47
+ import { gateSpecsFromContract, renderTestsEvidence } from "./contract-gates.js";
48
+ import { buildTaskContract } from "./task-contract-builder.js";
34
49
  import { ArtifactStore } from "@claudexor/artifact-store";
35
- import { EventLog } from "@claudexor/event-log";
36
50
  import { assertMandatoryContext, buildContextPack, rawContextForEnvelope, preflightEvidence, writeEvidencePacket, } from "@claudexor/context";
37
- import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, ensureClaudeBridge, ensureGitRepository, consumeRawPatchEnvelope, snapshotTree, } from "@claudexor/workspace";
51
+ import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, ensureClaudeBridge, consumeRawPatchEnvelope, snapshotTree, } from "@claudexor/workspace";
38
52
  import { blockedDecisionOverride, finalVerifyBlocks, finalVerifyPatch, verifyAndDeliver, } from "@claudexor/delivery";
39
53
  import { HarnessGateway } from "@claudexor/gateway";
40
54
  import { ReadinessLedger, evaluateConvergence, failureSignature, gatesPassed, reviewCandidate, revalidateFindings, runGates, } from "@claudexor/review";
41
55
  import { arbitrate } from "@claudexor/arbitration";
42
56
  import { buildSynthesisPlan, decideSynthesis } from "@claudexor/synthesis";
43
- import { attemptCostEvidence, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, explainRanking, loadHarnessMetrics, promptFingerprint, unknownCostSettlement, rankHarnesses, reviewUsageCostSettlement, } from "@claudexor/budget";
44
- import { readTextSafe, appendLine, assertNoInlineSecretValues, containsSecretLikeToken, DELEGATION_ENV, hashJson, newId, noProjectRepoRoot, nowIso, redactSecrets, safeInvoke, sha256, userConfigDir, writeText, } from "@claudexor/util";
57
+ import { attemptCostEvidence, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, explainRanking, loadHarnessMetrics, promptFingerprint, rankHarnesses, reviewUsageCostSettlement, } from "@claudexor/budget";
58
+ import { readTextSafe, appendLine, assertNoInlineSecretValues, DELEGATION_ENV, hashJson, newId, noProjectRepoRoot, redactSecrets, safeInvoke, sha256, userConfigDir, writeText, } from "@claudexor/util";
45
59
  import { assertWriteIsolation } from "./write-isolation.js";
46
- /** A routed candidate adapter plus its manifest capabilities and user settings. */
47
- /** The two access profiles that map to codex `danger-full-access` / an
48
- * unsandboxed lane — the only ones under which a full-access-requiring MCP
49
- * injection (the belt on codex) can reach the daemon. */
50
- export function isFullAccess(access) {
51
- return access === "full" || access === "external_sandbox_full";
52
- }
53
- /**
54
- * A routing preflight refusal (`RoutingPreflightError`: quality routing with no
55
- * comparable user-declared tier for the intent) is a CONFIGURATION error, not a
56
- * harness-availability problem (A-1/D-9/#22). Classifying it as
57
- * `harness_unavailable` sent the operator to re-auth or wait for a harness; the
58
- * real fix is to configure a tier or change the routing goal. Detected by the
59
- * typed `code` (robust across duplicate `@claudexor/budget` package copies) so
60
- * EVERY strategy's routing catch (ask/agent/plan/deep-scan/council) classifies
61
- * it identically. Returns the failure category + matching remediation.
62
- */
63
- export function routingFailureClassification(err) {
64
- const isPreflightRefusal = !!err &&
65
- typeof err === "object" &&
66
- err.code === "routing_preflight_refused";
67
- if (isPreflightRefusal) {
68
- return { category: "config_error", nextActions: harnessFailureNextActions("config_error") };
69
- }
70
- return { category: "harness_unavailable" };
71
- }
72
60
  const LABELS = "ABCDEFGHIJ".split("");
73
61
  const NO_PROJECT_ROOT = noProjectRepoRoot();
74
62
  /** Concurrency cap for parallel candidates/explorers (locked decision: min(n, 4)). */
75
63
  const MAX_PARALLEL_CANDIDATES = 4;
76
64
  /** Default wait for one interactive answer before a benign decline. */
77
65
  const DEFAULT_INTERACTION_TIMEOUT_MS = 900_000;
78
- /** Run `work` over `items` with bounded concurrency, preserving item order via index. */
79
- async function runBounded(items, limit, work) {
80
- if (items.length === 0)
81
- return;
82
- const concurrency = Math.max(1, Math.min(limit, items.length));
83
- let next = 0;
84
- const workers = Array.from({ length: concurrency }, async () => {
85
- for (;;) {
86
- const idx = next++;
87
- if (idx >= items.length)
88
- return;
89
- await work(items[idx], idx);
90
- }
91
- });
92
- await Promise.all(workers);
93
- }
94
66
  export class Orchestrator {
95
67
  deps;
96
68
  gateway;
@@ -118,7 +90,7 @@ export class Orchestrator {
118
90
  });
119
91
  }
120
92
  async run(input) {
121
- const resolved = this.resolveRunInput(input);
93
+ let resolved = this.resolveRunInput(input);
122
94
  // INV-062 at the ENGINE boundary: every surface fences prompts already,
123
95
  // but a direct embedder (or the daemon-less local REPL fallback) reaches
124
96
  // this entry without one. Prompts, per-run instructions, AND outputSchema
@@ -137,6 +109,25 @@ export class Orchestrator {
137
109
  throw new Error(`unknown mode: ${String(resolved.mode)}`);
138
110
  }
139
111
  const mode = parsedMode.data;
112
+ if (resolved.delegate === true && mode !== "agent") {
113
+ throw new Error(`Delegate is an agent-only strategy (got mode=${mode})`);
114
+ }
115
+ const runId = resolved.runId ?? newId("run");
116
+ let delegatedLedgerAttached = false;
117
+ resolved = {
118
+ ...resolved,
119
+ runId,
120
+ taskId: resolved.taskId ?? newId("task"),
121
+ onDelegatedLedgerAttached: () => {
122
+ delegatedLedgerAttached = true;
123
+ },
124
+ };
125
+ if (resolved.delegate === true) {
126
+ resolved = {
127
+ ...resolved,
128
+ delegationParentRunId: runId,
129
+ };
130
+ }
140
131
  const projectProtectedPaths = mode === "agent" ? this.projectConfig(resolved.repoRoot).constraints.protected_paths : [];
141
132
  assertWriteIsolation({
142
133
  mode,
@@ -173,41 +164,63 @@ export class Orchestrator {
173
164
  if (resolved.repoRoot !== NO_PROJECT_ROOT) {
174
165
  assertMandatoryContext(resolved.repoRoot, this.projectConfig(resolved.repoRoot).context.mandatory_files);
175
166
  }
176
- // Reviewer panels are validated only inside the strategies that actually
177
- // review (race/convergence under agent, and plan) — AFTER run-dir
167
+ // Reviewer panels are validated only inside Agent strategies that actually
168
+ // review (race/convergence; Plan Council is the plan critique path) — AFTER run-dir
178
169
  // creation, so a doomed explicit panel yields typed failure ARTIFACTS
179
170
  // (failure.yaml naming the refusal) instead of a bare pre-run throw.
180
- // ask/audit never spawn reviewers, so a panel there never spends doctor/
171
+ // Ask and Plan never spawn code reviewers, so a panel there never spends doctor/
181
172
  // model probes and never fails a run that would not use it.
182
173
  // Whole-strategy terminal net: once a strategy ANNOUNCES its
183
174
  // run, any escaped throw still stamps failure.yaml + summary + run.failed
184
175
  // instead of orphaning events.jsonl.
185
- return guardAnnouncedRun(resolved.signal, (announce) => {
186
- switch (mode) {
187
- case "ask":
188
- // `--deep-scan` widens the answer into the bounded multi-scout
189
- // research sweep with synthesis (the old `audit --swarm`/`explore`).
190
- return resolved.deepScan
191
- ? this.runDeepScan(resolved, announce)
192
- : this.runAsk(resolved, announce);
193
- case "agent":
194
- // Engine strategies are FLAGS on agent (v0.9 collapse): `--until-clean`
195
- // and `--attempts` select the convergence loop; `--n` selects the race
196
- // width; `--create` switches the candidate intent to create_from_scratch.
197
- if (resolved.untilClean)
198
- return this.runConvergence(resolved, mode, null, announce);
199
- if (resolved.attempts !== undefined && resolved.attempts !== null) {
200
- return this.runConvergence(resolved, mode, resolved.attempts, announce);
201
- }
202
- return this.runRace({ ...resolved, n: resolved.n ?? 1 }, mode, announce);
203
- case "plan":
204
- return this.runPlan(resolved, announce);
176
+ const releaseRunState = (settledRunId) => {
177
+ this.routingRationaleByRun.delete(settledRunId);
178
+ this.deps.delegationBudgetAuthority?.releaseRun(settledRunId);
179
+ };
180
+ try {
181
+ return await guardAnnouncedRun(resolved.signal, (announce) => {
182
+ switch (mode) {
183
+ case "ask":
184
+ // `--deep-scan` widens the answer into the bounded multi-scout
185
+ // research sweep with synthesis (the old `audit --swarm`/`explore`).
186
+ return resolved.deepScan
187
+ ? this.runDeepScan(resolved, announce)
188
+ : this.runAsk(resolved, announce);
189
+ case "agent":
190
+ // Engine strategies are FLAGS on agent (v0.9 collapse): `--until-clean`
191
+ // and `--attempts` select the convergence loop; `--n` selects the race
192
+ // width; `--create` switches the candidate intent to create_from_scratch.
193
+ if (resolved.untilClean)
194
+ return this.runConvergence(resolved, mode, null, announce);
195
+ if (resolved.attempts !== undefined && resolved.attempts !== null) {
196
+ return this.runConvergence(resolved, mode, resolved.attempts, announce);
197
+ }
198
+ return this.runRace({ ...resolved, n: resolved.n ?? 1 }, mode, announce);
199
+ case "plan":
200
+ return this.runPlan(resolved, announce);
201
+ }
202
+ }, async ({ runId }) => {
203
+ const authority = this.deps.delegationBudgetAuthority;
204
+ if (!authority?.hasParent(runId))
205
+ return;
206
+ authority.beginParentClose(runId);
207
+ await authority.waitForChildren(runId);
208
+ },
209
+ // Single per-run terminalization hook: release the routing-rationale map
210
+ // entry on EVERY terminal (incl. a run that died before its telemetry
211
+ // writer ran, which is the leak this closes).
212
+ releaseRunState);
213
+ }
214
+ catch (error) {
215
+ // A durable startup sink may refuse `run.created` before the strategy can
216
+ // announce its context. Release only a delegated child ledger acquired by
217
+ // THIS invocation; a caller-supplied run-id collision must never release
218
+ // another live parent/child authority or its routing state.
219
+ if (delegatedLedgerAttached) {
220
+ this.deps.delegationBudgetAuthority?.releaseRun(runId);
205
221
  }
206
- },
207
- // Single per-run terminalization hook: release the routing-rationale map
208
- // entry on EVERY terminal (incl. a run that died before its telemetry
209
- // writer ran, which is the leak this closes).
210
- (runId) => this.routingRationaleByRun.delete(runId));
222
+ throw error;
223
+ }
211
224
  }
212
225
  async resolveReviewers(cwd, runAuthPreference, onIgnoredSetting) {
213
226
  if (this.deps.reviewers)
@@ -280,11 +293,7 @@ export class Orchestrator {
280
293
  }
281
294
  authPreferenceForHarness(repoRoot, harnessId, runAuthPreference) {
282
295
  const cfg = this.config(repoRoot)?.global;
283
- const explicit = (v) => v && v !== "auto" ? v : undefined;
284
- return (explicit(runAuthPreference) ??
285
- explicit(cfg?.harnesses?.[harnessId]?.auth_preference) ??
286
- explicit(cfg?.routing?.auth_preference) ??
287
- "auto");
296
+ return effectiveAuthPreference(runAuthPreference, cfg?.harnesses?.[harnessId]?.auth_preference, cfg?.routing?.auth_preference);
288
297
  }
289
298
  artifactStore(input) {
290
299
  if (input.mode === "ask" && input.contextMode === "off" && input.repoRoot === NO_PROJECT_ROOT) {
@@ -307,20 +316,23 @@ export class Orchestrator {
307
316
  estimateUsdFloor(repoRoot) {
308
317
  return this.config(repoRoot)?.global.budget.estimate_usd_floor ?? 0.05;
309
318
  }
319
+ /** Delegate children overlap their still-running parent, so child-side paid
320
+ * units use the same bounded floor as later slots in a parallel wave. */
321
+ reservationEstimateUsd(input, parallel = false) {
322
+ return parallel || Boolean(input.delegatedFromRunId)
323
+ ? this.estimateUsdFloor(input.repoRoot)
324
+ : undefined;
325
+ }
310
326
  execRootOf(input) {
311
327
  return input.executionRoot ?? input.repoRoot;
312
328
  }
313
- sessionSpecFields(input, harnessId, log) {
329
+ async sessionSpecFields(input, harnessId, log, defaultRoute = null) {
314
330
  const cfg = this.config(input.repoRoot)?.global;
315
- const profile = this.preflightProfile(input, harnessId, log);
316
- const explicit = (v) => (v && v !== "auto" ? v : undefined);
331
+ const profile = await this.preflightProfile(input, harnessId, log, defaultRoute);
317
332
  return {
318
333
  // "auto" at ANY level falls through (thread turns send the thread default
319
334
  // "auto" as a per-run value; it must not shadow a configured preference).
320
- auth_preference: explicit(input.authPreference) ??
321
- explicit(cfg?.harnesses?.[harnessId]?.auth_preference) ??
322
- explicit(cfg?.routing?.auth_preference) ??
323
- "auto",
335
+ auth_preference: effectiveAuthPreference(input.authPreference, cfg?.harnesses?.[harnessId]?.auth_preference, cfg?.routing?.auth_preference),
324
336
  resume_session_id: resumeSessionForProfile(input.resumeSessions?.[harnessId], profile),
325
337
  credential_profile: profile,
326
338
  };
@@ -395,19 +407,60 @@ export class Orchestrator {
395
407
  const policy = this.config(repoRoot)?.global.harnesses?.[harnessId]?.profile_policy;
396
408
  return policy ?? { limit_action: "fail", rotation_eligible: [], headroom_threshold: 0.9 };
397
409
  }
398
- preflightProfile(input, harnessId, log) {
410
+ /** Fresh profile readiness for one rotation decision epoch. Accounts uses
411
+ * the same probe wrapper + admission predicate when projecting next_up. */
412
+ async readyProfileIdsForRotation(input, harnessId, current, excluded = new Set()) {
413
+ const profiles = staticRotationCandidates({
414
+ registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
415
+ harnessId,
416
+ policy: this.profilePolicy(input.repoRoot, harnessId),
417
+ current,
418
+ excluded,
419
+ });
420
+ const adapter = this.deps.registry.get(harnessId);
421
+ const entries = await Promise.all(profiles.map(async (profile) => ({
422
+ profile,
423
+ status: await probeCredentialProfileStatus(profile, adapter?.probeCredentialProfile?.bind(adapter)),
424
+ })));
425
+ return new Set(entries
426
+ .filter(({ profile, status }) => profileStatusAdmits(profile, status))
427
+ .map(({ profile }) => profile.profile_id));
428
+ }
429
+ async preflightProfile(input, harnessId, log, defaultRoute) {
399
430
  const profile = this.resolveCredentialProfile(input, harnessId);
400
431
  const policy = this.profilePolicy(input.repoRoot, harnessId);
401
432
  const registry = this.config(input.repoRoot)?.global.credential_profiles ?? [];
402
433
  const snapshots = this.deps.quotaSnapshots?.() ?? [];
403
434
  const emit = (type, payload) => log?.emit(type, payload);
435
+ const breach = profileHeadroomBreach(snapshots, harnessId, profile?.profile_id ?? null, policy.headroom_threshold);
436
+ const readyProfileIds = policy.limit_action === "rotate" &&
437
+ breach !== null &&
438
+ (profile !== null || defaultRoute === "local_session")
439
+ ? await this.readyProfileIdsForRotation(input, harnessId, profile)
440
+ : new Set();
404
441
  if (!profile) {
405
442
  // Unpinned runs (INV-135 auto-balance): under `rotate`, a fresh
406
443
  // default-subject headroom breach starts on the next eligible
407
444
  // subscription profile instead; `fail`/`ask` change nothing.
408
- return preflightDefaultSubject({ harnessId, policy, registry, snapshots, emit });
445
+ return preflightDefaultSubject({
446
+ harnessId,
447
+ policy,
448
+ registry,
449
+ snapshots,
450
+ readyProfileIds,
451
+ defaultRoute,
452
+ emit,
453
+ });
409
454
  }
410
- return preflightCredentialProfile({ profile, harnessId, policy, registry, snapshots, emit });
455
+ return preflightCredentialProfile({
456
+ profile,
457
+ harnessId,
458
+ policy,
459
+ registry,
460
+ snapshots,
461
+ readyProfileIds,
462
+ emit,
463
+ });
411
464
  }
412
465
  /**
413
466
  * Resolve candidate adapters: explicit `--harness`, else available real harnesses, then
@@ -416,89 +469,11 @@ export class Orchestrator {
416
469
  * expand to n. Fails loudly if nothing can perform the intent.
417
470
  */
418
471
  resolveRunInput(input) {
419
- if (input.contextMode === "off" &&
420
- !(input.mode === "ask" && input.repoRoot === NO_PROJECT_ROOT)) {
421
- throw new Error("contextMode 'off' is only supported for Ask without a repoRoot");
422
- }
423
- const cfg = this.config(input.repoRoot);
424
- const configuredPool = cfg?.global.routing.eligible_harnesses;
425
- const harnesses = input.harnesses ?? (configuredPool && configuredPool.length > 0 ? configuredPool : undefined);
426
- // GH #25 precedence: an explicit --primary-harness wins and is validated
427
- // against the pool; else a single-item explicit pool infers itself as
428
- // primary (shipped in #34); else the configured default primary applies.
429
- const explicitPrimary = input.primaryHarness;
430
- const configPrimary = cfg?.global.routing.primary_harness;
431
- const primaryHarness = explicitPrimary ??
432
- (input.harnesses?.length === 1 ? input.harnesses[0] : undefined) ??
433
- configPrimary ??
434
- undefined;
435
- if (primaryHarness &&
436
- harnesses &&
437
- harnesses.length > 0 &&
438
- !harnesses.includes(primaryHarness)) {
439
- if (explicitPrimary) {
440
- // An explicit primary must be a member of the eligible pool (authoritative).
441
- throw new Error(`primary harness '${explicitPrimary}' is not in the eligible harness pool (${harnesses.join(", ")}); ` +
442
- `pass --primary-harness as one of [${harnesses.join(", ")}], or add '${explicitPrimary}' to --harness`);
443
- }
444
- // GH #25 remainder: a MULTI-harness pool whose CONFIGURED default primary
445
- // is absent, with no --primary-harness pinned, is ambiguous — the engine
446
- // must not silently reroute. Refuse with a structured, copy-pasteable fix
447
- // naming the pool, the missing primary, and the exact flag to add.
448
- throw new HarnessUnavailableError(`ambiguous primary harness: the configured default primary '${primaryHarness}' is not in the selected pool [${harnesses.join(", ")}], ` +
449
- `and no --primary-harness was given. Pin one explicitly, e.g. \`--primary-harness ${harnesses[0]}\` ` +
450
- `(or another of [${harnesses.join(", ")}]).`);
451
- }
452
- if (input.web && input.externalContextPolicy && input.web !== input.externalContextPolicy) {
453
- throw new Error(`contradictory web policy: web='${input.web}' vs externalContextPolicy='${input.externalContextPolicy}' (pass one, or equal values)`);
454
- }
455
- const web = input.web ?? input.externalContextPolicy ?? "auto";
456
- // INV-103: scalar `model` expands only to the resolved primary, never the pool;
457
- // an explicit per-harness map wins. Unknown map keys fail loudly (INV-021).
458
- const knownHarnessIds = new Set(this.deps.registry.keys());
459
- for (const key of Object.keys(input.models ?? {})) {
460
- if (!knownHarnessIds.has(key)) {
461
- throw new Error(`models map names unknown harness '${key}' (registered: ${[...knownHarnessIds].sort().join(", ")}); ` +
462
- `run \`claudexor harness list --all\``);
463
- }
464
- }
465
- const models = { ...input.models };
466
- if (input.model) {
467
- const scalarTarget = primaryHarness ?? (harnesses && harnesses.length === 1 ? harnesses[0] : undefined);
468
- if (!scalarTarget) {
469
- throw new Error(`a scalar model ('${input.model}') is ambiguous without a primary harness: ` +
470
- `the pool is ${harnesses && harnesses.length > 0 ? `[${harnesses.join(", ")}]` : "auto-resolved"} — ` +
471
- `set a primary harness, pass exactly one --harness, or use a harness-scoped model map`);
472
- }
473
- models[scalarTarget] ??= input.model;
474
- }
475
- // QA-035: FREEZE the config-derived per-harness default_model into the
476
- // resolved model map at initial normalization, exactly like an explicit
477
- // input. Without this the TaskContract records `routing_models: {}` and an
478
- // Exact Retry re-resolves the model against CURRENT settings — silently
479
- // changing the route after a settings edit. A per-turn/scalar value already
480
- // set wins (??=). Only a known resolved pool can be frozen here; a pure
481
- // auto pool's lanes are not yet known (documented seam).
482
- const harnessCfg = cfg?.global.harnesses ?? {};
483
- for (const hid of harnesses ?? []) {
484
- const def = harnessCfg[hid]?.default_model;
485
- if (def)
486
- models[hid] ??= def;
487
- }
488
- return {
489
- ...input,
490
- harnesses,
491
- primaryHarness,
492
- model: undefined,
493
- models,
494
- routingGoal: input.routingGoal ??
495
- this.deps.routingGoal ??
496
- cfg?.project.budget?.routing_goal ??
497
- cfg?.global.routing.goal ??
498
- "auto",
499
- web,
500
- externalContextPolicy: web,
501
- };
472
+ return resolveRunInputDefaults(input, {
473
+ config: this.config(input.repoRoot),
474
+ registryIds: this.deps.registry.keys(),
475
+ routingGoal: this.deps.routingGoal,
476
+ });
502
477
  }
503
478
  async resolveCandidateAdapters(input, intent, ledger, log, routeContext,
504
479
  /** QA-034: when provided, the pool-ordering rationale is recorded under this
@@ -562,6 +537,27 @@ export class Orchestrator {
562
537
  }
563
538
  }
564
539
  }
540
+ const attachments = input.attachments ?? [];
541
+ // Resolve the complete attachment pool once from manifest truth. If any
542
+ // lane still lacks usable discovery truth, preserve the existing per-lane
543
+ // discovery/doctor precedence and resolve that lane inside the route loop.
544
+ const canResolveAttachmentPool = attachments.length > 0 &&
545
+ ids.every((id) => {
546
+ const status = statusById.get(id);
547
+ return status?.manifest != null && status.status !== "unavailable";
548
+ });
549
+ const attachmentPoolAdmission = canResolveAttachmentPool
550
+ ? this.requestRequirements.resolveAttachmentPool(explicitPool ? "explicit" : "auto", attachments, ids.map((id) => ({
551
+ harnessId: id,
552
+ declarations: statusById.get(id)?.manifest?.capability_profile.attachment_inputs ?? null,
553
+ available: true,
554
+ })))
555
+ : null;
556
+ if (attachmentPoolAdmission?.outcome === "refused") {
557
+ throw new HarnessUnavailableError(attachmentPoolAdmission.message ??
558
+ "no available harness lane can receive the selected attachments");
559
+ }
560
+ const attachmentRejectionById = new Map(attachmentPoolAdmission?.rejected.map((admission) => [admission.harnessId, admission]) ?? []);
565
561
  const policy = input.web ?? input.externalContextPolicy ?? "auto";
566
562
  const pool = [];
567
563
  const dropped = [];
@@ -695,9 +691,10 @@ export class Orchestrator {
695
691
  dropLane(id, "web", why);
696
692
  continue;
697
693
  }
698
- const attachmentRefusal = this.requestRequirements.attachmentRefusal(id, input.attachments ?? [], manifest.capability_profile.attachment_inputs);
699
- if (attachmentRefusal) {
700
- dropLane(id, "attachment", attachmentRefusal);
694
+ const attachmentAdmission = attachmentRejectionById.get(id) ??
695
+ this.requestRequirements.resolveAttachmentLane(id, attachments, manifest.capability_profile.attachment_inputs);
696
+ if (!attachmentAdmission.admitted) {
697
+ dropLane(id, "attachment", attachmentAdmission.message ?? `${id} rejects attachments`);
701
698
  continue;
702
699
  }
703
700
  const reason = status.reasons.length > 0 ? `: ${status.reasons.join("; ")}` : "";
@@ -717,6 +714,14 @@ export class Orchestrator {
717
714
  access: requiredAccess,
718
715
  }),
719
716
  denyRequirement: this.requestRequirements.resolveDenyPaths(id, (input.denyPaths?.length ?? 0) > 0),
717
+ delegationRequirement: this.requestRequirements.resolveDelegation({
718
+ harnessId: id,
719
+ requested: input.delegate === true,
720
+ runtimeAvailable: input.delegationBelt != null,
721
+ manifestCapable: manifest.capability_profile.mcp_injection,
722
+ requiresFullAccess: manifest.capability_profile.mcp_injection_requires_full_access,
723
+ fullAccess: isFullAccess(requiredAccess),
724
+ }),
720
725
  effortLevels: manifest.capabilities.effort_levels,
721
726
  knownModels: manifest.capabilities.known_models,
722
727
  // A selected profile's credential_kind IS the route (round-18 #2);
@@ -763,6 +768,7 @@ export class Orchestrator {
763
768
  }
764
769
  emitPrimaryDivergence(log, input.primaryHarness, ordered, pool, dropped);
765
770
  const n = input.n ?? ordered.length;
771
+ const selectionOrder = ordered;
766
772
  const out = [];
767
773
  if (droppedLanes.length > 0 && !allowDuplicateFill) {
768
774
  // QA-043: lanes were dropped from an AUTO best-of pool (an explicit pool
@@ -771,15 +777,15 @@ export class Orchestrator {
771
777
  // masks the omission. Clamp to distinct survivors and disclose below.
772
778
  // (Deep-scan sets allowDuplicateFill: its width is scout coverage, not
773
779
  // harness diversity, so a dropped lane must not cut the scout count.)
774
- for (let i = 0; i < Math.min(n, ordered.length); i++)
775
- out.push(ordered[i]);
780
+ for (let i = 0; i < Math.min(n, selectionOrder.length); i++)
781
+ out.push(selectionOrder[i]);
776
782
  }
777
783
  else {
778
784
  // No lane was dropped: a pool smaller than `n` is an intentional
779
785
  // best-of-N on the available harness(es) (e.g. explicit `--harness codex
780
786
  // -n 3`), so the historical width fill is preserved.
781
787
  for (let i = 0; i < n; i++)
782
- out.push(ordered[i % ordered.length]);
788
+ out.push(selectionOrder[i % selectionOrder.length]);
783
789
  }
784
790
  // Disclose an auto-pool omission / width clamp once, with the
785
791
  // requested-vs-effective route receipt (never silent — QA-043).
@@ -791,25 +797,24 @@ export class Orchestrator {
791
797
  droppedLanes,
792
798
  });
793
799
  this.requestRequirements.requireEffectiveBrowser(input.browser === true, out.map((lane) => lane.browserRequirement));
794
- // Delegation belt (D32): agent-only, and only on a lane whose adapter can
795
- // inject MCP servers. A requested delegate with NO injecting lane is a typed
796
- // preflight refusal naming the harness(es) never a silently dropped belt.
797
- if (input.delegate === true && !out.some((lane) => lane.supportsMcpInjection)) {
798
- const names = [...new Set(out.map((lane) => lane.adapter.id))].join(", ");
799
- throw new HarnessUnavailableError(`--delegate requires a harness that can host the Claudexor delegation belt (capability_profile.mcp_injection); the routed harness(es) [${names}] cannot inject MCP servers — choose claude or codex, or drop --delegate`);
800
- }
801
- // A belt-injecting lane may still be UNABLE to reach the daemon at its
802
- // access: codex's workspace-write seatbelt cancels the belt's daemon-crossing
803
- // MCP call, so codex only hosts the belt at FULL access (same as its browser
804
- // MCP). If EVERY injecting lane requires full access but runs below it, the
805
- // belt would be injected only to be silently cancelled by the sandbox — the
806
- // exact non-delegation this guard prevents. Refuse with the real remedy.
800
+ // Owner decision (2026-07-26): known PRE-START belt unavailability does
801
+ // not discard the requested Agent work. Continue without Delegate and emit
802
+ // a durable typed warning. Once a descriptor is injected, typed startup
803
+ // failure stays terminal in attemptTelemetry (no mid-attempt downgrade).
807
804
  if (input.delegate === true) {
808
- const injecting = out.filter((lane) => lane.supportsMcpInjection);
809
- const canHostBelt = injecting.some((lane) => !lane.mcpInjectionRequiresFullAccess || isFullAccess(lane.adapterAccess));
810
- if (!canHostBelt) {
811
- const names = [...new Set(injecting.map((lane) => lane.adapter.id))].join(", ");
812
- throw new HarnessUnavailableError(`--delegate needs a belt-hosting lane at full access: [${names}] can inject MCP servers but sandbox-cancel the delegation belt below full access (capability_profile.mcp_injection_requires_full_access) — re-run with --access full, or route a lane (e.g. claude) that hosts the belt at workspace_write`);
805
+ const unavailable = out
806
+ .map((lane) => lane.delegationRequirement)
807
+ .filter((resolution) => !resolution.effective);
808
+ if (unavailable.length > 0) {
809
+ log?.emit("delegation.belt.degraded", {
810
+ requested: true,
811
+ effective: out.some((lane) => lane.delegationRequirement.effective),
812
+ reason: unavailable[0]?.reason ?? "runtime_unavailable",
813
+ lanes: unavailable.map((resolution) => ({
814
+ harness_id: resolution.harness_id,
815
+ reason: resolution.reason,
816
+ })),
817
+ });
813
818
  }
814
819
  }
815
820
  // outputSchema is MANDATORY (Quiz-6a): a selected lane that cannot
@@ -846,6 +851,8 @@ export class Orchestrator {
846
851
  */
847
852
  orderPool(pool, input, intent, statusById, ledger, runId) {
848
853
  let ordered = pool;
854
+ let rationale = null;
855
+ let selectionReason = null;
849
856
  if (pool.length > 0) {
850
857
  const routeLedger = ledger ?? new BudgetLedger();
851
858
  const config = this.config(input.repoRoot).global;
@@ -927,16 +934,35 @@ export class Orchestrator {
927
934
  const ranked = rankHarnesses(remaining, routeCtx)
928
935
  .map((candidate) => byId.get(candidate.harnessId))
929
936
  .filter((candidate) => Boolean(candidate));
930
- // QA-034: the rationale is run evidence recorded ONCE at pool ordering,
931
- // pinned to routeCtx.now so it cannot disagree with the order just taken.
932
- if (runId)
933
- this.routingRationaleByRun.set(runId, explainRanking(remaining, routeCtx));
937
+ rationale = explainRanking(remaining, routeCtx);
934
938
  ordered = ranked;
935
939
  }
940
+ if (input.delegate === true && input.primaryHarnessExplicit !== true) {
941
+ const delegateFirst = [
942
+ ...ordered.filter((lane) => lane.delegationRequirement.effective),
943
+ ...ordered.filter((lane) => !lane.delegationRequirement.effective),
944
+ ];
945
+ if (delegateFirst.some((lane, index) => lane !== ordered[index])) {
946
+ ordered = delegateFirst;
947
+ selectionReason = "delegate_effective_first";
948
+ }
949
+ }
936
950
  if (input.primaryHarness) {
937
951
  const primary = ordered.find((r) => r.adapter.id === input.primaryHarness);
938
- if (primary)
952
+ if (primary && primary !== ordered[0]) {
939
953
  ordered = [primary, ...ordered.filter((r) => r !== primary)];
954
+ selectionReason = "explicit_primary";
955
+ }
956
+ }
957
+ // QA-034: persist the FINAL selected order, including request constraints
958
+ // that intentionally override the underlying cost/quota ranking. This is
959
+ // what keeps route evidence aligned with the lane actually executed.
960
+ if (runId && rationale) {
961
+ this.routingRationaleByRun.set(runId, {
962
+ ...rationale,
963
+ order: ordered.map((lane) => lane.adapter.id),
964
+ reason: selectionReason ?? rationale.reason,
965
+ });
940
966
  }
941
967
  return ordered;
942
968
  }
@@ -1037,114 +1063,9 @@ export class Orchestrator {
1037
1063
  return this.config(repoRoot).project;
1038
1064
  }
1039
1065
  buildContract(input, taskId, mode) {
1040
- const resolvedCfg = this.config(input.repoRoot);
1041
- const cfg = resolvedCfg.project;
1042
- const readOnlyMode = mode === "ask" || mode === "plan";
1043
- const requestedAccess = input.access ?? (readOnlyMode ? "readonly" : resolvedCfg.trust.access_default);
1044
- // Effective access is COMPUTED by the engine, never echoed from a client:
1045
- // read-only modes clamp to readonly regardless of the request.
1046
- const effectiveAccess = readOnlyMode ? "readonly" : requestedAccess;
1047
- // TrustConfig is USER-LEVEL only (versioned repo config must never
1048
- // self-grant sensitive powers): unsandboxed full access requires an
1049
- // explicit allow in ~/.claudexor trust settings — loud error, no downgrade.
1050
- // The gate applies to the EFFECTIVE profile: a read-only run clamped to
1051
- // readonly never runs unsandboxed and needs no trust allow.
1052
- if (effectiveAccess === "full" && !resolvedCfg.trust.allow_full_access) {
1053
- // Typed refusal: the `code` rides the daemon job record onto the thread
1054
- // turn (TurnEnqueueError.code), so surfaces key remedies on the CODE —
1055
- // never on substring-matching this human message.
1056
- throw Object.assign(new Error(`access profile 'full' requires allow_full_access: true in the user-level trust file for this repo ` +
1057
- `(${trustConfigPath(input.repoRoot)}); enable it with \`claudexor trust --allow-full-access\` — refusing to run unsandboxed`),
1058
- // Refusal semantics are born at the throw (W24): the one-time grant is
1059
- // a 403, and the daemon persists this status onto the job record.
1060
- { code: TRUST_FULL_ACCESS_CODE, status: 403 });
1061
- }
1062
- const externalContextPolicy = input.web ?? input.externalContextPolicy ?? "auto";
1063
- // Deterministic gate commands come from explicit run input, then versioned
1064
- // project config. Without these, gateSpecs is empty and convergence is
1065
- // review-only; with them, convergence is test-driven.
1066
- const resolvedGates = resolveContractGates({
1067
- repoRoot: input.repoRoot,
1068
- effectiveAccess,
1069
- config: cfg,
1070
- trustGrants: resolvedCfg.trust.test_command_grants,
1071
- operatorCommands: input.tests ?? [],
1072
- projectCommands: cfg.tests?.commands ?? [],
1073
- });
1074
- const commands = resolvedGates.commands;
1075
- const protectedPaths = [...new Set(cfg.constraints.protected_paths)];
1076
- const autoProtectedPaths = resolvedGates.autoProtectedPaths;
1077
- const protectedPathApprovals = [
1078
- ...new Map([...(input.protectedPathApprovals ?? [])].map((approval) => [approval.path, approval])).values(),
1079
- ];
1080
- return TaskContractSchema.parse({
1081
- schema_version: SCHEMA_VERSION,
1082
- task_id: taskId,
1083
- created_at: nowIso(),
1084
- repo: { root: input.repoRoot, base_ref: input.baseRef ?? "HEAD", dirty_policy: "snapshot" },
1085
- mode: { kind: mode },
1086
- user_intent: { raw: redactSecrets(input.prompt) },
1087
- // Redacted for symmetry with user_intent.raw — a no-op on fenced input
1088
- // (the inline-secret fence already blocked any secret-like value at every
1089
- // ingress incl. this engine boundary), so task-producing lanes read back
1090
- // the real instructions via harnessSpecKnobs().
1091
- instructions: input.instructions === undefined ? undefined : redactSecrets(input.instructions),
1092
- // Already normalized/strictified at the engine boundary (run() refuses
1093
- // unsupported shapes before any run dir exists).
1094
- output_schema: input.outputSchema ?? null,
1095
- auth_preference: input.authPreference ?? "auto",
1096
- credential_profile_id: input.credentialProfileId ?? null,
1097
- max_turns: input.maxTurns ?? null,
1098
- constraints: {
1099
- protected_paths: protectedPaths,
1100
- deny_paths: [...new Set(input.denyPaths ?? [])],
1101
- auto_protected_paths: autoProtectedPaths,
1102
- protected_path_approvals: protectedPathApprovals,
1103
- },
1104
- tests: { commands },
1105
- access: {
1106
- requested_profile: requestedAccess,
1107
- effective_profile: effectiveAccess,
1108
- },
1109
- external_context: {
1110
- policy: externalContextPolicy,
1111
- web_required: externalContextPolicy === "cached" || externalContextPolicy === "live",
1112
- // Per-route upgrades (e.g. claude cached->live) are disclosed in events
1113
- // and telemetry.yaml; the immutable contract records the requested policy.
1114
- effective_mode: externalContextPolicy,
1115
- },
1116
- // Harness-native tool names are adapter knowledge; the neutral contract
1117
- // carries only the policy plus user-configured allow/deny lists (wired
1118
- // from per-harness settings).
1119
- tool_permission_policy: {
1120
- web: externalContextPolicy,
1121
- allow: [],
1122
- deny: [],
1123
- },
1124
- budget: {
1125
- routing_goal: input.routingGoal ?? this.deps.routingGoal ?? cfg?.budget?.routing_goal ?? "auto",
1126
- paid_budget: this.resolvePaidBudget(input.paidBudget, resolvedCfg),
1127
- },
1128
- // The resolved harness-scoped model map (scalar already expanded to the
1129
- // primary by resolveRunInput). The contract is what route spec building
1130
- // reads — there is no run-global model (INV-103).
1131
- routing_models: input.models ?? {},
1132
- // QA-035: freeze the RESOLVED reasoning-effort per known lane so Exact
1133
- // Retry replays it instead of re-resolving current settings. Precedence
1134
- // (specific beats general): the harness-scoped `efforts` map entry, then a
1135
- // per-turn scalar `input.effort`, then the harness settings default — the
1136
- // same map that Exact Retry replays so a NON-PRIMARY lane keeps its own
1137
- // frozen effort (QA-035 completeness). Only known-pool lanes are frozen
1138
- // here (a pure auto pool's lanes resolve later — documented seam).
1139
- routing_efforts: Object.fromEntries([...new Set([...(input.harnesses ?? []), ...Object.keys(input.efforts ?? {})])]
1140
- .map((hid) => [
1141
- hid,
1142
- input.efforts?.[hid] ??
1143
- input.effort ??
1144
- resolvedCfg.global.harnesses?.[hid]?.effort ??
1145
- null,
1146
- ])
1147
- .filter((entry) => entry[1] !== null)),
1066
+ return buildTaskContract(input, taskId, mode, {
1067
+ paidBudget: this.deps.paidBudget,
1068
+ routingGoal: this.deps.routingGoal,
1148
1069
  });
1149
1070
  }
1150
1071
  /**
@@ -1168,13 +1089,15 @@ export class Orchestrator {
1168
1089
  * a WRITING agent intent (the delegator integrates results in its workspace;
1169
1090
  * read lanes and reviewers have nothing to delegate). */
1170
1091
  delegationBeltFor(input, intent, routed, resolvedBudget) {
1171
- if (!input?.delegate || !input.delegationBelt || !routed.supportsMcpInjection)
1092
+ if (!input?.delegate ||
1093
+ !input.delegationBelt ||
1094
+ !input.delegationParentRunId ||
1095
+ !routed.delegationRequirement.effective)
1172
1096
  return [];
1173
1097
  // A lane that sandbox-cancels the belt below full access (codex) must NOT
1174
- // receive a belt it cannot use that is the silent non-delegation. The
1175
- // preflight already refused a run whose ONLY injecting lanes are such lanes
1176
- // below full access; here we simply skip injecting into an individual lane
1177
- // that cannot host it, so a mixed pool keeps the belt on the lanes that can.
1098
+ // receive a belt it cannot use. Per-lane requirement resolution records the
1099
+ // typed degradation, while a mixed pool keeps the belt on lanes that can
1100
+ // host it.
1178
1101
  if (routed.mcpInjectionRequiresFullAccess && !isFullAccess(routed.adapterAccess))
1179
1102
  return [];
1180
1103
  const writingIntents = ["implement", "create_from_scratch", "repair"];
@@ -1190,6 +1113,8 @@ export class Orchestrator {
1190
1113
  ...input.delegationBelt,
1191
1114
  env: {
1192
1115
  ...input.delegationBelt.env,
1116
+ [DELEGATION_ENV.parentRunId]: input.delegationParentRunId,
1117
+ [DELEGATION_ENV.repoRoot]: input.repoRoot,
1193
1118
  [DELEGATION_ENV.budget]: JSON.stringify(resolvedBudget),
1194
1119
  },
1195
1120
  },
@@ -1219,9 +1144,9 @@ export class Orchestrator {
1219
1144
  /**
1220
1145
  * D-16: the WorkReport transport envelope for one route. Called at every
1221
1146
  * task-producing spec-build site AFTER harnessSpecKnobs so it OVERRIDES the
1222
- * plain caller-schema transport with the compiled `{work_report, output}`
1223
- * envelope on capable routes. The returned `mode` is retained by the caller
1224
- * and handed to `unwrapWorkReportEnvelope` when the answer is finalized.
1147
+ * plain caller-schema transport with the resolved WorkReport channel on
1148
+ * capable routes. The returned `mode` is retained by the caller and handed
1149
+ * to `unwrapWorkReportEnvelope` when the answer is finalized.
1225
1150
  */
1226
1151
  workReportEnvelopeFor(routed, contract, interactive) {
1227
1152
  return resolveWorkReportEnvelope({
@@ -1235,7 +1160,7 @@ export class Orchestrator {
1235
1160
  /**
1236
1161
  * D-16: apply the resolved WorkReport transport to a built spec — set the
1237
1162
  * envelope output_schema (constrained/side_tool routes) and APPEND the fenced
1238
- * envelope instruction (validated routes, e.g. cursor). Mutates the spec in
1163
+ * metadata instruction (validated routes, e.g. cursor). Mutates the spec in
1239
1164
  * place and returns the mode the answer unwrap consumes. Called at every
1240
1165
  * task-producing spec-build site so the transport is never wired one-off.
1241
1166
  */
@@ -1312,7 +1237,7 @@ export class Orchestrator {
1312
1237
  * Returns null (no packet, no pointer) for native resume, a fresh thread, or
1313
1238
  * a non-thread run. Never throws: continuity failure degrades to no packet.
1314
1239
  */
1315
- async resolveContinuity(runInput, harnessId, resolvedProfileId, nativeResumeAvailable, store, paths, repoRoot, log) {
1240
+ async resolveContinuity(runInput, harnessId, resolvedProfileId, nativeResumeAvailable, sessionFields, store, paths, repoRoot, log) {
1316
1241
  const ctx = runInput.threadContinuity;
1317
1242
  if (!runInput.threadId || !ctx)
1318
1243
  return null;
@@ -1350,7 +1275,6 @@ export class Orchestrator {
1350
1275
  // summary. Same credential route + scoped lane home a real read-only
1351
1276
  // thread turn uses (INV-034/135). Best-effort in its OWN guard — a summary
1352
1277
  // failure keeps the full mechanical packet, never drops it.
1353
- const sessionFields = this.sessionSpecFields(runInput, harnessId);
1354
1278
  req.cachedSummary = await resolveContinuitySummary({
1355
1279
  req,
1356
1280
  threadId: runInput.threadId,
@@ -1418,13 +1342,15 @@ export class Orchestrator {
1418
1342
  // Isolated scoped-home sessions are never retained after disposal.
1419
1343
  const inPlaceEnvelope = envelope.worktree_path === envelope.repo_root;
1420
1344
  const rawContextPacket = await rawContextForEnvelope(routed.implementationTransport, envelope);
1421
- const sessionFields = runInput ? this.sessionSpecFields(runInput, adapter.id, log) : undefined;
1345
+ const sessionFields = runInput
1346
+ ? await this.sessionSpecFields(runInput, adapter.id, log, routed.authRouteEstimate)
1347
+ : undefined;
1422
1348
  // Continuity (INV-137): once the lane (harness + resolved profile) is known,
1423
1349
  // build the continuation packet, materialize context/THREAD.md, and point
1424
1350
  // the prompt at it — never embed the packet body in the prompt. Replaces the
1425
1351
  // old static session.rebound "not_portable" phrase with a real disclosure.
1426
1352
  const laneContinuity = runInput
1427
- ? 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)
1353
+ ? 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)
1428
1354
  : null;
1429
1355
  let spec = HarnessRunSpec.parse({
1430
1356
  session_id: newId("ses"),
@@ -1493,7 +1419,7 @@ export class Orchestrator {
1493
1419
  const browserServerName = spec.browser ? "browser" : null;
1494
1420
  const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
1495
1421
  knobs.webPolicy === "cached" ||
1496
- knobs.webPolicy === "live", effectiveWebMode ?? knobs.webPolicy, [routed.browserRequirement, routed.denyRequirement], knobs.model, beltServerName, browserServerName);
1422
+ knobs.webPolicy === "live", effectiveWebMode ?? knobs.webPolicy, [routed.browserRequirement, routed.denyRequirement, routed.delegationRequirement], knobs.model, beltServerName, browserServerName);
1497
1423
  let activeSessionId = spec.session_id;
1498
1424
  const onAbort = () => {
1499
1425
  void adapter.cancel?.(activeSessionId)?.catch(() => { });
@@ -1527,13 +1453,16 @@ export class Orchestrator {
1527
1453
  try {
1528
1454
  const watched = withInactivityWatchdog(adapter.run(runSpec), {
1529
1455
  timeoutMs: inactivityMs,
1456
+ countsAsProgress: countsAsAgentProgress,
1530
1457
  onTimeout: () => {
1531
1458
  attemptAbort.abort();
1532
1459
  void adapter.cancel?.(activeSessionId)?.catch(() => { });
1533
1460
  },
1534
- // Waiting on the USER (pending interaction) is legitimate
1535
- // silence the interaction channel enforces its own wait budget.
1461
+ // Waiting on the USER is legitimate silence. The interaction policy
1462
+ // either enforces a finite deadline or waits until answer/cancel/
1463
+ // terminal/restart when the timeout is disabled.
1536
1464
  isSuspended: () => (interaction?.pendingCount?.() ?? 0) > 0,
1465
+ suspensionVersion: () => interaction?.suspensionVersion?.() ?? 0,
1537
1466
  });
1538
1467
  for await (const ev of watched) {
1539
1468
  if (signal?.aborted)
@@ -1573,10 +1502,9 @@ export class Orchestrator {
1573
1502
  observeNativeSessionEvent(runInput, adapter.id, safeEv);
1574
1503
  observeAuthSwitch(log, adapter.id, attemptId, safeEv);
1575
1504
  observeAttemptTelemetry(telemetry, safeEv);
1576
- // QA-024: the injected delegation belt's MCP server reported `failed`
1577
- // to start. Disclose it ONCE as a typed run event the moment the
1578
- // `started` frame reveals it the harness is about to run without
1579
- // `mcp__<belt>__*` tools and may degrade to its own native subagent.
1505
+ // QA-024: the injected delegation belt's MCP server reported a
1506
+ // terminal startup failure. Disclose it ONCE while live; recoverable
1507
+ // exact tool-result failures are evaluated at attempt finalization.
1580
1508
  // The terminal outcome axis (delegationBeltUnavailable) reflects it
1581
1509
  // too; this event makes the failure visible while the run is live.
1582
1510
  if (telemetry.delegationBelt.requested &&
@@ -1664,13 +1592,18 @@ export class Orchestrator {
1664
1592
  // W5.4 failover: a typed-limit hit rebuilds the spec on a NEW vendor
1665
1593
  // session under the next profile with provenance (vendor_limit_rejected).
1666
1594
  if (harnessErrored && runInput && !signal?.aborted) {
1595
+ const rotationPolicy = this.profilePolicy(contract.repo.root, adapter.id);
1596
+ const readyProfileIds = sawTypedLimit && deliverableEmpty && rotationPolicy.limit_action === "rotate"
1597
+ ? await this.readyProfileIdsForRotation(runInput, adapter.id, spec.credential_profile ?? null, triedProfiles)
1598
+ : new Set();
1667
1599
  const rotated = rotateSpecOnTypedLimit({
1668
1600
  spec,
1669
1601
  harnessId: adapter.id,
1670
1602
  attemptId,
1671
- policy: this.profilePolicy(contract.repo.root, adapter.id),
1603
+ policy: rotationPolicy,
1672
1604
  registry: this.config(contract.repo.root)?.global.credential_profiles ?? [],
1673
1605
  snapshots: this.deps.quotaSnapshots?.() ?? [],
1606
+ readyProfileIds,
1674
1607
  triedProfiles,
1675
1608
  sawTypedLimit,
1676
1609
  deliverableEmpty,
@@ -1727,20 +1660,20 @@ export class Orchestrator {
1727
1660
  if (webUnsatisfied(telemetry)) {
1728
1661
  errors.push(webEvidenceFailure(telemetry.web));
1729
1662
  }
1730
- const diff = await wsm.diff(envelope);
1731
- // D-16: un-nest {work_report, output} so answer.md persists the OUTPUT, not the envelope.
1663
+ // D-16: remove the WorkReport transport so answer.md persists only the deliverable.
1732
1664
  const unwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", workReportMode, {
1733
1665
  sideToolReport: telemetry.sideToolWorkReport ?? undefined,
1734
1666
  });
1735
- // X119: persist the VERBATIM redacted bytes; trim ONLY for the emptiness check.
1736
1667
  const redacted = redactSecrets(unwrapped.deliverable);
1737
- const answerText = redacted.trim().length > 0 ? redacted : undefined;
1668
+ const candidateAnswer = redacted.trim().length > 0 ? redacted : undefined;
1669
+ const { diff, refusal: secretDiffRefusal } = await secretDiff.quarantineCandidateWorkspace(wsm, envelope, inPlaceEnvelope, candidateAnswer);
1670
+ harnessErrored = secretDiff.recordSecretDiffRefusal(secretDiffRefusal, errors, harnessErrored);
1671
+ const answerText = secretDiffRefusal ? undefined : candidateAnswer;
1738
1672
  const deliverableEvidence = diff.trim().length > 0 || Boolean(answerText);
1739
- // Cancelled attempts skip gates entirely: the operator asked to
1740
- // stop NOW; running a 600s-per-gate suite after the abort delays the ack
1673
+ // Cancelled attempts skip gates: running a 600s-per-gate suite delays the ack
1741
1674
  // and burns compute on a result nobody will adopt. Diff/attempt.yaml
1742
1675
  // still land, so partial work stays inspectable.
1743
- const gateSignalAborted = signal?.aborted === true;
1676
+ const gateSignalAborted = signal?.aborted === true || secretDiffRefusal !== undefined;
1744
1677
  if (!gateSignalAborted) {
1745
1678
  log?.emit("gate.started", {
1746
1679
  attempt_id: attemptId,
@@ -1770,6 +1703,14 @@ export class Orchestrator {
1770
1703
  });
1771
1704
  }
1772
1705
  const webBlocked = webUnsatisfied(telemetry);
1706
+ // A descriptor that was injected and then reported failed is past the
1707
+ // pre-start degradation boundary. Hard-fail this attempt; never continue as
1708
+ // ordinary Agent or let a native vendor subagent masquerade as belt work.
1709
+ const delegationError = delegateFailure.delegationFailureError(telemetry);
1710
+ if (delegationError) {
1711
+ harnessErrored = true;
1712
+ errors.push(delegationError);
1713
+ }
1773
1714
  // D-16 unified finalizer: fold the WorkReport / context signals into the
1774
1715
  // deliverable + work_state. A broken contract on a constrained route
1775
1716
  // elevates harnessErrored (never a prose success).
@@ -1794,14 +1735,6 @@ export class Orchestrator {
1794
1735
  workState: finalized.workState,
1795
1736
  });
1796
1737
  const attemptDir = join(paths.attemptsDir, attemptId);
1797
- try {
1798
- assertNoSecretLikeTokens("candidate patch diff", diff);
1799
- }
1800
- catch (err) {
1801
- // The stream already settled real spend; a post-stream assertion throw
1802
- // must carry it so the slot catch settles the TRUE cost, not 0.
1803
- throw Object.assign(err instanceof Error ? err : new Error(String(err)), { costUsd: cost });
1804
- }
1805
1738
  recordCleanAttemptMetrics(globalConfigDir(), adapter.id, {
1806
1739
  costUsd: cost,
1807
1740
  streamMs: attemptStreamEndedMs - attemptStartedMs,
@@ -1809,11 +1742,13 @@ export class Orchestrator {
1809
1742
  aborted: signal?.aborted === true,
1810
1743
  authMode: telemetry.authMode,
1811
1744
  });
1812
- const producedFiles = writeCandidateAttemptArtifacts({
1745
+ const producedFiles = AC.withAttemptFailureCost(() => writeCandidateAttemptArtifacts({
1813
1746
  store,
1814
1747
  attemptDir,
1815
1748
  worktreePath: envelope.worktree_path,
1816
1749
  diff,
1750
+ persistPatch: secretDiffRefusal === undefined,
1751
+ persistProducedMedia: secretDiffRefusal === undefined,
1817
1752
  answerText,
1818
1753
  record: {
1819
1754
  attempt_id: attemptId,
@@ -1825,9 +1760,14 @@ export class Orchestrator {
1825
1760
  errors: errors.slice(0, 5),
1826
1761
  ...telemetrySummary(telemetry),
1827
1762
  outcome: telemetry.outcome,
1763
+ ...(secretDiffRefusal ? { secret_diff_refusal: secretDiffRefusal } : {}),
1828
1764
  gates: gates.map((g) => ({ id: g.id, status: g.status })),
1829
1765
  branch: envelope.branch_name,
1830
1766
  },
1767
+ }), {
1768
+ totalUsd: cost,
1769
+ estimated: costEstimated,
1770
+ settlement: attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost),
1831
1771
  });
1832
1772
  return {
1833
1773
  attemptId,
@@ -1844,6 +1784,7 @@ export class Orchestrator {
1844
1784
  costEstimated,
1845
1785
  errors: errors.slice(0, 8),
1846
1786
  telemetry,
1787
+ ...(secretDiffRefusal ? { secretDiffRefusal } : {}),
1847
1788
  outcomeClass: finalized.outcomeClass,
1848
1789
  };
1849
1790
  }
@@ -1854,66 +1795,17 @@ export class Orchestrator {
1854
1795
  // Thin delegate — the channel mechanics live in interaction.ts.
1855
1796
  return interactionChannelFor(input, log, runId, taskId, attemptId, harnessId, supportsInteractive, DEFAULT_INTERACTION_TIMEOUT_MS);
1856
1797
  }
1857
- /**
1858
- * Guarantee a git boundary for write-mode runs. Non-git project folders are
1859
- * initialized in place (`git init`, deterministic baseline commit) without
1860
- * creating or editing `.gitignore`, and the action is announced via a
1861
- * `project.git.initialized` event. Returns the failure message when the
1862
- * boundary cannot be established (the terminal failure events are already
1863
- * emitted); null on success.
1864
- */
1865
- async ensureWriteModeGitBoundary(repoRoot, log, store, paths, runId, mode) {
1866
- if (repoRoot === NO_PROJECT_ROOT)
1867
- return null;
1868
- try {
1869
- const result = await ensureGitRepository(repoRoot);
1870
- if (result.initialized || result.baselineCommitted) {
1871
- log.emit("project.git.initialized", {
1872
- repo_root: repoRoot,
1873
- initialized: result.initialized,
1874
- baseline_committed: result.baselineCommitted,
1875
- gitignore_seeded: result.gitignoreSeeded,
1876
- head_sha: result.headSha,
1877
- });
1878
- }
1879
- return null;
1880
- }
1881
- catch (err) {
1882
- const message = safeErrorMessage(err);
1883
- writeFailure(store, paths, {
1884
- phase: "workspace",
1885
- category: "project",
1886
- safeMessage: message,
1887
- runDir: paths.root,
1888
- nextActions: [
1889
- "Check the project folder permissions",
1890
- "Initialize git manually (git init)",
1891
- "Retry the run",
1892
- ],
1893
- });
1894
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: failed\n- Phase: workspace\n\n${message}\n`);
1895
- log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
1896
- log.emit("run.failed", {
1897
- lifecycle: "failed",
1898
- facts: makeOutcomeFacts("failed", { reason: "harness_failed" }),
1899
- reason: "harness_failed",
1900
- phase: "workspace",
1901
- error: message,
1902
- failure_ref: "final/failure.yaml",
1903
- });
1904
- return message;
1905
- }
1906
- }
1907
1798
  /**
1908
1799
  * D-14 layer 3 (AGENTS.md unification, INV-113): the ONE new live-tree write.
1909
1800
  * When the PROJECT root has `AGENTS.md` and no `CLAUDE.md`, drop a thin
1910
1801
  * `CLAUDE.md` (`@AGENTS.md` import + Claudexor ownership marker) so a Claude
1911
1802
  * Code route reads the same instruction file codex/cursor read natively.
1912
1803
  *
1913
- * Fenced exactly where the automatic git-init boundary is: read-only modes
1914
- * never reach this run-prep stage; `--in-place` stateful targets are left
1915
- * untouched; the write targets the PROJECT root (`repoRoot`), never a worktree
1916
- * envelope. The workspace helper adds exclusive-create + no-follow +
1804
+ * The project-root bridge has its own narrower fence: read-only modes never
1805
+ * reach this run-prep stage and `--in-place` stateful targets are left
1806
+ * untouched. Git admission is independently owned by `runStartRequiresGit`.
1807
+ * The write targets the PROJECT root (`repoRoot`), never a worktree envelope.
1808
+ * The workspace helper adds exclusive-create + no-follow +
1917
1809
  * idempotency, so a hand-written or symlinked `CLAUDE.md` is never overwritten
1918
1810
  * and a concurrent/second prep is a no-op. Announced via a typed
1919
1811
  * `project.claude_bridge.created` event on an actual create only — the git-init
@@ -1938,37 +1830,6 @@ export class Orchestrator {
1938
1830
  });
1939
1831
  }
1940
1832
  }
1941
- /**
1942
- * Freeze-on-implement delivery (D17/D27): verify the frozen plan's hash and
1943
- * materialize it as context/PLAN.md in the run artifact tree — OUTSIDE every
1944
- * worktree, so it can never dirty a diff — then point the prompt at the
1945
- * absolute path. A mismatched or unreadable plan fails LOUDLY before any
1946
- * harness spawns (the tamper fence; retry replays planRef verbatim, so a
1947
- * retried implement can never silently run without its plan).
1948
- */
1949
- withPlanBrief(input, store, paths, log) {
1950
- if (!input.planRef)
1951
- return input;
1952
- const text = readTextSafe(input.planRef.path);
1953
- if (!text || !text.trim()) {
1954
- throw new Error(`implement plan: the frozen plan at ${input.planRef.path} is missing or unreadable`);
1955
- }
1956
- const digest = sha256(text).replace(/^sha256:/, "");
1957
- if (digest !== input.planRef.sha256) {
1958
- 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`);
1959
- }
1960
- const briefPath = join(paths.contextDir, "PLAN.md");
1961
- store.writeText(briefPath, text);
1962
- log.emit("plan.brief.materialized", {
1963
- plan_run_id: input.planRef.runId,
1964
- sha256: input.planRef.sha256,
1965
- path: "context/PLAN.md",
1966
- });
1967
- return {
1968
- ...input,
1969
- prompt: `${input.prompt}\n\nThe approved plan is at: ${briefPath} — read it before starting and re-read it as needed.`,
1970
- };
1971
- }
1972
1833
  async runRace(input, mode, announce) {
1973
1834
  const taskId = input.taskId ?? newId("task");
1974
1835
  const runId = input.runId ?? newId("run");
@@ -1976,29 +1837,28 @@ export class Orchestrator {
1976
1837
  // announced: a refused run must fail the request loudly, not 200 a runId
1977
1838
  // and leave an orphaned run dir without a terminal event.
1978
1839
  const contract = this.buildContract(input, taskId, mode);
1979
- const store = this.artifactStore(input);
1980
- const paths = store.createRun(runId);
1981
- const log = new EventLog(paths.eventsPath, runId, taskId, input.onEvent, input.threadId);
1982
- input = this.withPlanBrief(input, store, paths, log);
1983
- // The execution root is the tree the harness mutates: the project itself for
1984
- // in-place threads/ordinary runs, or the thread's persistent worktree for an
1985
- // isolated thread. Config/artifacts/contract stay anchored to repoRoot. Both
1986
- // the WorkspaceManager and the git boundary resolve against this SINGLE root.
1987
- const execRoot = this.execRootOf(input);
1988
- const wsm = new WorkspaceManager(execRoot);
1989
- safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
1990
- log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
1991
- const ledger = this.rootLedger(input, contract, log);
1992
- announce?.({
1993
- log,
1994
- store,
1995
- paths,
1840
+ const planBrief = verifiedPlanBrief(input);
1841
+ const quotaSnapshots = this.quotaSnapshotPreflight();
1842
+ const { store, paths, log, ledger } = beginAnnouncedRun({
1843
+ input,
1844
+ contract,
1845
+ quotaSnapshots,
1846
+ store: this.artifactStore(input),
1847
+ authority: this.deps.delegationBudgetAuthority,
1996
1848
  runId,
1997
1849
  taskId,
1998
1850
  mode,
1999
1851
  phase: "race",
2000
- spend: () => ledger.spend(),
2001
- });
1852
+ prompt: input.prompt,
1853
+ }, announce);
1854
+ input = withPlanBrief(input, store, paths, log, planBrief);
1855
+ // The execution root is the tree the harness mutates: the project itself
1856
+ // for in-place threads/ordinary runs, or the thread's persistent worktree
1857
+ // for an isolated thread. Config/artifacts/contract stay anchored to
1858
+ // repoRoot. Both the WorkspaceManager and the git boundary resolve against
1859
+ // this SINGLE root.
1860
+ const execRoot = this.execRootOf(input);
1861
+ const wsm = new WorkspaceManager(execRoot);
2002
1862
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
2003
1863
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
2004
1864
  // Write modes need a git boundary for worktree isolation and honest diffs.
@@ -2007,17 +1867,17 @@ export class Orchestrator {
2007
1867
  // silent mutation (user-locked decision, comparator: Codex requires git).
2008
1868
  // For an isolated thread the execution root is already a git worktree, so
2009
1869
  // this is a no-op there; for in-place it ensures the live project is git.
2010
- const gitPreconditionError = await this.ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
1870
+ const gitPreconditionError = await ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
2011
1871
  if (gitPreconditionError) {
2012
1872
  return {
2013
1873
  runId,
2014
1874
  taskId,
2015
1875
  mode,
2016
1876
  lifecycle: "failed",
2017
- facts: makeOutcomeFacts("failed", { reason: "harness_failed" }),
1877
+ facts: makeOutcomeFacts("failed", { reason: gitPreconditionError.reason }),
2018
1878
  winner: null,
2019
1879
  runDir: paths.root,
2020
- summary: gitPreconditionError,
1880
+ summary: gitPreconditionError.message,
2021
1881
  candidates: [],
2022
1882
  };
2023
1883
  }
@@ -2087,6 +1947,7 @@ export class Orchestrator {
2087
1947
  candidates: [],
2088
1948
  };
2089
1949
  }
1950
+ activateDelegationParent(this.deps.delegationBudgetAuthority, input, runId, ledger, adapters, log);
2090
1951
  const reviewersOutcome = await this.resolveReviewersWithArtifacts(input, log, store, paths, runId, taskId, mode);
2091
1952
  if ("failed" in reviewersOutcome)
2092
1953
  return reviewersOutcome.failed;
@@ -2114,7 +1975,7 @@ export class Orchestrator {
2114
1975
  attemptId,
2115
1976
  intent: this.candidateIntent(input),
2116
1977
  harnessId: routed.adapter.id,
2117
- cost: attemptCostEvidence(routed.adapter.id, attemptId, i > 0 ? this.estimateUsdFloor(input.repoRoot) : undefined, this.routeBillingKnowledge(input, routed.adapter.id)),
1978
+ cost: attemptCostEvidence(routed.adapter.id, attemptId, this.reservationEstimateUsd(input, i > 0), this.routeBillingKnowledge(input, routed.adapter.id)),
2118
1979
  });
2119
1980
  log.emit("budget.lease.created", {
2120
1981
  granted: lease.granted,
@@ -2280,6 +2141,13 @@ export class Orchestrator {
2280
2141
  continuation_count: candidateContinuationCount,
2281
2142
  packet_turns: packet.continuity.disclosure.packetTurns,
2282
2143
  });
2144
+ log.emit("harness.started", {
2145
+ harness_id: adapter.id,
2146
+ attempt_id: contAttemptId,
2147
+ external_context_policy: knobs.webPolicy,
2148
+ ...(knobs.ignored.length > 0 ? { ignored_settings: knobs.ignored } : {}),
2149
+ continuation_of: run.attemptId,
2150
+ });
2283
2151
  const contLeaseId = contLease.lease?.lease_id ?? "";
2284
2152
  try {
2285
2153
  const contRun = await this.runCandidateInEnvelope(slot.routed, envelope, contAttemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
@@ -2311,7 +2179,7 @@ export class Orchestrator {
2311
2179
  effectiveRun = contRun;
2312
2180
  }
2313
2181
  catch (err) {
2314
- ledger.settle(contLeaseId, unknownCostSettlement("continuation-error", 0));
2182
+ ledger.settle(contLeaseId, AC.attemptFailureCost(err, "continuation-error", 0).settlement);
2315
2183
  log.emit("harness.completed", {
2316
2184
  harness_id: adapter.id,
2317
2185
  attempt_id: contAttemptId,
@@ -2335,18 +2203,9 @@ export class Orchestrator {
2335
2203
  envelope = undefined;
2336
2204
  }
2337
2205
  catch (err) {
2338
- // Envelope creation (or another pre-stream step) failed; stream errors
2339
- // are absorbed inside runCandidateInEnvelope with their real cost. A
2340
- // post-stream throw (e.g. the secret-token assertion) carries its
2341
- // streamed spend on the error — settle the TRUE cost, never launder
2342
- // real spend down to 0.
2343
- const carriedCost = typeof err?.costUsd === "number"
2344
- ? err.costUsd
2345
- : 0;
2346
- ledger.settle(slot.leaseId, unknownCostSettlement("post-stream-error", carriedCost));
2206
+ const failureCost = AC.attemptFailureCost(err, "post-stream-error", 0);
2207
+ ledger.settle(slot.leaseId, failureCost.settlement);
2347
2208
  const message = safeErrorMessage(err);
2348
- // envelope is still undefined when wsm.create() itself threw — that is
2349
- // a workspace-phase infrastructure failure, not a harness error.
2350
2209
  const infraPhase = envelope === undefined ? "workspace" : "harness";
2351
2210
  log.emit("harness.completed", {
2352
2211
  harness_id: adapter.id,
@@ -2355,26 +2214,22 @@ export class Orchestrator {
2355
2214
  error: message,
2356
2215
  phase: infraPhase,
2357
2216
  });
2358
- // Minimal attempt record so failure.yaml's rawDetailRef never dangles.
2359
- store.writeYaml(join(paths.attemptsDir, slot.attemptId, "attempt.yaml"), {
2360
- attempt_id: slot.attemptId,
2361
- harness_id: adapter.id,
2362
- cost_usd: carriedCost,
2363
- errored: true,
2364
- phase: infraPhase,
2365
- errors: [message],
2366
- });
2217
+ store.writeYaml(join(paths.attemptsDir, slot.attemptId, "attempt.yaml"), AC.attemptFailureRecord(slot.attemptId, adapter.id, failureCost, infraPhase, message));
2367
2218
  runsBySlot[slotIdx] = {
2368
2219
  attemptId: slot.attemptId,
2369
2220
  harnessId: adapter.id,
2370
2221
  label: slot.label,
2371
2222
  diff: "",
2372
2223
  gates: [],
2373
- cost: 0,
2224
+ cost: failureCost.totalUsd,
2374
2225
  errored: true,
2375
- costEstimated: false,
2226
+ costEstimated: failureCost.estimated,
2376
2227
  errors: [message],
2377
- telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [slot.routed.browserRequirement, slot.routed.denyRequirement], knobs.model),
2228
+ telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [
2229
+ slot.routed.browserRequirement,
2230
+ slot.routed.denyRequirement,
2231
+ slot.routed.delegationRequirement,
2232
+ ], knobs.model),
2378
2233
  infraPhase,
2379
2234
  };
2380
2235
  }
@@ -2397,7 +2252,7 @@ export class Orchestrator {
2397
2252
  // arbitration (as the race-adoption path does) would fold those user edits
2398
2253
  // into the revert target and let a later revert clobber them.
2399
2254
  let earlyPostTurnSha = null;
2400
- if (input.inPlace === true && requestedSingleCandidate) {
2255
+ if (input.inPlace && requestedSingleCandidate && runs.every((run) => !run.secretDiffRefusal)) {
2401
2256
  try {
2402
2257
  earlyPostTurnSha = await snapshotTree(execRoot);
2403
2258
  }
@@ -2409,6 +2264,20 @@ export class Orchestrator {
2409
2264
  await disposeReviewEnvelopes();
2410
2265
  return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null), ledger.spend(), input.signal, store);
2411
2266
  }
2267
+ const failedDelegation = delegateFailure.dominantRaceCandidateFailure(runs);
2268
+ if (failedDelegation) {
2269
+ const failure = delegateFailure.candidateFailureTerminal(failedDelegation, "race");
2270
+ await disposeReviewEnvelopes();
2271
+ await delegateFailure.persistFailedInPlaceWorkProduct({
2272
+ ...{ store, log, paths, execRoot, preTurnSha, taskId, mode },
2273
+ live: input.inPlace === true && failedDelegation.reviewCwd === execRoot,
2274
+ run: failedDelegation,
2275
+ postTurnSha: earlyPostTurnSha,
2276
+ kind: input.create === true ? "new_repo" : "patch",
2277
+ });
2278
+ this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null);
2279
+ return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), failure.metadata);
2280
+ }
2412
2281
  if (runs.length === 0) {
2413
2282
  const budgetReason = ledger.terminal();
2414
2283
  // QA-050: when the zero-candidate cause is a budget refusal, the shared
@@ -2428,12 +2297,7 @@ export class Orchestrator {
2428
2297
  why_winner: why,
2429
2298
  evidence_facts: ["no candidates were produced"],
2430
2299
  apply_recommendation: "continue",
2431
- budget_summary: {
2432
- spend_usd: ledger.spend(),
2433
- estimated: false,
2434
- cash_usd: ledger.spend(),
2435
- valuation_usd: ledger.valuation(),
2436
- },
2300
+ budget_summary: decisionBudgetSummary(ledger),
2437
2301
  });
2438
2302
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Phase: ${agentBudgetMapping ? "budget" : "executor"}\n\n${why}\n`);
2439
2303
  if (agentBudgetMapping) {
@@ -2480,7 +2344,7 @@ export class Orchestrator {
2480
2344
  if (workingRuns.length === 0) {
2481
2345
  await disposeReviewEnvelopes();
2482
2346
  const first = runs[0];
2483
- const phase = first.infraPhase ?? "harness";
2347
+ const phase = first.secretDiffRefusal ? "artifact_security" : (first.infraPhase ?? "harness");
2484
2348
  const { facts, why: rootCause } = partitionCandidates(runs);
2485
2349
  store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), {
2486
2350
  winner: null,
@@ -2488,12 +2352,7 @@ export class Orchestrator {
2488
2352
  why_winner: rootCause,
2489
2353
  evidence_facts: runs.map((r) => `${r.attemptId} produced no work: ${r.errors[0] ?? "unknown"}`),
2490
2354
  apply_recommendation: "continue",
2491
- budget_summary: {
2492
- spend_usd: ledger.spend(),
2493
- estimated: false,
2494
- cash_usd: ledger.spend(),
2495
- valuation_usd: ledger.valuation(),
2496
- },
2355
+ budget_summary: decisionBudgetSummary(ledger),
2497
2356
  });
2498
2357
  this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null);
2499
2358
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}\n- Phase: ${phase}\n\n${rootCause}\n`);
@@ -2513,9 +2372,11 @@ export class Orchestrator {
2513
2372
  rawDetailRef: `attempts/${first.attemptId}/attempt.yaml`,
2514
2373
  eventRefs: existingEventRefs,
2515
2374
  runDir: paths.root,
2516
- nextActions: phase === "workspace"
2517
- ? ["Check the project folder", "Open diagnostics", "Retry the run"]
2518
- : harnessFailureNextActions(harnessCategory),
2375
+ nextActions: first.secretDiffRefusal
2376
+ ? secretDiff.secretDiffNextActions(first.secretDiffRefusal)
2377
+ : phase === "workspace"
2378
+ ? ["Check the project folder", "Open diagnostics", "Retry the run"]
2379
+ : harnessFailureNextActions(harnessCategory),
2519
2380
  });
2520
2381
  log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
2521
2382
  log.emit("run.failed", {
@@ -2574,7 +2435,7 @@ export class Orchestrator {
2574
2435
  // candidates (a trivial greeting in agent mode no longer burns two reviewers on
2575
2436
  // "(empty diff)"). Candidates still flow through arbitration/gates so the
2576
2437
  // no_op/answer outcome and gate failures are unchanged.
2577
- evidences = await this.reviewRuns(workingRuns, reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal);
2438
+ evidences = await this.reviewRuns(workingRuns, reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input));
2578
2439
  }
2579
2440
  catch (err) {
2580
2441
  // Review preflight/evidence failures end TERMINALLY with artifacts —
@@ -2599,7 +2460,7 @@ export class Orchestrator {
2599
2460
  attemptId: "synth",
2600
2461
  intent: "synthesize",
2601
2462
  harnessId: synthRouted.adapter.id,
2602
- cost: attemptCostEvidence(synthRouted.adapter.id, "synth", undefined, this.routeBillingKnowledge(input, synthRouted.adapter.id)),
2463
+ cost: attemptCostEvidence(synthRouted.adapter.id, "synth", this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, synthRouted.adapter.id)),
2603
2464
  });
2604
2465
  if (lease.granted) {
2605
2466
  let envelope;
@@ -2635,7 +2496,7 @@ export class Orchestrator {
2635
2496
  runs.push(run);
2636
2497
  try {
2637
2498
  if (isWorkingCandidate(run)) {
2638
- const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal);
2499
+ const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input));
2639
2500
  evidences.push(...synthEvidence);
2640
2501
  workingRuns.push(run);
2641
2502
  }
@@ -2648,7 +2509,7 @@ export class Orchestrator {
2648
2509
  }
2649
2510
  }
2650
2511
  catch (err) {
2651
- ledger.settle(lease.lease?.lease_id ?? "", unknownCostSettlement("synthesis-error"));
2512
+ ledger.settle(lease.lease?.lease_id ?? "", AC.attemptFailureCost(err, "synthesis-error").settlement);
2652
2513
  log.emit("harness.completed", {
2653
2514
  attempt_id: "synth",
2654
2515
  status: "failed",
@@ -2666,14 +2527,7 @@ export class Orchestrator {
2666
2527
  }
2667
2528
  let result;
2668
2529
  try {
2669
- result = arbitrate(evidences, {
2670
- spendUsd: ledger.spend(),
2671
- estimatedSpend: runs.some((r) => r.costEstimated),
2672
- // QA-010b: carry the settled cash + subscription-valuation totals
2673
- // (reviewer panel included) onto the decision record.
2674
- cashUsd: ledger.spend(),
2675
- valuationUsd: ledger.valuation(),
2676
- });
2530
+ result = arbitrate(evidences, arbitrationBudgetOptions(ledger));
2677
2531
  }
2678
2532
  catch (err) {
2679
2533
  // Arbitration throws end terminally with artifacts, never as an orphan.
@@ -2691,8 +2545,12 @@ export class Orchestrator {
2691
2545
  // Winner can only be a candidate that actually produced work; corpses are
2692
2546
  // excluded from arbitration upstream and from the fallback here.
2693
2547
  const winnerRun = workingRuns.find((r) => r.attemptId === result.decision.winner) ?? workingRuns[0];
2694
- // A reviewer escalation to a human is a BLOCKED terminal, not a silent risk note.
2695
- const needsHuman = evidences.some((e) => e.findings.some((f) => f.severity === "NEEDS_HUMAN" && isBlocking(f)));
2548
+ const winnerEvidence = winnerRun
2549
+ ? evidences.find((e) => e.attemptId === winnerRun.attemptId)
2550
+ : undefined;
2551
+ // D9 winner-only NEEDS_HUMAN gate, fail-closed on a winner with no review
2552
+ // evidence record (see winnerNeedsHuman).
2553
+ const needsHuman = winnerNeedsHuman(winnerRun?.attemptId ?? null, evidences);
2696
2554
  // Run-level review_verified is the WINNER's verification: an
2697
2555
  // empty-diff loser's unverified route must not drag the shipped result's
2698
2556
  // flag false. No winner -> fall back to the all-candidates view.
@@ -2755,12 +2613,11 @@ export class Orchestrator {
2755
2613
  })) {
2756
2614
  log.emit("output.ready", { kind: "artifact", path });
2757
2615
  }
2758
- assertNoSecretLikeTokens("final patch diff", winnerRun.diff);
2616
+ secretDiff.assertNoSecretLikeTokens("final patch diff", winnerRun.diff);
2759
2617
  const patchSha256 = sha256(winnerRun.diff);
2760
2618
  store.writeText(join(paths.finalDir, "patch.diff"), winnerRun.diff);
2761
2619
  const wstats = diffStats(winnerRun.diff);
2762
2620
  const hasDiff = winnerRun.diff.trim().length > 0;
2763
- const winnerEvidence = evidences.find((e) => e.attemptId === winnerRun.attemptId);
2764
2621
  const blockers = winnerEvidence
2765
2622
  ? winnerEvidence.findings.filter((f) => isBlocking(f)).length
2766
2623
  : 0;
@@ -3073,7 +2930,7 @@ export class Orchestrator {
3073
2930
  env: reviewHome.env,
3074
2931
  }).finally(() => reviewHome.dispose());
3075
2932
  }
3076
- async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal) {
2933
+ async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal, reservationEstimateUsd) {
3077
2934
  const evidences = [];
3078
2935
  for (const run of runs) {
3079
2936
  const candidateCwd = run.reviewCwd ?? cwd;
@@ -3086,13 +2943,13 @@ export class Orchestrator {
3086
2943
  // (so a failing test gate or no_op outcome is unchanged), just unreviewed.
3087
2944
  const hasDiff = run.diff.trim().length > 0;
3088
2945
  // Reviewer panels spend real money: reserve before, settle the observed cost.
3089
- const reviewLease = hasDiff
2946
+ const reviewLease = hasDiff && reviewers.length > 0
3090
2947
  ? ledger?.reserve({
3091
2948
  taskId: taskId ?? "task",
3092
2949
  attemptId: run.attemptId,
3093
2950
  intent: "review",
3094
2951
  harnessId: "review-panel",
3095
- cost: attemptCostEvidence("review-panel", run.attemptId),
2952
+ cost: attemptCostEvidence("review-panel", run.attemptId, reservationEstimateUsd),
3096
2953
  })
3097
2954
  : undefined;
3098
2955
  const result = hasDiff && reviewers.length > 0 && (reviewLease?.granted ?? true)
@@ -3119,11 +2976,16 @@ export class Orchestrator {
3119
2976
  reviewSpendUsd: 0,
3120
2977
  reviewSpendEstimated: false,
3121
2978
  reviewCashUsd: 0,
2979
+ reviewCashKnowledge: "unknown",
3122
2980
  reviewValuationUsd: 0,
2981
+ reviewValuationKnowledge: "unknown",
3123
2982
  reviewUnknownUsd: 0,
3124
2983
  };
3125
2984
  if (reviewLease?.granted) {
3126
- ledger?.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(result.reviewCashUsd, result.reviewValuationUsd, result.reviewSpendEstimated, [`attempt:${run.attemptId}`, "review:panel"], result.reviewUnknownUsd));
2985
+ ledger?.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(result.reviewCashUsd, result.reviewValuationUsd, {
2986
+ cash: result.reviewCashKnowledge,
2987
+ valuation: result.reviewValuationKnowledge,
2988
+ }, [`attempt:${run.attemptId}`, "review:panel"], result.reviewUnknownUsd));
3127
2989
  if ((result.reviewSpendUsd ?? 0) > 0) {
3128
2990
  log.emit("budget.observation", {
3129
2991
  harness_id: "review-panel",
@@ -3231,10 +3093,21 @@ export class Orchestrator {
3231
3093
  const runId = input.runId ?? newId("run");
3232
3094
  // Contract validation BEFORE the run is announced (see runRace).
3233
3095
  const contract = this.buildContract(input, taskId, mode);
3234
- const store = this.artifactStore(input);
3235
- const paths = store.createRun(runId);
3236
- const log = new EventLog(paths.eventsPath, runId, taskId, input.onEvent, input.threadId);
3237
- input = this.withPlanBrief(input, store, paths, log);
3096
+ const planBrief = verifiedPlanBrief(input);
3097
+ const quotaSnapshots = this.quotaSnapshotPreflight();
3098
+ const { store, paths, log, ledger } = beginAnnouncedRun({
3099
+ input,
3100
+ contract,
3101
+ quotaSnapshots,
3102
+ store: this.artifactStore(input),
3103
+ authority: this.deps.delegationBudgetAuthority,
3104
+ runId,
3105
+ taskId,
3106
+ mode,
3107
+ phase: "convergence",
3108
+ prompt: input.prompt,
3109
+ }, announce);
3110
+ input = withPlanBrief(input, store, paths, log, planBrief);
3238
3111
  // The execution root is the tree the harness mutates (thread worktree for an
3239
3112
  // isolated thread, else the project). The WorkspaceManager AND the git
3240
3113
  // boundary must resolve against the SAME root — the race path does so via the
@@ -3243,24 +3116,11 @@ export class Orchestrator {
3243
3116
  const execRoot = this.execRootOf(input);
3244
3117
  const wsm = new WorkspaceManager(execRoot);
3245
3118
  const readiness = new ReadinessLedger();
3246
- const ledger = this.rootLedger(input, contract, log);
3247
3119
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
3248
- safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
3249
- log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
3250
- announce?.({
3251
- log,
3252
- store,
3253
- paths,
3254
- runId,
3255
- taskId,
3256
- mode,
3257
- phase: "convergence",
3258
- spend: () => ledger.spend(),
3259
- });
3260
3120
  // Live (in-place) isolation deliberately tolerates non-git stateful
3261
3121
  // environments; only envelope isolation needs the git boundary.
3262
3122
  if (!input.inPlace) {
3263
- const gitPreconditionError = await this.ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
3123
+ const gitPreconditionError = await ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
3264
3124
  if (gitPreconditionError) {
3265
3125
  return {
3266
3126
  spendUsd: ledger.spend(),
@@ -3268,10 +3128,10 @@ export class Orchestrator {
3268
3128
  taskId,
3269
3129
  mode,
3270
3130
  lifecycle: "failed",
3271
- facts: makeOutcomeFacts("failed", { reason: "harness_failed" }),
3131
+ facts: makeOutcomeFacts("failed", { reason: gitPreconditionError.reason }),
3272
3132
  winner: null,
3273
3133
  runDir: paths.root,
3274
- summary: gitPreconditionError,
3134
+ summary: gitPreconditionError.message,
3275
3135
  candidates: [],
3276
3136
  };
3277
3137
  }
@@ -3334,6 +3194,7 @@ export class Orchestrator {
3334
3194
  candidates: [],
3335
3195
  };
3336
3196
  }
3197
+ activateDelegationParent(this.deps.delegationBudgetAuthority, input, runId, ledger, adapterPool, log);
3337
3198
  // Fail fast on a provably unwinnable predicate instead of burning paid
3338
3199
  // rounds: the default convergence predicate requires a clean cross-family
3339
3200
  // review, which needs >=2 healthy reviewer provider families.
@@ -3405,7 +3266,6 @@ export class Orchestrator {
3405
3266
  const allCooledDown = () => adapterPool.every((a) => ledger.cooldownActive(a.adapter.id));
3406
3267
  const attemptTelemetries = [];
3407
3268
  let lastDiffStable = true;
3408
- let reviewSpendEstimated = false;
3409
3269
  try {
3410
3270
  // The contract's ENGINE-COMPUTED effective profile drives the envelope and
3411
3271
  // every attempt spec (parity with runRace); telemetry must never claim an
@@ -3460,7 +3320,7 @@ export class Orchestrator {
3460
3320
  attemptId,
3461
3321
  intent: "repair",
3462
3322
  harnessId: adapter.id,
3463
- cost: attemptCostEvidence(adapter.id, attemptId, undefined, this.routeBillingKnowledge(input, adapter.id)),
3323
+ cost: attemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, adapter.id)),
3464
3324
  });
3465
3325
  if (!lease.granted) {
3466
3326
  exhausted = true;
@@ -3495,30 +3355,48 @@ export class Orchestrator {
3495
3355
  });
3496
3356
  }
3497
3357
  catch (err) {
3498
- // Envelope/setup failure before the stream; stream errors are absorbed
3499
- // inside runCandidateInEnvelope with their real accumulated cost.
3500
- ledger.settle(lease.lease?.lease_id ?? "", unknownCostSettlement("attempt-error"));
3358
+ // Setup failures remain unknown; post-stream persistence failures
3359
+ // carry their route-specific settlement from runCandidateInEnvelope.
3360
+ const failureCost = AC.attemptFailureCost(err, "attempt-error");
3361
+ const message = safeErrorMessage(err);
3362
+ ledger.settle(lease.lease?.lease_id ?? "", failureCost.settlement);
3501
3363
  log.emit("harness.completed", {
3502
3364
  harness_id: adapter.id,
3503
3365
  attempt_id: attemptId,
3504
3366
  status: "failed",
3505
- error: safeErrorMessage(err),
3367
+ error: message,
3506
3368
  });
3369
+ store.writeYaml(join(paths.attemptsDir, attemptId, "attempt.yaml"), AC.attemptFailureRecord(attemptId, adapter.id, failureCost, "harness", message));
3507
3370
  run = {
3508
3371
  attemptId,
3509
3372
  harnessId: adapter.id,
3510
3373
  label: `Attempt ${attempt}`,
3511
3374
  diff: "",
3512
3375
  gates: [],
3513
- cost: 0,
3376
+ cost: failureCost.totalUsd,
3514
3377
  errored: true,
3515
- costEstimated: false,
3516
- errors: [safeErrorMessage(err)],
3517
- telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [routed.browserRequirement, routed.denyRequirement], knobs.model),
3378
+ costEstimated: failureCost.estimated,
3379
+ errors: [message],
3380
+ telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [routed.browserRequirement, routed.denyRequirement, routed.delegationRequirement], knobs.model),
3518
3381
  };
3519
3382
  }
3520
3383
  lastRun = run;
3521
3384
  attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry: run.telemetry });
3385
+ // Cancellation/deadline keeps priority over a belt failure finalized concurrently.
3386
+ if (input.signal?.aborted)
3387
+ break;
3388
+ if (delegateFailure.candidateFailureKind(run)) {
3389
+ const failure = delegateFailure.candidateFailureTerminal(run, "convergence");
3390
+ await delegateFailure.persistFailedInPlaceWorkProduct({
3391
+ ...{ store, log, paths, execRoot, preTurnSha, taskId, mode },
3392
+ live: input.inPlace === true,
3393
+ run,
3394
+ kind: input.create === true ? "new_repo" : "patch",
3395
+ attempts: attempt,
3396
+ });
3397
+ this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attemptTelemetries, null);
3398
+ return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), failure.metadata);
3399
+ }
3522
3400
  // D-16 r8: interrupted (errored===false) would CONVERGE a partial diff
3523
3401
  // as clean — break BEFORE review; a harness error still gate-retries.
3524
3402
  if (run.outcomeClass === "interrupted") {
@@ -3554,7 +3432,7 @@ export class Orchestrator {
3554
3432
  attemptId,
3555
3433
  intent: "review",
3556
3434
  harnessId: "review-panel",
3557
- cost: attemptCostEvidence("review-panel", attemptId),
3435
+ cost: attemptCostEvidence("review-panel", attemptId, this.reservationEstimateUsd(input)),
3558
3436
  })
3559
3437
  : null;
3560
3438
  const reviewResult = reviewers.length > 0 && (reviewLease?.granted ?? false)
@@ -3580,11 +3458,16 @@ export class Orchestrator {
3580
3458
  reviewSpendUsd: 0,
3581
3459
  reviewSpendEstimated: false,
3582
3460
  reviewCashUsd: 0,
3461
+ reviewCashKnowledge: "unknown",
3583
3462
  reviewValuationUsd: 0,
3463
+ reviewValuationKnowledge: "unknown",
3584
3464
  reviewUnknownUsd: 0,
3585
3465
  };
3586
3466
  if (reviewLease?.granted) {
3587
- ledger.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(reviewResult.reviewCashUsd, reviewResult.reviewValuationUsd, reviewResult.reviewSpendEstimated, [`attempt:${attemptId}`, "review:panel"], reviewResult.reviewUnknownUsd));
3467
+ ledger.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(reviewResult.reviewCashUsd, reviewResult.reviewValuationUsd, {
3468
+ cash: reviewResult.reviewCashKnowledge,
3469
+ valuation: reviewResult.reviewValuationKnowledge,
3470
+ }, [`attempt:${attemptId}`, "review:panel"], reviewResult.reviewUnknownUsd));
3588
3471
  if ((reviewResult.reviewSpendUsd ?? 0) > 0) {
3589
3472
  log.emit("budget.observation", {
3590
3473
  harness_id: "review-panel",
@@ -3596,8 +3479,6 @@ export class Orchestrator {
3596
3479
  unknown_usd: reviewResult.reviewUnknownUsd,
3597
3480
  estimated: reviewResult.reviewSpendEstimated === true,
3598
3481
  });
3599
- if (reviewResult.reviewSpendEstimated === true)
3600
- reviewSpendEstimated = true;
3601
3482
  }
3602
3483
  }
3603
3484
  else if (reviewLease && !reviewLease.granted) {
@@ -3779,13 +3660,7 @@ export class Orchestrator {
3779
3660
  if (lastRun && !interrupted) {
3780
3661
  const arb = arbitrate([
3781
3662
  toCandidateEvidence(lastRun, contract, lastFindings, lastFinalReviewClean, actualReviewVerified),
3782
- ], {
3783
- spendUsd: ledger.spend(),
3784
- estimatedSpend: lastRun.costEstimated || reviewSpendEstimated,
3785
- // QA-010b: settled cash + valuation (reviewer panel included).
3786
- cashUsd: ledger.spend(),
3787
- valuationUsd: ledger.valuation(),
3788
- });
3663
+ ], arbitrationBudgetOptions(ledger));
3789
3664
  decision = arb.decision;
3790
3665
  store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), decision);
3791
3666
  // A converged run adopts the arbitration axes (checks/review); an
@@ -3847,7 +3722,7 @@ export class Orchestrator {
3847
3722
  // work_product (its partial patch.diff stays diagnostic via attempts/);
3848
3723
  // in-place keeps the product so the honest Revert offer survives.
3849
3724
  if (lastRun && (!interrupted || input.inPlace === true)) {
3850
- assertNoSecretLikeTokens("final patch diff", lastRun.diff);
3725
+ secretDiff.assertNoSecretLikeTokens("final patch diff", lastRun.diff);
3851
3726
  const patchSha256 = sha256(lastRun.diff);
3852
3727
  store.writeText(join(paths.finalDir, "patch.diff"), lastRun.diff);
3853
3728
  // Honest apply-state (parity with runRace single-candidate in-place): a
@@ -4017,285 +3892,57 @@ export class Orchestrator {
4017
3892
  reviewVerified: actualReviewVerified,
4018
3893
  };
4019
3894
  }
4020
- /** plan mode: multi-harness planning -> aggregate -> (optional) plan review -> plan. Read-only. */
4021
- /**
4022
- * Wrap the user's goal in an explicit "plan, do not implement" instruction.
4023
- * Without this the raw prompt ("make a racing game") reaches the harness with
4024
- * only a read-only sandbox, so the model tries to BUILD it and dumps code into
4025
- * the plan when writes are blocked — the v0.9 "HTML in the plan" bug. The
4026
- * read-only access still enforces it; this gives the model the right job.
4027
- */
4028
- planPrompt(goal) {
4029
- return [
4030
- `You are planning, NOT implementing. Explore the repository read-only and produce a plan another agent will execute later. Do not write files or output full implementations.`,
4031
- ``,
4032
- `## Goal`,
4033
- goal,
4034
- ``,
4035
- `## Required output (markdown)`,
4036
- `1. Approach — 2-3 sentences on how you'd solve this.`,
4037
- `2. Steps — a numbered list; each step names the file(s) it touches and what changes.`,
4038
- `3. Risks & edge cases.`,
4039
- `4. End your response with a section titled exactly:`,
4040
- ``,
4041
- `## Open Questions`,
4042
- ``,
4043
- `List every decision the user must make before implementation, one per bullet, in EXACTLY this format:`,
4044
- ``,
4045
- `- [single] <question> :: <option A> :: <option B>`,
4046
- `- [multi] <question> :: <option A> :: <option B>`,
4047
- `- [text] <question that has no good fixed options>`,
4048
- ``,
4049
- `Rules: [single] = pick exactly one; [multi] = pick one or more; [text] = free-form (no "::" options). Ground every option in THIS repository. If nothing is ambiguous, write a single bullet: - (none)`,
4050
- ``,
4051
- `Keep it concise. Reference real paths you found. Do NOT paste large code blocks; describe the change instead.`,
4052
- ].join("\n");
3895
+ /** Bind private route/session preparation to the planner-attempt owner. */
3896
+ plannerAttemptDeps() {
3897
+ return {
3898
+ billingKnowledge: (input, harnessId) => this.routeBillingKnowledge(input, harnessId),
3899
+ inactivityTimeoutMs: (repoRoot) => harnessInactivityTimeoutMs(this.config(repoRoot)),
3900
+ quotaEventSink: this.deps.quotaEventSink,
3901
+ prepare: async (args) => {
3902
+ const { input, contract, taskId, runId, log, store, paths, routed, attemptId } = args;
3903
+ const adapter = routed.adapter;
3904
+ const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
3905
+ const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
3906
+ const sessionFields = await this.sessionSpecFields(input, adapter.id, log, routed.authRouteEstimate);
3907
+ const laneContinuity = args.laneRun
3908
+ ? 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)
3909
+ : null;
3910
+ const spec = HarnessRunSpec.parse({
3911
+ session_id: newId("ses"),
3912
+ intent: args.intent,
3913
+ prompt: laneContinuity?.pointerLine
3914
+ ? `${args.promptBody}\n\n${laneContinuity.pointerLine}`
3915
+ : args.promptBody,
3916
+ cwd: this.execRootOf(input),
3917
+ access: "readonly",
3918
+ attachments: input.attachments ?? [],
3919
+ ...sessionFields,
3920
+ ...this.harnessSpecKnobs(contract, knobs, args.intent),
3921
+ env_inheritance: envInheritance(this.config(input.repoRoot)),
3922
+ env: (args.laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? args.fallbackHome,
3923
+ });
3924
+ const plannerAbort = new AbortController();
3925
+ spec.extra["abortSignal"] = input.signal
3926
+ ? AbortSignal.any([input.signal, plannerAbort.signal])
3927
+ : plannerAbort.signal;
3928
+ const planInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
3929
+ if (planInteraction)
3930
+ spec.extra["interactionChannel"] = planInteraction;
3931
+ const planWorkMode = this.applyWorkEnvelope(spec, this.workReportEnvelopeFor(routed, contract, Boolean(planInteraction)));
3932
+ return {
3933
+ knobs,
3934
+ effectiveWeb,
3935
+ spec,
3936
+ plannerAbort,
3937
+ planInteraction,
3938
+ planWorkMode,
3939
+ };
3940
+ },
3941
+ };
4053
3942
  }
4054
3943
  /** One read-only planner spawn shared by solo fallback, Council drafts, and merge. */
4055
3944
  async runPlannerAttempt(args) {
4056
- const { input, contract, taskId, runId, log, store, paths, ledger, routed, attemptId } = args;
4057
- const adapter = routed.adapter;
4058
- const lease = ledger.reserve({
4059
- taskId,
4060
- attemptId,
4061
- intent: args.intent,
4062
- harnessId: adapter.id,
4063
- cost: attemptCostEvidence(adapter.id, attemptId, undefined, this.routeBillingKnowledge(input, adapter.id)),
4064
- });
4065
- if (!lease.granted) {
4066
- log.emit("budget.lease.created", {
4067
- granted: false,
4068
- reason: lease.reason,
4069
- denied: lease.denied,
4070
- attempt_id: attemptId,
4071
- harness_id: adapter.id,
4072
- });
4073
- return {
4074
- attemptId,
4075
- harnessId: adapter.id,
4076
- status: "failed",
4077
- outcomeClass: "clean", // never spawned: refused pre-flight by the budget gate
4078
- error: lease.reason ?? "budget lease denied",
4079
- text: null,
4080
- telemetry: null,
4081
- budgetDenied: true,
4082
- budgetDenial: {
4083
- code: lease.denied ?? "hard_cap",
4084
- reason: lease.reason ?? "budget lease denied",
4085
- harnessId: adapter.id,
4086
- attemptId,
4087
- },
4088
- };
4089
- }
4090
- const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
4091
- const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
4092
- const planSessionFields = this.sessionSpecFields(input, adapter.id, log);
4093
- // Continuity (INV-137): a thread PLAN turn is a chat turn — hydrate a
4094
- // lane switch/gap with a packet and disclose it.
4095
- const laneContinuity = args.laneRun
4096
- ? 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)
4097
- : null;
4098
- const spec = HarnessRunSpec.parse({
4099
- session_id: newId("ses"),
4100
- intent: args.intent,
4101
- prompt: laneContinuity?.pointerLine
4102
- ? `${args.promptBody}\n\n${laneContinuity.pointerLine}`
4103
- : args.promptBody,
4104
- cwd: this.execRootOf(input),
4105
- access: "readonly",
4106
- // Planners must SEE any image/file the user attached (e.g. "plan a fix for
4107
- // what's in this screenshot"), not just agent/race runs.
4108
- attachments: input.attachments ?? [],
4109
- ...planSessionFields,
4110
- ...this.harnessSpecKnobs(contract, knobs, args.intent),
4111
- env_inheritance: envInheritance(this.config(input.repoRoot)),
4112
- // A thread plan turn spawns in its DURABLE per-lane home so its native
4113
- // session is reachable for resume next turn (INV-034); a non-thread
4114
- // plan keeps the disposable route-context home.
4115
- env: (args.laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? args.fallbackHome,
4116
- });
4117
- const plannerAbort = new AbortController();
4118
- spec.extra["abortSignal"] = input.signal
4119
- ? AbortSignal.any([input.signal, plannerAbort.signal])
4120
- : plannerAbort.signal;
4121
- const planInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
4122
- if (planInteraction)
4123
- spec.extra["interactionChannel"] = planInteraction;
4124
- // D-16: compile the WorkReport envelope for the plan lane (require plan text
4125
- // below folds the deliverable; the veto rides work_state).
4126
- const planWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(planInteraction));
4127
- const planWorkMode = this.applyWorkEnvelope(spec, planWorkEnvelope);
4128
- const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
4129
- const answer = new AnswerAssembly();
4130
- const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
4131
- knobs.webPolicy === "cached" ||
4132
- knobs.webPolicy === "live", effectiveWeb, [],
4133
- // Requested-model capture: a plan lane silently downgraded to another
4134
- // model surfaces the mismatch in its route receipt, just like agent.
4135
- knobs.model);
4136
- const onAbort = () => {
4137
- void adapter.cancel?.(spec.session_id)?.catch(() => { });
4138
- };
4139
- if (input.signal) {
4140
- if (input.signal.aborted)
4141
- onAbort();
4142
- else
4143
- input.signal.addEventListener("abort", onAbort, { once: true });
4144
- }
4145
- let cost = 0;
4146
- let costEstimated = false;
4147
- let harnessError = null;
4148
- const budgetSignalState = { quotaPressureDisclosed: false };
4149
- try {
4150
- log.emit("harness.started", {
4151
- harness_id: adapter.id,
4152
- attempt_id: attemptId,
4153
- external_context_policy: knobs.webPolicy,
4154
- ...(knobs.ignored.length > 0 ? { ignored_settings: knobs.ignored } : {}),
4155
- });
4156
- if (!input.signal?.aborted) {
4157
- const watchedPlan = withInactivityWatchdog(adapter.run(spec), {
4158
- timeoutMs: harnessInactivityTimeoutMs(this.config(input.repoRoot)),
4159
- onTimeout: () => {
4160
- plannerAbort.abort();
4161
- void adapter.cancel?.(spec.session_id)?.catch(() => { });
4162
- },
4163
- isSuspended: () => (planInteraction?.pendingCount?.() ?? 0) > 0,
4164
- });
4165
- for await (const ev of watchedPlan) {
4166
- if (input.signal?.aborted)
4167
- break;
4168
- const safeEv = redactHarnessEvent(ev);
4169
- safeInvoke(input.onHarnessEvent, safeEv);
4170
- // A thread PLAN turn IS a chat turn now (INV-034): its native
4171
- // session lives in the DURABLE per-lane home, so record it for the
4172
- // next lane turn's resume. Council members are distinct lanes.
4173
- if (args.laneRun)
4174
- observeNativeSessionEvent(input, adapter.id, safeEv);
4175
- observeAuthSwitch(log, adapter.id, attemptId, safeEv);
4176
- log.emit("harness.event", harnessEventPayload(adapter.id, attemptId, safeEv));
4177
- appendLine(attemptEventsPath, JSON.stringify(safeEv));
4178
- observeAttemptTelemetry(telemetry, safeEv);
4179
- if (safeEv.plan_progress) {
4180
- log.emit("plan.progress", {
4181
- attempt_id: attemptId,
4182
- harness_id: adapter.id,
4183
- items: safeEv.plan_progress.items,
4184
- });
4185
- }
4186
- // read-only routes burn quota too — same single owner as the agent loop.
4187
- observeBudgetSignals(ledger, log, adapter.id, attemptId, safeEv, budgetSignalState);
4188
- this.deps.quotaEventSink?.(adapter.id, safeEv);
4189
- if (safeEv.type === "usage" && safeEv.usage?.cost_usd) {
4190
- cost += safeEv.usage.cost_usd;
4191
- if (safeEv.usage.estimated)
4192
- costEstimated = true;
4193
- log.emit("budget.observation", {
4194
- harness_id: adapter.id,
4195
- attempt_id: attemptId,
4196
- kind: "spend",
4197
- usd: safeEv.usage.cost_usd,
4198
- estimated: safeEv.usage.estimated === true,
4199
- });
4200
- }
4201
- // A TYPED final message wins verbatim over joined narration.
4202
- answer.observe(safeEv);
4203
- if (safeEv.type === "error")
4204
- harnessError = safeEv.error ? redactSecrets(safeEv.error) : "harness emitted an error";
4205
- }
4206
- }
4207
- }
4208
- catch (err) {
4209
- harnessError = safeErrorMessage(err);
4210
- }
4211
- finally {
4212
- input.signal?.removeEventListener("abort", onAbort);
4213
- ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost));
4214
- }
4215
- // D-16: unwrap and require PLAN TEXT — a plan with no text is not delivered.
4216
- // The unwrap runs BEFORE the error axes: the deliverable it yields is what
4217
- // decides whether an unrecovered tool error is fatal (explorer parity).
4218
- const planUnwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", planWorkMode, {
4219
- sideToolReport: telemetry.sideToolWorkReport ?? undefined,
4220
- });
4221
- const planText = redactSecrets(planUnwrapped.deliverable).trim();
4222
- const unrecovered = unrecoveredToolErrors(telemetry);
4223
- const webBlocked = webUnsatisfied(telemetry);
4224
- if (!harnessError && webBlocked) {
4225
- harnessError = webEvidenceFailure(telemetry.web);
4226
- }
4227
- // INV-043/INV-044, explorer parity: a DELIVERED plan keeps an unrecovered
4228
- // non-web tool error as warning evidence instead of discarding the plan (see
4229
- // the helper). Web keeps its hard gate above; the finalizer outranks both.
4230
- harnessError ??= unrecoveredToolErrorFailure(unrecovered, planText.length > 0);
4231
- const planFinalized = finalizeAttempt({
4232
- deliverableEvidence: planText.length > 0,
4233
- harnessErrored: harnessError !== null && !webBlocked,
4234
- workReport: planUnwrapped.workReport,
4235
- workReportSource: planUnwrapped.source,
4236
- workReportViolation: planUnwrapped.contractViolation,
4237
- contextTerminalExhausted: telemetry.contextExhausted,
4238
- });
4239
- // A broken WorkReport contract is a hard failure only when the finalizer
4240
- // ranked it so (a terminal context exhaustion outranks it).
4241
- if (!harnessError && planFinalized.outcomeClass === "contract_failure") {
4242
- harnessError = `work_report contract: ${planUnwrapped.contractViolation}`;
4243
- }
4244
- // D-16 r9: an interrupted (context-exhausted) planner is NEVER a clean
4245
- // plan — partial text must not become final/plan.md as success. A VETO
4246
- // (needs_input/incomplete work_state) is DIFFERENT by the sealed D-16
4247
- // contract (X35, INV-116 canaries): the plan still delivers, lifecycle
4248
- // succeeded, and the work_state veto rides the OUTCOME (non-zero exit) —
4249
- // it must not be laundered into a harness failure either direction.
4250
- if (!harnessError && planFinalized.outcomeClass === "interrupted") {
4251
- harnessError = "context capacity exhausted before the plan completed";
4252
- }
4253
- const attemptError = harnessError ??
4254
- (planFinalized.deliverablePresent ? null : "planner produced no plan text") ??
4255
- (input.signal?.aborted ? "planner cancelled" : null);
4256
- setAttemptOutcome(telemetry, {
4257
- deliverablePresent: planFinalized.deliverablePresent,
4258
- gatesPassed: null,
4259
- harnessErrored: (harnessError !== null && !webBlocked) || planFinalized.harnessErrored,
4260
- webRequiredUnsatisfied: webBlocked,
4261
- workState: planFinalized.workState,
4262
- });
4263
- if (attemptError) {
4264
- log.emit("harness.completed", {
4265
- harness_id: adapter.id,
4266
- attempt_id: attemptId,
4267
- status: webBlocked ? "blocked" : "failed",
4268
- error: attemptError,
4269
- ...telemetrySummary(telemetry),
4270
- });
4271
- return {
4272
- attemptId,
4273
- harnessId: adapter.id,
4274
- status: webBlocked ? "blocked" : "failed",
4275
- outcomeClass: planFinalized.outcomeClass,
4276
- error: attemptError,
4277
- text: null,
4278
- telemetry,
4279
- budgetDenied: false,
4280
- };
4281
- }
4282
- const text = planText || "(no output)";
4283
- log.emit("harness.completed", {
4284
- harness_id: adapter.id,
4285
- attempt_id: attemptId,
4286
- status: "success",
4287
- ...telemetrySummary(telemetry),
4288
- });
4289
- return {
4290
- attemptId,
4291
- harnessId: adapter.id,
4292
- status: "success",
4293
- outcomeClass: planFinalized.outcomeClass,
4294
- error: null,
4295
- text,
4296
- telemetry,
4297
- budgetDenied: false,
4298
- };
3945
+ return executePlannerAttempt(this.plannerAttemptDeps(), args);
4299
3946
  }
4300
3947
  async runPlan(input, announce) {
4301
3948
  const taskId = input.taskId ?? newId("task");
@@ -4303,22 +3950,19 @@ export class Orchestrator {
4303
3950
  // Plan runs get the same immutable contract truth as every other mode;
4304
3951
  // contract validation runs BEFORE the run is announced (see runRace).
4305
3952
  const contract = this.buildContract(input, taskId, "plan");
4306
- const store = this.artifactStore(input);
4307
- const paths = store.createRun(runId);
4308
- const log = new EventLog(paths.eventsPath, runId, taskId, input.onEvent, input.threadId);
4309
- safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
4310
- log.emit("run.created", { mode: "plan", prompt: redactSecrets(input.prompt) });
4311
- const ledger = this.rootLedger(input, contract, log);
4312
- announce?.({
4313
- log,
4314
- store,
4315
- paths,
3953
+ const quotaSnapshots = this.quotaSnapshotPreflight();
3954
+ const { store, paths, log, ledger } = beginAnnouncedRun({
3955
+ input,
3956
+ contract,
3957
+ quotaSnapshots,
3958
+ store: this.artifactStore(input),
3959
+ authority: this.deps.delegationBudgetAuthority,
4316
3960
  runId,
4317
3961
  taskId,
4318
3962
  mode: "plan",
4319
3963
  phase: "plan",
4320
- spend: () => ledger.spend(),
4321
- });
3964
+ prompt: input.prompt,
3965
+ }, announce);
4322
3966
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
4323
3967
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
4324
3968
  // W3.3: ONE resolved read-only context — the routing point-probe and every
@@ -4426,6 +4070,7 @@ export class Orchestrator {
4426
4070
  roHome,
4427
4071
  contextSection,
4428
4072
  laneRun,
4073
+ estimateUsdFloor: this.estimateUsdFloor(input.repoRoot),
4429
4074
  });
4430
4075
  }
4431
4076
  const plans = [];
@@ -4453,8 +4098,9 @@ export class Orchestrator {
4453
4098
  attemptId,
4454
4099
  laneRun,
4455
4100
  fallbackHome: roHome.env,
4456
- promptBody: this.planPrompt(input.prompt) + contextSection,
4101
+ promptBody: planPrompt(input.prompt) + contextSection,
4457
4102
  intent: "plan",
4103
+ reservationEstimateUsd: this.reservationEstimateUsd(input),
4458
4104
  });
4459
4105
  if (outcome.budgetDenied) {
4460
4106
  // QA-050: retain the denied planner slot before breaking so the
@@ -4533,7 +4179,6 @@ export class Orchestrator {
4533
4179
  }
4534
4180
  if (plans.length === 0) {
4535
4181
  return writePlanHarnessFailure(this.planRunDeps(), {
4536
- input,
4537
4182
  contract,
4538
4183
  taskId,
4539
4184
  runId,
@@ -4576,7 +4221,7 @@ export class Orchestrator {
4576
4221
  runPlannerAttempt: (a) => this.runPlannerAttempt(a),
4577
4222
  writeRunTelemetry: (store, paths, contract, runId, taskId, mode, attempts, finalAttemptId) => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attempts, finalAttemptId),
4578
4223
  execRootOf: (input) => this.execRootOf(input),
4579
- planPrompt: (goal) => this.planPrompt(goal),
4224
+ planPrompt,
4580
4225
  };
4581
4226
  }
4582
4227
  /** ask: one selected harness answers read-only questions; no patch/apply controls. */
@@ -4602,24 +4247,8 @@ export class Orchestrator {
4602
4247
  defaultPrompt: "Explore this repository and synthesize evidence-cited findings, omissions, and follow-up questions.",
4603
4248
  }, announce);
4604
4249
  }
4605
- resolvePaidBudget(inputBudget, cfg) {
4606
- return inputBudget ?? this.deps.paidBudget ?? cfg.global.budget.paid_budget_per_run;
4607
- }
4608
- rootLedger(_input, contract, log) {
4609
- // The root ledger discloses into THIS run's log: the ledger is the one
4610
- // owner of the cash fact (subscription-entitled work settles to 0 there),
4611
- // and the UI renders `budget.cash` verbatim — never inferring money from
4612
- // route labels (W4.3 sol #15).
4613
- const ledger = new BudgetLedger(contract.budget.paid_budget, undefined, {
4614
- onCashSettled: (cashSpendUsd, valuationUsd) => log.emit("budget.cash", {
4615
- cash_spend_usd: cashSpendUsd,
4616
- valuation_usd: valuationUsd,
4617
- }),
4618
- });
4619
- for (const snapshot of this.deps.quotaSnapshots?.() ?? []) {
4620
- ledger.observeQuotaSnapshot(snapshot);
4621
- }
4622
- return ledger;
4250
+ quotaSnapshotPreflight() {
4251
+ return [...(this.deps.quotaSnapshots?.() ?? [])].map((snapshot) => QuotaSnapshotSchema.parse(snapshot));
4623
4252
  }
4624
4253
  routeBillingKnowledge(input, harnessId) {
4625
4254
  // A selected profile's credential_kind decides billing (round-18 #2).
@@ -4649,10 +4278,10 @@ export class Orchestrator {
4649
4278
  // scout reserve) so a subscription route is not refused for lacking a
4650
4279
  // cash quote.
4651
4280
  attemptCostEvidence(harnessId, attemptId, this.estimateUsdFloor(input.repoRoot), this.routeBillingKnowledge(input, harnessId)),
4652
- buildSpec: (routed, homeEnv, prompt, attemptId) => {
4281
+ buildSpec: async (routed, homeEnv, prompt, attemptId) => {
4653
4282
  const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
4654
4283
  const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
4655
- const sessionFields = this.sessionSpecFields(input, routed.adapter.id, log);
4284
+ const sessionFields = await this.sessionSpecFields(input, routed.adapter.id, log, routed.authRouteEstimate);
4656
4285
  const spec = HarnessRunSpec.parse({
4657
4286
  session_id: newId("ses"),
4658
4287
  intent: "synthesize",
@@ -4693,22 +4322,19 @@ export class Orchestrator {
4693
4322
  // Contract validation BEFORE the run is announced (see runRace). The
4694
4323
  // recorded user intent is the CALLER's goal.
4695
4324
  const contract = this.buildContract({ ...input, prompt: opts.contractIntent ?? prompt }, taskId, opts.mode);
4696
- const store = this.artifactStore(input);
4697
- const paths = store.createRun(runId);
4698
- const log = new EventLog(paths.eventsPath, runId, taskId, input.onEvent, input.threadId);
4699
- safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
4700
- log.emit("run.created", { mode: opts.mode, prompt: redactSecrets(prompt) });
4701
- const ledger = this.rootLedger(input, contract, log);
4702
- announce?.({
4703
- log,
4704
- store,
4705
- paths,
4325
+ const quotaSnapshots = this.quotaSnapshotPreflight();
4326
+ const { store, paths, log, ledger } = beginAnnouncedRun({
4327
+ input,
4328
+ contract,
4329
+ quotaSnapshots,
4330
+ store: this.artifactStore(input),
4331
+ authority: this.deps.delegationBudgetAuthority,
4706
4332
  runId,
4707
4333
  taskId,
4708
4334
  mode: opts.mode,
4709
4335
  phase: "report",
4710
- spend: () => ledger.spend(),
4711
- });
4336
+ prompt,
4337
+ }, announce);
4712
4338
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
4713
4339
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
4714
4340
  // The ask/deep-scan report stays bare (its scouts read the tree themselves);
@@ -4850,10 +4476,10 @@ export class Orchestrator {
4850
4476
  intent: opts.intent,
4851
4477
  harnessId: adapter.id,
4852
4478
  // QA-019: an n>1 deep-scan scout admits under a FINITE estimate floor
4853
- // (mirror of the candidate loop): the first scout reserves without a
4854
- // floor, but later scouts pass the repo's usd floor so a subscription
4855
- // swarm is not refused for lacking a per-attempt cash quote under a cap.
4856
- cost: attemptCostEvidence(adapter.id, attemptId, opts.deepScan && idx > 0 ? this.estimateUsdFloor(input.repoRoot) : undefined, this.routeBillingKnowledge(input, adapter.id)),
4479
+ // (mirror of the candidate loop): the first top-level scout reserves
4480
+ // without a floor; later scouts and every real Delegate child pass the
4481
+ // repo floor because they overlap an existing family unit.
4482
+ cost: attemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input, opts.deepScan && idx > 0), this.routeBillingKnowledge(input, adapter.id)),
4857
4483
  });
4858
4484
  if (!lease.granted) {
4859
4485
  log.emit("budget.lease.created", {
@@ -4882,67 +4508,120 @@ export class Orchestrator {
4882
4508
  }
4883
4509
  return { status: "budget_denied", reason: lease.reason ?? "budget lease denied" };
4884
4510
  }
4885
- // Lease granted: the attempt is now committed to run — disclose the launch.
4886
- onLaunch?.();
4887
- const knobs = this.routeSpecKnobs(routed, contract, modelOverride, input.effort);
4888
- const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
4889
- const explorerPrompt = (opts.deepScan
4890
- ? `${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.`
4891
- : prompt) + contextSection;
4892
- const sessionFields = this.sessionSpecFields(input, adapter.id, log);
4893
- const grantResume = sessionFields.resume_session_id !== null && !resumeGranted.has(adapter.id);
4894
- if (grantResume)
4895
- resumeGranted.add(adapter.id);
4896
- // Continuity (INV-137): a thread ASK turn is a chat turn — hydrate a lane
4897
- // switch/gap with a packet and disclose it. Gated on laneRun (deep-scan
4898
- // scouts are excluded from laneRun); native resume is available only when
4899
- // this slot was granted the lane's recorded session.
4900
- const laneContinuity = laneRun
4901
- ? await this.resolveContinuity(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null, grantResume, store, paths, this.execRootOf(input), log)
4902
- : null;
4903
- // D-16d: the continuation packet pointer rides after the lane pointer so
4904
- // the fresh session is re-grounded in the exhausted attempt's work.
4905
- const promptWithPointers = [explorerPrompt, laneContinuity?.pointerLine, continuationPointer]
4906
- .filter((p) => Boolean(p))
4907
- .join("\n\n");
4908
- let spec = HarnessRunSpec.parse({
4909
- session_id: newId("ses"),
4910
- intent: opts.intent,
4911
- prompt: promptWithPointers,
4912
- cwd: this.execRootOf(input),
4913
- access: "readonly",
4914
- // ASK/EXPLORE/AUDIT read-only runs must forward the user's attachments —
4915
- // a live "describe this image" turn sent an image that was being dropped here, so
4916
- // the model honestly reported it saw nothing (the v0.13 attachment bug).
4917
- attachments: input.attachments ?? [],
4918
- auth_preference: sessionFields.auth_preference,
4919
- credential_profile: sessionFields.credential_profile,
4920
- resume_session_id: grantResume ? sessionFields.resume_session_id : null,
4921
- ...this.harnessSpecKnobs(contract, knobs, opts.intent),
4922
- env_inheritance: envInheritance(this.config(input.repoRoot)),
4923
- // A thread lane turn spawns in its DURABLE per-lane home so the native
4924
- // session it records is reachable for resume next turn; everything else
4925
- // uses the disposable route-context home.
4926
- env: (laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? roHome.env,
4927
- });
4928
- const reportAbort = new AbortController();
4929
- spec.extra["abortSignal"] = input.signal
4930
- ? AbortSignal.any([input.signal, reportAbort.signal])
4931
- : reportAbort.signal;
4932
- const reportInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
4933
- if (reportInteraction)
4934
- spec.extra["interactionChannel"] = reportInteraction;
4935
- // D-16: compile the WorkReport envelope for the read-only lane.
4936
- const readonlyWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(reportInteraction));
4937
- const readonlyWorkMode = this.applyWorkEnvelope(spec, readonlyWorkEnvelope);
4938
- const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
4939
- const answer = new AnswerAssembly();
4940
- const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
4941
- knobs.webPolicy === "cached" ||
4942
- knobs.webPolicy === "live", effectiveWeb, [],
4943
- // Requested-model capture so ask/audit route receipts detect a silent
4944
- // model downgrade (typed model_mismatch), not just agent runs.
4945
- knobs.model);
4511
+ // As with planners, the granted lease owns profile/continuity/spec
4512
+ // preparation. Contain a pre-stream rejection as this attempt's failure;
4513
+ // parallel siblings can then finish before the shared HOME is disposed.
4514
+ const preparation = await (async () => {
4515
+ // Lease granted: the attempt is now committed to run — disclose the launch.
4516
+ onLaunch?.();
4517
+ const knobs = this.routeSpecKnobs(routed, contract, modelOverride, input.effort);
4518
+ const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
4519
+ const explorerPrompt = (opts.deepScan
4520
+ ? `${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.`
4521
+ : prompt) + contextSection;
4522
+ const sessionFields = await this.sessionSpecFields(input, adapter.id, log, routed.authRouteEstimate);
4523
+ const grantResume = sessionFields.resume_session_id !== null && !resumeGranted.has(adapter.id);
4524
+ if (grantResume)
4525
+ resumeGranted.add(adapter.id);
4526
+ // Continuity (INV-137): a thread ASK turn is a chat turn — hydrate a lane
4527
+ // switch/gap with a packet and disclose it. Gated on laneRun (deep-scan
4528
+ // scouts are excluded from laneRun); native resume is available only when
4529
+ // this slot was granted the lane's recorded session.
4530
+ const laneContinuity = laneRun
4531
+ ? await this.resolveContinuity(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null, grantResume, sessionFields, store, paths, this.execRootOf(input), log)
4532
+ : null;
4533
+ // D-16d: the continuation packet pointer rides after the lane pointer so
4534
+ // the fresh session is re-grounded in the exhausted attempt's work.
4535
+ const promptWithPointers = [
4536
+ explorerPrompt,
4537
+ laneContinuity?.pointerLine,
4538
+ continuationPointer,
4539
+ ]
4540
+ .filter((p) => Boolean(p))
4541
+ .join("\n\n");
4542
+ const spec = HarnessRunSpec.parse({
4543
+ session_id: newId("ses"),
4544
+ intent: opts.intent,
4545
+ prompt: promptWithPointers,
4546
+ cwd: this.execRootOf(input),
4547
+ access: "readonly",
4548
+ // ASK/EXPLORE/AUDIT read-only runs must forward the user's attachments —
4549
+ // a live "describe this image" turn sent an image that was being dropped here, so
4550
+ // the model honestly reported it saw nothing (the v0.13 attachment bug).
4551
+ attachments: input.attachments ?? [],
4552
+ auth_preference: sessionFields.auth_preference,
4553
+ credential_profile: sessionFields.credential_profile,
4554
+ resume_session_id: grantResume ? sessionFields.resume_session_id : null,
4555
+ ...this.harnessSpecKnobs(contract, knobs, opts.intent),
4556
+ env_inheritance: envInheritance(this.config(input.repoRoot)),
4557
+ // A thread lane turn spawns in its DURABLE per-lane home so the native
4558
+ // session it records is reachable for resume next turn; everything else
4559
+ // uses the disposable route-context home.
4560
+ env: (laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? roHome.env,
4561
+ });
4562
+ const reportAbort = new AbortController();
4563
+ spec.extra["abortSignal"] = input.signal
4564
+ ? AbortSignal.any([input.signal, reportAbort.signal])
4565
+ : reportAbort.signal;
4566
+ const reportInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
4567
+ if (reportInteraction)
4568
+ spec.extra["interactionChannel"] = reportInteraction;
4569
+ // D-16: compile the WorkReport envelope for the read-only lane.
4570
+ const readonlyWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(reportInteraction));
4571
+ const readonlyWorkMode = this.applyWorkEnvelope(spec, readonlyWorkEnvelope);
4572
+ const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
4573
+ const answer = new AnswerAssembly();
4574
+ const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
4575
+ knobs.webPolicy === "cached" ||
4576
+ knobs.webPolicy === "live", effectiveWeb, [],
4577
+ // Requested-model capture so ask/audit route receipts detect a silent
4578
+ // model downgrade (typed model_mismatch), not just agent runs.
4579
+ knobs.model);
4580
+ return {
4581
+ knobs,
4582
+ spec,
4583
+ reportAbort,
4584
+ reportInteraction,
4585
+ readonlyWorkMode,
4586
+ attemptEventsPath,
4587
+ answer,
4588
+ telemetry,
4589
+ };
4590
+ })().then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
4591
+ if (!preparation.ok) {
4592
+ const message = `read-only attempt setup failed: ${safeErrorMessage(preparation.error)}`;
4593
+ AC.settleGrantedAttemptLease({
4594
+ ledger,
4595
+ leaseId: lease.lease?.lease_id ?? "",
4596
+ attemptId,
4597
+ harnessId: adapter.id,
4598
+ costUsd: 0,
4599
+ costEstimated: false,
4600
+ preStreamFailureSource: "readonly-pre-stream",
4601
+ });
4602
+ const telemetry = createAttemptTelemetry(contract.external_context.policy, contract.external_context.web_required, contract.external_context.effective_mode);
4603
+ setAttemptOutcome(telemetry, {
4604
+ deliverablePresent: false,
4605
+ gatesPassed: null,
4606
+ harnessErrored: true,
4607
+ webRequiredUnsatisfied: false,
4608
+ });
4609
+ attempts.push({
4610
+ attemptId,
4611
+ harnessId: adapter.id,
4612
+ status: "failed",
4613
+ report: "",
4614
+ error: message,
4615
+ telemetry,
4616
+ });
4617
+ attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
4618
+ if (opts.deepScan) {
4619
+ store.writeText(join(paths.findingsDir, `${attemptId}-error.md`), `# Explorer ${attemptId} failed\n\n${message}\n`);
4620
+ }
4621
+ return { status: "launched" };
4622
+ }
4623
+ const { knobs, spec: preparedSpec, reportAbort, reportInteraction, readonlyWorkMode, attemptEventsPath, answer, telemetry, } = preparation.value;
4624
+ let spec = preparedSpec;
4946
4625
  const retryPolicy = transientRetryPolicy(this.config(input.repoRoot));
4947
4626
  let activeSessionId = spec.session_id;
4948
4627
  const onAbort = () => {
@@ -4982,11 +4661,13 @@ export class Orchestrator {
4982
4661
  try {
4983
4662
  const watchedReport = withInactivityWatchdog(adapter.run(runSpec), {
4984
4663
  timeoutMs: harnessInactivityTimeoutMs(this.config(input.repoRoot)),
4664
+ countsAsProgress: countsAsAgentProgress,
4985
4665
  onTimeout: () => {
4986
4666
  reportAbort.abort();
4987
4667
  void adapter.cancel?.(activeSessionId)?.catch(() => { });
4988
4668
  },
4989
4669
  isSuspended: () => (reportInteraction?.pendingCount?.() ?? 0) > 0,
4670
+ suspensionVersion: () => reportInteraction?.suspensionVersion?.() ?? 0,
4990
4671
  });
4991
4672
  for await (const ev of watchedReport) {
4992
4673
  if (input.signal?.aborted)
@@ -5050,13 +4731,18 @@ export class Orchestrator {
5050
4731
  // W5.4 reactive failover, READ-ONLY lane (same contract as the
5051
4732
  // candidate lane; typed limits only, never plain transients).
5052
4733
  if (harnessError && !input.signal?.aborted) {
4734
+ const rotationPolicy = this.profilePolicy(input.repoRoot, adapter.id);
4735
+ const readyProfileIds = sawTypedLimit && reportSoFar.length === 0 && rotationPolicy.limit_action === "rotate"
4736
+ ? await this.readyProfileIdsForRotation(input, adapter.id, spec.credential_profile ?? null, triedProfiles)
4737
+ : new Set();
5053
4738
  const rotated = rotateSpecOnTypedLimit({
5054
4739
  spec,
5055
4740
  harnessId: adapter.id,
5056
4741
  attemptId,
5057
- policy: this.profilePolicy(input.repoRoot, adapter.id),
4742
+ policy: rotationPolicy,
5058
4743
  registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
5059
4744
  snapshots: this.deps.quotaSnapshots?.() ?? [],
4745
+ readyProfileIds,
5060
4746
  triedProfiles,
5061
4747
  sawTypedLimit,
5062
4748
  deliverableEmpty: reportSoFar.length === 0,
@@ -5098,7 +4784,17 @@ export class Orchestrator {
5098
4784
  }
5099
4785
  finally {
5100
4786
  input.signal?.removeEventListener("abort", onAbort);
5101
- ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost));
4787
+ AC.settleGrantedAttemptLease({
4788
+ ledger,
4789
+ leaseId: lease.lease?.lease_id ?? "",
4790
+ attemptId,
4791
+ harnessId: adapter.id,
4792
+ costUsd: cost,
4793
+ costEstimated,
4794
+ authMode: telemetry.authMode,
4795
+ usageCost: telemetry.usageCost,
4796
+ preStreamFailureSource: "readonly-pre-stream",
4797
+ });
5102
4798
  }
5103
4799
  if (harnessError && telemetry.transientFailures.length > 0) {
5104
4800
  log.emit("route.transient.exhausted", {
@@ -5109,11 +4805,14 @@ export class Orchestrator {
5109
4805
  });
5110
4806
  }
5111
4807
  attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
5112
- // D-16: un-nest the {work_report, output} envelope; the OUTPUT is the report.
4808
+ // D-16: remove the WorkReport transport; the deliverable is the report.
5113
4809
  const roUnwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", readonlyWorkMode, {
5114
4810
  sideToolReport: telemetry.sideToolWorkReport ?? undefined,
5115
4811
  });
5116
- const report = redactSecrets(roUnwrapped.deliverable);
4812
+ // Trim symmetrically with the plan path: a whitespace-only answer is not
4813
+ // a delivered report (the final-artifact wrapper heading would otherwise
4814
+ // make it read as present content by construction).
4815
+ const report = redactSecrets(roUnwrapped.deliverable).trim();
5117
4816
  const unrecovered = unrecoveredToolErrors(telemetry);
5118
4817
  const webBlocked = webUnsatisfied(telemetry);
5119
4818
  const reportPresent = report.length > 0;
@@ -5141,6 +4840,13 @@ export class Orchestrator {
5141
4840
  harnessErrored: harnessError !== null && !webBlocked,
5142
4841
  webRequiredUnsatisfied: webBlocked,
5143
4842
  workState: roFinalized.workState,
4843
+ // A read-only attempt that completed CLEANLY with an honestly empty
4844
+ // answer is a success with deliverable_present=false ("(no output)"),
4845
+ // never a fake contract failure — the trim above must not convert a
4846
+ // phantom deliverable into a phantom harness failure. Only the clean
4847
+ // finalizer class qualifies: contract failures, vetoes, and context
4848
+ // interruptions keep the strict deliverable requirement.
4849
+ emptyDeliverableAllowed: roFinalized.outcomeClass === "clean",
5144
4850
  });
5145
4851
  if (harnessError) {
5146
4852
  log.emit("harness.completed", {
@@ -5571,7 +5277,7 @@ export class Orchestrator {
5571
5277
  }
5572
5278
  this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, opts.deepScan ? null : (succeeded[0]?.attemptId ?? null), deepScanSynthesis);
5573
5279
  log.emit("output.ready", {
5574
- kind: opts.mode === "ask" ? "answer" : "report",
5280
+ kind: opts.deepScan ? "report" : "answer",
5575
5281
  path: `final/${opts.artifactName}`,
5576
5282
  });
5577
5283
  if (opts.deepScan) {
@@ -5629,11 +5335,16 @@ export class Orchestrator {
5629
5335
  .map((a) => `${a.attemptId}:${a.harnessId}:${a.status}`)
5630
5336
  .join(", ");
5631
5337
  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`);
5338
+ const reportProducerAttemptId = opts.deepScan &&
5339
+ deepScanSynthesis?.status === "succeeded" &&
5340
+ deepScanSynthesis.reducer_attempt_id
5341
+ ? deepScanSynthesis.reducer_attempt_id
5342
+ : (succeeded[0]?.attemptId ?? "a01");
5632
5343
  store.writeYaml(join(paths.finalDir, "work_product.yaml"), {
5633
5344
  id: newId("wp"),
5634
5345
  kind: "report",
5635
5346
  source_task_id: taskId,
5636
- producer_attempt_id: succeeded[0]?.attemptId ?? "a01",
5347
+ producer_attempt_id: reportProducerAttemptId,
5637
5348
  files: Object.fromEntries([[opts.artifactName, join(paths.finalDir, opts.artifactName)]]),
5638
5349
  meta: {
5639
5350
  harnesses: attempts.map((a) => a.harnessId),
@@ -5642,7 +5353,7 @@ export class Orchestrator {
5642
5353
  read_only: true,
5643
5354
  },
5644
5355
  });
5645
- log.emit("work_product.emitted", { kind: "report", winner: succeeded[0]?.attemptId ?? null });
5356
+ log.emit("work_product.emitted", { kind: "report", winner: reportProducerAttemptId });
5646
5357
  const workVetoed = terminalFacts.work_state?.state === "needs_input" ||
5647
5358
  terminalFacts.work_state?.state === "incomplete";
5648
5359
  if (terminalFacts.lifecycle !== "succeeded") {
@@ -5694,9 +5405,4 @@ export class Orchestrator {
5694
5405
  };
5695
5406
  }
5696
5407
  }
5697
- function assertNoSecretLikeTokens(label, text) {
5698
- if (containsSecretLikeToken(text)) {
5699
- throw new Error(`${label} contains secret-like token; refusing to persist artifact`);
5700
- }
5701
- }
5702
5408
  //# sourceMappingURL=orchestrator.js.map